From 9280664149d6afb402f05882858ef919fbd8b803 Mon Sep 17 00:00:00 2001 From: immrsd <103599616+immrsd@users.noreply.github.com> Date: Thu, 26 Sep 2024 07:25:38 +0200 Subject: [PATCH] Add ERC721Enumerable (#391) --- packages/core-cairo/CHANGELOG.md | 4 + packages/core-cairo/src/contract.ts | 28 +- packages/core-cairo/src/erc721.test.ts | 37 ++- packages/core-cairo/src/erc721.test.ts.md | 266 ++++++++++++++++-- packages/core-cairo/src/erc721.test.ts.snap | Bin 2022 -> 2270 bytes packages/core-cairo/src/erc721.ts | 107 ++++--- packages/core-cairo/src/generate/erc721.ts | 1 + packages/core-cairo/src/set-access-control.ts | 2 +- packages/ui/src/cairo/ERC721Controls.svelte | 7 + 9 files changed, 358 insertions(+), 94 deletions(-) diff --git a/packages/core-cairo/CHANGELOG.md b/packages/core-cairo/CHANGELOG.md index 057ab9cc..27c62dbe 100644 --- a/packages/core-cairo/CHANGELOG.md +++ b/packages/core-cairo/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Add ERC721Enumerable. ([#391](https://github.com/OpenZeppelin/contracts-wizard/pull/391)) + ## 0.15.0 (2024-09-19) - Add Account and EthAccount. ([#387](https://github.com/OpenZeppelin/contracts-wizard/pull/387)) diff --git a/packages/core-cairo/src/contract.ts b/packages/core-cairo/src/contract.ts index 81833f74..fa635d00 100644 --- a/packages/core-cairo/src/contract.ts +++ b/packages/core-cairo/src/contract.ts @@ -86,15 +86,15 @@ export interface Argument { export class ContractBuilder implements Contract { readonly name: string; readonly account: boolean; - license: string = 'MIT'; + license = 'MIT'; upgradeable = false; readonly constructorArgs: Argument[] = []; readonly constructorCode: string[] = []; - private componentsMap: Map = new Map(); - private implementedTraitsMap: Map = new Map(); - private superVariablesMap: Map = new Map(); + private componentsMap: Map = new Map(); + private implementedTraitsMap: Map = new Map(); + private superVariablesMap: Map = new Map(); private standaloneImportsSet: Set = new Set(); private interfaceFlagsSet: Set = new Set(); @@ -126,7 +126,7 @@ export class ContractBuilder implements Contract { return this.interfaceFlagsSet; } - addStandaloneImport(fullyQualified: string) { + addStandaloneImport(fullyQualified: string): void { this.standaloneImportsSet.add(fullyQualified); } @@ -141,7 +141,7 @@ export class ContractBuilder implements Contract { return !present; } - addImplToComponent(component: Component, impl: Impl) { + addImplToComponent(component: Component, impl: Impl): void { this.addComponent(component); let c = this.componentsMap.get(component.name); if (c == undefined) { @@ -153,7 +153,7 @@ export class ContractBuilder implements Contract { } } - addSuperVariable(variable: Variable) { + addSuperVariable(variable: Variable): boolean { if (this.superVariablesMap.has(variable.name)) { return false; } else { @@ -162,7 +162,7 @@ export class ContractBuilder implements Contract { } } - addImplementedTrait(baseTrait: BaseImplementedTrait) { + addImplementedTrait(baseTrait: BaseImplementedTrait): ImplementedTrait { const key = baseTrait.name; const existingTrait = this.implementedTraitsMap.get(key); if (existingTrait !== undefined) { @@ -180,7 +180,7 @@ export class ContractBuilder implements Contract { } } - addFunction(baseTrait: BaseImplementedTrait, fn: BaseFunction) { + addFunction(baseTrait: BaseImplementedTrait, fn: BaseFunction): ContractFunction { const t = this.addImplementedTrait(baseTrait); const signature = this.getFunctionSignature(fn); @@ -203,17 +203,17 @@ export class ContractBuilder implements Contract { return contractFn; } - private getFunctionSignature(fn: BaseFunction) { + private getFunctionSignature(fn: BaseFunction): string { return [fn.name, '(', ...fn.args.map(a => a.name), ')'].join(''); } - addFunctionCodeBefore(baseTrait: BaseImplementedTrait, fn: BaseFunction, codeBefore: string) { + addFunctionCodeBefore(baseTrait: BaseImplementedTrait, fn: BaseFunction, codeBefore: string): void { this.addImplementedTrait(baseTrait); const existingFn = this.addFunction(baseTrait, fn); existingFn.codeBefore = [ ...existingFn.codeBefore ?? [], codeBefore ]; } - addConstructorArgument(arg: Argument) { + addConstructorArgument(arg: Argument): void { for (const existingArg of this.constructorArgs) { if (existingArg.name == arg.name) { return; @@ -222,11 +222,11 @@ export class ContractBuilder implements Contract { this.constructorArgs.push(arg); } - addConstructorCode(code: string) { + addConstructorCode(code: string): void { this.constructorCode.push(code); } - addInterfaceFlag(flag: string) { + addInterfaceFlag(flag: string): void { this.interfaceFlagsSet.add(flag); } } diff --git a/packages/core-cairo/src/erc721.test.ts b/packages/core-cairo/src/erc721.test.ts index a7ba1146..36e05cc6 100644 --- a/packages/core-cairo/src/erc721.test.ts +++ b/packages/core-cairo/src/erc721.test.ts @@ -5,6 +5,14 @@ import { printContract } from './print'; import { erc721 } from '.'; +const allFeaturesON: Partial = { + mintable: true, + burnable: true, + pausable: true, + enumerable: true, + upgradeable: true +} as const; + function testERC721(title: string, opts: Partial) { test(title, t => { const c = buildERC721({ @@ -51,36 +59,35 @@ testERC721('mintable', { mintable: true, }); +testERC721('enumerable', { + enumerable: true, +}); + +testERC721('pausable + enumerable', { + pausable: true, + enumerable: true, +}); + testERC721('mintable + roles', { mintable: true, access: 'roles', }); testERC721('full non-upgradeable', { - mintable: true, - pausable: true, - burnable: true, + ...allFeaturesON, upgradeable: false, }); -testERC721('full upgradeable', { - mintable: true, - pausable: true, - burnable: true, - upgradeable: true, -}); +testERC721('full upgradeable', allFeaturesON); testAPIEquivalence('API default'); testAPIEquivalence('API basic', { name: 'CustomToken', symbol: 'CTK' }); testAPIEquivalence('API full upgradeable', { + ...allFeaturesON, name: 'CustomToken', - symbol: 'CTK', - burnable: true, - mintable: true, - pausable: true, - upgradeable: true, + symbol: 'CTK' }); test('API assert defaults', async t => { @@ -91,4 +98,6 @@ test('API isAccessControlRequired', async t => { t.is(erc721.isAccessControlRequired({ mintable: true }), true); t.is(erc721.isAccessControlRequired({ pausable: true }), true); t.is(erc721.isAccessControlRequired({ upgradeable: true }), true); + t.is(erc721.isAccessControlRequired({ burnable: true }), false); + t.is(erc721.isAccessControlRequired({ enumerable: true }), false); }); \ No newline at end of file diff --git a/packages/core-cairo/src/erc721.test.ts.md b/packages/core-cairo/src/erc721.test.ts.md index e93e218e..3c384636 100644 --- a/packages/core-cairo/src/erc721.test.ts.md +++ b/packages/core-cairo/src/erc721.test.ts.md @@ -357,17 +357,9 @@ Generated by [AVA](https://avajs.dev). token_id: u256,␊ auth: ContractAddress,␊ ) {␊ - let contract_state = ERC721Component::HasComponent::get_contract(@self);␊ + let contract_state = self.get_contract();␊ contract_state.pausable.assert_not_paused();␊ }␊ - ␊ - fn after_update(␊ - ref self: ERC721Component::ComponentState,␊ - to: ContractAddress,␊ - token_id: u256,␊ - auth: ContractAddress,␊ - ) {␊ - }␊ }␊ ␊ #[abi(embed_v0)]␊ @@ -494,6 +486,220 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## enumerable + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^0.16.0␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin::access::ownable::OwnableComponent;␊ + use openzeppelin::introspection::src5::SRC5Component;␊ + use openzeppelin::token::erc721::ERC721Component;␊ + use openzeppelin::token::erc721::extensions::ERC721EnumerableComponent;␊ + use openzeppelin::upgrades::UpgradeableComponent;␊ + use openzeppelin::upgrades::interface::IUpgradeable;␊ + use starknet::ClassHash;␊ + use starknet::ContractAddress;␊ + ␊ + component!(path: ERC721Component, storage: erc721, event: ERC721Event);␊ + component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ + component!(path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + ␊ + #[abi(embed_v0)]␊ + impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC721EnumerableImpl = ERC721EnumerableComponent::ERC721EnumerableImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + ␊ + impl ERC721InternalImpl = ERC721Component::InternalImpl;␊ + impl ERC721EnumerableInternalImpl = ERC721EnumerableComponent::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc721: ERC721Component::Storage,␊ + #[substorage(v0)]␊ + src5: SRC5Component::Storage,␊ + #[substorage(v0)]␊ + erc721_enumerable: ERC721EnumerableComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC721Event: ERC721Component::Event,␊ + #[flat]␊ + SRC5Event: SRC5Component::Event,␊ + #[flat]␊ + ERC721EnumerableEvent: ERC721EnumerableComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc721.initializer("MyToken", "MTK", "");␊ + self.erc721_enumerable.initializer();␊ + self.ownable.initializer(owner);␊ + }␊ + ␊ + impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait {␊ + fn before_update(␊ + ref self: ERC721Component::ComponentState,␊ + to: ContractAddress,␊ + token_id: u256,␊ + auth: ContractAddress,␊ + ) {␊ + let mut contract_state = self.get_contract_mut();␊ + contract_state.erc721_enumerable.before_update(to, token_id);␊ + }␊ + }␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + }␊ + ` + +## pausable + enumerable + +> Snapshot 1 + + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts for Cairo ^0.16.0␊ + ␊ + #[starknet::contract]␊ + mod MyToken {␊ + use openzeppelin::access::ownable::OwnableComponent;␊ + use openzeppelin::introspection::src5::SRC5Component;␊ + use openzeppelin::security::pausable::PausableComponent;␊ + use openzeppelin::token::erc721::ERC721Component;␊ + use openzeppelin::token::erc721::extensions::ERC721EnumerableComponent;␊ + use openzeppelin::upgrades::UpgradeableComponent;␊ + use openzeppelin::upgrades::interface::IUpgradeable;␊ + use starknet::ClassHash;␊ + use starknet::ContractAddress;␊ + ␊ + component!(path: ERC721Component, storage: erc721, event: ERC721Event);␊ + component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ + component!(path: PausableComponent, storage: pausable, event: PausableEvent);␊ + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + component!(path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent);␊ + component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ + ␊ + #[abi(embed_v0)]␊ + impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl;␊ + #[abi(embed_v0)]␊ + impl PausableImpl = PausableComponent::PausableImpl;␊ + #[abi(embed_v0)]␊ + impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC721EnumerableImpl = ERC721EnumerableComponent::ERC721EnumerableImpl;␊ + ␊ + impl ERC721InternalImpl = ERC721Component::InternalImpl;␊ + impl PausableInternalImpl = PausableComponent::InternalImpl;␊ + impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + impl ERC721EnumerableInternalImpl = ERC721EnumerableComponent::InternalImpl;␊ + impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ + ␊ + #[storage]␊ + struct Storage {␊ + #[substorage(v0)]␊ + erc721: ERC721Component::Storage,␊ + #[substorage(v0)]␊ + src5: SRC5Component::Storage,␊ + #[substorage(v0)]␊ + pausable: PausableComponent::Storage,␊ + #[substorage(v0)]␊ + ownable: OwnableComponent::Storage,␊ + #[substorage(v0)]␊ + erc721_enumerable: ERC721EnumerableComponent::Storage,␊ + #[substorage(v0)]␊ + upgradeable: UpgradeableComponent::Storage,␊ + }␊ + ␊ + #[event]␊ + #[derive(Drop, starknet::Event)]␊ + enum Event {␊ + #[flat]␊ + ERC721Event: ERC721Component::Event,␊ + #[flat]␊ + SRC5Event: SRC5Component::Event,␊ + #[flat]␊ + PausableEvent: PausableComponent::Event,␊ + #[flat]␊ + OwnableEvent: OwnableComponent::Event,␊ + #[flat]␊ + ERC721EnumerableEvent: ERC721EnumerableComponent::Event,␊ + #[flat]␊ + UpgradeableEvent: UpgradeableComponent::Event,␊ + }␊ + ␊ + #[constructor]␊ + fn constructor(ref self: ContractState, owner: ContractAddress) {␊ + self.erc721.initializer("MyToken", "MTK", "");␊ + self.ownable.initializer(owner);␊ + self.erc721_enumerable.initializer();␊ + }␊ + ␊ + impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait {␊ + fn before_update(␊ + ref self: ERC721Component::ComponentState,␊ + to: ContractAddress,␊ + token_id: u256,␊ + auth: ContractAddress,␊ + ) {␊ + let mut contract_state = self.get_contract_mut();␊ + contract_state.pausable.assert_not_paused();␊ + contract_state.erc721_enumerable.before_update(to, token_id);␊ + }␊ + }␊ + ␊ + #[abi(embed_v0)]␊ + impl UpgradeableImpl of IUpgradeable {␊ + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) {␊ + self.ownable.assert_only_owner();␊ + self.upgradeable.upgrade(new_class_hash);␊ + }␊ + }␊ + ␊ + #[generate_trait]␊ + #[abi(per_item)]␊ + impl ExternalImpl of ExternalTrait {␊ + #[external(v0)]␊ + fn pause(ref self: ContractState) {␊ + self.ownable.assert_only_owner();␊ + self.pausable.pause();␊ + }␊ + ␊ + #[external(v0)]␊ + fn unpause(ref self: ContractState) {␊ + self.ownable.assert_only_owner();␊ + self.pausable.unpause();␊ + }␊ + }␊ + }␊ + ` + ## mintable + roles > Snapshot 1 @@ -622,6 +828,7 @@ Generated by [AVA](https://avajs.dev). use openzeppelin::introspection::src5::SRC5Component;␊ use openzeppelin::security::pausable::PausableComponent;␊ use openzeppelin::token::erc721::ERC721Component;␊ + use openzeppelin::token::erc721::extensions::ERC721EnumerableComponent;␊ use starknet::ContractAddress;␊ use starknet::get_caller_address;␊ ␊ @@ -629,6 +836,7 @@ Generated by [AVA](https://avajs.dev). component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ component!(path: PausableComponent, storage: pausable, event: PausableEvent);␊ component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + component!(path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent);␊ ␊ #[abi(embed_v0)]␊ impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl;␊ @@ -636,10 +844,13 @@ Generated by [AVA](https://avajs.dev). impl PausableImpl = PausableComponent::PausableImpl;␊ #[abi(embed_v0)]␊ impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC721EnumerableImpl = ERC721EnumerableComponent::ERC721EnumerableImpl;␊ ␊ impl ERC721InternalImpl = ERC721Component::InternalImpl;␊ impl PausableInternalImpl = PausableComponent::InternalImpl;␊ impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + impl ERC721EnumerableInternalImpl = ERC721EnumerableComponent::InternalImpl;␊ ␊ #[storage]␊ struct Storage {␊ @@ -651,6 +862,8 @@ Generated by [AVA](https://avajs.dev). pausable: PausableComponent::Storage,␊ #[substorage(v0)]␊ ownable: OwnableComponent::Storage,␊ + #[substorage(v0)]␊ + erc721_enumerable: ERC721EnumerableComponent::Storage,␊ }␊ ␊ #[event]␊ @@ -664,12 +877,15 @@ Generated by [AVA](https://avajs.dev). PausableEvent: PausableComponent::Event,␊ #[flat]␊ OwnableEvent: OwnableComponent::Event,␊ + #[flat]␊ + ERC721EnumerableEvent: ERC721EnumerableComponent::Event,␊ }␊ ␊ #[constructor]␊ fn constructor(ref self: ContractState, owner: ContractAddress) {␊ self.erc721.initializer("MyToken", "MTK", "");␊ self.ownable.initializer(owner);␊ + self.erc721_enumerable.initializer();␊ }␊ ␊ impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait {␊ @@ -679,16 +895,9 @@ Generated by [AVA](https://avajs.dev). token_id: u256,␊ auth: ContractAddress,␊ ) {␊ - let contract_state = ERC721Component::HasComponent::get_contract(@self);␊ + let mut contract_state = self.get_contract_mut();␊ contract_state.pausable.assert_not_paused();␊ - }␊ - ␊ - fn after_update(␊ - ref self: ERC721Component::ComponentState,␊ - to: ContractAddress,␊ - token_id: u256,␊ - auth: ContractAddress,␊ - ) {␊ + contract_state.erc721_enumerable.before_update(to, token_id);␊ }␊ }␊ ␊ @@ -750,6 +959,7 @@ Generated by [AVA](https://avajs.dev). use openzeppelin::introspection::src5::SRC5Component;␊ use openzeppelin::security::pausable::PausableComponent;␊ use openzeppelin::token::erc721::ERC721Component;␊ + use openzeppelin::token::erc721::extensions::ERC721EnumerableComponent;␊ use openzeppelin::upgrades::UpgradeableComponent;␊ use openzeppelin::upgrades::interface::IUpgradeable;␊ use starknet::ClassHash;␊ @@ -760,6 +970,7 @@ Generated by [AVA](https://avajs.dev). component!(path: SRC5Component, storage: src5, event: SRC5Event);␊ component!(path: PausableComponent, storage: pausable, event: PausableEvent);␊ component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);␊ + component!(path: ERC721EnumerableComponent, storage: erc721_enumerable, event: ERC721EnumerableEvent);␊ component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent);␊ ␊ #[abi(embed_v0)]␊ @@ -768,10 +979,13 @@ Generated by [AVA](https://avajs.dev). impl PausableImpl = PausableComponent::PausableImpl;␊ #[abi(embed_v0)]␊ impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl;␊ + #[abi(embed_v0)]␊ + impl ERC721EnumerableImpl = ERC721EnumerableComponent::ERC721EnumerableImpl;␊ ␊ impl ERC721InternalImpl = ERC721Component::InternalImpl;␊ impl PausableInternalImpl = PausableComponent::InternalImpl;␊ impl OwnableInternalImpl = OwnableComponent::InternalImpl;␊ + impl ERC721EnumerableInternalImpl = ERC721EnumerableComponent::InternalImpl;␊ impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl;␊ ␊ #[storage]␊ @@ -785,6 +999,8 @@ Generated by [AVA](https://avajs.dev). #[substorage(v0)]␊ ownable: OwnableComponent::Storage,␊ #[substorage(v0)]␊ + erc721_enumerable: ERC721EnumerableComponent::Storage,␊ + #[substorage(v0)]␊ upgradeable: UpgradeableComponent::Storage,␊ }␊ ␊ @@ -800,6 +1016,8 @@ Generated by [AVA](https://avajs.dev). #[flat]␊ OwnableEvent: OwnableComponent::Event,␊ #[flat]␊ + ERC721EnumerableEvent: ERC721EnumerableComponent::Event,␊ + #[flat]␊ UpgradeableEvent: UpgradeableComponent::Event,␊ }␊ ␊ @@ -807,6 +1025,7 @@ Generated by [AVA](https://avajs.dev). fn constructor(ref self: ContractState, owner: ContractAddress) {␊ self.erc721.initializer("MyToken", "MTK", "");␊ self.ownable.initializer(owner);␊ + self.erc721_enumerable.initializer();␊ }␊ ␊ impl ERC721HooksImpl of ERC721Component::ERC721HooksTrait {␊ @@ -816,16 +1035,9 @@ Generated by [AVA](https://avajs.dev). token_id: u256,␊ auth: ContractAddress,␊ ) {␊ - let contract_state = ERC721Component::HasComponent::get_contract(@self);␊ + let mut contract_state = self.get_contract_mut();␊ contract_state.pausable.assert_not_paused();␊ - }␊ - ␊ - fn after_update(␊ - ref self: ERC721Component::ComponentState,␊ - to: ContractAddress,␊ - token_id: u256,␊ - auth: ContractAddress,␊ - ) {␊ + contract_state.erc721_enumerable.before_update(to, token_id);␊ }␊ }␊ ␊ diff --git a/packages/core-cairo/src/erc721.test.ts.snap b/packages/core-cairo/src/erc721.test.ts.snap index 4deaf77f9001d7f148ef957dde2b2ca4923fcefb..bac3f22ed65d8fd588e38a5946fa66cc1c50e20d 100644 GIT binary patch literal 2270 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 E04Uqu=l}o! diff --git a/packages/core-cairo/src/erc721.ts b/packages/core-cairo/src/erc721.ts index 2b430277..eb661d8d 100644 --- a/packages/core-cairo/src/erc721.ts +++ b/packages/core-cairo/src/erc721.ts @@ -19,6 +19,7 @@ export const defaults: Required = { burnable: false, pausable: false, mintable: false, + enumerable: false, access: commonDefaults.access, upgradeable: commonDefaults.upgradeable, info: commonDefaults.info @@ -35,6 +36,7 @@ export interface ERC721Options extends CommonContractOptions { burnable?: boolean; pausable?: boolean; mintable?: boolean; + enumerable?: boolean; } function withDefaults(opts: ERC721Options): Required { @@ -45,6 +47,7 @@ function withDefaults(opts: ERC721Options): Required { burnable: opts.burnable ?? defaults.burnable, pausable: opts.pausable ?? defaults.pausable, mintable: opts.mintable ?? defaults.mintable, + enumerable: opts.enumerable ?? defaults.enumerable }; } @@ -62,9 +65,6 @@ export function buildERC721(opts: ERC721Options): Contract { if (allOpts.pausable) { addPausable(c, allOpts.access); - addPausableHook(c); - } else { - c.addStandaloneImport('openzeppelin::token::erc721::ERC721HooksEmptyImpl'); } if (allOpts.burnable) { @@ -75,48 +75,54 @@ export function buildERC721(opts: ERC721Options): Contract { addMintable(c, allOpts.access); } + if (allOpts.enumerable) { + addEnumerable(c); + } + setAccessControl(c, allOpts.access); setUpgradeable(c, allOpts.upgradeable, allOpts.access); setInfo(c, allOpts.info); + addHooks(c, allOpts); return c; } -function addPausableHook(c: ContractBuilder) { - const ERC721HooksTrait: BaseImplementedTrait = { - name: `ERC721HooksImpl`, - of: 'ERC721Component::ERC721HooksTrait', - tags: [], - priority: 0, - }; - c.addImplementedTrait(ERC721HooksTrait); - - c.addStandaloneImport('starknet::ContractAddress'); - - c.addFunction(ERC721HooksTrait, { - name: 'before_update', - args: [ - { name: 'ref self', type: `ERC721Component::ComponentState` }, - { name: 'to', type: 'ContractAddress' }, - { name: 'token_id', type: 'u256' }, - { name: 'auth', type: 'ContractAddress' }, - ], - code: [ - 'let contract_state = ERC721Component::HasComponent::get_contract(@self)', - 'contract_state.pausable.assert_not_paused()', - ], - }); - - c.addFunction(ERC721HooksTrait, { - name: 'after_update', - args: [ - { name: 'ref self', type: `ERC721Component::ComponentState` }, - { name: 'to', type: 'ContractAddress' }, - { name: 'token_id', type: 'u256' }, - { name: 'auth', type: 'ContractAddress' }, - ], - code: [], - }); +function addHooks(c: ContractBuilder, opts: ERC721Options) { + const usesCustomHooks = opts.pausable || opts.enumerable; + if (usesCustomHooks) { + const ERC721HooksTrait: BaseImplementedTrait = { + name: `ERC721HooksImpl`, + of: 'ERC721Component::ERC721HooksTrait', + tags: [], + priority: 0, + }; + c.addImplementedTrait(ERC721HooksTrait); + c.addStandaloneImport('starknet::ContractAddress'); + + const requiresMutState = opts.enumerable; + const initStateLine = requiresMutState + ? 'let mut contract_state = self.get_contract_mut()' + : 'let contract_state = self.get_contract()'; + const beforeUpdateCode = [initStateLine]; + if (opts.pausable) { + beforeUpdateCode.push('contract_state.pausable.assert_not_paused()'); + } + if (opts.enumerable) { + beforeUpdateCode.push('contract_state.erc721_enumerable.before_update(to, token_id)'); + } + c.addFunction(ERC721HooksTrait, { + name: 'before_update', + args: [ + { name: 'ref self', type: `ERC721Component::ComponentState` }, + { name: 'to', type: 'ContractAddress' }, + { name: 'token_id', type: 'u256' }, + { name: 'auth', type: 'ContractAddress' }, + ], + code: beforeUpdateCode, + }); + } else { + c.addStandaloneImport('openzeppelin::token::erc721::ERC721HooksEmptyImpl'); + } } function addERC721Mixin(c: ContractBuilder) { @@ -138,6 +144,10 @@ function addBase(c: ContractBuilder, name: string, symbol: string, baseUri: stri ); } +function addEnumerable(c: ContractBuilder) { + c.addComponent(components.ERC721EnumerableComponent, [], true); +} + function addBurnable(c: ContractBuilder) { c.addStandaloneImport('core::num::traits::Zero'); c.addStandaloneImport('starknet::get_caller_address'); @@ -170,6 +180,27 @@ const components = defineComponents( { value: 'ERC721Component::InternalImpl', }, }, + ERC721EnumerableComponent: { + path: 'openzeppelin::token::erc721::extensions', + substorage: { + name: 'erc721_enumerable', + type: 'ERC721EnumerableComponent::Storage', + }, + event: { + name: 'ERC721EnumerableEvent', + type: 'ERC721EnumerableComponent::Event', + }, + impls: [ + { + name: 'ERC721EnumerableImpl', + value: 'ERC721EnumerableComponent::ERC721EnumerableImpl', + }, + ], + internalImpl: { + name: 'ERC721EnumerableInternalImpl', + value: 'ERC721EnumerableComponent::InternalImpl', + }, + }, }); const functions = defineFunctions({ diff --git a/packages/core-cairo/src/generate/erc721.ts b/packages/core-cairo/src/generate/erc721.ts index 448adcf7..143ff659 100644 --- a/packages/core-cairo/src/generate/erc721.ts +++ b/packages/core-cairo/src/generate/erc721.ts @@ -11,6 +11,7 @@ const blueprint = { symbol: ['MTK'], baseUri: ['https://example.com/'], burnable: booleans, + enumerable: booleans, pausable: booleans, mintable: booleans, access: accessOptions, diff --git a/packages/core-cairo/src/set-access-control.ts b/packages/core-cairo/src/set-access-control.ts index 3c98afd6..bdaca0f3 100644 --- a/packages/core-cairo/src/set-access-control.ts +++ b/packages/core-cairo/src/set-access-control.ts @@ -12,7 +12,7 @@ export type Access = typeof accessOptions[number]; export function setAccessControl(c: ContractBuilder, access: Access) { switch (access) { case 'ownable': { - c.addComponent(components.OwnableComponent, [{ lit:'owner' }], true); + c.addComponent(components.OwnableComponent, [{ lit: 'owner' }], true); c.addStandaloneImport('starknet::ContractAddress'); c.addConstructorArgument({ name: 'owner', type: 'ContractAddress'}); diff --git a/packages/ui/src/cairo/ERC721Controls.svelte b/packages/ui/src/cairo/ERC721Controls.svelte index edd04aa6..90076be2 100644 --- a/packages/ui/src/cairo/ERC721Controls.svelte +++ b/packages/ui/src/cairo/ERC721Controls.svelte @@ -68,6 +68,13 @@ Useful for emergency response. +