首页
/ fuels-ts 合约余额查询实战:深入解析 Contract.getBalance 的用法与底层实现

fuels-ts 合约余额查询实战:深入解析 Contract.getBalance 的用法与底层实现

2026-09-05 11:33:30作者:舒璇辛Bertina

在 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);
}

从源码结构看,这个方法本身非常轻量,它做了两件事:

  1. 接收一个 assetId 参数(BytesLike,即 0x 开头的 32 字节资产 ID 十六进制字符串);
  2. 将合约自身地址 this.id 与资产 ID 一起委托给 Provider.getContractBalance 完成真正的查询。

其返回值是一个 Promise<BN>,即 packages/account/src/providers/provider.tsgetContractBalance 的最终形态:

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 函数有三个参数:

  1. amount_to_transfer:要转出的资产数量;
  2. asset_id:被转移的代币的资产 ID(即已部署代币合约的 AssetId);
  3. 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
);

逐步拆解:

  1. 部署合约:通过 typegen 生成的 TransferToAddressFactory.deploy(wallet) 将示例合约部署到本地测试节点,并拿到返回的 contract 实例;
  2. 设定金额amountToForward = 40(调用时转发给合约的资产总额),amountToTransfer = 10(合约内部转给收款地址的数量);
  3. 准备资产 ID:用 provider.getBaseAssetId() 获取当前链的基础资产 ID,包装为 Sway 端期望的 { bits: ... } 结构;
  4. 执行合约调用contract.functions.transfer(...) 构造调用,callParams({ forward: [40, baseAssetId] }) 表示随本次调用向合约转发 40 个基础资产,.call() 提交并拿到 waitForResult
  5. 验证余额:交易确认后调用 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.tspackages/account/src/providers/provider.ts),该方法是一次 GraphQL 只读查询,不产生交易,可在任意需要核对合约资产的位置安全调用。

沿着本文路径,你可以进一步阅读 Sway 示例合约文档示例脚本 以及 contract.test.ts 中完整的余额相关 E2E 用例,复现「转发 40、转出 10、剩余 30」的完整验证流程。

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