智能合约到智能合约调用
原文:https://docs.elrond.com/developers/developer-reference/elrond-wasm-contract-calls
本指南提供了一些如何从另一个合约调用合约的示例。更多的例子可以在合约可组合性特性测试中找到。
有三种方法可以完成这些呼叫:
- 导入被调用方合约的源代码并使用自动生成的代理(推荐)
- 手动编写代理
- 手动序列化函数名和参数(不推荐)
方法一:导入合约
如果您可以访问被调用方合约的代码,那么导入自动生成的代理就很容易。只需导入合约(以及合约本身可能使用的任何其他模块):
[dependencies.contract-crate-name]
path = "relative-path-to-contract-crate"
如果您想要使用被调用方合约导入的外部模块中包含的端点(即,在与主合约不同的机箱中),您还必须将该模块添加到依赖项中,就像您添加主合约一样。
此外,在调用方代码中,您必须添加以下导入:
use module_namespace::ProxyTrait as _;
如果您使用 rust-analyser VSCode 扩展,它可能会抱怨找不到这个,但是如果您实际构建了合约,编译器可以很好地找到它。
一旦您导入了合约和它可能使用的任何外部模块,您必须声明您的代理创建者函数:
#[proxy]
fn contract_proxy(&self, sc_address: ManagedAddress) -> contract_namespace::Proxy<Self::Api>;
此函数创建一个包含被调用方合约的所有端点的对象,并自动处理序列化。
假设您希望调用合约中的以下端点:
#[endpoint(myEndpoint)]
fn my_endpoint(&self, arg: BigUint) -> BigUint {
// implementation
}
要调用此端点,您应该在调用方合约中这样做:
let biguint_result = self.contract_proxy(callee_sc_address)
.my_endpoint(my_biguint_arg)
.execute_on_dest_context();
这执行了对callee_sc_address
合约的同步调用,其中my_biguint_arg
用作arg: BigUint
的输入。请注意,您也不必指定myEndpoint
名称。它是自动处理的。
在执行这个调用之后,您可以在调用者合约中执行更多的代码,如您所愿使用biguint_result
。
注意:请记住,这只适用于相同分片的合约。如果合约在不同的分片中,您必须使用异步调用或传输并执行。
合约类型为合约调用
目前有两种主要类型的合约对合约呼叫可用:
- 通过 execute_on_dest_context 的同步、相同分片调用(如上所示)
- 异步调用
异步调用
异步调用可以通过 transfer_execute(在您不关心结果的情况下)启动,也可以通过 async_call(当您希望保存来自被调用方合约的结果或执行一些额外的计算时)启动。请记住,回调中的逻辑应该保持在最低限度,因为他们通常收到很少的气体来履行他们的职责。
要使用上面描述的代理启动传输和执行调用,您可以简单地用transfer_execute
方法替换execute_on_dest_context
方法。请记住,在这种情况下,您无法获得返回的BigUint
。
相反,如果您想要启动一个异步调用,您必须使用async_call
方法,并对返回的对象使用call_and_exit()
方法。
使用上面的示例,您的异步调用将如下所示:
#[endpoint]
fn caller_endpoint(&self) {
// other code here
self.contract_proxy(callee_sc_address)
.my_endpoint(my_biguint_arg)
.async_call()
.call_and_exit();
}
回调
如果您想基于异步调用的结果执行一些逻辑,或者只是在调用后进行一些清理,您必须声明一个回调函数。例如,假设我们想在结果是偶数的情况下做一些基于的事情,在结果是奇数的情况下做一些其他的事情,并在出现错误的情况下做一些清理。我们的回调函数如下所示:
#[callback]
fn my_endpoint_callback(
&self,
#[call_result] result: ManagedAsyncCallResult<BigUint>
) {
match result {
ManagedAsyncCallResult::Ok(value) => {
if value % 2 == 0 {
// do something
} else {
// do something else
}
},
ManagedAsyncCallResult::Err(err) => {
// log the error in storage
self.err_storage().set(&err.err_msg);
},
}
}
为了将这个回调分配给前面提到的异步调用,我们像这样挂钩它:
#[endpoint]
fn caller_endpoint(&self) {
// other code here
self.contract_proxy(callee_sc_address)
.my_endpoint(my_biguint_arg)
.async_call()
.with_callback(self.callbacks().my_endpoint_callback())
.call_and_exit();
}
尽管从理论上讲,智能合约只能有一个回调函数,但是 Rust framework 会在您启动异步调用时通过在存储中保存回调函数的 ID 来为您处理这个问题,并且它知道如何检索 ID 并在调用返回时调用正确的函数。
回调自变量
您的回调可能有在启动异步调用时提供给它的附加参数。这些将在执行初始异步调用之前自动保存,并在调用回调时检索。示例:
#[callback]
fn my_endpoint_callback(
&self,
original_caller: ManagedAddress,
#[call_result] result: ManagedAsyncCallResult<BigUint>
) {
match result {
ManagedAsyncCallResult::Ok(value) => {
if value % 2 == 0 {
// do something
} else {
// do something else
}
},
ManagedAsyncCallResult::Err(err) => {
// log the error in storage
self.err_storage().set(&err.err_msg);
},
}
}
为了将这个回调分配给前面提到的异步调用,我们像这样挂钩它:
#[endpoint]
fn caller_endpoint(&self) {
// other code here
let caller = self.blockchain().get_caller();
self.contract_proxy(callee_sc_address)
.my_endpoint(my_biguint_arg)
.async_call()
.with_callback(self.callbacks().my_endpoint_callback(caller))
.call_and_exit();
}
注意回调现在有一个参数:
self.callbacks().my_endpoint_callback(caller)
然后,您可以像使用任何其他函数参数一样在回调中使用original_caller
。
代理付款中的参数
假设您想要调用一个#[payable]
端点,定义如下:
#[payable("*")]
#[endpoint(myEndpoint)]
fn my_payable_endpoint(&self, arg: BigUint) -> BigUint {
let payment = self.call_value().egld_or_single_esdt();
// implementation
}
要通过支付,您可以使用with_egld_or_single_esdt_token_transfer
方法:
#[endpoint]
fn caller_endpoint(&self, token: EgldOrEsdtTokenIdentifier, nonce: u64, amount: BigUint) {
// other code here
self.contract_proxy(callee_sc_address)
.my_endpoint(token, nonce, amount, my_biguint_arg)
.with_egld_or_single_esdt_token_transfer(token, nonce, amount)
.async_call()
.call_and_exit();
}
with_egld_or_single_esdt_token_transfer
允许添加单个 ESDT 代币的 EGLD 支付作为支付。
其他类型的支付也有类似的功能:
add_esdt_token_transfer
-适用于单次 ESDT 转账with_egld_transfer
-用于 EGLD 转账with_multi_token_transfer
-适用于 ESDT 多次转机
回拨款项
如果你期望收到一笔付款而不是支付合约,记住回调函数是默认的#[payable]
,所以你不需要添加注释:
#[callback]
fn my_endpoint_callback(&self, #[call_result] result: ManagedAsyncCallResult<BigUint>) {
let payment = self.call_value().egld_or_single_esdt();
match result {
ManagedAsyncCallResult::Ok(value) => {
if value % 2 == 0 {
// do something
} else {
// do something else
}
},
ManagedAsyncCallResult::Err(err) => {
// log the error in storage
self.err_storage().set(&err.err_msg);
},
}
}
请记住,当挂钩回调时,您不需要指定付款:
#[endpoint]
fn caller_endpoint(&self) {
// other code here
self.contract_proxy(callee_sc_address)
.my_endpoint(my_biguint_arg)
.async_call()
.with_callback(self.callbacks().my_endpoint_callback())
.call_and_exit();
}
气限执行
with_gas_limit
允许您指定通话的气体限制。默认情况下,所有剩余的 gas 都被传递,任何剩余的 gas 要么被返回用于进一步执行(对于同步调用),要么被返回用于回调执行(对于异步调用)。
方法二:手动编写代理
有时您无法访问被调用方合约代码,或者导入它很不方便(例如,不同的框架版本)。在这种情况下,您必须手动声明您的代理。让我们使用与第一种方法相同的示例端点:
mod callee_proxy {
elrond_wasm::imports!();
#[elrond_wasm::proxy]
pub trait CalleeContract {
#[payable("*")]
#[endpoint(myEndpoint)]
fn my_payable_endpoint(&self, arg: BigUint) -> BigUint;
}
}
这是你唯一需要改变的事情。其余的都是一样的,您声明一个代理构建器,调用与方法#1 中的完全一样。
#[proxy]
fn contract_proxy(&self, sc_address: ManagedAddress) -> callee_proxy::Proxy<Self::Api>;
方法三:手动呼叫(不推荐)
如果出于某种原因,您不想使用合约代理,您可以手动创建一个ContractCall
对象:
let mut contract_call = ContractCall::new(
self.api,
dest_sc_address,
ManagedBuffer::new_from_bytes(endpoint_name),
);
其中dest_sc_address
是被调用者合约的地址,endpoint_name
将是b"myEndpoint"
。
从这里开始,您将使用contract_call.push_endpoint_arg(&your_arg)
和方法#1 杂项类别中描述的手动付款加法器功能。
然后,您将使用与之前自动构建的ContractCall
对象相同的execute_on_dest_context
、transfer_execute
和async_call
方法。
只有在必要时才使用这种方法,因为如果像这样处理低级代码,很容易出错。
结论
我们希望这涵盖了关于如何从您的合约调用另一个合约的所有问题。尽可能使用方法 1,必要时使用方法 2。尽可能避免方法 3。