---
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 重新推導餘額，直到你要的目標時間點。對於一個活躍的錢包，這意味著數千次呼叫、大量解析工作，以及算錯的高風險。歸檔節點（archival node）能幫你讀取舊狀態，但它們仍然無法直接回答「列出這個持有者在 slot X 時的代幣帳戶」這類問題。

## 運作方式

這個方法保留了你已經熟悉的 `getTokenAccountsByOwner` 語法，並加入了一項：`slot`。slot 是 Solana 的時鐘，大致相當於一個區塊，傳入 slot 代表「告訴我這個錢包在歷史上那個精確時間點的狀態」。

呼叫這個方法時，你會拿到錢包的**代幣帳戶**（token accounts），也就是持有錢包所擁有的每一種代幣的個別紀錄。在 Solana 上，錢包本身並不直接持有餘額，所以列出這些帳戶才是讀取持有狀況的方式。每個帳戶都會附帶其餘額與 **lamports**，也就是每個帳戶為了存在於鏈上必須攜帶的少量 SOL（一枚代幣的小數分割，1 SOL 等於 10 億 lamports）。這個方法涵蓋 Solana 的兩種代幣標準：**SPL Token**，這是最原始的標準，幾乎所有知名代幣（如 USDC）都基於它；以及 **Token-2022**，較新的版本，附加了轉帳手續費、機密餘額等功能。這兩種標準對應不同的 program ID，所以單一 `programId` 過濾條件只會回傳該 program 底下的帳戶。

在呼叫背後，是一個持續建置的歷史索引：一份針對每個代幣帳戶的版本化紀錄，記錄誰在哪些 slot 之間持有多少數量，並隨著新區塊產生持續更新。你可以用兩種方式限定查詢範圍：按 mint，讀取某個代幣在過去某個 slot 的餘額；或按 program，列出該 program 在該 slot 下所擁有的每個代幣帳戶。若要取得橫跨兩種標準的完整投資組合，分別用 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 呼叫，讓你的團隊能專注在產品本身，而不是底層管線工程。

這讓一些原本很麻煩的工作變得可行：

- 任何歷史時間點的投資組合與損益（PnL）快照
- 綁定特定 slot 或日期的成本基礎與稅務申報
- 爭議處理、鑑識與事件調查，例如證明某個錢包在被盜前一個區塊持有什麼
- 需要準確歷史狀態、卻不想重演交易紀錄的分析與索引管線

我們在測試期間，將結果與鏈上真實資料進行了比對驗證。抽樣的歷史查詢結果與鏈上餘額及 lamports 完全吻合，包含像是 slot 80,000,000 時的 KIN 餘額這類深層歷史資料。

## 開始使用

這個方法已在標準的 Solana mainnet 端點上開放使用，所以如果你已經透過 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 並發送你的第一個請求。

## 常見問答

### getTokenAccountsByOwnerAtSlot 做什麼？

它會回傳一個 Solana 錢包在你指定的過去 slot 時的代幣帳戶，包含餘額與 lamports。它與 `getTokenAccountsByOwner` 的結構相同，只是多了一個 `slot` 參數。

### 什麼是 SPL Token 與 Token-2022？

它們是 Solana 的兩種代幣標準。SPL Token 是最原始的標準，幾乎涵蓋網路上所有知名代幣。Token-2022 是較新的版本，新增了轉帳手續費、機密餘額等可選功能。這個方法兩者都涵蓋。

### 這和 getTokenAccountsByOwner 有什麼不同？

`getTokenAccountsByOwner` 只回傳當前的持有狀況。`getTokenAccountsByOwnerAtSlot` 則能回答同一個問題在任何過去 slot 的狀態，它讀取的是歷史索引，而不是即時節點狀態。

### 我需要歸檔節點或自訂索引器嗎？

不需要。這個方法讀取的是預先建好的歷史索引，所以你不需要自己運行歸檔基礎設施，也不必重演交易歷史。

### 我可以查詢錢包的完整投資組合，而不只是單一代幣嗎？

可以，但如果錢包同時持有兩種標準的代幣，就無法只用一次 `programId` 呼叫完成。按 `mint` 限定範圍可查詢單一代幣，按 `programId` 限定範圍則可列出該 program 在某個 slot 下的所有帳戶。SPL Token 與 Token-2022 使用不同的 program ID，所以要取得完整投資組合，需分別查詢兩者並合併結果。

### 隨著鏈持續前進，分頁如何保持準確？

`pageKey` 固定了你查詢的 slot，所以即使有新區塊到達，跨頁的結果仍保持一致，每頁最多可回傳 10,000 個帳戶。
