首页
/ fuels-rs 调用其他合约实战:with_contracts 与 with_contract_ids 的输入输出准备机制

fuels-rs 调用其他合约实战:with_contracts 与 with_contract_ids 的输入输出准备机制

2026-09-05 11:41:27作者:袁立春Spencer

当 Sway 合约的方法内部会调用其他合约时,交易必须包含相应的 Input::Contract / Output::Contract,否则运行时无法为该外部合约分配状态上下文。本文基于 fuels-rs 官方文档 docs/src/calling-contracts/other-contracts.md,完整讲解 CallHandler 提供的两个依赖注入方法 with_contractswith_contract_ids 的用法差异、端到端测试的完整示例,并结合源码剖析其底层实现链路,帮助你在跨合约调用、日志解码和缺失合约修复场景中正确配置交易输入输出。

背景:为什么调用其他合约需要显式声明依赖

在 FuelVM 的模型中,合约本身不是"活的"进程:每次调用合约都需要把合约二进制和状态上下文重新载入交易。因此,如果你的合约方法内部会调用另一个合约(例如链上 A 合约调用 B 合约),你在构造这笔交易时就必须把 B 合约声明为交易的 InputOutput,这样 FuelVM 才能在执行过程中为 B 分配可读写内存与状态。

这一点与 EVM 的心智模型不同:在以太坊上你只需知道目标合约地址即可发起调用,而 Fuel 要求在交易层面显式列出所有参与计算的合约。fuels-rs 的 CallHandler(调用链构建器)替你完成这项工作——你只需要把外部合约"喂"给它,它会在构建 ScriptTransaction 时自动生成对应的输入输出配对。

两个方法:with_contracts 与 with_contract_ids

CallHandler 提供了两个设置外部合约依赖的构建器方法(builder method),可按链式方式插在 methods().xxx(...) 之后、.call() 之前:

with_contracts:传入 abigen 生成的合约实例

// 传入通过 abigen! 宏生成的合约实例
let response = contract_caller_instance
    .methods()
    .increment_from_contract(lib_contract_id, 42)
    .with_contracts(&[&lib_contract_instance])
    .call()
    .await?;

with_contracts(&[&contract_instance, ...]) 要求你传入abigen! 宏创建的合约实例(即实现了 ContractDependency trait 的实例)。它的核心优势在于:不仅会登记合约 ID,还会把外部合约的 LogDecoder 合并进当前调用的日志解码器。这意味着外部合约发出的 log 事件以及 require 失败等回滚错误,都能被调用方合约正确传播并解码,而不是以裸字节形式出现在 receipts 中。

with_contract_ids:只传入合约 ID

// 只传入合约 ID,适合不需要解码外部合约日志的场景
let response = contract_caller_instance
    .methods()
    .increment_from_contract(lib_contract_id, 42)
    .with_contract_ids(&[lib_contract_id])
    .call()
    .await?;

如果你不需要解码外部合约的日志,或者手边根本没有 abigen! 生成的合约实例(比如你只拿得到一个 ContractId),就可以改用 with_contract_ids(&[&contract_id, ...]),直接提供所需的合约 ID 列表。这是更轻量、更通用的方式。

端到端测试:test_contract_calling_contract

上面的两段代码并非凭空构造,它们来自仓库中的 E2E 测试 test_contract_calling_contract。该测试的完整流程值得完整阅读,因为它同时覆盖了两条路径和多种调用形态:

  1. 测试资产:测试通过 setup_program_test! 宏同时部署了两个合约——被调用方 LibContract(提供 incrementrequire 方法)和调用方 LibContractCaller,并且把 LibContract 部署了两份lib_contract_instancelib_contract_instance2)以模拟对两个不同合约的并发依赖。

  2. Sway 侧的实现ContractCaller 通过 abi(LibContract, contract_id.into()) 动态构造外部合约代理并发起调用,例如:

    fn increment_from_contract(contract_id: ContractId, value: u64) -> u64 {
        let contract_instance = abi(LibContract, contract_id.into());
        contract_instance.increment(value)
    }
    
  3. 单合约依赖with_contracts 路径):

    let response = contract_caller_instance
        .methods()
        .increment_from_contract(lib_contract_id, 42)
        .with_contracts(&[&lib_contract_instance])
        .call()
        .await?;
    assert_eq!(43, response.value);
    
  4. 多合约依赖:测试中还演示了同时依赖两个 LibContract 实例的写法——注意两个实例类型相同但部署位置不同,SDK 按传入顺序与合约 ID 对应:

    let response = contract_caller_instance
        .methods()
        .increment_from_contracts(lib_contract_id, lib_contract_id2, 42)
        // Note that the two lib_contract_instances have different types
        .with_contracts(&[&lib_contract_instance, &lib_contract_instance2])
        .call()
        .await?;
    assert_eq!(86, response.value); // 43 + 43
    
  5. 仅 ID 依赖with_contract_ids 路径):与第 3 步完全等价的调用,只是用 with_contract_ids(&[lib_contract_id]) 替代合约实例,最终同样得到 43

这个测试验证了一个关键结论:两种写法在"能否成功调用"上没有区别,区别只在于是否携带日志解码能力。

源码剖析:从 CallHandler 到 ContractDependencyConfigurator

方法定义与日志解码器合并

两个方法都定义在 CallHandler 上,源码位于 call_handler.rs

  • with_contract_idsL174-L178):把 ID 切片拷贝成 Vec 后调用 self.call.with_external_contracts(...),把外部合约登记到调用对象上。文档注释明确说明其效果是"创建 fuel_tx::Input::Contract / fuel_tx::Output::Contract 配对并写入交易"。

  • with_contractsL188-L197):在同样的登记动作之外,多了一个循环——

    pub fn with_contracts(mut self, contracts: &[&dyn ContractDependency]) -> Self {
        self.call = self
            .call
            .with_external_contracts(contracts.iter().map(|c| c.id()).collect());
        for c in contracts {
            self.log_decoder.merge(c.log_decoder());
        }
        self
    }
    

    这里正是"外部合约日志可解码"的来源:每个传入实例的 LogDecoder 被合并进 CallHandler 持有的解码器,后续 CallResponse::logs() 与回滚错误解析都会用到它。

ContractDependency trait:合约实例为什么"够用"

with_contracts 的参数类型是 &dyn ContractDependency,这个 trait 定义在 call_handler.rs L33-L36,极其精简:

// Trait implemented by contract instances so that
// they can be passed to the `with_contracts` method
pub trait ContractDependency {
    fn id(&self) -> ContractId;
    fn log_decoder(&self) -> LogDecoder;
}

也就是说,SDK 只需要合约实例暴露两样东西:合约 ID 和日志解码器。任何由 abigen! 生成的合约实例都实现了该 trait,因此可以无差别地传入 with_contracts

ContractDependencyConfigurator:依赖如何落到交易上

with_external_contracts 本身来自密封 trait ContractDependencyConfigurator。它同时为 ContractCallScriptCall 提供实现,把 ID 写入各自结构体的 external_contracts 字段:

impl ContractDependencyConfigurator for ContractCall {
    fn with_external_contracts(self, external_contracts: Vec<ContractId>) -> Self {
        ContractCall {
            external_contracts,
            ..self
        }
    }
}

在后续构建交易的环节,external_contracts 与主合约 ID 一起被收集为去重集合(见 utils.rs 中的 extract_unique_contract_ids),再由交易构建器转换为成对的 Input::Contract / Output::Contract。这也解释了为什么同一个合约即使被多次调用也只需登记一次。

进阶:缺失合约的自动探测——determine_missing_contracts

即使你声明了依赖,如果 Sway 侧通过运行时变量(例如配置常量或存储槽)动态确定了被调用合约,静态构建时也可能漏掉某些 ID。SDK 为此提供了一个自愈机制 determine_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),
    }
}

其工作方式:先以"真实执行"方式做一次模拟;若模拟因 PanicReason::ContractNotInInputs 失败,就通过 find_ids_of_missing_contracts 扫描 receipts,从每条 Receipt::Panic 中提取缺失的合约 ID 并自动追加为外部依赖,供下一次真实提交使用。这为"合约 ID 在运行时才确定"的场景提供了兜底手段。

选型建议与小结

场景 推荐方法 理由
持有 abigen! 生成的合约实例,且需要解码外部合约日志或 require 回滚信息 with_contracts(&[&instance, ...]) 自动合并 LogDecoder,日志与回滚可解码
只有 ContractId,不关心外部合约日志 with_contract_ids(&[id, ...]) 轻量、无需实例
被调用合约 ID 运行时才确定、静态无法枚举 with_contract_ids 后配合 determine_missing_contracts 模拟失败自动补齐缺失依赖

小结:fuels-rs 中"合约调用合约"的核心约束是交易必须携带外部合约的 Input/Output,而 CallHandler::with_contractswith_contract_ids 分别以"合约实例"和"合约 ID"两种粒度替你完成这项登记,前者额外提供日志解码能力。参考文档 other-contracts.md、E2E 测试 contracts.rs 与源码 call_handler.rs,即可在项目中正确接入跨合约调用。

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