fuels-ts 合约余额查询实战:深入解析 Contract.getBalance 的用法与底层实现
在 Fuel 网络中,合约(Contract)本身可以持有资产,而余额的准确跟踪直接关系到资金流转的正确性。本文围绕 fuels-ts 官方指南中的「Contract Balance」章节展开,讲解 Contract.getBalance 方法的实际用法,并结合 SDK 源码剖析其从调用链到 GraphQL 查询的完整实现路径,帮助你掌握如何验证合约在转账、调用前后的资产结余。
为什么要关注合约余额
与钱包不同,合约是持久化的资产持有者:你可以向合约转发(forward)资产用于支付调用费用,也可以让合约把资产转给任意地址。在支付高成本操作(costly operations)时,必须清楚合约当前某项资产(asset)的可用余额,才能:
- 确认之前转发进合约的资产还剩多少,避免余额不足导致调用失败;
- 在合约向外转出资产后,精确验证「转入总额 − 转出额 = 剩余余额」这一不变量;
- 区分不同资产(基础资产或自定义代币)分别持有的数量。
官方指南给出的核心结论是:getBalance 返回的是该合约在某项资产上的当前总可用余额,与资产被转入多少次、被花去过多少次无关——它是一个状态查询,而非流水查询。
The getBalance Method 解析
Contract.getBalance 的签名定义在 packages/program/src/contract.ts 中:
/**
* Get the balance for a given asset ID for this contract.
*
* @param assetId - The specified asset ID.
* @returns The balance of the contract for the specified asset.
*/
getBalance(assetId: BytesLike) {
return this.provider.getContractBalance(this.id, assetId);
}
从源码结构看,这个方法本身非常轻量,它做了两件事:
- 接收一个
assetId参数(BytesLike,即 0x 开头的 32 字节资产 ID 十六进制字符串); - 将合约自身地址
this.id与资产 ID 一起委托给Provider.getContractBalance完成真正的查询。
其返回值是一个 Promise<BN>,即 packages/account/src/providers/provider.ts 中 getContractBalance 的最终形态:
async getContractBalance(
contractId: string | Address,
assetId: BytesLike
): Promise<BN> {
const { contractBalance } = await this.operations.getContractBalance({
contract: new Address(contractId).toB256(),
asset: hexlify(assetId),
});
return bn(contractBalance.amount, 10);
}
可以看到 SDK 底层是通过 GraphQL 操作(operations.getContractBalance,定义于 operations.graphql)向节点发起查询,节点返回该合约在指定资产上的 amount,SDK 将其包装为十进制 BN 实例返回。因此:
- 参数:
assetId支持任意BytesLike,既可用provider.getBaseAssetId()获取的链上基础资产 ID,也可以是已部署代币合约的资产 ID; - 返回值:
BN大整数,可直接调用.toNumber()、.toString()参与断言或展示; - 语义:查询的是该合约「此刻」持有的该资产总量,是余额快照。
示例合约:一个转出资产的 transfer 合约
指南以一个「把指定数量的某资产转给某地址」的合约作为示例,其 Sway 源码位于 apps/docs/sway/transfer-to-address/src/main.sw:
contract;
use std::asset::transfer;
abi TransferToAddress {
#[payable]
fn transfer(amount_to_transfer: u64, asset_id: AssetId, recipient: b256);
}
impl TransferToAddress for Contract {
#[payable]
fn transfer(amount_to_transfer: u64, asset_id: AssetId, recipient: b256) {
let recipient_address = Address::from(recipient);
transfer(
Identity::Address(recipient_address),
asset_id,
amount_to_transfer,
);
}
}
transfer 函数有三个参数:
amount_to_transfer:要转出的资产数量;asset_id:被转移的代币的资产 ID(即已部署代币合约的 AssetId);recipient:接收方钱包地址(b256,内部会转为 Sway 的Address类型)。
函数体调用的是 Sway 标准库内置的 std::asset::transfer,作用与其名字一致:把合约持有的指定资产按数量转移到目标地址。注意 #[payable] 注解——它允许外部调用向该函数转发资产,这正是「先给合约打入一笔大于所需转账额的资产」这一示例前提的由来:调用方需要额外提供资产供合约执行转出操作。
完整调用流程:转发资产、执行调用、查询余额
SDK 文档示例代码位于 apps/docs/src/guide/contracts/snippets/contract-balance.ts,完整流程如下:
import type { AssetId } from 'fuels';
import { Provider, Wallet } from 'fuels';
import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../env';
import { TransferToAddressFactory } from '../../../typegend';
const provider = new Provider(LOCAL_NETWORK_URL);
const wallet = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const { waitForResult: waitForDeploy } =
await TransferToAddressFactory.deploy(wallet);
const { contract } = await waitForDeploy();
const amountToForward = 40;
const amountToTransfer = 10;
const baseAssetId = await provider.getBaseAssetId();
const recipient = Wallet.generate({
provider,
});
const asset: AssetId = {
bits: baseAssetId,
};
const { waitForResult } = await contract.functions
.transfer(amountToTransfer, asset, recipient.address.toB256())
.callParams({
forward: [amountToForward, baseAssetId],
})
.call();
await waitForResult();
const contractBalance = await contract.getBalance(baseAssetId);
console.log(
'contract balance reduced by amountToTransfer',
contractBalance.toNumber() === amountToForward - amountToTransfer
);
逐步拆解:
- 部署合约:通过 typegen 生成的
TransferToAddressFactory.deploy(wallet)将示例合约部署到本地测试节点,并拿到返回的contract实例; - 设定金额:
amountToForward = 40(调用时转发给合约的资产总额),amountToTransfer = 10(合约内部转给收款地址的数量); - 准备资产 ID:用
provider.getBaseAssetId()获取当前链的基础资产 ID,包装为 Sway 端期望的{ bits: ... }结构; - 执行合约调用:
contract.functions.transfer(...)构造调用,callParams({ forward: [40, baseAssetId] })表示随本次调用向合约转发 40 个基础资产,.call()提交并拿到waitForResult; - 验证余额:交易确认后调用
contract.getBalance(baseAssetId),断言其恰好等于amountToForward - amountToTransfer,即40 - 10 = 30。
这个例子直观展示了 getBalance 的核心用途:在合约调用完成后,用它确认合约余额精确等于「转发总额减去已转出额」。
底层实现与测试佐证
为了更清楚地理解 getBalance 在生态中的位置,可以从源码与测试两侧印证其调用链:
调用链(从 SDK 到节点)
Contract.getBalance(assetId) → Provider.getContractBalance(contractId, assetId) → GraphQL 操作 getContractBalance(查询参数为合约地址 b256 与资产 ID 的十六进制字符串)→ 返回 BN。整个过程是一次只读的状态查询,不产生交易,也不消耗 Gas。
仓库内的 E2E 测试验证
packages/fuel-gauge/src/contract.test.ts 中的多个用例反复使用了「先查余额 → 转账 → 再查余额」的模式来验证余额的增减,例如:
const initialBalance = new BN(
await contract.getBalance(await provider.getBaseAssetId())
).toNumber();
const tx = await wallet.transferToContract(
contract.id,
amountToContract,
await provider.getBaseAssetId()
);
await tx.waitForResult();
const finalBalance = new BN(
await contract.getBalance(await provider.getBaseAssetId())
).toNumber();
expect(finalBalance).toBe(initialBalance + amountToContract.toNumber());
这些测试覆盖了基础资产、非基础资产(自定义代币 TestAssetId)以及大数值(2^61)场景,均断言「终余额 = 初始余额 + 转入额」。值得注意的是,这里余额的增加来自 Account.transferToContract(定义于 packages/account/src/account.ts),它通过组装一个转账脚本把钱包资产直接打入合约地址——与文档示例中通过 callParams.forward 随合约调用转发资产是两条不同的资产流入路径,但查询余额的方式完全一致:都是 contract.getBalance(assetId)。
小结与实战要点
Contract.getBalance(assetId)是 fuels-ts 中查询合约某项资产当前总余额的标准入口,返回Promise<BN>;- 它查询的是状态快照,与历史上转入/转出的次数无关,适合在每次交易
waitForResult()之后做余额断言; - 资产流入合约的常见路径有两条:钱包端
wallet.transferToContract(contractId, amount, assetId),以及合约调用时callParams({ forward: [amount, assetId] });无论哪条路径,后续余额验证都用同一方法; - 查询不同资产只需替换
assetId参数,基础资产可用provider.getBaseAssetId()动态获取,避免硬编码; - 从实现看(
packages/program/src/contract.ts→packages/account/src/providers/provider.ts),该方法是一次 GraphQL 只读查询,不产生交易,可在任意需要核对合约资产的位置安全调用。
沿着本文路径,你可以进一步阅读 Sway 示例合约、文档示例脚本 以及 contract.test.ts 中完整的余额相关 E2E 用例,复现「转发 40、转出 10、剩余 30」的完整验证流程。
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