Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
网络与协议指南阅读约 12 分钟

Solana getTokenAccountBalance:amount 与 uiAmount 及小数位

解析 Solana getTokenAccountBalance 返回的四个字段,并了解为什么 uiAmountString 是获取精确代币余额的安全字段。

TL;DR

Solana getTokenAccountBalance 返回同一个 SPL 代币余额的四种视图:amount(以基础单位表示的原始整数字符串)、decimals(mint 的小数位数)、uiAmount(按 decimals 缩放后的 JSON 数字)以及 uiAmountString(同一缩放值,但以精确十进制字符串表示)。人类可读余额等于 amount 除以 10^decimals。由于 JSON 数字是浮点数,对于大额或高小数位代币,uiAmount 可能丢失精度,因此涉及资金时应信任 uiAmountString。本指南解释代币账户模型,展示可运行的 Node.js 示例,用于推导关联代币账户并重新计算人类可读金额,并提供结果表,以便对照您自己的 RPC 端点验证行为。

getTokenAccountBalance 背后的 SPL 代币账户模型

在 Solana 上,SPL 代币余额并不存放在钱包中,而是存放在代币账户中:一个独立的链上账户,持有以基础单位表示的原始整数金额,并引用定义该代币的 mint。mint 是小数位数的权威来源,即构成一个完整代币的基础单位数量。因此,人类可读余额为 amount / 10^decimals,这个除法必须由您自己执行,或让 RPC 代为执行。Solana 代币文档描述了这种 mint-小数位-基础单位的关系,以及将所有者与 mint 关联到确定性代币账户地址的关联代币账户(ATA)推导方式。

这种分离很重要,因为 getTokenAccountBalance 接收的是单个代币账户地址,而不是钱包地址。钱包地址是持有 lamports 的系统账户;代币账户是持有 SPL 代币金额的另一种账户类型。将钱包地址传给 getTokenAccountBalance 是常见错误,会导致报错或返回意外账户。要读取钱包针对特定 mint 的余额,您需要先从所有者和 mint 推导出 ATA,然后查询该代币账户。关于账户类型和租金的更广泛讨论,请参阅读取 Solana 账户和代币余额。

  • 代币账户:持有以基础单位表示的原始整数金额,并引用 mint。
  • Mint:定义小数位数,即每个完整代币对应的基础单位数量。
  • 人类可读余额:amount / 10^decimals。
  • ATA:由所有者 + mint 推导出的确定性代币账户。

getTokenAccountBalance 响应约定

Solana getTokenAccountBalance 参考说明该方法接收单个代币账户地址,并返回包含 amount、decimals、uiAmount 和 uiAmountString 的 value 对象,外层是标准 JSON-RPC 2.0 响应信封,并带有 context slot。JSON-RPC 2.0 规范定义了该信封:jsonrpc 版本、id,以及 result 或 error。context slot 告诉您节点使用哪个账本 slot 来回答,这是判断数据新鲜度的依据。

这四个字段是同一个余额的四种视图。amount 是原始整数字符串,以基础单位表示。decimals 是 mint 的小数位数。uiAmount 是等于 amount 按 decimals 缩放后的 JSON 数字。uiAmountString 是同一缩放值,但以十进制字符串表示。RPC 将 amount 作为字符串返回,因为原始代币金额可能超出许多语言的安全整数范围;出于同样原因,它也返回 uiAmountString。该方法只读取一个代币账户,因此不是投资组合查询。

  • amount:原始整数字符串,基础单位。
  • decimals:mint 小数位数。
  • uiAmount:JSON 数字,amount 按 decimals 缩放。
  • uiAmountString:精确十进制字符串,amount 按 decimals 缩放。
  • context.slot:用于回答的账本 slot。

为什么涉及资金时应信任 uiAmountString

在大多数运行时(包括 JavaScript)中,JSON 数字是浮点数。像 1234567.89 这样的值并不总能精确表示,而且问题会随着金额增大或代币小数位增多而加剧。uiAmount 是 JSON 数字,因此在传输或解析过程中可能丢失精度。uiAmountString 是十进制字符串,因此能保留节点生成的每一位数字。对于会计、对账或任何必须精确的比较,请将 uiAmountString 解析为十进制字符串或大数类型,而不是信任 uiAmount。

这不是 Solana 特有的怪癖,而是 JSON 数字的属性。安全模式是将 amount 和 uiAmountString 视为权威字段,将 uiAmount 仅视为显示便利。如果必须使用 uiAmount,请有意进行舍入,并且绝不要将其用作键或相等性检查。Solana getTokenAccountBalance 参考记录了这两个字段,因此选择权在您,但精度保证属于字符串。

  • uiAmount 是 JSON 数字,可能丢失精度。
  • uiAmountString 保留精确数字。
  • 使用 amount + decimals 进行精确计算。
  • 仅将 uiAmount 用于显示。

查询前推导关联代币账户

由于 getTokenAccountBalance 需要代币账户,因此针对钱包和 mint 查询的第一步是推导 ATA。ATA 是由所有者、代币程序和 mint 计算出的程序派生地址。Solana 代币文档涵盖了此推导过程。如果 ATA 尚不存在,查询将失败或返回空结果;您可能需要创建它或处理账户缺失的情况。对于拥有许多代币账户的钱包,枚举它们是另一项任务,详见大型钱包的 getTokenAccountsByOwner 分页。

一个微妙之处:如果钱包在 ATA 约定之外创建了多个代币账户,那么同一 mint 可能有多个代币账户。ATA 是规范的那个,但不是唯一可能的。如果余额看起来不对,请确认您查询的确实是您认为的代币账户。Solana API 指南(RPC Assistant)是解决端点级问题的有用伴侣。

  • 查询前从所有者 + mint 推导 ATA。
  • 显式处理账户缺失的情况。
  • 一个钱包可能为同一 mint 持有多个代币账户。
  • 信任余额前确认代币账户地址。

使用 @solana/web3.js 的可运行 Node.js 示例

下面的示例为所有者和 mint 推导 ATA,调用 getTokenAccountBalance,打印所有四个字段,并根据 amount 和 decimals 重新计算人类可读金额,以显示这些值一致。它使用 @solana/web3.js 和一个公共 RPC 端点占位符。请将端点和所有者/mint 值替换为您自己的。重新计算使用大整数安全方法,因此不会继承浮点问题。

安装 @solana/web3.js 后使用 Node.js 运行。输出应显示 amount、decimals、uiAmount、uiAmountString,以及一个与 uiAmountString 匹配的重新计算金额。如果 ATA 不存在,调用会抛出异常;请捕获该情况并报告,而不是假设余额为零。

// npm install @solana/web3.js
const { Connection, PublicKey, getAssociatedTokenAddress } = require('@solana/web3.js');

async function main() {
  const endpoint = 'https://api.mainnet-beta.solana.com';
  const connection = new Connection(endpoint, 'confirmed');

  const owner = new PublicKey('REPLACE_WITH_OWNER_WALLET');
  const mint = new PublicKey('REPLACE_WITH_MINT');

  const ata = await getAssociatedTokenAddress(mint, owner);
  console.log('ATA:', ata.toBase58());

  try {
    const res = await connection.getTokenAccountBalance(ata);
    const { amount, decimals, uiAmount, uiAmountString } = res.value;

    console.log('amount:', amount);
    console.log('decimals:', decimals);
    console.log('uiAmount:', uiAmount);
    console.log('uiAmountString:', uiAmountString);

    // Recompute human amount from raw amount and decimals without floats.
    const raw = BigInt(amount);
    const scale = BigInt(10) ** BigInt(decimals);
    const whole = raw / scale;
    const frac = raw % scale;
    const fracStr = frac.toString().padStart(decimals, '0').replace(/0+$/, '');
    const recomputed = fracStr.length ? `${whole}.${fracStr}` : whole.toString();

    console.log('recomputed:', recomputed);
    console.log('match:', recomputed === uiAmountString);
  } catch (err) {
    console.error('Query failed (missing ATA or bad account?):', err.message);
  }
}

main();

使用 fetch 的原始 JSON-RPC 示例

如果您不想添加依赖,同样的查询也可以通过原始 JSON-RPC 完成。请求体遵循 JSON-RPC 2.0 规范:jsonrpc 版本、id、method 和 params。method 是 getTokenAccountBalance,唯一参数是代币账户地址。响应包含相同的四个字段以及 context slot。此示例使用 fetch 并打印解析后的值。

当您想查看确切的线上响应(包括 context slot 和任何错误对象)时,请使用此形式。它也是在将特定 RPC 端点接入应用之前测试它的最简单方法。请将端点和代币账户地址替换为您自己的。

// Node.js 18+ (global fetch)
async function getBalance(tokenAccount) {
  const endpoint = 'https://api.mainnet-beta.solana.com';
  const body = {
    jsonrpc: '2.0',
    id: 1,
    method: 'getTokenAccountBalance',
    params: [tokenAccount]
  };

  const res = await fetch(endpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body)
  });

  const json = await res.json();
  if (json.error) {
    console.error('RPC error:', json.error);
    return;
  }

  const { amount, decimals, uiAmount, uiAmountString } = json.result.value;
  console.log('slot:', json.result.context.slot);
  console.log('amount:', amount);
  console.log('decimals:', decimals);
  console.log('uiAmount:', uiAmount);
  console.log('uiAmountString:', uiAmountString);
}

getBalance('REPLACE_WITH_TOKEN_ACCOUNT');

用于验证您自己端点的结果表

由于提供商行为和节点版本可能不同,请对照您自己的端点验证响应约定,而不是信任单个示例。用来自您 RPC 提供商的真实值填写下表。重新计算的金额应与 uiAmountString 完全匹配;如果不匹配,请检查小数位处理或解析器。记录 context slot,以便判断数据新鲜度。

此表是一种测量方法,不是基准测试。它不声称任何提供商的延迟或吞吐量。它只是让您确认所关心账户的 amount、decimals、uiAmount 和 uiAmountString 是否一致。

  • mint:代币 mint 地址。
  • 代币账户:您查询的代币账户。
  • 原始 amount:返回的 amount 字段。
  • decimals:返回的 decimals 字段。
  • uiAmount:返回的 JSON 数字。
  • uiAmountString:返回的十进制字符串。
  • 重新计算金额:由您计算的 amount / 10^decimals。
  • 匹配:重新计算值是否等于 uiAmountString。
  • context slot:响应中的 slot。

getTokenAccountBalance 与 getTokenSupply 及 getBalance 的对比

这三个方法回答不同的问题,容易混淆。getBalance 返回原生系统账户的 lamports,而不是 SPL 代币余额。getTokenAccountBalance 返回一个 SPL 代币账户的余额。getTokenSupply 返回 mint 的总供应量,而不是任何单个持有者的余额。如果您想要钱包在多个 mint 上的持仓,仅靠这些方法都不够;您需要 getTokenAccountsByOwner,详见大型钱包的 getTokenAccountsByOwner 分页。

实用规则:SOL 使用 getBalance,特定代币账户使用 getTokenAccountBalance,mint 级总量使用 getTokenSupply。混淆它们是错误数字的常见来源。对于 Token-2022 mint,转账费用和预扣金额会进一步复杂化持有者实际控制的金额,详见 Token-2022 转账费用和预扣金额。

  • getBalance:原生账户的 lamports。
  • getTokenAccountBalance:一个 SPL 代币账户。
  • getTokenSupply:mint 的总供应量。
  • getTokenAccountsByOwner:钱包的所有代币账户。

小数位因 mint 而异,必须读取,绝不能假设

小数位来自 mint,并非通用。USDC 使用 6 位小数,许多 Solana 代币使用 9 位,其他则不同。假设每个代币都是 9 位会产生错误的人类可读余额。唯一权威来源是 mint 账户,而 getTokenAccountBalance 方便地在返回 amount 的同时返回 decimals,因此在常见情况下无需第二次调用。尽管如此,请将 decimals 视为数据,而不是常量。

如果您缓存 decimals,请在 mint 更改或切换代币时使缓存失效。过期的 decimals 值会静默破坏每个派生余额。Solana 代币文档是 decimals 如何存储和使用的参考。

  • USDC:6 位小数。
  • 许多 Solana 代币:9 位小数。
  • 始终从 mint 或响应中读取 decimals。
  • mint 更改时使缓存的 decimals 失效。

限制与权衡

getTokenAccountBalance 只读取一个代币账户。它不是投资组合方法,不会枚举钱包的持仓。为此,请使用 getTokenAccountsByOwner。decimals 字段只有在反映 mint 时才是权威的;如果您从其他地方推导 decimals,则需自行承担风险。uiAmount 精度无法保证,因为它是 JSON 数字,因此 uiAmountString 或 amount 加 decimals 才是安全路径。

新鲜度是另一个权衡。刚提交的交易可能不会反映在您查询的 commitment 级别。使用适当的 commitment,如果需要等待,请轮询或订阅,而不是假设第一次读取就是最终结果。对于基于订阅的更新,请参阅accountSubscribe 编码:base64 与 jsonParsed。最后,提供商对错误形状和 context 字段的行为虽有文档记录,但可能因提供商而异,因此请针对您实际使用的端点进行测试。

  • 只读取一个代币账户。
  • decimals 仅从 mint 获取才权威。
  • uiAmount 精度无法保证。
  • 新鲜度取决于 commitment 和时机。
  • 错误形状可能因提供商而异。

排查账户类型错误、UI 混淆和过期读取

账户类型错误:如果传入钱包地址,节点可能返回错误或不同账户的数据。请先从所有者和 mint 推导 ATA,并确认查询的地址是代币账户。UI 混淆:如果 UI 显示的数字与 RPC 不同,请检查 UI 使用的是 uiAmount(浮点)还是 uiAmountString(精确),以及是否应用了正确的 decimals。不匹配通常意味着浮点舍入或错误的 decimals 假设。

过期读取:如果最近的转账缺失,请检查 context slot 和 commitment 级别。较低 commitment 的读取可能滞后。在更高 commitment 重新查询或等待确认。如果 ATA 不存在,调用会失败而不是返回零;请显式处理该情况,以免 UI 显示误导性余额。对于端点级问题,Solana API 指南(RPC Assistant)和 API 服务页面是有用的参考。

  • 账户类型错误:推导并验证 ATA。
  • UI 混淆:优先使用 uiAmountString 和正确的 decimals。
  • 过期读取:检查 context slot 和 commitment。
  • ATA 缺失:处理错误,不要假设为零。

后续步骤和相关阅读

现在您已经能解析 getTokenAccountBalance,自然的下一步是使用 getTokenAccountsByOwner 枚举钱包持仓,详见大型钱包的 getTokenAccountsByOwner 分页。如果您需要租金和账户类型等账户级上下文,请阅读读取 Solana 账户和代币余额。关于 Token-2022 的具体内容,请参阅 Token-2022 转账费用和预扣金额。

要选择端点并了解提供商级行为,请从 Solana 网络页面和 OnFinality Learn 中心开始。如果您正在规划生产流量,请查看 RPC 定价和 API 服务概览。对于交互式端点问题,Solana API 指南(RPC Assistant)是一个很好的伴侣。

  • 枚举持仓:getTokenAccountsByOwner。
  • 账户上下文:getAccountInfo 和租金。
  • Token-2022:转账费用和预扣金额。
  • 端点和定价:/en/networks/solana 和 /en/pricing/rpc。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始