关于 Near 合约开发中 Promise 的思考和总结

Near 是一条基于 Receipt 异步模型的分片区块链,在智能合约开发中,对异步调用的考虑非常重要,near_sdk 使用 Promise 作为异步编程范式,本文主要对 Promise 进行思考和总结。

near_sdk 提供了三种 Promise API:

  • 低级 API:env 模块中 promise 相关的函数
  • 中级 API:Promise 组件
  • 高级 API:ext_contract 宏,仅用于跨合约调用

通常不使用低级 API;在编写跨合约调用的时候,通常使用高级 API 以减少代码量;在编写其他 Action 时,使用中级 API。

Promise 组件是对低级 API 的封装,而高级 API 只是 Promise 组件的一种简化写法,因此 Promise 组件是本篇文章的思考中心,下文将 Promise 组件简称为 Promise。

Promise 的类型

通过阅读 promise.rs 源码我们可以看到,Promise 有两种类型:
关于 Near 合约开发中 Promise 的思考和总结_第1张图片

Single 类型的 Promise 有:

  • new 关联函数返回的 Promise
  • then 方法返回的 Promise

Joint 类型的 Promise 有:

  • and 方法返回的 Promise

Single 和 Joint 的区别如下:

Single Joint
添加 Action X
作为合约方法的返回 X
作为回调 X
作为被回调

创建一个 Promise

当我们创建一个 Promise 并调用一个与 Action 相关的方法时会发生什么?

Promise::new(account_id).create_account();

继续阅读源码,可以看到,无论是哪种 Action,都调用了 add_action 方法。
关于 Near 合约开发中 Promise 的思考和总结_第2张图片
追踪该方法,发现 Action 存储在 PromiseSingle 中。
关于 Near 合约开发中 Promise 的思考和总结_第3张图片
而 PromiseSingle 存储在 Promise 中。
关于 Near 合约开发中 Promise 的思考和总结_第4张图片
也就是说,调用 create_account 并未实际发生账户创建,只是保存了 Action 到 Promise 中,这点与 JS 的 Promise 差别很大,我们知道 JS 的 Promise 创建后会直接执行 executor,但在 Near 中不会发生这种情况。

执行 Promise 中的 Action

既然调用与 Action 相关的方法时没有执行 Action,那么在什么时候执行?

继续阅读源码,发现 near_sdk 为 Promise 实现了一个 Drop Trait,调用了 construct_recursively 方法。
关于 Near 合约开发中 Promise 的思考和总结_第5张图片
在 construct_recursively 方法的内部调用了低级 API。
关于 Near 合约开发中 Promise 的思考和总结_第6张图片
通过低级 API 生成 Promise 对应的 Receipt,然后在后续区块执行 Receipt(Promise)中的任务(Action)。因此可以将 Promise 抽象成 Receipt,Action 是否会执行,取决于 Receipt 是否成功生成。

合约方法返回「Promise」

合约方法有两种返回情况:

  • 「Promise」:return Promise 组件类型 或 return PromiseOrValue::Promise 枚举值
// return Promise 组件类型
pub fn return_promise_example_1(&self) -> Promise {
    Promise::new(alice_id).transfer(amount)
}
// return PromiseOrValue::Promise 枚举值
pub fn return_promise_example_2(&self) -> PromiseOrValue<()> {
    PromiseOrValue::Promise(
        Promise::new(alice_id).transfer(amount)
    )
}
  • 「Value」:return 非 Promise 组件类型,如 (), U128, String 或 return PromiseOrValue::Value 枚举值
// return 非 Promise 组件类型
pub fn return_value_example_1(&self) -> String {
    "This is Value".into()
}
// return PromiseOrValue::Value 枚举值
pub fn return_value_example_2(&self) -> PromiseOrValue {
    PromiseOrValue::Value(
        "This is Value".into()
    )
}

合约方法返回「Promise」,代表合约方法的返回值为对应 Promise 的执行结果,因此合约方法执行完毕后无法立即返回结果,需要等待 Promise 调用链全部完成才能获取返回结果。合约方法返回「Value」代表合约方法执行完毕后立即返回结果。

Promise schedule

Promise schedule 指调用 and 或 then 方法对 Promise 的执行顺序进行安排

  • and:两个 Promise 在同一个区块执行
// 转账给 alice 和转账给 bob 这两笔交易在同一个区块执行
pub fn and_example(&self) {
    Promise::new(alice_id)
        .transfer(amount)
        .and(
            Promise::new(bob_id)
                .transfer(amount)
        );
}
  • then:两个 Promise 在先后区块(不一定相邻)执行,then 通常用于对 前一个Promise 进行回调
// 转账给 alice 和转账给 bob 这两笔交易在先后区块执行
pub fn then_example_1(&self) {
    Promise::new(alice_id)
        .transfer(amount)
        .then(
            Promise::new(bob_id)
                .transfer(amount)
        );
}

// 用 callback 函数对 转账给 alice 这笔交易进行回调
pub fn then_example_2(&self) {
    Promise::new(alice_id)
        .transfer(amount)
        .then(
            ext_self::callback(current_id, attached_deposit, gas)
        );
}

Promise 调用链

以下两种情况会产生 Promise 的链式调用

  1. 合约方法返回「Promise」
  2. Promise 之间使用 then 进行 schedule

例子1

合约 a,方法 f1
合约 b,方法 f2
合约 c,方法 f3

外部调用 a.f1, f1 调用 b.f2,f2 调用 c.f3

合约 a 伪代码

pub fn f1(&self) -> Promise {
    ext_b::f2(b_id, attached_deposit, gas)
}

合约 b 伪代码

pub fn f2(&self) -> Promise {
    ext_c::f3(c_id, attached_deposit, gas)
}

合约 c 伪代码

pub fn f3(&self) -> String {
    "f3".into()
}
假设在 251 号区块提交了一笔 Transaction,则:
在 251 号区块把 Transaction 转换为 Receipt
在 252 号区块执行 a.f1
在 253 号区块执行 b.f2
在 254 号区块执行 c.f3

因此 Promise 调用链为:
a.f1 -> b.f2 -> c.f3

例子2

合约 alice,方法 hi_alice, bye_alice
合约 bob,  方法 hi_bob,   bye_bob
合约 jack, 方法 execuse_me

外部调用 alice.hi_alice,hi_alice 调用 bob.hi_bob,hi_bob 调用 jack.execuse_me

合约 alice 伪代码:

pub fn hi_alice(&self) -> Promise {
    ext_bob::hi_bob(bob_id, attached_deposit, gas)
        .then(ext_self::bye_alice(current_id, attached_deposit, gas))
}

pub fn bye_alice(&self) {
    log!("{}", "bye alice")
}

合约 bob 伪代码:

pub fn hi_bob(&self) -> Promise {
    ext_jack::execuse_me(jack_id, attached_deposit, gas)
        .then(ext_self::bye_bob(current_id, attached_deposit, gas))
}

pub fn bye_bob(&self) {
    log!("{}", "bye bob")
}

合约 jack 伪代码

pub fn execuse_me(&self) {
    log!("{}", "execuse me ?")
}
假设在 251 号区块提交了一笔 Transaction,则:
在 251 号区块把 Transaction 转换为 Receipt
在 252 号区块执行 alice.hi_alice
在 253 号区块执行 bob.hi_bob
在 254 号区块执行 jack.execuse_me
在 255 号区块执行 bob.bye_bob
在 256 号区块执行 alice.bye_alice

因此 Promise 调用链为:
alice.hi_alice -> bob.hi_bob -> jack.execuse_me -> bob.bye_bob -> alice.bye_alice

例子3:

在 ref 中用 wNEAR 兑换 OCT

涉及到的合约方法有:

wnear: ft_transfer_call, ft_resolve_transfer
ref:   ft_on_transfer,   exchange_callback_post_withdraw
oct:   ft_transfer

假设在251号区块提交了一笔Transaction,则调用结果如下图所示
关于 Near 合约开发中 Promise 的思考和总结_第7张图片

在 251 号区块把 Transaction 转换为 Receipt
在 252 号区块执行 wnear.ft_transfer_call
在 253 号区块执行 ref.ft_on_transfer
在 254 号区块执行 oct.ft_transfer 和 wnear.ft_resolve_transfer
在 255 号区块执行 ref.exchange_callback_post_withdraw

存在两条 Promise 调用链,分别为:
1. wnear.ft_transfer_call -> ref.ft_on_transfer -> wnear.ft_resolve_transfer
2. oct.ft_transfer -> ref.exchange_callback_post_withdraw

为什么本例会出现两条 Promise 调用链,它与例子2有什么不同?

我们可以比较一下两个例子:

1. wnear.ft_transfer_call              (return PromiseOrValue) => alice.hi_alice  (return Promise) 
    前者返回「Promise」或「Value」,后者返回「Promise」
2. wnear.ft_resolve_transfer           (return U128)           => alice.bye_alice (return ())      
    两者均返回「Value」
3. ref.ft_on_transfer                  (return PromiseOrValue) => bob.hi_bob      (return Promise) 
    前者返回「Promise」或「Value」,后者返回「Promise」
4. ref.exchange_callback_post_withdraw (return ())             => bob.bye_bob     (return ())      
    两者均返回「Value」
5. oct.ft_transfer                     (return ())             => jack.execuse_me (return ())      
    两者均返回「Value」

可以看到对于合约方法的返回情况,只有两处不同,分别是 wnear.ft_transfer_call 和 ref.ft_on_transfer,阅读 FungibleToken 源码可知,ft_transfer_call 返回PromiseOrValue::Promise 枚举值,即返回「Promise」
关于 Near 合约开发中 Promise 的思考和总结_第8张图片
因此不同的地方只有 ref.ft_on_transfer,我们再来看看 ref 的源码。
关于 Near 合约开发中 Promise 的思考和总结_第9张图片
可以看到 ref 的 receiver 实现返回的是 PromiseOrValue::Value 枚举值,即返回「Value」,这代表 ref.ft_on_transfer 执行完成后可以立即返回,因此 wnear.ft_resolve_transfer 在下个区块可以直接开始回调,而无需等待 ref.exchange_callback_post_withdraw 调用完成,这就是本例与例子2 Promise 调用链不一样的原因。

Promise 使用注意事项

  • 若合约方法 panic,则方法内 Promise 对应的 Receipt 不会生成
  • 一个 Promise 中的多个 Action 之间具有原子性
  • 多个 Promise 之间不具有原子性

你可能感兴趣的:(near)