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

读取 Solana 账户:getAccountInfo、租金豁免与代币余额

了解 Solana 账户数据、租金和 SPL 代币余额的工作原理,以及每种读取应使用哪种 RPC 方法。

TL;DR

Solana 上的每一份状态都存储在一个账户中:一个 32 字节的地址、一个 lamport 余额、一个所有者程序、一个可执行标志、一个租金纪元以及一个不透明的数据缓冲区。要读取钱包的 SOL 余额,请使用 getBalance;要读取完整的账户元数据和数据,请使用 getAccountInfo;要读取 SPL 代币持有量,您必须通过 getTokenAccountsByOwner 查询代币账户,而不是钱包的 lamport 余额。租金豁免是通过维持一个随账户数据大小变化的最低 lamport 余额来强制执行的,可通过 getMinimumBalanceForRentExemption 查询。本指南解释了这些机制,并提供了可运行的示例,以避免常见的误读。

直接回答:您应该使用哪种 RPC 方法?

如果您需要钱包的 SOL(lamport)余额,请使用钱包地址调用 getBalance。如果您需要完整的账户状态——所有者、可执行标志、租金纪元以及原始数据缓冲区——请调用 getAccountInfo。如果您需要 SPL 代币余额(例如 USDC 或 NFT),不要在钱包或铸币厂上调用 getBalance;而是调用 getTokenAccountsByOwner 来列出钱包拥有的代币账户,可以选择按铸币厂过滤。对于铸币厂的总供应量或最大持有者,请使用 getTokenSupplygetTokenLargestAccounts。这种区别是大多数“空白账户”或“零余额”错误的根源。

Solana JSON-RPC 参考文档位于 solana.com/developers,是方法语义的权威来源。OnFinality 的 Solana RPC 端点和提供商(RPC 助手) 页面列出了支持这些方法的端点。

  • getBalance – 仅返回任何账户(钱包、铸币厂、代币账户)的 lamport 余额。
  • getAccountInfo – 返回完整的账户对象:lamports、owner、executable、rentEpoch 和 data。
  • getTokenAccountsByOwner – 返回钱包拥有的代币账户(及其余额),可选择按铸币厂过滤。
  • getTokenLargestAccounts – 返回给定铸币厂的最大代币账户。
  • getTokenSupply – 返回铸币厂的总供应量。

Solana 如何存储状态:账户模型

Solana 不是传统的键值存储,没有单独的余额表和智能合约存储表。相反,每一份状态都是一个账户——一个具有固定头部和不透明字节数组的单一数据结构。头部包含:lamports(以 lamport 为单位的 SOL 余额,1 SOL = 1e9 lamports)、owner(拥有此账户并可修改其数据的程序)、executable(该账户是否为程序)、rent_epoch(下次收取租金的纪元)以及 data(可变长度的字节缓冲区)。

账户的 data 对运行时完全不透明;只有所有者程序才能写入。对于系统拥有的账户(如钱包),数据为空。对于 SPL 代币账户,数据是一个 165 字节的二进制结构,编码了铸币厂、所有者、余额和其他字段。这种设计意味着读取代币余额需要反序列化账户数据,而不仅仅是读取一个数字。

Solana 关于 账户AccountInfo 结构的文档提供了规范模型。OnFinality 的 Solana 网络概述 提供了账户如何融入更广泛链的上下文。

  • 账户地址:32 字节 ed25519 公钥。
  • Lamports:最小的 SOL 单位;1 SOL = 1,000,000,000 lamports。
  • 所有者:可以修改账户数据的程序。
  • 可执行:仅对程序账户为 true。
  • 租金纪元:下次租金到期的纪元(如果未豁免租金)。
  • 数据:不透明的字节数组,通常使用 bincode 或自定义布局序列化。

租金与租金豁免:为什么存在最低余额

为了防止状态膨胀,Solana 对存储数据的账户收取租金。租金在每个纪元边界从账户的 lamport 余额中扣除。但是,如果账户持有至少租金豁免的最低余额,则永久免租。这个最低余额与大小相关:更大的数据缓冲区需要更大的 lamport 存款。

getMinimumBalanceForRentExemption RPC 方法返回给定数据大小所需的确切 lamport 数量。例如,一个具有 165 字节数据的 SPL 代币账户需要特定的最低余额(在 Solana 源代码中有记录,但您可以实时查询)。如果账户低于此阈值,它将成为“支付租金”账户,如果余额降至零,可能会被垃圾回收。

当您通过 SPL 代币程序创建代币账户时,系统会自动从出资钱包转移租金豁免的最低余额。这就是为什么您经常在代币账户中看到少量 SOL 余额的原因。Solana 租金文档 解释了经济原理。对于实际的 RPC 使用,始终使用您计划创建的账户的数据长度调用 getMinimumBalanceForRentExemption

  • 租金从未豁免租金的账户收取。
  • 租金豁免账户不支付租金,也永远不会被回收。
  • 最低余额 = f(数据大小),通过 getMinimumBalanceForRentExemption 查询。
  • 代币账户通常由为其提供资金的钱包以租金豁免方式创建。

使用 getAccountInfo 读取账户数据

getAccountInfo 是读取任何账户完整状态的主力方法。它接受一个地址和可选配置:commitmentencoding(base58、base64 或 base64+zstd)以及 dataSlice 以仅获取数据的一部分。响应包括 lamportsownerexecutablerentEpochdata(作为 [encodedData, encoding] 数组)。

一个常见的陷阱是,getAccountInfo 对于不存在的账户返回 null,而不是空对象。如果您看到 null,则该账户从未被创建或已被删除。此外,默认编码是 base58,对于大数据效率低下;对于程序账户或代币账户,请使用 base64

以下 curl 示例获取 SPL 代币程序本身的账户信息(地址 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)。请注意,数据很大且经过 base64 编码。

curl https://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "getAccountInfo",
  "params": [
    "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
    {
      "encoding": "base64",
      "commitment": "confirmed"
    }
  ]
}'

# Expected output (truncated):
# {
#   "jsonrpc": "2.0",
#   "result": {
#     "context": { "slot": 123456 },
#     "value": {
#       "data": ["base64string...", "base64"],
#       "executable": true,
#       "lamports": 1000000000,
#       "owner": "BPFLoader2111111111111111111111111111111111111",
#       "rentEpoch": 0
#     }
#   }
# }

SPL 代币账户:165 字节布局与反序列化

SPL 代币账户是由 SPL 代币程序(TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)拥有的账户。它们的数据恰好是 165 字节,并遵循固定布局:mint(32 字节)、owner(32 字节)、amount(u64,小端序)、delegate(32 字节,如果没有则为全零)、state(1 字节:0=未初始化,1=已初始化,2=冻结)、is_native(1 字节)、delegated_amount(u64)、close_authority(32 字节,可选)。

要读取余额,您必须反序列化偏移量 64 处的 amount 字段(在 mint 和 owner 之后)。许多 SDK 提供了辅助函数:@solana/spl-tokenunpackAccount@solana/web3.jsAccountInfo,但没有代币特定的解析。以下 Node.js 示例使用 @solana/spl-token 获取并解析代币账户。

SPL 代币程序源代码 是布局的权威参考。对于快速手动检查,您可以使用 getAccountInfo 并切片数据。

// npm install @solana/web3.js @solana/spl-token
import { Connection, PublicKey } from '@solana/web3.js';
import { getAccount, unpackAccount } from '@solana/spl-token';

const connection = new Connection('https://api.mainnet-beta.solana.com');
const tokenAccountAddress = new PublicKey('YOUR_TOKEN_ACCOUNT_ADDRESS');

// Using getAccount (high-level)
const account = await getAccount(connection, tokenAccountAddress);
console.log('Balance:', account.amount.toString());

// Using unpackAccount (lower-level)
const info = await connection.getAccountInfo(tokenAccountAddress);
const parsed = unpackAccount(tokenAccountAddress, info);
console.log('Owner:', parsed.owner.toBase58());
console.log('Mint:', parsed.mint.toBase58());
console.log('Amount:', parsed.amount.toString());

钱包与关联代币账户:为什么余额可能为零

钱包的 SOL 余额存储在钱包的系统账户中。钱包的代币余额存储在钱包账户中;它存储在一个或多个由 SPL 代币程序拥有的独立代币账户中。最常见的代币账户是关联代币账户(ATA),其地址通过 findProgramAddress 使用种子 [wallet, TOKEN_PROGRAM_ID, mint] 从钱包和铸币厂确定性派生。

如果用户从未为特定铸币厂创建 ATA,他们可能仍然在手动创建的非 ATA 代币账户中拥有代币,或者他们可能拥有零代币。因此,在钱包上查询 getBalance 仅返回 SOL,在钱包上查询 getAccountInfo 返回空数据。要查找钱包的所有代币账户,请使用 getTokenAccountsByOwner

ATA 派生公式为:findProgramAddress([owner, TOKEN_PROGRAM_ID, mint], TOKEN_PROGRAM_ID)SPL 关联代币账户文档 对此进行了解释。以下示例派生 ATA 并获取其余额。

// Node.js example to derive ATA and get balance
import { Connection, PublicKey } from '@solana/web3.js';
import { getAssociatedTokenAddress } from '@solana/spl-token';

const connection = new Connection('https://api.mainnet-beta.solana.com');
const wallet = new PublicKey('YOUR_WALLET_ADDRESS');
const mint = new PublicKey('YOUR_MINT_ADDRESS');

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

const info = await connection.getAccountInfo(ata);
if (info === null) {
  console.log('ATA does not exist. User may have no tokens or uses a non-ATA token account.');
} else {
  const balance = await connection.getTokenAccountBalance(ata);
  console.log('Token balance:', balance.value.amount);
}

代币持有量:getTokenAccountsByOwner、getTokenLargestAccounts 和 getTokenSupply

要列出钱包拥有的所有代币账户,请使用 getTokenAccountsByOwner。此方法接受所有者地址和可选过滤器:mint(按特定代币过滤)和 programId(按代币程序过滤,对于 token-2022 很有用)。它返回一个 { pubkey, account } 对象数组,其中每个 account 是带有 base64 数据的标准 AccountInfo。您必须反序列化每个账户才能获得余额。

该方法通过 beforelimit 参数支持分页,其中 before 是代币账户公钥游标。这对于拥有许多代币账户的钱包至关重要。

对于铸币厂的聚合统计,getTokenSupply 返回总供应量,getTokenLargestAccounts 返回按余额排名的前 N 个代币账户。这些对于分析很有用,但除非您还获取账户的所有者字段,否则不会告诉您哪个钱包拥有代币账户。

以下 curl 示例获取钱包的所有 USDC 代币账户。将钱包地址替换为真实地址。

curl https://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "getTokenAccountsByOwner",
  "params": [
    "WALLET_ADDRESS",
    {
      "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
    },
    {
      "encoding": "jsonParsed"
    }
  ]
}'

# Expected output (truncated):
# {
#   "jsonrpc": "2.0",
#   "result": {
#     "context": { "slot": 123456 },
#     "value": [
#       {
#         "pubkey": "TOKEN_ACCOUNT_ADDRESS",
#         "account": {
#           "data": {
#             "parsed": {
#               "info": {
#                 "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
#                 "owner": "WALLET_ADDRESS",
#                 "state": "initialized",
#                 "tokenAmount": {
#                   "amount": "1000000",
#                   "decimals": 6,
#                   "uiAmount": 1.0
#                 }
#               },
#               "type": "account"
#             },
#             "program": "spl-token",
#             "space": 165
#           },
#           "executable": false,
#           "lamports": 2039280,
#           "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
#           "rentEpoch": 0
#         }
#       }
#     ]
#   }
# }

账户写入版本与编码:避免常见陷阱

当您获取账户数据时,data 字段作为数组 [encoded, encoding] 返回。编码可以是 base58base64base64+zstd。对于大数据,base58 效率低下,可能达到响应大小限制。对于程序账户或代币账户,始终使用 base64

另一个陷阱是账户写入版本——这是 Solana 运行时中的一个概念,用于跟踪账户被写入的次数。这在 getAccountInfo 中不公开,但与交易模拟和版本化交易相关。对于读取状态,您通常不需要它,但请注意某些 RPC 响应在上下文中包含 version 字段。

当使用 jsonParsed 编码(如上面的示例)时,RPC 节点会自动反序列化已知的代币账户,为您提供人类可读的 tokenAmount 对象。这是无需手动反序列化即可读取代币余额的最简单方法。但是,jsonParsed 仅适用于由已知程序(如 SPL Token)拥有的账户。对于自定义程序,您必须使用 base64 并自行反序列化。

Solana RPC 文档 详细说明了编码选项。OnFinality 的 API 服务 在其端点中支持这些编码。

  • 对于大数据使用 base64 以避免 base58 膨胀。
  • 对于 SPL 代币账户使用 jsonParsed 以获得预反序列化的余额。
  • 对于自定义程序,获取 base64 并根据程序的布局进行反序列化。
  • 账户数据作为 [data, encoding] 返回;不要将其视为普通字符串。

故障排除清单:为什么您看到零或空数据

如果您从 getAccountInfo 得到 null,则该账户不存在。对于从未创建的 ATA,这是正常的。如果您获得一个账户但数据为空,则您可能正在查看系统账户(钱包)而不是代币账户。如果您获得一个代币账户但余额为零,则该账户可能未初始化或已冻结。

承诺级别很重要:processed 可能在最终确定之前返回数据,而 finalized 确保状态是规范的。对于大多数读取,confirmed 是一个很好的默认值。如果您在调用之间看到不一致的结果,请检查您的承诺。

编码不匹配是另一个常见问题:如果您请求 base58 但期望 JSON 解析的对象,您将得到一个字符串。始终将编码与您的解析逻辑匹配。

最后,请记住,在代币铸币厂上调用 getBalance 返回的是铸币厂的 lamport 余额(用于资助铸币厂账户的 SOL),而不是代币供应量。要获取代币供应量,请使用 getTokenSupply

  • null 账户 → 不存在;创建它或检查地址。
  • 钱包上的空数据 → 这是正常的;代币余额在单独的账户中。
  • 零代币余额 → 检查代币账户是否已初始化且未冻结。
  • 错误的承诺 → 使用 confirmedfinalized 以获得一致的读取。
  • 编码不匹配 → 为代币账户请求 jsonParsed 或正确解析 base64
  • 在铸币厂上调用 getBalance → 返回 lamports,而不是代币供应量。

限制与权衡

通过 RPC 读取账户数据具有固有的局限性。首先,getAccountInfo 返回整个数据缓冲区,对于程序账户(例如,SPL 代币程序超过 100 KB)可能很大。重复获取此类数据可能效率低下;考虑使用 dataSlice 仅获取您需要的字节。

其次,RPC 提供商通常施加速率限制和有效负载大小限制。具体数字因提供商而异;OnFinality 的 RPC 定价 页面列出了计划,但此处未发布具体上限。请始终检查您的提供商的文档。

第三,对于拥有许多代币的钱包,getTokenAccountsByOwner 可能返回大量账户。对于生产使用,分页是强制性的。该方法的 limit 参数受提供商限制(因提供商而异)。

最后,账户数据仅与您查询的插槽一样新。对于实时应用程序,请使用 WebSocket 订阅(例如 accountSubscribe)来监控更改。OnFinality 的 监控 RPC 端点和节点健康 指南涵盖了这一点。

  • 大数据缓冲区会减慢响应速度;使用 dataSlice
  • 提供商的速率限制和上限各不相同;请检查您的计划。
  • 对于拥有许多代币账户的钱包,分页是必需的。
  • 对于实时更新,请使用 WebSocket 订阅而不是轮询。

后续步骤与进一步阅读

既然您了解了账户模型和 RPC 方法,您就可以构建可靠的索引器和 dApp。要深入了解,请探索 Solana RPC 端点和提供商(RPC 助手) 以选择最适合您需求的端点。有关历史账户状态,请参阅 通过 RPC 读取 Solana 历史交易数据

如果您正在其他链上构建,同样的原则也适用:访问历史区块链数据 涵盖了一般模式,使用 eth_getProof 进行状态证明 展示了以太坊的做法。

有关 Solana 的更广泛概述,请访问 Solana 网络页面。别忘了查看 OnFinality Learn 中心 获取更多教程。如果您需要生产级 RPC 访问,请查看 API 服务RPC 定价 页面。

  • 使用您自己的地址尝试这些示例并比较结果。
  • 使用填充表格记录您的测量结果:方法、地址、承诺、编码、结果和备注。
  • 尝试使用 dataSlice 仅获取代币账户的 amount 字段。

永远不用担心基础设施

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

开始