dojo-token

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Dojo Tokens

Dojo代币

Implement ERC20/ERC721 tokens in Cairo, deploy them alongside your Dojo world, and configure Torii to index balances, transfers, and metadata.
在Cairo中实现ERC20/ERC721代币,将其与Dojo世界一同部署,并配置Torii以索引余额、转账记录及元数据。

When to Use This Skill

适用场景

  • "Implement ERC20 token for game currency"
  • "Create NFT items with ERC721"
  • "Deploy an ERC20 token with my world"
  • "Index token balances with Torii"
  • "Query token transfers"
  • "Use Origami for tokens"
  • "为游戏货币实现ERC20代币"
  • "使用ERC721创建NFT道具"
  • "与我的世界一同部署ERC20代币"
  • "使用Torii索引代币余额"
  • "查询代币转账记录"
  • "使用Origami处理代币"

What This Skill Does

技能功能

  • Implement ERC20 fungible tokens and ERC721 NFTs in Cairo
  • Deploy token contracts as external contracts via
    sozo migrate
  • Configure Torii to index token balances, transfers, and metadata
  • Query token data via SQL and client SDKs
  • 在Cairo中实现ERC20 fungible代币和ERC721 NFT
  • 通过
    sozo migrate
    将代币合约作为外部合约部署
  • 配置Torii以索引代币余额、转账记录及元数据
  • 通过SQL和客户端SDK查询代币数据

Using Origami Library

使用Origami库

Add to
Scarb.toml
:
toml
[dependencies]
origami_token = { git = "https://github.com/dojoengine/origami", tag = "v1.0.0" }
Origami provides reusable token components following standard interfaces. Refer to the Origami documentation for the latest API.
Scarb.toml
中添加:
toml
[dependencies]
origami_token = { git = "https://github.com/dojoengine/origami", tag = "v1.0.0" }
Origami提供遵循标准接口的可复用代币组件。 请参考Origami文档获取最新API。

Simple Token Implementation

简单代币实现

You can implement tokens using standard Dojo models without Origami.
你可以不使用Origami,通过标准Dojo模型实现代币。

ERC20-like Currency

类ERC20货币

cairo
#[derive(Copy, Drop, Serde)]
#[dojo::model]
pub struct Gold {
    #[key]
    pub player: ContractAddress,
    pub amount: u256,
}

#[starknet::interface]
trait IGoldToken<T> {
    fn mint(ref self: T, to: ContractAddress, amount: u256);
    fn transfer(ref self: T, to: ContractAddress, amount: u256);
    fn balance_of(self: @T, account: ContractAddress) -> u256;
}

#[dojo::contract]
mod gold_token {
    use super::{IGoldToken, Gold};
    use starknet::{ContractAddress, get_caller_address};
    use dojo::model::ModelStorage;

    #[abi(embed_v0)]
    impl GoldTokenImpl of IGoldToken<ContractState> {
        fn mint(ref self: ContractState, to: ContractAddress, amount: u256) {
            let mut world = self.world_default();

            let mut balance: Gold = world.read_model(to);
            balance.amount += amount;
            world.write_model(@balance);
        }

        fn transfer(ref self: ContractState, to: ContractAddress, amount: u256) {
            let mut world = self.world_default();
            let from = get_caller_address();

            let mut from_balance: Gold = world.read_model(from);
            let mut to_balance: Gold = world.read_model(to);

            assert(from_balance.amount >= amount, 'insufficient balance');

            from_balance.amount -= amount;
            to_balance.amount += amount;

            world.write_model(@from_balance);
            world.write_model(@to_balance);
        }

        fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
            let world = self.world_default();
            let balance: Gold = world.read_model(account);
            balance.amount
        }
    }

    #[generate_trait]
    impl InternalImpl of InternalTrait {
        fn world_default(self: @ContractState) -> dojo::world::WorldStorage {
            self.world(@"my_game")
        }
    }
}
cairo
#[derive(Copy, Drop, Serde)]
#[dojo::model]
pub struct Gold {
    #[key]
    pub player: ContractAddress,
    pub amount: u256,
}

#[starknet::interface]
trait IGoldToken<T> {
    fn mint(ref self: T, to: ContractAddress, amount: u256);
    fn transfer(ref self: T, to: ContractAddress, amount: u256);
    fn balance_of(self: @T, account: ContractAddress) -> u256;
}

#[dojo::contract]
mod gold_token {
    use super::{IGoldToken, Gold};
    use starknet::{ContractAddress, get_caller_address};
    use dojo::model::ModelStorage;

    #[abi(embed_v0)]
    impl GoldTokenImpl of IGoldToken<ContractState> {
        fn mint(ref self: ContractState, to: ContractAddress, amount: u256) {
            let mut world = self.world_default();

            let mut balance: Gold = world.read_model(to);
            balance.amount += amount;
            world.write_model(@balance);
        }

        fn transfer(ref self: ContractState, to: ContractAddress, amount: u256) {
            let mut world = self.world_default();
            let from = get_caller_address();

            let mut from_balance: Gold = world.read_model(from);
            let mut to_balance: Gold = world.read_model(to);

            assert(from_balance.amount >= amount, 'insufficient balance');

            from_balance.amount -= amount;
            to_balance.amount += amount;

            world.write_model(@from_balance);
            world.write_model(@to_balance);
        }

        fn balance_of(self: @ContractState, account: ContractAddress) -> u256 {
            let world = self.world_default();
            let balance: Gold = world.read_model(account);
            balance.amount
        }
    }

    #[generate_trait]
    impl InternalImpl of InternalTrait {
        fn world_default(self: @ContractState) -> dojo::world::WorldStorage {
            self.world(@"my_game")
        }
    }
}

ERC721-like NFT

类ERC721 NFT

cairo
#[derive(Copy, Drop, Serde)]
#[dojo::model]
pub struct Weapon {
    #[key]
    pub token_id: u256,
    pub owner: ContractAddress,
    pub damage: u32,
    pub rarity: u8,
}

#[starknet::interface]
trait IWeaponNFT<T> {
    fn mint(ref self: T, to: ContractAddress, damage: u32) -> u256;
    fn transfer(ref self: T, to: ContractAddress, token_id: u256);
    fn owner_of(self: @T, token_id: u256) -> ContractAddress;
}

#[dojo::contract]
mod weapon_nft {
    use super::{IWeaponNFT, Weapon};
    use starknet::{ContractAddress, get_caller_address};
    use dojo::model::ModelStorage;

    #[abi(embed_v0)]
    impl WeaponNFTImpl of IWeaponNFT<ContractState> {
        fn mint(ref self: ContractState, to: ContractAddress, damage: u32) -> u256 {
            let mut world = self.world_default();

            let token_id: u256 = world.uuid().into();

            let weapon = Weapon {
                token_id,
                owner: to,
                damage,
                rarity: 1,
            };

            world.write_model(@weapon);
            token_id
        }

        fn transfer(ref self: ContractState, to: ContractAddress, token_id: u256) {
            let mut world = self.world_default();
            let from = get_caller_address();

            let mut weapon: Weapon = world.read_model(token_id);
            assert(weapon.owner == from, 'not owner');

            weapon.owner = to;
            world.write_model(@weapon);
        }

        fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress {
            let world = self.world_default();
            let weapon: Weapon = world.read_model(token_id);
            weapon.owner
        }
    }

    #[generate_trait]
    impl InternalImpl of InternalTrait {
        fn world_default(self: @ContractState) -> dojo::world::WorldStorage {
            self.world(@"my_game")
        }
    }
}
cairo
#[derive(Copy, Drop, Serde)]
#[dojo::model]
pub struct Weapon {
    #[key]
    pub token_id: u256,
    pub owner: ContractAddress,
    pub damage: u32,
    pub rarity: u8,
}

#[starknet::interface]
trait IWeaponNFT<T> {
    fn mint(ref self: T, to: ContractAddress, damage: u32) -> u256;
    fn transfer(ref self: T, to: ContractAddress, token_id: u256);
    fn owner_of(self: @T, token_id: u256) -> ContractAddress;
}

#[dojo::contract]
mod weapon_nft {
    use super::{IWeaponNFT, Weapon};
    use starknet::{ContractAddress, get_caller_address};
    use dojo::model::ModelStorage;

    #[abi(embed_v0)]
    impl WeaponNFTImpl of IWeaponNFT<ContractState> {
        fn mint(ref self: ContractState, to: ContractAddress, damage: u32) -> u256 {
            let mut world = self.world_default();

            let token_id: u256 = world.uuid().into();

            let weapon = Weapon {
                token_id,
                owner: to,
                damage,
                rarity: 1,
            };

            world.write_model(@weapon);
            token_id
        }

        fn transfer(ref self: ContractState, to: ContractAddress, token_id: u256) {
            let mut world = self.world_default();
            let from = get_caller_address();

            let mut weapon: Weapon = world.read_model(token_id);
            assert(weapon.owner == from, 'not owner');

            weapon.owner = to;
            world.write_model(@weapon);
        }

        fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress {
            let world = self.world_default();
            let weapon: Weapon = world.read_model(token_id);
            weapon.owner
        }
    }

    #[generate_trait]
    impl InternalImpl of InternalTrait {
        fn world_default(self: @ContractState) -> dojo::world::WorldStorage {
            self.world(@"my_game")
        }
    }
}

Token Events

代币事件

Emit events so Torii and clients can track token operations:
cairo
#[derive(Copy, Drop, Serde)]
#[dojo::event]
pub struct TokenTransferred {
    #[key]
    pub from: ContractAddress,
    #[key]
    pub to: ContractAddress,
    pub amount: u256,
}

// Emit in your functions
world.emit_event(@TokenTransferred { from, to, amount });
触发事件以便Torii和客户端追踪代币操作:
cairo
#[derive(Copy, Drop, Serde)]
#[dojo::event]
pub struct TokenTransferred {
    #[key]
    pub from: ContractAddress,
    #[key]
    pub to: ContractAddress,
    pub amount: u256,
}

// 在函数中触发事件
world.emit_event(@TokenTransferred { from, to, amount });

Deploying Token Contracts

部署代币合约

Token contracts are deployed as external contracts alongside your Dojo world. See
dojo-deploy
skill for general deployment workflow.
代币合约作为外部合约与Dojo世界一同部署。 请参考
dojo-deploy
技能了解通用部署流程。

Add to Scarb.toml

添加至Scarb.toml

toml
[[target.starknet-contract]]
build-external-contracts = [
    "dojo::world::world_contract::world",
    "tokens::models::m_ERC20Token"
]
toml
[[target.starknet-contract]]
build-external-contracts = [
    "dojo::world::world_contract::world",
    "tokens::models::m_ERC20Token"
]

Configure in Profile

在配置文件中设置

In
dojo_dev.toml
, define the token as an external contract:
toml
[[external_contracts]]
contract_name = "ERC20Token"
instance_name = "GoldToken"
salt = "1"
constructor_data = [
    "str:Gold Coin",                # Token name
    "sstr:GOLD",                    # Symbol
    "u256:1000000000000000000",     # Total supply (1e18)
    "0x1234567890abcdef..."         # Owner address
]
Add more
[[external_contracts]]
blocks for additional tokens. Deploy with
sozo build && sozo migrate
. Note the contract addresses from the output — you need them for Torii.
dojo_dev.toml
中,将代币定义为外部合约:
toml
[[external_contracts]]
contract_name = "ERC20Token"
instance_name = "GoldToken"
salt = "1"
constructor_data = [
    "str:Gold Coin",                # 代币名称
    "sstr:GOLD",                    # 符号
    "u256:1000000000000000000",     # 总供应量(1e18)
    "0x1234567890abcdef..."         # 所有者地址
]
添加更多
[[external_contracts]]
块以部署更多代币。 使用
sozo build && sozo migrate
进行部署。 注意输出中的合约地址——你需要用它们配置Torii。

Indexing Tokens with Torii

使用Torii索引代币

Torii indexes ERC token contracts separately from Dojo world state. You must explicitly tell Torii which contracts to watch. See
dojo-indexer
skill for general Torii configuration.
Torii会将ERC代币合约与Dojo世界状态分开索引。 你必须明确告知Torii需要监控哪些合约。 请参考
dojo-indexer
技能了解完整的Torii配置。

Configuration

配置

Add token contracts to
[indexing]
using the
ERC20:
or
ERC721:
prefix:
toml
undefined
使用
ERC20:
ERC721:
前缀将代币合约添加至
[indexing]
部分:
toml
undefined

torii.toml

torii.toml

[indexing] contracts = [ "ERC20:0xYOUR_GOLD_TOKEN_ADDRESS", "ERC721:0xYOUR_WEAPON_NFT_ADDRESS", ]

Or via CLI:

```bash
torii --world 0xYOUR_WORLD \
  --indexing.contracts "ERC20:0xGOLD_TOKEN" \
  --indexing.contracts "ERC721:0xWEAPON_NFT"
You can also index well-known tokens on the network:
toml
[indexing]
contracts = [
    "ERC20:0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", # ETH
    "ERC20:0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", # STRK
]
[indexing] contracts = [ "ERC20:0xYOUR_GOLD_TOKEN_ADDRESS", "ERC721:0xYOUR_WEAPON_NFT_ADDRESS", ]

或通过CLI:

```bash
torii --world 0xYOUR_WORLD \
  --indexing.contracts "ERC20:0xGOLD_TOKEN" \
  --indexing.contracts "ERC721:0xWEAPON_NFT"
你也可以索引网络上的知名代币:
toml
[indexing]
contracts = [
    "ERC20:0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", # ETH
    "ERC20:0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", # STRK
]

Querying Token Data

查询代币数据

Once indexed, three database tables become available:
  • tokens
    — metadata (name, symbol, decimals)
  • balances
    — per-account balances
  • erc_transfers
    — transfer history
索引完成后,将有三个数据库表可用:
  • tokens
    —— 元数据(名称、符号、小数位数)
  • balances
    —— 账户余额
  • erc_transfers
    —— 转账历史

SQL

SQL

sql
SELECT * FROM tokens;
SELECT * FROM balances WHERE account_address = '0xPLAYER';
SELECT * FROM erc_transfers ORDER BY rowid DESC LIMIT 20;
sql
SELECT * FROM tokens;
SELECT * FROM balances WHERE account_address = '0xPLAYER';
SELECT * FROM erc_transfers ORDER BY rowid DESC LIMIT 20;

JavaScript SDK

JavaScript SDK

typescript
import { useTokens } from "@dojoengine/sdk/react";

function TokenBalance({ address }: { address: string }) {
    const { tokens, getBalance, toDecimal } = useTokens({
        accountAddresses: [address],
    });

    return (
        <div>
            {tokens.map((token, idx) => (
                <div key={idx}>
                    {token.symbol}: {toDecimal(token, getBalance(token))}
                </div>
            ))}
        </div>
    );
}
typescript
import { useTokens } from "@dojoengine/sdk/react";

function TokenBalance({ address }: { address: string }) {
    const { tokens, getBalance, toDecimal } = useTokens({
        accountAddresses: [address],
    });

    return (
        <div>
            {tokens.map((token, idx) => (
                <div key={idx}>
                    {token.symbol}: {toDecimal(token, getBalance(token))}
                </div>
            ))}
        </div>
    );
}

Troubleshooting

故障排除

"Empty tokens/balances tables"

"Empty tokens/balances tables"

  • Verify the contract address matches what was deployed
  • Check the prefix is correct (
    ERC20:
    vs
    ERC721:
    )
  • Ensure the contract implements standard ERC Transfer events
  • 验证合约地址与部署的地址一致
  • 检查前缀是否正确(
    ERC20:
    vs
    ERC721:
  • 确保合约实现了标准的ERC Transfer事件

"Token not showing in Torii"

"Token not showing in Torii"

  • Restart Torii after adding new contracts
  • Check Torii logs for indexing errors
  • 添加新合约后重启Torii
  • 查看Torii日志中的索引错误

"Balance shows 0"

"Balance shows 0"

  • Tokens are indexed from transfer events, not storage reads
  • Mint or transfer tokens to generate events Torii can index
  • 代币通过转账事件索引,而非直接读取存储
  • 进行代币铸造或转账操作,生成Torii可索引的事件

Related Skills

相关技能

  • dojo-model: Token models extend Dojo models
  • dojo-system: Token logic in systems
  • dojo-test: Test token operations
  • dojo-deploy: General world deployment workflow
  • dojo-indexer: Full Torii configuration and queries
  • dojo-client: Client SDK integration
  • dojo-model: Token models extend Dojo models
  • dojo-system: Token logic in systems
  • dojo-test: Test token operations
  • dojo-deploy: General world deployment workflow
  • dojo-indexer: Full Torii configuration and queries
  • dojo-client: Client SDK integration