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

ERC-721 Metadata hook #80

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions token/src/components/tests.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use dojo::world::IWorldDispatcher;

#[starknet::interface]
// trait IERC721EnumMintBurnPreset {
trait IERC721MetadataHooksMock<TContractState> {
// 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<ContractState>;

impl ERC721ApprovalInternalImpl = erc721_approval_component::InternalImpl<ContractState>;
impl ERC721BalanceInternalImpl = erc721_balance_component::InternalImpl<ContractState>;
impl ERC721MetadataInternalImpl = erc721_metadata_component::InternalImpl<ContractState>;
impl ERC721MintableInternalImpl = erc721_mintable_component::InternalImpl<ContractState>;
impl ERC721OwnerInternalImpl = erc721_owner_component::InternalImpl<ContractState>;

#[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<TContractState> of erc721_metadata_component::ERC721MetadataHooksTrait<TContractState> {
fn custom_uri(
self: @erc721_metadata_component::ComponentState<TContractState>,
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
}
33 changes: 25 additions & 8 deletions token/src/components/token/erc721/erc721_metadata.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ struct ERC721MetaModel {
trait IERC721Metadata<TState> {
fn name(self: @TState) -> ByteArray;
fn symbol(self: @TState) -> ByteArray;
fn token_uri(ref self: TState, token_id: u256) -> ByteArray;
fn token_uri(self: @TState, token_id: u256) -> ByteArray;
}

#[starknet::interface]
trait IERC721MetadataCamel<TState> {
fn tokenURI(ref self: TState, tokenId: u256) -> ByteArray;
fn tokenURI(self: @TState, tokenId: u256) -> ByteArray;
}

///
/// ERC20Metadata Component
/// ERC721Metadata Component
///
#[starknet::component]
mod erc721_metadata_component {
Expand All @@ -54,12 +54,24 @@ mod erc721_metadata_component {
const INVALID_TOKEN_ID: felt252 = 'ERC721: invalid token ID';
}

///
/// Hooks
///
trait ERC721MetadataHooksTrait<TContractState> {
fn custom_uri(
self: @ComponentState<TContractState>,
base_uri: @ByteArray,
token_id: u256,
) -> ByteArray;
}

#[embeddable_as(ERC721MetadataImpl)]
impl ERC721Metadata<
TContractState,
+HasComponent<TContractState>,
+IWorldProvider<TContractState>,
impl ERC721Owner: erc721_owner_comp::HasComponent<TContractState>,
+ERC721MetadataHooksTrait<TContractState>,
+Drop<TContractState>,
> of IERC721Metadata<ComponentState<TContractState>> {
fn name(self: @ComponentState<TContractState>) -> ByteArray {
Expand All @@ -68,7 +80,7 @@ mod erc721_metadata_component {
fn symbol(self: @ComponentState<TContractState>) -> ByteArray {
self.get_meta().symbol
}
fn token_uri(ref self: ComponentState<TContractState>, token_id: u256) -> ByteArray {
fn token_uri(self: @ComponentState<TContractState>, token_id: u256) -> ByteArray {
self.get_uri(token_id)
}
}
Expand All @@ -79,9 +91,10 @@ mod erc721_metadata_component {
+HasComponent<TContractState>,
+IWorldProvider<TContractState>,
impl ERC721Owner: erc721_owner_comp::HasComponent<TContractState>,
+ERC721MetadataHooksTrait<TContractState>,
+Drop<TContractState>,
> of IERC721MetadataCamel<ComponentState<TContractState>> {
fn tokenURI(ref self: ComponentState<TContractState>, tokenId: u256) -> ByteArray {
fn tokenURI(self: @ComponentState<TContractState>, tokenId: u256) -> ByteArray {
self.get_uri(tokenId)
}
}
Expand All @@ -93,17 +106,21 @@ mod erc721_metadata_component {
+HasComponent<TContractState>,
+IWorldProvider<TContractState>,
impl ERC721Owner: erc721_owner_comp::HasComponent<TContractState>,
impl Hooks: ERC721MetadataHooksTrait<TContractState>,
+Drop<TContractState>,
> of InternalTrait<TContractState> {
fn get_meta(self: @ComponentState<TContractState>) -> ERC721MetaModel {
get!(self.get_contract().world(), get_contract_address(), (ERC721MetaModel))
}

fn get_uri(ref self: ComponentState<TContractState>, token_id: u256) -> ByteArray {
let mut erc721_owner = get_dep_component_mut!(ref self, ERC721Owner);
fn get_uri(self: @ComponentState<TContractState>, token_id: u256) -> ByteArray {
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);
Expand Down
20 changes: 20 additions & 0 deletions token/src/components/token/erc721/erc721_metadata_hooks.cairo
Original file line number Diff line number Diff line change
@@ -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<TContractState> of erc721_metadata_component::ERC721MetadataHooksTrait<TContractState> {
fn custom_uri(
self: @erc721_metadata_component::ComponentState<TContractState>,
base_uri: @ByteArray,
token_id: u256,
) -> ByteArray { "" }
}
1 change: 1 addition & 0 deletions token/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions token/src/presets/erc721/enumerable_mintable_burnable.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ trait IERC721EnumMintBurnPreset<TState> {
// IERC721
fn name(self: @TState) -> ByteArray;
fn symbol(self: @TState) -> ByteArray;
fn token_uri(ref self: TState, token_id: u256) -> ByteArray;
fn token_uri(self: @TState, token_id: u256) -> ByteArray;
fn owner_of(self: @TState, account: ContractAddress) -> bool;
fn get_approved(self: @TState, token_id: u256) -> ContractAddress;
fn approve(ref self: TState, to: ContractAddress, token_id: u256);
Expand All @@ -15,7 +15,7 @@ trait IERC721EnumMintBurnPreset<TState> {
fn token_of_owner_by_index(self: @TState, owner: ContractAddress, index: u256) -> u256;

// IERC721CamelOnly
fn tokenURI(ref self: TState, token_id: u256) -> ByteArray;
fn tokenURI(self: @TState, token_id: u256) -> ByteArray;

// IWorldProvider
fn world(self: @TState,) -> IWorldDispatcher;
Expand Down Expand Up @@ -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;

Expand Down
5 changes: 3 additions & 2 deletions token/src/presets/erc721/mintable_burnable.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ trait IERC721MintableBurnablePreset<TState> {
// IERC721
fn name(self: @TState) -> ByteArray;
fn symbol(self: @TState) -> ByteArray;
fn token_uri(ref self: TState, token_id: u256) -> ByteArray;
fn token_uri(self: @TState, token_id: u256) -> ByteArray;
fn owner_of(self: @TState, account: ContractAddress) -> bool;
fn balance_of(self: @TState, account: ContractAddress) -> u256;
fn get_approved(self: @TState, token_id: u256) -> ContractAddress;
fn transfer_from(ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u256);
fn approve(ref self: TState, to: ContractAddress, token_id: u256);

// IERC721CamelOnly
fn tokenURI(ref self: TState, token_id: u256) -> ByteArray;
fn tokenURI(self: @TState, token_id: u256) -> ByteArray;
fn balanceOf(self: @TState, account: ContractAddress) -> u256;
fn transferFrom(ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u256);

Expand Down Expand Up @@ -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;

Expand Down