fuels-ts 实战:基于合约调用构建自定义交易(getTransactionRequest、addCoinOutput 与 assembleTx 全链路解析)
本文围绕 fuels-ts 文档中 Custom Transactions From Contract Calls 这篇 cookbook 展开,完整演示如何通过合约调用(Contract Call)构建并定制一笔自定义交易:既更新链上状态(Counter 合约的 increment_count),又在同一笔交易里向指定收款地址转移基础资产。读完本文,你将掌握 fuels-ts 中「invocation scope 生成交易请求 → 手工追加 coin output → assembleTx 组装 → 发送并解析函数返回值」这条完整链路,并理解背后 ScriptTransactionRequest、addCoinOutput 等关键 API 的源码实现。
背景:从 Script 定制交易走向合约定制交易
在 Fuel 网络上,一笔交易的最小执行单元是 script 或 contract。fuels-ts 的 cookbook 中有一篇姊妹篇 Custom Transactions,演示了如何直接实例化 ScriptTransactionRequest,为单笔交易追加多种 program 类型与资产,从而构建复杂交易。
而合约调用场景比 script 更进一步:你可以直接调用合约中已编译好的函数、读取并修改链上状态。原始文档给出的核心思路是——先通过合约实例创建一个 invocation scope(调用作用域),再用 scope.getTransactionRequest() 得到一笔预填充好的 ScriptTransactionRequest,之后就可以在这笔请求上自由定制(例如追加给第三方的币输出),最后组装并提交。原文档完整示例位于 custom-contract-calls.ts。
核心要素总览
整个流程涉及五个关键步骤,每一步都对应一个明确的 API:
| 步骤 | API | 作用 |
|---|---|---|
| 1. 部署并连接合约 | CounterFactory.deploy(wallet) + new Contract(...) |
部署 Counter 合约 并建立合约实例 |
| 2. 创建调用作用域 | contractInstance.functions.increment_count(amount) |
得到 invocation scope,预置函数调用参数 |
| 3. 生成交易请求 | scope.getTransactionRequest() |
得到 ScriptTransactionRequest |
| 4. 定制交易 | request.addCoinOutput(to, amount, assetId) |
向收款地址追加一笔 coin 输出 |
| 5. 组装、发送、解析 | provider.assembleTx({...}) → wallet.sendTransaction → buildFunctionResult |
补全费用与找零、提交交易、反序列化函数返回值 |
完整代码示例与逐步解析
以下是 cookbook 完整示例(文件中使用顶层 await,依赖文档工程通过 fuels typegen 生成的 typegend 类型):
import { bn, buildFunctionResult, Contract, Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../env';
import { CounterFactory } from '../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const baseAssetId = await provider.getBaseAssetId();
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const deploy = await CounterFactory.deploy(wallet);
const { contract } = await deploy.waitForResult();
const receiverWallet = Wallet.generate({ provider });
const amountToRecipient = bn(10_000); // 0x2710
// Connect to the contract
const contractInstance = new Contract(contract.id, contract.interface, wallet);
// Create an invocation scope for the contract function you'd like to call in the transaction
const scope = contractInstance.functions.increment_count(amountToRecipient);
// Build a transaction request from the invocation scope
const request = await scope.getTransactionRequest();
// Add coin output for the recipient
request.addCoinOutput(receiverWallet.address, amountToRecipient, baseAssetId);
const { assembledRequest } = await provider.assembleTx({
request,
feePayerAccount: wallet,
accountCoinQuantities: [
{
amount: amountToRecipient,
assetId: baseAssetId,
account: wallet,
changeOutputAccount: wallet,
},
],
});
// Submit the transaction
const response = await wallet.sendTransaction(assembledRequest);
await response.waitForResult();
// Get result of contract call
const { value } = await buildFunctionResult({
funcScope: scope,
isMultiCall: false,
program: contract,
transactionResponse: response,
});
console.log('value', value);
// <BN: 0x2710>
const receiverBalance = await receiverWallet.getBalance(baseAssetId);
console.log('balance', receiverBalance.toNumber());
1. 连接网络、部署合约、准备两个账户
new Provider(LOCAL_NETWORK_URL)连接本地网络,provider.getBaseAssetId()获取链上基础资产 ID(用于费用与转账);Wallet.fromPrivateKey从私钥恢复签名钱包,作为部署者、调用者、付费方(fee payer)与找零接收方;CounterFactory.deploy(wallet)是 typegen 生成的合约工厂,部署完成后通过deploy.waitForResult()拿到contract(含contract.id与contract.interface);receiverWallet是一个全新的随机钱包,用来验证「合约调用 + 向第三方转账」能在同一笔交易中同时完成。
2. 创建 invocation scope 并生成 ScriptTransactionRequest
const contractInstance = new Contract(contract.id, contract.interface, wallet);
const scope = contractInstance.functions.increment_count(amountToRecipient);
const request = await scope.getTransactionRequest();
scope 是一次 increment_count 函数调用的 invocation scope,此时尚未发送任何交易。getTransactionRequest() 会执行 prepareTransaction() 并返回预填充好的 ScriptTransactionRequest——此时请求中已经包含合约输入/输出、witness、gas 估算等,只是资产层面还缺「转给收款人」这一笔。从源码看,getTransactionRequest 的实现非常轻量,见 base-invocation-scope.ts:
async getTransactionRequest(): Promise<ScriptTransactionRequest> {
await this.prepareTransaction();
return this.transactionRequest;
}
同一个类还提供了 fromRequest(request),用于在外部定制完请求后把它重新挂回 scope(例如后续要再次调用 call 或复用该 scope)。
3. 定制交易:向收款人追加 coin 输出
request.addCoinOutput(receiverWallet.address, amountToRecipient, baseAssetId);
addCoinOutput 是 TransactionRequest 基类上的方法,其实现见 transaction-request.ts:
addCoinOutput(to: AddressLike, amount: BigNumberish, assetId: BytesLike) {
this.pushOutput({
type: OutputType.Coin,
to: addressify(to).toB256(),
amount,
assetId,
});
return this;
}
可以看到它本质上就是把一条 OutputType.Coin 的输出推入请求的 outputs 列表:目标地址被 addressify 规范化为 B256 格式,金额与资产 ID 原样写入。这一步正是「自定义交易」的精髓——合约调用本身只更新状态,转账给第三方完全是在请求层面手工追加的。基类还有 addCoinOutputs 批量版本可用于一次追加多笔转账。
4. 使用 assembleTx 组装交易
const { assembledRequest } = await provider.assembleTx({
request,
feePayerAccount: wallet,
accountCoinQuantities: [
{
amount: amountToRecipient,
assetId: baseAssetId,
account: wallet,
changeOutputAccount: wallet,
},
],
});
assembleTx 是 fuels-ts 当前推荐的交易组装入口,负责根据请求消耗自动选取输入 coins、补齐费用(policy)与找零输出,返回可直接发送的 assembledRequest。参数说明:
request:上一步定制完的ScriptTransactionRequest;feePayerAccount:交易费用的支付账户,本例为部署钱包;accountCoinQuantities:声明该账户需要提供的资产数量(这里 10_000 基础资产用于覆盖转账额与手续费),changeOutputAccount指定找零回到谁手中。
值得注意:ScriptTransactionRequest 上早期的 estimateAndFund 方法已被标记为 deprecated,源码注释明确指向使用 provider.assembleTx 代替(见 script-transaction-request.ts 中对该方法的弃用标注),新代码应统一走 assembleTx。
5. 发送交易并解析合约返回值
const response = await wallet.sendTransaction(assembledRequest);
await response.waitForResult();
const { value } = await buildFunctionResult({
funcScope: scope,
isMultiCall: false,
program: contract,
transactionResponse: response,
});
console.log('value', value); // <BN: 0x2710>
const receiverBalance = await receiverWallet.getBalance(baseAssetId);
console.log('balance', receiverBalance.toNumber());
- 用
wallet.sendTransaction提交组装后的请求并等待最终结果; buildFunctionResult把交易响应中的回执数据按 scope 的 ABI 反序列化,取回increment_count的返回值value(示例输出<BN: 0x2710>,即十进制 10000,与传入的amountToRecipient一致,证明合约函数按参数更新了计数);- 最后一行查询
receiverWallet的余额,验证自定义追加的 coin output 确实把资产转入了收款地址——这两处断言共同覆盖了「状态更新」与「资产转移」两个目标。
ScriptTransactionRequest 的结构解析
从源码结构看,ScriptTransactionRequest 继承自 TransactionRequest 基类(定义于 script-transaction-request.ts),在基类的 inputs / outputs / witnesses / policies 之上额外携带三个 script 专属字段:
type = TransactionType.Script:标记交易类型;gasLimit:交易 gas 上限;script/scriptData:要执行的 script 字节码与编码后的参数(构造函数中缺省回退到内置的returnZeroScript)。
对于合约调用,invocation scope 会把「调用合约」所需的最小 script 写入这两个字段,并在 inputs/outputs 中填入 InputType.Contract 等对应的合约项。理解这一点后就能明白:合约调用交易本质上仍然是一笔 Script 交易,合约只是作为输入被挂载进来,因此「在请求上追加任意 coin output」天然合法且不受合约函数签名限制。
注意事项与适用前提
- 环境与依赖:示例代码依赖文档工程的
env.ts提供LOCAL_NETWORK_URL、WALLET_PVT_KEY,并需要本地 Fuel 节点可用;CounterFactory来自fuels typegen生成的typegend目录,Counter 合约源文件 与 Sway 工程配置见 Forc.toml。 - 组装入口:请以
provider.assembleTx作为资金准备与组装的标准入口;旧的estimateAndFund已弃用。 - 找零与费用:
accountCoinQuantities中的数量应预留手续费空间,changeOutputAccount决定余额回退账户;本例中付费方与找零方都是wallet。 - 适用边界:该模式适用于「合约调用 + 额外资产操作」的组合交易;如果交易不涉及合约,可参考姊妹篇 Custom Transactions 直接基于 script 构建。
小结
这篇 cookbook 演示了 fuels-ts 中合约级自定义交易的标准姿势:invocation scope 通过 getTransactionRequest() 把合约调用「物化」为一笔可修改的 ScriptTransactionRequest,开发者随后可用 addCoinOutput 等基类 API 在输出层面任意定制,再用 provider.assembleTx 完成资源选取、费用与找零的自动化组装,最后经 buildFunctionResult 解析出结构化返回值。配合 transaction-request.ts、script-transaction-request.ts 与 base-invocation-scope.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