首页
/ fuels-rs 交易依赖估算:让 SDK 自动为合约调用补齐外部契约与可变输出

fuels-rs 交易依赖估算:让 SDK 自动为合约调用补齐外部契约与可变输出

2026-09-05 16:07:39作者:裘旻烁

本篇基于 fuels-rs 官方文档 tx-dependency-estimation.md 展开。当一个 Sway 合约方法在链上调用另一个外部合约、并向指定地址铸造资产时,这笔交易往往需要手工声明外部契约(external contracts)与可变输出(variable outputs),否则调用会直接 revert。本文完整继承官方文档的三个代码示例,并结合 call_handler.rsutils.rs 的源码实现,讲清 determine_missing_contracts()VariableOutputPolicy::EstimateMinimum 背后的 dry run 估算机制,帮助你在不事先知道依赖数量的情况下自动补齐交易依赖。

问题场景:缺少依赖的调用会 revert

官方文档给出的场景是:调用方合约 lib_contract_caller 的方法 mint_then_increment_from_contract 会先调用外部合约,随后向指定地址铸造资产。示例代码来自 examples/contracts/src/lib.rs,其中部署了两个 Sway 合约(位于 e2e/sway/contracts/lib_contracte2e/sway/contracts/lib_contract_caller),并通过 abigen! 宏生成绑定:

abigen!(Contract(
    name = "MyContract",
    abi = "e2e/sway/contracts/lib_contract_caller/out/release/lib_contract_caller-abi.json"
));

let called_contract_id: ContractId = Contract::load_from(
    "../../e2e/sway/contracts/lib_contract/out/release/lib_contract.bin",
    LoadConfiguration::default(),
)?
.deploy(&wallet, TxPolicies::default())
.await?
.contract_id;

如果不声明任何依赖直接发起调用,SDK 会在后台构造交易并提交,链上执行因缺少外部契约输入而失败,返回 Err(Error::Transaction(Reason::Failure { .. }))

let address = wallet.address();
let amount = 100;

let response = contract_methods
    .mint_then_increment_from_contract(called_contract_id, amount, address.into())
    .call()
    .await;

assert!(matches!(
    response,
    Err(Error::Transaction(Reason::Failure { .. }))
));

这个 revert 的根因是:FuelVM 执行 call 指令时要求被调用的外部合约必须已经出现在交易的 Inputs 中,否则触发 ContractNotInInputs panic;而铸造资产又需要为外部合约预留交易输出(output)。SDK 提供了两条解决路径:手工声明,或让 SDK 估算。

方案一:手动声明依赖

如果你已知外部合约的 ContractId 以及所需可变输出数量,可以用两个 builder 方法显式指定,这正是文档中 dependency_estimation_manual 示例的写法:

let response = contract_methods
    .mint_then_increment_from_contract(called_contract_id, amount, address.into())
    .with_variable_output_policy(VariableOutputPolicy::Exactly(1))
    .with_contract_ids(&[called_contract_id])
    .call()
    .await?;
  • with_contract_ids(&[called_contract_id]):把外部契约 ID 加入交易输入。CallHandler 还提供了 with_contracts(&[&contract_instance, ...]),适用于持有 abigen! 生成的合约实例的场景,其优点是后续可以解码来自该外部合约的日志,参见 other-contracts.md
  • with_variable_output_policy(VariableOutputPolicy::Exactly(1)):声明该交易需要恰好 1 个可变输出。VariableOutputPolicy 的默认值是 Exactly(0),即默认不添加任何可变输出。

VariableOutputPolicy 定义于 packages/fuels-core/src/types/transaction_builders.rs

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VariableOutputPolicy {
    /// Perform a dry run of the transaction estimating the minimum number of variable outputs to
    /// add.
    EstimateMinimum,
    /// Add exactly these many variable outputs to the transaction.
    Exactly(usize),
}

手动方式的局限很明显:你必须事先知道外部合约 ID 和输出数量的准确值。对于嵌套调用多层合约、或输出数量依赖运行时逻辑的复杂场景,逐一手工维护既不现实也容易出错。

方案二:依赖自动估算

文档给出的替代方案是链式组合两个调用:

  • .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
  • .determine_missing_contracts()

组合后,SDK 会在后台执行多次模拟调用(dry run),把缺失的依赖估算出来并自动设置。完整的 dependency_estimation 示例如下,注意 determine_missing_contracts() 是异步方法,必须在 .call() 之前 await

let response = contract_methods
    .mint_then_increment_from_contract(called_contract_id, amount, address.into())
    .with_variable_output_policy(VariableOutputPolicy::EstimateMinimum)
    .determine_missing_contracts()
    .await?
    .call()
    .await?;

示例测试随后验证了效果:两次调用成功后,钱包持有的 asset_id 余额从 amount 增长到 2 * amount,证明估算出的依赖确实让 mint 逻辑在链上正确执行(见 examples/contracts/src/lib.rs)。

源码解析:determine_missing_contracts 如何工作

packages/fuels-programs/src/calls/call_handler.rs 中,CallHandlerdetermine_missing_contracts 实现如下:

pub async fn determine_missing_contracts(mut self) -> Result<Self> {
    match self.simulate(Execution::realistic()).await {
        Ok(_) => Ok(self),

        Err(Error::Transaction(Reason::Failure { ref receipts, .. })) => {
            for contract_id in find_ids_of_missing_contracts(receipts) {
                self.call.append_external_contract(contract_id);
            }

            Ok(self)
        }

        Err(other_error) => Err(other_error),
    }
}

机制可以拆解为三步:

  1. 先模拟执行一次self.simulate(Execution::realistic()) 会基于当前调用构造一笔完整交易,交给节点做 dry run。从 call_handler.rssimulate 实现可以看到,它调用 provider.dry_run_opt(tx, true, None, at_height),即在本地节点上按真实执行路径预演这笔交易,而不真正上链。

  2. 从失败收据中定位缺失合约。如果模拟失败且失败原因是 Reason::Failure,SDK 调用 find_ids_of_missing_contracts 扫描收据:

    pub fn find_ids_of_missing_contracts(receipts: &[Receipt]) -> Vec<ContractId> {
        receipts
            .iter()
            .filter_map(|receipt| match receipt {
                Receipt::Panic {
                    reason,
                    contract_id,
                    ..
                } if *reason.reason() == PanicReason::ContractNotInInputs => {
                    let contract_id = contract_id
                        .expect("panic caused by a contract not in inputs must have a contract id");
                    Some(contract_id)
                }
                _ => None,
            })
            .collect()
    }
    

    它专门匹配 PanicReason::ContractNotInInputs 的 panic 收据,并从收据中取出缺失合约的 ContractId。这正是前面 revert 示例中链上实际产生的错误类型。

  3. 把缺失合约追加进调用。每个找到的 ContractId 都会通过 append_external_contract 追加到 self.call 的外部契约列表中,下一次 build 交易时就会被放入 Inputs

需要强调的一个行为边界是:如果模拟失败但不是「合约缺失」类的 Reason::Failure(例如其他 panic 或网络错误),该方法会把原始错误原样返回(Err(other_error) => Err(other_error)),不会吞掉错误,也不会做无效修复。

可变输出估算的代价与边界

VariableOutputPolicy::EstimateMinimum 的源码注释(packages/fuels-core/src/types/transaction_builders.rs)明确说明其原理与风险:

Estimation of variable outputs is performed by saturating the transaction with variable outputs and counting the number of outputs used.

即 SDK 用一批可变输出把交易「灌满」,执行一次 dry run 后统计实际被用到的输出数量,以此确定最小需求数。这也解释了文档开头所说的「以在后台运行多次模拟调用的为代价」。同一段注释还给出了官方警告:如果脚本会自省可变输出的数量并据此调整逻辑(理论上可以一直铸造输出直到把所有可变输出占满),这种估算将基本失效——此时应改为 Exactly(n) 手动指定。

适用范围:script 调用与 multicall

文档末尾的 Note 指出,with_variable_output_policydetermine_missing_contracts 同样适用于 script 调用和多调用(multicall)。这一点在源码中可以得到印证:

  • VariableOutputPolicyScriptTransactionBuilder 的字段之一(见 transaction_builders.rs),script 交易构造流程同样消费该策略;
  • MultiCallcall_handler.rs 中实现了自己的 determine_missing_contracts,与单次合约调用的逻辑一致,但使用 simulate_without_decode() 做模拟(多调用返回 Vec 值,模拟阶段无需解码),并对每个找到的缺失合约调用 append_external_contract

另一个必须注意的限制:determine_missing_contracts() 只负责把外部契约补进交易输入,不会启用来自外部合约的日志解码。如果你需要读取被调外部合约的日志,应改用 with_contracts(&[&contract_instance, ...]) 传入 abigen! 实例,详见 other-contracts.md

小结与实践建议

  • 依赖已知且稳定(固定外部合约、固定输出数量)时,优先使用 with_contract_ids + with_variable_output_policy(VariableOutputPolicy::Exactly(n)),零额外模拟开销,交易行为完全确定;
  • 依赖不确定或随链上状态变化时,使用 with_variable_output_policy(VariableOutputPolicy::EstimateMinimum) + determine_missing_contracts().await?,接受一次额外 dry run 的代价换取自动化;
  • 记住 determine_missing_contracts() 返回的是 Result<Self>,需要在调用链上 await? 后再 .call()
  • 若需要解码外部合约日志,或脚本会依据可变输出数量动态调整逻辑,自动估算不可靠,应回退到手动声明。

相关延伸阅读:variable-outputs.md 讲解可变输出的完整用法,other-contracts.md 讲解外部契约输入与日志解码,multicalls.md 讲解多调用中依赖声明的组合方式。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384