Sensei MCP
Official
by dojoengine
- sensei-mcp
- resources
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.