Solana 质押账户是由质押程序(Stake11111111111111111111111111111111111111)拥有的链上账户,其数据编码了 Meta 部分(免租金储备、授权的 staker 和 withdrawer、锁定期)以及 Stake 状态,后者为 Uninitialized、Initialized、Stake(委托)或 RewardsPool 之一。你可以使用 getProgramAccounts 并将范围限定为质押程序 ID 来发现质押账户,使用 getAccountInfo 并指定 jsonParsed 来获取单个账户并读取委托字段,以及使用 getStakeMinimumDelegation 检查集群最低委托量。委托会经过可能跨越多个 epoch 的预热斜坡逐步激活,而停用会设置一个 deactivation epoch,之后账户会冷却一段时间才能提取。本文展示如何通过 JSON-RPC 读取这些字段,计算质押账户是完全激活还是可提取,并避免常见陷阱,例如未过滤的 getProgramAccounts 扫描和速率限制错误。
质押程序账户模型与状态机
Solana 质押账户是由质押程序拥有的链上账户,其程序 ID 为 Stake11111111111111111111111111111111111111。账户数据编码了两个逻辑部分:Meta 部分,包含免租金储备、授权的 staker 和 withdrawer 公钥以及可选的锁定期;以及 Stake 状态,为 Uninitialized、Initialized、Stake(委托)或 RewardsPool 之一。Stake 状态是携带委托信息的部分,例如 voter 公钥、委托的 lamports、激活 epoch 和停用 epoch。
状态机很重要,因为同一个账户在被委托时可以从 Initialized 变为 Stake,在停用时从 Stake 回到可提取状态。通过 JSON-RPC 读取账户会给你该状态在特定 commitment 级别下的快照。关于 commitment 如何影响你所看到的内容,请参阅 Solana commitment 级别与交易确认。
二进制布局和 jsonParsed 结构由质押程序和 RPC 节点的解析器版本定义。这意味着你看到的确切字段名和嵌套可能因客户端和运行时而异,因此应将解析输出视为可能随升级而变化的已记录行为,而非冻结的 schema。
- 所有者:Stake11111111111111111111111111111111111111
- Meta:免租金储备、授权 staker、授权 withdrawer、锁定期
- Stake 状态:Uninitialized、Initialized、Stake(委托)、RewardsPool
- 委托字段:voter 公钥、质押 lamports、activationEpoch、deactivationEpoch
使用 getProgramAccounts 发现质押账户
要查找质押账户,请调用 getProgramAccounts 并将质押程序 ID 作为程序地址。由于质押程序拥有许多账户,未过滤的调用可能返回非常大的结果集,对节点来说开销很高。Solana getProgramAccounts 参考文档记录了 dataSize 和 memcmp 等过滤器,可让你缩小扫描范围。使用 dataSize 定位质押账户的大小,使用 memcmp 在需要特定字段时匹配已知偏移处的字节。
在实践中,生产代码应进行窄过滤或使用索引器,而不是扫描每个质押账户。如果你必须对大结果集分页,请请求有界范围并从最后看到的账户继续。即使方法不同,Solana getSignaturesForAddress 分页 中的分页模式对于游标式读取是一个有用的思维模型。
提供商行为各不相同:有些端点允许大型 getProgramAccounts 扫描,有些则不允许。如果你的请求被拒绝或限流,这是已记录的/因提供商而异的行为,你应该缩小过滤范围或切换到索引数据源。
- 将调用范围限定为质押程序 ID
- 使用 dataSize 匹配质押账户大小
- 使用 memcmp 匹配已知偏移处的字段
- 对大结果分页,避免无界扫描
使用 getAccountInfo 获取单个质押账户
对于已知的质押账户地址,getAccountInfo 返回账户数据。请求 jsonParsed 编码会要求节点将质押程序布局解码为命名字段,这是读取委托结构的最快方式。Solana getAccountInfo 参考文档记录了 jsonParsed 质押账户布局,包括解析后的委托字段。
当 jsonParsed 不可用或你需要字节级控制时,请求 base64 并自行解码二进制布局。base64 路径在解析器变化时更稳定,但需要你跟踪质押程序布局。关于账户读取、租金和代币账户的更广泛讨论,请参阅 Solana getAccountInfo、租金和代币账户。
解析后的委托通常暴露 voter 公钥、以 lamports 计的委托质押量、激活 epoch 和停用 epoch。将这些字段与当前 epoch 比较,以推断预热和冷却状态。
- 使用 jsonParsed 获取命名的委托字段
- 需要字节级控制时使用 base64
- 读取 voter、质押 lamports、activationEpoch、deactivationEpoch
- 比较 epoch 以确定激活或冷却状态
使用 getStakeMinimumDelegation 读取集群最低委托量
getStakeMinimumDelegation 返回集群当前的最低委托量(以 lamports 计)。Solana getStakeMinimumDelegation 参考文档说明返回值是最低委托 lamports。该值决定新的质押账户是否会真正激活:如果你委托的金额低于最低值,委托可能不会变为活跃。
最低值是一个链参数,因此它是已记录的/因集群和升级而异。不要硬编码它。在提交委托交易之前,在运行时读取它并与你打算委托的金额进行比较。
如果你正在构建质押流程,请先调用 getStakeMinimumDelegation,然后根据它验证用户金额。这可以避免账户处于 Initialized 但从未变为 Stake 的令人困惑的状态。
- 返回最低委托 lamports
- 链参数:因集群和升级而异
- 在提交前验证委托金额
- 防止 Initialized 但从未激活的账户
激活预热与 Epoch 时间
委托不会立即完全激活。它会经历一个可能跨越多个 epoch 的激活(预热)斜坡。activationEpoch 字段标记委托开始激活的 epoch,质押会根据质押激活时间表变为完全活跃。要计算委托何时完全激活,请使用 getEpochInfo 读取当前 epoch 并与 activationEpoch 比较。
集群 epoch 时间不是挂钟常量。Epoch 持续时间取决于 slot 时间和集群配置,因此你应该以 epoch 而非秒来推理。如果你需要挂钟估计,请从当前 epoch 的开始时间和观察到的 slot 速率推导,并将其视为估计值。
一个实用的检查是:如果当前 epoch 大于 activationEpoch 加上预热跨度,则委托完全激活。确切的预热跨度由质押程序定义且可能变化,因此请针对你查询的集群进行验证。
- activationEpoch 标记预热的开始
- 预热可能跨越多个 epoch
- 使用 getEpochInfo 获取当前 epoch
- Epoch 持续时间不是固定的挂钟常量
停用冷却与可提取性
停用会设置一个 deactivation epoch。之后,账户会在随后的 epoch 中冷却,然后才能提取 SOL。要检测“正在停用但尚不可提取”,请将 deactivationEpoch 与当前 epoch 比较:如果当前 epoch 小于或等于 deactivationEpoch,则账户仍在冷却中。
一旦冷却完成,质押不再被委托,withdrawer 可以转移 lamports。提取路径是针对质押程序的交易,而不是 JSON-RPC 读取,但读取会告诉你何时可以安全尝试。
如果你正在监控许多账户,请同时跟踪 deactivationEpoch 和当前 epoch。一个简单的规则是:当当前 epoch 大于 deactivationEpoch 加上冷却跨度时,可提取。与预热一样,请针对集群验证冷却跨度。
- deactivationEpoch 标记冷却的开始
- 冷却跨越随后的 epoch
- 当当前 epoch 超过 deactivationEpoch 加上冷却跨度时可提取
- 在尝试提取交易前读取状态
可运行的 Node.js 示例:最低委托量、解析账户和程序扫描
以下 Node.js 示例使用 @solana/web3.js 调用 getStakeMinimumDelegation,使用 getAccountInfo jsonParsed 获取已知质押账户,使用 dataSize 过滤器对质押程序调用 getProgramAccounts,并打印每个账户是正在激活还是正在冷却。请将 RPC 端点和示例质押账户地址替换为你自己的值。
该示例有意保持小巧,以便你改编。它打印最低委托量、解析后的委托字段、质押账户数量,以及基于当前 epoch 的每个账户的激活或冷却状态。
const { Connection, PublicKey } = require('@solana/web3.js');
const RPC_ENDPOINT = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
const STAKE_PROGRAM_ID = new PublicKey('Stake11111111111111111111111111111111111111');
const SAMPLE_STAKE_ACCOUNT = new PublicKey('YOUR_STAKE_ACCOUNT_ADDRESS');
async function main() {
const connection = new Connection(RPC_ENDPOINT, 'confirmed');
// 1. Minimum delegation
const minDelegation = await connection.getStakeMinimumDelegation();
console.log('Minimum delegation (lamports):', minDelegation);
// 2. Parsed stake account
const accountInfo = await connection.getParsedAccountInfo(SAMPLE_STAKE_ACCOUNT);
console.log('Parsed account:', JSON.stringify(accountInfo.value?.data, null, 2));
// 3. Program accounts with dataSize filter
const accounts = await connection.getProgramAccounts(STAKE_PROGRAM_ID, {
filters: [{ dataSize: 200 }],
});
console.log('Stake accounts found:', accounts.length);
// 4. Activation / cooldown status
const epochInfo = await connection.getEpochInfo();
const currentEpoch = epochInfo.epoch;
for (const { pubkey, account } of accounts.slice(0, 10)) {
const parsed = account.data;
const delegation = parsed?.parsed?.info?.stake?.delegation;
if (!delegation) continue;
const activationEpoch = delegation.activationEpoch;
const deactivationEpoch = delegation.deactivationEpoch;
const activating = currentEpoch <= activationEpoch;
const coolingDown = deactivationEpoch !== '18446744073709551615' && currentEpoch <= deactivationEpoch;
console.log(pubkey.toBase58(), { currentEpoch, activationEpoch, deactivationEpoch, activating, coolingDown });
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});质押账户读取的可复现结果表
使用下表记录你自己的端点对每个质押账户返回的内容。通过运行上面的示例并复制值来填写。这可以将你的观察与已记录行为以及提供商特定结果区分开。
由于解析结构和最低委托量可能因集群和升级而异,该表是捕获你环境的诚实方式。不要假设你在一个集群上看到的值适用于另一个集群。
- 地址:质押账户公钥
- 状态:Uninitialized、Initialized、Stake 或 RewardsPool
- Voter:委托的 voter 公钥
- 委托 lamports:质押金额
- 激活 epoch:预热开始的时间
- 停用 epoch:冷却开始的时间
- 当前 epoch:来自 getEpochInfo
- 完全激活?:当前 epoch 超过预热
- 可提取?:当前 epoch 超过冷却
限制、提供商行为与权衡
二进制布局和 jsonParsed 结构由质押程序和 RPC 节点的解析器版本定义。这是已记录的/因客户端和运行时而异,因此在一个客户端中出现的字段在另一个客户端中可能命名不同或缺失。在生产中依赖它之前,请始终验证结构。
具有大结果集的 getProgramAccounts 开销很大,通常会触发速率限制或 429。生产代码应进行窄过滤、分页结果或使用索引器。如果你需要托管的 RPC 容量,请查看 RPC 定价 和 API 服务 选项。
集群 epoch 时间不是挂钟常量,因此任何关于委托何时完全激活或可提取的估计都应以 epoch 表示,并针对集群重新检查。关于端点选择和网络详情,请参阅 Solana 网络。
- 解析结构因客户端和运行时而异
- 大型 getProgramAccounts 扫描会触发速率限制
- Epoch 时间不是固定的挂钟常量
- 生产环境优先使用窄过滤器或索引器
排查常见的质押账户读取失败
如果 getProgramAccounts 返回 429 或超时,请缩小过滤范围、添加 dataSize 过滤器或切换到索引源。如果 jsonParsed 返回意外结构,请回退到 base64 并自行解码质押程序布局。如果你的端点不支持 getStakeMinimumDelegation,请检查提供商的方法支持,因为可用性是已记录的/因提供商而异。
如果委托从未变为活跃,请将委托金额与 getStakeMinimumDelegation 比较。如果账户看起来正在停用但不可提取,请将 deactivationEpoch 与来自 getEpochInfo 的当前 epoch 比较。关于基于订阅的读取和编码选择,请参阅 Solana accountSubscribe 编码。
如有疑问,请在更高的 commitment 级别重新读取账户并确认 epoch。过时的快照是导致状态混乱的常见原因。
- 429 或超时:缩小过滤器或使用索引器
- 意外解析结构:回退到 base64
- 方法不可用:检查提供商支持
- 从未激活:与最低委托量比较
- 不可提取:将 deactivationEpoch 与当前 epoch 比较
质押账户监控的后续步骤
一旦你能读取单个质押账户,就可以扩展该模式以随时间监控许多账户。跟踪 activationEpoch 和 deactivationEpoch,并在账户从激活转为完全活跃或从冷却转为可提取时发出警报。使用方法级上下文请参阅 Solana API 指南(RPC Assistant),相关指南请参阅 OnFinality Learn 中心。
对于生产工作负载,请选择支持你所需方法和扫描大小的端点。查看 Solana 网络 和 RPC 定价,使容量与你的读取模式匹配。
最后,请为你的集群保持本文的结果表最新。这是区分已记录行为、提供商特定行为和你自己测量的最可靠方式。
- 随时间监控 activationEpoch 和 deactivationEpoch
- 对状态转换发出警报
- 选择支持你扫描大小的端点
- 保持每个集群的结果表