fuels-ts 钱包资产转账与合约划转实战指南:transfer / createTransfer / batchTransfer 全解析
【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts
本文以 fuels-ts(Fuel Network TypeScript SDK)官方指南 wallet-transferring.md 为骨架,系统讲解如何在燃料网络上使用 SDK 的钱包能力完成资产划转:包括账户间单笔与批量转账、向合约划转资产、在提交交易前预取交易 ID,以及转账前的余额校验。读完本文,你将掌握transfer、createTransfer、batchTransfer、transferToContract、batchTransferToContracts的完整参数语义、底层实现与正确使用场景。
转账方法总览与选型
fuels-ts 的资产转移能力统一收敛在Account(Wallet是它的一个实现)之上。按照"转给谁、是否批量、是否需要预构建"可以快速定位合适的方法:
| 方法 | 目标 | 单笔/批量 | 返回类型 | 是否先返回交易请求 |
|---|---|---|---|---|
transfer | 钱包/账户地址 | 单笔 | TransactionResponse | 否(内部封装) |
createTransfer | 钱包/账户地址 | 单笔 | ScriptTransactionRequest | 是 |
batchTransfer | 多个钱包/账户地址 | 批量 | TransactionResponse | 否 |
transferToContract | 单个合约 | 单笔 | TransactionResponse | 否 |
batchTransferToContracts | 多个合约 | 批量 | TransactionResponse | 否 |
环境准备:Provider、钱包与基础资产 ID
在发起任何转账之前,需要先完成三件事:连接 Provider、实例化发送方/接收方钱包、取得链上的基础资产 ID(Base Asset ID,即用于支付 gas 的原生资产)。所有示例代码均可在 apps/docs/src/guide/wallets/snippets/wallet-transferring 目录下找到对应文件。
import { Provider, Wallet } from 'fuels'; import { LOCAL_NETWORK_URL, WALLET_PVT_KEY, WALLET_PVT_KEY_2, } from './env'; // 本地测试环境的 URL 与私钥 const provider = new Provider(LOCAL_NETWORK_URL); const baseAssetId = await provider.getBaseAssetId(); const sender = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); const destination = Wallet.fromPrivateKey(WALLET_PVT_KEY_2, provider);几点说明:
Wallet.fromPrivateKey通过私钥在指定provider上构造钱包实例,是文档与测试中最常用的实例化方式;也可参考 钱包实例化指南 使用助记词、HD 派生或 JSON 钱包等方式。provider.getBaseAssetId()返回当前网络的"基础资产 ID",作为transfer系列方法第三个可选参数的默认值。- 示例中的环境变量取自 env.ts,仅适用于本地/测试网络;实际接入主网时请替换为对应网络的 URL 与自己的密钥,切勿在源码中硬编码主网私钥。
账户间转账:transfer
transfer发起一个把资产从当前钱包转移到另一个账户的交易请求,共接收三个核心参数:
- 接收方钱包地址(
destination.address); - 要转移的资产数量(
amount); - 资产 ID(可选,缺省时使用基础资产 ID)。
调用后返回一个 resolve 为TransactionResponse的 Promise。要等待交易真正被节点打包上链,需要调用response.waitForResult()。完整示例(对应 between-accounts.ts):
const amountToTransfer = 500; const response = await sender.transfer( destination.address, // 接收方钱包地址 amountToTransfer, // 转账金额 baseAssetId // 资产 ID ); await response.waitForResult(); // 转账完成后可校验接收方余额 const balance = await destination.getBalance(baseAssetId);从源码看,transfer并不是独立实现,而是createTransfer与sendTransaction的组合:它先调用createTransfer构建并装配一个ScriptTransactionRequest,填充转账数据,再通过this.sendTransaction(request, { estimateTxDependencies: false })把交易提交给节点(见 packages/account/src/account.ts)。因此文档中才会说"transfer 内部创建了一个ScriptTransactionRequest、填充转账信息并提交交易"。
不同资产类型的默认处理
transfer的第三个参数assetId是可选的:在 createTransfer 实现 中,当未显式传入资产 ID 时会回退到provider.getBaseAssetId(),即默认转移基础资产。
提交前获取交易 ID:createTransfer
有些场景(如先展示交易哈希给用户确认、或与后端预签名流程配合)需要在真正提交给节点之前就拿到交易 ID。此时应改用createTransfer:
createTransfer同样会创建并填充一个ScriptTransactionRequest,但它不提交,而是把请求对象返回给你。之后你可以用request.getTransactionId(chainId)计算交易 ID(示例见 create-transfer.ts):
const amountToTransfer = 200; const baseAssetId = await provider.getBaseAssetId(); const transactionRequest = await sender.createTransfer( destination.address, amountToTransfer, baseAssetId ); const chainId = await provider.getChainId(); const transactionId = transactionRequest.getTransactionId(chainId); // 确认无误后再正式提交 const response = await sender.sendTransaction(transactionRequest); // 链上返回的交易 id 与上面预计算的 transactionId 一致 const { id } = await response.wait(); console.log('transactionId', id === transactionId); // true需要注意:createTransfer在返回前还会经过assembleTx等装配流程完成 coin 选择、gas price 与费用校验(源码见 account.ts),因此返回的请求已经处于"可发送"状态,直接交给sendTransaction即可。
修改请求会改变交易 ID
交易 ID 是对交易请求内容的哈希。任何对请求的修改——例如调整gasLimit、maxFee、输入输出等字段——都会导致哈希变化,产生一个全新的交易 ID。
注意:只有完成所有修改之后再计算交易 ID 才是安全的。若先取 ID、后改请求,先前拿到的 ID 将作废。
唯一的例外是"只增减 witnesses 数组内的见证人数量不会改变交易哈希"(见 create-transfer-2.ts 中的注释)。下面演示了这个陷阱:
const transactionRequest = await sender.createTransfer( destination.address, amountToTransfer, baseAssetId ); const chainId = await provider.getChainId(); const transactionId = transactionRequest.getTransactionId(chainId); // 修改请求中的 gasLimit 会生成新的交易哈希 / 交易 ID transactionRequest.gasLimit = bn(1000); const response = await sender.sendTransaction(transactionRequest); // 链上返回的交易 id 与上面预计算的 transactionId 不再相同 const { id } = await response.wait(); console.log('transactionId', id !== transactionId); // true批量转账到多个钱包:batchTransfer
当需要一次交易内把资产转到多个地址(可能是不同的地址、不同的资产)时,使用Account.batchTransfer。它接收一组TransferParams:
type TransferParams = { amount: BigNumberish; // 转账数量 assetId: BytesLike; // 资产 ID destination: string | Address; // 目标地址 };一个"多接收方 + 多资产"的示例(对应 transfers/batch-transfer.ts):
import type { TransferParams } from 'fuels'; import { Provider, Wallet } from 'fuels'; const provider = new Provider(LOCAL_NETWORK_URL); const baseAssetId = await provider.getBaseAssetId(); const sender = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); const recipient1 = Wallet.generate({ provider }); const recipient2 = Wallet.generate({ provider }); const someOtherAssetId = '0x0101010101010101010101010101010101010101010101010101010101010101'; const transfersToMake: TransferParams[] = [ { amount: 100, assetId: baseAssetId, destination: recipient1.address }, { amount: 200, assetId: baseAssetId, destination: recipient2.address }, { amount: 300, assetId: someOtherAssetId, destination: recipient2.address }, ]; const tx = await sender.batchTransfer(transfersToMake); await tx.waitForResult(); const { isStatusSuccess } = await tx.waitForResult(); console.log('Transaction should be successful', isStatusSuccess);从实现角度看,batchTransfer会新建一个ScriptTransactionRequest,通过addBatchTransfer把整组转账参数写入请求(account.ts)。Wallet.generate则用于创建只具备本地密钥对、尚未关联任何链上余额的接收方——若在真实网络上测试,请确保收款地址存在或可访问。
转账到合约:transferToContract
向已部署合约划转资产时使用transferToContract。它的参数结构与transfer高度相似,唯一差别在于第一个参数不再是钱包地址,而是由合约 ID 构造出的Address实例:
- 如果手里有该合约的
Contract实例,直接使用它的id属性即可(contract.id本身是Address); - 如果合约是通过
forc deploy部署的、或并非由你部署,通常只有一个十六进制字符串形式的合约 ID,此时需要用new Address('0x123...')将其包装成Address。
完整示例(对应 transferring-to-contracts.ts,其中CounterFactory是 typegen 生成的合约工厂):
import { Provider, Wallet } from 'fuels'; import { CounterFactory } from './typegend'; // typegen 生成的合约工厂 const provider = new Provider(LOCAL_NETWORK_URL); const sender = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); // 部署合约并取得其 Address const deploy = await CounterFactory.deploy(sender); const { contract } = await deploy.waitForResult(); const amountToTransfer = 400; const assetId = await provider.getBaseAssetId(); const contractId = contract.id; // Address 类型 const tx = await sender.transferToContract( contractId, // 目标:合约的 Address amountToTransfer, assetId ); await tx.waitForResult(); console.log('balance', (await contract.getBalance(assetId)).toNumber() !== 0);重要:
transferToContract只能用于向合约转账。向普通账户地址转账请使用transfer,二者不可混用。
从源码看,transferToContract本身只是batchTransferToContracts的特例——它把单个参数包装成数组后转发(account.ts)。这解释了为什么两者的行为与校验逻辑完全一致。
批量转账到多个合约:batchTransferToContracts
一次交易向多个合约(可为不同合约、不同资产)划转时,使用Account.batchTransferToContracts,参数为ContractTransferParams[]:
type ContractTransferParams = { contractId: string | Address; // 目标合约 ID / Address amount: BigNumberish; // 划转数量 assetId?: BytesLike; // 资产 ID(可选,缺省为基础资产) };示例(对应 transferring-to-multiple-contracts.ts):
import type { ContractTransferParams } from 'fuels'; import { Provider, Wallet } from 'fuels'; import { CounterFactory, EchoValuesFactory } from './typegend'; const provider = new Provider(LOCAL_NETWORK_URL); const sender = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); const baseAssetId = await provider.getBaseAssetId(); const assetA = /* 某个测试资产 ID */; const deploy1 = await CounterFactory.deploy(sender); const { contract: contract1 } = await deploy1.waitForResult(); const deploy2 = await EchoValuesFactory.deploy(sender); const { contract: contract2 } = await deploy2.waitForResult(); const contractTransferParams: ContractTransferParams[] = [ { contractId: contract1.id, amount: 999, assetId: baseAssetId }, { contractId: contract1.id, amount: 550, assetId: assetA }, { contractId: contract2.id, amount: 200, assetId: assetA }, ]; const transfer = await sender.batchTransferToContracts(contractTransferParams); await transfer.waitForResult();结合源码可以提炼出该方法的几个关键实现细节(account.ts):
- 默认资产:每一项未显式提供
assetId时,统一回退为链的基础资产 ID; - 入参校验:任何一组的
amount若小于等于 0,会直接抛出FuelError(校验发生在构建交易之前),因此不要传 0 或负值; - 合约地址包装:每个
contractId都会被new Address(...)统一标准化后再写入交易输入; - 交易仍通过
ScriptTransactionRequest构建,并在提交前完成 gas / 费用装配。
重要:
batchTransferToContracts只能用于合约间批量划转,不能在其中混入普通账户地址。需要对多个账户转账时,请使用上文的batchTransfer。
无论使用哪种方法,都请记得对返回的TransactionResponse调用waitForResult(),确保交易被成功打包上链后再继续后续逻辑。
转账前的余额校验与失败排查
在发起转账前,请务必确认钱包有足够资金。余额不足时,转账请求会以错误失败,错误信息为:
The transaction does not have enough funds to cover its execution.燃料网络上的每笔交易都需要消耗 gas(以基础资产支付),因此余额不足既可能指被转移资产本身不够,也可能指用于覆盖执行费用(gas + 网络费用)的基础资产不够。transfer底层由 assembleTx 与费用校验逻辑 在提交前完成资金核算,不满足时会直接报错。
查询余额的推荐方式是使用Account.getBalance(assetId),它会汇总当前钱包在该资产下所有未花费 coin 的总额(完整指南见 checking-balances.md):
import type { BN } from 'fuels'; import { Provider, Wallet } from 'fuels'; const provider = new Provider(LOCAL_NETWORK_URL); const myWallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider); // 返回值为 BigNumber(BN),不是普通 number const balance: BN = await myWallet.getBalance(await provider.getBaseAssetId());若想一次性查看钱包持有的全部资产,使用getBalances(),它返回CoinQuantity[]数组(示例见 checking-balances-two.ts):
const myOtherWallet = Wallet.fromPrivateKey(WALLET_PVT_KEY_2, provider); const { balances } = await myOtherWallet.getBalances(); console.log('balances:', balances);小结:五种方法的内在关系
回顾 fuels-ts 的源码实现,可以把本文涉及的方法串成一条清晰的调用链(全部位于 packages/account/src/account.ts):
transferToContract→batchTransferToContracts(单元素数组特例,L574-L584);transfer→createTransfer(构建ScriptTransactionRequest并装配 gas/费用)→sendTransaction(L488-L499);createTransfer/batchTransfer/batchTransferToContracts都基于ScriptTransactionRequest构建交易,经过 coin 汇总(默认自动整合零散 coin,可用skipAutoConsolidation跳过)与 gas/费用校验后提交节点。
因此,选择方法的实际准则非常简单:面向普通账户用transfer/batchTransfer,面向合约用transferToContract/batchTransferToContracts;需要在提交前预取交易 ID 时改用createTransfer,且务必在所有字段改动完成之后再计算交易 ID。
【免费下载链接】fuels-tsFuel Network Typescript SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考