由於 SUI 的合約機制跟 EVM 所使用的技術概念和機制都不同,這篇文章將會介紹如何在 SUI Chain 上發行代幣。

關於 SUI 本身的技術與介紹可以參考 Messari深度報告:Sui 技術優勢在哪?撐出 L1 公鏈新天地 或是 SUI 的官網都有細節的技術介紹,比較特別的是在 SUI 上面都是以 object 作為一個單位與中心進行交互,文中有一段很簡單的介紹:

以物件為中心的資料模型

與其他分散式帳本相區別的關鍵特性是 Sui 的以物件為中心的資料模型。大多數智能合約平臺,如以太坊、Solana 和 Aptos,使用帳戶來追蹤區塊鏈的狀態,其中帳戶是儲存使用者餘額的資料結構。其他平臺如比特幣和 Cardano 使用未消費交易輸出(UTXO)來記錄區塊鏈的狀態,也就是說,UTXO 代表了在交易執行後剩餘的資產數量。

Sui 將這兩種方法結合成一種混合模型,其中其歷史儲存在具有全域性唯一 ID 的物件中。物件還包含元資料,用於確定不同物件的特性,如所有權和交易歷史(部分來源於物件隨機數值,也稱為版本號)。Sui 的以物件為中心資料模型意味著全域性狀態只是所有 Sui 物件的集合。從結構上講,這採用了有向無環圖(DAG)的形式,其中物件對應於頂點,交易對應於邊,稱為 「活動物件」 的物件對應於沒有出邊的頂點。

在 Sui 中,所有交易都將物件作為輸入,並生成新的或修改後的物件作為輸出。每個物件都包含產生它的最後一筆交易的hash值。可用作輸入的物件稱為 「活動」 物件。因此,通過觀察所有活動物件,可以確定全域性狀態。

技術實作

接下來將會介紹如何在 SUI Chain 上發行代幣(Token),Sui wallet 與如何透過 faucet 領取 SUI 這邊就不多作介紹。

這邊用 devnet 作為測試:

$ cargo install --locked --git https://github.com/MystenLabs/sui.git --branch devnet sui

VSCode 整合

https://docs.sui.io/build/install#integrated-development-environment

$ cargo install --git https://github.com/move-language/move move-analyzer --branch sui-move --features "address32"

模塊

開啟模塊

$ sui move new learnsui

建立 move 合約

$ cd learnsui
$ touch /sources/yishconin.move

實作官方的 coin module: https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/coin.md#module-0x2coin

https://examples.sui.io/samples/coin.html

// This is an example module that creates a new cryptocurrency called YISHCOIN. It uses the Sui coin 
// standard to do so. This module code was inspired by the Sui Move by Example book 
// (https://examples.sui.io/samples/coin.html)
// 
// coin module: https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/coin.md#module-0x2coin
module learnsui::yishcoin {
    
    use std::option;
    use sui::coin;                          // https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/coin.md
    use sui::transfer;                      // https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/transfer.md
    use sui::url::{Self, Url};              // https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/url.md
    use sui::tx_context::{Self, TxContext}; // https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/tx_context.md

    struct YISHCOIN has drop {}

    /// Module initializer is called once on module publish. A treasury cap is sent to the 
    /// publisher, who then controls minting and burning
    //
    // coin::create_currency(): https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/coin.md#function-create_currency
    // transfer::public_freeze_object(): https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/transfer.md#function-public_freeze_object
    // transfer::public_transfer(): https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/transfer.md#function-public_transfer
    fun init( witness: YISHCOIN, ctx: &mut TxContext) {
        // Function interface: public fun create_currency<T: drop>(witness: T, decimals: u8, symbol: vector<u8>, name: vector<u8>, description: vector<u8>, icon_url: option::Option<url::Url>, ctx: &mut tx_context::TxContext): (coin::TreasuryCap<T>, coin::CoinMetadata<T>)
        let (treasuryCap, metadata) = coin::create_currency(
            /*witnes=*/witness, 
            /*decimals=*/6, 
            /*symbol=*/b"YISHCOIN", 
            /*name=*/b"Yish Coin", 
            /*description=*/b"This is about Yish Coin on Sui move", 
            /*icon_url=*/option::some<Url>(url::new_unsafe_from_bytes(b"https://yish.dev/512.png")), 
            /*ctx=*/ctx
        );
        
        // Freezes the object. Freezing the object means that the  object: 
        // - Is immutable
        // - Cannot be transferred
        //
        // Note: transfer::freeze_object() cannot be used since CoinMetadata is defined in another 
        //       module
        transfer::public_freeze_object(metadata);
        
        // Send the TreasuryCap object to the publisher of the module
        //
        // Note: transfer::transfer() cannot be used since TreasuryCap is defined in another module
        transfer::public_transfer(treasuryCap, tx_context::sender(ctx))
    }

    // This function is an example of how internal_mint_coin() can be used. 
    // 
    // Note that there is coin::mint_and_transfer but this examples shows how 
    // transfer::public_transfer works
    // 
    // coin::mint_and_transfer(): https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/coin.md#function-mint_and_transfer
    // transfer::public_transfer(): https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/transfer.md#function-public_transfer
    public entry fun mint
    (
        cap: &mut coin::TreasuryCap<learnsui::yishcoin::YISHCOIN>, 
        recipient: address,
        value: u64, 
        ctx: &mut tx_context::TxContext
    )
    {
        // mint the new coin with the given value
        let new_coin = internal_mint_coin(cap, value, ctx);

        // transfer the new coin to the recipient
        transfer::public_transfer(new_coin, recipient);
    }

    // This function is an example of how internal_burn_coin() can be used.
    public entry fun burn
    (
        cap: &mut coin::TreasuryCap<learnsui::yishcoin::YISHCOIN>, 
        coin: coin::Coin<learnsui::yishcoin::YISHCOIN>
    )
    {
        // Burn the coin 
        // 
        // Note: internal_burn_coin returns a u64 but it can be ignored since u64 has drop
        internal_burn_coin(cap, coin);
    }
    
    // This is the internal mint function. This function uses the Coin::mint function to create and 
    // return a new Coin object containing a balance of the given value
    //
    // coin::mint(): https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/coin.md#function-mint
    fun internal_mint_coin
    (
        cap: &mut coin::TreasuryCap<learnsui::yishcoin::YISHCOIN>, 
        value: u64, 
        ctx: &mut tx_context::TxContext
    ): coin::Coin<learnsui::yishcoin::YISHCOIN>
    { 
        coin::mint(cap, value, ctx)
    } 

    // This is the internal burn function. This function uses the Coin::burn function to take a coin
    // and destroy it. The function returns the value of the coin that was destroyed.
    //
    // coin::burn(): https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/coin.md#function-burn
    fun internal_burn_coin
    (
        cap: &mut coin::TreasuryCap<learnsui::yishcoin::YISHCOIN>, 
        coin: coin::Coin<learnsui::yishcoin::YISHCOIN>
    ): u64
    {
        coin::burn(cap, coin)
    }
}

測試合約

$ sui move test

構建合約

$ sui move build

到這邊合約的部分已經完成,接下來進入配置 client 與地址配置、部屬到鏈上運行環節。

生成/佈署

$ sui client addresses
Config file ["/Users/yish/.sui/sui_config/client.yaml"] doesn't exist, do you want to connect to a Sui Full node server [y/N]?y
Sui Full node server URL (Defaults to Sui Devnet if not specified) :
# 這邊選擇 ed25519 方便後續新增到 sui wallet 查看與轉移
# 要把相關註記詞和內容記錄下來
Select key scheme to generate keypair (0 for ed25519, 1 for secp256k1, 2: for secp256r1):
0
Generated new keypair for address with scheme "ed25519" [0xqxokzcw3r3nur3st6gmmqf7kx5jcdokqvsnd7unjvdtxx6vuevnrqm5rtvn5urok]
Secret Recovery Phrase : [hello john doe ...]
╭───────────────┬──────────────────────────────────────────────────────────────────────────╮
│ activeAddress │  0xqxokzcw3r3nur3st6gmmqf7kx5jcdokqvsnd7unjvdtxx6vuevnrqm5rtvn5urok      │
│ addresses     │ ╭──────────────────────────────────────────────────────────────────────╮ │
│               │ │  0xqxokzcw3r3nur3st6gmmqf7kx5jcdokqvsnd7unjvdtxx6vuevnrqm5rtvn5urok  │ │
│               │ ╰──────────────────────────────────────────────────────────────────────╯ │
╰───────────────┴──────────────────────────────────────────────────────────────────────────╯

相關對應 client 指令都可以在裡面找到:

$ sui client -h

取得需要的 gas fee:

curl --location --request POST 'https://faucet.devnet.sui.io/gas' \
--header 'Content-Type: application/json' \
--data-raw '{"FixedAmountRequest":{"recipient":"0xqxokzcw3r3nur3st6gmmqf7kx5jcdokqvsnd7unjvdtxx6vuevnrqm5rtvn5urok"}}'

確保有足夠的 gas fee,這邊會注意到 gasCoinId 跟原本地址不同,等等會需要用到這個 gasCoinId:

sui client gas
╭────────────────────────────────────────────────────────────────────┬─────────────╮
│ gasCoinId                                                          │ gasBalance  │
├────────────────────────────────────────────────────────────────────┼─────────────┤
│ 0xephada5juljryk0xou3hlpx13trcs2hlbe9abzoz9k4zz3zhiivohhm0byhqcccf │ 10000000000╰────────────────────────────────────────────────────────────────────┴─────────────╯

發布合約到鏈上,這邊可以看到要填入 gasCoinId 並且配置 gas budget,devnet 配置最高即可:

$ sui client publish learnsui --gas 0xephada5juljryk0xou3hlpx13trcs2hlbe9abzoz9k4zz3zhiivohhm0byhqcccf --gas-budget 100000000 --skip-dependency-verification

會出現很多細節資訊以及對應的內容,這邊都先記錄下來。

接著列出 object 清單,確保 Coin / TreasuryCap / UpgradeCap 已經存在於合約中:

$ sui client objects
╭───────────────────────────────────────────────────────────────────────────────────────╮
│ ╭────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ objectId   │  0xvrt0b5onpepysv4jwh0hfqdc30knavvuqxrgjubtt5dep1bhe416gfre0p78scpl  │ │
│ │ version    │  18                                                                  │ │
│ │ digest     │  77alFykMvAlHGwwU99y8peqdlxERw7o5F14OQluYrl5t                        │ │
│ │ objectType │  0x0000..0002::coin::Coin                                            │ │
│ ╰────────────┴──────────────────────────────────────────────────────────────────────╯ │
│ ╭────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ objectId   │  0xka8ngiqfod8twbugwppayfyhv2uq3rl9tvjzkx7rshm81zouaxyzq4cdjm6fh9x8  │ │
│ │ version    │  18                                                                  │ │
│ │ digest     │  u8lLD9Box1R1qrmAKNutW1vLGtswgT1sAPv1cXBcAilZ                        │ │
│ │ objectType │  0x0000..0002::coin::TreasuryCap                                     │ │
│ ╰────────────┴──────────────────────────────────────────────────────────────────────╯ │
│ ╭────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ objectId   │  0xqpgzictudtmnhtybuduckaeaa0kro5d1zjzv6dgvycjneg5dhgm6ezvnqcwusyew  │ │
│ │ version    │  18                                                                  │ │
│ │ digest     │  bYCnj95JH4bGqwWpoTkQXQCwGQFV8VKBialCd2OWtE7J                        │ │
│ │ objectType │  0x0000..0002::package::UpgradeCap                                   │ │
│ ╰────────────┴──────────────────────────────────────────────────────────────────────╯ │
╰───────────────────────────────────────────────────────────────────────────────────────╯

接著在 sui wallet 登入剛剛生成的 address,查找 transaction 是否有成功,並且 view on explorer 查看一下:

https://suiexplorer.com/object/0x4b515033d783a2089d853b834e9e06960bab0280eec9adfb537cc0baa2cd5f37?deviceId=d924976c-e5b7-4f94-8283-16f92bf9d81d&network=devnet

查找 TreasuryCap 相關內容等等 mint and transfer 需要用到:

$ sui client object 0xka8ngiqfod8twbugwppayfyhv2uq3rl9tvjzkx7rshm81zouaxyzq4cdjm6fh9x8
╭───────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ objectId      │  0x7cbf02bedf3dbdafa9c4f8cedd6adba4546fda6544d03be7d3f23121936177b4                                                                                                    │
│ version       │  20│ digest        │  86xumhjUgcyo56wDMFmiHvQMY8VJ5VeHLMjP5NZ6Gr3T                                                                                                                          │
│ objType       │  0x2::coin::TreasuryCap<0x6bc1ddbad255ec35e7a73850b729275ef591a8528e4ab0c67761fcd4fcfc314f::yishcoin::YISHCOIN>                                                        │
│ ownerType     │  AddressOwner                                                                                                                                                          │
│ prevTx        │  FZrCVJJvk4ci3xEjaFMa6Bfp4wr3WqBAStZtYpkdhKg7                                                                                                                          │
│ storageRebate │  1763200│ content       │ ╭───────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │
│               │ │ dataType          │  moveObject                                                                                                                                    │ │
│               │ │ type              │  0x2::coin::TreasuryCap<0xgeqngwm1empj8kfozwcqeunkg98yrgr4ty0a0ewpcqy6o7gcwmhpdvycjlsauhij::yishcoin::YISHCOIN>                                │ │
│               │ │ hasPublicTransfer │  true                                                                                                                                          │ │
│               │ │ fields            │ ╭──────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ │
│               │ │                   │ │ id           │ ╭────┬──────────────────────────────────────────────────────────────────────╮                                               │ │ │
│               │ │                   │ │              │ │ id │  0x7cbf02bedf3dbdafa9c4f8chsyjiola1246fda6544d03be7d3f23121936177b4  │                                               │ │ │
│               │ │                   │ │              │ ╰────┴──────────────────────────────────────────────────────────────────────╯                                               │ │ │
│               │ │                   │ │ total_supply │ ╭────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ │ │
│               │ │                   │ │              │ │ type   │  0x2::balance::Supply<0xgeqngwm1empj8kfozwcqeunkg98yrgr4ty0a0ewpcqy6o7gcwmhpdvycjlsauhij::yishcoin::YISHCOIN>  │ │ │ │
│               │ │                   │ │              │ │ fields │ ╭───────┬─────────────────╮                                                                                    │ │ │ │
│               │ │                   │ │              │ │        │ │ value │  8958888888888  │                                                                                    │ │ │ │
│               │ │                   │ │              │ │        │ ╰───────┴─────────────────╯                                                                                    │ │ │ │
│               │ │                   │ │              │ ╰────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ │ │
│               │ │                   │ ╰──────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ │
│               │ ╰───────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
╰───────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

接著到合約內部開始進行 mint 和 transfer token to someone:

可以參考官方 Coin module implementation: https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/coin.md#function-mint

public fun mint<T>(cap: &mut coin::TreasuryCap<T>, value: u64, ctx: &mut tx_context::TxContext): coin::Coin<T>
  • Type0: 填入 0xgeqngwm1empj8kfozwcqeunkg98yrgr4ty0a0ewpcqy6o7gcwmhpdvycjlsauhij::yishcoin::YISHCOIN 指定 TreasuryCap type
  • Arg0: 填入 TreasuryCap objectId: 0xka8ngiqfod8twbugwppayfyhv2uq3rl9tvjzkx7rshm81zouaxyzq4cdjm6fh9x8
  • Arg1: 填入 mint 數量:800000000
  • Arg2: 填入 recipient 的 address:0x0028f0b1e2a045163ceaee65ce54b54b820e610cf99463cd909221360f985eda

執行後會顯示如下:

接著就可以到鏈上查找交易數據: https://suiscan.xyz/devnet/coin/0x6bc1ddbad255ec35e7a73850b729275ef591a8528e4ab0c67761fcd4fcfc314f::yishcoin::YISHCOIN/txs

後記

至此整個在 SUI 上發行 token 的流程就完成了,當然還有很多細節需要再去深入理解,作為一條新起的 L1,技術上來說確實是很超前的,也歸功於過去 Diem 以及創始人的技術積累,但就如同其他公鏈一樣,其敘事能力跟技術是要同時一起成長的,未來將會持續觀察。

參考