Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ERC2981 (Royalty Info) for Cairo #413

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d4f6afb
Add functionality for declaring constants within traits
immrsd Oct 1, 2024
42bca98
Specify return types for printing functions, support constants for tr…
immrsd Oct 1, 2024
6af508f
Merge master
immrsd Nov 21, 2024
5df51a2
Support uint Cairo types
immrsd Nov 27, 2024
5e3cf95
Add support for ERC2981 extension
immrsd Nov 27, 2024
168c862
Add RoyaltyInfo for ERC1155
immrsd Nov 27, 2024
3d72bd9
Add RoyaltyInfo for ERC721
immrsd Nov 27, 2024
6155fea
Introduce RoyaltyInfoSection with UI controls
immrsd Nov 27, 2024
6b37bd9
Enable errors proxying to ERC1155 controls
immrsd Nov 27, 2024
0f52a2c
Add RoyaltyInfo section to ERC1155 controls
immrsd Nov 27, 2024
6fee79c
Add RoyaltyInfo section to ERC721 controls
immrsd Nov 27, 2024
b02d19d
Update changelog
immrsd Nov 27, 2024
9219dfd
Fix unnecessary brackets for Cairo constants import
immrsd Nov 27, 2024
83d8017
Make Access control required for RoyaltyInfo
immrsd Nov 27, 2024
8b4037c
Update snapshot for Cairo custom contract tests
immrsd Nov 27, 2024
f424d78
Apply minor fixes
immrsd Nov 27, 2024
310b23f
Add missing semicolons
immrsd Nov 29, 2024
d1b80ce
Rename setRoyaltyInfoIfNeeded to setRoyaltyInfo
immrsd Nov 29, 2024
d5b9830
Sanitize ERC20 premint value
immrsd Nov 29, 2024
30b858d
Add explicit function return types
immrsd Nov 29, 2024
81a51f2
Introduce DEFAULT_ACCESS_CONTROL constant
immrsd Nov 29, 2024
81fc2eb
Remove number check for negative
immrsd Nov 29, 2024
1609800
Fix broken link
immrsd Nov 29, 2024
826ba89
Add multiple Royalty info options for generate-based-testing
immrsd Nov 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core-cairo/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.19.0 (2024-11-27)

- Add ERC2981 (RoyaltyInfo) for ERC721 and ERC1155 ([#413](https://github.com/OpenZeppelin/contracts-wizard/pull/413))

## 0.18.0 (2024-11-15)

- **Breaking changes**:
Expand Down
25 changes: 24 additions & 1 deletion packages/core-cairo/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface Contract {
superVariables: Variable[];
}

export type Value = string | number | { lit: string } | { note: string, value: Value };
export type Value = string | number | bigint | { lit: string } | { note: string, value: Value };

export interface Component {
name: string;
Expand Down Expand Up @@ -57,6 +57,7 @@ export interface BaseImplementedTrait {
}

export interface ImplementedTrait extends BaseImplementedTrait {
superVariables: Variable[];
functions: ContractFunction[];
}

Expand Down Expand Up @@ -172,6 +173,7 @@ export class ContractBuilder implements Contract {
name: baseTrait.name,
of: baseTrait.of,
tags: [ ...baseTrait.tags ],
superVariables: [],
functions: [],
priority: baseTrait.priority,
};
Expand All @@ -180,6 +182,27 @@ export class ContractBuilder implements Contract {
}
}

addSuperVariableToTrait(baseTrait: BaseImplementedTrait, newVar: Variable): boolean {
const trait = this.addImplementedTrait(baseTrait);
for (const existingVar of trait.superVariables) {
if (existingVar.name === newVar.name) {
if (existingVar.type !== newVar.type) {
throw new Error(
`Tried to add duplicate super var ${newVar.name} with different type: ${newVar.type} instead of ${existingVar.type}.`
);
}
if (existingVar.value !== newVar.value) {
throw new Error(
`Tried to add duplicate super var ${newVar.name} with different value: ${newVar.value} instead of ${existingVar.value}.`
);
}
return false; // No need to add, already exists
}
}
trait.superVariables.push(newVar);
return true;
}

addFunction(baseTrait: BaseImplementedTrait, fn: BaseFunction): ContractFunction {
const t = this.addImplementedTrait(baseTrait);

Expand Down
2 changes: 1 addition & 1 deletion packages/core-cairo/src/custom.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ Generated by [AVA](https://avajs.dev).
use openzeppelin::upgrades::interface::IUpgradeable;␊
use starknet::ClassHash;␊
use starknet::ContractAddress;␊
use super::{UPGRADER_ROLE};␊
use super::UPGRADER_ROLE;␊
component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent);␊
component!(path: SRC5Component, storage: src5, event: SRC5Event);␊
Expand Down
Binary file modified packages/core-cairo/src/custom.test.ts.snap
Binary file not shown.
11 changes: 8 additions & 3 deletions packages/core-cairo/src/erc1155.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { printContract } from './print';
import { addSRC5Component } from './common-components';
import { externalTrait } from './external-trait';
import { toByteArray } from './utils/convert-strings';
import { RoyaltyInfoOptions, setRoyaltyInfo, defaults as royaltyInfoDefaults } from './set-royalty-info';

export const defaults: Required<ERC1155Options> = {
name: 'MyToken',
Expand All @@ -19,6 +20,7 @@ export const defaults: Required<ERC1155Options> = {
pausable: false,
mintable: false,
updatableUri: true,
royaltyInfo: royaltyInfoDefaults,
access: commonDefaults.access,
upgradeable: commonDefaults.upgradeable,
info: commonDefaults.info
Expand All @@ -35,6 +37,7 @@ export interface ERC1155Options extends CommonContractOptions {
pausable?: boolean;
mintable?: boolean;
updatableUri?: boolean;
royaltyInfo?: RoyaltyInfoOptions;
}

function withDefaults(opts: ERC1155Options): Required<ERC1155Options> {
Expand All @@ -45,11 +48,12 @@ function withDefaults(opts: ERC1155Options): Required<ERC1155Options> {
pausable: opts.pausable ?? defaults.pausable,
mintable: opts.mintable ?? defaults.mintable,
updatableUri: opts.updatableUri ?? defaults.updatableUri,
royaltyInfo: opts.royaltyInfo ?? defaults.royaltyInfo,
};
}

export function isAccessControlRequired(opts: Partial<ERC1155Options>): boolean {
return opts.mintable === true || opts.pausable === true || opts.updatableUri !== false || opts.upgradeable === true;
return opts.mintable === true || opts.pausable === true || opts.updatableUri !== false || opts.upgradeable === true || opts.royaltyInfo?.enabled === true;
}

export function buildERC1155(opts: ERC1155Options): Contract {
Expand All @@ -76,11 +80,12 @@ export function buildERC1155(opts: ERC1155Options): Contract {
addSetBaseUri(c, allOpts.access);
}

addHooks(c, allOpts);

setAccessControl(c, allOpts.access);
setUpgradeable(c, allOpts.upgradeable, allOpts.access);
setInfo(c, allOpts.info);
setRoyaltyInfo(c, allOpts.royaltyInfo, allOpts.access);

addHooks(c, allOpts);

return c;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/core-cairo/src/erc20.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseImplementedTrait, Contract, ContractBuilder } from './contract';
import { Contract, ContractBuilder } from './contract';
import { Access, requireAccessControl, setAccessControl } from './set-access-control';
import { addPausable } from './add-pausable';
import { defineFunctions } from './utils/define-functions';
Expand All @@ -10,7 +10,7 @@ import { defineComponents } from './utils/define-components';
import { contractDefaults as commonDefaults } from './common-options';
import { printContract } from './print';
import { externalTrait } from './external-trait';
import { toByteArray, toFelt252 } from './utils/convert-strings';
import { toByteArray, toFelt252, toUint } from './utils/convert-strings';
import { addVotesComponent } from './common-components';


Expand Down Expand Up @@ -193,7 +193,7 @@ function addPremint(c: ContractBuilder, amount: string) {
});
}

const premintAbsolute = getInitialSupply(amount, 18);
const premintAbsolute = toUint(getInitialSupply(amount, 18), 'premint', 'u256');

c.addStandaloneImport('starknet::ContractAddress');
c.addConstructorArgument({ name:'recipient', type:'ContractAddress' });
Expand Down
8 changes: 7 additions & 1 deletion packages/core-cairo/src/erc721.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { addSRC5Component, addVotesComponent } from './common-components';
import { externalTrait } from './external-trait';
import { toByteArray, toFelt252 } from './utils/convert-strings';
import { OptionsError } from './error';
import { RoyaltyInfoOptions, setRoyaltyInfo, defaults as royaltyInfoDefaults } from './set-royalty-info';

export const defaults: Required<ERC721Options> = {
name: 'MyToken',
Expand All @@ -22,6 +23,7 @@ export const defaults: Required<ERC721Options> = {
mintable: false,
enumerable: false,
votes: false,
royaltyInfo: royaltyInfoDefaults,
appName: '', // Defaults to empty string, but user must provide a non-empty value if votes are enabled
appVersion: 'v1',
access: commonDefaults.access,
Expand All @@ -42,6 +44,7 @@ export interface ERC721Options extends CommonContractOptions {
mintable?: boolean;
enumerable?: boolean;
votes?: boolean;
royaltyInfo?: RoyaltyInfoOptions;
appName?: string;
appVersion?: string;
}
Expand All @@ -55,14 +58,15 @@ function withDefaults(opts: ERC721Options): Required<ERC721Options> {
pausable: opts.pausable ?? defaults.pausable,
mintable: opts.mintable ?? defaults.mintable,
enumerable: opts.enumerable ?? defaults.enumerable,
royaltyInfo: opts.royaltyInfo ?? defaults.royaltyInfo,
votes: opts.votes ?? defaults.votes,
appName: opts.appName ?? defaults.appName,
appVersion: opts.appVersion ?? defaults.appVersion
};
}

export function isAccessControlRequired(opts: Partial<ERC721Options>): boolean {
return opts.mintable === true || opts.pausable === true || opts.upgradeable === true;
return opts.mintable === true || opts.pausable === true || opts.upgradeable === true || opts.royaltyInfo?.enabled === true;
}

export function buildERC721(opts: ERC721Options): Contract {
Expand Down Expand Up @@ -92,6 +96,8 @@ export function buildERC721(opts: ERC721Options): Contract {
setAccessControl(c, allOpts.access);
setUpgradeable(c, allOpts.upgradeable, allOpts.access);
setInfo(c, allOpts.info);
setRoyaltyInfo(c, allOpts.royaltyInfo, allOpts.access);

addHooks(c, allOpts);

return c;
Expand Down
4 changes: 3 additions & 1 deletion packages/core-cairo/src/generate/erc1155.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ERC1155Options } from '../erc1155';
import { accessOptions } from '../set-access-control';
import { infoOptions } from '../set-info';
import { upgradeableOptions } from '../set-upgradeable';
import { testRoyaltyInfoOptions } from '../set-royalty-info';
import { generateAlternatives } from './alternatives';

const booleans = [true, false];
Expand All @@ -13,8 +14,9 @@ const blueprint = {
pausable: booleans,
mintable: booleans,
updatableUri: booleans,
access: accessOptions,
upgradeable: upgradeableOptions,
royaltyInfo: testRoyaltyInfoOptions,
access: accessOptions,
info: infoOptions,
};

Expand Down
2 changes: 2 additions & 0 deletions packages/core-cairo/src/generate/erc721.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ERC721Options } from '../erc721';
import { accessOptions } from '../set-access-control';
import { infoOptions } from '../set-info';
import { upgradeableOptions } from '../set-upgradeable';
import { testRoyaltyInfoOptions } from '../set-royalty-info';
import { generateAlternatives } from './alternatives';

const booleans = [true, false];
Expand All @@ -17,6 +18,7 @@ const blueprint = {
appVersion: ['v1'],
pausable: booleans,
mintable: booleans,
royaltyInfo: testRoyaltyInfoOptions,
access: accessOptions,
upgradeable: upgradeableOptions,
info: infoOptions,
Expand Down
2 changes: 2 additions & 0 deletions packages/core-cairo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ export type { Access } from './set-access-control';
export type { Account } from './account';
export type { Upgradeable } from './set-upgradeable';
export type { Info } from './set-info';
export type { RoyaltyInfoOptions } from './set-royalty-info';

export { premintPattern } from './erc20';

export { defaults as infoDefaults } from './set-info';
export { defaults as royaltyInfoDefaults } from './set-royalty-info';

export type { OptionsErrorMessages } from './error';
export { OptionsError } from './error';
Expand Down
47 changes: 33 additions & 14 deletions packages/core-cairo/src/print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,34 +37,37 @@ function withSemicolons(lines: string[]): string[] {
return lines.map(line => line.endsWith(';') ? line : line + ';');
}

function printSuperVariables(contract: Contract) {
function printSuperVariables(contract: Contract): string[] {
return withSemicolons(contract.superVariables.map(v => `const ${v.name}: ${v.type} = ${v.value}`));
}

function printImports(contract: Contract) {
function printImports(contract: Contract): string[] {
const lines: string[] = [];
sortImports(contract).forEach(i => lines.push(`use ${i}`));
return withSemicolons(lines);
}

function sortImports(contract: Contract) {
function sortImports(contract: Contract): string[] {
const componentImports = contract.components.flatMap(c => `${c.path}::${c.name}`);
const allImports = componentImports.concat(contract.standaloneImports);
if (contract.superVariables.length > 0) {
allImports.push(`super::{${contract.superVariables.map(v => v.name).join(', ')}}`);
const superVars = contract.superVariables;
if (superVars.length === 1) {
allImports.push(`super::${superVars[0]!.name}`);
} else if (superVars.length > 1) {
allImports.push(`super::{${superVars.map(v => v.name).join(', ')}}`);
}
return allImports.sort();
}

function printComponentDeclarations(contract: Contract) {
function printComponentDeclarations(contract: Contract): string[] {
const lines = [];
for (const component of contract.components) {
lines.push(`component!(path: ${component.name}, storage: ${component.substorage.name}, event: ${component.event.name});`);
}
return lines;
}

function printImpls(contract: Contract) {
function printImpls(contract: Contract): Lines[] {
const externalImpls = contract.components.flatMap(c => c.impls);
const internalImpls = contract.components.flatMap(c => c.internalImpl ? [c.internalImpl] : []);

Expand All @@ -74,7 +77,7 @@ function printImpls(contract: Contract) {
);
}

function printImpl(impl: Impl, internal = false) {
function printImpl(impl: Impl, internal = false): string[] {
const lines = [];
if (!internal) {
lines.push('#[abi(embed_v0)]');
Expand All @@ -83,7 +86,7 @@ function printImpl(impl: Impl, internal = false) {
return lines;
}

function printStorage(contract: Contract) {
function printStorage(contract: Contract): (string | string[])[] {
const lines = [];
// storage is required regardless of whether there are components
lines.push('#[storage]');
Expand All @@ -98,7 +101,7 @@ function printStorage(contract: Contract) {
return lines;
}

function printEvents(contract: Contract) {
function printEvents(contract: Contract): (string | string[])[] {
const lines = [];
if (contract.components.length > 0) {
lines.push('#[event]');
Expand All @@ -115,7 +118,7 @@ function printEvents(contract: Contract) {
return lines;
}

function printImplementedTraits(contract: Contract) {
function printImplementedTraits(contract: Contract): Lines[] {
const impls = [];

// sort first by priority, then number of tags, then name
Expand All @@ -133,15 +136,22 @@ function printImplementedTraits(contract: Contract) {
const implLines = [];
implLines.push(...trait.tags.map(t => `#[${t}]`));
implLines.push(`impl ${trait.name} of ${trait.of} {`);

const superVars = withSemicolons(
trait.superVariables.map(v => `const ${v.name}: ${v.type} = ${v.value}`)
);
implLines.push(superVars);

const fns = trait.functions.map(fn => printFunction(fn));
implLines.push(spaceBetween(...fns));

implLines.push('}');
impls.push(implLines);
}
return spaceBetween(...impls);
}

function printFunction(fn: ContractFunction) {
function printFunction(fn: ContractFunction): Lines[] {
const head = `fn ${fn.name}`;
const args = fn.args.map(a => printArgument(a));

Expand Down Expand Up @@ -191,7 +201,7 @@ function printConstructor(contract: Contract): Lines[] {
}
}

function hasInitializer(parent: Component) {
function hasInitializer(parent: Component): boolean {
return parent.initializer !== undefined && parent.substorage?.name !== undefined;
}

Expand Down Expand Up @@ -220,14 +230,23 @@ export function printValue(value: Value): string {
} else {
throw new Error(`Number not representable (${value})`);
}
} else if (typeof value === 'bigint') {
return `${value}`
} else {
return `"${value}"`;
}
}

// generic for functions and constructors
// kindedName = 'fn foo'
function printFunction2(kindedName: string, args: string[], tag: string | undefined, returns: string | undefined, returnLine: string | undefined, code: Lines[]): Lines[] {
function printFunction2(
kindedName: string,
args: string[],
tag: string | undefined,
returns: string | undefined,
returnLine: string | undefined,
code: Lines[]
): Lines[] {
const fn = [];

if (tag !== undefined) {
Expand Down
Loading