模式:烫手山芋

在能力系统中,没有任何能力的结构体被称为 烫手山芋 。 它不能被存储(既不能作为对象, 也不能作为另一个结构体的字段), 也不能被复制丢弃。 因此,一旦构造完成,它必须优雅地由其模块解包, 否则由于未使用的值无法丢弃,交易将中止。

如果你熟悉支持 回调 的编程语言,可以将“烫手山芋”理解为必须调用回调函数的义务。如果你不调用它,交易将中止。

这个名字源自儿童游戏“烫手山芋”,游戏中球在玩家之间快速传递,没有人希望在音乐停止时成为最后一个持球的人,否则他们会出局。 这正是该模式的最佳比喻——“烫手山芋”结构体的实例在函数调用之间传递,任何模块都不能保留它。

定义一个烫手山芋

没有任何能力的结构体都可以是“烫手山芋”。例如,以下结构体就是一个“烫手山芋”:

public struct Request {}

由于 Request 没有任何能力,不能被存储或忽略,因此模块必须提供一个解包它的函数。例如:

/// Constructs a new `Request`
public fun new_request(): Request { Request {} }

/// Unpacks the `Request`. Due to the nature of the hot potato, this function
/// must be called to avoid aborting the transaction.
public fun confirm_request(request: Request) {
    let Request {} = request;
}

使用示例

在以下示例中,Promise 作为“烫手山芋”被用来确保从容器中借出的值在使用后被归还。 Promise 结构体包含了被借出对象的 ID 和容器的 ID,确保借出的值没有被替换成其他对象, 并且被正确归还到对应的容器中。

/// A generic container for any Object with `key + store`. The Option type
/// is used to allow taking and putting the value back.
public struct Container<T: key + store> has key {
    id: UID,
    value: Option<T>,
}

/// A Hot Potato struct that is used to ensure the borrowed value is returned.
public struct Promise {
    /// The ID of the borrowed object. Ensures that there wasn't a value swap.
    id: ID,
    /// The ID of the container. Ensures that the borrowed value is returned to
    /// the correct container.
    container_id: ID,
}

/// A module that allows borrowing the value from the container.
public fun borrow_val<T: key + store>(container: &mut Container<T>): (T, Promise) {
    assert!(container.value.is_some());
    let value = container.value.extract();
    let id = object::id(&value);
    (value, Promise { id, container_id: object::id(container) })
}

/// Put the taken item back into the container.
public fun return_val<T: key + store>(
    container: &mut Container<T>, value: T, promise: Promise
) {
    let Promise { id, container_id } = promise;
    assert!(object::id(container) == container_id);
    assert!(object::id(&value) == id);
    container.value.fill(value);
}

应用场景

以下是“烫手山芋”模式的一些常见应用场景:

借用

上述示例所示, “烫手山芋”模式在借用场景中非常有效,能够保证借出的值被正确归还到原始容器中。 虽然示例中聚焦于存储在 Option 中的值, 但同样的模式也可以应用于其他存储类型,比如动态字段

闪电贷

“烫手山芋”模式的经典示例是闪电贷。闪电贷是一种在同一笔交易中借入并偿还的贷款。 借入的资金用于执行一些操作,之后归还给贷款方。 “烫手山芋”模式确保借入的资金会被归还给贷款方。

此模式的使用示例可能如下所示:

// 从贷款方借入资金。
let (asset_a, potato) = lender.borrow(amount);

// 使用借入的资金执行一些操作。
let asset_b = dex.trade(loan);
let proceeds = another_contract::do_something(asset_b);

// 保留佣金并将其余部分返还给贷款方。
let pay_back = proceeds.split(amount, ctx);
lender.repay(pay_back, potato);
transfer::public_transfer(proceeds, ctx.sender());

变量路径执行

“烫手山芋”模式可以用于引入执行路径中的变化。 例如,如果某个模块允许用“奖励积分”或美元购买一部 Phone,那么可以使用“烫手山芋”模式将购买与支付解耦。 这种方式与某些商店的工作方式非常相似——你从货架上取下商品,然后到收银台付款。

/// A `Phone`. Can be purchased in a store.
public struct Phone has key, store { id: UID }

/// A ticket that must be paid to purchase the `Phone`.
public struct Ticket { amount: u64 }

/// Return the `Phone` and the `Ticket` that must be paid to purchase it.
public fun purchase_phone(ctx: &mut TxContext): (Phone, Ticket) {
    (
        Phone { id: object::new(ctx) },
        Ticket { amount: 100 }
    )
}

/// The customer may pay for the `Phone` with `BonusPoints` or `SUI`.
public fun pay_in_bonus_points(ticket: Ticket, payment: Coin<BONUS>) {
    let Ticket { amount } = ticket;
    assert!(payment.value() == amount);
    abort 0 // omitting the rest of the function
}

/// The customer may pay for the `Phone` with `USD`.
public fun pay_in_usd(ticket: Ticket, payment: Coin<USD>) {
    let Ticket { amount } = ticket;
    assert!(payment.value() == amount);
    abort 0 // omitting the rest of the function
}

这种解耦技术允许将购买逻辑与支付逻辑分离,使代码更加模块化并更易于维护。 Ticket 可以拆分为一个独立的模块,提供基础的支付接口,而商店的实现可以扩展以支持其他商品,而无需更改支付逻辑。

组合模式

“烫手山芋”模式可以用于以组合的方式将不同模块连接在一起。 模块可以定义与“烫手山芋”交互的方式,例如给它盖上类型签名,或从中提取一些信息。 这样,“烫手山芋”可以在不同模块之间传递,甚至在同一交易中传递到不同的包中。

最重要的组合模式是请求模式,我们将在下一节中介绍。

在 Sui 框架中的应用

该模式在 Sui 框架中以各种形式使用。以下是一些示例:

  • sui::borrow - 使用“烫手山芋”模式确保借出的值被正确归还到原始容器。
  • sui::transfer_policy - 定义了 TransferRequest,一种只能在满足所有条件时才能被消耗的“烫手山芋”。
  • sui::token - 在闭环代币系统中,ActionRequest 携带已执行操作的信息,并像 TransferRequest 一样收集批准。

总结

  • “烫手山芋”是没有能力的结构体,它必须伴随有创建和销毁的方法。
  • “烫手山芋”用于确保在交易结束前执行某些操作,类似于回调函数。
  • “烫手山芋”最常见的应用场景包括借用、闪电贷、变量路径执行和组合模式。