news 2026/9/9 22:50:40

Fuel TypeScript SDK 交易组装实战:assembleTx 全参数解析与 Fuel UTXO 找零机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Fuel TypeScript SDK 交易组装实战:assembleTx 全参数解析与 Fuel UTXO 找零机制

Fuel TypeScript SDK 交易组装实战:assembleTx 全参数解析与 Fuel UTXO 找零机制

【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts

assembleTx是 Fuel TypeScript SDK(fuels-ts)中负责交易"预组装"的核心方法:它接收一份尚未完整的交易请求,自动补齐输入(inputs)、输出(outputs)与交易策略(policies),并完成 gas 价格估算与费用校验。SDK 中几乎所有高层 API(账户转账、合约部署、Blob 部署、合约调用)最终都经由它落地。读完本文,你将掌握assembleTx的完整参数语义、返回结构,理解 Fuel 基于 UTXO 的找零(change)机制对"谁收到找零"的决定性影响,并能在多账户、多资产场景下正确、安全地组装一笔可签名上链的交易。

assembleTx 能为你做什么

在 Fuel 中,交易不能只携带"想做什么"(例如向某个地址转 100 个 base asset),还必须明确说明资源从哪来(输入 UTXO)、找零给谁(change 输出)、费用由谁承担(fee payer)以及采用何种 gas 价格。手工拼装这些字段极易出错,而assembleTx正是把这一复杂过程收敛为一个方法调用。

根据官方指南(见 assemble-tx.md),它主要处理以下事项:

  • 不同资产所需的币数量(coin quantity)汇总;
  • 指定费用支付账户(fee payer);
  • gas 与费用估算;
  • predicate(谓词)的估算;
  • 资源排除(忽略特定资源 ID,避免把某笔 UTXO 或消息重复计入)。

其方法签名与入口位于 provider.ts:

async assembleTx<T extends TransactionRequest>( params: AssembleTxParams<T> ): Promise<AssembleTxResponse<T>>

注意,方法属于Provider实例,因此调用前需要一个已连接网络的Provider(本地节点可参考各示例中使用的LOCAL_NETWORK_URL)。

AssembleTxParams:参数全解

assembleTx的核心输入类型为AssembleTxParams<T>,其完整定义位于 provider.ts:

export type AssembleTxParams<T extends TransactionRequest = TransactionRequest> = { // The transaction request to assemble request: T; // Coin quantities required for the transaction, optional if transaction only needs funds for the fee accountCoinQuantities?: AccountCoinQuantity[]; // Account that will pay for the transaction fees feePayerAccount: Account; // Block horizon for gas price estimation (default: 10) blockHorizon?: number; // Whether to estimate predicates (default: true) estimatePredicates?: boolean; // Resources to be ignored when funding the transaction (optional) resourcesIdsToIgnore?: ResourcesIdsToIgnore; // Amount of gas to reserve (optional) reserveGas?: BigNumberish; };

各参数含义与默认值如下:

参数是否必填默认值说明
request必填待组装的交易请求,如ScriptTransactionRequestCreateTransactionRequest等。
feePayerAccount必填负责支付交易费用的账户。
accountCoinQuantities可选[]交易需要的各资产币数量数组。若交易只需要覆盖手续费(例如纯转账且资产由 fee payer 提供),可省略。
blockHorizon可选10gas 价格估算时向前预看的区块数量。
estimatePredicates可选true是否对 predicate 进行 gas 估算。
resourcesIdsToIgnore可选资助交易时需忽略的资源(UTXO 或 message)。
reserveGas可选额外预留的 gas 数量。

这些默认值并非只在文档层面约定,而是直接体现在实现里。在 provider.ts 中可以看到解构参数时显式写出的默认值:

const { request, reserveGas, resourcesIdsToIgnore, feePayerAccount, blockHorizon = 10, estimatePredicates = true, accountCoinQuantities = [], } = params;

accountCoinQuantities 的内部结构

每个AccountCoinQuantity条目包含以下字段:

字段必填默认行为说明
amount必填需要的币数量(费用部分无需手动计入,系统会自动叠加 base asset 手续费)。
assetId必填资产的 ID(可先通过provider.getBaseAssetId()取得 base asset)。
account可选默认为根级feePayerAccount提供该部分资源的账户。
changeOutputAccount可选默认为account(若account未提供则回退到feePayerAccount接收该assetId全部已花费资源找零的账户。

account / changeOutputAccount 的默认行为

accountchangeOutputAccount都允许省略,它们的"缺省回退链"在实现中对应如下代码(provider.ts):

const { amount, assetId, account = feePayerAccount, changeOutputAccount } = quantity; const changeAccountAddress = changeOutputAccount ? changeOutputAccount.address.toB256() : account.address.toB256();

即:

  1. 未指定account时,直接回退到根级feePayerAccount
  2. 未指定changeOutputAccount时,回退到本条目的account(若account也缺省,则最终落到feePayerAccount)。

官方 default-behaviors.ts 示例逐一演示了这三种写法:

const accountCoinQuantities: AccountCoinQuantity[] = [ { amount: 100, assetId: baseAssetId, // account 未指定 => 默认取 feePayerAccount // changeOutputAccount 未指定 => 默认取 feePayerAccount }, { amount: 200, assetId: TestAssetId.A.value, account: accountA, // changeOutputAccount 未指定 => 默认取 accountA }, { amount: 300, assetId: TestAssetId.B.value, account: accountB, changeOutputAccount: accountC, // account 与 changeOutputAccount 均显式指定 }, ];

实现中的一个隐含细节:fee payer 自动补位

从源码看,provider.ts 还处理了一种边界情况:如果feePayerAccount没有出现在任何accountCoinQuantities条目中,assembleTx会自动把它作为"金额为 0 的 base asset 需求量"追加进 required balances 末尾,其 change 输出指向baseAssetChange(即已显式声明的 base asset change 地址)或 fee payer 自身地址。这意味着费用虽然由 fee payer 承担,但找零归属会被统一收敛到你声明的 change 策略上——这是理解后续"谁收到找零"问题的关键实现基础。

返回值 AssembleTxResponse

方法返回AssembleTxResponse<T>,定义同样在 provider.ts:

export type AssembleTxResponse<T extends TransactionRequest = TransactionRequest> = { assembledRequest: T; // 已完整组装、带齐 inputs/outputs/policies 的交易请求 gasPrice: BN; // 估算出的 gas 价格 receipts: TransactionResultReceipt[]; // 解析后的 dry run receipts rawReceipts: TransactionReceiptJson[]; // 未解析的原始 receipts };

各字段说明:

  • assembledRequest:组装完成、可直接签名提交的交易请求;
  • gasPrice:按blockHorizon估算的 gas 价格;
  • receipts:dry run 返回、已解析的 receipts;
  • rawReceipts:dry run 返回的未解析 receipts。

需要特别说明的是:assembleTx在返回前会对交易做一次 dry run 以验证其可行性。若 dry run 失败,实现会读取status.type === 'DryRunFailureStatus'的分支并通过extractDryRunError抛出解析后的错误,而不会静默返回一笔注定失败的交易(provider.ts)。

基本用法:组装一笔可签名的转账

basic-usage.ts 给出了完整的入门示例。核心流程如下:

const provider = new Provider(LOCAL_NETWORK_URL); const baseAssetId = await provider.getBaseAssetId(); const accountA = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); const accountB = Wallet.fromPrivateKey(WALLET_PVT_KEY_2, provider); const transferAmount = 100; // 1. 先声明"想做什么":向 accountB 输出 100 个 base asset const request = new ScriptTransactionRequest(); request.addCoinOutput(accountB.address, transferAmount, baseAssetId); const accountCoinQuantities: AccountCoinQuantity[] = [ { amount: transferAmount, assetId: baseAssetId, account: accountA, changeOutputAccount: accountA, // 可选 }, ]; // 2. 组装:补齐 inputs / outputs / policies const { assembledRequest, gasPrice, receipts } = await provider.assembleTx({ request, accountCoinQuantities, feePayerAccount: accountA, blockHorizon: 10, estimatePredicates: true, }); // 3. 组装完成后即可签名并提交 const submit = await accountA.sendTransaction(assembledRequest); await submit.waitForResult();

使用要点:

  • request只描述业务意图(这里是为accountB增加一笔 coin 输出),不要手工去填输入与 change,那是assembleTx的职责;
  • assembleTx会在需要时自动补入 base asset 手续费所需资源,因此accountCoinQuantities中的amount无需包含费用;
  • 返回的assembledRequest待签名状态的请求,随后用发送方(这里是accountA)的sendTransaction提交并waitForResult等待上链。

为什么必须关注 change:Fuel 的 UTXO 找零模型

accountchangeOutputAccount看似只是两个可选字段,但官方文档用大量篇幅专门提醒开发者注意它们——原因在于 Fuel 的UTXO 记账模型与以太坊的账户模型存在本质差异。

整笔消费:UTXO 模型下没有"部分花费"

在 Fuel 中,一笔交易只要把某个 UTXO 列入输入,就会整体消费该 UTXO,即使业务上只需要其中一小部分。官方文档给出过一个直观例子:

假设你有一个价值 10 ETH 的 UTXO。当你创建一笔只想转出 1 Gwei 的交易时,整个 10 ETH 的 UTXO 仍会被消耗。交易随后会:

  • 为接收方创建一个 1 Gwei 的 UTXO;
  • 为剩余部分(10 ETH - 1 Gwei - 费用)再创建一个 UTXO,并发送到OutputChange中指定的地址。

这里的"剩余部分"就是找零(change),而OutputChange决定了找零落到谁手里。可以说,OutputChange保证了"你的钱还能回到你手中"。

Fuel 约束:每个 assetId 每笔交易最多一个 change 输出

Fuel 有一条关键规则:一笔交易内,每个assetId只允许存在一个OutputChange。这意味着,如果一笔交易同时花销了多个账户对同一资产(比如都是 ETH)的 UTXO,那么只有一个人能收到这笔资产的全部找零——也就是OutputChange上写明的那位。若处理不当,就会出现"一个账户出了钱、另一个账户拿了找零"的意外结果。

多账户场景下的找零归属:multiple-output-change 示例

把上面两条规则叠加到assembleTxaccountCoinQuantities[].changeOutputAccount就是你在 SDK 层面显式指定"这个 assetId 的找零归谁"的开关。

官方 multiple-output-change.ts 演示了这一典型场景:

let { assembledRequest } = await provider.assembleTx({ request, feePayerAccount: accountA, accountCoinQuantities: [ { amount: transferAmount, assetId: baseAssetId, account: accountB, /** * accountB 将收到找零。虽然这里显式声明, * 但即使不写,它也会默认回退到 account 属性(此处同样是 accountB)。 */ changeOutputAccount: accountB, }, ], });

在这个例子中:

  • accountB的资源被显式请求进accountCoinQuantities(作为本次转账金额的提供方);
  • accountAfeePayerAccount,因此它也会投入资源来覆盖手续费;
  • changeOutputAccount被显式设为accountB

最终效果是:无论交易中消费了谁的 UTXO(包括accountA为了付手续费投进去的 UTXO),这笔资产的全部找零都会发给accountB

官方文档给出了一个极具警示性的推演:假设accountA投入了一枚 10 ETH 的 UTXO,由于 UTXO 必须整笔消费,交易会花掉完整的 10 ETH,而剩余找零会流向accountB而非accountA——这正是"只配置了 fee payer,却没仔细想 change 归属"可能踩中的坑。因此代码后续还须由实际出资账户完成见证人签名:

assembledRequest = await accountB.populateTransactionWitnessesSignature(assembledRequest); const submit = await accountA.sendTransaction(assembledRequest); await submit.waitForResult();

底层原理:assembleTx 在 provider 内部做了什么

结合仓库源码可以看清assembleTx的完整数据流(provider.ts):

  1. 归一化余额请求:遍历accountCoinQuantities,对每条生成{ account, amount, assetId, changePolicy };当assetId是 base asset 时,额外记录该条目的 change 地址作为baseAssetChange
  2. fee payer 自动补位:若 fee payer 未出现在任何条目中,以金额0追加一条 base asset 余额请求,保证费用有账户兜底;
  3. 资源排除处理:调用adjustResourcesToIgnoreForAddresses,基于本次交易涉及的地址集合裁剪resourcesIdsToIgnore——配合 SDK 内部的资源缓存,避免把某地址已占用的资源重复列入输入(同时这也是传入该参数的意义所在);
  4. 调用 GraphQL 组装端点:将request.toTransactionBytes()blockHorizonfeeAddressIndex(fee payer 在余额请求数组中的下标)、requiredBalancesestimatePredicatesexcludeInputreserveGas一并提交给 Fuel Core 的operations.assembleTx,由其执行估算与 dry run;
  5. 回填并校验:把节点返回的 witnesses / inputs / outputs 反序列化后写回request;若 dry run 状态为失败则解析 receipts 并抛错。

账户解析支持 predicate

accountCoinQuantitiesfeePayerAccount的账户最终会交给 assemble-tx-helpers.ts 中的resolveAccountForAssembleTxParams序列化:

  • 普通账户序列化为{ address }
  • predicate 账户(实现上通过'bytes' in account判定)则序列化为{ predicate, predicateAddress, predicateData },将谓词字节码、地址与数据一并上报节点用于估算。

这解释了为什么assembleTx需要estimatePredicates参数:当交易涉及 predicate 资源时,gas 估算必须把 predicate 执行的开销计算在内。

与已废弃旧 API 的关系

assembleTxgetTransactionCost+fund、以及estimateAndFund等旧流程的替代方案,后者已标记为 deprecated,并将在未来版本移除。如果你正在迁移旧代码,可参考仓库中的官方迁移指南 assemble-tx-migration-guide.md。相比旧 API,新方法的优势可概括为三点:对"谁付手续费、谁出资源"的控制更显式可精确指定每个账户、每种资产各自提供多少数量在声明每个账户的 coin quantity 时就能直接控制该 assetId 的找零归属

测试验证

仓库的燃料 gauge 集成测试 assemble-tx.test.ts 覆盖了assembleTx在真实 Fuel 节点上的行为,包含多账户、多资产与找零归属等场景,可作为理解本文内容后进一步对照源码学习的入口。

Best Practices:官方建议清单

  1. 始终提供余额充足的feePayerAccount:手续费不足会让 dry run 失败并在组装阶段即抛错;
  2. 多账户共享同一 assetId 时,务必谨慎对待 change 输出
    • Fuel 每个 assetId 每笔交易只允许一个 change 输出;
    • 若同一 assetId 的资源来自多个账户,则只有一个账户能收到全部已花费资源的找零;
    • 请与交易涉及的各方事先协调,明确由谁接收该 assetId 的找零,再通过changeOutputAccount显式声明。

Notes:行为特性小结

  • assembleTx会自动处理 base asset 的手续费需求,accountCoinQuantitiesamount无需叠加费用;
  • 返回前会对交易执行 dry run 校验,失败即抛错,避免把无效交易放行到签名环节;
  • 组装后的交易会依据accountCoinQuantities带齐全部必要的 inputs 与 outputs;
  • gas 与费用估算基于指定的blockHorizon完成;
  • 若要彻底掌控找零去向,最稳妥的做法是:永远显式写出accountchangeOutputAccount,不依赖默认回退逻辑。

【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:48:45

湖南单招职业技能测试题型

湖南高职单招采用 “文化素质 职业技能” 的考试模式&#xff0c;综合成绩总分 600 分&#xff0c;文化素质测试与职业技能测试各占 300 分。A 类应届普高生不需要参加院校组织的文化笔试&#xff0c;文化成绩直接使用学考语数外折算&#xff0c;职业适应性测试&#xff08;属…

作者头像 李华
网站建设 2026/9/9 22:48:14

深入解析SmmBackdoor:UEFI系统管理模式中的后门攻防实录

简介&#xff1a;面向UEFI固件安全研究者、系统底层开发者和安全爱好者&#xff0c;围绕SmmBackdoor这一利用系统管理模式&#xff08;SMM&#xff09;植入后门的高级恶意技术&#xff0c;提供从原理理解到代码复现的关键材料&#xff0c;帮助解决对SMM后门实现与防御认知不足的…

作者头像 李华
网站建设 2026/9/9 22:47:12

MySQL内置函数实战指南:从字符串处理到数据分析的SQL效率提升

写这篇MySQL内置函数的分享&#xff0c;起因是上周帮一个学弟排查一个数据统计的问题&#xff1a;他写了一大段业务代码&#xff0c;从数据库里取出关联数据再在Java里循环做字符串拼接和日期格式化&#xff0c;代码又长又慢&#xff0c;优化之后换成数据库函数一条SQL就搞定了…

作者头像 李华
网站建设 2026/9/9 22:45:45

从零用Python和Pygame写俄罗斯方块:核心逻辑与实战

简介&#xff1a;这是一份基于Python语言实现的俄罗斯方块小游戏源码包&#xff0c;主要面向Python初学者、游戏开发爱好者以及需要课程设计的同学。压缩包内共包含4个文件&#xff0c;以1个核心Python脚本为主&#xff0c;另附2个wav格式游戏音效和1个mp3背景音乐&#xff0c;…

作者头像 李华
网站建设 2026/9/9 22:45:33

隐私政策页面URL设计与故障排查完整指南

很多人做网站&#xff0c;功能页和落地页都打磨得不错&#xff0c;唯独隐私政策页面是被敷衍过去的重灾区。我这两年接手过几个因为隐私政策网站URL翻车的项目&#xff0c;有的是链接写成了动态参数&#xff0c;一换登录态就失效&#xff1b;有的是被WebView拦住死活打不开&…

作者头像 李华
网站建设 2026/9/9 22:43:59

生成的DLL多了个d?揭秘调试版与发布版的命名机制与处理方案

做 Windows 开发的朋友&#xff0c;肯定都遇到过这种让人摸不着头脑的情况&#xff1a;明明项目名是 MyLibrary &#xff0c;编译完一看输出目录&#xff0c;躺着的是 MyLibraryd.dll 。你要是不留心直接拿去用&#xff0c;要么是程序启动就报找不到 DLL&#xff0c;要么是…

作者头像 李华