Sandwich合约源码解析 | Move dApp 极速入门(拾叁)

Sandwich 是 Sui 官方 Examples 里的案例之一:

https://github.com/MystenLabs/sui/tree/main/sui_programmability/examples

https://github.com/NonceGeek/Web3-dApp-Camp/tree/main/move-dapp/sui/sandwich

Sandwich 是一个很好的「MVP」案例,帮助我们理解 Sui 合约的基本结构。

0x01 合约结构

我们可以抽象地把合约分为三个部分:

// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

/// Example of objects that can be combined to create
/// new objects
module basics::sandwich {
    // packages: 包

  // structs: 结构体

  // consts: 常量

  // functions: 函数
}

0x02 Sandwich 的结构体

包含HamBreadSandwichGroceryGroceryOwnerCapability 这几个组成部分:

struct Ham has key {
    id: UID
}

struct Bread has key {
    id: UID
}

struct Sandwich has key {
    id: UID,
}

// This Capability allows the owner to withdraw profits
struct GroceryOwnerCapability has key {
    id: UID
}

// Grocery is created on module init
struct Grocery has key {
    id: UID,
    profits: Balance<SUI>
}

关于 Sui 中的基础类型,可见:

https://docs.sui.io/build/programming-with-objects/ch1-object-basics

https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/object.move

2.1 UID

https://github.com/MystenLabs/sui/blob/aa5fe5bf68b20cc2def0392cbab71f8bcdad0060/crates/sui-framework/sources/object.move#L35

  • 定义存储中对象 ID 的全局唯一 ID。 对任何具备 Key 能力的 Struct 也即 Object 而言,必须将"id: UID"作为其第一个字段。
  • 不会有两个 UID 类型的值相等,也即它们全局唯一。换句话说,对于任意两个值 id1: UIDid2: UIDid1 != id2
  • 这是一个 privileged 类型,只能从 TxContext 派生。
  • UID 没有 drop 能力,所以删除 UID 需要调用 delete

0x03 函数

3.1 初始化函数

在模块初始化的时候会调取的函数,在本例中,init 函数会创建一个grocery

/// On module init, create a grocery
fun init(ctx: &mut TxContext) {
    transfer::share_object(Grocery {
        id: object::new(ctx),
        profits: balance::zero<SUI>()
    });

    transfer::transfer(GroceryOwnerCapability {
        id: object::new(ctx)
    }, tx_context::sender(ctx));
}

3.1.1 transfer::share_object 函数

https://docs.sui.io/devnet/build/move/sui-move-library#shared

要使 obj共享,可以调用:

transfer::share_object(obj);

在这个调用之后,obj保持可变,但被所有人共享,即任何人都可以发送交易来改变这个对象。 但是,这样的对象不能作为字段传输或嵌入到另一个obj中。 有关详细信息,请参阅共享对象文档:

https://docs.sui.io/devnet/build/move/sui-move-library#:~:text=details%2C%20see%20the-,shared%20objects,-documentation.

3.1.2 transfer::transfer 函数

https://docs.sui.io/devnet/build/move/sui-move-library#owned-by-an-address

Transfer 模块提供了操作对象所有权所需的 API。

最常见的情况是将obj传输到地址。 例如,当一个新obj被创建时,它通常被转移到一个地址,这样该地址就拥有该obj。 要将对象 obj传输到地址的方法如下:

use sui::transfer;

transfer::transfer(obj, recipient);

此调用将完全消耗该obj,使其在当前交易中不再可访问。 一旦一个地址拥有一个obj,对于这个obj的任何未来使用(读取或写入),交易的签名者必须是该obj的所有者。

3.1.3 object::new 函数

https://docs.sui.io/build/programming-with-objects/ch1-object-basics#create-sui-object

既然我们已经学会了如何定义一个 Sui 对象类型,那么我们如何创建/实例化一个 Sui 对象呢? 为了根据其类型创建一个新的 Sui 对象,我们必须为每个字段分配一个初始值,包括 id。 为 Sui 对象创建新 UID 的唯一方法是调用 object::new。 新函数将当前交易内容作为生成唯一 ID 的参数。 交易内容是 &mut TxContext 类型,应该从入口函数(可以直接从事务中调用的函数)向下传递。以ColorObject 为例定义构造函数:

// object creates an alias to the object module, which allows us call
// functions in the module, such as the `new` function, without fully
// qualifying, e.g. `sui::object::new`.
use sui::object;
// tx_context::TxContext creates an alias to the the TxContext struct in tx_context module.
use sui::tx_context::TxContext;

fun new(red: u8, green: u8, blue: u8, ctx: &mut TxContext): ColorObject {
    ColorObject {
        id: object::new(ctx),
        red,
        green,
        blue,
    }
}

3.1.4 init 函数做了什么?

回顾init函数,我们就可以给其加上注释了:

/// On module init, create a grocery
fun init(ctx: &mut TxContext) {
        /// 将 Grocery 对象设置为共享状态
    transfer::share_object(Grocery {
        id: object::new(ctx),
        profits: balance::zero<SUI>()
    });
        /// transfer 一个 GroceryOwnerCapability 对象给交易发送人
    transfer::transfer(GroceryOwnerCapability {
        id: object::new(ctx)
    }, tx_context::sender(ctx));
}

3.2 交易函数

和 aptos 一样,public entry fun的函数即是可以外部访问的。

/// Exchange `c` for some ham
public entry fun buy_ham(
    grocery: &mut Grocery,
    c: Coin<SUI>,
    ctx: &mut TxContext
) {
    let b = coin::into_balance(c);
    assert!(balance::value(&b) >= HAM_PRICE, EInsufficientFunds);
    balance::join(&mut grocery.profits, b);
    transfer::transfer(Ham { id: object::new(ctx) }, tx_context::sender(ctx))
}

/// Exchange `c` for some bread
public entry fun buy_bread(
    grocery: &mut Grocery,
    c: Coin<SUI>,
    ctx: &mut TxContext
) {
    let b = coin::into_balance(c);
    assert!(balance::value(&b) >= BREAD_PRICE, EInsufficientFunds);
    balance::join(&mut grocery.profits, b);
    transfer::transfer(Bread { id: object::new(ctx) }, tx_context::sender(ctx))
}

3.2.1 coin::into_balance函数

https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/coin.move#L122

销毁一枚 Coin 的 wrapper (unwrapper) 并返回其余额。

3.2.2 balance::join 函数

https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/balance.move#L71

将两个余额合并到一起。

3.2.3 buy_ham 函数做了什么?

加上注释:

/// Exchange `c` for some ham
public entry fun buy_ham(
    grocery: &mut Grocery,
    c: Coin<SUI>,
    ctx: &mut TxContext
) {
    let b = coin::into_balance(c); /// 销毁 Coin 并获得其余额
    assert!(balance::value(&b) >= HAM_PRICE, EInsufficientFunds); // 判断是否支付了正确的金额
    balance::join(&mut grocery.profits, b); // 合并现有余额和 b 
    transfer::transfer(Ham { id: object::new(ctx) }, tx_context::sender(ctx)) // 转让 Ham
}

3.3 组合函数

销毁 Ham 和 Bread 两个obj,然后转让一个 Sandwich obj 给交易发送人。

/// Combine the `ham` and `bread` into a delicious sandwich
public entry fun make_sandwich(
    ham: Ham, bread: Bread, ctx: &mut TxContext
) {
    let Ham { id: ham_id } = ham;
    let Bread { id: bread_id } = bread;
    object::delete(ham_id);
    object::delete(bread_id);
    transfer::transfer(Sandwich { id: object::new(ctx) }, tx_context::sender(ctx))
}

3.4 Profits 相关函数

关于 Funcs 类型详细的描述可见:

https://docs.sui.io/build/move#move-functions

https://mp.weixin.qq.com/s/OXLyiUKzpFzAzc-PVxLvTA

/// See the profits of a grocery
public fun profits(grocery: &Grocery): u64 {
    balance::value(&grocery.profits)
}

/// Owner of the grocery can collect profits by passing his capability
public entry fun collect_profits(_cap: &GroceryOwnerCapability, grocery: &mut Grocery, ctx: &mut TxContext) {
    let amount = balance::value(&grocery.profits);

    assert!(amount > 0, ENoProfits);

    // Take a transferable `Coin` from a `Balance`
    let coin = coin::take(&mut grocery.profits, amount, ctx);

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

3.4.1 coin::take 函数

https://github.com/MystenLabs/sui/blob/b8ace6ff5e3045f6e3fdd9a7ff076dfd2c236a61/crates/sui-framework/sources/coin.move#L131

Balance 中取出价值 valueCoin,如果 value > balance.value 则中止。

0x4 Sandwich 合约部署与调用实践

CLI 的安装以及基础指令参考:

https://mp.weixin.qq.com/s/jrz3p9x495HpAvQEYRNiZw

4.1 部署

$ sui move build
$ sui client publish ./  --gas 0x82db13db77f034873cf3f1f2e43fc1237e08664e --gas-budget 30000 --verify-dependencies

4.1.1 在浏览器中查看部署信息

https://explorer.sui.io/transaction/2U84nEgz9QM94zH5mZ79Az1EVypF2WyzdWsCM14qAJZH

Tips:sui client 地址相关用例。

$ sui client new-address secp256k1 | ed25519 # 新建地址
$ sui client addresses # 查看所有地址
$ sui client switch --address [addr] # 切换地址

4.2 调用

Tips:sui client 转账相关用例。

$ sui client gas
$ sui client transfer-sui --to 0x181bd292dbe70628479b85e873460caa3e180fe2 --sui-coin-object-id 0x82db13db77f034873cf3f1f2e43fc1237e08664e --gas-budget 30000

4.2.1 查看所有的 gas

$ sui client gas

4.2.2 构建调用方法

$ sui client call --function buy_bread --module sandwich --package 0x08204ed92afcfdf9d0f6727a2c7d40db93a059d8 --args 0xca3f4ad2b0dea3264f38878db34cecb75a6336e0 0x520e1cecf280effc4f2129d5109f11be0c000d35 --gas [one_gas_object] --gas-budget 30000

对应函数源码:

public entry fun buy_bread(
    grocery: &mut Grocery,
    c: Coin<SUI>,
    ctx: &mut TxContext
) {
    ……
}

Grocery 是合约的obj,因此我们填入 4.1.1 中查看到的 obj_id 到参数1,再填入一个 4.2.1 中所查看的 gas obj 到参数 2。

我们可以在浏览器中查看到调用记录:

https://explorer.sui.io/transaction/5sK6AecDrwhjCpYGw7gEmkYMMihq5QCMH9SXXkjRtpmZ

点击 Created,我们可以看到成功创建了一片bread

评论