18.轻松入门Move: 设计模式

今天要介绍两个比较常见的设计模式烫手山芋和一次性见证者,烫手山芋可以保证函数调用完必须调用某类函数,而一次性见证者则可以保证函数只会执行一次。

烫手山芋(hot phtato)

烫手山芋是指那些没人任何能力的结构体。没有key能力不是对象,就无法转移给账户地址或者另一个对象;没有store能力就无法保存在对象中;没有copy能力就不能复制;没有drop能力就无法自动析构。拿到“烫手山芋”的方法如果不在定义“烫手山芋”的模块内就没有权限手动析构它,那就只有调用有能力处理“烫手山芋”的方法。也就是说“烫手山芋”可以作为一定会调用某类方法的一个保证。

烫手山芋的应用非常广泛,常见的例子就是闪电贷,闪电贷需要在同一个事务中借款和还款。为了保证借款后一定会还款,贷款方法会返回借款和一个“烫手山芋”,在同一个事务中必须调用还款的方法来将“烫手山芋”销毁掉,否则会导致也无法成功借款。

还有一个例子就是上一章我们讲解包的升级流程的时候,包的升级流程分为三步:授权、升级、提交升级,这三步缺一不可且必须在同一个事务中完成。为了保证流程的完整性,授权这一步会返回UpgradeTicket这个“烫手山芋”,来保证同一个事务中一定会调用升级命令,在升级完又会返回UpgradeReciept(也是“烫手山芋”)用来保证同一个事务会调用提交升级。

下面我们通过一个示例来演示如何实现“烫手山芋”的设计模式。我们需要实现购买手机的功能,分为两个步骤:支付和取手机。为了保证支付成功后一定会在同一个事务调用取手机的方法,会在支付方法返回一个支付凭证(也就是“烫手山芋”),只有取手机的方法实现了消费支付凭证的逻辑,所以必须调用支付之后必须调用取手机的方法。

#![allow(unused)]
fn main() {
module test6::test6 {
    use sui::coin::Coin;
    use test6::store;
    use std::string::String;
    use sui::sui::SUI;

    public entry fun buy_phone(model: String, coin: Coin<SUI>, ctx: &mut TxContext) {
        //支付成功拿到票据,必须取货,否则事务失败
        let br =  store::pay(model, coin);
        //取货
        transfer::public_transfer(store::pick_up_phone(br, ctx), tx_context::sender(ctx));
    }
}
}

值得注意的是,只有在定义支付凭证的模块外调用支付方法,才能让“烫手山芋”的模式生效,否则在定义支付凭证的模块内程序可以手动析构支付凭证,以此绕开“烫手山芋”模式。

在另一个模块,定义支付凭证、实现支付方法和取手机方法

#![allow(unused)]
fn main() {
module test6::store {
    use sui::coin::Coin;
    use std::string::String;
    use sui::sui::SUI;

    public struct Phone has key,store{
        id: UID,
        model: String
    }
    //支付凭证
    public struct BoughtReciept {
        model: String,
    }
    //支付
    public fun pay(model: String, coin: Coin<SUI>): BoughtReciept {
        /*处理支付相关逻辑
            ...
            
        支付成功
        */
        //返回支付凭证
        BoughtReciept{
            model: model,
        }
    }
    //取手机
    public fun pick_up_phone(bought_reciept: BoughtReciept, ctx: &mut TxContext): Phone {
  		//消费支付凭证
        let BoughtReciept{model:model} = bought_reciept;
        Phone{
            id: object::new(ctx),
            model: model,
        }
    }
}
}

一次性见证者(One-Time Witness)

一次性见证者是一种特殊的类型,可以保证在包的整个生命周期中最多有一个实例。它的主要作用是用于保证函数只会执行一次。

如果这个结构体跟模块名称一样且全部大写,有且仅有一个drop能力,没有字段或者只有一个bool类型字段那么这个结构体就是一次性见证者。可以通过sui::types::is_one_time_witness函数来判断是否是一次性见证者。

这个结构体不允许手动实例化这个结构体,只会在发布包的时候自动调用init函数的过程中生成实例,并作为参数传递给init函数。那什么是init函数呢?

init函数

init函数是模块的初始化函数,一个模块只允许有一个init函数。仅在包发布的时候自动执行一次,后续不再执行即便升级包也不再执行,也不能手动调用它。

init函数必须满足以下特征:

  • 函数名为init
  • 参数列表的最后一个参数一定是TxContent的可变引用或者不可变引用
  • 参数列表的第一个参数,可能是一次性见证者
  • 没有任何返回
为什么需要一次性见证者?

刚接触一次性见证者的朋友们可能会觉得疑惑,init函数已经保证只会在发布的时候执行一次了,为什么还需要一次性见证者?

以coin模块的create_currency为例,这个函数用于创建一个货币类型,并返回新货币类型的发币权限和元数据对象。源代码如下:

#![allow(unused)]
fn main() {
module sui::coin {    
	public fun create_currency<T: drop>(
        witness: T,
        decimals: u8,
        symbol: vector<u8>,
        name: vector<u8>,
        description: vector<u8>,
        icon_url: Option<Url>,
        ctx: &mut TxContext
    ): (TreasuryCap<T>, CoinMetadata<T>) {
        // 保证类型T一定是一个一次性见证者
        assert!(sui::types::is_one_time_witness(&witness), EBadWitness);

        (
            TreasuryCap {
                id: object::new(ctx),
                total_supply: balance::create_supply(witness)
            },
            CoinMetadata {
                id: object::new(ctx),
                decimals,
                name: string::utf8(name),
                symbol: ascii::string(symbol),
                description: string::utf8(description),
                icon_url
            }
        )
    }
}
}

这个函数的第一个参数就是一次性见证者类型,需要witness是因为调用balance::create_supply(witness)函数需要,那我们再看balance::create_supply函数为什么需要一次性见证者。

#![allow(unused)]
fn main() {
module sui::balance {
    public fun create_supply<T: drop>(_: T): Supply<T> {
        Supply { value: 0 }
    }
}
}

事实是它的实现根本不依赖这个一次性见证者,甚至在参数栏就把它丢弃了!

换个角度,如果我们需要调用create_currency方法就必须传入witness实例,前面讲到witness的实例只会在init函数中出现,那就意味着create_currency方法只能在init函数中调用,也就保证了同一个模块这个方法只会调用一次!定义方法的时候只需要要求传入witness就能控制方法的调用次数和调用时机,这个设计模式可以说十分精妙!

我们以eth模块为例来介绍一次性见证者的使用方法:

#![allow(unused)]
fn main() {
module bridged_eth::eth {
    use std::option;

    use sui::coin;
    use sui::transfer;
    use sui::tx_context;
    use sui::tx_context::TxContext;
	//定义一次性见证者
    struct ETH has drop {}

    const DECIMAL: u8 = 8;
	//一次性见证者一定是init函数的第一个参数
    fun init(otw: ETH, ctx: &mut TxContext) {
        //保证coin::create_currency函数只会在这个模块调用一次
        let (treasury_cap, metadata) = coin::create_currency(
            otw,
            DECIMAL,
            b"ETH",
            b"Ethereum",
            b"Bridged Ethereum token",
            option::none(),
            ctx
        );
        transfer::public_freeze_object(metadata);
        transfer::public_transfer(treasury_cap, tx_context::sender(ctx))
    }
}
}

参考资料:

https://docs.sui.io/concepts/sui-move-concepts/init

https://docs.sui.io/concepts/sui-move-concepts/one-time-witness

了解更多Move内容:

  • telegram: t.me/move_cn
  • QQ群: 79489587