fuels-rs 合约调用成本估算:estimate_transaction_cost 参数、TransactionCost 结构与源码实现详解
本文基于 fuels-rs 官方文档 docs/src/calling-contracts/cost-estimation.md 展开,详解如何通过对 CallHandler 调用 estimate_transaction_cost(tolerance, block_horizon) 对单次合约调用和多调用(multi-call)交易进行成本预估:从 TransactionCost 各字段的含义、tolerance 与 block_horizon 两个参数的取值与默认值,到底层基于 dry-run 与 gas price 估算的实现调用链。读完后你将能在提交交易前准确预估 gas 消耗与费用,并将预估结果用于设置 gas limit 或向用户展示费用。
TransactionCost:估算结果的结构体定义
文档指出,estimate_transaction_cost 的返回类型为 TransactionCost,该结构体包含与本次估算相关的全部关键信息。在 SDK 源码 provider.rs 中,其定义如下:
#[derive(Debug, Clone, PartialEq)]
pub struct TransactionCost {
pub gas_price: u64,
pub metered_bytes_size: u64,
pub total_fee: u64,
pub script_gas: u64,
pub total_gas: u64,
}
各字段含义如下:
| 字段 | 类型 | 说明 |
|---|---|---|
gas_price |
u64 |
估算时采用的 gas 单价(来源于节点的 gas price 估算接口) |
metered_bytes_size |
u64 |
交易按节点收费规则计费的字节大小 |
total_fee |
u64 |
应用容差后的交易总费用 |
script_gas |
u64 |
花在脚本(script)执行部分上的 gas 消耗 |
total_gas |
u64 |
应用容差后的 gas 总消耗 |
文档特别注明:script_gas 指的是 gas 中花在脚本执行上的那一部分。在 Fuel 的交易模型中,一次合约调用实际上是包装在一个 script 交易内执行的,因此 gas 消耗被区分为“脚本执行消耗”与“整体消耗”(后者还包含合约调用等内部执行),这一区分对于调试“gas 用在哪了”非常有用。
两个参数:tolerance 与 block_horizon
estimate_transaction_cost(tolerance: Option<f64>, block_horizon: Option<u32>) 的两个参数均为 Option 类型,不传时使用 SDK 内置默认值。默认值定义在 constants.rs:
pub const DEFAULT_GAS_ESTIMATION_TOLERANCE: f64 = 0.2;
pub const DEFAULT_GAS_ESTIMATION_BLOCK_HORIZON: u32 = 5;
-
tolerance(容差,默认 0.2 即 20%):节点通过 dry-run 得到的 gas 与费用是“当下状态”的精确值,但交易真正上链时网络状态可能已变化(例如链上状态被其他交易修改、gas 价格波动)。tolerance用于在这两个数值上追加一层安全余量。从源码 provider.rs 的实现看,余量是这样施加的:fn apply_tolerance(value: u64, tolerance: f64) -> u64 { (value as f64 * (1.0 + tolerance)).ceil() as u64 }即对
total_gas和total_fee分别乘以1 + tolerance并向上取整。传Some(0.0)则不做任何放大,得到节点 dry-run 的原始估值。 -
block_horizon(区块地平线,默认 5):用于节点端 gas price 估算时参考的区块窗口大小,影响gas_price字段的取值。该值会透传给Provider::estimate_gas_price(block_horizon)(见 provider.rs),再经由节点计算得到当前 gas 单价。
底层实现:从 CallHandler 到 dry-run 的调用链
CallHandler::estimate_transaction_cost 定义在 call_handler.rs,其逻辑为:
- 调用
self.build_tx()先构建出一笔完整的待执行交易(build_tx内部会解析账户所需资源、组装ScriptTransactionBuilder等); - 通过
self.account.try_provider()?取到账户绑定的Provider; - 委托给
Provider::estimate_transaction_cost(tx, tolerance, block_horizon)。
Provider 侧的实现(provider.rs)揭示了估算的完整流程:
pub async fn estimate_transaction_cost<T: Transaction>(
&self,
tx: T,
tolerance: Option<f64>,
block_horizon: Option<u32>,
) -> Result<TransactionCost> {
let block_horizon = block_horizon.unwrap_or(DEFAULT_GAS_ESTIMATION_BLOCK_HORIZON);
let tolerance = tolerance.unwrap_or(DEFAULT_GAS_ESTIMATION_TOLERANCE);
let EstimateGasPrice { gas_price, .. } = self.estimate_gas_price(block_horizon).await?;
let tx_status = self.dry_run_opt(tx.clone(), false, None, None).await?;
let total_gas = Self::apply_tolerance(tx_status.total_gas(), tolerance);
let total_fee = Self::apply_tolerance(tx_status.total_fee(), tolerance);
let receipts = tx_status.take_receipts();
Ok(TransactionCost {
gas_price,
metered_bytes_size: tx.metered_bytes_size() as u64,
total_fee,
total_gas,
script_gas: Self::get_script_gas_used(&receipts),
})
}
可以归纳出估算由三步组成:
- 取 gas 单价:
estimate_gas_price(block_horizon)向节点查询 gas price; - dry-run 交易:
dry_run_opt让节点在不落盘的情况下执行这笔交易,得到total_gas/total_fee以及执行回执(receipts); - 提取 script_gas:
get_script_gas_used(provider.rs)从回执末尾查找Receipt::ScriptResult,取其gas_used作为脚本执行消耗的 gas。
也就是说,TransactionCost 中的每一项都来自真实执行模拟,而非经验公式,因此对实际费用有很强的参考价值。
实战示例:单次合约调用的成本估算
文档第一个示例演示了如何对单个合约方法调用进行估算(对应源码 examples/contracts/src/lib.rs):
let contract_instance = MyContract::new(contract_id, wallet);
let tolerance = Some(0.0);
let block_horizon = Some(1);
let transaction_cost = contract_instance
.methods()
.initialize_counter(42) // Build the ABI call
.estimate_transaction_cost(tolerance, block_horizon) // Get estimated transaction cost
.await?;
关键点:
contract_instance.methods().initialize_counter(42)返回一个CallHandler,即文档所说的“Build the ABI call”——先构建好 ABI 调用对象,但此时并未提交交易;- 在其上直接调用
.estimate_transaction_cost(tolerance, block_horizon)即完成估算,全程无链上写入; - 示例中显式传入
tolerance = Some(0.0)、block_horizon = Some(1),得到的是“当前状态、无余量”的精确估值。
该用法在仓库的测试中也被真实验证过:examples/contracts/src/lib.rs 中的 contract_call_cost_estimation 测试对 initialize_counter(42) 断言了 script_gas == 2371、total_gas == 8623(在仓库配套 Sway 合约 e2e/sway/contracts/contract_test 与本地节点上的基准值)。
实战示例:多调用(multi-call)交易的成本估算
当一笔交易要连续调用多个合约方法时,可以先将各调用挂到一个 CallHandler 上再统一估算(对应源码 examples/contracts/src/lib.rs):
let call_handler_1 = contract_methods.initialize_counter(42);
let call_handler_2 = contract_methods.get_array([42; 2]);
let multi_call_handler = CallHandler::new_multi_call(wallet.clone())
.add_call(call_handler_1)
.add_call(call_handler_2);
let tolerance = Some(0.0);
let block_horizon = Some(1);
let transaction_cost = multi_call_handler
.estimate_transaction_cost(tolerance, block_horizon) // Get estimated transaction cost
.await?;
要点:
CallHandler::new_multi_call(wallet)创建多调用 handler,通过链式add_call追加每一次调用;- 估算接口与单调用完全一致——
estimate_transaction_cost内部会先把整笔多调用交易build_tx,再整体 dry-run,因此得到的是包含所有调用的合并成本; - 对应测试
multi_call_cost_estimation(examples/contracts/src/lib.rs)对两次调用断言了script_gas == 3863、total_gas == 10_692,可见多调用的总消耗并非各次简单相加,脚本侧开销也有差异,这正是需要先估算再决策的原因。
估算结果的典型用途
文档给出估算结果的两种常见用途:
- 为真实调用设置 gas limit:拿到
total_gas(含容差)后,可以在TxPolicies中将该值设为本次调用的 gas limit,避免调用时因 gas 不足被拒,同时避免盲目设置过大 limit 浪费费用; - 向用户展示预估费用:
total_fee与gas_price可以直接用于前端提示“本次操作预计花费 X”,提升交互透明度。
另外,文档特别注明:脚本(script)也提供同样的估算接口——即该接口不仅适用于合约调用的 CallHandler,脚本场景同样可基于 Provider::estimate_transaction_cost 对构建好的交易进行成本预估,方法签名与参数语义完全一致。
小结
- 估算入口:
CallHandler::estimate_transaction_cost(tolerance, block_horizon),定义于 packages/fuels-programs/src/calls/call_handler.rs; - 估算结果:
TransactionCost { gas_price, metered_bytes_size, total_fee, script_gas, total_gas },定义于 packages/fuels-accounts/src/provider.rs; - 默认参数:
tolerance = 0.2、block_horizon = 5,定义于 packages/fuels-core/src/utils/constants.rs; - 估算原理:构建交易 → 查询 gas price → 节点 dry-run → 对
total_gas/total_fee应用容差 → 从Receipt::ScriptResult提取script_gas; - 适用场景:单次合约调用、多调用批量估算、脚本交易估算,服务于 gas limit 设置与费用展示两类核心需求。
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 StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00