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 有两种类型:
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 方法。
追踪该方法,发现 Action 存储在 PromiseSingle 中。
而 PromiseSingle 存储在 Promise 中。
也就是说,调用 create_account 并未实际发生账户创建,只是保存了 Action 到 Promise 中,这点与 JS 的 Promise 差别很大,我们知道 JS 的 Promise 创建后会直接执行 executor,但在 Near 中不会发生这种情况。
执行 Promise 中的 Action
既然调用与 Action 相关的方法时没有执行 Action,那么在什么时候执行?
继续阅读源码,发现 near_sdk 为 Promise 实现了一个 Drop Trait,调用了 construct_recursively 方法。
在 construct_recursively 方法的内部调用了低级 API。
通过低级 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 的链式调用
- 合约方法返回「Promise」
- 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,则调用结果如下图所示
在 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」
因此不同的地方只有 ref.ft_on_transfer,我们再来看看 ref 的源码。
可以看到 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 之间不具有原子性