首页
/ fuels-ts 实战:基于合约调用构建自定义交易(getTransactionRequest、addCoinOutput 与 assembleTx 全链路解析)

fuels-ts 实战:基于合约调用构建自定义交易(getTransactionRequest、addCoinOutput 与 assembleTx 全链路解析)

2026-09-05 21:22:55作者:伍霜盼Ellen

本文围绕 fuels-ts 文档中 Custom Transactions From Contract Calls 这篇 cookbook 展开,完整演示如何通过合约调用(Contract Call)构建并定制一笔自定义交易:既更新链上状态(Counter 合约的 increment_count),又在同一笔交易里向指定收款地址转移基础资产。读完本文,你将掌握 fuels-ts 中「invocation scope 生成交易请求 → 手工追加 coin output → assembleTx 组装 → 发送并解析函数返回值」这条完整链路,并理解背后 ScriptTransactionRequestaddCoinOutput 等关键 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.sendTransactionbuildFunctionResult 补全费用与找零、提交交易、反序列化函数返回值

完整代码示例与逐步解析

以下是 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.idcontract.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);

addCoinOutputTransactionRequest 基类上的方法,其实现见 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_URLWALLET_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.tsscript-transaction-request.tsbase-invocation-scope.ts 的源码,可以完整理解该链路的每一步落在哪些字段与调用上。

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