Sensei MCP

Official
An example of a ERC20 token contract in dojo // SPDX-License-Identifier: MIT // Compatible with OpenZeppelin Contracts for Cairo ^0.20.0 #[starknet::contract] mod ERC20Token { use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); // External #[abi(embed_v0)] impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>; #[abi(embed_v0)] impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>; // Internal impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>; impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>; #[storage] struct Storage { #[substorage(v0)] erc20: ERC20Component::Storage, #[substorage(v0)] ownable: OwnableComponent::Storage, } #[event] #[derive(Drop, starknet::Event)] enum Event { #[flat] ERC20Event: ERC20Component::Event, #[flat] OwnableEvent: OwnableComponent::Event, } #[constructor] fn constructor( ref self: ContractState, owner: ContractAddress, name: ByteArray, symbol: ByteArray, initial_supply: u256, recipient: ContractAddress, ) { self.ownable.initializer(owner); self.erc20.initializer(name, symbol); self.erc20.mint(recipient, initial_supply); } /// This implementation is not secure, only for testing purposes and quick minting. #[generate_trait] #[abi(per_item)] impl ExternalImpl of ExternalTrait { #[external(v0)] fn mint(ref self: ContractState, amount: u256) { self.erc20.mint(starknet::get_caller_address(), amount); } } } An example of a ERC721 contract in dojo // SPDX-License-Identifier: MIT // Compatible with OpenZeppelin Contracts for Cairo ^0.20.0 #[starknet::contract] mod ERC721Token { use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::token::erc721::{ERC721Component, ERC721HooksEmptyImpl}; use starknet::ContractAddress; use crate::externals::components::erc4906::ERC4906Component; component!(path: ERC721Component, storage: erc721, event: ERC721Event); component!(path: SRC5Component, storage: src5, event: SRC5Event); component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); component!(path: ERC4906Component, storage: erc4906, event: ERC4906Event); // External #[abi(embed_v0)] impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl<ContractState>; #[abi(embed_v0)] impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>; #[abi(embed_v0)] impl ERC4906MixinImpl = ERC4906Component::ERC4906Implementation<ContractState>; // Internal impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>; impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>; #[storage] struct Storage { #[substorage(v0)] erc721: ERC721Component::Storage, #[substorage(v0)] src5: SRC5Component::Storage, #[substorage(v0)] ownable: OwnableComponent::Storage, #[substorage(v0)] erc4906: ERC4906Component::Storage, } #[event] #[derive(Drop, starknet::Event)] enum Event { #[flat] ERC721Event: ERC721Component::Event, #[flat] SRC5Event: SRC5Component::Event, #[flat] OwnableEvent: OwnableComponent::Event, #[flat] ERC4906Event: ERC4906Component::Event, } #[constructor] fn constructor( ref self: ContractState, owner: ContractAddress, name: ByteArray, symbol: ByteArray, base_uri: ByteArray, ) { self.ownable.initializer(owner); self.erc721.initializer(name, symbol, base_uri); } /// This implementation is not secure, only for testing purposes and quick minting. #[generate_trait] #[abi(per_item)] impl ERC721Demo of ERC721DemoTrait { #[external(v0)] fn mint(ref self: ContractState, token_id: u256) { self.erc721.mint(starknet::get_caller_address(), token_id); } #[external(v0)] fn update_token_metadata(ref self: ContractState, token_id: u256) { // Only owner can update metadata self.ownable.assert_only_owner(); // Emit metadata update event self.erc4906.emit_metadata_update(token_id); } #[external(v0)] fn update_batch_token_metadata( ref self: ContractState, from_token_id: u256, to_token_id: u256, ) { // Only owner can update metadata self.ownable.assert_only_owner(); // Emit batch metadata update event self.erc4906.emit_batch_metadata_update(from_token_id, to_token_id); } #[external(v0)] fn update_tokens_metadata(ref self: ContractState, token_ids: Span<u256>) { // Only owner can update metadata self.ownable.assert_only_owner(); // Emit metadata update event for each token let mut i: usize = 0; loop { if i >= token_ids.len() { break; } self.erc4906.emit_metadata_update(*token_ids.at(i)); i += 1; } } } } An example of an ERC1155 token in dojo // SPDX-License-Identifier: MIT // Compatible with OpenZeppelin Contracts for Cairo ^0.20.0 #[starknet::contract] mod ERC1155Token { use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::introspection::src5::SRC5Component; use openzeppelin::token::erc1155::{ERC1155Component, ERC1155HooksEmptyImpl}; use starknet::ContractAddress; use crate::externals::components::erc4906::ERC4906Component; component!(path: ERC1155Component, storage: erc1155, event: ERC1155Event); component!(path: SRC5Component, storage: src5, event: SRC5Event); component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); component!(path: ERC4906Component, storage: erc4906, event: ERC4906Event); // External #[abi(embed_v0)] impl ERC1155MixinImpl = ERC1155Component::ERC1155MixinImpl<ContractState>; #[abi(embed_v0)] impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>; #[abi(embed_v0)] impl ERC4906MixinImpl = ERC4906Component::ERC4906Implementation<ContractState>; // Internal impl ERC1155InternalImpl = ERC1155Component::InternalImpl<ContractState>; impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>; #[storage] struct Storage { #[substorage(v0)] erc1155: ERC1155Component::Storage, #[substorage(v0)] src5: SRC5Component::Storage, #[substorage(v0)] ownable: OwnableComponent::Storage, #[substorage(v0)] erc4906: ERC4906Component::Storage, } #[event] #[derive(Drop, starknet::Event)] enum Event { #[flat] ERC1155Event: ERC1155Component::Event, #[flat] SRC5Event: SRC5Component::Event, #[flat] OwnableEvent: OwnableComponent::Event, #[flat] ERC4906Event: ERC4906Component::Event, } #[constructor] fn constructor(ref self: ContractState, owner: ContractAddress, base_uri: ByteArray) { self.ownable.initializer(owner); self.erc1155.initializer(base_uri); } /// This implementation is not secure, only for testing purposes and quick minting. #[generate_trait] #[abi(per_item)] impl ExternalImpl of ExternalTrait { #[external(v0)] fn token_uri(ref self: ContractState, token_id: u256) -> ByteArray { let seed = starknet::get_execution_info().block_info.block_number; format!( "data:application/json,{{ \"image\": \"https://api.dicebear.com/9.x/lorelei-neutral/png?seed={}\" }}", seed, ) } #[external(v0)] fn mint(ref self: ContractState, token_id: u256, value: u256) { self .erc1155 .update( starknet::contract_address_const::<0x0>(), starknet::get_caller_address(), array![token_id].span(), array![value].span(), ); // Seems to not be supported by default dojo account. // self.erc1155.mint_with_acceptance_check(account, token_id, value, data); } #[external(v0)] fn transfer_from( ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256, value: u256, ) { self.erc1155.update(from, to, array![token_id].span(), array![value].span()); // safe transfer from does not support default account since they dont implement // receiver. } #[external(v0)] fn batch_mint(ref self: ContractState, token_ids: Span<u256>, values: Span<u256>) { self .erc1155 .update( starknet::contract_address_const::<0x0>(), starknet::get_caller_address(), token_ids, values, ); // Seems to not be supported by default dojo account. // self.erc1155.batch_mint_with_acceptance_check(account, token_ids, values, data); } #[external(v0)] fn update_token_metadata(ref self: ContractState, token_id: u256) { // Only owner can update metadata self.ownable.assert_only_owner(); // Emit metadata update event self.erc4906.emit_metadata_update(token_id); } #[external(v0)] fn update_batch_token_metadata( ref self: ContractState, from_token_id: u256, to_token_id: u256, ) { // Only owner can update metadata self.ownable.assert_only_owner(); // Emit batch metadata update event self.erc4906.emit_batch_metadata_update(from_token_id, to_token_id); } // Optional: Batch update specific token IDs #[external(v0)] fn update_tokens_metadata(ref self: ContractState, token_ids: Span<u256>) { // Only owner can update metadata self.ownable.assert_only_owner(); // Emit metadata update event for each token let mut i: usize = 0; loop { if i >= token_ids.len() { break; } self.erc4906.emit_metadata_update(*token_ids.at(i)); i += 1; } } } } An example dojo_dev.toml profile with those external contracts declared [world] description = "example world" name = "example" seed = "dojo_examples" [[models]] tag = "ns-Message" description = "Message sent by a player" [[models]] tag = "ns-Position" description = "position of a player in the world" [[models]] tag = "ns-Moves" description = "move of a player in the world" [[events]] tag = "ns-Moved" description = "when a player has moved" [[contracts]] tag = "ns-actions" description = "set of actions for a player" [[external_contracts]] contract_name = "ERC20Token" instance_name = "GoldToken" salt = "1" constructor_data = ["0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba", "str:Gold", "str:GOLD", "u256:0x10000000000000", "0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba"] [[external_contracts]] contract_name = "ERC20Token" instance_name = "WoodToken" salt = "1" constructor_data = ["0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba", "str:Wood", "str:WOOD", "u256:0x10000000000000", "0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba"] [[external_contracts]] contract_name = "ERC721Token" instance_name = "Badge" salt = "1" constructor_data = ["0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba", "str:Badge", "str:BDG", "str:https://badge.com/" ] [[external_contracts]] contract_name = "ERC1155Token" instance_name = "Rewards" salt = "1" constructor_data = ["0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba", "str:https://rewards.com/" ] [[external_contracts]] contract_name = "Bank" salt = "1" constructor_data = ["0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba"] [[external_contracts]] contract_name = "Saloon" constructor_data = [] salt = "1" [lib_versions] "ns-simple_math" = "0_1_0" [namespace] default = "ns" [env] rpc_url = "http://localhost:5050/" # Default account for katana with seed = 0 account_address = "0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba" private_key = "0x1800000000300000180000000000030000000000003006001800006600" world_address = "0x24334e79a3c56e5374c5bdd148c22ff2f0de3b4dc6e734e22ea49795f367221" ipfs_config.url = "https://ipfs.infura.io:5001" ipfs_config.username = "2EBrzr7ZASQZKH32sl2xWauXPSA" ipfs_config.password = "12290b883db9138a8ae3363b6739d220" [init_call_args] "ns-others" = ["0xff"] [writers] "ns" = [ "ns-mock_token", "ns-actions", "ns-others" ] You should define them like this for each one of the contracts [[external_contracts]] contract_name = "ERC20Token" instance_name = "GoldToken" salt = "1" constructor_data = ["0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba", "str:Gold", "str:GOLD", "u256:0x10000000000000", "0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba"] and always pass in the correct constructor_data while referring to the token contract implementation to know what is the constructor data.