Solana 上的每一份状态都存储在一个账户中:一个 32 字节的地址、一个 lamport 余额、一个所有者程序、一个可执行标志、一个租金纪元以及一个不透明的数据缓冲区。要读取钱包的 SOL 余额,请使用 getBalance;要读取完整的账户元数据和数据,请使用 getAccountInfo;要读取 SPL 代币持有量,您必须通过 getTokenAccountsByOwner 查询代币账户,而不是钱包的 lamport 余额。租金豁免是通过维持一个随账户数据大小变化的最低 lamport 余额来强制执行的,可通过 getMinimumBalanceForRentExemption 查询。本指南解释了这些机制,并提供了可运行的示例,以避免常见的误读。
直接回答:您应该使用哪种 RPC 方法?
如果您需要钱包的 SOL(lamport)余额,请使用钱包地址调用 getBalance。如果您需要完整的账户状态——所有者、可执行标志、租金纪元以及原始数据缓冲区——请调用 getAccountInfo。如果您需要 SPL 代币余额(例如 USDC 或 NFT),不要在钱包或铸币厂上调用 getBalance;而是调用 getTokenAccountsByOwner 来列出钱包拥有的代币账户,可以选择按铸币厂过滤。对于铸币厂的总供应量或最大持有者,请使用 getTokenSupply 和 getTokenLargestAccounts。这种区别是大多数“空白账户”或“零余额”错误的根源。
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 是读取任何账户完整状态的主力方法。它接受一个地址和可选配置:commitment、encoding(base58、base64 或 base64+zstd)以及 dataSlice 以仅获取数据的一部分。响应包括 lamports、owner、executable、rentEpoch 和 data(作为 [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-token 有 unpackAccount,@solana/web3.js 有 AccountInfo,但没有代币特定的解析。以下 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。您必须反序列化每个账户才能获得余额。
该方法通过 before 和 limit 参数支持分页,其中 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] 返回。编码可以是 base58、base64 或 base64+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账户 → 不存在;创建它或检查地址。- 钱包上的空数据 → 这是正常的;代币余额在单独的账户中。
- 零代币余额 → 检查代币账户是否已初始化且未冻结。
- 错误的承诺 → 使用
confirmed或finalized以获得一致的读取。 - 编码不匹配 → 为代币账户请求
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字段。