sui cli 详细讲解

Usage: sui <COMMAND>

Commands:
  start                  Start sui network
  network
  genesis                Bootstrap and initialize a new sui network
  genesis-ceremony
  keytool                Sui keystore tool
  console                Start Sui interactive console
  client                 Client for interacting with the Sui network
  validator              A tool for validators and validator candidates
  move                   Tool to build and test Move applications
  bridge-committee-init  Command to initialize the bridge committee, usually used when running local bridge cluster
  fire-drill             Tool for Fire Drill
  help                   Print this message or the help of the given subcommand(s)

Options:
  -h, --help     Print help
  -V, --version  Print version

Move

1 命令说明

sui move的子命令集是用于构建和测试Move合约的,以下将进行分类并逐一介绍命令的使用。

可通过help/-h获取完整的子命令:

$ sui move -h
Tool to build and test Move applications

Usage: sui move [OPTIONS] <COMMAND>

Commands:
  build
  coverage        Inspect test coverage for this package. A previous test run with the
                      `--coverage` flag must have previously been run
  disassemble
  manage-package  Record addresses (Object IDs) for where this package is published on chain (this
                      command sets variables in Move.lock)
  migrate         Migrate to Move 2024 for the package at `path`. If no path is provided defaults
                      to current directory
  new             Create a new Move package with name `name` at `path`. If `path` is not provided
                      the package will be created in the directory `name`
  test            Run Move unit tests in this package

2 命令分类

根据命令实现功能分为以下几类:

2.1 项目类

子命令功能说明
new创建Move项目
build编译并构建Move项目

2.2 测试类

子命令功能说明
test运行Move项目的单元测试
coverage检查测试覆盖率

2.3 迁移类

子命令功能说明
migrate老版合约语法迁移到Move 2024新版语法

2.4 其它类

子命令功能说明
disassemble反汇编Move字节码
manage-package记录包的发布地址

3 命令详解

3.1 项目类

3.1.1 new: 创建Move项目

(1) 命令说明

执行该命令可以更具指定名称创建一个新的Move项目。

Create a new Move package with name `name` at `path`. If `path` is not provided the package will be
created in the directory `name`

Usage: sui move new [OPTIONS] <NAME>

Arguments:
  <NAME>  The name of the package to be created
(2) 命令使用

命令成功执行后,将会搭建起基本的项目工程。

$ sui move new simple_bank

$ tree simple_bank/
simple_bank/
├── Move.toml
├── sources
│   └── simple_bank.move
└── tests
    └── simple_bank_tests.move

3.1.2 build: 编译并构建Move项目

(1) 命令说明
Usage: sui move build [OPTIONS]
(2) 命令使用
  • 执行编译命令
$ sui move build
UPDATING GIT DEPENDENCY https://github.com/MystenLabs/sui.git
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING simple_bank
  • 查看编译后输出的结果

可以看到生成的合约字节码文件:simple_bank.mv

$ tree -L 3 build/
build/
├── locks
└── simple_bank
    ├── BuildInfo.yaml
    ├── bytecode_modules
    │   ├── dependencies
    │   └── simple_bank.mv
    ├── source_maps
    │   ├── dependencies
    │   └── simple_bank.mvsm
    └── sources
        ├── dependencies
        └── simple_bank.move

3.2 测试类

3.2.1 test: 运行Move项目的单元测试

(1) 命令说明

执行该命令可以运行当前Move项目的单元测试。

Run Move unit tests in this package

Usage: sui move test [OPTIONS] [filter]

Arguments:
  [filter]  An optional filter string to determine which unit tests to run. A unit test will be run
            only if it contains this string in its fully qualified
            (<addr>::<module_name>::<fn_name>) name
(2) 命令使用
  • 跑全量单元测试
$ sui move test

INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING sui_marketplace
Running Move unit tests
[ PASS    ] movefans::sui_marketplace_tests::test_add_item
[ PASS    ] movefans::sui_marketplace_tests::test_add_item_failure
[ PASS    ] movefans::sui_marketplace_tests::test_create_shop
[ PASS    ] movefans::sui_marketplace_tests::test_purchase_item
[ PASS    ] movefans::sui_marketplace_tests::test_purchase_item_failure
[ PASS    ] movefans::sui_marketplace_tests::test_unlist_item
[ PASS    ] movefans::sui_marketplace_tests::test_withdraw_from_shop
Test result: OK. Total tests: 7; passed: 7; failed: 0
  • 跑指定单元测试

可以根据单元测试名称,跑指定单元测试

$ sui move test test_add_item

INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING sui_marketplace
Running Move unit tests
[ PASS    ] movefans::sui_marketplace_tests::test_add_item
[ PASS    ] movefans::sui_marketplace_tests::test_add_item_failure
Test result: OK. Total tests: 2; passed: 2; failed: 0

3.2.2 coverage: 查看测试覆盖率

(1) 命令说明

执行该命令可以分析当前项目的测试覆盖率。在之前,需要先运行sui test --coverage命令,生成测试覆盖率报告。

注:需要使用debug模式Sui CLI,方可使用--coverage参数,否则会提示:

The --coverage flag is currently supported only in debug builds. Please build the Sui CLI from source in debug mode.
Inspect test coverage for this package. A previous test run with the `--coverage` flag
must have previously been run

Usage: sui move coverage [OPTIONS] <COMMAND>

Commands:
  summary   Display a coverage summary for all modules in this package
  source    Display coverage information about the module against source code
  bytecode  Display coverage information about the module against disassembled bytecode
(2) 命令使用
  • 执行测试命令,开启覆盖率分析
$ /data/sui/target/debug/sui move test --coverage
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING sui_marketplace
Running Move unit tests
[ PASS    ] movefans::sui_marketplace_tests::test_add_item
[ PASS    ] movefans::sui_marketplace_tests::test_add_item_failure
[ PASS    ] movefans::sui_marketplace_tests::test_create_shop
[ PASS    ] movefans::sui_marketplace_tests::test_purchase_item
[ PASS    ] movefans::sui_marketplace_tests::test_purchase_item_failure
[ PASS    ] movefans::sui_marketplace_tests::test_unlist_item
[ PASS    ] movefans::sui_marketplace_tests::test_withdraw_from_shop
Test result: OK. Total tests: 7; passed: 7; failed: 0
  • 命令成功执行,将会在当前目录生成以下文件
.trace
.coverage_map.mvcov
  • 查看测试覆盖率汇总信息

根据输出内容可见该项目的单测覆盖率为:76.40%

$ sui move coverage summary

+-------------------------+
| Move Coverage Summary   |
+-------------------------+
Module 0000000000000000000000000000000000000000000000000000000000000000::sui_marketplace
>>> % Module coverage: 76.40
+-------------------------+
| % Move Coverage: 76.40  |
+-------------------------+
  • 查看源代码层面模块单测覆盖率
$ sui move coverage source --module sui_marketplace

将会输出如下信息,显式红色的代码片段表示未被单测覆盖

image-20240721233245812

  • 查看字节码层面模块单测覆盖率
$ sui move coverage bytecode --module  sui_marketplace

image-20240721233304619

3.3 迁移类

3.3.1 migrate: 老版合约语法迁移到Move 2024新版语法

SUI Move2024年迎来重大更新,引入了许多新功能,涵盖新特性,例如:方法语法(method syntax位置域(positional fields循环标签(loop labels等,以及不兼容更新,例如:数据类型可见性(Datatype visibility requirements)可变性要求(Mutability requirements) 等。这些更新为Move编程语言引入了新的定义数据和调用函数的方式等,使得在Sui上构建应用程序更加高效灵活,也为未来要推出的新功能铺平道路。

如何快速将历史传统Move合约迁移到Sui Move 2024新版合约语法,使用该命令即可。

(1) 命令说明

执行该命令可以将历史传统Move合约迁移到Sui Move 2024新版合约语法。

Migrate to Move 2024 for the package at `path`. If no path is provided defaults to current directory

Usage: sui move migrate [OPTIONS]
(2) 命令使用

命令成功执行后,终端会显示要进行的更改的合约差异,如果接受更改,会自动将现存 历史版本(legacy 的合约,迁移成 新版合约(2024.beta 代码,并会更新Move.toml文件,同时也会生成一个migration.patch文件,将变更差异记录在其中。 更多介绍可参考: https://blog.sui.io/move-2024-migration-guide/ https://learnblockchain.cn/article/7824

$ sui move migrate
UPDATING GIT DEPENDENCY https://github.com/MystenLabs/sui.git
Package toml does not specify an edition. As of 2024, Move requires all packages to define a language edition.

Please select one of the following editions:

1) 2024.beta
2) legacy

Selection (default=1): 1

Would you like the Move compiler to migrate your code to Move 2024? (Y/n) y

Generated changes . . .
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING simple_bank

The following changes will be made.
============================================================

--- ./sources/simple_bank.move
+++ ./sources/simple_bank.move
@@ -15 +15 @@
-    struct SimpleBank has key {
+    public struct SimpleBank has key {
@@ -20 +20 @@
-    struct EventDeposit has copy, drop {
+    public struct EventDeposit has copy, drop {
@@ -26 +26 @@
-    struct EventWithdraw has copy, drop {
+    public struct EventWithdraw has copy, drop {


============================================================
Apply changes? (Y/n) y

Updating "./sources/simple_bank.move" . . .

Changes complete
Wrote patchfile out to: ./migration.patch

Recorded edition in 'Move.toml'

3.4 其它类

3.4.1 disassemble: 反汇编Move字节码

(1) 命令说明

执行该命令可以反汇编Move二进制字节码文件为汇编代码。

Usage: sui move disassemble [OPTIONS] <module_path>

Arguments:
  <module_path>  Path to a .mv file to disassemble

(2) 命令使用
$ sui move disassemble ./build/simple_bank/bytecode_modules/simple_bank.mv
// Move bytecode v6
module 0.simple_bank {
use 0000000000000000000000000000000000000000000000000000000000000002::balance;
use 0000000000000000000000000000000000000000000000000000000000000002::coin;
use 0000000000000000000000000000000000000000000000000000000000000002::event;
use 0000000000000000000000000000000000000000000000000000000000000002::object;
use 0000000000000000000000000000000000000000000000000000000000000002::sui;
use 0000000000000000000000000000000000000000000000000000000000000002::transfer;
use 0000000000000000000000000000000000000000000000000000000000000002::tx_context;
use 0000000000000000000000000000000000000000000000000000000000000002::vec_map;


struct SimpleBank has key {
        id: UID,
        balances: VecMap<address, Balance<SUI>>
}
struct EventDeposit has copy, drop {
        sender: address,
        amount: u64,
        balance: u64
}
struct EventWithdraw has copy, drop {
        sender: address,
        amount: u64,
        balance: u64
}

init(ctx#0#0: &mut TxContext) {
B0:
        0: MoveLoc[0](ctx#0#0: &mut TxContext)
        1: Call object::new(&mut TxContext): UID
        2: Call vec_map::empty<address, Balance<SUI>>(): VecMap<address, Balance<SUI>>
        3: Pack[0](SimpleBank)
        4: Call transfer::share_object<SimpleBank>(SimpleBank)
        5: Ret

}
entry public deposit(simpleBank#0#0: &mut SimpleBank, amount#0#0: &mut Coin<SUI>, ctx#0#0: &mut TxContext) {
L0:     value#1#0: u64
B0:
        0: CopyLoc[1](amount#0#0: &mut Coin<SUI>)
        1: FreezeRef
        2: Call coin::value<SUI>(&Coin<SUI>): u64
        3: LdU64(0)
        4: Gt
        5: BrFalse(7)
B1:
        6: Branch(15)
B2:
        7: MoveLoc[0](simpleBank#0#0: &mut SimpleBank)
        8: Pop
        ......
        51: MoveLoc[6](value#1#0: u64)
        52: MoveLoc[5](totalBalance#1#0: u64)
        53: Pack[1](EventDeposit)
        54: Call event::emit<EventDeposit>(EventDeposit)
        55: Ret

}
entry public withdraw(simpleBank#0#0: &mut SimpleBank, amount#0#0: u64, ctx#0#0: &mut TxContext) {
B0:
        0: CopyLoc[1](amount#0#0: u64)
        1: LdU64(0)
        2: Gt
        3: BrFalse(5)
B1:
        4: Branch(11)
B2:
        5: MoveLoc[0](simpleBank#0#0: &mut SimpleBank)
        6: Pop
        7: MoveLoc[2](ctx#0#0: &mut TxContext)
        8: Pop
        ......
        57: FreezeRef
        58: Call balance::value<SUI>(&Balance<SUI>): u64
        59: Pack[2](EventWithdraw)
        60: Call event::emit<EventWithdraw>(EventWithdraw)
        61: Ret

}

Constants [
        0 => u64: 1
        1 => u64: 2
        2 => u64: 3
]
}

3.4.2 manage-package: 记录包的发布地址

(1) 命令说明

执行该命令可以手工区记录包的发布地址。

参数说明:

  • --environment: 指定当前链网络环境,可通过命令sui client active-env查看
  • --network-id: 指定当前链网络ID,可通过命令sui client chain-identifier查看
  • --original-id: 指定包首次发布的地址,若未升级过包,则该地址与Move.toml中的published-at地址相同
  • --latest-id: 指定当前包的最新地址,若未升级过包,则与--original-id相同;若升级了包,这与当前在Move.toml中的published-at地址相同
  • --version-number: 指定当前包的版本号
Record addresses (Object IDs) for where this package is published on chain (this command sets
variables in Move.lock)

Usage: sui move manage-package [OPTIONS] --environment <ENVIRONMENT> --network-id <CHAIN_ID> --original-id <ORIGINAL_ID> --latest-id <LATEST_ID> --version-number <VERSION_NUMBER>
(2) 命令使用
sui move manage-package --environment "$(sui client active-env)" \
                  --network-id "$(sui client chain-identifier)" \
                  --original-id 'ORIGINAL-ADDRESS' \
                  --latest-id 'LATEST-ADDRESS' \
                  --version-number 'VERSION-NUMBER'

Client

1 命令说明

sui client的子命令集是用于跟Sui网络交互相关的命令,命令非常丰富,以下将进行分类并逐一介绍命令的使用。

可通过help/-h获取完整的子命令:

$ sui client -h
Client for interacting with the Sui network

Usage: sui client [OPTIONS] [COMMAND]

Commands:
  active-address              Default address used for commands when none specified
  active-env                  Default environment used for commands when none specified
  addresses                   Obtain the Addresses managed by the client
  balance                     List the coin balance of an address
  call                        Call Move function
  chain-identifier            Query the chain identifier from the rpc endpoint
  dynamic-field               Query a dynamic field by its address
  envs                        List all Sui environments
  execute-signed-tx           Execute a Signed Transaction. This is useful when the user prefers to sign elsewhere and use this command to execute
  execute-combined-signed-tx  Execute a combined serialized SenderSignedData string
  faucet                      Request gas coin from faucet. By default, it will use the active
                                  address and the active network
  gas                         Obtain all gas objects owned by the address. An address' alias
                                  can be used instead of the address
  merge-coin                  Merge two coin objects into one coin
  new-address                 Generate new address and keypair with keypair scheme flag
                                  {ed25519 | secp256k1 | secp256r1} with optional derivation path,
                                  default to m/44'/784'/0'/0'/0' for ed25519 or m/54'/784'/0'/0/0
                                  for secp256k1 or m/74'/784'/0'/0/0 for secp256r1. Word length can
                                  be { word12 | word15 | word18 | word21 | word24} default to
                                  word12 if not specified
  new-env                     Add new Sui environment
  object                      Get object info
  objects                     Obtain all objects owned by the address. It also accepts an
                                  address by its alias
  pay                         Pay coins to recipients following specified amounts, with input
                                  coins. Length of recipients must be the same as that of amounts
  pay-all-sui                 Pay all residual SUI coins to the recipient with input coins,
                                  after deducting the gas cost. The input coins also include the
                                  coin for gas payment, so no extra gas coin is required
  pay-sui                     Pay SUI coins to recipients following following specified
                                  amounts, with input coins. Length of recipients must be the same
                                  as that of amounts. The input coins also include the coin for gas
                                  payment, so no extra gas coin is required
  ptb                         Run a PTB from the provided args
  publish                     Publish Move modules
  split-coin                  Split a coin object into multiple coins
  switch                      Switch active address and network(e.g., devnet, local rpc server)
  tx-block                    Get the effects of executing the given transaction block
  transfer                    Transfer object
  transfer-sui                Transfer SUI, and pay gas with the same SUI coin object. If
                                  amount is specified, only the amount is transferred; otherwise
                                  the entire object is transferred
  upgrade                     Upgrade Move modules
  verify-bytecode-meter       Run the bytecode verifier on the package
  verify-source               Verify local Move packages against on-chain packages, and
                                  optionally their dependencies
  profile-transaction         Profile the gas usage of a transaction. Unless an output filepath
                                  is not specified, outputs a file
                                  `gas_profile_{tx_digest}_{unix_timestamp}.json` which can be
                                  opened in a flamegraph tool such as speedscope
  replay-transaction          Replay a given transaction to view transaction effects. Set
                                  environment variable MOVE_VM_STEP=1 to debug
  replay-batch                Replay transactions listed in a file
  replay-checkpoint           Replay all transactions in a range of checkpoints
  help                        Print this message or the help of the given subcommand(s)

2 命令分类

根据命令实现功能分为以下几类:

2.1 链网络类

子命令功能说明
envs查看当前客户端所有配置链网络环境
active-env获取当前默认环境
new-env添加链网络环境
switch切换网络
chain-identifier获取当前链网络标识

2.2 地址类

子命令功能说明
addresses获取当前客户端管理的所有地址
active-address获取当前默认用户地址
new-address创建新地址
switch切换地址

2.3 对象类

子命令功能说明
objects获取地址拥有的所有对象
object获取对象信息
transfer转移对象
dynamic-field查询对象的动态域(dynamic field)

2.4 COIN类

子命令功能说明
balance获取地址代币余额
faucet领水
gas获取地址或别名的所有gas对象
merge-coin代币合并
split-coin代币拆分
transfer-sui转移SUI
pay支付COIN到指定地址
pay-sui支付SUI到指定地址
pay-all-sui转移所有SUI到指定地址

2.5 合约类

子命令功能说明
publish部署合约
call调用合约方法
upgrade升级合约

2.6 交易类

子命令功能说明
execute-signed-tx执行签名交易
execute-combined-signed-tx执行多签交易
tx-block获取指定交易快的执行影响(effects)

2.7 PTB类

子命令功能说明
ptb根据指定参数执行PTB

2.8 调优类

子命令功能说明
profile-transaction交易gas消耗分析
replay-transaction给定交易重放
replay-batch批量交易重放
replay-checkpoint重放指定一段检查点区间内的所有交易

3 命令详解

3.1 链网络类

3.1.1 envs: 查看当前客户端所有配置链网络环境

(1) 命令说明

执行该命令会查看当前客户端所有配置链网络环境,包括环境别名、环境地址、是否为活跃环境。 相关信息来自本地的配置文件: ~/.sui/sui_config/client.yaml

List all Sui environments
(2) 命令使用
$ sui client envs
╭──────────┬───────────────────────────────────────┬────────╮
│ alias    │ url                                   │ active │
├──────────┼───────────────────────────────────────┼────────┤
│ devnet   │ https://fullnode.devnet.sui.io:443    │        │
│ mainnet  │ https://sui-mainnet.nodeinfra.com:443 │ *      │
│ testnet  │ https://fullnode.testnet.sui.io:443   │        │
╰──────────┴───────────────────────────────────────┴────────╯

3.1.2 active-env: 获取当前默认环境

(1) 命令说明

执行该命令将获取当前客户端使用的链网络环境。

Default environment used for commands when none specified
(2) 命令使用
$ sui client active-env
mainnet

3.1.3 new-env: 添加链网络环境

(1) 命令说明

执行该命令将添加新的链网络环境。

Add new Sui environment
(2) 命令使用
  • 执行添加命令
$ sui client new-env --alias localnet --rpc http://127.0.0.1:9000
Added new Sui env [localnet] to config.
  • 在此查看链网络
$ sui client envs
╭──────────┬───────────────────────────────────────┬────────╮
│ alias    │ url                                   │ active │
├──────────┼───────────────────────────────────────┼────────┤
│ devnet   │ https://fullnode.devnet.sui.io:443    │        │
│ mainnet  │ https://sui-mainnet.nodeinfra.com:443 │ *      │
│ localnet │ http://127.0.0.1:9000                 │        │
│ testnet  │ https://fullnode.testnet.sui.io:443   │        │
╰──────────┴───────────────────────────────────────┴────────╯

可见localnet网络已经添加进去。

3.1.4 switch: 切换网络

(1) 命令说明

执行该命令使用不同的参数,可以切换地址和网络,这里我们使用--env命令进行网络切换。

Switch active address and network(e.g., devnet, local rpc server)

Usage: sui client switch [OPTIONS]

Options:
      --address <ADDRESS>  An address to be used as the active address for subsequent
                           commands. It accepts also the alias of the address
      --env <ENV>          The RPC server URL (e.g., local rpc server, devnet rpc
                           server, etc) to be used for subsequent commands
(2) 命令使用
$ sui client switch --env localnet
Active environment switched to [localnet]

3.1.5 chain-identifier: 获取当前链网络标识

(1) 命令说明

执行该命令将会获得当前链网络标识。

(2) 命令使用
$ sui client chain-identifier
35834a8a

3.2 地址类

3.2.1 addresses: 获取当前客户端管理的所有地址

(1) 命令说明

执行该命令将获取当前客户端管理的所有地址,包括地址别名和地址。 数据来源于本地文件~/.sui/sui_config/sui.aliases

注:本地文件~/.sui/sui_config/sui.aliases中存储的是地址base64编码的公钥,~/.sui/sui_config/sui.keystore存储的是地址私钥

Obtain the Addresses managed by the client
(2) 命令使用
$ sui client addresses --json
{
  "activeAddress": "0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a",
  "addresses": [
    [
      "jason",
      "0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a"
    ],
    [
      "yas",
      "0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19"
    ],
    [
      "yoy",
      "0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0"
    ]
  ]
}

3.2.2 active-address: 获取当前默认用户地址

(1) 命令说明

执行该命令将获取当前客户端默认使用的用户地址。

Default address used for commands when none specified
(2) 命令使用
$ sui client active-address
0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a

3.2.3 new-address: 创建新地址

(1) 命令说明

执行该命令将创建一个新的地址,并返回地址和助记词。

Generate new address and keypair with keypair scheme flag {ed25519 | secp256k1 | secp256r1} with optional derivation path, default to m/44'/784'/0'/0'/0' for ed25519 or m/54'/784'/0'/0/0 for secp256k1 or m/74'/784'/0'/0/0 for secp256r1. Word length can be { word12 | word15 | word18 | word21 | word24} default to word12 if not specified

Usage: sui client new-address [OPTIONS] <KEY_SCHEME> [ALIAS] [WORD_LENGTH] [DERIVATION_PATH]

Arguments:
  <KEY_SCHEME>
  [ALIAS]            The alias must start with a letter and can contain only letters, digits, hyphens (-), or underscores (_)
  [WORD_LENGTH]
  [DERIVATION_PATH]
(2) 命令使用
$ sui client new-address ed25519 js --json
Keys saved as Base64 with 33 bytes `flag || privkey` ($BASE64_STR).
        To see Bech32 format encoding, use `sui keytool export $SUI_ADDRESS` where
        $SUI_ADDRESS can be found with `sui keytool list`. Or use `sui keytool convert $BASE64_STR`.
{
  "alias": "js",
  "address": "0xe760ab0e273b4ec996abe76ce67e895e13064611cb4215d7358f2a63188e1a98",
  "keyScheme": "ED25519",
  "recoveryPhrase": "dismiss split donor onion hour heavy stand work detail issue room pink lion enroll"
}

3.2.4 switch: 切换地址

(1) 命令说明

执行该命令使用不同的参数,可以切换地址和网络,这里我们使用--address命令进行地址切换。

Switch active address and network(e.g., devnet, local rpc server)

Usage: sui client switch [OPTIONS]

Options:
      --address <ADDRESS>  An address to be used as the active address for subsequent
                           commands. It accepts also the alias of the address
      --env <ENV>          The RPC server URL (e.g., local rpc server, devnet rpc
                           server, etc) to be used for subsequent commands
(2) 命令使用
$ sui client switch --address js
Active address switched to 0xe760ab0e273b4ec996abe76ce67e895e13064611cb4215d7358f2a63188e1a98

3.3 对象类

3.3.1 objects: 获取地址拥有的所有对象

(1) 命令说明

执行该命令将获取地址拥有的所有对象。

Obtain all objects owned by the address. It also accepts an address by its alias
(2) 命令使用
# Usage: sui client objects [OPTIONS] [owner_address]

$ sui client objects --json
[
  {
    "data": {
      "objectId": "0xcc0b1b68febda7888b7681a70199ab291b38941ee965ed27af1b0bdf8ae79f76",
      "version": "25131978",
      "digest": "33Ny3AZ3q169QD2uvrso6QtfigyFqN8DccZ1cUJR7v2y",
      "type": "0x99bb55e386947db154dcb23b8f39191536c6332b659f3c6aa7596a0cd1f5c4bc::LuckyCafe::Card",
      "owner": {
        "AddressOwner": "0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a"
      },
      "previousTransaction": "4qYbEkvQWWshcgyg93NVtHt4hAMqnU9CMt86HJentZ4B",
      "storageRebate": "1535200",
      "content": {
        "dataType": "moveObject",
        "type": "0x99bb55e386947db154dcb23b8f39191536c6332b659f3c6aa7596a0cd1f5c4bc::LuckyCafe::Card",
        "hasPublicTransfer": true,
        "fields": {
          "cafe_id": "0x0463f197aa3c90a6c668089267aef6cff0ae3a50ed8144a7da84a49ece45f2e1",
          "id": {
            "id": "0xcc0b1b68febda7888b7681a70199ab291b38941ee965ed27af1b0bdf8ae79f76"
          }
        }
      }
    }
  },
......

3.3.2 object: 获取对象信息

(1) 命令说明

执行该命令将获取指定对象信息。

Get object info
(2) 命令使用
$ sui client object 0xcc0b1b68febda7888b7681a70199ab291b38941ee965ed27af1b0bdf8ae79f76 --json
{
  "objectId": "0xcc0b1b68febda7888b7681a70199ab291b38941ee965ed27af1b0bdf8ae79f76",
  "version": "25131978",
  "digest": "33Ny3AZ3q169QD2uvrso6QtfigyFqN8DccZ1cUJR7v2y",
  "type": "0x99bb55e386947db154dcb23b8f39191536c6332b659f3c6aa7596a0cd1f5c4bc::LuckyCafe::Card",
  "owner": {
    "AddressOwner": "0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a"
  },
  "previousTransaction": "4qYbEkvQWWshcgyg93NVtHt4hAMqnU9CMt86HJentZ4B",
  "storageRebate": "1535200",
  "content": {
    "dataType": "moveObject",
    "type": "0x99bb55e386947db154dcb23b8f39191536c6332b659f3c6aa7596a0cd1f5c4bc::LuckyCafe::Card",
    "hasPublicTransfer": true,
    "fields": {
      "cafe_id": "0x0463f197aa3c90a6c668089267aef6cff0ae3a50ed8144a7da84a49ece45f2e1",
      "id": {
        "id": "0xcc0b1b68febda7888b7681a70199ab291b38941ee965ed27af1b0bdf8ae79f76"
      }
    }
  }
}

3.3.3 transfer: 转移对象

(1) 命令说明

执行该命令将指定对象转移给指定地址。

Transfer object

Usage: sui client transfer [OPTIONS] --to <TO> --object-id <OBJECT_ID>

Options:
      --to <TO>
          Recipient address (or its alias if it's an address in the keystore)
      --object-id <OBJECT_ID>
          ID of the object to transfer
(2) 命令使用
$ export CARD=0xcc0b1b68febda7888b7681a70199ab291b38941ee965ed27af1b0bdf8ae79f76

$ sui client transfer --to alice --object-id $CARD

# 可见AddressOwner已经进行了转移
$ sui client object 0xcc0b1b68febda7888b7681a70199ab291b38941ee965ed27af1b0bdf8ae79f76 --json
{
  "objectId": "0xcc0b1b68febda7888b7681a70199ab291b38941ee965ed27af1b0bdf8ae79f76",
  "version": "28078186",
  "digest": "2y7xvkF9fKPM2jdmxrwShtFsJXQhH7C17KLXBu2zepPN",
  "type": "0x99bb55e386947db154dcb23b8f39191536c6332b659f3c6aa7596a0cd1f5c4bc::LuckyCafe::Card",
  "owner": {
    "AddressOwner": "0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19"
  },
  "previousTransaction": "CUoG2NZT5L94fHqWZNTrAoUs88HDAhyXiSLxL3m9Zn6U",
  "storageRebate": "1535200",
  "content": {
    "dataType": "moveObject",
    "type": "0x99bb55e386947db154dcb23b8f39191536c6332b659f3c6aa7596a0cd1f5c4bc::LuckyCafe::Card",
    "hasPublicTransfer": true,
    "fields": {
      "cafe_id": "0x0463f197aa3c90a6c668089267aef6cff0ae3a50ed8144a7da84a49ece45f2e1",
      "id": {
        "id": "0xcc0b1b68febda7888b7681a70199ab291b38941ee965ed27af1b0bdf8ae79f76"
      }
    }
  }
}

3.3.4 dynamic-field: 查询对象的动态域

(1) 命令说明

执行该命令将查询指定对象动态域。

Query a dynamic field by its address

Usage: sui client dynamic-field [OPTIONS] <object_id>

Arguments:
  <object_id>  The ID of the parent object

Options:
      --cursor <CURSOR>  Optional paging cursor
      --limit <LIMIT>    Maximum item returned per page [default: 50]
      --json             Return command outputs in json format
(2) 命令使用
$ sui client dynamic-field 0x9e2d68fb1263c6addde282640438c6910c219922de5e07fd2b1a94fcf40b6214

3.4 COIN类

3.4.1 balance: 获取地址代币余额

(1) 命令说明

执行该命令将获取当前地址代币余额。

List the coin balance of an address

Usage: sui client balance [OPTIONS] [ADDRESS]

Arguments:
  [ADDRESS]  Address (or its alias)

Options:
      --coin-type <COIN_TYPE>  Show balance for the specified coin (e.g.,
                               0x2::sui::SUI). All coins will be shown if none is
                               passed
      --with-coins             Show a list with each coin's object ID and balance
(2) 命令使用
  • 获取代币余额
$ sui client balance
╭───────────────────────────────────────────────╮
│ Balance of coins owned by this address        │
├───────────────────────────────────────────────┤
│ ╭───────────────────────────────────────────╮ │
│ │ coin      balance (raw)  balance          │ │
│ ├───────────────────────────────────────────┤ │
│ │ Sui       14915416944    14.91 SUI        │ │
│ │ RZX-name  10000          100.00 RZX-sym   │ │
│ ╰───────────────────────────────────────────╯ │
╰───────────────────────────────────────────────╯
  • 获取指定代币余额
$ sui client balance --coin-type 0x2::sui::SUI
╭────────────────────────────────────────╮
│ Balance of coins owned by this address │
├────────────────────────────────────────┤
│ ╭──────────────────────────────────╮   │
│ │ coin  balance (raw)  balance     │   │
│ ├──────────────────────────────────┤   │
│ │ Sui   14915416944    14.91 SUI   │   │
│ ╰──────────────────────────────────╯   │
╰────────────────────────────────────────╯
  • 获取每个代币对象ID和余额
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Balance of coins owned by this address                                                                  │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮      │
│ │ Sui: 1 coin, Balance: 14915416944 (14.91 SUI SUI)                                              │      │
│ ├────────────────────────────────────────────────────────────────────────────────────────────────┤      │
│ │ coinId                                                              balance (raw)  balance     │      │
│ ├────────────────────────────────────────────────────────────────────────────────────────────────┤      │
│ │ 0x043286ba5f16a930c04014e6a8790329a36f01103768055d9daace7cb2466cba  14915416944    14.91 SUI   │      │
│ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯      │
│ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │
│ │ RZX-name: 1 coin, Balance: 10000 (100.00 RZX-sym RZX-sym)                                           │ │
│ ├─────────────────────────────────────────────────────────────────────────────────────────────────────┤ │
│ │ coinId                                                              balance (raw)  balance          │ │
│ ├─────────────────────────────────────────────────────────────────────────────────────────────────────┤ │
│ │ 0x79985fae7d22dffac0a1f31e8bc90d52440af2a06242de55f99fe646df808ff7  10000          100.00 RZX-sym   │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                         │
╰─────────────────────

3.4.2 faucet: 领水

(1) 命令说明

执行该命令将领取开发网或测试网水龙头中的代币

Request gas coin from faucet. By default, it will use the active address and the active
network

Usage: sui client faucet [OPTIONS]

Options:
      --address <ADDRESS>  Address (or its alias)
(2) 命令使用
$ sui client faucet
Request successful. It can take up to 1 minute to get the coin. Run sui client gas to check your gas coins.

3.4.3 gas: 获取地址或别名的所有gas对象

(1) 命令说明

执行该命令将获取当前地址或别名的所有gas对象。

Obtain all gas objects owned by the address. An address' alias can be used
instead of the address

Usage: sui client gas [OPTIONS] [owner_address]

Arguments:
  [owner_address]  Address (or its alias) owning the objects
(2) 命令使用
$ sui client gas
╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0x07904d4deba1a822d52a674205548b36b28859620282479144454d3c096ffa15 │ 10000000000        │ 10.00            │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯

3.4.4 merge-coin: 代币合并

(1) 命令说明

执行该命令将合并两个代币对象。

Merge two coin objects into one coin

Usage: sui client merge-coin [OPTIONS] --primary-coin <PRIMARY_COIN> --coin-to-merge <COIN_TO_MERGE>

Options:
      --primary-coin <PRIMARY_COIN>
          The address of the coin to merge into
      --coin-to-merge <COIN_TO_MERGE>
          The address of the coin to be merged
(2) 命令使用
  • 合并前,查询gas
$ sui client gas
╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0x07904d4deba1a822d52a674205548b36b28859620282479144454d3c096ffa15 │ 10000000000        │ 10.00            │
│ 0x9094f1af0b746bc2ee9c4c11621910aa9342bc5b9d1040bdf0dc8886fba40824 │ 10000000000        │ 10.00            │
│ 0xd0c4c7f1d3056114ff378a5504dfb035a9a5254e1e3fd3eb84f562100e7a65b8 │ 10000000000        │ 10.00            │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯
  • 执行合并
$ export COIN1=0x07904d4deba1a822d52a674205548b36b28859620282479144454d3c096ffa15
$ export COIN2=0x9094f1af0b746bc2ee9c4c11621910aa9342bc5b9d1040bdf0dc8886fba40824

$ sui client merge-coin --primary-coin $COIN1 --coin-to-merge $COIN2
  • 合并后,查询gas 可见COIN2已经合并到了COIN1中。
╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0x07904d4deba1a822d52a674205548b36b28859620282479144454d3c096ffa15 │ 20000000000        │ 20.00            │
│ 0xd0c4c7f1d3056114ff378a5504dfb035a9a5254e1e3fd3eb84f562100e7a65b8 │ 9999958360         │ 9.99             │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯

3.4.5 split-coin: 代币拆分

(1) 命令说明

执行该命令将拆分一个代币对象。

Split a coin object into multiple coins

Usage: sui client split-coin [OPTIONS] --coin-id <COIN_ID> <--amounts <AMOUNTS>...|--count <COUNT>>

Options:
      --coin-id <COIN_ID>
          ID of the coin object to split
      --amounts <AMOUNTS>...
          Specific amounts to split out from the coin
      --count <COUNT>
          Count of equal-size coins to split into
(2) 命令使用
  • 按指定数量拆分
$ export COIN=0x07904d4deba1a822d52a674205548b36b28859620282479144454d3c096ffa15
$ sui client split-coin --coin-id $COIN --amounts 100000 200000

$ sui client gas
╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0x07904d4deba1a822d52a674205548b36b28859620282479144454d3c096ffa15 │ 19999700000        │ 19.99            │
│ 0xc5ec8a908d629218dc3d01faf04f52f535747c2c40e3d57e62628a5514ad83ff │ 200000             │ 0.00             │
│ 0xfbc39da4b681f4c2824e49dad479b38b8f2890a2996385aef06210094e140697 │ 100000             │ 0.00             │
│ 0xd0c4c7f1d3056114ff378a5504dfb035a9a5254e1e3fd3eb84f562100e7a65b8 │ 9996962600         │ 9.99             │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯
  • 按份数拆分
$ export COIN=0xfbc39da4b681f4c2824e49dad479b38b8f2890a2996385aef06210094e140697
$ sui client split-coin --coin-id $COIN --count 2

$ sui client gas
╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0xfbc39da4b681f4c2824e49dad479b38b8f2890a2996385aef06210094e140697 │ 50000              │ 0.00             │
│ 0x52df2bade5d24245a66782cd0da27dcfa0daaf9a322cd880364c9aaedb14bc41 │ 50000              │ 0.00             │
│ 0xc5ec8a908d629218dc3d01faf04f52f535747c2c40e3d57e62628a5514ad83ff │ 200000             │ 0.00             │
│ 0x07904d4deba1a822d52a674205548b36b28859620282479144454d3c096ffa15 │ 19997692240        │ 19.99            │
│ 0xd0c4c7f1d3056114ff378a5504dfb035a9a5254e1e3fd3eb84f562100e7a65b8 │ 9996962600         │ 9.99             │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯

3.4.6 transfer-sui: 转移SUI

(1) 命令说明

执行该命令将转移SUI到另一个地址。

Transfer SUI, and pay gas with the same SUI coin object. If amount is
specified, only the amount is transferred; otherwise the entire object is
transferred

Usage: sui client transfer-sui [OPTIONS] --to <TO> --sui-coin-object-id <SUI_COIN_OBJECT_ID>

Options:
      --to <TO>
          Recipient address (or its alias if it's an address in the keystore)
      --sui-coin-object-id <SUI_COIN_OBJECT_ID>
          ID of the coin to transfer. This is also the gas object
      --amount <AMOUNT>
          The amount to transfer, if not specified, the entire coin object
          will be transferred
(2) 命令使用
  • 转移指定数量
$ sui client gas alice
╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0x05b1b5c60d1b777a5dfc7d5a32874a563f303b1902c0fcd7ea7ab2bd6c0dc29e │ 996016240          │ 0.99             │
│ 0x23accfba5fd1743b042c7c0a1596cf346b986c3f1844585ab1c8058baeca00a8 │ 250000000          │ 0.25             │
│ 0x2fcab6efdd3c96746d3e0bc2dc27995b0c68797bf439fc797b2c96181ab426e3 │ 250000000          │ 0.25             │
│ 0x7d233de155d5a58855447b3ae165e9ec605109861ce14f07f63bd83eb01a4856 │ 250000000          │ 0.25             │
│ 0xd2e2c66fa2d0b0ee4316987a7b1d9e004ce56ef21ee92ba28f8c094097d1bbbf │ 250000000          │ 0.25             │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯

$ sui client gas bob
No gas coins are owned by this address

$ export COIN=0xd2e2c66fa2d0b0ee4316987a7b1d9e004ce56ef21ee92ba28f8c094097d1bbbf

$ sui client  transfer-sui --to bob --sui-coin-object-id $COIN --amount 50000000

$ sui client gas alice
╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0x05b1b5c60d1b777a5dfc7d5a32874a563f303b1902c0fcd7ea7ab2bd6c0dc29e │ 996016240          │ 0.99             │
│ 0x23accfba5fd1743b042c7c0a1596cf346b986c3f1844585ab1c8058baeca00a8 │ 250000000          │ 0.25             │
│ 0x2fcab6efdd3c96746d3e0bc2dc27995b0c68797bf439fc797b2c96181ab426e3 │ 250000000          │ 0.25             │
│ 0x7d233de155d5a58855447b3ae165e9ec605109861ce14f07f63bd83eb01a4856 │ 250000000          │ 0.25             │
│ 0xd2e2c66fa2d0b0ee4316987a7b1d9e004ce56ef21ee92ba28f8c094097d1bbbf │ 198002120          │ 0.19             │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯

$ sui client gas bob
╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0x724b02db0f8e50aced87f8871d808a4eca06e132345faa57658cd4d1fd12d655 │ 50000000           │ 0.05             │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯
  • 全部转移
$ export COIN=0x7d233de155d5a58855447b3ae165e9ec605109861ce14f07f63bd83eb01a4856

$ sui client  transfer-sui --to bob --sui-coin-object-id $COIN

$ sui client gas alice
╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0x05b1b5c60d1b777a5dfc7d5a32874a563f303b1902c0fcd7ea7ab2bd6c0dc29e │ 996016240          │ 0.99             │
│ 0x23accfba5fd1743b042c7c0a1596cf346b986c3f1844585ab1c8058baeca00a8 │ 250000000          │ 0.25             │
│ 0x2fcab6efdd3c96746d3e0bc2dc27995b0c68797bf439fc797b2c96181ab426e3 │ 250000000          │ 0.25             │
│ 0xd2e2c66fa2d0b0ee4316987a7b1d9e004ce56ef21ee92ba28f8c094097d1bbbf │ 198002120          │ 0.19             │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯

$ sui client gas bob
╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0x724b02db0f8e50aced87f8871d808a4eca06e132345faa57658cd4d1fd12d655 │ 50000000           │ 0.05             │
│ 0x7d233de155d5a58855447b3ae165e9ec605109861ce14f07f63bd83eb01a4856 │ 248990120          │ 0.24             │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯

3.4.7 pay: 支付COIN到指定地址

(1) 命令说明

执行该命令将支付COIN到指定地址,支持批量转账,接受地址个数与转账金额个数必须相同。

Pay coins to recipients following specified amounts, with input
coins. Length of recipients must be the same as that of amounts

Usage: sui client pay [OPTIONS]

Options:
      --input-coins <INPUT_COINS>...
          The input coins to be used for pay recipients, following
          the specified amounts
      --recipients <RECIPIENTS>...
          The recipient addresses, must be of same length as
          amounts. Aliases of addresses are also accepted as input
      --amounts <AMOUNTS>...
          The amounts to be paid, following the order of
          recipients
(2) 命令使用
  • 执行转账命令

分别给AliceBob转账100200 RZX

$ export COIN_ID=0x79985fae7d22dffac0a1f31e8bc90d52440af2a06242de55f99fe646df808ff7
$ sui client pay --input-coins $COIN_ID --recipients alice bob --amounts  100 200
  • 查看Alice地址当前余额
╭─────────────────────────────────────────────╮
│ Balance of coins owned by this address      │
├─────────────────────────────────────────────┤
│ ╭─────────────────────────────────────────╮ │
│ │ coin      balance (raw)  balance        │ │
│ ├─────────────────────────────────────────┤ │
│ │ Sui       1694018460     1.69 SUI       │ │
│ │ RZX-name  100            1.00 RZX-sym   │ │
│ ╰─────────────────────────────────────────╯ │
╰─────────────────────────────────────────────╯
  • 查看Bob地址当前余额
╭─────────────────────────────────────────────╮
│ Balance of coins owned by this address      │
├─────────────────────────────────────────────┤
│ ╭─────────────────────────────────────────╮ │
│ │ coin      balance (raw)  balance        │ │
│ ├─────────────────────────────────────────┤ │
│ │ Sui       298990320      0.29 SUI       │ │
│ │ RZX-name  200            2.00 RZX-sym   │ │
│ ╰─────────────────────────────────────────╯ │
╰─────────────────────────────────────────────╯

3.4.8 pay-sui: 支付SUI到指定地址

(1) 命令说明

执行该命令将支付SUI到指定地址,支持批量转账,且可以支持多个COIN会进行合并后再转币。

Pay SUI coins to recipients following following specified amounts, with input
coins. Length of recipients must be the same as that of amounts. The input
coins also include the coin for gas payment, so no extra gas coin is required

Usage: sui client pay-sui [OPTIONS]

Options:
      --input-coins <INPUT_COINS>...
          The input coins to be used for pay recipients, including the gas
          coin
      --recipients <RECIPIENTS>...
          The recipient addresses, must be of same length as amounts. Aliases
          of addresses are also accepted as input
      --amounts <AMOUNTS>...
          The amounts to be paid, following the order of recipients
(2) 命令使用
  • 单地址转
$ export COIN_ID=0x241d78606cb3b7d2f880854c49781e7c537d318a52c499faef24e101455adf9a

$ sui client pay-sui --input-coins $COIN_ID --recipients alice --amounts 10000

$ sui client gas alice
╭────────────────────────────────────────────────────────────────────┬────────────╮
│ gasCoinId                                                          │ gasBalance │
├────────────────────────────────────────────────────────────────────┼────────────┤
│ 0x73166aad134319e9c26b19ec6ed0ce419812a7e2612c9ea0e1b93767de423c4d │ 10000      │
╰────────────────────────────────────────────────────────────────────┴────────────╯

$ sui client gas jason
╭────────────────────────────────────────────────────────────────────┬────────────╮
│ gasCoinId                                                          │ gasBalance │
├────────────────────────────────────────────────────────────────────┼────────────┤
│ 0x241d78606cb3b7d2f880854c49781e7c537d318a52c499faef24e101455adf9a │ 3760487788 │
│ 0x70b96720fadb6aa45620ab84efd9139e4674057207c93e4375350ec695865fab │ 988221840  │
╰────────────────────────────────────────────────────────────────────┴────────────╯
  • 多地址转
$ sui client pay-sui --input-coins $COIN_ID  --recipients alice bob --amounts 10000 10000

$ sui client gas alice
╭────────────────────────────────────────────────────────────────────┬────────────╮
│ gasCoinId                                                          │ gasBalance │
├────────────────────────────────────────────────────────────────────┼────────────┤
│ 0x73166aad134319e9c26b19ec6ed0ce419812a7e2612c9ea0e1b93767de423c4d │ 10000      │
│ 0xb6013d96900f73c43654a48d5c1aef39838513ec33a189593f962e31f5bc7898 │ 10000      │
╰────────────────────────────────────────────────────────────────────┴────────────╯

$ sui client gas bob
╭────────────────────────────────────────────────────────────────────┬────────────╮
│ gasCoinId                                                          │ gasBalance │
├────────────────────────────────────────────────────────────────────┼────────────┤
│ 0xedca429699b178481b88e39502ed39bd322e315c582a70520bd32930a25dd758 │ 10000      │
╰────────────────────────────────────────────────────────────────────┴────────────╯
  • 合并后转

若指定多个coin的话,会先将这些coin合并后,再进行转币。

$ export COIN_ID2=0x70b96720fadb6aa45620ab84efd9139e4674057207c93e4375350ec695865fab
$ sui client pay-sui --input-coins $COIN_ID $COIN_ID2 --recipients alice bob --amounts 20000
80000

$ sui client gas jason
╭────────────────────────────────────────────────────────────────────┬────────────╮
│ gasCoinId                                                          │ gasBalance │
├────────────────────────────────────────────────────────────────────┼────────────┤
│ 0x241d78606cb3b7d2f880854c49781e7c537d318a52c499faef24e101455adf9a │ 4743595988 │
╰────────────────────────────────────────────────────────────────────┴────────────╯

$ sui client gas alice
╭────────────────────────────────────────────────────────────────────┬────────────╮
│ gasCoinId                                                          │ gasBalance │
├────────────────────────────────────────────────────────────────────┼────────────┤
│ 0x73166aad134319e9c26b19ec6ed0ce419812a7e2612c9ea0e1b93767de423c4d │ 10000      │
│ 0xb6013d96900f73c43654a48d5c1aef39838513ec33a189593f962e31f5bc7898 │ 10000      │
│ 0xbfe36b88a30be6bf814c26f7aa4ebeedd4c4a3a189238ba8ce2e712fd5eecc31 │ 20000      │
╰────────────────────────────────────────────────────────────────────┴────────────╯

$ sui client gas bob
╭────────────────────────────────────────────────────────────────────┬────────────╮
│ gasCoinId                                                          │ gasBalance │
├────────────────────────────────────────────────────────────────────┼────────────┤
│ 0xac824c384f0bab18bfff13da09c8ba9e65607c6918076b0f90005b5034071f92 │ 80000      │
│ 0xedca429699b178481b88e39502ed39bd322e315c582a70520bd32930a25dd758 │ 10000      │
╰────────────────────────────────────────────────────────────────────┴────────────╯

3.4.9 pay-all-sui: 支付所有SUI到指定地址

(1) 命令说明

执行该命令会将指定地址的所有SUI转移到指定地址。

Pay all residual SUI coins to the recipient with input coins, after deducting
the gas cost. The input coins also include the coin for gas payment, so no
extra gas coin is required

Usage: sui client pay-all-sui [OPTIONS] --recipient <RECIPIENT>

Options:
      --input-coins <INPUT_COINS>...
          The input coins to be used for pay recipients, including the gas
          coin
      --recipient <RECIPIENT>
          The recipient address (or its alias if it's an address in the
          keystore)
(2) 命令使用
$ export COIN_ID=0x241d78606cb3b7d2f880854c49781e7c537d318a52c499faef24e101455adf9a

$ sui client pay-all-sui --input-coins $COIN_ID --recipient alice

$ sui client gas jason
No gas coins are owned by this address

$ sui client gas alice
╭────────────────────────────────────────────────────────────────────┬────────────╮
│ gasCoinId                                                          │ gasBalance │
├────────────────────────────────────────────────────────────────────┼────────────┤
│ 0x241d78606cb3b7d2f880854c49781e7c537d318a52c499faef24e101455adf9a │ 4742586108 │
│ 0x73166aad134319e9c26b19ec6ed0ce419812a7e2612c9ea0e1b93767de423c4d │ 10000      │
│ 0xb6013d96900f73c43654a48d5c1aef39838513ec33a189593f962e31f5bc7898 │ 10000      │
│ 0xbfe36b88a30be6bf814c26f7aa4ebeedd4c4a3a189238ba8ce2e712fd5eecc31 │ 20000      │
╰────────────────────────────────────────────────────────────────────┴────────────╯

3.5 合约类

3.5.1 publish: 部署合约

(1) 命令说明

执行该命令会将指定目录(默认当前目录)下的Move合约编译成字节码,并将字节码部署到链上。

Publish Move modules

Usage: sui client publish [OPTIONS] [package_path]

Arguments:
  [package_path]  Path to directory containing a Move package [default: .]
(2) 命令使用
  • 执行该命令会创建:PackageID,后续在调用合约方法或升级合约都会使用到。
  • 如果在init合约构造方法中有创建Object,也会输出对应ObjectId
$ sui client publish

3.5.2 call: 调用合约方法

(1) 命令说明

执行该命令会调用指定合约的指定方法,并返回执行结果。

  • --package指定调用的合约包名
  • --module指定调用的合约模块名
  • --function指定调用的合约方法名
  • --args指定调用方法的参数(可选)
  • --type-args指定泛型参数类型(可选)
Call Move function

Usage: sui client call [OPTIONS] --package <PACKAGE> --module <MODULE> --function <FUNCTION>

Options:
      --package <PACKAGE>
          Object ID of the package, which contains the module
      --module <MODULE>
          The name of the module in the package
      --function <FUNCTION>
          Function name in module
      --type-args <TYPE_ARGS>...
          Type arguments to the generic function being called. All must be
          specified, or the call will fail
      --args <ARGS>...
          Simplified ordered args like in the function syntax ObjectIDs,
          Addresses must be hex strings
(2) 命令使用
$ client call --function claim_red_packet --package $PACKAGE_ID --module red_packet --type-args 0x2::sui::SUI --args $RED_POCKET $WEATHER_ORACLE 0x6

3.5.3 upgrade: 升级合约

(1) 命令说明

执行该命令会将指定目录(默认当前目录)下的修改后的Move合约编译成字节码,并将字节码升级链上合约。

Upgrade Move modules

Usage: sui client upgrade [OPTIONS] --upgrade-capability <UPGRADE_CAPABILITY> [package_path]

Arguments:
  [package_path]  Path to directory containing a Move package [default: .]
(2) 命令使用
$ sui client upgrade --upgrade-capability $UPGRADE_CAP_ID

3.6 交易类

3.6.1 execute-signed-tx: 执行签名交易

(1) 命令说明

执行该命令可以执行已经签名交易。

Execute a Signed Transaction. This is useful when the user prefers to sign elsewhere and use this command
to execute

Usage: sui client execute-signed-tx [OPTIONS] --tx-bytes <TX_BYTES>

Options:
      --tx-bytes <TX_BYTES>      BCS serialized transaction data bytes without its type tag, as base64
                                 encoded string. This is the output of sui client command using
                                 --serialize-unsigned-transaction
      --signatures <SIGNATURES>  A list of Base64 encoded signatures `flag || signature || pubkey`
(2) 命令使用
  • 构造待签名交易
$ sui client pay --input-coins $COIN_ID --recipients alice --amounts  500 --serialize-unsigned-transaction

AAADAQB5mF+ufSLf+sCh8x6LyQ1SRAryoGJC3lX5n+ZG34CP95RwrAEAAAAAILvEC/HKvJqm/K/PIvvqw3z+C53wDTkrF2SHvVugWXHFAAj0AQAAAAAAAAAgaYcvxHgfEV4I9y3TfeEhbEMa/qT6pLeU5TJ9pZq85oECAgEAAAEBAQABAQMAAAAAAQIAXFiC1zpuW26hdD+wKO/14NfMi3rhI9J4VsX+Zm2RVpoBBDKGul8WqTDAQBTmqHkDKaNvARA3aAVdnarOfLJGbLqUcKwBAAAAACB0eN1gH6aZ1wcAk4XFWk41okSYHd6bdQQETp27SRBN01xYgtc6bltuoXQ/sCjv9eDXzIt64SPSeFbF/mZtkVaa6AMAAAAAAADYUEQAAAAAAAA=
  • 进行交易签名

$ export RAW_TX_DATA=AAADAQB5mF+ufSLf+sCh8x6LyQ1SRAryoGJC3lX5n+ZG34CP95RwrAEAAAAAILvEC/HKvJqm/K/PIvvqw3z+C53wDTkrF2SHvVugWXHFAAj0AQAAAAAAAAAgaYcvxHgfEV4I9y3TfeEhbEMa/qT6pLeU5TJ9pZq85oECAgEAAAEBAQABAQMAAAAAAQIAXFiC1zpuW26hdD+wKO/14NfMi3rhI9J4VsX+Zm2RVpoBBDKGul8WqTDAQBTmqHkDKaNvARA3aAVdnarOfLJGbLqUcKwBAAAAACB0eN1gH6aZ1wcAk4XFWk41okSYHd6bdQQETp27SRBN01xYgtc6bltuoXQ/sCjv9eDXzIt64SPSeFbF/mZtkVaa6AMAAAAAAADYUEQAAAAAAAA=

$ sui keytool sign --address jason --data $RAW_TX_DATA
╭──────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ suiAddress   │ 0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a                                                                                               │
│ rawTxData    │ AAADAQB5mF+ufSLf+sCh8x6LyQ1SRAryoGJC3lX5n+ZG34CP95RwrAEAAAAAILvEC/HKvJqm/K/PIvvqw3z+C53wDTkrF2SHvVugWXHFAAj0AQAAAAAAAAAgaYcvxHgfEV4I9y3TfeEhbEMa/qT6pLeU5TJ9pZq8 │
│              │ 5oECAgEAAAEBAQABAQMAAAAAAQIAXFiC1zpuW26hdD+wKO/14NfMi3rhI9J4VsX+Zm2RVpoBBDKGul8WqTDAQBTmqHkDKaNvARA3aAVdnarOfLJGbLqUcKwBAAAAACB0eN1gH6aZ1wcAk4XFWk41okSYHd6bdQQE │
│              │ Tp27SRBN01xYgtc6bltuoXQ/sCjv9eDXzIt64SPSeFbF/mZtkVaa6AMAAAAAAADYUEQAAAAAAAA=                                                                                     │
│ intent       │ ╭─────────┬─────╮                                                                                                                                                │
│              │ │ scope   │  0  │                                                                                                                                                │
│              │ │ version │  0  │                                                                                                                                                │
│              │ │ app_id  │  0  │                                                                                                                                                │
│              │ ╰─────────┴─────╯                                                                                                                                                │
│ rawIntentMsg │ AAAAAAADAQB5mF+ufSLf+sCh8x6LyQ1SRAryoGJC3lX5n+ZG34CP95RwrAEAAAAAILvEC/HKvJqm/K/PIvvqw3z+C53wDTkrF2SHvVugWXHFAAj0AQAAAAAAAAAgaYcvxHgfEV4I9y3TfeEhbEMa/qT6pLeU5TJ9 │
│              │ pZq85oECAgEAAAEBAQABAQMAAAAAAQIAXFiC1zpuW26hdD+wKO/14NfMi3rhI9J4VsX+Zm2RVpoBBDKGul8WqTDAQBTmqHkDKaNvARA3aAVdnarOfLJGbLqUcKwBAAAAACB0eN1gH6aZ1wcAk4XFWk41okSYHd6b │
│              │ dQQETp27SRBN01xYgtc6bltuoXQ/sCjv9eDXzIt64SPSeFbF/mZtkVaa6AMAAAAAAADYUEQAAAAAAAA=                                                                                 │
│ digest       │ m2QJPh0Ez2FsPhhCbkdVpXeXD2PkAQUgJGw3m4hIyTw=                                                                                                                     │
│ suiSignature │ AMmljxE4qrgfWYsCk00KtAm60f0bCJt1Qpy0kkg8yv997IxnBq0v3YcXCc9lKponq6BztRa5KI8jTpaep8yaPwGIIWyPVESl2t0z8FQnUdfJFGkkHr/YUBwVWQtnZBZycQ==                             │
╰──────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
  • 发送已交易签名
$ export SIGN=AMmljxE4qrgfWYsCk00KtAm60f0bCJt1Qpy0kkg8yv997IxnBq0v3YcXCc9lKponq6BztRa5KI8jTpaep8yaPwGIIWyPVESl2t0z8FQnUdfJFGkkHr/YUBwVWQtnZBZycQ==
$ sui client execute-signed-tx --tx-bytes $RAW_TX_DATA --signatures $SIGN
  • 查看Alice地址当前余额,可以看到余额发生的变化
╭─────────────────────────────────────────────╮
│ Balance of coins owned by this address      │
├─────────────────────────────────────────────┤
│ ╭─────────────────────────────────────────╮ │
│ │ coin      balance (raw)  balance        │ │
│ ├─────────────────────────────────────────┤ │
│ │ Sui       3931920823     3.93 SUI       │ │
│ │ RZX-name  500            5.00 RZX-sym   │ │
│ ╰─────────────────────────────────────────╯ │
╰─────────────────────────────────────────────╯

3.6.2 execute-combined-signed-tx: 执行联合签名交易

(1) 命令说明
Execute a combined serialized SenderSignedData string

Usage: sui client execute-combined-signed-tx [OPTIONS] --signed-tx-bytes <SIGNED_TX_BYTES>

Options:
      --signed-tx-bytes <SIGNED_TX_BYTES>
          BCS serialized sender signed data, as base64 encoded string. This is the output of sui client
          command using --serialize-signed-transaction
(2) 命令使用
  • 构造联合签名交易
$ sui client pay --input-coins $COIN_ID --recipients alice --amounts 800 --serialize-si
gned-transaction

AQAAAAAAAwEAeZhfrn0i3/rAofMei8kNUkQK8qBiQt5V+Z/mRt+Aj/eVcKwBAAAAACCJUku+MB0abKgwUUGVSlr0uGAnfPaY9d40jepHg5vwwAAIIAMAAAAAAAAAIGmHL8R4HxFeCPct033hIWxDGv6k+qS3lOUyfaWavOaBAgIBAAABAQEAAQEDAAAAAAECAFxYgtc6bltuoXQ/sCjv9eDXzIt64SPSeFbF/mZtkVaaAQQyhrpfFqkwwEAU5qh5AymjbwEQN2gFXZ2qznyyRmy6lXCsAQAAAAAgFcDqz+koiG/Vz+IQRUcK59xWHRMppgnk6im7FFK1NptcWILXOm5bbqF0P7Ao7/Xg18yLeuEj0nhWxf5mbZFWmugDAAAAAAAA2FBEAAAAAAAAAWEAmRLC932b4vydYmAhGRKuoQxcd9baeMDh77jK9X2aEKfMKi8kHrmNzCS6QR7PN1vPEZB2AnK9mJIl6rCduUh5DIghbI9URKXa3TPwVCdR18kUaSQev9hQHBVZC2dkFnJx
  • 发送联合签名交易
$ export SIGNED_TX_BYTES=AQAAAAAAAwEAeZhfrn0i3/rAofMei8kNUkQK8qBiQt5V+Z/mRt+Aj/eVcKwBAAAAACCJUku+MB0abKgwUUGVSlr0uGAnfPaY9d40jepHg5vwwAAIIAMAAAAAAAAAIGmHL8R4HxFeCPct033hIWxDGv6k+qS3lOUyfaWavOaBAgIBAAABAQEAAQEDAAAAAAECAFxYgtc6bltuoXQ/sCjv9eDXzIt64SPSeFbF/mZtkVaaAQQyhrpfFqkwwEAU5qh5AymjbwEQN2gFXZ2qznyyRmy6lXCsAQAAAAAgFcDqz+koiG/Vz+IQRUcK59xWHRMppgnk6im7FFK1NptcWILXOm5bbqF0P7Ao7/Xg18yLeuEj0nhWxf5mbZFWmugDAAAAAAAA2FBEAAAAAAAAAWEAmRLC932b4vydYmAhGRKuoQxcd9baeMDh77jK9X2aEKfMKi8kHrmNzCS6QR7PN1vPEZB2AnK9mJIl6rCduUh5DIghbI9URKXa3TPwVCdR18kUaSQev9hQHBVZC2dkFnJx

$ sui client execute-combined-signed-tx --signed-tx-bytes $SIGNED_TX_BYTES
  • 查看Alice地址当前余额,可以看到余额发生的变化
╭──────────────────────────────────────────────╮
│ Balance of coins owned by this address       │
├──────────────────────────────────────────────┤
│ ╭──────────────────────────────────────────╮ │
│ │ coin      balance (raw)  balance         │ │
│ ├──────────────────────────────────────────┤ │
│ │ Sui       3931920823     3.93 SUI        │ │
│ │ RZX-name  1300           13.00 RZX-sym   │ │
│ ╰──────────────────────────────────────────╯ │
╰──────────────────────────────────────────────╯

3.6.3 tx-block: 获取指定交易的执行影响

(1) 命令说明

执行该命令可以获取指定交易的执行影响。

Get the effects of executing the given transaction block

Usage: sui client tx-block [OPTIONS] <digest>

Arguments:
  <digest>  Digest of the transaction block
(2) 命令使用
$ sui client tx-block HzG4Z3vv4MA6rgPVJRUZmLLfNJJhEyE3iWK6LHjTYNBG

3.7 PTB类

  • PTB可编程交易块,是SUI提供的一个非常强大的一个功能,它允许在一个交易中执行多种操作,甚至去调用不同的合约。
  • 直接通过命令行脚本的方式去创建和执行PTB,可以方便开发人员在不用通过SDK写代码的情况下便能构建和执行PTB,也为自动化任务场景提供了很大的灵活性和便利性。
  • 由于ptb命令比较复杂,单独自成一类,进行讲解

3.7.1 ptb: 根据指定参数执行PTB

(1) 命令说明

执行该命令可以构建、预览和执行PTB,下面将对主要的命令行参数逐一进行说明。

Build, preview, and execute programmable transaction blocks. Depending on your
shell, you might have to use quotes around arrays or other passed values. Use
--help to see examples for how to use the core functionality of this command.

Usage: sui client ptb [OPTIONS]

Options:
      --assign <NAME> <VALUE>
          Assign a value to a variable name to use later in the PTB.
      --dry-run
          Perform a dry run of the PTB instead of executing it.
      --gas-coin <ID>
          The object ID of the gas coin to use. If not specified, it will try to
          use the first gas coin that it finds that has at least the requested
          gas-budget balance.
      --gas-budget <MIST>
          An optional gas budget for this PTB (in MIST). If gas budget is not
          provided, the tool will first perform a dry run to estimate the gas
          cost, and then it will execute the transaction. Please note that this
          incurs a small cost in performance due to the additional dry run call.
      --make-move-vec <TYPE> <[VALUES]>
          Given n-values of the same type, it constructs a vector. For non
          objects or an empty vector, the type tag must be specified.
      --merge-coins <INTO_COIN> <[COIN OBJECTS]>
          Merge N coins into the provided coin.
      --move-call <PACKAGE::MODULE::FUNCTION> <TYPE_ARGS> <FUNCTION_ARGS>
          Make a move call to a function.
      --split-coins <COIN> <[AMOUNT]>
          Split the coin into N coins as per the given array of amounts.
      --transfer-objects <[OBJECTS]> <TO>
          Transfer objects to the specified address.
      --publish <MOVE_PACKAGE_PATH>
          Publish the Move package. It takes as input the folder where the
          package exists.
      --upgrade <MOVE_PACKAGE_PATH>
          Upgrade the Move package. It takes as input the folder where the
          package exists.
      --preview
          Preview the list of PTB transactions instead of executing them.
      --serialize-unsigned-transaction
          Instead of executing the transaction, serialize the bcs bytes of the
          unsigned transaction data using base64 encoding.
      --serialize-signed-transaction
          Instead of executing the transaction, serialize the bcs bytes of the
          signed transaction data using base64 encoding.
      --summary
          Show only a short summary (digest, execution status, gas cost). Do not
          use this flag when you need all the transaction data and the execution
          effects.
      --warn-shadows
          Enable shadow warning when the same variable name is declared multiple
          times. Off by default.
(2) 命令使用
(a) 变量绑定(--assign

可以使用--assign参数将值绑定到变量。

  • 命令用法
--assign <NAME> <VALUE>
	Assign a value to a variable name to use later in the PTB.
  • 使用示例

    • 用法1:为变量赋值

      --assign value 1000
      
    • 用法2:将上一个命令的结果绑定到变量

      --split-coins gas [1000] \
      --assign coin
      
(b) 地址和对象ID的表达(@
  • 地址和对象ID前需要添加**@**,以便和普通十六进制数值区分开
  • 对于本地钱包中的地址,可以使用别名,不用添加@
(c) 转移对象(--transfer-objects

可以使用--transfer-objects参数转移对象。

  • 命令用法
--transfer-objects <[OBJECTS]> <TO>
	Transfer objects to the specified address.
  • 使用示例

注:命令中Coin对象ID和地址均需使用@开头,以跟普通十六进制数值区分开

sui client ptb \
  --assign coin @0x75794e86466c09edbe26a814c2e2f46a64f0391a05a7e9725dfad1bed2229b36 \
  --transfer-objects [coin] @0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a
(d) 拆分代币(--split-coins

使用--split-coins参数可以拆分代币。

  • 命令用法
--split-coins <COIN> <[AMOUNT]>
	Split the coin into N coins as per the given array of amounts.
  • 使用示例

    • 从Gas中拆分

      sui client ptb \
      	--split-coins gas [1000] \
      	--assign coins \
      	--transfer-objects [coins] jason
      

      image-20240712000249123

    • 从Coin对象中拆分

      sui client ptb \
      	--split-coins @0x1c8bbc2765028d99fbeba11caa5d7a7e13c110bbe0f4c9afb53c9f2f51cb897c [1000] \
      	--assign coins \
      	--transfer-objects [coins] jason
      
(e) 合并代币(--merge-coins
  • 命令用法
--merge-coins <INTO_COIN> <[COIN OBJECTS]>
	Merge N coins into the provided coin.
  • 使用示例
sui client ptb \
  --assign base_coin @0xe8d7bf9b843f4f4adc5962976f3e9afa677f431f9752ac4c3849b86a7798b20c\
	--split-coins gas [1000, 2000, 3000] \
	--assign coins \
	--merge-coins base_coin [coins.0, coins.1, coins.2] \
	--transfer-objects [coins] jason
  • 执行前

image-20240712000324842

  • 执行后

image-20240712000338998

(f) 交易预览(--preview

如果构造了复杂的PTB,可以使用--preview参数来预览PTB交易执行列表而非执行它。

  • 命令用法
--preview
	Preview the list of PTB transactions instead of executing them.
  • 使用示例
sui client ptb \
	--assign value1 2024 \
  --assign to_address1 jason \
  --assign to_address2 @0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19 \
  --split-coins gas [value1,4096,8848] \
  --assign coins \
  --transfer-objects [coins.0] to_address1 \
  --transfer-objects [coins.1, coins.2] to_address2 \
  --gas-budget 100000000 \
  --preview

image-20240712000351319

(g) 摘要展示(--summary

使用--summary参数,可以进行简短的摘要展示,包括:交易哈希、执行状态、Gas消耗。

  • 命令用法
--summary
	Show only a short summary (digest, execution status, gas cost). Do not use this flag when you need all the transaction data and the execution effects.
  • 使用示例
sui client ptb \
	--assign value1 2024 \
  --assign to_address1 jason \
  --assign to_address2 @0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19 \
  --split-coins gas [value1,4096,8848] \
  --assign coins \
  --transfer-objects [coins.0] to_address1 \
  --transfer-objects [coins.1, coins.2] to_address2 \
  --gas-budget 100000000 \
  --summary
image-20240712000400379
(h) 试运行(dry-run

使用--dry-run参数,可以试运行PTB,输出执行信息,但非实际上链执行。

  • 命令用法
--dry-run
	Perform a dry run of the PTB instead of executing it.
  • 使用示例
sui client ptb \
	--assign value1 2024 \
  --assign to_address1 jason \
  --assign to_address2 @0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19 \
  --split-coins gas [value1,4096,8848] \
  --assign coins \
  --transfer-objects [coins.0] to_address1 \
  --transfer-objects [coins.1, coins.2] to_address2 \
  --gas-budget 100000000 \
  --dry-run
(i) 对象数组(--make-move-vec

使用--make-move-vec参数,可以构造一个对象数组,可以在MoveCall中使用。

  • 命令用法
--make-move-vec <TYPE> <[VALUES]>
	Given n-values of the same type, it constructs a vector. For non objects or an empty vector, the type tag must be specified.
  • 使用示例
sui client ptb \
	--make-move-vec "<u64>" [1111, 2222] \
	--make-move-vec "<address>" [jason, @0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19]
(j) 方法调用(--move-call

使用--move-call参数,可以调用合约中的方法。

  • 命令用法
--move-call <PACKAGE::MODULE::FUNCTION> <TYPE> <FUNCTION_ARGS>
	Make a move call to a function.
  • 使用示例

以红包合约创建红包接口(send_new_red_packet)调用为例(详见:https://learnblockchain.cn/article/7772)。

之前我们在命令行下,为了创建一个红包很繁琐的使用了多条命令,现在使用PTB只需要一条命令就能轻松搞定。

以下命令,将创建5个拼手气红包,总金额0.1 SUI

sui client ptb \
  --assign PACKAGE_ID @0xe5417558cf7edc87840fef347f294dc0fa7bdcd82c043e630c504d233c6b4784 \
  --assign COUNT 5 \
  --assign AMOUNT 100000000 \
  --split-coins gas [AMOUNT] \
  --assign coins \
  --move-call PACKAGE_ID::red_packet::send_new_red_packet "<0x2::sui::SUI>" COUNT coins.0

image-20240712000425000

(k) 部署包(--publish

使用--publish参数可以部署Move包,和sui client publish命令相比,sui client ptb --publish命令必须显式的转移创建包成功后产生的UpgradeCap对象。

  • 命令用法
--publish <MOVE_PACKAGE_PATH>
	Publish the Move package. It takes as input the folder where the package exists.
  • 使用示例
sui client ptb \
  --move-call sui::tx_context::sender \
  --assign sender \
  --publish "." \
  --assign upgrade_cap \
  --transfer-objects [upgrade_cap] sender \

image-20240712000435836

3.8 调优类

3.8.1 profile-transaction: 交易gas消耗分析

(1) 命令说明

执行该命令,可以分析指定交易的gas消耗情况。

注:要启用分析功能,需要编译Sui项目时指定--features gas-profiler

$ cargo build --features gas-profiler --release

或者:

$ cargo install --locked --git https://github.com/MystenLabs/sui.git --branch <BRANCH-NAME> --features gas-profiler sui
Profile the gas usage of a transaction. Unless an output filepath is not specified, outputs a file
`gas_profile_{tx_digest}_{unix_timestamp}.json` which can be opened in a flamegraph tool such as speedscope

Usage: sui client profile-transaction [OPTIONS] --tx-digest <TX_DIGEST>

Options:
  -t, --tx-digest <TX_DIGEST>            The digest of the transaction to replay
  -p, --profile-output <PROFILE_OUTPUT>  If specified, overrides the filepath of the output profile, for example --
                                         /temp/my_profile_name.json will write output to
                                         `/temp/my_profile_name_{tx_digest}_{unix_timestamp}.json` If an output filepath is
                                         not specified, it will output a file
                                         `gas_profile_{tx_digest}_{unix_timestamp}.json` to the working directory
(2) 命令使用
$ sui client profile-transaction --tx-digest 6yE93gtnBzyq9XAtKiJ6bQwrs91B6vVRuK4QucRjKhY9

INFO sui_replay: Executing tx: 6yE93gtnBzyq9XAtKiJ6bQwrs91B6vVRuK4QucRjKhY9
INFO sui_replay::replay: Using RPC URL: https://fullnode.devnet.sui.io:443
INFO move_vm_profiler: Gas profile written to file: ./gas_profile.json_6yE93gtnBzyq9XAtKiJ6bQwrs91B6vVRuK4QucRjKhY9_1720628540191496861.json
Execution finished successfully.

命令成功之后后,会输出分析结构到json文件中,这个文件可以在类似speedscope的火焰图工具中打开,以便对交易gas使用情况进行可视化分析。

3.8.2 replay-transaction: 给定交易重放

(1) 命令说明

执行该命令可以重放指定交易,并查看交易执行效果。

Replay a given transaction to view transaction effects. Set environment variable
MOVE_VM_STEP=1 to debug

Usage: sui client replay-transaction [OPTIONS] --tx-digest <TX_DIGEST>

Options:
  -t, --tx-digest <TX_DIGEST>
          The digest of the transaction to replay
      --gas-info
          Log extra gas-related information
      --ptb-info
          Log information about each programmable transaction command
  -e, --executor-version <EXECUTOR_VERSION>
          Optional version of the executor to use, if not specified defaults to
          the one originally used for the transaction
  -p, --protocol-version <PROTOCOL_VERSION>
          Optional protocol version to use, if not specified defaults to the one
          originally used for the transaction
(2) 命令使用
INFO sui_replay: Executing tx: 6yE93gtnBzyq9XAtKiJ6bQwrs91B6vVRuK4QucRjKhY9
INFO sui_replay::replay: Using RPC URL: https://fullnode.devnet.sui.io:443

╭───────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Transaction Effects                                                                               │
├───────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Digest: 6yE93gtnBzyq9XAtKiJ6bQwrs91B6vVRuK4QucRjKhY9                                              │
│ Status: Success                                                                                   │
│ Executed Epoch: 58                                                                                │
│                                                                                                   │
│ Created Objects:                                                                                  │
│  ┌──                                                                                              │
│  │ ID: 0x91ed399242d4b0424280385af7c890477521c469e1e51deada8f8b29876b27bc                         │
│  │ Owner: Shared( 7 )                                                                             │
│  │ Version: 7                                                                                     │
│  │ Digest: 9XrVCsYjKMXK6dFiRRQLoCTVvS1B5KUJfW9kGrZGADkR                                           │
│  └──                                                                                              │
│  ┌──                                                                                              │
│  │ ID: 0xb9e8c31d1557825b8a93ad695213056a7595e6e546f21b10f794670754375e45                         │
│  │ Owner: Account Address ( 0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a )  │
│  │ Version: 7                                                                                     │
│  │ Digest: CvmwZCtRYqA7hk2qQ4sfWPiqsBaKVqReMbihe8UNFMrD                                           │
│  └──                                                                                              │
│  ┌──                                                                                              │
│  │ ID: 0xc94b31119ae9ab32fb6d72385b5611cfbfcb08a9ba673558345044fd493b202d                         │
│  │ Owner: Immutable                                                                               │
│  │ Version: 1                                                                                     │
│  │ Digest: EM7NCDMp1MYaF9N2tKqUb1sA1BF1pDgp3sNaQqGrybHS                                           │
│  └──                                                                                              │
│ Mutated Objects:                                                                                  │
│  ┌──                                                                                              │
│  │ ID: 0x07904d4deba1a822d52a674205548b36b28859620282479144454d3c096ffa15                         │
│  │ Owner: Account Address ( 0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a )  │
│  │ Version: 7                                                                                     │
│  │ Digest: DGGDwDw6xdJFiLQGEyCxoFh8wh1WcZvUCaetG7uxwUYF                                           │
│  └──                                                                                              │
│ Gas Object:                                                                                       │
│  ┌──                                                                                              │
│  │ ID: 0x07904d4deba1a822d52a674205548b36b28859620282479144454d3c096ffa15                         │
│  │ Owner: Account Address ( 0x5c5882d73a6e5b6ea1743fb028eff5e0d7cc8b7ae123d27856c5fe666d91569a )  │
│  │ Version: 7                                                                                     │
│  │ Digest: DGGDwDw6xdJFiLQGEyCxoFh8wh1WcZvUCaetG7uxwUYF                                           │
│  └──                                                                                              │
│ Gas Cost Summary:                                                                                 │
│    Storage Cost: 15967600 MIST                                                                    │
│    Computation Cost: 1000000 MIST                                                                 │
│    Storage Rebate: 978120 MIST                                                                    │
│    Non-refundable Storage Fee: 9880 MIST                                                          │
│                                                                                                   │
│ Transaction Dependencies:                                                                         │
│    DeqpG2KUFuu3Rkf6rwKaGeYSjwxM1x9r6HqWKeGaqnQZ                                                   │
│    FMmtZmZ6S5oNVUeUuNwvB3fGH44ekyxzzywfSBuHvtjn                                                   │
╰───────────────────────────────────────────────────────────────────────────────────────────────────╯
Execution finished successfully. Local and on-chain effects match.

3.8.3 replay-batch: 批量交易重放

(1) 命令说明

执行该命令可以批量重放指定文件中的交易,并查看交易执行效果。

Replay transactions listed in a file

Usage: sui client replay-batch [OPTIONS] --path <PATH>

Options:
  -p, --path <PATH>      The path to the file of transaction digests to replay,
                         with one digest per line
  -t, --terminate-early  If an error is encountered during a transaction, this
                         specifies whether to terminate or continue
(2) 命令使用
$ sui client replay-batch --path ./txs.txt 
INFO sui_replay::batch_replay: [1/3] Replaying transaction TransactionDigest(6yE93gtnBzyq9XAtKiJ6bQwrs91B6vVRuK4QucRjKhY9)...
INFO sui_replay::batch_replay: [2/3] Replaying transaction TransactionDigest(EME9x9B8iWHhpUa3BpEKkYgmq1P18jUgC9hQN89kWkxP)...
INFO sui_replay::batch_replay: [3/3] Replaying transaction TransactionDigest(zKFQ9qrHVFHcoJnxcYmUCCHhfDv1uwwzzgEErmAKo7z)...
INFO sui_replay::batch_replay: Replaying transaction TransactionDigest(EME9x9B8iWHhpUa3BpEKkYgmq1P18jUgC9hQN89kWkxP) succeeded
INFO sui_replay::batch_replay: Replaying transaction TransactionDigest(6yE93gtnBzyq9XAtKiJ6bQwrs91B6vVRuK4QucRjKhY9) succeeded
INFO sui_replay::batch_replay: Replaying transaction TransactionDigest(zKFQ9qrHVFHcoJnxcYmUCCHhfDv1uwwzzgEErmAKo7z) succeeded
INFO sui_replay::batch_replay: Finished replaying 3 transactions, took 1.957193059s
INFO sui_replay::batch_replay: All transactions passed

3.8.4 replay-checkpoint: 重放指定一段检查点区间内的所有交易

(1) 命令说明

执行该命令可以重放指定一段检查点区间内的所有交易,并查看交易执行效果。 检查点(checkpoint)类似传统区块链的区块。

Replay all transactions in a range of checkpoints

Usage: sui client replay-checkpoint [OPTIONS] --start <START> --end <END>

Options:
  -s, --start <START>    The starting checkpoint sequence number of the range
                         of checkpoints to replay
  -e, --end <END>        The ending checkpoint sequence number of the range of
                         checkpoints to replay
  -t, --terminate-early  If an error is encountered during a transaction, this
                         specifies whether to terminate or continue
(2) 命令使用
$ sui client replay-checkpoint --start 87468287 --end 87468289

Keytool

1 命令说明

sui keytool的子命令集主要是涉及私钥相关的命令行工具,包括私钥创建、签名验签、单签多签及zkLogin相关命令等。

可通过help/-h获取完整的子命令:

Sui keystore tool

Usage: sui keytool [OPTIONS] <COMMAND>

Commands:
  update-alias                             Update an old alias to a new one. If a new alias is not provided, a random one
                                               will be generated
  convert                                  Convert private key in Hex or Base64 to new format (Bech32 encoded 33 byte flag
                                               || private key starting with "suiprivkey"). Hex private key format import and
                                               export are both deprecated in Sui Wallet and Sui CLI Keystore. Use `sui keytool
                                               import` if you wish to import a key to Sui Keystore
  decode-or-verify-tx                      Given a Base64 encoded transaction bytes, decode its components. If a signature
                                               is provided, verify the signature against the transaction and output the result
  decode-multi-sig                         Given a Base64 encoded MultiSig signature, decode its components. If tx_bytes is
                                               passed in, verify the multisig
  generate                                 Generate a new keypair with key scheme flag {ed25519 | secp256k1 | secp256r1}
                                               with optional derivation path, default to m/44'/784'/0'/0'/0' for ed25519 or
                                               m/54'/784'/0'/0/0 for secp256k1 or m/74'/784'/0'/0/0 for secp256r1. Word length
                                               can be { word12 | word15 | word18 | word21 | word24} default to word12 if not
                                               specified
  import                                   Add a new key to Sui CLI Keystore using either the input mnemonic phrase or a
                                               Bech32 encoded 33-byte `flag || privkey` starting with "suiprivkey", the key
                                               scheme flag {ed25519 | secp256k1 | secp256r1} and an optional derivation path,
                                               default to m/44'/784'/0'/0'/0' for ed25519 or m/54'/784'/0'/0/0 for secp256k1 or
                                               m/74'/784'/0'/0/0 for secp256r1. Supports mnemonic phrase of word length 12, 15,
                                               18, 21, 24. Set an alias for the key with the --alias flag. If no alias is
                                               provided, the tool will automatically generate one
  export                                   Output the private key of the given key identity in Sui CLI Keystore as Bech32
                                               encoded string starting with `suiprivkey`
  list                                     List all keys by its Sui address, Base64 encoded public key, key scheme name in
                                               sui.keystore
  load-keypair                             This reads the content at the provided file path. The accepted format can be
                                               [enum SuiKeyPair] (Base64 encoded of 33-byte `flag || privkey`) or `type
                                               AuthorityKeyPair` (Base64 encoded `privkey`). This prints out the account keypair
                                               as Base64 encoded `flag || privkey`, the network keypair, worker keypair,
                                               protocol keypair as Base64 encoded `privkey`
  multi-sig-address                        To MultiSig Sui Address. Pass in a list of all public keys `flag || pk` in
                                               Base64. See `keytool list` for example public keys
  multi-sig-combine-partial-sig            Provides a list of participating signatures (`flag || sig || pk` encoded in
                                               Base64), threshold, a list of all public keys and a list of their weights that
                                               define the MultiSig address. Returns a valid MultiSig signature and its sender
                                               address. The result can be used as signature field for `sui client
                                               execute-signed-tx`. The sum of weights of all signatures must be >= the threshold
  multi-sig-combine-partial-sig-legacy     
  show                                     Read the content at the provided file path. The accepted format can be [enum
                                               SuiKeyPair] (Base64 encoded of 33-byte `flag || privkey`) or `type
                                               AuthorityKeyPair` (Base64 encoded `privkey`). It prints its Base64 encoded public
                                               key and the key scheme flag
  sign                                     Create signature using the private key for for the given address (or its alias)
                                               in sui keystore. Any signature commits to a [struct IntentMessage] consisting of
                                               the Base64 encoded of the BCS serialized transaction bytes itself and its intent.
                                               If intent is absent, default will be used
  sign-kms                                 Creates a signature by leveraging AWS KMS. Pass in a key-id to leverage Amazon
                                               KMS to sign a message and the base64 pubkey. Generate PubKey from pem using
                                               MystenLabs/base64pemkey Any signature commits to a [struct IntentMessage]
                                               consisting of the Base64 encoded of the BCS serialized transaction bytes itself
                                               and its intent. If intent is absent, default will be used
  unpack                                   This takes [enum SuiKeyPair] of Base64 encoded of 33-byte `flag || privkey`). It
                                               outputs the keypair into a file at the current directory where the address is the
                                               filename, and prints out its Sui address, Base64 encoded public key, the key
                                               scheme, and the key scheme flag
  zk-login-sign-and-execute-tx             Given the max_epoch, generate an OAuth url, ask user to paste the redirect with
                                               id_token, call salt server, then call the prover server, create a test
                                               transaction, use the ephemeral key to sign and execute it by assembling to a
                                               serialized zkLogin signature
  zk-login-enter-token                     A workaround to the above command because sometimes token pasting does not work
                                               (for Facebook). All the inputs required here are printed from the command above
  zk-login-sig-verify                      Given a zkLogin signature, parse it if valid. If `bytes` provided, parse it as
                                               either as TransactionData or PersonalMessage based on `intent_scope`. It verifies
                                               the zkLogin signature based its latest JWK fetched. Example request: sui keytool
                                               zk-login-sig-verify --sig $SERIALIZED_ZKLOGIN_SIG --bytes $BYTES --intent-scope 0
                                               --network devnet --curr-epoch 10
  zk-login-insecure-sign-personal-message  TESTING ONLY: Generate a fixed ephemeral key and its JWT token with test issuer.
                                               Produce a zklogin signature for the given data and max epoch. e.g. sui keytool
                                               zk-login-insecure-sign-personal-message --data "hello" --max-epoch 5

2 命令分类

根据命令实现功能分为以下几类:

2.1 密钥对类

子命令功能说明
generate生成密钥对
show查看密钥
unpack解码密钥
convert密钥格式转换
import导入私钥到Sui CLI Keystore
export导出私钥
list列出本地Keystore所有私钥对应的地址及公钥等信息
update-alias更新别名
load-keypair加载多种类型的密钥对

2.2 单签类

子命令功能说明
sign交易签名
decode-or-verify-tx交易解码或签名交易验签

2.3 多签类

子命令功能说明
multi-sig-address创建多重签名地址
multi-sig-combine-partial-sig合并签名
decode-multi-sig多签解码

2.4 zkLogin类

子命令功能说明
zk-login-sign-and-execute-tx zkLogin登录、验签、执行接口
zk-login-sig-verify zkLogin签名验证

3 命令详解

3.1 链网络类

3.1.1 generate:生成密钥对

(1) 命令说明

执行该命令可以生成新密钥对,用户可以指定密钥方案和派生路径,若不指定将使用默认值,默认助记词长度为12个单词

Generate a new keypair with key scheme flag {ed25519 | secp256k1 | secp256r1} with optional derivation path, default to m/44'/784'/0'/0'/0' for ed25519 or m/54'/784'/0'/0/0 for secp256k1 or m/74'/784'/0'/0/0 for secp256r1. Word length can be { word12 | word15 | word18 | word21 | word24} default to word12 if not specified

Usage: sui keytool generate [OPTIONS] <KEY_SCHEME> [DERIVATION_PATH] [WORD_LENGTH]

Arguments:
  <KEY_SCHEME>
  [DERIVATION_PATH]
  [WORD_LENGTH]
KEY SCHEMEDEFAULT DERIVATION PATH
ed25519m/44'/784'/0'/0'/0'
secp256k1m/54'/784'/0'/0/0
secp256r1m/74'/784'/0'/0/0
(2) 命令使用
  • 直接创建
$ sui keytool generate ed25519
╭─────────────────┬────────────────────────────────────────────────────────────────────────────────────────────╮
│ alias           │                                                                                            │
│ suiAddress      │  0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4                        │
│ publicBase64Key │  AGwzSkAPgy3Pkgl+ZFVZWfdFQz9COw46z4RX2WKCRY4a                                              │
│ keyScheme       │  ed25519                                                                                   │
│ flag            │  0                                                                                         │
│ mnemonic        │  fetch mushroom celery actress capital clip update neutral kangaroo deal picture identify  │
│ peerId          │  6c334a400f832dcf92097e64555959f745433f423b0e3acf8457d96282458e1a                          │
╰─────────────────┴────────────────────────────────────────────────────────────────────────────────────────────╯

# 本地生成私钥文件
$ cat 0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4.key 
AEXleFEKuJrNh9E3kpvwLqdsfeQfuliUQQ61cvJVfq4N
  • 指定派生路径和助记词长度创建
$ sui keytool generate secp256k1 "m/54'/784'/0'/0/0" word15
╭─────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ alias           │                                                                                                        │
│ suiAddress      │  0x5f5bc9a9d12da2210ad43be0d8120989c280a409e2bed53691a05ca505c8f93a                                    │
│ publicBase64Key │  AQNxN3EAZbPLFUNT9z8HgnaNlRumUvGSmawdr1igYMVCrg==                                                      │
│ keyScheme       │  secp256k1                                                                                             │
│ flag            │  1                                                                                                     │
│ mnemonic        │  over shaft village combine puzzle muscle fantasy relief early select acquire boy tourist hybrid fine  │
│ peerId          │                                                                                                        │
╰─────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────╯

# 本地生成私钥文件
$ cat 0x5f5bc9a9d12da2210ad43be0d8120989c280a409e2bed53691a05ca505c8f93a.key 
Af+2Y9EdQuFUUePBcUfbJHduivR+cg794FrmrvBS1rXf

3.1.2 show:查看密钥

(1)命令说明

执行该命令可以读取指定路径的私钥文件,并打印Base64编码公钥、密钥方案等信息。

Read the content at the provided file path. The accepted format can be [enum SuiKeyPair] (Base64 encoded of 33-byte `flag || privkey`) or `type AuthorityKeyPair` (Base64 encoded `privkey`). It prints its Base64 encoded public key and the key scheme flag

Usage: sui keytool show [OPTIONS] <FILE>

Arguments:
  <FILE>
(2)命令使用

输出内容跟generate生成的一致。

$ sui keytool show ./0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4.key
╭─────────────────┬──────────────────────────────────────────────────────────────────────╮
│ alias           │                                                                      │
│ suiAddress      │  0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4  │
│ publicBase64Key │  AGwzSkAPgy3Pkgl+ZFVZWfdFQz9COw46z4RX2WKCRY4a                        │
│ keyScheme       │  ed25519                                                             │
│ flag            │  0                                                                   │
│ peerId          │  6c334a400f832dcf92097e64555959f745433f423b0e3acf8457d96282458e1a    │
╰─────────────────┴──────────────────────────────────────────────────────────────────────╯

3.1.3 unpack:解码密钥

(1)命令说明

该命令跟show命令输出内容一致,只不过show是查看密钥文件,而unpack是直接解析密钥。

This takes [enum SuiKeyPair] of Base64 encoded of 33-byte `flag || privkey`). It outputs the keypair into a file at the current directory where the address is the filename, and prints out its Sui address, Base64 encoded public key, the key scheme, and the key scheme flag

Usage: sui keytool unpack [OPTIONS] <KEYPAIR>

Arguments:
  <KEYPAIR>
(2)命令使用

内容跟show命令输出完全一致。

$ sui keytool unpack AEXleFEKuJrNh9E3kpvwLqdsfeQfuliUQQ61cvJVfq4N
╭─────────────────┬──────────────────────────────────────────────────────────────────────╮
│ alias           │                                                                      │
│ suiAddress      │  0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4  │
│ publicBase64Key │  AGwzSkAPgy3Pkgl+ZFVZWfdFQz9COw46z4RX2WKCRY4a                        │
│ keyScheme       │  ed25519                                                             │
│ flag            │  0                                                                   │
│ peerId          │  6c334a400f832dcf92097e64555959f745433f423b0e3acf8457d96282458e1a    │
╰─────────────────┴──────────────────────────────────────────────────────────────────────╯

3.1.4 convert:密钥格式转换

(1)命令说明

执行该命令可以将私钥从十六进制或Base64编码的格式,转换为新格式。

Convert private key in Hex or Base64 to new format (Bech32 encoded 33 byte flag || private key starting with "suiprivkey"). Hex private key format import and export are both deprecated in Sui Wallet and Sui
CLI Keystore. Use `sui keytool import` if you wish to import a key to Sui Keystore

Usage: sui keytool convert [OPTIONS] <VALUE>

Arguments:
  <VALUE>
(2)命令使用
$ sui keytool  convert AEXleFEKuJrNh9E3kpvwLqdsfeQfuliUQQ61cvJVfq4N
╭────────────────┬──────────────────────────────────────────────────────────────────────────╮
│ bech32WithFlag │  suiprivkey1qpz727z3p2uf4nv86yme9xls96nkcl0yr7a939zpp66h9uj406hq6myd3l7  │
│ base64WithFlag │  AEXleFEKuJrNh9E3kpvwLqdsfeQfuliUQQ61cvJVfq4N                            │
│ hexWithoutFlag │  45e578510ab89acd87d137929bf02ea76c7de41fba5894410eb572f2557eae0d        │
│ scheme         │  ed25519                                                                 │
╰────────────────┴──────────────────────────────────────────────────────────────────────────╯

3.1.5 import:导入私钥到Sui CLI Keystore

(1)命令说明

执行该命令可以添加私钥到Sui命令行的Keystore中。可以使用助记词、编码后的私钥作为输入字符串,并指定密钥方案,及可选的派生路径。还可支持指定别名,若未指定,将随机生成别名。

Add a new key to Sui CLI Keystore using either the input mnemonic phrase or a Bech32 encoded 33-byte `flag || privkey` starting with "suiprivkey", the key scheme flag {ed25519 | secp256k1 | secp256r1} and an
optional derivation path, default to m/44'/784'/0'/0'/0' for ed25519 or m/54'/784'/0'/0/0 for secp256k1 or m/74'/784'/0'/0/0 for secp256r1. Supports mnemonic phrase of word length 12, 15, 18, 21, 24. Set an
alias for the key with the --alias flag. If no alias is provided, the tool will automatically generate one

Usage: sui keytool import [OPTIONS] <INPUT_STRING> <KEY_SCHEME> [DERIVATION_PATH]

Arguments:
  <INPUT_STRING>
  <KEY_SCHEME>
  [DERIVATION_PATH]

Options:
      --alias <ALIAS>  Sets an alias for this address. The alias must start with a letter and can contain only letters, digits, hyphens (-), or underscores (_)
(2)命令使用
  • 使用已编码私钥导入

注:需要使用新格式的私钥,可以使用convert命令进行转换得到。

$ sui keytool import --alias yas suiprivkey1qpz727z3p2uf4nv86yme9xls96nkcl0yr7a939zpp66h9uj406hq6myd3l7 ed25519

$ sui keytool list
╭────────────────────────────────────────────────────────────────────────────────────────────╮
│ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ alias           │  yas                                                                 │ │
│ │ suiAddress      │  0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4  │ │
│ │ publicBase64Key │  AGwzSkAPgy3Pkgl+ZFVZWfdFQz9COw46z4RX2WKCRY4a                        │ │
│ │ keyScheme       │  ed25519                                                             │ │
│ │ flag            │  0                                                                   │ │
│ │ peerId          │  6c334a400f832dcf92097e64555959f745433f423b0e3acf8457d96282458e1a    │ │
│ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
╰────────────────────────────────────────────────────────────────────────────────────────────╯
  • 使用助记词导入

注:当前版本(04ed3b62103cf5cef07bba9416679cf55715c22b)如果导入助记词,--alias参数不生效,已提PR#17935

$ sui keytool import --alias yoy "over shaft village combine puzzle muscle fantasy relief early select acquire boy tourist hybrid fine" secp256k1

$ sui keytool list
╭────────────────────────────────────────────────────────────────────────────────────────────╮
│ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ alias           │  yas                                                                 │ │
│ │ suiAddress      │  0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4  │ │
│ │ publicBase64Key │  AGwzSkAPgy3Pkgl+ZFVZWfdFQz9COw46z4RX2WKCRY4a                        │ │
│ │ keyScheme       │  ed25519                                                             │ │
│ │ flag            │  0                                                                   │ │
│ │ peerId          │  6c334a400f832dcf92097e64555959f745433f423b0e3acf8457d96282458e1a    │ │
│ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
│ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ alias           │  yoy                                                                 │ │
│ │ suiAddress      │  0x5f5bc9a9d12da2210ad43be0d8120989c280a409e2bed53691a05ca505c8f93a  │ │
│ │ publicBase64Key │  AQNxN3EAZbPLFUNT9z8HgnaNlRumUvGSmawdr1igYMVCrg==                    │ │
│ │ keyScheme       │  secp256k1                                                           │ │
│ │ flag            │  1                                                                   │ │
│ │ peerId          │                                                                      │ │
│ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
╰────────────────────────────────────────────────────────────────────────────────────────────╯

3.1.6 export:导出私钥

(1)命令说明

执行该命令可以根据密钥标识(别名or地址)导出Sui CLI Keystore中私钥,以及公钥等信息。

Output the private key of the given key identity in Sui CLI Keystore as Bech32 encoded string starting with `suiprivkey`

Usage: sui keytool export [OPTIONS] --key-identity <KEY_IDENTITY>

Options:
      --key-identity <KEY_IDENTITY>
(2)命令使用
  • 使用别名
$ sui keytool export --key-identity yas
╭────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────╮
│ exportedPrivateKey │  suiprivkey1qpz727z3p2uf4nv86yme9xls96nkcl0yr7a939zpp66h9uj406hq6myd3l7                    │
│ key                │ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│                    │ │ alias           │  yas                                                                 │ │
│                    │ │ suiAddress      │  0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4  │ │
│                    │ │ publicBase64Key │  AGwzSkAPgy3Pkgl+ZFVZWfdFQz9COw46z4RX2WKCRY4a                        │ │
│                    │ │ keyScheme       │  ed25519                                                             │ │
│                    │ │ flag            │  0                                                                   │ │
│                    │ │ peerId          │  6c334a400f832dcf92097e64555959f745433f423b0e3acf8457d96282458e1a    │ │
│                    │ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
╰────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────╯
  • 使用地址
$ sui keytool export --key-identity 0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4
╭────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────╮
│ exportedPrivateKey │  suiprivkey1qpz727z3p2uf4nv86yme9xls96nkcl0yr7a939zpp66h9uj406hq6myd3l7                    │
│ key                │ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│                    │ │ alias           │  yas                                                                 │ │
│                    │ │ suiAddress      │  0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4  │ │
│                    │ │ publicBase64Key │  AGwzSkAPgy3Pkgl+ZFVZWfdFQz9COw46z4RX2WKCRY4a                        │ │
│                    │ │ keyScheme       │  ed25519                                                             │ │
│                    │ │ flag            │  0                                                                   │ │
│                    │ │ peerId          │  6c334a400f832dcf92097e64555959f745433f423b0e3acf8457d96282458e1a    │ │
│                    │ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
╰────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────╯

3.1.7 list:列出本地Keystore所有私钥对应的地址及公钥等信息

(1)命令说明

执行该命令可以列出本地Sui Keystore中,默认路径:~/.sui/sui_config/sui.keystore,所有私钥对应的地址、公钥等信息。

List all keys by its Sui address, Base64 encoded public key, key scheme name in sui.keystore

Usage: sui keytool list [OPTIONS]

Options:
  -s, --sort-by-alias  Sort by alias
(2)命令使用
$ sui keytool list
╭────────────────────────────────────────────────────────────────────────────────────────────╮
│ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ alias           │  yas                                                                 │ │
│ │ suiAddress      │  0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4  │ │
│ │ publicBase64Key │  AGwzSkAPgy3Pkgl+ZFVZWfdFQz9COw46z4RX2WKCRY4a                        │ │
│ │ keyScheme       │  ed25519                                                             │ │
│ │ flag            │  0                                                                   │ │
│ │ peerId          │  6c334a400f832dcf92097e64555959f745433f423b0e3acf8457d96282458e1a    │ │
│ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
╰────────────────────────────────────────────────────────────────────────────────────────────╯

3.1.8 update-alias:更新别名

(1)命令说明

命令该命令可以修改别名,如果没有指定,将使用随机别名。

Update an old alias to a new one. If a new alias is not provided, a random one will be generated

Usage: sui keytool update-alias [OPTIONS] <OLD_ALIAS> [NEW_ALIAS]

Arguments:
  <OLD_ALIAS>
  [NEW_ALIAS]  The alias must start with a letter and can contain only letters, digits, dots, hyphens (-), or underscores (_)
(2)命令使用
  • 修改为指定别名
$ sui keytool update-alias yas yasmine
Old alias yas was updated to yasmine

$ sui keytool list
╭────────────────────────────────────────────────────────────────────────────────────────────╮
│ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ alias           │  yasmine                                                             │ │
│ │ suiAddress      │  0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4  │ │
│ │ publicBase64Key │  AGwzSkAPgy3Pkgl+ZFVZWfdFQz9COw46z4RX2WKCRY4a                        │ │
│ │ keyScheme       │  ed25519                                                             │ │
│ │ flag            │  0                                                                   │ │
│ │ peerId          │  6c334a400f832dcf92097e64555959f745433f423b0e3acf8457d96282458e1a    │ │
│ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
╰────────────────────────────────────────────────────────────────────────────────────────────╯
  • 修改为随机别名
$ sui keytool update-alias yoy
Old alias yoy was updated to festive-quartz

$ sui keytool list
╭────────────────────────────────────────────────────────────────────────────────────────────╮
│ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ alias           │  festive-quartz                                                      │ │
│ │ suiAddress      │  0x5f5bc9a9d12da2210ad43be0d8120989c280a409e2bed53691a05ca505c8f93a  │ │
│ │ publicBase64Key │  AQNxN3EAZbPLFUNT9z8HgnaNlRumUvGSmawdr1igYMVCrg==                    │ │
│ │ keyScheme       │  secp256k1                                                           │ │
│ │ flag            │  1                                                                   │ │
│ │ peerId          │                                                                      │ │
│ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
╰────────────────────────────────────────────────────────────────────────────────────────────╯

3.1.9 load-keypair:加载多种类型的密钥对

(1)命令说明

执行该命令可以加载多种类型的密钥对。

This reads the content at the provided file path. The accepted format can be [enum SuiKeyPair] (Base64 encoded of 33-byte `flag || privkey`) or `type AuthorityKeyPair` (Base64 encoded `privkey`).

This prints out the account keypair as Base64 encoded `flag || privkey`, the network keypair, worker keypair, protocol keypair as Base64 encoded `privkey`

Usage: sui keytool load-keypair [OPTIONS] <FILE>

Arguments:
  <FILE>
(2)命令使用
$ sui keytool  load-keypair  ~/0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4.key 
╭────────────────┬────────────────────────────────────────────────╮
│ accountKeypair │  AEXleFEKuJrNh9E3kpvwLqdsfeQfuliUQQ61cvJVfq4N  │
│ networkKeypair │  ReV4UQq4ms2H0TeSm/Aup2x95B+6WJRBDrVy8lV+rg0=  │
│ workerKeypair  │  ReV4UQq4ms2H0TeSm/Aup2x95B+6WJRBDrVy8lV+rg0=  │
│ keyScheme      │  ed25519                                       │
╰────────────────┴────────────────────────────────────────────────╯

3.2 单签类

3.2.1 sign:交易签名

(1)命令说明

执行该命令会使用Keystone中地址或别名对应的私钥进行交易签名。

Create signature using the private key for for the given address (or its alias) in sui keystore.

Any signature commits to a [struct IntentMessage] consisting of the Base64 encoded of the BCS serialized transaction bytes itself and its intent. If intent is absent, default will be used

Usage: sui keytool sign [OPTIONS] --address <ADDRESS> --data <DATA>

Options:
      --address <ADDRESS>
      --data <DATA>
      --json               Return command outputs in json format
      --intent <INTENT>
(2)命令使用
  • 构造待签名数据
$ sui client pay-all-sui --input-coins 0xaeda80a73b6eca09e705f3fe4c0980ff6bbd11682822770506d985eefeeb9175 --recip
ient yoy --gas-budget 10000000 --serialize-unsigned-transaction

AAABACBfW8mp0S2iIQrUO+DYEgmJwoCkCeK+1TaRoFylBcj5OgEBAQABAAAKtkW20cU2U0pqLPRhY5Co9JIrD4sSfklKfrn93nCsxAGu2oCnO27KCecF8/5MCYD/a70RaCgidwUG2YXu/uuRdR3TDwAAAAAAIKFBk8eQNjePP9XUGoGEheBe+6FWVN6F3cpznptxrIprCrZFttHFNlNKaiz0YWOQqPSSKw+LEn5JSn65/d5wrMToAwAAAAAAAICWmAAAAAAAAA==
  • 交易签名
$ sui keytool sign --address yas --data AAABACBfW8mp0S2iIQrUO+DYEgmJwoCkCeK+1TaRoFylBcj5OgEBAQABAAAKtkW20cU2U0pqLPRhY5Co9JIrD4sSfklKfrn93nCsxAGu2oCnO27KCecF8/5MCYD/a70RaCgidwUG2YXu/uuRdR3TDwAAAAAAIKFBk8eQNjePP9XUGoGEheBe+6FWVN6F3cpznptxrIprCrZFttHFNlNKaiz0YWOQqPSSKw+LEn5JSn65/d5wrMToAwAAAAAAAICWmAAAAAAAAA==

╭──────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ suiAddress   │ 0x0ab645b6d1c536534a6a2cf4616390a8f4922b0f8b127e494a7eb9fdde70acc4                                                                                               │
│ rawTxData    │ AAABACBfW8mp0S2iIQrUO+DYEgmJwoCkCeK+1TaRoFylBcj5OgEBAQABAAAKtkW20cU2U0pqLPRhY5Co9JIrD4sSfklKfrn93nCsxAGu2oCnO27KCecF8/5MCYD/a70RaCgidwUG2YXu/uuRdR3TDwAAAAAAIKFB │
│              │ k8eQNjePP9XUGoGEheBe+6FWVN6F3cpznptxrIprCrZFttHFNlNKaiz0YWOQqPSSKw+LEn5JSn65/d5wrMToAwAAAAAAAICWmAAAAAAAAA==                                                     │
│ intent       │ ╭─────────┬─────╮                                                                                                                                                │
│              │ │ scope   │  0  │                                                                                                                                                │
│              │ │ version │  0  │                                                                                                                                                │
│              │ │ app_id  │  0  │                                                                                                                                                │
│              │ ╰─────────┴─────╯                                                                                                                                                │
│ rawIntentMsg │ AAAAAAABACBfW8mp0S2iIQrUO+DYEgmJwoCkCeK+1TaRoFylBcj5OgEBAQABAAAKtkW20cU2U0pqLPRhY5Co9JIrD4sSfklKfrn93nCsxAGu2oCnO27KCecF8/5MCYD/a70RaCgidwUG2YXu/uuRdR3TDwAAAAAA │
│              │ IKFBk8eQNjePP9XUGoGEheBe+6FWVN6F3cpznptxrIprCrZFttHFNlNKaiz0YWOQqPSSKw+LEn5JSn65/d5wrMToAwAAAAAAAICWmAAAAAAAAA==                                                 │
│ digest       │ ZeckWeZmL+knRiNvYByrgOg9ce/g1N2JoFT5w86nw7I=                                                                                                                     │
│ suiSignature │ AEM7o1w9vXOzUwbT2jxmZDLHViVwUDVh0vHk14ykTwQzBI7LVrrv6kdllbXO/rpvVfxjwL9H6EU4uWK5E+8yDwJsM0pAD4Mtz5IJfmRVWVn3RUM/QjsOOs+EV9ligkWOGg==                             │
╰──────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

image-20240525234237406

3.2.2 decode-or-verify-tx:交易解码或签名交易验签

(1)命令说明

执行该命令会对Base64编码的交易进行解码,如果提供了签名信息,还会进行交易验签,并输出验签结果。

Given a Base64 encoded transaction bytes, decode its components.

If a signature is provided, verify the signature against the transaction and output the result

Usage: sui keytool decode-or-verify-tx [OPTIONS] --tx-bytes <TX_BYTES>

Options:
      --tx-bytes <TX_BYTES>
      --json                   Return command outputs in json format
      --sig <SIG>
      --cur-epoch <CUR_EPOCH>  [default: 0]
(2)命令使用
  • 交易解码
$ sui keytool decode-or-verify-tx --tx-bytes AAABACBfW8mp0S2iIQrUO+DYEgmJwoCkCeK+1TaRoFylBcj5OgEBAQABAAAKtkW20cU2U0pqLPRhY5Co9JIrD4sSfklKfrn93nCsxAGu2oCnO27KCecF8/5MCYD/a70RaCgidwUG2YXu/uuRdR3TDwAAAAAAIKFBk8eQNjePP9XUGoGEheBe+6FWVN6F3cpznptxrIprCrZFttHFNlNKaiz0YWOQqPSSKw+LEn5JSn65/d5wrMToAwAAAAAAAICWmAAAAAAAAA==
image-20240525235257649
  • 交易解码及验签
$ sui keytool decode-or-verify-tx --tx-bytes AAABACBfW8mp0S2iIQrUO+DYEgmJwoCkCeK+1TaRoFylBcj5OgEBAQABAAAKtkW20cU2U0pqLPRhY5Co9JIrD4sSfklKfrn93nCsxAGu2oCnO27KCecF8/5MCYD/a70RaCgidwUG2YXu/uuRdR3TDwAAAAAAIKFBk8eQNjePP9XUGoGEheBe+6FWVN6F3cpznptxrIprCrZFttHFNlNKaiz0YWOQqPSSKw+LEn5JSn65/d5wrMToAwAAAAAAAICWmAAAAAAAAA== --sig AEM7o1w9vXOzUwbT2jxmZDLHViVwUDVh0vHk14ykTwQzBI7LVrrv6kdllbXO/rpvVfxjwL9H6EU4uWK5E+8yDwJsM0pAD4Mtz5IJfmRVWVn3RUM/QjsOOs+EV9ligkWOGg==

输出中上半部分是交易解码后的内容,下半部分是验签结果:

image-20240525234956252

3.3 多签类

Sui 支持多重签名(multisig)交易,这需要多个密钥进行授权,而不是单密钥签名。

Sui 支持 kn 多重签名交易,其中 k 是阈值,n 是所有参与方的总权重。最大参与方数量为 10

多重签名的有效参与密钥是纯 Ed25519ECDSA Secp256k1ECDSA Secp256r1

如果序列化的多重签名包含足够数量的有效签名,其权重之和超过阈值,SUI 将视为多重签名有效,并执行交易。

3.3.1 multi-sig-address:创建多重签名地址

(1)命令说明
To MultiSig Sui Address. Pass in a list of all public keys `flag || pk` in Base64. See `keytool list` for example public keys

Usage: sui keytool multi-sig-address [OPTIONS] --threshold <THRESHOLD>

Options:
      --threshold <THRESHOLD>
      --json                   Return command outputs in json format
      --pks <PKS>...
      --weights <WEIGHTS>...
(2)命令使用
  • 创建3个不同密钥方案的地址
$ sui client new-address ed25519 js1
╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Created new keypair and saved it to keystore.                                                        │
├────────────────┬─────────────────────────────────────────────────────────────────────────────────────┤
│ alias          │ js1                                                                                 │
│ address        │ 0x1a142b16f802832d5ab2247ab89fffae686168088b16d93aabb2e53f1cdc99f0                  │
│ keyScheme      │ ed25519                                                                             │
│ recoveryPhrase │ wait virtual cushion tornado cover grain excuse warfare dwarf runway satisfy crater │
╰────────────────┴─────────────────────────────────────────────────────────────────────────────────────╯

$ sui client new-address secp256k1 js2
╭───────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Created new keypair and saved it to keystore.                                                     │
├────────────────┬──────────────────────────────────────────────────────────────────────────────────┤
│ alias          │ js2                                                                              │
│ address        │ 0x895d63a8db8bdb07bf450adcaa47fea03cfb1082f4c379b74493a1386b730d6f               │
│ keyScheme      │ secp256k1                                                                        │
│ recoveryPhrase │ disorder fiber mention december scrap wreck curtain option emotion any keen away │
╰────────────────┴──────────────────────────────────────────────────────────────────────────────────╯

$ sui client new-address secp256r1 js3
╭─────────────────────────────────────────────────────────────────────────────────────────╮
│ Created new keypair and saved it to keystore.                                           │
├────────────────┬────────────────────────────────────────────────────────────────────────┤
│ alias          │ js3                                                                    │
│ address        │ 0x60b8928150fe7a43bbb83dc156b315d3624dd3d2a4178026892e64fe1ab0fd43     │
│ keyScheme      │ secp256r1                                                              │
│ recoveryPhrase │ steak lake dune ski banner eternal debate pond figure tell leave faith │
╰────────────────┴────────────────────────────────────────────────────────────────────────╯
  • 将地址添加到环境变量
export JS1=0x1a142b16f802832d5ab2247ab89fffae686168088b16d93aabb2e53f1cdc99f0
export JS2=0x895d63a8db8bdb07bf450adcaa47fea03cfb1082f4c379b74493a1386b730d6f
export JS3=0x60b8928150fe7a43bbb83dc156b315d3624dd3d2a4178026892e64fe1ab0fd43
  • 查看本地keystore
sui keytool list
╭────────────────────────────────────────────────────────────────────────────────────────────╮
│ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ alias           │  js1                                                                 │ │
│ │ suiAddress      │  0x1a142b16f802832d5ab2247ab89fffae686168088b16d93aabb2e53f1cdc99f0  │ │
│ │ publicBase64Key │  AOMfccBag19KFImrHhmJK+8e4W96vTHIqeuvojzQbU3h                        │ │
│ │ keyScheme       │  ed25519                                                             │ │
│ │ flag            │  0                                                                   │ │
│ │ peerId          │  e31f71c05a835f4a1489ab1e19892bef1ee16f7abd31c8a9ebafa23cd06d4de1    │ │
│ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
│ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ alias           │  js3                                                                 │ │
│ │ suiAddress      │  0x60b8928150fe7a43bbb83dc156b315d3624dd3d2a4178026892e64fe1ab0fd43  │ │
│ │ publicBase64Key │  AgI7OJmvbQSHY1GKBIEoA/kiZt/PtPJEBM+hBGVCOw0Vzw==                    │ │
│ │ keyScheme       │  secp256r1                                                           │ │
│ │ flag            │  2                                                                   │ │
│ │ peerId          │                                                                      │ │
│ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
│ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ alias           │  js2                                                                 │ │
│ │ suiAddress      │  0x895d63a8db8bdb07bf450adcaa47fea03cfb1082f4c379b74493a1386b730d6f  │ │
│ │ publicBase64Key │  AQLX+tlpa2ylw057I7nBjyxza9QZQIi0n83KMBSrP7qL2A==                    │ │
│ │ keyScheme       │  secp256k1                                                           │ │
│ │ flag            │  1                                                                   │ │
│ │ peerId          │                                                                      │ │
│ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │
╰────────────────────────────────────────────────────────────────────────────────────────────╯
  • 创建多重签名
export PK1=AOMfccBag19KFImrHhmJK+8e4W96vTHIqeuvojzQbU3h
export PK2=AQLX+tlpa2ylw057I7nBjyxza9QZQIi0n83KMBSrP7qL2A==
export PK3=AgI7OJmvbQSHY1GKBIEoA/kiZt/PtPJEBM+hBGVCOw0Vzw== 

$ sui keytool multi-sig-address --pks $PK1 $PK2 $PK3 --weights 2 1 1 --threshold 2
╭─────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────╮
│ multisigAddress │  0xdb4aad8bb29537e7cdbfca302a8f08fae33413d911bb38ae723dd022834976d0                            │
│ multisig        │ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │
│                 │ │ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │ │
│                 │ │ │ address         │  0x1a142b16f802832d5ab2247ab89fffae686168088b16d93aabb2e53f1cdc99f0  │ │ │
│                 │ │ │ publicBase64Key │  AOMfccBag19KFImrHhmJK+8e4W96vTHIqeuvojzQbU3h                        │ │ │
│                 │ │ │ weight          │  2                                                                   │ │ │
│                 │ │ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │ │
│                 │ │ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │ │
│                 │ │ │ address         │  0x895d63a8db8bdb07bf450adcaa47fea03cfb1082f4c379b74493a1386b730d6f  │ │ │
│                 │ │ │ publicBase64Key │  AQLX+tlpa2ylw057I7nBjyxza9QZQIi0n83KMBSrP7qL2A==                    │ │ │
│                 │ │ │ weight          │  1                                                                   │ │ │
│                 │ │ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │ │
│                 │ │ ╭─────────────────┬──────────────────────────────────────────────────────────────────────╮ │ │
│                 │ │ │ address         │  0x60b8928150fe7a43bbb83dc156b315d3624dd3d2a4178026892e64fe1ab0fd43  │ │ │
│                 │ │ │ publicBase64Key │  AgI7OJmvbQSHY1GKBIEoA/kiZt/PtPJEBM+hBGVCOw0Vzw==                    │ │ │
│                 │ │ │ weight          │  1                                                                   │ │ │
│                 │ │ ╰─────────────────┴──────────────────────────────────────────────────────────────────────╯ │ │
│                 │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ │
╰─────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────╯

$ export MULT_ADDR=0xdb4aad8bb29537e7cdbfca302a8f08fae33413d911bb38ae723dd022834976d0
  • 查看余额

通过领水或其他账户转入资金到多签地址。

$ sui client gas $MULT_ADDR
╭────────────────────────────────────────────────────────────────────┬────────────────────┬──────────────────╮
│ gasCoinId                                                          │ mistBalance (MIST) │ suiBalance (SUI) │
├────────────────────────────────────────────────────────────────────┼────────────────────┼──────────────────┤
│ 0x4e550a9a10d92954a1d874aa286a909ae4027647deaad2b884cdd730f0094670 │ 1000000000         │ 1.00             │
│ 0x73f172daa2de2f65d139f0e4d29536a5949bdd37149ae5a8eacc769064da3139 │ 1000000000         │ 1.00             │
╰────────────────────────────────────────────────────────────────────┴────────────────────┴──────────────────╯

3.3.2 multi-sig-combine-partial-sig:合并签名

(1)命令说明
Provides a list of participating signatures (`flag || sig || pk` encoded in Base64), threshold, a list of all public keys and a list of their weights that define the MultiSig address.

Returns a valid MultiSig signature and its sender address. The result can be used as signature field for `sui client execute-signed-tx`. The sum of weights of all signatures must be >= the threshold

Usage: sui keytool multi-sig-address [OPTIONS] --threshold <THRESHOLD>

Options:
      --threshold <THRESHOLD>
      --json                   Return command outputs in json format
      --pks <PKS>...
      --weights <WEIGHTS>...
(2)命令使用
  • 序列化交易

使用--serialize-unsigned-transaction标志以输出Base64编码的交易

注:需要通过--gas参数指定多签地址支付Gas的对象,否则会报错:

Cannot find gas coin for signer address 0x4acf728ebb248340e135386c7e030a29dfc002b7383819b792b2d79c3e897986 with amount sufficient for the required gas budget 100000000. If you are using the pay or transfer commands, you can use pay-sui or transfer-sui commands instead, which will use the only object as gas payment.
$ export COIN=0x73f172daa2de2f65d139f0e4d29536a5949bdd37149ae5a8eacc769064da3139
$ export GAS=0x4e550a9a10d92954a1d874aa286a909ae4027647deaad2b884cdd730f0094670

$ sui client split-coin --coin-id $COIN --amounts 10000 20000 --gas-budget 100000000 --gas $GAS --serialize-unsigned-transaction
AAACAQBz8XLaot4vZdE58OTSlTallJvdNxSa5ajqzHaQZNoxOWfXDQAAAAAAILYJ4ZUHGvglO3LahPvKGQ1oA0F7Z0wcJthFl37yCe5XABECECcAAAAAAAAgTgAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDcGF5CXNwbGl0X3ZlYwEHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDc3VpA1NVSQACAQAAAQEA20qti7KVN+fNv8owKo8I+uM0E9kRuziucj3QIoNJdtABTlUKmhDZKVSh2HSqKGqQmuQCdkfeqtK4hM3XMPAJRnDJAA8AAAAAACDh/18LfCtSaf0IBHcvy1GsTrIhjZCzLdn/V01COkNOE9tKrYuylTfnzb/KMCqPCPrjNBPZEbs4rnI90CKDSXbQ6AMAAAAAAAAA4fUFAAAAAAA=
  • 记录序列化后的交易到环境变量
$ export TX_BYTES=AAACAQBz8XLaot4vZdE58OTSlTallJvdNxSa5ajqzHaQZNoxOWfXDQAAAAAAILYJ4ZUHGvglO3LahPvKGQ1oA0F7Z0wcJthFl37yCe5XABECECcAAAAAAAAgTgAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDcGF5CXNwbGl0X3ZlYwEHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDc3VpA1NVSQACAQAAAQEA20qti7KVN+fNv8owKo8I+uM0E9kRuziucj3QIoNJdtABTlUKmhDZKVSh2HSqKGqQmuQCdkfeqtK4hM3XMPAJRnDJAA8AAAAAACDh/18LfCtSaf0IBHcvy1GsTrIhjZCzLdn/V01COkNOE9tKrYuylTfnzb/KMCqPCPrjNBPZEbs4rnI90CKDSXbQ6AMAAAAAAAAA4fUFAAAAAAA=
  • JS2签名交易

根据前面我们设置的权重和阀值,要么JS1单签名即可,要么JS2JS3多签。我们看多签的情形,分别使用JS2JS3的私钥进行多签。

$ sui keytool sign --address $JS2 --data $TX_BYTES --json
{
  "suiAddress": "0x895d63a8db8bdb07bf450adcaa47fea03cfb1082f4c379b74493a1386b730d6f",
  "rawTxData": "AAACAQBz8XLaot4vZdE58OTSlTallJvdNxSa5ajqzHaQZNoxOWfXDQAAAAAAILYJ4ZUHGvglO3LahPvKGQ1oA0F7Z0wcJthFl37yCe5XABECECcAAAAAAAAgTgAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDcGF5CXNwbGl0X3ZlYwEHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDc3VpA1NVSQACAQAAAQEA20qti7KVN+fNv8owKo8I+uM0E9kRuziucj3QIoNJdtABTlUKmhDZKVSh2HSqKGqQmuQCdkfeqtK4hM3XMPAJRnDJAA8AAAAAACDh/18LfCtSaf0IBHcvy1GsTrIhjZCzLdn/V01COkNOE9tKrYuylTfnzb/KMCqPCPrjNBPZEbs4rnI90CKDSXbQ6AMAAAAAAAAA4fUFAAAAAAA=",
  "intent": {
    "scope": 0,
    "version": 0,
    "app_id": 0
  },
  "rawIntentMsg": "AAAAAAACAQBz8XLaot4vZdE58OTSlTallJvdNxSa5ajqzHaQZNoxOWfXDQAAAAAAILYJ4ZUHGvglO3LahPvKGQ1oA0F7Z0wcJthFl37yCe5XABECECcAAAAAAAAgTgAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDcGF5CXNwbGl0X3ZlYwEHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDc3VpA1NVSQACAQAAAQEA20qti7KVN+fNv8owKo8I+uM0E9kRuziucj3QIoNJdtABTlUKmhDZKVSh2HSqKGqQmuQCdkfeqtK4hM3XMPAJRnDJAA8AAAAAACDh/18LfCtSaf0IBHcvy1GsTrIhjZCzLdn/V01COkNOE9tKrYuylTfnzb/KMCqPCPrjNBPZEbs4rnI90CKDSXbQ6AMAAAAAAAAA4fUFAAAAAAA=",
  "digest": "umxieFeADI2TcONrVrVYfohzEAndWuxX1UMvqKwojJ0=",
  "suiSignature": "Acn184bDbxjv0RVJSaW0WGqG/IJmOSmNun2rLppfaSRdM5rqNepFgP91dWS/HxtqxHOpLMqV46sq0jsVnA+NYmEC1/rZaWtspcNOeyO5wY8sc2vUGUCItJ/NyjAUqz+6i9g="
}
  • JS3签名交易
$ sui keytool sign --address $JS3 --data $TX_BYTES --json
sui keytool sign --address $JS3 --data $TX_BYTES --json
{
  "suiAddress": "0x60b8928150fe7a43bbb83dc156b315d3624dd3d2a4178026892e64fe1ab0fd43",
  "rawTxData": "AAACAQBz8XLaot4vZdE58OTSlTallJvdNxSa5ajqzHaQZNoxOWfXDQAAAAAAILYJ4ZUHGvglO3LahPvKGQ1oA0F7Z0wcJthFl37yCe5XABECECcAAAAAAAAgTgAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDcGF5CXNwbGl0X3ZlYwEHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDc3VpA1NVSQACAQAAAQEA20qti7KVN+fNv8owKo8I+uM0E9kRuziucj3QIoNJdtABTlUKmhDZKVSh2HSqKGqQmuQCdkfeqtK4hM3XMPAJRnDJAA8AAAAAACDh/18LfCtSaf0IBHcvy1GsTrIhjZCzLdn/V01COkNOE9tKrYuylTfnzb/KMCqPCPrjNBPZEbs4rnI90CKDSXbQ6AMAAAAAAAAA4fUFAAAAAAA=",
  "intent": {
    "scope": 0,
    "version": 0,
    "app_id": 0
  },
  "rawIntentMsg": "AAAAAAACAQBz8XLaot4vZdE58OTSlTallJvdNxSa5ajqzHaQZNoxOWfXDQAAAAAAILYJ4ZUHGvglO3LahPvKGQ1oA0F7Z0wcJthFl37yCe5XABECECcAAAAAAAAgTgAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDcGF5CXNwbGl0X3ZlYwEHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDc3VpA1NVSQACAQAAAQEA20qti7KVN+fNv8owKo8I+uM0E9kRuziucj3QIoNJdtABTlUKmhDZKVSh2HSqKGqQmuQCdkfeqtK4hM3XMPAJRnDJAA8AAAAAACDh/18LfCtSaf0IBHcvy1GsTrIhjZCzLdn/V01COkNOE9tKrYuylTfnzb/KMCqPCPrjNBPZEbs4rnI90CKDSXbQ6AMAAAAAAAAA4fUFAAAAAAA=",
  "digest": "umxieFeADI2TcONrVrVYfohzEAndWuxX1UMvqKwojJ0=",
  "suiSignature": "AmDOU/g+mjPsT7dL91nt9APYZwyHuP4Is59A4saH1mWgMwJKdnJpepg6I3hJl8K5eE7nf2IYXxLnVUiw2YOSgSsCOziZr20Eh2NRigSBKAP5Imbfz7TyRATPoQRlQjsNFc8="
}
  • 将签名信息记录到环境变量
export SIG2=Acn184bDbxjv0RVJSaW0WGqG/IJmOSmNun2rLppfaSRdM5rqNepFgP91dWS/HxtqxHOpLMqV46sq0jsVnA+NYmEC1/rZaWtspcNOeyO5wY8sc2vUGUCItJ/NyjAUqz+6i9g=

export SIG3=AmDOU/g+mjPsT7dL91nt9APYZwyHuP4Is59A4saH1mWgMwJKdnJpepg6I3hJl8K5eE7nf2IYXxLnVUiw2YOSgSsCOziZr20Eh2NRigSBKAP5Imbfz7TyRATPoQRlQjsNFc8=
  • 合并签名

执行该命令需要提供需要超过阀值的所有签名,以及所有公钥、权重、定义多签地址的阈值

$ sui keytool multi-sig-combine-partial-sig --pks $PK1 $PK2 $PK3 --weights 2 1 1 --threshold 2 --sigs $SIG2 $SIG3 --json
{
  "multisigAddress": "0xdb4aad8bb29537e7cdbfca302a8f08fae33413d911bb38ae723dd022834976d0",
  "multisigParsed": "AwIByfXzhsNvGO/RFUlJpbRYaob8gmY5KY26fasuml9pJF0zmuo16kWA/3V1ZL8fG2rEc6ksypXjqyrSOxWcD41iYQJgzlP4Ppoz7E+3S/dZ7fQD2GcMh7j+CLOfQOLGh9ZloDMCSnZyaXqYOiN4SZfCuXhO539iGF8S51VIsNmDkoErBgADAOMfccBag19KFImrHhmJK+8e4W96vTHIqeuvojzQbU3hAgEC1/rZaWtspcNOeyO5wY8sc2vUGUCItJ/NyjAUqz+6i9gBAgI7OJmvbQSHY1GKBIEoA/kiZt/PtPJEBM+hBGVCOw0VzwECAA==",
  "multisigSerialized": "AwIByfXzhsNvGO/RFUlJpbRYaob8gmY5KY26fasuml9pJF0zmuo16kWA/3V1ZL8fG2rEc6ksypXjqyrSOxWcD41iYQJgzlP4Ppoz7E+3S/dZ7fQD2GcMh7j+CLOfQOLGh9ZloDMCSnZyaXqYOiN4SZfCuXhO539iGF8S51VIsNmDkoErBgADAOMfccBag19KFImrHhmJK+8e4W96vTHIqeuvojzQbU3hAgEC1/rZaWtspcNOeyO5wY8sc2vUGUCItJ/NyjAUqz+6i9gBAgI7OJmvbQSHY1GKBIEoA/kiZt/PtPJEBM+hBGVCOw0VzwECAA=="
}
  • 执行多签交易
export MULTI_SIGN=AwIByfXzhsNvGO/RFUlJpbRYaob8gmY5KY26fasuml9pJF0zmuo16kWA/3V1ZL8fG2rEc6ksypXjqyrSOxWcD41iYQJgzlP4Ppoz7E+3S/dZ7fQD2GcMh7j+CLOfQOLGh9ZloDMCSnZyaXqYOiN4SZfCuXhO539iGF8S51VIsNmDkoErBgADAOMfccBag19KFImrHhmJK+8e4W96vTHIqeuvojzQbU3hAgEC1/rZaWtspcNOeyO5wY8sc2vUGUCItJ/NyjAUqz+6i9gBAgI7OJmvbQSHY1GKBIEoA/kiZt/PtPJEBM+hBGVCOw0VzwECAA==

sui client execute-signed-tx --tx-bytes $TX_BYTES --signatures $MULTI_SIGN
image-20240528123924034
  • 执行成功

多签交易成功执行,代币已经拆分。

image-20240528124007589

3.3.3 decode-multi-sig:多签解码

(1)命令说明

使用该命令可以将多重签名进行反序列化成人类可读的信息,如果传入了tx_bytes信息,还会进行验签。

Given a Base64 encoded MultiSig signature, decode its components. If tx_bytes is passed in, verify the multisig

Usage: sui keytool decode-multi-sig [OPTIONS] --multisig <MULTISIG>

Options:
      --multisig <MULTISIG>
      --json                   Return command outputs in json format
      --tx-bytes <TX_BYTES>
      --cur-epoch <CUR_EPOCH>  [default: 0]
(2)命令使用
  • 多签解码
$ sui keytool decode-multi-sig --multisig $MULTI_SIG

image-20240528124542818

  • 多签解码及验签
$ sui keytool decode-multi-sig --multisig $MULTI_SIGN --tx-bytes $TX_BYTES

image-20240528124633386

3.4 zkLogin类

3.4.1 zk-login-sign-and-execute-tx :zkLogin登录、验签、执行接口

(1)命令说明

该命令没有实际作用,主要是用来体验zkLogin的整个流程,包括:

  • 创建OAuth授权连接
  • 用户登录后,获取并填入回调连接
  • 访问官方盐值服务器获取盐值
  • 访问零知识证明服务器获取零知识证明
  • 领水
  • 创建测试转账交易
  • 使用临时私钥签名交易
  • 获取零知识证明签名(partialZkLoginSignature
  • 组装成zkLogin签名(zkLoginSignature
Given the max_epoch, generate an OAuth url, ask user to paste the redirect with id_token, call salt server, then call the prover server, create a test transaction, use the ephemeral key to sign and execute it by assembling to a serialized zkLogin signature

Usage: sui keytool zk-login-sign-and-execute-tx [OPTIONS] --max-epoch <MAX_EPOCH>

Options:
      --max-epoch <MAX_EPOCH>
      --json                   Return command outputs in json format
      --network <NETWORK>      [default: devnet]
      --fixed
      --test-multisig
      --sign-with-sk
  • 若设置--test-multisig 将使用多签地址(zkLogin地址+普通公钥地址)
  • 若设置--sign-with-sk将使用传统私钥签名,否则使用zkLogin签名
(2)命令使用

执行命令后,会依次经历以下步骤:

$ sui keytool zk-login-sign-and-execute-tx --max-epoch 70
  • 创建本地临时公私钥对
Ephemeral keypair: Ok("suiprivkey1qr45fwuwmjnjehprdxkvhwkyjmf6qk00mhqdl3xk267q74rgfy7c7er5xmx")
Ephemeral key identifier: 0x73a6b62b367842dfabd4e504b3a0908e41556031a82ec0ca7a4a8be02653ec35
Keys saved as Base64 with 33 bytes `flag || privkey` ($BASE64_STR).
        To see Bech32 format encoding, use `sui keytool export $SUI_ADDRESS` where
        $SUI_ADDRESS can be found with `sui keytool list`. Or use `sui keytool convert $BASE64_STR`.
Ephemeral pubkey (BigInt): 28052518987115222205950885460171773646293850437629977637885534225349525629480
  • 创建随机数
Jwt randomness: 101460194016860753987736196028199365769
  • 创建OAuth认证URL
Visit URL (Google): https://accounts.google.com/o/oauth2/v2/auth?client_id=25769832374-famecqrhe2gkebt5fvqms2263046lj96.apps.googleusercontent.com&response_type=id_token&redirect_uri=https://sui.io/&scope=openid&nonce=3-QOndCJH1FxFKKWagDYtp55tNY
Visit URL (Twitch): https://id.twitch.tv/oauth2/authorize?client_id=rs1bh065i9ya4ydvifixl4kss0uhpt&force_verify=true&lang=en&login_type=login&redirect_uri=https://sui.io/&response_type=id_token&scope=openid&nonce=3-QOndCJH1FxFKKWagDYtp55tNY
Visit URL (Facebook): https://www.facebook.com/v17.0/dialog/oauth?client_id=233307156352917&redirect_uri=https://sui.io/&scope=openid&nonce=3-QOndCJH1FxFKKWagDYtp55tNY&response_type=id_token
Visit URL (Kakao): https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=aa6bddf393b54d4e0d42ae0014edfd2f&redirect_uri=https://sui.io/&nonce=3-QOndCJH1FxFKKWagDYtp55tNY
Token exchange URL (Kakao): https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id=aa6bddf393b54d4e0d42ae0014edfd2f&redirect_uri=https://sui.io/&code=$YOUR_AUTH_CODE
Visit URL (Apple): https://appleid.apple.com/auth/authorize?client_id=nl.digkas.wallet.client&redirect_uri=https://sui.io/&scope=email&response_mode=form_post&response_type=code%20id_token&nonce=3-QOndCJH1FxFKKWagDYtp55tNY
Visit URL (Slack): https://slack.com/openid/connect/authorize?response_type=code&client_id=2426087588661.5742457039348&redirect_uri=https://sui.io/&nonce=3-QOndCJH1FxFKKWagDYtp55tNY&scope=openid
Token exchange URL (Slack): https://slack.com/api/openid.connect.token?code=$YOUR_AUTH_CODE&client_id=2426087588661.5742457039348&client_secret=39b955a118f2f21110939bf3dff1de90
Visit URL (AWS): https://zklogin-example.auth.us-east-1.amazoncognito.com/login?response_type=token&client_id=6c56t7re6ekgmv23o7to8r0sic&redirect_uri=https://www.sui.io/&nonce=3-QOndCJH1FxFKKWagDYtp55tNY
Visit URL (Microsoft): https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=2e3e87cb-bf24-4399-ab98-48343d457124&scope=openid&response_type=id_token&redirect_uri=https://www.sui.io&nonce=3-QOndCJH1FxFKKWagDYtp55tNY
Visit URL (KarrierOne): https://openid.karrier.one/Account/PhoneLogin?ReturnUrl=/connect/authorize?nonce=3-QOndCJH1FxFKKWagDYtp55tNY&redirect_uri=https://sui.io/&response_type=id_token&scope=openid&client_id=kns-dev
Visit URL (Credenza3): https://accounts.credenza3.com/oauth2/authorize?client_id=65954ec5d03dba0198ac343a&response_type=token&scope=openid+profile+email+phone&redirect_uri=https://example.com/callback&nonce=3-QOndCJH1FxFKKWagDYtp55tNY&state=state
Finish login and paste the entire URL here (e.g. https://sui.io/#id_token=...):
  • 拷贝地址在浏览器上完成登录授权,将回调URL拷贝到终端
https://sui.io/#id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjY3MTk2NzgzNTFhNWZhZWRjMmU3MDI3NGJiZWE2MmRhMmE4YzRhMTIiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5j~~~~~~Zjc1YmIwIn0.SjG7u1Sl~~~~~~FMUpRM0UyWjNsUFQwUjVUUV8
  • 创建用户盐值
User salt: 129390038577185583942388216820280642146
  • 创建零知识证明
ZkLogin inputs:
"{\"proofPoints\":{\"a\":[\"3464122124523047189471302918932521913058101296434097879073230534482478015348\",\"7834220540079467494418627192539806336472950063825875562138714746024255848251\",\"1\"],\"b\":[[\"19149492441362199307992882783995410707830125405171666381869005049428683081245\",\"14693022937282666003361508079034885255225159928091721231467803006565525373800\"],[\"7759544580258454506271810736350416707977134016192790189783348675168151967611\",\"5929837073147710682722028159163622033159597640618145251073419791030505497940\"],[\"1\",\"0\"]],\"c\":[\"12652371454998480582495208481033029272511889587526425501593640172098172893482\",\"13857954280172815217181847487498445155102759737509673968759142141602304595134\",\"1\"]},\"issBase64Details\":{\"value\":\"yJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLC\",\"indexMod4\":1},\"headerBase64\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IjY3MTk2NzgzNTFhNWZhZWRjMmU3MDI3NGJiZWE2MmRhMmE4YzRhMTIiLCJ0eXAiOiJKV1QifQ\"}"
test_multisig false
  • 创建zkLogin地址,领水并发送测试交易

这里会输出交易字节和zkLogin签名,在下一个命令将会用到。

Use single zklogin address as sender
Sender: 0x04b739c3ec5cfc2472a9fda71bbfcf261e40c9c2f43b5ebae25eaa5a16969dff
Faucet requested and created test transaction: "AAACACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAhGXLkarOWmgp60zvUy95QCT9D5A+7TsN8WhKxF7oRmdDAAAAAAAAACDhMXD7Qkf8UQ26YbD5oNs05wGgpH+7a487cyX8W52kgQEBAQEBAAEAAAS3OcPsXPwkcqn9pxu/zyYeQMnC9DteuuJeqloWlp3/Aep5YrfKI3J4QvSJ94ZhG2IDOi+C2wd4UYy+xoMf6cOiQwAAAAAAAAAgVXax1zcQvqU+P64xElQ/xBcbAuiNeGu9UHh4GhgugIgEtznD7Fz8JHKp/acbv88mHkDJwvQ7XrriXqpaFpad/+gDAAAAAAAAQEtMAAAAAAAA"
Single zklogin sig Serialized: "BQNMMzQ2NDEyMjEyNDUyMzA0NzE4OTQ3MTMwMjkxODkzMjUyMTkxMzA1ODEwMTI5NjQzNDA5Nzg3OTA3MzIzMDUzNDQ4MjQ3ODAxNTM0OEw3ODM0MjIwNTQwMDc5NDY3NDk0NDE4NjI3MTkyNTM5ODA2MzM2NDcyOTUwMDYzODI1ODc1NTYyMTM4NzE0NzQ2MDI0MjU1ODQ4MjUxATEDAk0xOTE0OTQ5MjQ0MTM2MjE5OTMwNzk5Mjg4Mjc4Mzk5NTQxMDcwNzgzMDEyNTQwNTE3MTY2NjM4MTg2OTAwNTA0OTQyODY4MzA4MTI0NU0xNDY5MzAyMjkzNzI4MjY2NjAwMzM2MTUwODA3OTAzNDg4NTI1NTIyNTE1OTkyODA5MTcyMTIzMTQ2NzgwMzAwNjU2NTUyNTM3MzgwMAJMNzc1OTU0NDU4MDI1ODQ1NDUwNjI3MTgxMDczNjM1MDQxNjcwNzk3NzEzNDAxNjE5Mjc5MDE4OTc4MzM0ODY3NTE2ODE1MTk2NzYxMUw1OTI5ODM3MDczMTQ3NzEwNjgyNzIyMDI4MTU5MTYzNjIyMDMzMTU5NTk3NjQwNjE4MTQ1MjUxMDczNDE5NzkxMDMwNTA1NDk3OTQwAgExATADTTEyNjUyMzcxNDU0OTk4NDgwNTgyNDk1MjA4NDgxMDMzMDI5MjcyNTExODg5NTg3NTI2NDI1NTAxNTkzNjQwMTcyMDk4MTcyODkzNDgyTTEzODU3OTU0MjgwMTcyODE1MjE3MTgxODQ3NDg3NDk4NDQ1MTU1MTAyNzU5NzM3NTA5NjczOTY4NzU5MTQyMTQxNjAyMzA0NTk1MTM0ATExeUpwYzNNaU9pSm9kSFJ3Y3pvdkwyRmpZMjkxYm5SekxtZHZiMmRzWlM1amIyMGlMQwFmZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNklqWTNNVGsyTnpnek5URmhOV1poWldSak1tVTNNREkzTkdKaVpXRTJNbVJoTW1FNFl6UmhNVElpTENKMGVYQWlPaUpLVjFRaWZRTDI0Mjg2Nzg5NDMzOTE4NTUwMjE5MTg1NTk5MDY3NjMxODcyMDkxMzk0ODE3Njc4NDMyOTU2NDgwNjExMDU3NDUyMDA2ODExMjEyMDhGAAAAAAAAAGEAWPfJxn9M85jNim3INjPwqj/C0goLgyBZyR4LNAq8FBEZji3WvqrKYoQGYapOMUlz1vKU+RpX56ejFSStaKIEBD4FKb+xXZrNwC96Ju1eTtfwLqNgpDHD7DrgMHlWT+oo"
╭──────────┬────────────────────────────────────────────────╮
│ txDigest │  FAbz83Q1PhKHJSw1Kj1oWBfCTegzFoHnMdRM88dE2A64  │
╰──────────┴────────────────────────────────────────────────╯
  • 浏览器查看交易

https://devnet.suivision.xyz/txblock/FAbz83Q1PhKHJSw1Kj1oWBfCTegzFoHnMdRM88dE2A64?tab=User+Signatures

image-20240530154558939

image-20240530154618399

3.4.2 zk-login-sig-verify :zkLogin签名验证

(1)命令说明
Given a zkLogin signature, parse it if valid.

If `bytes` provided, parse it as either as TransactionData or PersonalMessage based on `intent_scope`. It verifies the zkLogin signature based its latest JWK fetched.

Example request: sui
keytool zk-login-sig-verify --sig $SERIALIZED_ZKLOGIN_SIG --bytes $BYTES --intent-scope 0 --network devnet --curr-epoch 10

Usage: sui keytool zk-login-sig-verify [OPTIONS] --sig <SIG> --intent-scope <INTENT_SCOPE>

Options:
      --sig <SIG>                    The Base64 of the serialized zkLogin signature
      --bytes <BYTES>                The Base64 of the BCS encoded TransactionData or PersonalMessage
      --json                         Return command outputs in json format
      --intent-scope <INTENT_SCOPE>  Either 0 for TransactionData or 3 for PersonalMessage
      --cur-epoch <CUR_EPOCH>        The current epoch for the network to verify the signature's max_epoch against
      --network <NETWORK>            The network to verify the signature for, determines ZkLoginEnv [default: devnet]
(2)命令使用

zk-login-sign-and-execute-tx 命令输出的交易字节和签名作为参数传入。

sui keytool zk-login-sig-verify --sig BQNMMzQ2NDEyMjEyNDUyMzA0NzE4OTQ3MTMwMjkxODkzMjUyMTkxMzA1ODEwMTI5NjQzNDA5Nzg3OTA3MzIzMDUzNDQ4MjQ3ODAxNTM0OEw3ODM0MjIwNTQwMDc5NDY3NDk0NDE4NjI3MTkyNTM5ODA2MzM2NDcyOTUwMDYzODI1ODc1NTYyMTM4NzE0NzQ2MDI0MjU1ODQ4MjUxATEDAk0xOTE0OTQ5MjQ0MTM2MjE5OTMwNzk5Mjg4Mjc4Mzk5NTQxMDcwNzgzMDEyNTQwNTE3MTY2NjM4MTg2OTAwNTA0OTQyODY4MzA4MTI0NU0xNDY5MzAyMjkzNzI4MjY2NjAwMzM2MTUwODA3OTAzNDg4NTI1NTIyNTE1OTkyODA5MTcyMTIzMTQ2NzgwMzAwNjU2NTUyNTM3MzgwMAJMNzc1OTU0NDU4MDI1ODQ1NDUwNjI3MTgxMDczNjM1MDQxNjcwNzk3NzEzNDAxNjE5Mjc5MDE4OTc4MzM0ODY3NTE2ODE1MTk2NzYxMUw1OTI5ODM3MDczMTQ3NzEwNjgyNzIyMDI4MTU5MTYzNjIyMDMzMTU5NTk3NjQwNjE4MTQ1MjUxMDczNDE5NzkxMDMwNTA1NDk3OTQwAgExATADTTEyNjUyMzcxNDU0OTk4NDgwNTgyNDk1MjA4NDgxMDMzMDI5MjcyNTExODg5NTg3NTI2NDI1NTAxNTkzNjQwMTcyMDk4MTcyODkzNDgyTTEzODU3OTU0MjgwMTcyODE1MjE3MTgxODQ3NDg3NDk4NDQ1MTU1MTAyNzU5NzM3NTA5NjczOTY4NzU5MTQyMTQxNjAyMzA0NTk1MTM0ATExeUpwYzNNaU9pSm9kSFJ3Y3pvdkwyRmpZMjkxYm5SekxtZHZiMmRzWlM1amIyMGlMQwFmZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNklqWTNNVGsyTnpnek5URmhOV1poWldSak1tVTNNREkzTkdKaVpXRTJNbVJoTW1FNFl6UmhNVElpTENKMGVYQWlPaUpLVjFRaWZRTDI0Mjg2Nzg5NDMzOTE4NTUwMjE5MTg1NTk5MDY3NjMxODcyMDkxMzk0ODE3Njc4NDMyOTU2NDgwNjExMDU3NDUyMDA2ODExMjEyMDhGAAAAAAAAAGEAWPfJxn9M85jNim3INjPwqj/C0goLgyBZyR4LNAq8FBEZji3WvqrKYoQGYapOMUlz1vKU+RpX56ejFSStaKIEBD4FKb+xXZrNwC96Ju1eTtfwLqNgpDHD7DrgMHlWT+oo --bytes AAACACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAhGXLkarOWmgp60zvUy95QCT9D5A+7TsN8WhKxF7oRmdDAAAAAAAAACDhMXD7Qkf8UQ26YbD5oNs05wGgpH+7a487cyX8W52kgQEBAQEBAAEAAAS3OcPsXPwkcqn9pxu/zyYeQMnC9DteuuJeqloWlp3/Aep5YrfKI3J4QvSJ94ZhG2IDOi+C2wd4UYy+xoMf6cOiQwAAAAAAAAAgVXax1zcQvqU+P64xElQ/xBcbAuiNeGu9UHh4GhgugIgEtznD7Fz8JHKp/acbv88mHkDJwvQ7XrriXqpaFpad/+gDAAAAAAAAQEtMAAAAAAAA --intent-scope 0 --json
{
  "data": null,
  "parsed": "{\"inputs\":{\"proofPoints\":{\"a\":[\"3464122124523047189471302918932521913058101296434097879073230534482478015348\",\"7834220540079467494418627192539806336472950063825875562138714746024255848251\",\"1\"],\"b\":[[\"19149492441362199307992882783995410707830125405171666381869005049428683081245\",\"14693022937282666003361508079034885255225159928091721231467803006565525373800\"],[\"7759544580258454506271810736350416707977134016192790189783348675168151967611\",\"5929837073147710682722028159163622033159597640618145251073419791030505497940\"],[\"1\",\"0\"]],\"c\":[\"12652371454998480582495208481033029272511889587526425501593640172098172893482\",\"13857954280172815217181847487498445155102759737509673968759142141602304595134\",\"1\"]},\"issBase64Details\":{\"value\":\"yJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLC\",\"indexMod4\":1},\"headerBase64\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IjY3MTk2NzgzNTFhNWZhZWRjMmU3MDI3NGJiZWE2MmRhMmE4YzRhMTIiLCJ0eXAiOiJKV1QifQ\",\"addressSeed\":\"2428678943391855021918559906763187209139481767843295648061105745200681121208\"},\"maxEpoch\":70,\"userSignature\":\"AFj3ycZ/TPOYzYptyDYz8Ko/wtIKC4MgWckeCzQKvBQRGY4t1r6qymKEBmGqTjFJc9bylPkaV+enoxUkrWiiBAQ+BSm/sV2azcAveibtXk7X8C6jYKQxw+w64DB5Vk/qKA==\"}",
  "jwks": null,
  "res": null
}

string

ascii

模块说明

ascii模块定义了MoveASCII编码字符串(String)和字符(Char)类型。并提供了一系列的操作方法,用于字符串与字节数组、字符与u8之间的相互转换;以及字符串的常用操作,包括:追加、截取、计算长度、判断是否为空、判断是否合法等等。

源码路径

https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/packages/move-stdlib/sources/ascii.move

方法图解

方法说明

分类方法说明
类型转换方法as_bytes()ASCII编码字符串引用转字节数组引用
into_bytes()ASCII编码字符串转字节数组
string()字节数组转ASCII编码字符串
try_string()字节数组转Option ASCII编码字符串
to_string()ASCII编码字符串转UTF-8编码字符串
byte()字符转u8数字
char()u8数字转字符
字符串方法length()计算字符串长度
substring()从字符串中取子串
is_empty()判断字符串是否为空
index_of()获取子串首次出现位置
to_uppercase()字符串转大些
to_lowercase()字符串转小写
all_characters_printable()判断字符串是否均为可打印字符
insert()在字符串指定位置插入字符串
append()在字符串最后插入字符串
pop_char()从字符串尾部弹出字符
字符方法is_valid_char判断是否有有效字符
is_printable_char判断是否是可打印字符

代码示例

module cookbook::ascii_demo {
    use std::ascii::{Self, String, Char};

    const EInvalidBookIdx: u64 = 1;

    public struct Bookshelf has key {
		id: UID,
		books: vector<Book>,
        book_count: u64
	}

    public struct Book has store {
		idx: u64,
		title: String, 
        category: Char,
	}

    public fun create_bookshelf(ctx: &mut TxContext) {
        transfer::share_object(Bookshelf {
            id: object::new(ctx),
            books: vector::empty(),
            book_count: 0,
        });
	}

    public fun add_book(
        bookshelf: &mut Bookshelf,
        title: vector<u8>,
        category: u8
    ) {

        let book_id = bookshelf.books.length();

        let book = Book {
            idx: book_id,
            title: ascii::string(title),
            category: ascii::char(category),
        };

        bookshelf.books.push_back(book);
        bookshelf.book_count = bookshelf.book_count + 1;
    }

    public fun get_books(bookshelf: &Bookshelf):  &vector<Book> {
        &bookshelf.books
    }

    public fun get_book(bookshelf: &Bookshelf, idx: u64):  &Book {
        assert!(bookshelf.books.length() > idx, EInvalidBookIdx);
        &bookshelf.books[idx]
    }

    public fun get_book_mut_ref(bookshelf: &mut Bookshelf, idx: u64):  &mut Book {
        assert!(bookshelf.books.length() > idx, EInvalidBookIdx);
        &mut bookshelf.books[idx]
    }

    public fun get_title(book: &Book): String {
        book.title
    }

    public fun get_title_ref(book: &Book): &String {
        &book.title
    }

    public fun get_title_mut_ref(book: &mut Book): &mut String {
        &mut book.title
    }

    public fun get_category(book: &Book): Char {
        book.category
    }
}

#[test_only]
module cookbook::ascii_demo_test {
    use std::ascii;
    use std::string;
    use sui::test_scenario as ts;
    use cookbook::ascii_demo::{Bookshelf, create_bookshelf, add_book, get_books};

    #[test_only]
    use sui::test_utils::assert_eq;

    #[test]
    public fun test_ascii() {
        let alice = @0xa;    

        let mut ts = ts::begin(alice);

        {
            create_bookshelf(ts.ctx());
        };

        let expected_title = b"Mastering Bitcoin";
        let expected_category = 0x41;
        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            add_book(
                &mut bookshelf, 
                expected_title, 
                expected_category
            );

            ts::return_shared(bookshelf);
        };

        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();
            let books = bookshelf.get_books();
            assert_eq(books.length(), 1);

            {
                let book = bookshelf.get_book(0);
                let title = book.get_title();
                assert_eq(title.into_bytes(), expected_title);
                assert_eq(title, ascii::string(expected_title));
                assert_eq(option::some(title), ascii::try_string(expected_title));
                assert_eq(title.to_string(), string::utf8(expected_title));
            };

            {
                let book = bookshelf.get_book(0);
                let title_ref = book.get_title_ref();
                assert_eq(title_ref.length(), expected_title.length());
                assert_eq(title_ref.substring(10, 17), b"Bitcoin".to_ascii_string());
                assert_eq(title_ref.is_empty(), false);
                assert_eq(title_ref.index_of(&b"Bitcoin".to_ascii_string()), 10);
                assert_eq(title_ref.to_uppercase(), b"MASTERING BITCOIN".to_ascii_string());
                assert_eq(title_ref.to_lowercase(), b"mastering bitcoin".to_ascii_string());
                assert_eq(title_ref.all_characters_printable(), true);
                assert_eq(*title_ref.as_bytes(), expected_title);  // OK
                assert!(title_ref.as_bytes() == &expected_title);  // OK
                // assert_eq(title_ref.as_bytes(), &expected_title);  // ERROR assert_eq不支持引用
            };

            ts::return_shared(bookshelf);
        };

        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();
            let book = bookshelf.get_book_mut_ref(0);
            let title_mut_ref = book.get_title_mut_ref();

            {
                title_mut_ref.insert(17, b" and ".to_ascii_string());
                assert!(title_mut_ref == b"Mastering Bitcoin and ".to_ascii_string());

                title_mut_ref.append(b"Move".to_ascii_string());
                assert!(title_mut_ref == b"Mastering Bitcoin and Move".to_ascii_string());

                assert_eq(title_mut_ref.pop_char(), ascii::char(0x65)); // e
                assert_eq(title_mut_ref.pop_char(), ascii::char(0x76)); // v
                assert_eq(title_mut_ref.pop_char(), ascii::char(0x6F)); // o
                assert_eq(title_mut_ref.pop_char(), ascii::char(0x4D)); // M

                assert!(title_mut_ref == b"Mastering Bitcoin and ".to_ascii_string());
            };

            ts::return_shared(bookshelf);
        };

        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();
            {
                let book = bookshelf.get_book(0);
                let category = book.get_category();
                assert_eq(category.byte(), 0x41);
                assert_eq(category, ascii::char(0x41));

                assert_eq(ascii::is_valid_char(0x41), true);
                assert_eq(ascii::is_printable_char(0x41), true);
                assert_eq(ascii::is_printable_char(0x7F), false);
            };

            ts::return_shared(bookshelf);
        };

        ts.end();
    }
}

utf8

模块说明

string模块定义了MoveUTF-8编码字符串(String)。并提供了一系列的操作方法,用于UTF-8编码字符串与字节数组以及ASCII编码字符串之间相互转换;以及字符串的常用操作,包括:追加、截取、计算长度、判断是否为空等等。

源码路径

https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/packages/move-stdlib/sources/string.move

方法图解

方法说明

分类方法说明
类型转换方法as_bytes()UTF8编码字符串引用转字节数组引用
into_bytes()UTF8编码字符串转字节数组
utf8()字节数组转UTF8编码字符串
try_utf8()字节数组转Option UTF8编码字符串
to_ascii()UTF8编码字符串转ASCII编码字符串
from_ascii()ASCII编码字符串转UTF8编码字符串
字符串方法length()计算字符串长度
substring()从字符串中取子串
is_empty()判断字符串是否为空
index_of()获取子串首次出现位置
insert()在字符串指定位置插入UTF8编码字符串,指定位置必须位于有效的UTF8字符边界
append()UTF8编码字符串最后追加UTF8编码字符串
append_utf8()UTF8编码字符串最后追加字节数组,该字节数组需要为有效的UTF8编码

代码示例

#![allow(unused)]
fn main() {
module cookbook::utf8_demo {
    use std::string::{Self, String};

    const EInvalidBookIdx: u64 = 1;

    public struct Bookshelf has key {
		id: UID,
		books: vector<Book>,
        book_count: u64
	}

    public struct Book has store {
		idx: u64,
		title: String, 
	}

    public fun create_bookshelf(ctx: &mut TxContext) {
        transfer::share_object(Bookshelf {
            id: object::new(ctx),
            books: vector::empty(),
            book_count: 0,
        });
	}

    public fun add_book(
        bookshelf: &mut Bookshelf,
        title: vector<u8>,
    ) {

        let book_id = bookshelf.books.length();

        let book = Book {
            idx: book_id,
            title: string::utf8(title),
        };

        bookshelf.books.push_back(book);
        bookshelf.book_count = bookshelf.book_count + 1;
    }

    public fun get_books(bookshelf: &Bookshelf):  &vector<Book> {
        &bookshelf.books
    }

    public fun get_book(bookshelf: &Bookshelf, idx: u64):  &Book {
        assert!(bookshelf.books.length() > idx, EInvalidBookIdx);
        &bookshelf.books[idx]
    }

    public fun get_book_mut_ref(bookshelf: &mut Bookshelf, idx: u64):  &mut Book {
        assert!(bookshelf.books.length() > idx, EInvalidBookIdx);
        &mut bookshelf.books[idx]
    }

    public fun get_title(book: &Book): String {
        book.title
    }

    public fun get_title_ref(book: &Book): &String {
        &book.title
    }

    public fun get_title_mut_ref(book: &mut Book): &mut String {
        &mut book.title
    }
}

#[test_only]
module cookbook::utf8_demo_test {
    use std::string;
    use sui::test_scenario as ts;
    use cookbook::utf8_demo::{Bookshelf, create_bookshelf, add_book, get_books};

    #[test_only]
    use sui::test_utils::assert_eq;

    #[test]
    public fun test_utf8() {
        let alice = @0xa;    

        let mut ts = ts::begin(alice);

        {
            create_bookshelf(ts.ctx());
        };

        let expected_title = b"Mastering Bitcoin";
        let expected_title2 = b"精通比特币";
        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            add_book(
                &mut bookshelf, 
                expected_title, 
            );

            add_book(
                &mut bookshelf, 
                expected_title2, 
            );

            ts::return_shared(bookshelf);
        };

        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();
            let books = bookshelf.get_books();
            assert_eq(books.length(), 2);

            {
                let book = bookshelf.get_book(0);
                let title = book.get_title();
                assert_eq(title.into_bytes(), expected_title);
                assert_eq(title, string::utf8(expected_title));
                assert_eq(option::some(title), string::try_utf8(expected_title));
                assert_eq(title.to_ascii(), expected_title.to_ascii_string());
                assert_eq(title, expected_title.to_ascii_string().to_string());
            };

            {
                let book = bookshelf.get_book(1);
                let title_ref = book.get_title_ref();
                assert_eq(title_ref.is_empty(), false);
                assert_eq(title_ref.length(), expected_title2.length());
                assert_eq(title_ref.index_of(&b"比特币".to_string()), 6);
                assert_eq(title_ref.substring(6, 15), b"比特币".to_string());
            };

            ts::return_shared(bookshelf);
        };

        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();
            let book = bookshelf.get_book_mut_ref(1);
            let title_mut_ref = book.get_title_mut_ref();

            {
                title_mut_ref.append_utf8(b"和");
                assert!(title_mut_ref == b"精通比特币和".to_string());

                title_mut_ref.append(b"Move".to_string());
                assert!(title_mut_ref == b"精通比特币和Move".to_string());

                title_mut_ref.insert(15, b" ".to_string());
                title_mut_ref.insert(19, b" ".to_string());
                assert!(title_mut_ref == b"精通比特币 和 Move".to_string());
            };

            ts::return_shared(bookshelf);
        };

        ts.end();
    }
}
}

string_to_num

以下示例将数字字符串转换成数字,若遇到非数字的字符,将提前结束转换。

  • string_2_num
module cookbook::string_2_num {
    public fun string_to_num(bytes: vector<u8>): u64 {
        let mut num: u64 = 0;

        let len = bytes.length();
        let mut idx = 0;

        while (len - idx > 0) {

            // ASCII value of 0 is 48, ASCII value of 9 is 57
            if (bytes[idx] >= 48 && bytes[idx] <= 57) {
                num = num * 10 + ((bytes[idx] - 48) as u64);
            } else {
                break // Stop parsing when encountering a non-digit character
            };

            idx = idx + 1;
        };

        num
    }
}
  • string_2_num_tests
module cookbook::string_2_num_test {
    use cookbook::string_2_num::{string_to_num};

    #[test_only]
    use sui::test_utils::assert_eq;

    #[test]
    fun test_string_2_num() {
        let bytes = b"123456789";
        let num = string_to_num(bytes);
        assert_eq(num, 123456789);

        let bytes = b"123456789123456789";
        let num = string_to_num(bytes);
        assert_eq(num, 123456789123456789);

        let bytes = b"123456789ABCD";
        let num = string_to_num(bytes);
        assert_eq(num, 123456789);
    }
}

num_to_string

以下示例将数字转换成字节数组,再通过调用to_ascii_string()方法转换成数字字符串。

  • num_2_string
module cookbook::num_2_string {
    public fun u8_to_string(num: u8): vector<u8> {
        return num_to_string(num as u64)
    }

    public fun u16_to_string(num: u16): vector<u8> {
        return num_to_string(num as u64)
    }

    public fun u32_to_string(num: u32): vector<u8> {
        return num_to_string(num as u64)
    }

    public fun u64_to_string(num: u64): vector<u8> {
        return num_to_string(num)
    }

    public fun num_to_string(mut num: u64): vector<u8> {
        if (num == 0) {
            return b"0"
        };

        let mut bytes = vector::empty<u8>();
        while (num > 0) {
            let remainder = num % 10;                               // get the last digit
            num = num / 10;                                         // remove the last digit
            vector::push_back(&mut bytes, (remainder as u8) + 48);  // ASCII value of 0 is 48
        };

        vector::reverse(&mut bytes);
        return bytes
    }
}
  • num_2_string_tests
module cookbook::num_2_string_test {
    use cookbook::num_2_string::{u8_to_string, u16_to_string, u32_to_string, u64_to_string};

    #[test_only]
    use sui::test_utils::assert_eq;

    #[test]
    fun test_num_2_string() {
        let num: u8 = 255;
        let output = u8_to_string(num);
        assert_eq(output.to_ascii_string(), b"255".to_ascii_string());

        let num: u16 = 12345;
        let output = u16_to_string(num);
        assert_eq(output.to_ascii_string(), b"12345".to_ascii_string());

        let num: u32 = 123456789;
        let output = u32_to_string(num);
        assert_eq(output.to_ascii_string(), b"123456789".to_ascii_string());

        let num: u64 = 123456789123456789;
        let output = u64_to_string(num);
        assert_eq(output.to_ascii_string(), b"123456789123456789".to_ascii_string());
    }
}

random

模块说明

随机数常见于区块链游戏、彩票类、抽奖类等智能合约中,以确保公正性和公平性。

场景

  • NFT:在铸造 NFT 时随机分配属性,如稀有度或独特特征等
  • 游戏:游戏战斗中的随机元素,如:开启宝箱随机宝物,随机进入游戏场景等
  • 彩票或抽奖:彩票随机开奖或抽取中奖者
  • 协议:随机选举领导者或随机分配收益等

分类

其中链下随机数需要依赖外部第三方的服务,安全性和可靠性受第三方服务限制。 链上随机数包括不安全随机数如:时间戳交易哈希等,它们是可预测和可操控的,以及Sui原生的链上安全随机数random

  • 链上随机数
    • 时间戳
    • 交易哈希
    • random
  • 链下随机数
    • 预言机(如:weather-oracle
    • 第三方服务(如:drand

特点

Sui原生的链上随机数random(0x8),它具备以下特点:

  • 安全
  • 快速
  • 去中心化
  • 简单易用
  • 无需信任第三方

源码路径

方法图解

最佳实践

(1)使用私有的entry函数,不要使用public entry

(2)使用函数内直接创建的RandomGenerator,不要作为参数传递

(3)确保意外路径(unhappy path)不会比预期路径(happy path)消耗更多gas

(4)将获取随机数和使用随机数函数分开

(5)防止PTB级别的组合攻击(编译器)

Sui在使用Random作为输入的MoveCall命令之后拒绝了具有非TransferObjectsMergeCoins命令的PTB

代码示例

以下是一个投骰子的示例,每次调用将会得到一个 1~6 的随机数 NFT。

module cookbook::random{
    use sui::random::{Self, Random, new_generator};
    use sui::event;

    public struct Dice has key, store {
        id: UID,
        value: u8,
    }

    public struct DiceEvent has copy, drop {
        value: u8,
    }

    entry fun roll_dice(r: &Random, ctx: &mut TxContext): u8 {
        let mut generator = new_generator(r, ctx); 
        let result = random::generate_u8_in_range(&mut generator, 1, 6);
        result
    }

    entry fun roll_dice_nft(r: &Random, ctx: &mut TxContext) {
        let value = roll_dice(r, ctx);
        let dice = Dice {
            id: object::new(ctx),
            value,
        };

        event::emit(DiceEvent { value });

        transfer::transfer(dice, tx_context::sender(ctx));
    }
}

合约操作

合约部署

$ sui client publish
  • 记录合约包 ID
export PACKAGE_ID=0xc80da11fbee3b9b74f3cf3fc7f013b6c12041b6ba68261019cf77fa3cbdc0966

浏览器调用

https://suiscan.xyz/testnet/tx/BAYfpsCCSkrgRoLFy6EaSNNann9PJDuk6n592A6UrbKC

image-20240830195823496

可见得到的骰子点数为:4。

image-20240830200041365

命令行调用

$ sui client call --package $PACKAGE_ID --module random --function roll_dice_nft --args 0x8

可见得到的骰子点数为:4。

image-20240830200019372

代码调用

import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { SuiClient } from "@mysten/sui/client";
import { Transaction } from "@mysten/sui/transactions";

import dotenv from "dotenv";
dotenv.config();
const MNEMONIC = process.env.MNEMONIC!;
const keypair = Ed25519Keypair.deriveKeypair(MNEMONIC);

const PACKAGE_ID =
  "0xc80da11fbee3b9b74f3cf3fc7f013b6c12041b6ba68261019cf77fa3cbdc0966";
const MODULE_NAME = "random";
const FUNCTION_NAME = "roll_dice_nft";
const FULLNODE_URL = "https://fullnode.testnet.sui.io:443";

const SUI_CLIENT = new SuiClient({ url: FULLNODE_URL });

(async () => {
  let tx = new Transaction();
  tx.moveCall({
    target: `${PACKAGE_ID}::${MODULE_NAME}::${FUNCTION_NAME}`,
    arguments: [tx.object("0x8")],
  });

  try {
    const result = await SUI_CLIENT.signAndExecuteTransaction({
      transaction: tx,
      signer: keypair,
      options: {
        showEvents: true,
      },
    });

    console.log(
      `signAndExecuteTransactionBlock result: ${JSON.stringify(
        result,
        null,
        2
      )}`
    );
  } catch (e) {
    console.error(e);
  }
})();
  • 执行输出

可见得到的骰子点数为:6。

$ ts-node client.ts
signAndExecuteTransactionBlock result: {
  "digest": "FfG1UQHC3CiZg4aUsGjUA788gG6ZtpwXd5LKY12xYq5z",
  "events": [
    {
      "id": {
        "txDigest": "FfG1UQHC3CiZg4aUsGjUA788gG6ZtpwXd5LKY12xYq5z",
        "eventSeq": "0"
      },
      "packageId": "0xc80da11fbee3b9b74f3cf3fc7f013b6c12041b6ba68261019cf77fa3cbdc0966",
      "transactionModule": "random",
      "sender": "0xa244617bc05e4122fb825d3b9c63dbad96dd06fae8183c2f03027b1feff12028",
      "type": "0xc80da11fbee3b9b74f3cf3fc7f013b6c12041b6ba68261019cf77fa3cbdc0966::random::DiceEvent",
      "parsedJson": {
        "value": 6
      },
      "bcs": "7"
    }
  ],
  "confirmedLocalExecution": false
}

更多资料

https://docs.sui.io/guides/developer/advanced/randomness-onchain

https://www.youtube.com/watch?v=GxWk5WDnoD0

oracle

模块说明

本文将介绍和使用Sui官方提供的 天气预言机weather-oracle 进行随机数的创建。

注:因目前官方提供的部署在主网上的天气预言机版本比较老,已经无法正常使用了,故本示例中将在测试网上部署一个简化版本来演示。期待官方后续进行下更新。

源码路径

方法图解

代码示例

以下同样是投骰子的示例,每次调用将通过天气预言机得到一个 1~6 的随机数 NFT。

module cookbook::weather_oracle {
    use sui::event;
    use simple_weather_oracle::simple_weather::{WeatherOracle};

    public struct Dice has key, store {
        id: UID,
        value: u32,
    }

    public struct DiceEvent has copy, drop {
        value: u32,
    }

    entry fun roll_dice(weather_oracle: &WeatherOracle, geoname_id: u32): u32 {
        let random_pressure_sz = 
            simple_weather_oracle::simple_weather::city_weather_oracle_pressure(weather_oracle, geoname_id);

        let result = random_pressure_sz % 6 + 1;
        result
    }

    entry fun roll_dice_nft(weather_oracle: &WeatherOracle, geoname_id: u32, ctx: &mut TxContext) {
        let value = roll_dice(weather_oracle, geoname_id);
        let dice = Dice {
            id: object::new(ctx),
            value,
        };

        event::emit(DiceEvent { value });

        transfer::transfer(dice, tx_context::sender(ctx));
    }
}

合约操作

前置准备

因官方的天气预言机已经无法正常使用,故个人在测试网部署一个简化版本,进行测试。待官方升级后,使用方式类似。

具体步骤如下:

(1)编写简化版weather-oracle合约

module oracle::simple_weather {
    use std::string::{Self, String};
    use sui::dynamic_object_field as dof;
    use sui::package;

    /// Define a capability for the admin of the oracle.
    public struct AdminCap has key, store { id: UID }

    /// // Define a one-time witness to create the `Publisher` of the oracle.
    public struct SIMPLE_WEATHER has drop {}

    // Define a struct for the weather oracle
    public struct WeatherOracle has key {
        id: UID,
        /// The address of the oracle.
        address: address,
        /// The name of the oracle.
        name: String,
        /// The description of the oracle.
        description: String,
    }

    // Define a struct for each city that the oracle covers
    public struct CityWeatherOracle has key, store {
        id: UID,
        geoname_id: u32, // The unique identifier of the city
        name: String, // The name of the city
        country: String, // The country of the city
        pressure: u32, // The atmospheric pressure in hPa
        // ... 
    }

    /// Module initializer. Uses One Time Witness to create Publisher and transfer it to sender.
    fun init(otw: SIMPLE_WEATHER, ctx: &mut TxContext) {
        package::claim_and_keep(otw, ctx); // Claim ownership of the one-time witness and keep it

        let cap = AdminCap { id: object::new(ctx) }; // Create a new admin capability object
        transfer::share_object(WeatherOracle {
            id: object::new(ctx),
            address: tx_context::sender(ctx),
            name: string::utf8(b"SuiMeteo"),
            description: string::utf8(b"A weather oracle for posting weather updates (temperature, pressure, humidity, visibility, wind metrics and cloud state) for major cities around the world. Currently the data is fetched from https://openweathermap.org. SuiMeteo provides the best available information, but it does not guarantee its accuracy, completeness, reliability, suitability, or availability. Use it at your own risk and discretion."),
        });
        transfer::public_transfer(cap, tx_context::sender(ctx)); // Transfer the admin capability to the sender.
    }

    // Public function for adding a new city to the oracle
    public fun add_city(
        _: &AdminCap, // The admin capability
        oracle: &mut WeatherOracle, // A mutable reference to the oracle object
        geoname_id: u32, // The unique identifier of the city
        name: String, // The name of the city
        country: String, // The country of the city
        ctx: &mut TxContext // A mutable reference to the transaction context
    ) {
        dof::add(&mut oracle.id, geoname_id, // Add a new dynamic object field to the oracle object with the geoname ID as the key and a new city weather oracle object as the value.
            CityWeatherOracle {
                id: object::new(ctx), // Assign a unique ID to the city weather oracle object 
                geoname_id, // Set the geoname ID of the city weather oracle object
                name,  // Set the name of the city weather oracle object
                country,  // Set the country of the city weather oracle object
                pressure: 0, // Initialize the pressure to be zero 
            }
        );
    }

    // Public function for removing an existing city from the oracle
    public fun remove_city(_: &AdminCap, oracle: &mut WeatherOracle, geoname_id: u32) {
        let CityWeatherOracle { id, geoname_id: _, name: _, country: _, pressure: _} = dof::remove(&mut oracle.id, geoname_id);
        object::delete(id);
    }

    // Public function for updating the weather conditions of a city
    public fun update(
        _: &AdminCap,
        oracle: &mut WeatherOracle,
        geoname_id: u32,
        pressure: u32,
    ) {
        let city_weather_oracle_mut = dof::borrow_mut<u32, CityWeatherOracle>(&mut oracle.id, geoname_id); // Borrow a mutable reference to the city weather oracle object with the geoname ID as the key
        city_weather_oracle_mut.pressure = pressure;
    }

    /// Returns the `pressure` of the `CityWeatherOracle` with the given `geoname_id`.
    public fun city_weather_oracle_pressure(
        weather_oracle: &WeatherOracle, 
        geoname_id: u32
    ): u32 {
        let city_weather_oracle = dof::borrow<u32, CityWeatherOracle>(&weather_oracle.id, geoname_id);
        city_weather_oracle.pressure
    }

    // This function updates the name of a weather oracle contract.
    // It takes an admin capability, a mutable reference to the weather oracle, and a new name as arguments.
    // It assigns the new name to the weather oracle's name field.
    public fun update_name(_: &AdminCap, weather_oracle: &mut WeatherOracle, name: String) {
        weather_oracle.name = name;
    }

    // This function updates the description of a weather oracle contract.
    // It takes an admin capability, a mutable reference to the weather oracle, and a new description as arguments.
    // It assigns the new description to the weather oracle's description field.
    public fun update_description(_: &AdminCap, weather_oracle: &mut WeatherOracle, description: String) {
        weather_oracle.description = description;
    }
}

(2)部署简化版weather-oracle合约

$ sui client publish
  • 记录关键对象
export PACKAGE_ID=0xd12ea4bb23747bbf18b54e3ce4857aa84a89f053e677d79f84642e99a1753f06

export WEATHER_ORACLE=0x50fe253f3913d4c120f1cc96a69db10b9bebed01a4fcb4e3957336f33d9fa4c3

export ADMIN_CAP=0x02714caa89b4d990321035c0d946c650bb5e20f3a73679cfc3ce2de15bb00103

(3)修改Move.toml

  • 将PACKAGE_ID更新到Move.toml
[package]
name = "simple_weather_oracle"
edition = "2024.beta"  
version = "1.0.0"
published-at = "0xd12ea4bb23747bbf18b54e3ce4857aa84a89f053e677d79f84642e99a1753f06"

[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }

[addresses]
oracle = "0xd12ea4bb23747bbf18b54e3ce4857aa84a89f053e677d79f84642e99a1753f06"

(4)提交简化版weather-oracle到github

(5)添加城市

$ export GEO_NAME_ID_SZ=1795566

$ sui client call --package $PACKAGE_ID --module simple_weather --function add_city --args $ADMIN_CAP $WEATHER_ORACLE $GEO_NAME_ID_SZ Shenzhen CN 

(6)更新气压值

$ sui client call --package $PACKAGE_ID --module simple_weather --function update --args $ADMIN_CAP $WEATHER_ORACLE $GEO_NAME_ID_SZ 99700

(7)获取气压值

$ sui client call --package $PACKAGE_ID --module simple_weather --function city_weather_oracle_pressure --args $WEATHER_ORACLE $GEO_NAME_ID_SZ

业务合约部署

$ sui client publish
  • 记录关键信息
export PACKAGE_ID=0xb5b69526e8c37cc195b4ca881c60a897194ffd11b8adb3638b5909fbdb44119e

浏览器调用

https://suiscan.xyz/testnet/object/0xb5b69526e8c37cc195b4ca881c60a897194ffd11b8adb3638b5909fbdb44119e/contracts

image-20240905121754297

可见得到的骰子点数为:5:

https://suiscan.xyz/testnet/tx/F1XkZ4VmyajJrwuuj6ooQFAnR2sGPna8LMwyf3a3yGuQ

image-20240905121836250

命令行调用

$ sui client call --package $PACKAGE_ID --module weather_oracle --function roll_dice_nft --args $WEATHER_ORACLE $GEO_NAME_ID_SZ

可见得到的骰子点数为:5。

image-20240905121531766

代码调用

import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { SuiClient } from "@mysten/sui/client";
import { Transaction } from "@mysten/sui/transactions";

import dotenv from "dotenv";
dotenv.config();
const MNEMONIC = process.env.MNEMONIC!;
const keypair = Ed25519Keypair.deriveKeypair(MNEMONIC);

const FULLNODE_URL = "https://fullnode.testnet.sui.io:443";
const PACKAGE_ID =
  "0xb5b69526e8c37cc195b4ca881c60a897194ffd11b8adb3638b5909fbdb44119e";
const WEATHER_ORACLE =
  "0x50fe253f3913d4c120f1cc96a69db10b9bebed01a4fcb4e3957336f33d9fa4c3";
const MODULE_NAME = "weather_oracle";
const FUNCTION_NAME = "roll_dice_nft";
const GEO_NAME_ID_SZ = 1795566;

const SUI_CLIENT = new SuiClient({ url: FULLNODE_URL });

(async () => {
  let tx = new Transaction();
  tx.moveCall({
    target: `${PACKAGE_ID}::${MODULE_NAME}::${FUNCTION_NAME}`,
    arguments: [tx.object(WEATHER_ORACLE), tx.pure.u32(Number(GEO_NAME_ID_SZ))],
  });

  try {
    const result = await SUI_CLIENT.signAndExecuteTransaction({
      transaction: tx,
      signer: keypair,
      options: {
        showEvents: true,
      },
    });

    console.log(
      `signAndExecuteTransactionBlock result: ${JSON.stringify(
        result,
        null,
        2
      )}`
    );
  } catch (e) {
    console.error(e);
  }
})();
  • 执行输出

可见得到的骰子点数为:5。

$ ts-node client_weather_oracle.ts 
signAndExecuteTransactionBlock result: {
  "digest": "E9MYantGeDx4XaWDhpBzCMELsLm3WeB5uq2SqGHfiaWb",
  "events": [
    {
      "id": {
        "txDigest": "E9MYantGeDx4XaWDhpBzCMELsLm3WeB5uq2SqGHfiaWb",
        "eventSeq": "0"
      },
      "packageId": "0xb5b69526e8c37cc195b4ca881c60a897194ffd11b8adb3638b5909fbdb44119e",
      "transactionModule": "weather_oracle",
      "sender": "0xa244617bc05e4122fb825d3b9c63dbad96dd06fae8183c2f03027b1feff12028",
      "type": "0xb5b69526e8c37cc195b4ca881c60a897194ffd11b8adb3638b5909fbdb44119e::weather_oracle::DiceEvent",
      "parsedJson": {
        "value": 5
      },
      "bcs": "8QwQj"
    }
  ],
  "confirmedLocalExecution": false
}

更多资料

https://docs.sui.io/guides/developer/app-examples/weather-oracle

https://blog.sui.io/sui-weather-oracle/

drand

模块说明

  • 定义

    Drand是一个分布式随机性信标(Distributed randomness beacon),提供公开可验证、不可预测且无偏的随机数服务。

  • 获取方式

    可以通过向Drand服务发送HTTP GET请求来获取随机数,还可以使用客户端库(GO、JS)为应用程序获取随机数。

  • 公共端点

    ProviderURL
    Protocol Labshttps://api.drand.sh/
    https://api2.drand.sh/
    https://api3.drand.sh/
    Cloudflarehttps://drand.cloudflare.com/
    StorSwifthttps://api.drand.secureweb3.com:6875/
  • 网络分类

    分类随机数产生频率网络模式生成方式安全性性能
    默认网络30s链式模式(chained mode每个随机数依赖于前一个随机数,形成随机数链较高较低
    快速网络3s非链式模式(unchained mode每个随机数独立生成,不依赖于之前的随机数较低较高

方法图解

https://drand.love/docs/http-api-reference

代码示例

以下同样是投骰子的示例,每次调用将通过传入drand随机数得到一个 1~6 的随机数 NFT。

module cookbook::drand{
    use sui::event;
    use cookbook::drand_lib::{derive_randomness, 
        verify_drand_signature, safe_selection};

    public struct Dice has key, store {
        id: UID,
        value: u8,
    }

    public struct DiceEvent has copy, drop {
        value: u8,
    }

    entry fun roll_dice(current_round: u64, drand_sig: vector<u8>): u8 {
        verify_drand_signature(drand_sig, current_round);

        let digest = derive_randomness(drand_sig);
        let random_index = safe_selection(6, &digest);

        (random_index as u8) + 1
    }

    entry fun roll_dice_nft(current_round: u64, drand_sig: vector<u8>, ctx: &mut TxContext) {
        let value = roll_dice(current_round, drand_sig);
        let dice = Dice {
            id: object::new(ctx),
            value,
        };

        event::emit(DiceEvent { value });

        transfer::transfer(dice, tx_context::sender(ctx));
    }
}

合约操作

业务合约部署

$ sui client publish
  • 记录关键信息
export PACKAGE_ID=0xf023a2832843d1d983fd91a25bdb36f43d45d0728bfc2df9c26c655e58ca09e2

命令行调用

# 获取最新轮次和随机数签名
$ curl -s https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/latest > output.txt
$ export CURRENT_ROUND=`jq '.round' output.txt`
$ export SIGNATURE=0x`jq -r '.signature' output.txt`

$ cat output.txt | jq
{
  "round": 10938210,
  "signature": "a23596a3f17b8c3553cbb590b6f11081603ab909d5603db45134c7aa9977c3bf9dfa0681144b8dfa8fed4452e9c2204d",
  "randomness": "d6e5474b7c1e27b09791bb9b483d16d9e01a7df2f1645b847ddae381ef0c6647"
}

$ sui client call --package $PACKAGE_ID --module drand --function roll_dice_nft --args $CURRENT_ROUND $SIGNATURE

可见得到的骰子点数为:4。

image-20240906182052727

代码调用

import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { SuiClient } from "@mysten/sui/client";
import { Transaction } from "@mysten/sui/transactions";

import axios from "axios";
import dotenv from "dotenv";
dotenv.config();
const MNEMONIC = process.env.MNEMONIC!;
const keypair = Ed25519Keypair.deriveKeypair(MNEMONIC);

const FULLNODE_URL = "https://fullnode.testnet.sui.io:443";
const PACKAGE_ID =
  "0xe94fae15e81744ec0d64c45de6efe8feeb7e14d9894b20316d7e57b7a8274ad0";
const MODULE_NAME = "drand";
const FUNCTION_NAME = "roll_dice_nft";

const SUI_CLIENT = new SuiClient({ url: FULLNODE_URL });

function hexStringToU8Vector(hexString: string): number[] {
  const u8Vector: number[] = [];
  for (let i = 0; i < hexString.length; i += 2) {
    u8Vector.push(parseInt(hexString.slice(i, i + 2), 16));
  }

  return u8Vector;
}

(async () => {
  const url =
    "https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/latest";

  const response = await axios.get(url);
  const data = response.data;

  const round = data.round; // eg: 10934869
  const signature = data.signature; // eg: "b2916323d2a94f95648f8cc72c3462352bcca735391f5d29c141166da03526b5ab6c63b0cc5905251f9d06e5e0420e9f";

  let tx = new Transaction();

  let signatureVec = hexStringToU8Vector(signature);

  tx.moveCall({
    target: `${PACKAGE_ID}::${MODULE_NAME}::${FUNCTION_NAME}`,
    arguments: [tx.pure.u64(round), tx.pure.vector("u8", signatureVec)],
  });

  try {
    const result = await SUI_CLIENT.signAndExecuteTransaction({
      transaction: tx,
      signer: keypair,
      options: {
        showEvents: true,
      },
    });

    console.log(
      `signAndExecuteTransactionBlock result: ${JSON.stringify(
        result,
        null,
        2
      )}`
    );
  } catch (e) {
    console.error(e);
  }
})();
  • 执行输出

可见得到的骰子点数为:6。

$ ts-node client_drand.ts 
signAndExecuteTransactionBlock result: {
  "digest": "GwP6FhHwLbS1LvSP4r7YzqZ9ucPYuQKqiXdEmpkNEBQf",
  "events": [
    {
      "id": {
        "txDigest": "GwP6FhHwLbS1LvSP4r7YzqZ9ucPYuQKqiXdEmpkNEBQf",
        "eventSeq": "0"
      },
      "packageId": "0xe94fae15e81744ec0d64c45de6efe8feeb7e14d9894b20316d7e57b7a8274ad0",
      "transactionModule": "drand",
      "sender": "0xa244617bc05e4122fb825d3b9c63dbad96dd06fae8183c2f03027b1feff12028",
      "type": "0xe94fae15e81744ec0d64c45de6efe8feeb7e14d9894b20316d7e57b7a8274ad0::drand::DiceEvent",
      "parsedJson": {
        "value": 6
      },
      "bcs": "7"
    }
  ],
  "confirmedLocalExecution": false
}

collection

dynamic_field

模块说明

  • dynamic_field(动态字段)模块定义将结构体和值组合在一起
  • 可以使用任意名字做字段,也可以在运行时动态进行添加和删除
  • 名称可以是任何拥有 copydropstore 能力的值,这些值包括基本类型以及拥有相应能力的结构体
  • 任何具有 store 能力的值都可以被存储
  • 可以不具备key能力,即不可直接从外部进行访问

源码路径

dynamic_field.move

方法图解

方法说明

分类方法说明
add<...>(object: &mut UID, name: Name, value: Value)向对象object添加名为name的值为value的动态字段
remove<...>(object: &mut UID, name: Name): Value从对象object中删除名为name的动态字段,若不存在将会报错
remove_if_exists<...>(object: &mut UID, name: Name): Option<Value>从对象object中删除名为name的动态字段,若存在则已option::some包装后返回,若不存在返回option::none()
borrow_mut<...>(object: &mut UID, name: Name): &mut Value从对象object中获取名为name的动态字段的可变引用,以便进行对动态字段的修改
borrow<...>(object: &UID, name: Name): &Value从对象object中获取名为name的动态字段的只读引用,用于进行信息查看
exists_<...>(object: &UID, name: Name): bool若对象object中存在名为name的动态字段则返回true,无需指定value类型
exists_with_type<...>(object: &UID, name: Name): bool若对象object中存在名为name的动态字段则返回true,需指定value类型

代码示例

采用书架和书本的示例,书本对象作为动态字段添加到书架上。

结构定义

// 书架结构定义
public struct Bookshelf has key {
    id: UID,
    // 书本数量
    book_count: u64
}

// 书本结构定义
public struct Book has store {
    // 书本标题
    title: String,
    // 书本描述
    description: String,
}

创建书架共享对象

public fun create_bookshelf(ctx: &mut TxContext) {
    transfer::share_object(Bookshelf {
        id: object::new(ctx),
        book_count: 0,
    });
}

添加书本到书架

调用dynamic_field::add方法。

public fun add_book(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>) {
    let book = Book {
        title: ascii::string(title),
        description: ascii::string(description)
    };

    dynamic_field::add(&mut bookshelf.id,title, book);
    bookshelf.book_count = bookshelf.book_count + 1;
}

获取书本

调用dynamic_field::borrow方法。

public fun get_book(bookshelf: &Bookshelf, title: vector<u8>): &Book {
    dynamic_field::borrow(&bookshelf.id, title)
}

设置书本的描述信息

调用dynamic_field::borrow_mut方法。

public fun set_book_desc(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>) {
    let book_mut_ref: &mut Book = dynamic_field::borrow_mut(&mut bookshelf.id, title);
    book_mut_ref.description = ascii::string(description);
}

判断书本是否存在

调用dynamic_field::exists_dynamic_field::exists_with_type方法。

public fun is_book_existed(bookshelf: &Bookshelf, title: vector<u8>): bool {
    dynamic_field::exists_(&bookshelf.id, title)
}

public fun is_book_exists_with_type(bookshelf: &Bookshelf, title: vector<u8>): bool {
    dynamic_field::exists_with_type<vector<u8>, Book>(&bookshelf.id, title)
}

从书架上移除书本

调用dynamic_field::removedynamic_field::remove_if_exists方法。

public fun remove_book(bookshelf: &mut Bookshelf, title: vector<u8>): Book {
    bookshelf.book_count = bookshelf.book_count - 1;
    dynamic_field::remove<vector<u8>, Book>(&mut bookshelf.id, title)
}

public fun remove_if_book_exists(bookshelf: &mut Bookshelf, title: vector<u8>): Option<Book> {
    bookshelf.book_count = bookshelf.book_count - 1;
    dynamic_field::remove_if_exists<vector<u8>, Book>(&mut bookshelf.id, title)
}

完整代码

  • dynamic_field
module cookbook::dynamic_field{
    use std::ascii::{Self, String};
    use sui::dynamic_field;

    public struct Bookshelf has key {
        id: UID,
        book_count: u64
    }

    public struct Book has store {
        title: String, 
        description: String,
    }

    // 创建书架共享对象
    public fun create_bookshelf(ctx: &mut TxContext) {
        transfer::share_object(Bookshelf {
            id: object::new(ctx),
            book_count: 0,
        });
	}

    // 放置书本到书架
    public fun add_book(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>) {
        let book = Book {
            title: ascii::string(title),
            description: ascii::string(description)
        };

        dynamic_field::add<vector<u8>, Book>(&mut bookshelf.id,title, book); 
        bookshelf.book_count = bookshelf.book_count + 1;
    }

    public fun add_book_obj(bookshelf: &mut Bookshelf, book: Book) {
        dynamic_field::add<vector<u8>, Book>(&mut bookshelf.id,
            book.title.into_bytes(), book); 
        bookshelf.book_count = bookshelf.book_count + 1;
    }

    // 拿取书本
    public fun get_book(bookshelf: &Bookshelf, title: vector<u8>): &Book {
        dynamic_field::borrow(&bookshelf.id, title)
    }

    // 设置书本的描述信息
    public fun set_book_desc(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>) {
        let book_mut_ref: &mut Book = dynamic_field::borrow_mut(&mut bookshelf.id, title);
        book_mut_ref.description = ascii::string(description);
    }

    // 判断书本是否存在
    public fun is_book_existed(bookshelf: &Bookshelf, title: vector<u8>): bool {
        dynamic_field::exists_(&bookshelf.id, title)
    }

    public fun is_book_exists_with_type(bookshelf: &Bookshelf, title: vector<u8>): bool {
        dynamic_field::exists_with_type<vector<u8>, Book>(&bookshelf.id, title)
    }

    // 从书架上移除书本
    public fun remove_book(bookshelf: &mut Bookshelf, title: vector<u8>): Book {
        bookshelf.book_count = bookshelf.book_count - 1;
        dynamic_field::remove<vector<u8>, Book>(&mut bookshelf.id, title)
    }

    // 如果存在指定标题书本,则从书架上移除书本
    public fun remove_if_book_exists(bookshelf: &mut Bookshelf, title: vector<u8>): Option<Book> {
        bookshelf.book_count = bookshelf.book_count - 1;
        dynamic_field::remove_if_exists<vector<u8>, Book>(&mut bookshelf.id, title)
    }

    public fun get_book_count(bookshelf: &Bookshelf): u64{
        bookshelf.book_count
    }

    public fun get_book_title(book: &Book): String {
        book.title
    }

    public fun get_book_desc(book: &Book): String {
        book.description
    }
}
  • dynamic_field_tests
#[test_only]
module cookbook::dynamic_field_tests {
    use std::ascii;
    use sui::test_scenario as ts;
    use cookbook::dynamic_field::{Bookshelf, create_bookshelf, add_book, get_book, 
        set_book_desc, is_book_existed, is_book_exists_with_type, remove_book,
        get_book_count, get_book_title, get_book_desc};

    #[test_only]
    use sui::test_utils::assert_eq;

    #[test]
    public fun test_dynamic_field() {
        let alice = @0xa;    

        let mut ts = ts::begin(alice);

        // 创建书架
        {
            create_bookshelf(ts.ctx());
        };

        // 放置书本到书架
        let expected_title = b"Mastering Bitcoin";
        let expected_description= b"1st Edition";
        let expected_new_description= b"3rd Edition";

        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            add_book(
                &mut bookshelf, 
                expected_title, 
                expected_description,
            );

            assert_eq(bookshelf.get_book_count(), 1);

            ts::return_shared(bookshelf);
        };

        // 拿取书本
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();

            let book = get_book(
                &bookshelf, 
                expected_title, 
            );

            assert_eq(book.get_book_title(), ascii::string(expected_title));
            assert_eq(book.get_book_desc(), ascii::string(expected_description));

            ts::return_shared(bookshelf);
        };

        // 设置书本的描述信息
        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            set_book_desc(
                &mut bookshelf, 
                expected_title, 
                expected_new_description,
            );

            let book = get_book(
                &bookshelf, 
                expected_title, 
            );

            assert_eq(book.get_book_title(), ascii::string(expected_title));
            assert_eq(book.get_book_desc(), ascii::string(expected_new_description));

            ts::return_shared(bookshelf);
        };

        // 判断书本是否存在
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();

            let is_existed = is_book_existed(
                &bookshelf, 
                expected_title, 
            );
            assert_eq(is_existed, true);

            let is_existed = is_book_exists_with_type(
                &bookshelf, 
                expected_title, 
            );
            assert_eq(is_existed, true);

            ts::return_shared(bookshelf);
        };

        // 从书架上移除书本
        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            assert_eq(bookshelf.get_book_count(), 1);

            let book = remove_book(
                &mut bookshelf, 
                expected_title, 
            );

            assert_eq(bookshelf.get_book_count(), 0);
            assert_eq(book.get_book_title(), ascii::string(expected_title));
            assert_eq(book.get_book_desc(), ascii::string(expected_new_description));

            bookshelf.add_book_obj(book);

            ts::return_shared(bookshelf);
        };

        ts.end();
    }
}

dynamic_object_field

模块说明

  • dynamic_object_field(动态对象字段)特性同dynamic_field(动态字段)
  • 与之不同的是值必须具备key能力,对象仍然能从外部通过 UID 进行访问

源码路径

dynamic_object_field.move

方法图解

方法说明

分类方法说明
add<...>(object: &mut UID, name: Name, value: Value)向对象object添加名为name的值为value的动态字段
remove<...>(object: &mut UID, name: Name): Value 从对象object中删除名为name的动态字段,若不存在将会报错
borrow_mut<...>(object: &mut UID, name: Name): &mut Value从对象object中获取名为name的动态字段的可变引用,以便进行对动态字段的修改
borrow<...>(object: &UID, name: Name): &Value从对象object中获取名为name的动态字段的只读引用,用于进行信息查看
exists_<...>(object: &UID, name: Name): bool若对象object中存在名为name的动态字段则返回true,无需指定value类型
exists_with_type<...>(object: &UID, name: Name): bool若对象object中存在名为name的动态字段则返回true,需指定value类型
id<Name: copy + drop + store>(object: &UID, name: Name,): Option<ID>返回动态对象字段中的对象ID

代码示例

同样采用书架(Bookshelf)和书本(Book)的示例,书本对象作为动态字段添加到书架上,其中书本需要具备key能力,该对象可以从外部通过 UID 进行访问。

结构定义

注:Book结构需要具备key能力,结构体中也必须具备UID

public struct Bookshelf has key {
    id: UID,
    book_count: u64
}

public struct Book has key, store {
    id: UID,
    title: String,
    description: String,
}

创建书架共享对象

// 创建书架共享对象
public fun create_bookshelf(ctx: &mut TxContext) {
    transfer::share_object(Bookshelf {
        id: object::new(ctx),
        book_count: 0,
    });
}

添加书本到书架

调用dynamic_field::add方法。

// 添加书本到书架
public fun add_book(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>, ctx: &mut TxContext) {
    let book = Book {
        id: object::new(ctx),
        title: ascii::string(title),
        description: ascii::string(description)
    };

    dynamic_object_field::add<vector<u8>, Book>(&mut bookshelf.id,title, book);
    bookshelf.book_count = bookshelf.book_count + 1;
}

public fun add_book_obj(bookshelf: &mut Bookshelf, book: Book) {
    dynamic_object_field::add<vector<u8>, Book>(&mut bookshelf.id,
        book.title.into_bytes(), book);
    bookshelf.book_count = bookshelf.book_count + 1;
}

获取书本

调用dynamic_field::borrow方法。

// 获取书本
public fun get_book(bookshelf: &Bookshelf, title: vector<u8>): &Book {
    dynamic_object_field::borrow(&bookshelf.id, title)
}

设置书本的描述信息

调用dynamic_field::borrow_mut方法。

// 设置书本的描述信息
public fun set_book_desc(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>) {
    let book_mut_ref: &mut Book = dynamic_object_field::borrow_mut(&mut bookshelf.id, title);
    book_mut_ref.description = ascii::string(description);
}

判断书本是否存在

调用dynamic_field::exists_dynamic_field::exists_with_type方法。

public fun is_book_existed(bookshelf: &Bookshelf, title: vector<u8>): bool {
    dynamic_object_field::exists_(&bookshelf.id, title)
}

public fun is_book_exists_with_type(bookshelf: &Bookshelf, title: vector<u8>): bool {
    dynamic_object_field::exists_with_type<vector<u8>, Book>(&bookshelf.id, title)
}

从书架上移除书本

调用dynamic_field::removedynamic_field::remove_if_exists方法。

// 从书架上移除书本
public fun remove_book(bookshelf: &mut Bookshelf, title: vector<u8>): Book {
    bookshelf.book_count = bookshelf.book_count - 1;
    dynamic_object_field::remove<vector<u8>, Book>(&mut bookshelf.id, title)
}

完整代码

  • dynamic_object_field
module cookbook::dynamic_object_field{
    use std::ascii::{Self, String};
    use sui::dynamic_object_field;

    public struct Bookshelf has key {
        id: UID,
        book_count: u64
    }

    public struct Book has key, store {
        id: UID,
        title: String, 
        description: String,
    }

    // 创建书架共享对象
    public fun create_bookshelf(ctx: &mut TxContext) {
        transfer::share_object(Bookshelf {
            id: object::new(ctx),
            book_count: 0,
        });
    }

    // 放置书本到书架
    public fun add_book(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>, ctx: &mut TxContext) {
        let book = Book {
            id: object::new(ctx),
            title: ascii::string(title),
            description: ascii::string(description)
        };

        dynamic_object_field::add<vector<u8>, Book>(&mut bookshelf.id,title, book); 
        bookshelf.book_count = bookshelf.book_count + 1;
    }

    public fun add_book_obj(bookshelf: &mut Bookshelf, book: Book) {
        dynamic_object_field::add<vector<u8>, Book>(&mut bookshelf.id,
            book.title.into_bytes(), book); 
        bookshelf.book_count = bookshelf.book_count + 1;
    }

    // 拿取书本
    public fun get_book(bookshelf: &Bookshelf, title: vector<u8>): &Book {
        dynamic_object_field::borrow(&bookshelf.id, title)
    }

    // 设置书本的描述信息
    public fun set_book_desc(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>) {
        let book_mut_ref: &mut Book = dynamic_object_field::borrow_mut(&mut bookshelf.id, title);
        book_mut_ref.description = ascii::string(description);
    }

    // 判断书本是否存在
    public fun is_book_existed(bookshelf: &Bookshelf, title: vector<u8>): bool {
        dynamic_object_field::exists_(&bookshelf.id, title)
    }

    public fun is_book_exists_with_type(bookshelf: &Bookshelf, title: vector<u8>): bool {
        dynamic_object_field::exists_with_type<vector<u8>, Book>(&bookshelf.id, title)
    }

    // 从书架上移除书本
    public fun remove_book(bookshelf: &mut Bookshelf, title: vector<u8>): Book {
        bookshelf.book_count = bookshelf.book_count - 1;
        dynamic_object_field::remove<vector<u8>, Book>(&mut bookshelf.id, title)
    }

    public fun get_book_count(bookshelf: &Bookshelf): u64{
        bookshelf.book_count
    }

    public fun get_book_title(book: &Book): String {
        book.title
    }

    public fun get_book_desc(book: &Book): String {
        book.description
    }
}
  • dynamic_field_tests
#[test_only]
module cookbook::dynamic_object_field_tests {
    use std::ascii;
    use sui::test_scenario as ts;
    use cookbook::dynamic_object_field::{Bookshelf, create_bookshelf, add_book, get_book, 
        set_book_desc, is_book_existed, is_book_exists_with_type, remove_book,
        get_book_count, get_book_title, get_book_desc};

    #[test_only]
    use sui::test_utils::assert_eq;

    #[test]
    public fun test_dynamic_object_field() {
        let alice = @0xa;    

        let mut ts = ts::begin(alice);

        // 创建书架
        {
            create_bookshelf(ts.ctx());
        };

        // 放置书本到书架
        let expected_title = b"Mastering Bitcoin";
        let expected_description= b"1st Edition";
        let expected_new_description= b"3rd Edition";

        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            add_book(
                &mut bookshelf, 
                expected_title, 
                expected_description,
                ts.ctx(),
            );

            assert_eq(bookshelf.get_book_count(), 1);

            ts::return_shared(bookshelf);
        };

        // 拿取书本
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();

            let book = get_book(
                &bookshelf, 
                expected_title, 
            );

            assert_eq(book.get_book_title(), ascii::string(expected_title));
            assert_eq(book.get_book_desc(), ascii::string(expected_description));

            ts::return_shared(bookshelf);
        };

        // 设置书本的描述信息
        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            set_book_desc(
                &mut bookshelf, 
                expected_title, 
                expected_new_description,
            );

            let book = get_book(
                &bookshelf, 
                expected_title, 
            );

            assert_eq(book.get_book_title(), ascii::string(expected_title));
            assert_eq(book.get_book_desc(), ascii::string(expected_new_description));

            ts::return_shared(bookshelf);
        };

        // 判断书本是否存在
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();

            let is_existed = is_book_existed(
                &bookshelf, 
                expected_title, 
            );
            assert_eq(is_existed, true);

            let is_existed = is_book_exists_with_type(
                &bookshelf, 
                expected_title, 
            );
            assert_eq(is_existed, true);

            ts::return_shared(bookshelf);
        };

        // 从书架上移除书本
        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            assert_eq(bookshelf.get_book_count(), 1);

            let book = remove_book(
                &mut bookshelf, 
                expected_title, 
            );

            assert_eq(bookshelf.get_book_count(), 0);
            assert_eq(book.get_book_title(), ascii::string(expected_title));
            assert_eq(book.get_book_desc(), ascii::string(expected_new_description));

            bookshelf.add_book_obj(book);

            ts::return_shared(bookshelf);
        };

        ts.end();
    }
}

vec_map & vec_set

模块说明

  • Sui映射(vec_map)集合(vec_set) 是基于vector实现的数据结构
  • vec_map 是一种映射结构,保证不包含重复的键,但是条目按照插入顺序排列,而不是按键的顺序。
  • vec_set 是一种集合结构,保证不包含重复的键。
  • 所有的操作时间复杂度为 O(N)N 为映射或集合的大小
  • 该结构提供方便的操作映射或集合的接口,如何需要按键的顺序排序的映射都需要手工额外处理

源码路径

方法图解

结构定义

VecMap

public struct VecMap<K: copy, V> has copy, drop, store {
    contents: vector<Entry<K, V>>,
}

/// An entry in the map
public struct Entry<K: copy, V> has copy, drop, store {
    key: K,
    value: V,
}

VecSet

public struct VecSet<K: copy + drop> has copy, drop, store {
    contents: vector<K>,
}

方法说明

功能vec_mapvec_set
创建空结构empty<K: copy, V>(): VecMap<K,V>empty<K: copy + drop>(): VecSet<K>
创建单元素结构singleton<K: copy + drop>(key: K): VecSet<K>
插入元素insert<K: copy, V>(self: &mut VecMap<K,V>, key: K, value: V) insert<K: copy + drop>(self: &mut VecSet<K>, key: K)
移除元素remove<K: copy, V>(self: &mut VecMap<K,V>, key: &K): (K, V) remove<K: copy + drop>(self: &mut VecSet<K>, key: &K)
是否包含元素contains<K: copy, V>(self: &VecMap<K, V>, key: &K): boolcontains<K: copy + drop>(self: &VecSet<K>, key: &K): bool
结构大小size<K: copy, V>(self: &VecMap<K,V>): u64size<K: copy + drop>(self: &VecSet<K>): u64
判断结构是否为空is_empty<K: copy, V>(self: &VecMap<K,V>): boolis_empty<K: copy + drop>(self: &VecSet<K>): bool
转换为元素数组into_keys_values<K: copy, V>(self: VecMap<K, V>): (vector<K>, vector<V>)into_keys<K: copy + drop>(self: VecSet<K>): vector<K>
根据元素数组进行构造from_keys_values<K: copy, V>( mut keys: vector<K>, mut values: vector<V>,)from_keys<K: copy + drop>(mut keys: vector<K>): VecSet<K>
获取键数组keys<K: copy, V>(self: &VecMap<K, V>): vector<K> keys<K: copy + drop>(self: &VecSet<K>): &vector<K>
按插入顺序获取指定键的坐标① get_idx<K: copy, V>(self: &VecMap<K,V>, key: &K): u64
② get_idx_opt<K: copy, V>(self: &VecMap<K,V>, key: &K): Option<u64>
① get_idx<K: copy + drop>(self: &VecSet<K>, key: &K): u64
② get_idx_opt<K: copy + drop>(self: &VecSet<K>, key: &K): Option<u64>
销毁空结构destroy_empty<K: copy, V>(self: VecMap<K, V>)
弹出最新插入元素pop<K: copy, V>(self: &mut VecMap<K,V>): (K, V)
获取键对应值① get<K: copy, V>(self: &VecMap<K,V>, key: &K): &V
② get_mut<K: copy, V>(self: &mut VecMap<K,V>, key: &K): &mut V
③ try_get<K: copy, V: copy>(self: &VecMap<K,V>, key: &K): Option<V>
按插入顺序索引键值对① get_entry_by_idx<K: copy, V>(self: &VecMap<K, V>, idx: u64): (&K, &V)
② get_entry_by_idx_mut<K: copy, V>(self: &mut VecMap<K, V>, idx: u64): (&K, &mut V)
根据索引坐标移除键值对remove_entry_by_idx<K: copy, V>(self: &mut VecMap<K, V>, idx: u64): (K, V)

代码示例

示例中定义了一个书架结构(Bookshelf),其中包含一个Bag类型的items,可以放入任意类型的键值对。示例中以书籍和玩具为例。

结构定义

public struct Bookshelf has key {
    id: UID,
    books: VecMap<String, Book>,
    book_names: VecSet<String>,
}

public struct Book has key, store {
    id: UID,
    title: String,
    description: String,
}

创建书架共享对象

调用 vec_map::empty()vec_set::empty() 方法

public fun create_bookshelf(ctx: &mut TxContext) {
    transfer::share_object(Bookshelf {
        id: object::new(ctx),
        books: vec_map::empty(),
        book_names: vec_set::empty(),
    });
}

放置书本到书架

调用 vec_map::insert()vec_set::insert() 方法

public fun add_book(bookshelf: &mut Bookshelf, title: vector<u8>,
    description: vector<u8>, ctx: &mut TxContext) {

    let book = Book {
        id: object::new(ctx),
        title: ascii::string(title),
        description: ascii::string(description)
    };

    bookshelf.books.insert(ascii::string(title), book);
    bookshelf.book_names.insert(ascii::string(title));
}

获取书本

调用 vec_map::get()vec_map::pop() 方法。

public fun get_book(bookshelf: &Bookshelf, title: &String): &Book {
    bookshelf.books.get(title)
}

public fun get_last_book(bookshelf: &mut Bookshelf): Book {
    let (_, book) = bookshelf.books.pop();
    book
}

设置书本的描述信息

调用vec_map::get_mut()方法。

public fun set_book_desc(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>) {
    let book_mut_ref: &mut Book = bookshelf.books.get_mut(&ascii::string(title));
    book_mut_ref.description = ascii::string(description);
}

判断书本是否存在

调用 vec_map::contains()vec_set::contains() 方法

public fun is_book_existed(bookshelf: &Bookshelf, title: vector<u8>): bool {
    // bookshelf.book_names.contains(&ascii::string(title))
    bookshelf.books.contains(&ascii::string(title))
}

从书架上移除书本

调用 vec_map::remove()vec_map::remove() 方法。

public fun remove_book(bookshelf: &mut Bookshelf, title: vector<u8>): Book {
    bookshelf.book_names.remove(&ascii::string(title));

    let (_, book) = bookshelf.books.remove(&ascii::string(title));
    book
}

判断书架是否为空

调用 vec_map::is_empty()vec_map::is_empty() 方法。

public fun is_bookshelf_empty(bookshelf: &Bookshelf): bool {
    // bookshelf.book_names.is_empty()
    bookshelf.books.is_empty()
}

获取书本数量

调用 vec_map::size()vec_map::size() 方法。

public fun get_book_count(bookshelf: &Bookshelf): u64{
    // bookshelf.book_names.size()
    bookshelf.books.size()
}

销毁空书架

调用vec_map::destroy_empty方法。

public fun destroy_empty_bookshelf(bookshelf: Bookshelf) {
    let Bookshelf {id, books, book_names:_} = bookshelf;
    books.destroy_empty();
    id.delete()
}

完整代码

  • vec_map_and_vec_set
module cookbook::vec_map_and_vec_set{
use std::ascii::{Self, String};
use sui::vec_map::{Self, VecMap};
use sui::vec_set::{Self, VecSet};

public struct Bookshelf has key {
    id: UID,
    books: VecMap<String, Book>,  
    book_names: VecSet<String>,
}

public struct Book has key, store {
    id: UID,
    title: String, 
    description: String,
}

// 创建书架共享对象
public fun create_bookshelf(ctx: &mut TxContext) {
    transfer::share_object(Bookshelf {
        id: object::new(ctx),
        books: vec_map::empty(),
        book_names: vec_set::empty(),
    });
}

// 销毁空书架
public fun destroy_empty_bookshelf(bookshelf: Bookshelf) {
    let Bookshelf {id, books, book_names:_} = bookshelf;
    books.destroy_empty();
    id.delete()
}

// 放置书本到书架
public fun add_book(bookshelf: &mut Bookshelf, title: vector<u8>, 
    description: vector<u8>, ctx: &mut TxContext) {

    let book = Book {
        id: object::new(ctx),
        title: ascii::string(title),
        description: ascii::string(description)
    };

    bookshelf.books.insert(ascii::string(title), book);
    bookshelf.book_names.insert(ascii::string(title));
}

// 拿取书本
public fun get_book(bookshelf: &Bookshelf, title: &String): &Book {
    bookshelf.books.get(title)
}

// 取出最后一本放到书架上的书
public fun get_last_book(bookshelf: &mut Bookshelf): Book {
    let (_, book) = bookshelf.books.pop();
    book
}

// 设置书本的描述信息
public fun set_book_desc(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>) {
    let book_mut_ref: &mut Book = bookshelf.books.get_mut(&ascii::string(title));
    book_mut_ref.description = ascii::string(description);
}

// 判断书本是否存在
public fun is_book_existed(bookshelf: &Bookshelf, title: vector<u8>): bool {
    // bookshelf.book_names.contains(&ascii::string(title))
    bookshelf.books.contains(&ascii::string(title))
}


// 判断书架是否为空
public fun is_bookshelf_empty(bookshelf: &Bookshelf): bool {
    // bookshelf.book_names.is_empty()
    bookshelf.books.is_empty()
}

// 从书架上移除书本
public fun remove_book(bookshelf: &mut Bookshelf, title: vector<u8>): Book {
    bookshelf.book_names.remove(&ascii::string(title));

    let (_, book) = bookshelf.books.remove(&ascii::string(title));
    book
}

public fun get_book_count(bookshelf: &Bookshelf): u64{
    // bookshelf.book_names.size()
    bookshelf.books.size()
}

public fun get_book_title(book: &Book): String {
    book.title
}

public fun get_book_desc(book: &Book): String {
    book.description
}
}
  • vec_map_and_vec_set_tests
#[test_only]
module cookbook::vec_map_and_vec_set_tests {
    use std::ascii;
    use sui::test_scenario as ts;
    use cookbook::vec_map_and_vec_set::{Bookshelf, create_bookshelf, destroy_empty_bookshelf, 
        add_book, get_book, set_book_desc, is_book_existed, remove_book, is_bookshelf_empty,
        get_book_count, get_book_title, get_book_desc};

    #[test_only]
    use sui::test_utils::assert_eq;

    #[test]
    public fun test_vec_map() {
        let alice = @0xa;    

        let mut ts = ts::begin(alice);

        // 创建书架
        {
            create_bookshelf(ts.ctx());
        };

        // 放置书本到书架
        let expected_title = b"Mastering Bitcoin";
        let expected_description= b"1st Edition";
        let expected_new_description= b"3rd Edition";

        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            add_book(
                &mut bookshelf, 
                expected_title, 
                expected_description,
                ts.ctx(),
            );

            assert_eq(bookshelf.get_book_count(), 1);
            assert_eq(bookshelf.is_bookshelf_empty(), false);

            ts::return_shared(bookshelf);
        };

        // 放置书本2到书架
        let expected_title2 = b"Move Cookbook";
        let expected_description2= b"1st Edition";

        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            add_book(
                &mut bookshelf, 
                expected_title2, 
                expected_description2,
                ts.ctx(),
            );

            assert_eq(bookshelf.get_book_count(), 2);

            ts::return_shared(bookshelf);
        };

        // 拿取书本
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();

            let book = get_book(
                &bookshelf, 
                &ascii::string(expected_title), 
            );

            assert_eq(book.get_book_title(), ascii::string(expected_title));
            assert_eq(book.get_book_desc(), ascii::string(expected_description));

            ts::return_shared(bookshelf);
        };

        // 设置书本的描述信息
        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            set_book_desc(
                &mut bookshelf, 
                expected_title, 
                expected_new_description,
            );

            let book = get_book(
                &bookshelf, 
                &ascii::string(expected_title), 
            );

            assert_eq(book.get_book_title(), ascii::string(expected_title));
            assert_eq(book.get_book_desc(), ascii::string(expected_new_description));

            ts::return_shared(bookshelf);
        };

        // 判断书本是否存在
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();

            let is_existed = is_book_existed(
                &bookshelf, 
                expected_title, 
            );
            assert_eq(is_existed, true);

            ts::return_shared(bookshelf);
        };

        // 从书架上借走书本
        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            {
                assert_eq(bookshelf.get_book_count(), 2);
                let book = remove_book(
                    &mut bookshelf, 
                    expected_title, 
                );
                assert_eq(bookshelf.get_book_count(), 1);
                assert_eq(book.get_book_title(), ascii::string(expected_title));
                assert_eq(book.get_book_desc(), ascii::string(expected_new_description));
                transfer::public_transfer(book, alice);
            };

            {
                assert_eq(bookshelf.get_book_count(), 1);
                let book = remove_book(
                    &mut bookshelf, 
                    expected_title2, 
                );
                assert_eq(bookshelf.get_book_count(), 0);
                assert_eq(book.get_book_title(), ascii::string(expected_title2));
                assert_eq(book.get_book_desc(), ascii::string(expected_description2));
                transfer::public_transfer(book, alice);
            };

            ts::return_shared(bookshelf);
        };

        // 销毁书架
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();
            destroy_empty_bookshelf(bookshelf);
        };

        ts.end();
    }
}

table

模块说明

  • Table是一种类似于map的集合
  • 与传统集合不同的是,它的键和值不是存储在 Table 值中,而是使用Sui的对象系统(sui::dynamic_field)进行存储
  • Table 结构仅作为一个句柄,用于在对象系统中检索这些键和值
  • 具有完全相同的键值映射的 Table 值不会被 == 判断为相等
  • Table<K, V> 是一个同构映射,意味着所有其键和值都具有相同的类型

源码路径

table.move

方法图解

结构定义

public struct Table<phantom K: copy + drop + store, phantom V: store> has key, store {
    /// the ID of this table
    id: UID,
    /// the number of key-value pairs in the table
    size: u64,
}

方法说明

分类方法说明
初始化new<...>(ctx: &mut TxContext): Table<K, V>创建空Table
清理destroy_empty<...>(table: Table<K, V>)销毁空Table,若不为空,将报错:ETableNotEmpty
drop<...>(table: Table<K, V>)删除一个可能非空的表格。只有在值具有drop能力时才可用
add<...>(table: &mut Table<K, V>, k: K, v: V)添加键值对到Table
Key已存在,将报错:EFieldAlreadyExists
remove<...>(table: &mut Table<K, V>, k: K): VTable中删除并返回指定Key的键值对,若Key不存在将报错:EFieldDoesNotExist
borrow_mut<...>(table: &mut Table<K, V>, k: K): &mut VTable中读取指定Key的值的可变引用,以便进行对值进行修改,若Key不存在将报错:EFieldDoesNotExist
borrow<...>(table: &Table<K, V>, k: K): &VTable中读取指定Key的值,若Key不存在将报错:EFieldDoesNotExist
contains<...>(table: &Table<K, V>, k: K): boolTable中包含指定的Key的值返回true,否则返回false
length<...>(table: &Table<K, V>): u64 获取Table的长度
is_empty<...>(table: &Table<K, V>): bool 当且仅当Table为空时返回true,否则返回false

代码示例

同样采用书架和书本的示例,书本对象将添加到书架的Table中。

结构定义

public struct Bookshelf has key {
    id: UID,
    books: Table<String, Book>
}

public struct Book has key, store {
    id: UID,
    title: String,
    description: String,
}

创建书架共享对象

调用table::new方法

public fun create_bookshelf(ctx: &mut TxContext) {
    transfer::share_object(Bookshelf {
        id: object::new(ctx),
        books: table::new<String, Book>(ctx),
    });
}

添加书本到书架

调用table::add方法

public fun add_book(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>, ctx: &mut TxContext) {
    let book = Book {
        id: object::new(ctx),
        title: ascii::string(title),
        description: ascii::string(description)
    };

    bookshelf.books.add(book.title, book);
}

获取书本

调用table::borrow方法。

public fun get_book(bookshelf: &Bookshelf, title: vector<u8>): &Book {
    bookshelf.books.borrow(ascii::string(title))
}

设置书本的描述信息

调用table::borrow_mut方法。

public fun set_book_desc(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>) {
    let book_mut_ref = bookshelf.books.borrow_mut(ascii::string(title));
    book_mut_ref.description = ascii::string(description);
}

判断书本是否存在

调用table::contains方法。

public fun is_book_existed(bookshelf: &Bookshelf, title: vector<u8>): bool {
    bookshelf.books.contains(ascii::string(title))
}

从书架上移除书本

调用table::remove方法。

// 从书架上移除书本
public fun remove_book(bookshelf: &mut Bookshelf, title: vector<u8>): Book {
    bookshelf.books.remove(ascii::string(title))
}

判断书架是否为空

调用table::is_empty方法。

public fun is_bookshelf_empty(bookshelf: &Bookshelf): bool {
    bookshelf.books.is_empty()
}

获取书本数量

调用table::length方法。

public fun get_book_count(bookshelf: &Bookshelf): u64{
    bookshelf.books.length()
}

销毁空书架

调用table::destroy_empty方法。

public fun destroy_empty_bookshelf(bookshelf: Bookshelf) {
    let Bookshelf {id, books} = bookshelf;
    books.destroy_empty();
    id.delete()
}

完整代码

  • table
module cookbook::table {
    use sui::table::{Self, Table};
    use std::ascii::{Self, String};

    public struct Bookshelf has key {
        id: UID,
        books: Table<String, Book> 
    }

    public struct Book has key, store {
        id: UID,
        title: String, 
        description: String,
    }

    // 创建书架
    public fun create_bookshelf(ctx: &mut TxContext) {
        transfer::share_object(Bookshelf {
            id: object::new(ctx),
            books: table::new<String, Book>(ctx),
        });
	}

    // 销毁空书架
    public fun destroy_empty_bookshelf(bookshelf: Bookshelf) {
        let Bookshelf {id, books} = bookshelf;
        books.destroy_empty();
        id.delete()
    }

    // 添加书籍
    public fun add_book(bookshelf: &mut Bookshelf, title: vector<u8>, 
        description: vector<u8>, ctx: &mut TxContext) {
        let book = Book {
            id: object::new(ctx),
            title: ascii::string(title),
            description: ascii::string(description)
        };

        bookshelf.books.add(book.title, book);
    }

    // 拿取书本
    public fun get_book(bookshelf: &Bookshelf, title: vector<u8>): &Book {
        bookshelf.books.borrow(ascii::string(title))
    }

    // 设置书本描述
    public fun set_book_desc(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>) {
        let book_mut_ref = bookshelf.books.borrow_mut(ascii::string(title));
        book_mut_ref.description = ascii::string(description);
    }

    // 从书架上移除书本
    public fun remove_book(bookshelf: &mut Bookshelf, title: vector<u8>): Book {
        bookshelf.books.remove(ascii::string(title))
    }

    // 判断书本是否存在
    public fun is_book_existed(bookshelf: &Bookshelf, title: vector<u8>): bool {
        bookshelf.books.contains(ascii::string(title))
    }

    // 判断书架是否为空
    public fun is_bookshelf_empty(bookshelf: &Bookshelf): bool {
        bookshelf.books.is_empty()
    }

    // 获取书本数量
    public fun get_book_count(bookshelf: &Bookshelf): u64{
        bookshelf.books.length()
    }

    public fun get_book_title(book: &Book): String {
        book.title
    }

    public fun get_book_desc(book: &Book): String {
        book.description
    }
}
  • table_tests
#[test_only]
module cookbook::table_tests {
    use std::ascii;
    use sui::test_scenario as ts;
    use cookbook::table::{Bookshelf, create_bookshelf, destroy_empty_bookshelf, 
        add_book, get_book, set_book_desc, is_book_existed, remove_book, is_bookshelf_empty,
        get_book_count, get_book_title, get_book_desc};

    #[test_only]
    use sui::test_utils::assert_eq;

    #[test]
    public fun test_table() {
        let alice = @0xa;    

        let mut ts = ts::begin(alice);

        // 创建书架
        {
            create_bookshelf(ts.ctx());
        };

        // 放置书本到书架
        let expected_title = b"Mastering Bitcoin";
        let expected_description= b"1st Edition";
        let expected_new_description= b"3rd Edition";

        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            add_book(
                &mut bookshelf, 
                expected_title, 
                expected_description,
                ts.ctx(),
            );

            assert_eq(bookshelf.get_book_count(), 1);
            assert_eq(bookshelf.is_bookshelf_empty(), false);

            ts::return_shared(bookshelf);
        };

        // 放置书本2到书架
        let expected_title2 = b"Move Cookbook";
        let expected_description2= b"1st Edition";

        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            add_book(
                &mut bookshelf, 
                expected_title2, 
                expected_description2,
                ts.ctx(),
            );

            assert_eq(bookshelf.get_book_count(), 2);

            ts::return_shared(bookshelf);
        };

        // 拿取书本
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();

            let book = get_book(
                &bookshelf, 
                expected_title, 
            );

            assert_eq(book.get_book_title(), ascii::string(expected_title));
            assert_eq(book.get_book_desc(), ascii::string(expected_description));

            ts::return_shared(bookshelf);
        };

        // 设置书本的描述信息
        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            set_book_desc(
                &mut bookshelf, 
                expected_title, 
                expected_new_description,
            );

            let book = get_book(
                &bookshelf, 
                expected_title, 
            );

            assert_eq(book.get_book_title(), ascii::string(expected_title));
            assert_eq(book.get_book_desc(), ascii::string(expected_new_description));

            ts::return_shared(bookshelf);
        };

        // 判断书本是否存在
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();

            let is_existed = is_book_existed(
                &bookshelf, 
                expected_title, 
            );
            assert_eq(is_existed, true);

            ts::return_shared(bookshelf);
        };

        // 从书架上借走书本
        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            {
                assert_eq(bookshelf.get_book_count(), 2);
                let book = remove_book(
                    &mut bookshelf, 
                    expected_title, 
                );
                assert_eq(bookshelf.get_book_count(), 1);
                assert_eq(book.get_book_title(), ascii::string(expected_title));
                assert_eq(book.get_book_desc(), ascii::string(expected_new_description));
                transfer::public_transfer(book, alice);
            };

            {
                assert_eq(bookshelf.get_book_count(), 1);
                let book = remove_book(
                    &mut bookshelf, 
                    expected_title2, 
                );
                assert_eq(bookshelf.get_book_count(), 0);
                assert_eq(book.get_book_title(), ascii::string(expected_title2));
                assert_eq(book.get_book_desc(), ascii::string(expected_description2));
                transfer::public_transfer(book, alice);
            };

            ts::return_shared(bookshelf);
        };

        // 销毁书架
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();
            destroy_empty_bookshelf(bookshelf);
        };

        ts.end();
    }
}

bag

模块说明

  • Bag同样是一种类似于map的集合
  • Table同构映射不同,Bag异构映射集合,可以容纳任意类型的键值对
  • Table类似,它的键和值不是存储在 Bag 值中,而是使用Sui的对象系统(sui::dynamic_field)进行存储
  • Bag 结构仅作为一个句柄,用于在对象系统中检索这些键和值
  • 具有完全相同的键值映射的 Bag 值不会被 == 判断为相等
  • Bag必须为空才能被销毁

源码路径

bag.move

方法图解

结构定义

Bag类型没有任何类型参数,可以使用任意类型作为键值。

public struct Bag has key, store {
    /// the ID of this bag
    id: UID,
    /// the number of key-value pairs in the bag
    size: u64,
}

方法说明

分类方法说明
初始化new(ctx: &mut TxContext): Bag创建空Bag
清理destroy_empty<...>(bag: Bag)销毁空Bag,若不为空,将报错:ETableNotEmpty
add<...>(bag: &mut Bag, k: K, v: V)添加键值对到Bag
Key已存在,将报错:EFieldAlreadyExists
remove<...>(bag: &mut Bag, k: K): VBag中删除并返回指定Key的键值对
Key不存在将报错:EFieldDoesNotExist
若值的类型与指定类型不匹配,将报错:EFieldTypeMismatch
borrow_mut<...>(bag: &Bag, k: K): &mut VBag中读取指定Key的值的可变引用,以便进行对值进行修改
Key不存在,将报错:EFieldDoesNotExist
若值的类型与指定类型不匹配,将报错:EFieldTypeMismatch
borrow<...>(bag: &Bag, k: K): &VBag中读取指定Key的值
Key不存在,将报错:EFieldDoesNotExist
若值的类型与指定类型不匹配,将报错:EFieldTypeMismatch
contains<K: ...>(bag: &Bag, k: K): boolBag中包含指定的Key的值返回true,否则返回false
contains_with_type<K: ..., V: store>(bag: &Bag, k: K): boolBag中包含指定的Key的指定类型的值将返回true,否则返回false
length<...>(bag: &Bag): u64 获取Bag的长度
is_empty<...>(bag: &Bag): bool 当且仅当Bag为空时返回true,否则返回false

代码示例

示例中定义了一个书架结构(Bookshelf),其中包含一个Bag类型的items,可以放入任意类型的键值对。示例中以在书架上放置书籍(Book)和玩具(Toy)两种不同的对象为例。

结构定义

public struct Bookshelf has key {
    id: UID,
    items: Bag,
}

// 书籍
public struct Book has key, store {
    id: UID,
    title: String,
    description: String,
}

// 玩具
public struct Toy has key, store {
    id: UID,
    name: String,
    category: String,
}

创建书架共享对象

调用bag::new方法

// 创建书架
public fun create_bookshelf(ctx: &mut TxContext) {
    transfer::share_object(Bookshelf {
        id: object::new(ctx),
        items: bag::new(ctx),
    });
}

放置书本到书架

调用bag::add方法

public fun add_book(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>, ctx: &mut TxContext) {
    let book = Book {
        id: object::new(ctx),
        title: ascii::string(title),
        description: ascii::string(description)
    };

    bookshelf.items.add(book.title, book);
}

放置玩具到书架

调用bag::add方法

public fun add_toy(bookshelf: &mut Bookshelf, name: vector<u8>, category: vector<u8>, ctx: &mut TxContext) {
    let toy = Toy {
        id: object::new(ctx),
        name: ascii::string(name),
        category: ascii::string(category)
    };

    bookshelf.items.add(toy.name, toy);
}

获取书本

调用bag::borrow方法。

public fun get_book(bookshelf: &Bookshelf, title: vector<u8>): &Book {
    bookshelf.items.borrow(ascii::string(title))
}

获取玩具

调用bag::borrow方法。

public fun get_toy(bookshelf: &Bookshelf, name: vector<u8>): &Toy{
    bookshelf.items.borrow(ascii::string(name))
}

设置书本的描述信息

调用bag::borrow_mut方法。

public fun set_book_desc(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>) {
    let book_mut_ref = bookshelf.items.borrow_mut<_, Book>(ascii::string(title));
    book_mut_ref.description = ascii::string(description);
}

判断书本是否存在

调用bag::contains方法。

public fun is_book_existed(bookshelf: &Bookshelf, title: vector<u8>): bool {
    bookshelf.items.contains(ascii::string(title))
}

从书架上移除书本

调用bag::remove方法。

public fun remove_book(bookshelf: &mut Bookshelf, title: vector<u8>): Book {
    bookshelf.items.remove(ascii::string(title))
}

从书架上移除玩具

调用bag::remove方法。

public fun remove_toy(bookshelf: &mut Bookshelf, name: vector<u8>): Toy{
    bookshelf.items.remove(ascii::string(name))
}

判断书架是否为空

调用bag::is_empty方法。

public fun is_bookshelf_empty(bookshelf: &Bookshelf): bool {
    bookshelf.items.is_empty()
}

获取书架上物品数量

调用bag::length方法。

public fun get_count(bookshelf: &Bookshelf): u64{
    bookshelf.items.length()
}

销毁空书架

调用bag::destroy_empty方法。

public fun destroy_empty_bookshelf(bookshelf: Bookshelf) {
    let Bookshelf {id, items} = bookshelf;
    items.destroy_empty();
    id.delete()
}

完整代码

  • bag
module cookbook::bag {
    use sui::bag::{Self, Bag};
    use std::ascii::{Self, String};

    public struct Bookshelf has key {
        id: UID,
        items: Bag,
    }

    // 书籍
    public struct Book has key, store {
        id: UID,
        title: String, 
        description: String,
    }

    // 玩具
    public struct Toy has key, store {
        id: UID,
        name: String, 
        category: String,
    }

    // 创建书架
    public fun create_bookshelf(ctx: &mut TxContext) {
        transfer::share_object(Bookshelf {
            id: object::new(ctx),
            items: bag::new(ctx),
        });
	}

    // 销毁空书架
    public fun destroy_empty_bookshelf(bookshelf: Bookshelf) {
        let Bookshelf {id, items} = bookshelf;
        items.destroy_empty();
        id.delete()
    }

    // 放置书籍
    public fun add_book(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>, ctx: &mut TxContext) {
        let book = Book {
            id: object::new(ctx),
            title: ascii::string(title),
            description: ascii::string(description)
        };

        bookshelf.items.add(book.title, book);
    }

    // 放置玩具
    public fun add_toy(bookshelf: &mut Bookshelf, name: vector<u8>, category: vector<u8>, ctx: &mut TxContext) {
        let toy = Toy {
            id: object::new(ctx),
            name: ascii::string(name),
            category: ascii::string(category)
        };

        bookshelf.items.add(toy.name, toy);
    }

    // 拿取书本
    public fun get_book(bookshelf: &Bookshelf, title: vector<u8>): &Book {
        bookshelf.items.borrow(ascii::string(title))
    }

    // 拿取玩具
    public fun get_toy(bookshelf: &Bookshelf, name: vector<u8>): &Toy{
        bookshelf.items.borrow(ascii::string(name))
    }

    // 设置书本描述
    public fun set_book_desc(bookshelf: &mut Bookshelf, title: vector<u8>, description: vector<u8>) {
        let book_mut_ref = bookshelf.items.borrow_mut<_, Book>(ascii::string(title));
        book_mut_ref.description = ascii::string(description);
    }

    // 从书架上移除书本
    public fun remove_book(bookshelf: &mut Bookshelf, title: vector<u8>): Book {
        bookshelf.items.remove(ascii::string(title))
    }

    // 从书架上移除玩具
    public fun remove_toy(bookshelf: &mut Bookshelf, name: vector<u8>): Toy{
        bookshelf.items.remove(ascii::string(name))
    }

    // 判断书本是否存在
    public fun is_book_existed(bookshelf: &Bookshelf, title: vector<u8>): bool {
        bookshelf.items.contains(ascii::string(title))
    }

    // 判断书架是否为空
    public fun is_bookshelf_empty(bookshelf: &Bookshelf): bool {
        bookshelf.items.is_empty()
    }

    public fun get_count(bookshelf: &Bookshelf): u64{
        bookshelf.items.length()
    }

    public fun get_book_title(book: &Book): String {
        book.title
    }

    public fun get_book_desc(book: &Book): String {
        book.description
    }

    public fun get_toy_name(toy: &Toy): String {
        toy.name
    }

    public fun get_toy_category(toy: &Toy): String {
        toy.category
    }
}
  • bag_tests
#[test_only]
module cookbook::bag_tests {
    use std::ascii;
    use sui::test_scenario as ts;
    use cookbook::bag::{Bookshelf, create_bookshelf, destroy_empty_bookshelf, 
        add_book, add_toy, get_book, get_toy, set_book_desc, is_book_existed, 
        remove_book, is_bookshelf_empty, get_count, get_book_title, get_book_desc,
        remove_toy, get_toy_name, get_toy_category};

    #[test_only]
    use sui::test_utils::assert_eq;

    #[test]
    public fun test_bag() {
        let alice = @0xa;    

        let mut ts = ts::begin(alice);

        // 创建书架
        {
            create_bookshelf(ts.ctx());
        };

        // 放置书本到书架
        let expected_title = b"Mastering Bitcoin";
        let expected_description= b"1st Edition";
        let expected_new_description= b"3rd Edition";

        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            add_book(
                &mut bookshelf, 
                expected_title, 
                expected_description,
                ts.ctx(),
            );

            assert_eq(bookshelf.get_count(), 1);
            assert_eq(bookshelf.is_bookshelf_empty(), false);

            ts::return_shared(bookshelf);
        };

        // 放置玩具到书架
        let expected_name= b"Lego set";
        let expected_category= b"Building Blocks";

        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            add_toy(
                &mut bookshelf, 
                expected_name, 
                expected_category,
                ts.ctx(),
            );

            assert_eq(bookshelf.get_count(), 2);

            ts::return_shared(bookshelf);
        };

        // 拿取书本和玩具
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();

            {
                let book = get_book(
                    &bookshelf, 
                    expected_title, 
                );
                assert_eq(book.get_book_title(), ascii::string(expected_title));
                assert_eq(book.get_book_desc(), ascii::string(expected_description));
            };

            {
                let toy = get_toy(
                    &bookshelf, 
                    expected_name, 
                );
                assert_eq(toy.get_toy_name(), ascii::string(expected_name));
                assert_eq(toy.get_toy_category(), ascii::string(expected_category));
            };

            ts::return_shared(bookshelf);
        };

        // 设置书本的描述信息
        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            set_book_desc(
                &mut bookshelf, 
                expected_title, 
                expected_new_description,
            );

            let book = get_book(
                &bookshelf, 
                expected_title, 
            );

            assert_eq(book.get_book_title(), ascii::string(expected_title));
            assert_eq(book.get_book_desc(), ascii::string(expected_new_description));

            ts::return_shared(bookshelf);
        };

        // 判断书本是否存在
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();

            let is_existed = is_book_existed(
                &bookshelf, 
                expected_title, 
            );
            assert_eq(is_existed, true);

            ts::return_shared(bookshelf);
        };

        // 从书架上拿走书本和玩具
        {
            ts.next_tx(alice);
            let mut bookshelf: Bookshelf = ts.take_shared();

            {
                assert_eq(bookshelf.get_count(), 2);
                let book = remove_book(
                    &mut bookshelf, 
                    expected_title, 
                );
                assert_eq(bookshelf.get_count(), 1);
                assert_eq(book.get_book_title(), ascii::string(expected_title));
                assert_eq(book.get_book_desc(), ascii::string(expected_new_description));
                transfer::public_transfer(book, alice);
            };

            {
                assert_eq(bookshelf.get_count(), 1);
                let toy = remove_toy(
                    &mut bookshelf, 
                    expected_name, 
                );
                assert_eq(bookshelf.get_count(), 0);
                assert_eq(toy.get_toy_name(), ascii::string(expected_name));
                assert_eq(toy.get_toy_category(), ascii::string(expected_category));
                transfer::public_transfer(toy, alice);
            };

            ts::return_shared(bookshelf);
        };

        // 销毁书架
        {
            ts.next_tx(alice);
            let bookshelf: Bookshelf = ts.take_shared();
            destroy_empty_bookshelf(bookshelf);
        };

        ts.end();
    }
}

钱包领水

本文将介绍Sui生态常见的三款钱包在测试网和开发网的领水方式。

Sui Wallet

image-20240921160822328

Surf Wallet

image-20240921161218933

Suiet

image-20240921161550332

discord领水

本文将介绍在Suidiscord领取测试网和开发网测试代币的方法。

加入Sui discord

https://discord.com/invite/sui

在水龙头频道领取测试代币

在Sui #devnet-faucet#testnet-faucet Discord频道中请求测试代币,请求格式如下:

!faucet <YOUR SUI ADDRESS>

命令行领水

本文将介绍使用Sui CLI命令和curl命令领取测试代币的方法。

CLI领水

  • 查看所在网络
$ sui client envs
  • 切换网络

若不是测试网或开发网进行切换。

$ sui client switch --env xxx
  • 执行领水命令
$ sui client faucet
Request successful. It can take up to 1 minute to get the coin. Run sui client gas to check your gas coins.
  • 查看领水结果
$ sui client gas --json
[
  {
    "gasCoinId": "0xb12724820ac35687458d97f2bfabb56ae77dc89140dc24918cf3afc6d0c76c1a",
    "mistBalance": 1000000000,
    "suiBalance": "1.00"
  }
]

curl命令领水

curl命令仅在开发网可以领水,请求格式如下:

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

IDE

VSCode插件

安装sui-move-analyzer language server

在安装VSCode插件前,需要先安装sui-move-analyzer language server,否则将报错:

image-20240922191857954

方式1:直接下载发行版本

  • 下载最新版本

    下载地址:https://github.com/movebit/sui-move-analyzer/releases

  • 改名为:sui-move-analyzersui-move-analyzer.exe

  • 添加到PATH环境变量中

  • 重启VSCode

方式2:使用cargo命令安装

  • 执行安装命令
cargo install --git http://github.com/movebit/sui-move-analyzer --branch master sui-move-analyzer
  • 添加到PATH环境变量中
export PATH=$HOME/.cargo/bin:$PATH
  • 重启VSCode

安装VSCode插件

在VSCode插件市场搜索并安装:sui-move-analyzer

image-20240922095708934

效果展示

image-20240922193116796

RustRover

Jetbrains全家桶产品使用方式类似,当前RustRover对个人非商业用途免费,推荐使用RR。

下载链接:https://www.jetbrains.com/rust/download

安装Sui CLI

如果环境中没有配置Sui CLI,请根据提示进行下载。

image-20240922092853810

image-20240922092919333

插件安装

路径:Setting ➜ Plugins ➜ Sui Move Language

效果展示

可见已具备语法高亮、函数补全等能力。

image-20240922093121563