diff --git a/token/src/components/tests.cairo b/token/src/components/tests.cairo index 2663c376..408cd1fd 100644 --- a/token/src/components/tests.cairo +++ b/token/src/components/tests.cairo @@ -15,6 +15,7 @@ mod mocks { mod erc721_balance_mock; mod erc721_enumerable_mock; mod erc721_metadata_mock; + mod erc721_metadata_hooks_mock; mod erc721_mintable_burnable_mock; mod erc721_receiver_mock; } @@ -55,6 +56,8 @@ mod token { #[cfg(test)] mod test_erc721_metadata; #[cfg(test)] + mod test_erc721_metadata_hooks; + #[cfg(test)] mod test_erc721_mintable_burnable; } } diff --git a/token/src/components/tests/mocks/erc721/erc721_metadata_hooks_mock.cairo b/token/src/components/tests/mocks/erc721/erc721_metadata_hooks_mock.cairo new file mode 100644 index 00000000..3870eaf6 --- /dev/null +++ b/token/src/components/tests/mocks/erc721/erc721_metadata_hooks_mock.cairo @@ -0,0 +1,88 @@ +use dojo::world::IWorldDispatcher; + +#[starknet::interface] +// trait IERC721EnumMintBurnPreset { +trait IERC721MetadataHooksMock { + // IWorldProvider + fn world(self: @TContractState,) -> IWorldDispatcher; +} + +#[dojo::contract] +mod erc721_metadata_hooks_mock { + + use starknet::{get_contract_address}; + use token::components::token::erc721::erc721_approval::erc721_approval_component; + use token::components::token::erc721::erc721_balance::erc721_balance_component; + use token::components::token::erc721::erc721_metadata::erc721_metadata_component; + use token::components::token::erc721::erc721_mintable::erc721_mintable_component; + use token::components::token::erc721::erc721_owner::erc721_owner_component; + + component!( + path: erc721_approval_component, storage: erc721_approval, event: ERC721ApprovalEvent + ); + component!(path: erc721_balance_component, storage: erc721_balance, event: ERC721BalanceEvent); + component!( + path: erc721_metadata_component, storage: erc721_metadata, event: ERC721MetadataEvent + ); + component!( + path: erc721_mintable_component, storage: erc721_mintable, event: ERC721MintableEvent + ); + component!(path: erc721_owner_component, storage: erc721_owner, event: ERC721OwnerEvent); + + #[abi(embed_v0)] + impl ERC721MetadataImpl = + erc721_metadata_component::ERC721MetadataImpl; + + impl ERC721ApprovalInternalImpl = erc721_approval_component::InternalImpl; + impl ERC721BalanceInternalImpl = erc721_balance_component::InternalImpl; + impl ERC721MetadataInternalImpl = erc721_metadata_component::InternalImpl; + impl ERC721MintableInternalImpl = erc721_mintable_component::InternalImpl; + impl ERC721OwnerInternalImpl = erc721_owner_component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc721_approval: erc721_approval_component::Storage, + #[substorage(v0)] + erc721_balance: erc721_balance_component::Storage, + #[substorage(v0)] + erc721_metadata: erc721_metadata_component::Storage, + #[substorage(v0)] + erc721_mintable: erc721_mintable_component::Storage, + #[substorage(v0)] + erc721_owner: erc721_owner_component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + ERC721ApprovalEvent: erc721_approval_component::Event, + ERC721BalanceEvent: erc721_balance_component::Event, + ERC721MetadataEvent: erc721_metadata_component::Event, + ERC721MintableEvent: erc721_mintable_component::Event, + ERC721OwnerEvent: erc721_owner_component::Event + } + + + // + // Metadata Hooks + // + use super::{IERC721MetadataHooksMockDispatcher, IERC721MetadataHooksMockDispatcherTrait}; + impl ERC721MetadataHooksImpl of erc721_metadata_component::ERC721MetadataHooksTrait { + fn custom_uri( + self: @erc721_metadata_component::ComponentState, + base_uri: @ByteArray, + token_id: u256, + ) -> ByteArray { + // + // example on how to access the world + // (does not work for testing, throws 'CONTRACT_NOT_DEPLOYED') + // + // let contract_address = get_contract_address(); + // let selfie = IERC721MetadataHooksMockDispatcher{ contract_address }; + // let _world = selfie.world(); + + format!("CUSTOM{}{}", base_uri, token_id) + } + } +} diff --git a/token/src/components/tests/mocks/erc721/erc721_metadata_mock.cairo b/token/src/components/tests/mocks/erc721/erc721_metadata_mock.cairo index 11f47d6e..2eec40ea 100644 --- a/token/src/components/tests/mocks/erc721/erc721_metadata_mock.cairo +++ b/token/src/components/tests/mocks/erc721/erc721_metadata_mock.cairo @@ -3,6 +3,7 @@ mod erc721_metadata_mock { use token::components::token::erc721::erc721_approval::erc721_approval_component; use token::components::token::erc721::erc721_balance::erc721_balance_component; use token::components::token::erc721::erc721_metadata::erc721_metadata_component; + use token::components::token::erc721::erc721_metadata_hooks::ERC721MetadataHooksEmptyImpl; use token::components::token::erc721::erc721_mintable::erc721_mintable_component; use token::components::token::erc721::erc721_owner::erc721_owner_component; diff --git a/token/src/components/tests/mocks/erc721/erc721_mintable_burnable_mock.cairo b/token/src/components/tests/mocks/erc721/erc721_mintable_burnable_mock.cairo index 199d9ca3..61a69732 100644 --- a/token/src/components/tests/mocks/erc721/erc721_mintable_burnable_mock.cairo +++ b/token/src/components/tests/mocks/erc721/erc721_mintable_burnable_mock.cairo @@ -3,6 +3,7 @@ mod erc721_mintable_burnable_mock { use token::components::token::erc721::erc721_approval::erc721_approval_component; use token::components::token::erc721::erc721_balance::erc721_balance_component; use token::components::token::erc721::erc721_metadata::erc721_metadata_component; + use token::components::token::erc721::erc721_metadata_hooks::ERC721MetadataHooksEmptyImpl; use token::components::token::erc721::erc721_mintable::erc721_mintable_component; use token::components::token::erc721::erc721_burnable::erc721_burnable_component; use token::components::token::erc721::erc721_owner::erc721_owner_component; diff --git a/token/src/components/tests/token/erc721/test_erc721_metadata_hooks.cairo b/token/src/components/tests/token/erc721/test_erc721_metadata_hooks.cairo new file mode 100644 index 00000000..e72b9340 --- /dev/null +++ b/token/src/components/tests/token/erc721/test_erc721_metadata_hooks.cairo @@ -0,0 +1,41 @@ +use integer::BoundedInt; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; +use dojo::test_utils::spawn_test_world; +use token::tests::constants::{OWNER}; + +use token::components::token::erc721::erc721_metadata::{erc_721_meta_model, ERC721MetaModel,}; +use token::components::token::erc721::erc721_metadata::erc721_metadata_component::{ + ERC721MetadataImpl, ERC721MetadataCamelImpl, InternalImpl +}; +use token::components::token::erc721::erc721_mintable::erc721_mintable_component::InternalImpl as ERC721MintableInternalImpl; + +use token::components::tests::mocks::erc721::erc721_metadata_hooks_mock::erc721_metadata_hooks_mock; +use starknet::storage::{StorageMemberAccessTrait}; + + +fn STATE() -> (IWorldDispatcher, erc721_metadata_hooks_mock::ContractState) { + let world = spawn_test_world(array![erc_721_meta_model::TEST_CLASS_HASH,]); + + let mut state = erc721_metadata_hooks_mock::contract_state_for_testing(); + state.world_dispatcher.write(world); + + (world, state) +} + +#[test] +fn test_erc721_metadata_hooks_initialize() { + let (_world, mut state) = STATE(); + + let NAME: ByteArray = "NAME"; + let SYMBOL: ByteArray = "SYMBOL"; + let URI: ByteArray = "URI"; + + state.erc721_metadata.initialize(NAME, SYMBOL, URI); + + assert(state.erc721_metadata.name() == "NAME", 'Should be NAME'); + assert(state.erc721_metadata.symbol() == "SYMBOL", 'Should be SYMBOL'); + + state.erc721_mintable.mint(OWNER(), 1); + assert(state.erc721_metadata.token_uri(1) == "CUSTOMURI1", 'Should be CUSTOMURI1'); + assert(state.erc721_metadata.tokenURI(1) == "CUSTOMURI1", 'Should be CUSTOMURI1'); +} diff --git a/token/src/components/token/erc721/erc721_metadata.cairo b/token/src/components/token/erc721/erc721_metadata.cairo index f36ddb00..d298b53c 100644 --- a/token/src/components/token/erc721/erc721_metadata.cairo +++ b/token/src/components/token/erc721/erc721_metadata.cairo @@ -54,12 +54,24 @@ mod erc721_metadata_component { const INVALID_TOKEN_ID: felt252 = 'ERC721: invalid token ID'; } + /// + /// Hooks + /// + trait ERC721MetadataHooksTrait { + fn custom_uri( + self: @ComponentState, + base_uri: @ByteArray, + token_id: u256, + ) -> ByteArray; + } + #[embeddable_as(ERC721MetadataImpl)] impl ERC721Metadata< TContractState, +HasComponent, +IWorldProvider, impl ERC721Owner: erc721_owner_comp::HasComponent, + +ERC721MetadataHooksTrait, +Drop, > of IERC721Metadata> { fn name(self: @ComponentState) -> ByteArray { @@ -79,6 +91,7 @@ mod erc721_metadata_component { +HasComponent, +IWorldProvider, impl ERC721Owner: erc721_owner_comp::HasComponent, + +ERC721MetadataHooksTrait, +Drop, > of IERC721MetadataCamel> { fn tokenURI(self: @ComponentState, tokenId: u256) -> ByteArray { @@ -93,6 +106,7 @@ mod erc721_metadata_component { +HasComponent, +IWorldProvider, impl ERC721Owner: erc721_owner_comp::HasComponent, + impl Hooks: ERC721MetadataHooksTrait, +Drop, > of InternalTrait { fn get_meta(self: @ComponentState) -> ERC721MetaModel { @@ -103,7 +117,10 @@ mod erc721_metadata_component { let mut erc721_owner = get_dep_component!(self, ERC721Owner); assert(erc721_owner.exists(token_id), Errors::INVALID_TOKEN_ID); let base_uri = self.get_meta().base_uri; - if base_uri.len() == 0 { + let custom_uri = Hooks::custom_uri(self, @base_uri, token_id); + if custom_uri.len() > 0 { + return custom_uri; + } else if base_uri.len() == 0 { return ""; } else { return format!("{}{}", base_uri, token_id); diff --git a/token/src/components/token/erc721/erc721_metadata_hooks.cairo b/token/src/components/token/erc721/erc721_metadata_hooks.cairo new file mode 100644 index 00000000..0fbe5074 --- /dev/null +++ b/token/src/components/token/erc721/erc721_metadata_hooks.cairo @@ -0,0 +1,20 @@ + +/// +/// An empty implementation of the ERC721MetadataHooksTrait +/// +/// When the hook is not required, import together with the component: +/// use token::components::token::erc721::erc721_metadata::erc721_metadata_component; +/// use token::components::token::erc721::erc721_metadata_hooks::ERC721MetadataHooksEmptyImpl; +/// +/// Or implement your own (example on erc721_metadata_hooks_mock.cairo) +/// + +use token::components::token::erc721::erc721_metadata::erc721_metadata_component; + +impl ERC721MetadataHooksEmptyImpl of erc721_metadata_component::ERC721MetadataHooksTrait { + fn custom_uri( + self: @erc721_metadata_component::ComponentState, + base_uri: @ByteArray, + token_id: u256, + ) -> ByteArray { "" } +} diff --git a/token/src/lib.cairo b/token/src/lib.cairo index 6d54fb8b..05ab5c32 100644 --- a/token/src/lib.cairo +++ b/token/src/lib.cairo @@ -23,6 +23,7 @@ mod components { mod erc721_burnable; mod erc721_enumerable; mod erc721_metadata; + mod erc721_metadata_hooks; mod erc721_mintable; mod erc721_owner; mod erc721_receiver; diff --git a/token/src/presets/erc721/enumerable_mintable_burnable.cairo b/token/src/presets/erc721/enumerable_mintable_burnable.cairo index 8502475d..f78f909e 100644 --- a/token/src/presets/erc721/enumerable_mintable_burnable.cairo +++ b/token/src/presets/erc721/enumerable_mintable_burnable.cairo @@ -82,6 +82,7 @@ mod ERC721EnumMintBurn { use token::components::token::erc721::erc721_burnable::erc721_burnable_component; use token::components::token::erc721::erc721_enumerable::erc721_enumerable_component; use token::components::token::erc721::erc721_metadata::erc721_metadata_component; + use token::components::token::erc721::erc721_metadata_hooks::ERC721MetadataHooksEmptyImpl; use token::components::token::erc721::erc721_mintable::erc721_mintable_component; use token::components::token::erc721::erc721_owner::erc721_owner_component; diff --git a/token/src/presets/erc721/mintable_burnable.cairo b/token/src/presets/erc721/mintable_burnable.cairo index 11217dad..615e1004 100644 --- a/token/src/presets/erc721/mintable_burnable.cairo +++ b/token/src/presets/erc721/mintable_burnable.cairo @@ -61,6 +61,7 @@ mod ERC721MintableBurnable { use token::components::token::erc721::erc721_balance::erc721_balance_component; use token::components::token::erc721::erc721_burnable::erc721_burnable_component; use token::components::token::erc721::erc721_metadata::erc721_metadata_component; + use token::components::token::erc721::erc721_metadata_hooks::ERC721MetadataHooksEmptyImpl; use token::components::token::erc721::erc721_mintable::erc721_mintable_component; use token::components::token::erc721::erc721_owner::erc721_owner_component;