首页
/ fuels-ts 多合约调用实战指南:用 multiCall 在单笔交易中批量调用合约函数

fuels-ts 多合约调用实战指南:用 multiCall 在单笔交易中批量调用合约函数

2026-09-05 23:56:04作者:晏闻田Solitary

本文基于 fuels-ts 官方文档《Multiple Contract Calls》(multi-contract-calls.md)编写,讲解如何在 Fuel Network TypeScript SDK 中通过 multiCall 方法在单笔交易中执行多个合约调用——既可以调用同一合约的多个函数,也可以跨合约混合调用,还可批量执行只读查询。读完本文,你将掌握 multiCall 的完整用法、.get / .simulate / .call 三种执行入口的时机差异,以及该方法在 Contract 类中的源码级实现。

为什么要做多合约调用

官方文档给出的核心结论是:你可以在单笔交易中执行多个合约调用,既可以针对同一合约,也可以针对不同的合约。这能够提升效率并降低整体交易成本

在 Fuel 网络上,每笔交易都需要支付 Gas 费用并产生链上状态变更。如果 dApp 的业务逻辑需要连续操作多个合约(例如「先读计数、再两次自增、再向另一个合约查询值」),逐个发起交易会放大费用与延迟;将它们打包进一笔交易的 multiCall 调用,可以把多次交互合并为一次提交。

同一合约的多次调用(Same Contract Multi Calls)

对同一合约批量调用多个函数,使用合约实例上的 multiCall 方法,传入一个由 functions 调用(FunctionInvocationScope 数组)组成的列表:

import { Provider, Wallet } from 'fuels';

import { CounterFactory } from '../../../../typegend';

const provider = new Provider(LOCAL_NETWORK_URL);
const deployer = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);

const counterContractTx = await CounterFactory.deploy(deployer);
const { contract: counterContract } = await counterContractTx.waitForResult();

const { waitForResult } = await counterContract
  .multiCall([
    counterContract.functions.get_count(),
    counterContract.functions.increment_count(2),
    counterContract.functions.increment_count(4),
  ])
  .call();

const { value: results } = await waitForResult();
// results[0] == 0
// results[1] == 2
// results[2] == 6

上述示例对应仓库中的 same-contract.ts 片段。它的行为要点:

  • multiCall 的参数是一个 FunctionInvocationScope 数组,每一项都是通过 contract.functions.<函数名>(...参数) 得到的调用作用域;
  • 所有调用按数组顺序排入队列,最终在一笔交易中依序执行;
  • 调用完成后,waitForResult() 返回的结果 value 是一个与调用顺序一一对应的结果数组:results[0]get_count 的返回值(0),results[1] 是第一次 increment_count(2) 后的值(2),results[2] 是第二次 increment_count(4) 后的值(6)——可以看到队列中的调用共享同一个合约状态,前一次写入会影响后一次的读取。

不同合约的多次调用(Different Contracts Multi Calls)

multiCall 同样支持在单笔交易中调用不同的合约。你只需在数组里混入来自多个合约实例的函数调用作用域:

import { Provider, Wallet } from 'fuels';

import { CounterFactory, EchoValuesFactory } from '../../../../typegend';

const provider = new Provider(LOCAL_NETWORK_URL);
const deployer = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);

const counterContractTx = await CounterFactory.deploy(deployer);
const { contract: counterContract } = await counterContractTx.waitForResult();
const echoContractTx = await EchoValuesFactory.deploy(deployer);
const { contract: echoContract } = await echoContractTx.waitForResult();

const { waitForResult } = await echoContract
  .multiCall([
    echoContract.functions.echo_u8(17),
    counterContract.functions.get_count(),
    counterContract.functions.increment_count(5),
  ])
  .call();

const { value: results } = await waitForResult();
// results[0] == 17
// results[1] == BN <0>
// results[2] == BN <5>

对应仓库中的 different-contracts.ts。从示例可以看出两个细节:

  • 调用 multiCall 的宿主合约(这里是 echoContract)只是「入口」,数组内既可以放它的函数调用,也可以放 counterContract 的调用,SDK 会在同一笔交易中依次完成对两个不同合约地址的调用;
  • 返回结果仍按调用顺序对齐:results[0]echo_u8(17) 的回显值 17,results[1] 是计数合约当前值 0,results[2] 是自增 5 之后的值 5(此处以 BN 大数类型返回)。

为每个调用链式附加 callParams

官方文档特别指出:你可以对 multiCall 中的每个合约调用链式追加受支持的方法,例如 callParams。这意味着批量调用并不牺牲单个调用的参数定制能力——转发资产、设置 Gas 参数等操作可以精确作用于数组中的某一项:

import { Provider, Wallet } from 'fuels';

import { EchoValuesFactory, ReturnContextFactory } from '../../../../typegend';

const provider = new Provider(LOCAL_NETWORK_URL);
const deployer = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);

const echoContractTx = await EchoValuesFactory.deploy(deployer);
const { contract: echoContract } = await echoContractTx.waitForResult();
const returnContextTx = await ReturnContextFactory.deploy(deployer);
const { contract: returnContextContract } =
  await returnContextTx.waitForResult();

const { waitForResult } = await echoContract
  .multiCall([
    echoContract.functions.echo_u8(10),
    returnContextContract.functions.return_context_amount().callParams({
      forward: [100, await provider.getBaseAssetId()],
    }),
  ])
  .call();

const { value: results } = await waitForResult();
// results[0] == 10
// results[1] == BN <100>

对应仓库中的 different-contracts-chain-methods.ts。示例中第二个调用通过 .callParams({ forward: [100, await provider.getBaseAssetId()] })return_context_amount 转发了 100 个基础资产,合约将收到的金额原样返回(results[1] == BN <100>)。这一模式适合在批量流程中为个别调用注入资产转账或费用参数,而不影响其他调用。

调用的排队与执行时机

官方文档给出了 multiCall 最关键的使用规则:

使用 multiCall 时,合约调用会被排队(queued),只有在你调用 .get.simulate.call 中的其中一个方法后才会真正执行。

也就是说,multiCall([...]) 本身只构建调用队列,不产生任何链上交互;真正的执行由三个终结方法触发,语义与 SDK 中其他调用作用域保持一致:

  • .call() —— 实际提交交易并等待确认,产生链上状态变更;
  • .simulate() —— 在本地模拟执行,验证交易是否可成功但不提交;
  • .get() —— 用于只读场景获取返回值(对写入型函数则等价于不落盘的读取)。

用 multiCall 批量执行只读合约调用

当 dApp 需要从多个合约读取数据时,multiCall 可以在单笔交易中批量执行多次 只读调用(对应文档中 methods.md#get 一节介绍的 .get() 只读调用模式),从而减少发送给网络的请求数量、合并数据获取过程,使 dApp 交互更高效

import { Provider, Wallet } from 'fuels';

import { CounterFactory, EchoValuesFactory } from '../../../../typegend';

const provider = new Provider(LOCAL_NETWORK_URL);
const deployer = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);

const counterContractTx = await CounterFactory.deploy(deployer);
const { contract: counterContract } = await counterContractTx.waitForResult();
const echoContractTx = await EchoValuesFactory.deploy(deployer);
const { contract: echoContract } = await echoContractTx.waitForResult();

const { waitForResult } = await echoContract
  .multiCall([
    counterContract.functions.get_count(),
    echoContract.functions.echo_u8(10),
    echoContract.functions.echo_str('Fuel'),
  ])
  .call();

const { value: results } = await waitForResult();
// results[0] == BN <0>
// results[1] == 10
// results[2] == 'Fuel'

对应仓库中的 different-contracts-readonly.ts。这一场景的价值在于:跨合约的多路查询被合并为一次网络往返与一次交易执行,结果按顺序返回(BN 计数值、u8 回显值、字符串回显值)。对于仪表盘、聚合数据页等需要并发读取多个合约状态的 dApp 页面,这是降低请求频率的推荐做法。

源码级实现:multiCall 与 MultiCallInvocationScope

从源码结构看,multiCall 的能力由 packages/program 包提供,其调用链非常清晰:

  1. 入口方法Contract.multiCall 接收一个 FunctionInvocationScope 数组,并直接构造并返回一个 MultiCallInvocationScope 实例:

    multiCall(calls: Array<FunctionInvocationScope>) {
      return new MultiCallInvocationScope(this, calls);
    }
    

    这印证了文档中「调用被排队」的说法——multiCall 本身不做任何提交动作,只是把传入的各函数作用域登记到批次中。

  2. 批次作用域MultiCallInvocationScope 继承自 BaseInvocationScope,构造函数中通过 this.addCalls(funcScopes) 一次性把整组调用挂入队列,并重写了 addCall / addCalls 以支持继续追加单个或成组的调用:

    constructor(contract: AbstractContract, funcScopes: Array<FunctionInvocationScope>) {
      super(contract, true);
      this.addCalls(funcScopes);
    }
    

    由于它继承自通用的 BaseInvocationScope,因此天然继承了 .get.simulate.call 等终结方法以及 callParams 等链式参数方法——这正是文档中「排队后由 .get / .simulate / .call 触发执行」以及「每个调用可单独链式附加 callParams」两条规则背后的实现机制。

  3. 测试佐证multiCall 的批量调用行为在 fuel-gauge 集成测试 中被大量使用(同一文件内还有 call-test-contract.test.tsreentrant-contract-calls.test.ts 等用例),覆盖了批量调用与返回值顺序、跨合约调用、日志解码等路径,可作为验证该 API 实际行为的参考。

小结

multiCall 是 fuels-ts 中「一笔交易、多次合约调用」的标准手段:

  • 调用同一合约的多个函数:contract.multiCall([fn1(), fn2(), ...])
  • 跨合约批量调用:在数组中混入不同合约实例的 functions 调用,SDK 自动在同一笔交易中依次完成;
  • 单个调用仍可链式附加 callParams 等参数定制;
  • 调用在 .get.simulate.call 触发前始终处于排队状态,三者分别对应只读获取、模拟执行与真实提交;
  • 批量只读查询可显著减少对网络的请求次数。

配合 methods.md 的常规调用说明与 inter-contract-calls.md 的合约间调用介绍,multiCall 构成了 fuels-ts 合约交互章节中提升交易效率的核心工具。

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