fuels-ts 多合约调用实战指南:用 multiCall 在单笔交易中批量调用合约函数
本文基于 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 包提供,其调用链非常清晰:
-
入口方法:Contract.multiCall 接收一个
FunctionInvocationScope数组,并直接构造并返回一个MultiCallInvocationScope实例:multiCall(calls: Array<FunctionInvocationScope>) { return new MultiCallInvocationScope(this, calls); }这印证了文档中「调用被排队」的说法——
multiCall本身不做任何提交动作,只是把传入的各函数作用域登记到批次中。 -
批次作用域: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」两条规则背后的实现机制。 -
测试佐证:
multiCall的批量调用行为在 fuel-gauge 集成测试 中被大量使用(同一文件内还有 call-test-contract.test.ts、reentrant-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 合约交互章节中提升交易效率的核心工具。
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