首页
/ fuels-rs 合约调用成本估算:estimate_transaction_cost 参数、TransactionCost 结构与源码实现详解

fuels-rs 合约调用成本估算:estimate_transaction_cost 参数、TransactionCost 结构与源码实现详解

2026-09-05 10:47:25作者:田桥桑Industrious

本文基于 fuels-rs 官方文档 docs/src/calling-contracts/cost-estimation.md 展开,详解如何通过对 CallHandler 调用 estimate_transaction_cost(tolerance, block_horizon) 对单次合约调用和多调用(multi-call)交易进行成本预估:从 TransactionCost 各字段的含义、toleranceblock_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_gastotal_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,其逻辑为:

  1. 调用 self.build_tx() 先构建出一笔完整的待执行交易(build_tx 内部会解析账户所需资源、组装 ScriptTransactionBuilder 等);
  2. 通过 self.account.try_provider()? 取到账户绑定的 Provider
  3. 委托给 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_gasget_script_gas_usedprovider.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 == 2371total_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_estimationexamples/contracts/src/lib.rs)对两次调用断言了 script_gas == 3863total_gas == 10_692,可见多调用的总消耗并非各次简单相加,脚本侧开销也有差异,这正是需要先估算再决策的原因。

估算结果的典型用途

文档给出估算结果的两种常见用途:

  1. 为真实调用设置 gas limit:拿到 total_gas(含容差)后,可以在 TxPolicies 中将该值设为本次调用的 gas limit,避免调用时因 gas 不足被拒,同时避免盲目设置过大 limit 浪费费用;
  2. 向用户展示预估费用total_feegas_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.2block_horizon = 5,定义于 packages/fuels-core/src/utils/constants.rs
  • 估算原理:构建交易 → 查询 gas price → 节点 dry-run → 对 total_gas/total_fee 应用容差 → 从 Receipt::ScriptResult 提取 script_gas
  • 适用场景:单次合约调用、多调用批量估算、脚本交易估算,服务于 gas limit 设置与费用展示两类核心需求。
登录后查看全文
热门项目推荐
相关项目推荐