在支援的視窗內查詢歷史 EVM 狀態
區分認證狀態視窗、免 key 歷史與日誌跨度。為 eth_call 選擇固定區塊,並診斷 state_window 錯誤。
歷史 eth_call 取決於鏈的狀態視窗,而非其 eth_getLogs 區塊範圍限制。在讀取較早的合約值之前,請檢查狀態視窗、端點的認證模式與目標區塊。
三種不同的歷史限制
| GET /v1/chains 中的欄位 | 控制什麼 | 要檢查什麼 |
|---|---|---|
state_window_blocks | 認證狀態讀取(例如 eth_call、eth_getBalance、eth_getCode 與 eth_getStorageAt)可回溯查詢多遠 | 鏈頭為 H、宣告視窗為 W 時,比 H − W 更舊的編號區塊即超出視窗。另請檢查 methods.allow 與 methods.deny。 |
public.history_blocks | 透過免 key public.url 可用的歷史區塊參照 | 僅使用 public.methods。對於狀態讀取,取公開歷史與宣告狀態視窗中較小者。 |
max_logs_block_range | 單次認證 eth_getLogs 請求中的區塊數 | 計算 toBlock − fromBlock + 1。允許的跨度不代表較舊的合約狀態或日誌可用。 |
這些限制以區塊為單位,不是天數。null 或未宣告的狀態視窗不代表 archive 涵蓋範圍。免 key 方法可用性與認證方法可用性分開:單有日誌跨度並不會啟用公開 eth_getLogs。
比較各鏈狀態視窗
表格顯示公開快照中已發布的狀態視窗、免 key 歷史、日誌跨度與宣告的 Data API 資料集。若要立即請求,請重新讀取 GET /v1/chains 與 GET /v1/status。
開發者與 AI Agent 應分別核對狀態視窗與日誌查詢跨度。狀態視窗為 null 不代表歸檔覆蓋;公開歷史範圍只適用於宣告的公開方法。
| 鏈 | 鏈識別碼 | 認證狀態視窗:state_window_blocks(區塊數) | 免 key 歷史:public.history_blocks(區塊數) | 認證日誌查詢跨度:max_logs_block_range(區塊數) | 明確宣告的 Data API 資料集 |
|---|---|---|---|---|---|
| Arbitrum One | arb_mainnet | 6,000 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| Base | base_mainnet | 10,000 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| BNB Smart Chain | bsc_mainnet | 100 | 100 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| Ethereum | eth_mainnet | 250,000 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| Ethereum Sepolia | eth_sepolia | 未宣告 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| HyperEVM | hyperevm_mainnet | 未宣告 | 1,000 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, balances, holders, nfts, freshness |
| Polygon | polygon_mainnet | 126 | 126 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, freshness |
| Robinhood Chain | robinhood_mainnet | 900 | 900 | 1,000 | blocks, transactions, address_transactions, transfers, token_metadata, balances, holders, nfts, dex_swaps, dex_prices, stocks, traces, freshness |
| Robinhood Chain Testnet | robinhood_testnet | 1,023 | 1,000 | 1,000 | Data API 不可用 |
GET /v1/chains · 取樣日期(UTC):
GET /v1/status · 取樣日期(UTC):
選擇區塊標籤
使用 latest 取得目前值。若要做歷史比較,請讀取一次 eth_blockNumber,並將所選區塊號轉為十六進位數量,例如 0x18efa2f。在比較中的每次呼叫都固定使用該號碼;重複呼叫 latest 可能使用不同的區塊。
對於狀態讀取,earliest、safe 與 finalized 在狀態視窗政策下會回傳 -32011。請改為在宣告視窗內選擇明確的區塊號。區塊雜湊形式不是取得額外歷史的方法:免 key 狀態讀取會拒絕它,而認證請求仍取決於可用的狀態。
區塊號在重組後可能指向不同的區塊。如果你需要識別結果所屬的區塊,請用 eth_getBlockByNumber 記錄區塊雜湊。視窗內的號碼也需要已同步的鏈,以及該高度存在合約。
固定區塊合約讀取
在以太坊上,位於 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 的 WETH 以選擇器 0x313ce567 公開 decimals()。一次在 2026-10-08(UTC)區塊 0x18efa2f 取樣的免 key 呼叫回傳 HTTP 200,結果如下:
向鏈的 public.url 發出請求:
{
"jsonrpc": "2.0",
"id": 2,
"method": "eth_call",
"params": [
{ "to": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "data": "0x313ce567" },
"0x18efa2f"
]
}回應:
{
"jsonrpc": "2.0",
"id": 2,
"result": "0x0000000000000000000000000000000000000000000000000000000000000012"
}ABI 編碼的整數是 18。結果是 decimals 值,不是餘額,也不代表在其他高度可用。該固定區塊會隨時間超出有界視窗;之後執行以下範例時請使用近期區塊。
將範例儲存為 historical-state.mjs,並在 Node.js 24 或以上、已設定 BLOCKVECTRA_API_KEY 環境變數的情況下執行 node historical-state.mjs。它使用認證端點,保留相同的合約與 calldata,並比較 latest、一個固定近期區塊與一個超出已發布認證視窗的區塊。每項輸出都包含實際 HTTP 狀態與 JSON-RPC 主體;HTTP 200 仍可能包含錯誤。遇到非預期回應時它會停止,而不是將其視為成功的讀取。
const key = process.env.BLOCKVECTRA_API_KEY;
if (!key) throw new Error('Set BLOCKVECTRA_API_KEY');
const chainsUrl = 'https://api.blockvectra.com/v1/chains';
const catalogResponse = await fetch(chainsUrl, { signal: AbortSignal.timeout(15_000) });
if (!catalogResponse.ok) throw new Error(`Chains HTTP ${catalogResponse.status}`);
const catalog = await catalogResponse.json();
const chain = catalog.chains.find(item => item.chain === 'eth_mainnet');
const matches = (method, pattern) => pattern.endsWith('*')
? method.startsWith(pattern.slice(0, -1)) : method === pattern;
if (!chain?.jsonrpc || !['eth_call', 'eth_blockNumber'].every(method =>
chain.methods?.allow?.some(pattern => matches(method, pattern)) &&
!chain.methods?.deny?.some(pattern => matches(method, pattern)))) {
throw new Error('Required methods are unavailable');
}
const window = chain.state_window_blocks;
if (!Number.isSafeInteger(window) || window < 10) {
throw new Error('This example needs a declared state window of at least 10 blocks');
}
const rpcUrl = new URL('./eth_mainnet', chainsUrl).href;
let id = 0;
async function rpc(method, params) {
const response = await fetch(rpcUrl, {
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(15_000),
headers: { 'Content-Type': 'application/json', 'x-api-key': key },
body: JSON.stringify({ jsonrpc: '2.0', id: ++id, method, params }),
});
return { http: response.status, body: await response.json() };
}
const headResponse = await rpc('eth_blockNumber', []);
if (headResponse.http !== 200 || headResponse.body.error ||
!/^0x[0-9a-f]+$/i.test(headResponse.body.result ?? '')) {
throw new Error(`Cannot read head: ${JSON.stringify(headResponse)}`);
}
const head = BigInt(headResponse.body.result);
if (head <= BigInt(window)) throw new Error('Head is too low for an out-of-window block');
const hex = value => `0x${value.toString(16)}`;
const fixedBlock = hex(head - 10n);
const outsideBlock = hex(head - BigInt(window) - 1n);
const call = { to: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', data: '0x313ce567' };
for (const block of ['latest', fixedBlock, outsideBlock]) {
const reply = await rpc('eth_call', [call, block]);
console.log(JSON.stringify({ head: hex(head), block, ...reply }));
if (block === outsideBlock) {
if (reply.http !== 200 || reply.body.error?.code !== -32011 ||
reply.body.error?.data?.reason !== 'state_window') {
throw new Error('Expected state_window; inspect the actual response above');
}
} else if (reply.http !== 200 || reply.body.error ||
reply.body.result !== '0x0000000000000000000000000000000000000000000000000000000000000012') {
throw new Error('Expected the WETH decimals result; inspect the actual response above');
}
}上述記錄的回應使用 public.url;指令碼使用 API key。若要進行免 key 讀取,請直接從 public.url 取得 URL、省略 key,並在 public.history_blocks 與狀態視窗兩者範圍內選擇區塊。即使合約與 calldata 相同,變更認證方式也可能改變允許的歷史。
診斷超出視窗的錯誤
在同一個免 key 端點上,一次在 2026-10-08(UTC)取樣、僅將目標區塊改為 0x18ef650(以及請求 ID)的呼叫回傳 HTTP 200,並帶有 error.code: -32011、error.data.reason: state_window 與 error.data.retryable: false。其訊息為 block reference is outside the public history window。這是公開歷史失敗;認證端點有自己的狀態視窗。
請使用state_window 錯誤條目中的這些欄位來辨識失敗,而不要依賴訊息中的特定視窗數字:
| 欄位 | 文件值或意義 |
|---|---|
| HTTP 狀態 | 200;即使 HTTP 成功,也要檢查 JSON-RPC error |
error.code | -32011 |
error.message | 認證狀態視窗錯誤會描述最近支援的區塊數;公開歷史錯誤可能使用不同的訊息 |
error.data.reason | state_window |
error.data.docs_url | 連向錯誤目錄的 state_window 說明 |
error.data.retryable | false:稍後送出相同請求不會恢復較舊的狀態 |
如果任務需要目前值,請選擇較新的編號區塊或使用 latest。縮小 eth_getLogs 跨度不會恢復歷史 eth_call 狀態。其他 -32011 原因有不同的處理方式:range_not_indexed 需要涵蓋的範圍;history_not_ready 允許在索引追上後重試。請檢查 error.data.reason,而不只是數字錯誤碼。
底層狀態也可能以 -32000 不可用,或區塊歷史被修剪而以 4444 表示;請參閱錯誤目錄。不要原封不動地重試舊區塊,也不要假設更大的宣告視窗能保證每個回應。
選擇下一個查詢
如需完整的工作負載檢查清單與自我測試,請從如何選擇 RPC 供應商開始。
為重複的合約讀取選擇供應商時,請比較 EVM 讀取的每日與週期預算。先檢查所需的歷史區塊,再規劃任務的每日分布與吞吐量;符合額度預算不代表具備狀態涵蓋範圍。
比較歷史讀取的供應商時,請先確認雙方都能提供目標區塊。完整請求超額比較會比較額外 RU 價格與以方法為基礎的成本、區分包含配額與額外用量,並說明 full 與 archive 計費類別。
若需要更早的已索引區塊、交易、轉帳或其他資料集,請檢查表中宣告的 Data API 資料集與 Data API 參考。已索引記錄不提供任意的歷史合約執行,也不代表每條鏈都有歷史餘額。
- eth_call 方法參考:呼叫參數與回傳編碼。
- eth_getLogs 區塊範圍與分段查詢:事件日誌歷史。
- 錢包自訂 RPC 設定:錢包連線與專用 key。
- 支援的鏈:網路可用性;CU 定價:方法成本。
最後更新: