fuels-rs 合约调用全指南:从 ABI 绑定、方法调用到结果解析与参数配置
本篇技术指南聚焦 Fuel Network Rust SDK(fuels-rs)中"如何调用已部署合约"这一完整链路:从为合约生成类型安全的方法绑定、构建与执行一次合约调用,到读取返回值与日志,再到通过 CallParameters 与 TxPolicies 精细化控制转账金额、资产与 Gas,以及将"提交交易"与"读取结果"分离的异步编程范式。读完本文,你将能够基于 SDK 编写出可运行的合约调用代码,并理解其背后 CallHandler、CallResponse 等核心类型的工作方式。本文以 docs/src/calling-contracts/index.md 为核心骨架,全部示例均可在本仓库源码中找到对应实现。
部署之后,你会想对合约做什么
当你参考 部署章节 完成合约部署后,通常会立刻面对四类需求:
- 调用合约中的方法(Read / Write);
- 配置调用参数与交易策略(如 Gas、费用、资产转账);
- 在调用中携带或转发 coins 与 gas;
- 读取并解读合约返回的值与日志。
本仓库对"调用合约"这一主题维护了一个完整的文档子章节,位于 docs/src/calling-contracts 目录下,包含基础调用、调用参数、交易策略、返回响应、日志、多合约调用、模拟执行、费用估算、自定义资产转移与自定义输入输出、低层级调用、变量输出、跨钱包调用等专题。本文先打通主线,再逐层展开这些可配置面。
一次最简单的合约调用
假设你在 Sway 侧编写了一个合约,其 ABI 中包含两个方法 initialize_counter(u64) 与 increment_counter(u64)。仓库中的 e2e/sway/contracts/contract_test/src/main.sw 正是这样一个典型合约:它通过 storage 持久化一个 counter,initialize_counter 负责写入初值,increment_counter 读取旧值并累加后写回,最后都返回当前的计数值。
在 Rust 侧,一次典型的调用流程如下(完整示例见 examples/contracts/src/lib.rs):
// 通过 abigen! 宏读取合约的 ABI JSON,
// 把合约的方法以类型安全的方式生成到 MyContract 上
abigen!(Contract(
name = "MyContract",
abi = "e2e/sway/contracts/contract_test/out/release/contract_test-abi.json"
));
// 用合约 ID 与签名钱包创建合约实例,之后即可调用链上方法
let contract_instance = MyContract::new(contract_id_2, wallet);
let response = contract_instance
.methods()
.initialize_counter(42) // 构建一次 ABI 调用
.call() // 执行网络调用
.await?; // 合约调用是异步的
assert_eq!(42, response.value);
let response = contract_instance
.methods()
.increment_counter(10)
.call()
.await?;
assert_eq!(52, response.value);
这一小段代码体现了 fuels-rs 合约调用的核心心智模型:abigen! 生成绑定 → MyContract::new 创建实例 → .methods().方法名(参数) 构建调用 → .call().await 执行并拿到 CallResponse。其中 contract_id 与 wallet 来自部署环节,例如通过 Contract::load_from(...).deploy(&wallet, TxPolicies::default()).await?.contract_id 获取(见 examples/contracts/src/lib.rs)。上述示例全程使用默认配置,足以覆盖大多数只读或纯逻辑调用。
为什么到处都在链式 .call().await?
细心的话你会发现 SDK 示例中总是出现 .call()、.await、?(或 unwrap())连写的调用链,这并非巧合,而是由 fuels-rs 的异步 + Result 设计决定的(详见 call-response.md):
- 调用执行前需要二选一:
.call()(真实提交交易)还是.simulate()(模拟执行,不产生状态变更,见 simulation.md); - 合约调用是异步的,你可以选择就地
.await,也可以并发发起多个任务,充分利用 Rust 的 async 生态; - 合约调用返回的是
Result<CallResponse, Error>,因此通常需要?或.unwrap()取出来。
CallResponse 的组成
当 Result 被解开后,你拿到的是一个 CallResponse,其定义位于 packages/fuels-programs/src/responses/call.rs,主要包含四个字段:
value:合约方法返回的真实值,其 Rust 类型由 ABI 精确推导。例如 Sway 的u64对应 Rust 的u64;Sway 元组(u8, bool)对应 Rust 的(u8, bool);若合约返回自定义结构体MyStruct { u64, b256 },abigen!会在编译期生成同名的 Rust struct,其中b256对应[u8; 32];receipts:该次合约调用产生的全部 receipts;gas_used:本次合约调用消耗的 Gas 量;tx_id:对应已提交交易的 ID。
Result 的判错与解错
对合约调用返回的 Result,可以直接用 is_ok / is_err 判断成功与否:
let is_ok = response.is_ok();
let is_error = response.is_err();
当 is_err() 为真时,可用 unwrap_err 取出错误信息:
if response.is_err() {
let err = response.unwrap_err();
println!("ERROR: {:?}", err);
};
将"提交"与"取值"分离:.submit() 模式
默认的 .call() 会在一次 await 内同时完成交易提交与结果轮询。但某些场景(例如需要精确控制等待时机、或想在后台异步运行交易)要求把提交交易与获取返回值拆开。此时可以使用 .submit()(见 examples/contracts/src/lib.rs):
// submit() 只负责把交易提交到链上,返回提交结果
let response = contract_instance
.methods()
.initialize_counter(42)
.submit()
.await?;
// 之后在需要取值时再调用 response() 等待执行完成并取回 value
tokio::time::sleep(Duration::from_millis(500)).await;
let value = response.response().await?.value;
assert_eq!(42, value);
从 SDK 的调用抽象看,.call() 与 .submit() 分别对应不同的"完成度":.call() 内部会替你完成提交并解析响应;.submit() 则把解析动作推迟到你显式调用 response.response() 时。相关的提交与响应载体分别位于 packages/fuels-programs/src/responses/submit.rs 与 packages/fuels-programs/src/responses/call.rs。
配置合约调用的各类参数
一次合约调用可以被精细配置。fuels-rs 将其拆分为两类截然不同的配置对象:
交易策略 TxPolicies
TxPolicies 描述的是承载这次调用的交易整体策略,包括(定义见 transaction.rs 中的 tx_policies_struct):
- Tip:支付给区块生产者的优先费,用于提升交易被打包的优先级;
- Witness Limit:交易允许携带的最大 witness 数据量;
- Maturity:在此之前交易无法被打包进块的"成熟块高";
- Expiration:在此之后交易无法被打包进块的"过期块高";
- Max Fee:本交易可支付的最高费用;
- Script Gas Limit:交易执行脚本代码可消耗的 Gas 上限。
当 Script Gas Limit 未显式设置时,SDK 会在后台估算执行所需 Gas 并自动设置为上限;当 Witness Limit 未设置时,SDK 会按交易构建器中所有 witness 与签名的大小自动补齐。
通过链式方法 with_tx_policies 传入自定义策略(见 examples/contracts/src/lib.rs):
let contract_methods = MyContract::new(contract_id, wallet.clone()).methods();
let tx_policies = TxPolicies::default()
.with_tip(1)
.with_script_gas_limit(1_000_000)
.with_maturity(0)
.with_expiration(10_000);
let response = contract_methods
.initialize_counter(42) // 选择合约方法
.with_tx_policies(tx_policies) // 链式挂上交易策略
.call() // 执行调用
.await?; // 异步等待
同样地,TxPolicies 也适用于部署合约、转账资产等场景——凡接受它的方法都可传入同一份策略(详见 tx-policies.md)。
调用参数 CallParameters
CallParameters 描述的则是这一次具体调用要携带与转发的资源,共三个字段:
- Amount:转发的 token 数量;
- Asset ID:转发的资产 ID(默认基础资产 base asset);
- Gas forwarded:转发给被调用合约的 Gas 上限。
这三个参数常配合 Sway 侧上下文读取使用。例如 contract_test 合约中标注了 #[payable] 的 get_msg_amount(),通过 Sway 标准库的 msg_amount() 返回本次调用携带的金额(见 main.sw):
#[payable]
fn get_msg_amount() -> u64 {
msg_amount()
}
在 Rust 侧,创建 CallParameters 实例并用链式方法 call_params 传入(见 examples/contracts/src/lib.rs):
let contract_methods = MyContract::new(contract_id, wallet.clone()).methods();
let tx_policies = TxPolicies::default();
// 转发 1_000_000 数量的基础资产给合约
let call_params = CallParameters::default().with_amount(1_000_000);
let response = contract_methods
.get_msg_amount() // 选择合约方法
.with_tx_policies(tx_policies) // 挂交易策略
.call_params(call_params)? // 挂调用参数
.call() // 执行调用
.await?;
关于 payable 的校验:
call_params之所以返回Result,是为了防止你向未标注#[payable]的合约方法转发资产。若将 Amount 设为非 0 值却调用非 payable 方法(如non_payable),SDK 会直接报错。相应的端到端测试见 e2e/tests/contracts.rs 中的non_payable_params。需要注意:向合约调用转发 Gas 始终被允许,与目标方法是否 payable 无关。
使用默认值
无论 TxPolicies 还是 CallParameters,都可以直接使用 ::default() 采用默认值(默认 CallParameters 常量定义见 constants.rs 的 default_call_parameters):
let response = contract_methods
.initialize_counter(42)
.call_params(CallParameters::default())?
.call()
.await?;
gas_forwarded 与默认 Gas 行为
CallParameters 中的 gas_forwarded 语义需要特别注意(详见 call-params.md):
- 它约束的是单次合约调用本身的 Gas 上限,而非整笔交易的 Gas 上限,因此会受交易级 Gas 限制的约束;
- 若它被设置成大于可用 Gas 的值,SDK 会把当前所有可用 Gas 全部转发过去;
- 若不设置调用参数,或使用
CallParameters::default(),则默认行为是把交易的 Gas limit 整体转发给该次调用。
扩展阅读:合约调用能力全景
index.md 指出"接下来我们看看如何进一步配置合约调用中众多不同的参数"。在深入源码前,以下专题子文档可帮助你按需组合这些能力:
- call-params.md:Amount / Asset ID / Gas forwarded 的完整配置与 payable 约束;
- tx-policies.md:Tip、Witness Limit、Maturity、Expiration、Max Fee、Script Gas Limit;
- call-response.md:
CallResponse的value、receipts、gas_used、tx_id与错误处理; - calls-with-different-wallets.md:用不同钱包发起调用;
- multicalls.md:一次交易中执行多次合约调用;
- simulation.md:不落账的模拟执行(
.simulate()); - cost-estimation.md:调用前估算交易成本;
- logs.md:解码并读取合约日志;
- custom-asset-transfer.md 与 variable-outputs.md:自定义资产转移与可变数量输出;
- custom-inputs-outputs.md:对交易输入输出做细粒度自定义;
- low-level-calls.md:不走 ABI 绑定的底层调用;
- other-contracts.md:在合约内调用其它合约;
- tx-dependency-estimation.md:交易依赖估算。
从源码结构看调用链的实现
如果希望进一步理解 .methods() 之后发生了什么,可以顺着以下源码路径阅读:
- examples/contracts/src/lib.rs:本文所有可运行示例的锚点源头(
use_deployed_contract、submit_response_contract、call_parameters等均在此文件); - packages/fuels-programs/src/calls:调用处理核心目录,其中 call_handler.rs 定义了方法调用的统一处理入口(
call/submit/simulate的编排),contract_call.rs 与 script_call.rs 分别处理合约与脚本两类调用载体; - packages/fuels-programs/src/responses/call.rs 与 packages/fuels-programs/src/responses/submit.rs:分别承载
CallResponse(含value/receipts/gas_used/tx_id)与"先提交、后取响应"两类结果对象; - packages/fuels-programs/src/calls/utils.rs:交易构建、Gas 计算等被共享的工具逻辑。
从这些文件的分工可以推断,fuels-rs 把"构建 ABI 调用 → 组装交易(注入 TxPolicies 与 CallParameters)→ 提交/模拟 → 解析 receipts 得到返回值"拆成了职责单一的多层组件:CallHandler 负责把一次 .methods().foo(args) 攒成可执行的调用,最终由交易层统一提交给 Provider 与 Fuel 节点。
小结
合约调用是 fuels-rs 日常开发中使用频率最高的能力。其核心心法可以浓缩为一条调用链:abigen! 生成 ABI 绑定 → Contract::new(contract_id, wallet) 得到实例 → .methods().foo(args) 选方法 → .with_tx_policies(...) 配交易策略 → .call_params(...) 配转发资源 → .call() / .submit() / .simulate() 决定执行方式 → .await 后从 CallResponse.value 取返回值。掌握了这条主链,再结合 index.md 导航下 docs/src/calling-contracts 各专题对多合约调用、日志、费用估算、模拟执行等能力的扩展,你就能在 fuels-rs 中自由编排各种复杂链上交互。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00