---
title: "推出历史 Solana 代币余额查询"
description: "getTokenAccountsByOwnerAtSlot 通过一次调用返回钱包在任意历史时间点持有的所有代币及其精确余额。"
---

# 推出历史 Solana 代币余额查询

<ImageBlock
  src="https://media.alchemy.com/gTABOAS.png"
  alt="getTokenAccountsByOwnerAtSlot — 历史 Solana 代币余额"
  width={1920}
  height={900}
  priority
/>

问一个 Solana 钱包今天持有什么,任何节点一次调用就能回答。但问它去年春天持有什么,或者恰好在被盗前那一刻持有什么,简单的办法就不存在了。节点只提供当前余额,不会保留谁在何时持有什么的历史记录。所以"这个钱包当时持有什么"的诚实答案一直是:自己重建。

今天我们要改变这一点。[`getTokenAccountsByOwnerAtSlot`](https://www.alchemy.com/docs/chains/solana/solana-api-endpoints/get-token-accounts-by-owner-at-slot) 在一次调用中返回钱包在过去任意时间点持有的全部内容,包括精确余额。

## 重建历史记录的问题

标准的 [`getTokenAccountsByOwner`](https://www.alchemy.com/docs/chains/solana/solana-api-endpoints/get-token-accounts-by-owner) 只回答一个问题:这个钱包现在持有什么。这满足了大多数实时应用流程的需求,但留下了一整类问题没有答案。

要自己重建过去的余额,你得回放这个账户的整个生命周期。用 [`getSignaturesForAddress`](https://www.alchemy.com/docs/chains/solana/solana-api-endpoints/get-signatures-for-address) 拉取钱包的完整签名历史,用 [`getTransaction`](https://www.alchemy.com/docs/chains/solana/solana-api-endpoints/get-transaction) 获取每一笔交易,然后逐个 slot 重新推导余额,直到你想要的目标点。对于一个活跃钱包,这意味着成千上万次调用、大量解析工作,而且很容易算错。归档节点能帮你读取旧状态,但它们仍然不能直接回答"列出这个 owner 在 slot X 时的 token 账户"这个问题。

## 工作原理

这个方法保留了你已经熟悉的 `getTokenAccountsByOwner` 语法,只增加了一样东西:`slot`。slot 是 Solana 的时钟,大致相当于一个区块,传入一个 slot 就意味着"给我这个钱包在历史上那个精确时间点的状态"。

调用后,你会得到钱包的 **token 账户**——持有钱包所拥有的每种 token 的独立记录。在 Solana 上,钱包本身不直接持有余额,所以列出这些账户才是读取持仓的方式。每个账户都带有自己的余额和 **lamports**,这是每个账户为了存在于链上而携带的少量 SOL(SOL 的小数部分,1 SOL 等于 10 亿 lamports)。该方法覆盖 Solana 的两种 token 标准:**SPL Token**,这是最初的标准,支撑着几乎所有知名 token,例如 USDC;以及 **Token-2022**,这是更新的版本,带有转账手续费、保密余额等额外功能。这两个标准位于不同的 program ID 下,所以单个 `programId` 过滤条件只会返回该 program 对应的账户。

调用背后是一个持续构建的历史索引,一份记录随时间变化的版本化数据:谁持有哪个账户、持有多少、在哪些 slot 之间持有,并随着新区块的产生保持更新。你可以用两种方式限定查询范围:按 mint 查询,读取某个 token 在过去某个 slot 的余额;或按 program 查询,列出该 program 下某个 slot 时的所有 token 账户。要获取跨两种标准的完整持仓,分别用 SPL Token 的 program ID 和 Token-2022 的 program ID 各调用一次,再合并结果。下面的示例仅限定在 SPL Token 范围内。

<CodeSnippet
  language="javascript"
  code={`const res = await fetch("https://solana-mainnet.g.alchemy.com/v2/{ALCHEMY_API_KEY}", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: 1,
    method: "getTokenAccountsByOwnerAtSlot",
    params: [
      "89zoESAF7JqYQz5uhf5eYwg3QsKXQy5QFs4WZQ4hMPnb",
      { programId: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" },
      { encoding: "jsonParsed", slot: 80000000 }
    ]
  })
});
console.log(await res.json());`}
/>

分页保持一致,是因为 `pageKey` 固定了你所请求的那个 slot。链的最新进度在你分页的过程中可能会继续推进,但你的结果仍然描述的是同一个时间点,每页最多可返回 10,000 个账户。

## 为什么这很重要

任意时间点的持仓成为了一等公民的查询能力。原本你需要自己搭建和维护的索引流水线,现在收敛成了一次 RPC 调用,这让你的团队能把精力放在产品上,而不是底层管道上。

这打开了一些以前很麻烦的工作:

- 任意历史时刻的投资组合和盈亏快照
- 与具体 slot 或日期相关联的成本基准和税务申报
- 争议处理、取证和事故调查,例如展示某个钱包在被盗前一个区块持有的内容
- 需要精确历史状态而无需回放的分析和索引流水线

我们在测试阶段用链上真实数据核实了结果。抽样的历史查询与链上余额及 lamports 完全一致,包括较久远的历史数据,例如 slot 80,000,000 时的 KIN 余额。

## 开始使用

该方法已在标准 Solana 主网端点上提供,所以如果你已经通过 Alchemy 发送 Solana RPC 请求,可以直接用现有的 API key 调用它。指向 `https://solana-mainnet.g.alchemy.com/v2/{ALCHEMY_API_KEY}`,加上一个 `slot`,就能读取到持仓数据。

参见 [Solana API quickstart](https://www.alchemy.com/docs/reference/solana-api-quickstart) 来设置 key 并发送第一个请求。

## FAQ

### getTokenAccountsByOwnerAtSlot 是做什么的?

它返回 Solana 钱包在你指定的过去某个 slot 时的 token 账户,包括余额和 lamports。它与 `getTokenAccountsByOwner` 结构相同,只是多了一个 `slot` 参数。

### 什么是 SPL Token 和 Token-2022?

它们是 Solana 的两种 token 标准。SPL Token 是最初的标准,覆盖了网络上几乎所有知名 token。Token-2022 是更新的版本,增加了转账手续费、保密余额等可选功能。该方法同时覆盖这两者。

### 这和 getTokenAccountsByOwner 有什么区别?

`getTokenAccountsByOwner` 只返回当前持仓。`getTokenAccountsByOwnerAtSlot` 能在任意过去的 slot 回答同样的问题,读取的是历史索引,而不是实时节点状态。

### 我需要归档节点或自定义索引器吗?

不需要。该方法读取的是一个预先构建好的历史索引,所以你不用运行归档基础设施,也不用自己回放交易历史。

### 我能查询钱包的完整投资组合,而不只是单个 token 吗?

可以,但如果钱包同时持有两种标准的 token,单次 `programId` 调用无法完成。可以按 `mint` 限定查询单个 token,或按 `programId` 限定查询该 program 下某个 slot 的所有账户。SPL Token 和 Token-2022 使用不同的 program ID,所以要获取完整投资组合,需要分别查询两者并合并结果。

### 随着链的推进,分页如何保持准确?

`pageKey` 固定了你查询的 slot,所以即使有新区块不断到达,各页结果仍保持一致,每页最多可返回 10,000 个账户。
