HyperEVM RPC 限流與日誌回填

處理 HyperEVM RPC 限流與 429 回應,有界呼叫認證 eth_getLogs,儲存游標並復原缺失活動。

直接答案

預設官方 HyperEVM 公共 RPC 的 eth_getLogs 單次查詢最多 50 個區塊(出處見官方 JSON-RPC 文件)。在 BlockVectra 上,認證的 eth_getLogs 請求單次最多涵蓋 1,000 個區塊(來自 GET /v1/chains 中的 hyperevm_mainnet.max_logs_block_range,包含兩端)。跨度超限回傳 HTTP 200、JSON-RPC -32602 與 logs_range_too_large,retryable: false(見錯誤目錄);按 [from, min(from + max − 1, end)] 分段並儲存 cursor(游標)以便斷點續跑,成功後從末尾加一繼續。官方公共 RPC 的每 IP 速率限制與 BlockVectra 的 key 限額分別見官方公共 RPC 限流與 429 及下方服務參數。

這篇指南協助你完成的任務

  • 探測 HyperEVM RPC:在選擇認證方法前,先使用 viem 或 ethers 進行公共讀取。
  • 回填有界日誌範圍:在 HyperEVM eth_getLogs 限制內回填,並根據回傳的錯誤做出重試判斷。
  • 讀取地址活動:使用 key 查詢已索引的交易與轉帳,檢查回傳的覆蓋範圍與新鮮度中繼資料。

三步驟任務:回填限定區塊範圍的 HyperEVM 日誌

先免 key 讀取最新區塊,建立 key,然後查詢合約在有限區塊範圍內的事件日誌。

選擇你所需的合約與區塊範圍。本任務只涵蓋該有界範圍;不保證完整的合約歷史。

1. 免 API key 讀取最新區塊

curl -sS "https://api.blockvectra.com/v1/hyperevm_mainnet/public" \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}'

JSON-RPC 的 result 是十六進位的最新區塊編號。這是由 GET /v1/chains 發布的 HyperEVM public.url。公開端點的 public.methods 不包含 eth_getLogs;步驟 3 需要 key。

2. 建立 API key

為此回填建立 key。建立 key 並儲存對話方塊中顯示的 secret,以用於 hyperevm_mainnet。

對於透過 HTTP 且不使用瀏覽器的 AI Agent,請參考程式化註冊指南。在 POST /auth/siwe/login JSON 請求主體中傳入指南 URL 的有效 ref,而不是範例中的 docs-signup;若無來源則省略。切勿要求使用者將 key 貼入對話中。

3. 使用你的 key 回填日誌

完整入門範本:blockvectra/hyperevm-backfill

將以下指令碼儲存為 hyperevm-task.ts。它可在 Node.js 24 或更高版本下執行,無需額外套件。將 BLOCKVECTRA_API_KEY 設定為你儲存的 key,並將 LOG_ADDRESS 設定為你要檢查的發出合約地址;將 key 保留在你的伺服器或本機終端機中。

export BLOCKVECTRA_API_KEY='replace-with-your-key'
export LOG_ADDRESS='replace-with-contract-address'
node hyperevm-task.ts

指令碼預設查詢最近的 max_logs_block_range 個區塊,接近創世區塊時查詢實際可用的區塊數。此上限在執行時從 /v1/chains 讀取。若要選擇其他有限範圍,請在執行前同時設定 FROM_BLOCK 與 TO_BLOCK,值可用十進位或 0x 十六進位區塊號。超過單次上限的範圍會拆分為連續區段,每段跨度不超過公開上限。

const apiKey = process.env.BLOCKVECTRA_API_KEY;
const address = process.env.LOG_ADDRESS;
if (!apiKey || apiKey === "replace-with-your-key") throw new Error("Set BLOCKVECTRA_API_KEY");
if (!address || !/^0x[0-9a-f]{40}$/i.test(address)) throw new Error("Set LOG_ADDRESS to a contract address");

const fromBlock = process.env.FROM_BLOCK;
const toBlock = process.env.TO_BLOCK;
if ((fromBlock !== undefined || toBlock !== undefined) && (!fromBlock || !toBlock)) {
  throw new Error("Set both FROM_BLOCK and TO_BLOCK");
}

const chainsUrl = "https://api.blockvectra.com/v1/chains";
const rpcUrl = new URL("./hyperevm_mainnet", chainsUrl).href;
async function readJson(url: string) {
  const response = await fetch(url, { signal: AbortSignal.timeout(15_000) });
  if (!response.ok) throw new Error(`Metadata HTTP ${response.status}`);
  return response.json();
}
const catalog = await readJson(chainsUrl);
const chain = catalog.chains.find((item: { chain: string }) => item.chain === "hyperevm_mainnet");
if (!chain || !Number.isSafeInteger(chain.max_logs_block_range) || chain.max_logs_block_range <= 0) {
  throw new Error("Missing or invalid max_logs_block_range");
}
if (!chain.methods.allow.includes("eth_getLogs") || chain.methods.deny.includes("eth_getLogs")) {
  throw new Error("eth_getLogs is unavailable on this chain");
}
const maxRange = BigInt(chain.max_logs_block_range);
const plans = await readJson("https://console-api.blockvectra.com/v1/plans");
function weight(method: string): bigint {
  const row = plans.method_weights.find((item: { method: string }) => item.method === method);
  if (!row || !Number.isSafeInteger(row.cu_weight) || row.cu_weight <= 0) {
    throw new Error(`Missing or invalid CU weight for ${method}`);
  }
  return BigInt(row.cu_weight);
}
const headWeight = weight("eth_blockNumber");
const logsWeight = weight("eth_getLogs");

type RpcBody = {
  result?: unknown;
  error?: { code: number; data?: { retryable?: boolean } };
};
async function rpc(method: string, params: unknown[]): Promise<unknown> {
  for (let attempt = 0; attempt < 4; attempt++) {
    const response = await fetch(rpcUrl, {
      method: "POST",
      redirect: "error",
      headers: { "Content-Type": "application/json", "x-api-key": apiKey! },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
      signal: AbortSignal.timeout(15_000),
    });
    const body = await response.json() as RpcBody;
    if (response.ok && !body.error && body.result !== undefined) return body.result;
    if (body.error?.data?.retryable !== true || attempt === 3) {
      throw new Error(`RPC failed: HTTP ${response.status}, code ${body.error?.code ?? "unknown"}`);
    }
    const retryAfter = response.headers.get("Retry-After");
    const delay = retryAfter === null ? 0 : /^\d+$/.test(retryAfter)
      ? Number(retryAfter) * 1000 : Date.parse(retryAfter) - Date.now();
    if (delay > 30_000) throw new Error("Retry-After exceeds 30 seconds; rerun later");
    const backoff = 1000 * 2 ** attempt + Math.random() * 250;
    await new Promise((resolve) => setTimeout(resolve, Math.max(backoff, Number.isFinite(delay) ? delay : 0)));
  }
  throw new Error("Retry limit reached");
}
function block(value: string): bigint {
  if (!/^(0x[0-9a-f]+|[0-9]+)$/i.test(value)) throw new Error("Invalid block number");
  return BigInt(value);
}
const head = await rpc("eth_blockNumber", []);
if (typeof head !== "string" || !/^0x[0-9a-f]+$/i.test(head)) throw new Error("Invalid chain head");
const latest = block(head);
const end = toBlock ? block(toBlock) : latest;
const start = fromBlock ? block(fromBlock)
  : end >= maxRange - 1n ? end - maxRange + 1n : 0n;
if (start > end || end > latest) throw new Error("Require 0 <= FROM_BLOCK <= TO_BLOCK <= latest");
const chunks = (end - start + maxRange) / maxRange;
console.error(`Window ${start}..${end}; chunks=${chunks}; estimated CU=${headWeight + chunks * logsWeight}`);

const hex = (value: bigint) => `0x${value.toString(16)}`;
for (let from = start; from <= end; from += maxRange) {
  const to = from + maxRange - 1n < end ? from + maxRange - 1n : end;
  const result = await rpc("eth_getLogs", [{ address, fromBlock: hex(from), toBlock: hex(to) }]);
  if (!Array.isArray(result)) throw new Error("Expected eth_getLogs result array");
  console.log(JSON.stringify({ fromBlock: hex(from), toBlock: hex(to), result }));
}

請求按順序執行。僅當 JSON-RPC 錯誤中的 error.data.retryable 為 true 時重試,每個請求最多嘗試四次,使用指數退避與抖動,並讀取 Retry-After 的秒數或 HTTP 日期。等待超過 30 秒時指令碼停止,可稍後重新執行。網路失敗、逾時、回應格式異常及不可重試錯誤立即停止;指令碼以失敗狀態退出,不報告完整回填成功。

標準輸出的每一行包含一個區段的 fromBlock、toBlock 與 result 陣列。result: [] 表示該段沒有相符的日誌。每條日誌可讀取以下欄位:

欄位意義
address發出事件的合約地址。
blockNumber、blockHash日誌所在區塊,區塊編號為十六進位。
transactionHash、transactionIndex、logIndex交易與日誌位置,索引為十六進位。
topics、data事件的索引參數與 ABI 編碼的非索引參數,需使用合約 ABI 解碼。
removed日誌是否因鏈重組而被移除。

最新區塊不代表已最終確認。需要穩定的歷史區間時,應選用應用程式認可的已確認 TO_BLOCK,並處理鏈重組。

若區間包含 B = TO_BLOCK − FROM_BLOCK + 1 個區塊,公開單次上限為 L,區段數就是 N = ceil(B / L)。從 GET /v1/plans 的 method_weights[].cu_weight 讀取 eth_getLogs 與 eth_blockNumber 權重。指令碼向標準錯誤輸出估算值:N × weight(eth_getLogs) + weight(eth_blockNumber),其中包含指令碼攜帶 key 查詢最新區塊的一次呼叫。估算不含額外呼叫及可能計費的重試,實際結算見計費規則。CU 按呼叫估算,不按回傳日誌條數計算。

事件推送: 使用下方分段 HTTP 輪詢,或按 Webhook 推送指南將監聽地址的事件發送到 HTTPS 接收端。GET /v1/push/chains 查看支援的鏈及確認數參數,請求攜帶 x-api-key。簽章校驗、去重與重放見該指南。Webhook 推送與 WebSocket 訂閱是獨立入口,後者按 /v1/chains 的 ws 與 subscriptions 判斷。

使用 viem 或 ethers 連線

參數 / 端點取值 / 範本驗證方式
Chain ID(EIP-155)999—
JSON-RPC(路徑攜帶 Key)POST https://api.blockvectra.com/v1/hyperevm_mainnet/{api_key}URL 路徑中傳入 API key
JSON-RPC(請求標頭攜帶 Key)POST https://api.blockvectra.com/v1/hyperevm_mainnet傳入 x-api-key: {api_key} 請求標頭
Data API 基址GET https://api.blockvectra.com/v1/data/hyperevm_mainnet/…傳入 x-api-key: {api_key} 請求標頭
公開狀態端點GET https://api.blockvectra.com/v1/status無需驗證(公開)

開發者與 AI Agent 可使用同一份伺服器端參數。使用 Node.js 24 或更新版本、viem 2 或 ethers 6,先使用公開讀取;呼叫帶 key 方法時,透過環境變數安全設定 BLOCKVECTRA_API_KEY。切勿將 key 或帶 key 的 RPC URL 放入瀏覽器程式碼、日誌或版本控制中。

儲存為 network.mjs。指令碼從 GET /v1/chains 讀取 chain_id 與方法策略。免 key 讀取使用目錄中的 public.url,只呼叫 public.methods 列出的方法;公開 HTTP 可用不代表 WebSocket 可用。

const chainSlug = process.env.BLOCKVECTRA_CHAIN ?? 'hyperevm_mainnet';
const key = process.env.BLOCKVECTRA_API_KEY;
const catalogUrl = 'https://api.blockvectra.com/v1/chains';
const response = await fetch(catalogUrl, { signal: AbortSignal.timeout(15_000) });
if (!response.ok) throw new Error(`Chains HTTP ${response.status}`);
const catalog = await response.json();
export const chainInfo = catalog.chains.find(item => item.chain === chainSlug);
if (!chainInfo || !Number.isSafeInteger(chainInfo.chain_id) || chainInfo.chain_id <= 0) {
  throw new Error('Missing chain or chain_id');
}
export function allows(method) {
  const matches = pattern => pattern.endsWith('*')
    ? method.startsWith(pattern.slice(0, -1)) : pattern === method;
  if (!key) return (chainInfo.public?.methods ?? []).some(matches);
  return (chainInfo.methods?.allow ?? []).some(matches)
    && !(chainInfo.methods?.deny ?? []).some(matches);
}
if (!allows('eth_chainId')) throw new Error('eth_chainId is unavailable');
export const rpcUrl = key
  ? new URL(`./${chainSlug}/${encodeURIComponent(key)}`, catalogUrl).href
  : chainInfo.public?.url;
if (!rpcUrl) throw new Error('Public RPC is unavailable; set BLOCKVECTRA_API_KEY');

儲存為 viem-client.mjs,透過 npm install viem@2 安裝後執行 node viem-client.mjs。

import { createPublicClient, defineChain, http } from 'viem';
import { chainInfo, rpcUrl } from './network.mjs';

export const chain = defineChain({
  id: chainInfo.chain_id,
  name: chainInfo.name,
  nativeCurrency: { name: 'HYPE', symbol: 'HYPE', decimals: 18 },
  rpcUrls: { default: { http: [rpcUrl] } },
});
export const client = createPublicClient({ chain, transport: http(rpcUrl) });
if (await client.getChainId() !== chain.id) throw new Error('RPC chain ID mismatch');
console.error(await client.getBlockNumber());

使用 ethers 時,儲存為 ethers-client.mjs,透過 npm install ethers@6 安裝後執行 node ethers-client.mjs。

import { JsonRpcProvider } from 'ethers';
import { chainInfo, rpcUrl } from './network.mjs';

const provider = new JsonRpcProvider(rpcUrl, chainInfo.chain_id, { batchMaxCount: 1 });
const network = await provider.getNetwork();
if (network.chainId !== BigInt(chainInfo.chain_id)) throw new Error('RPC chain ID mismatch');
console.log(await provider.getBlockNumber());
provider.destroy();

使用 Foundry 或 Hardhat 部署

目前 hyperevm_mainnet 目錄為 ws=false,methods.allow 未列出 eth_sendRawTransaction。BlockVectra 用於讀取;部署需要支援廣播的 RPC。將 DEPLOY_RPC_URL 設定為該服務商的帶驗證 HTTP URL,不要套用 BlockVectra 的方法或日誌跨度限額。簽名前核對目標 chain ID。

: "${DEPLOY_RPC_URL:?Set a broadcasting RPC URL}"
export RPC_URL="$DEPLOY_RPC_URL"
export CHAIN_ID="$(node --input-type=module -e "import { chainInfo } from './network.mjs'; console.log(chainInfo.chain_id)")"

接著按共用的 Foundry 或 Hardhat 部署教學部署。部署地址需持有 EVM HYPE;部署大型合約前先檢查下方雙區塊要求。

HYPE、小區塊與大型合約部署

HyperEVM 官方網路指南說明 gas 使用 HYPE,精度為 18 位(存取日期:2026-10-07)。部署地址需要在 HyperEVM 上持有 HYPE,HyperCore 餘額不能直接當作 EVM gas 餘額。轉入資金時按該頁連結的原生資產轉移說明操作。

雙區塊架構指南說明小區塊出塊快,大區塊出塊較慢、用於容納較大交易(存取日期:2026-10-07)。先估算部署 gas;超過小區塊預算時,部署地址需先成為已有 HyperCore 使用者,再簽名提交 Core action {"type":"evmUserModify","usingBigBlocks":true}。只調大交易 gas limit 不會選擇大區塊。完成後復原 usingBigBlocks=false,返回小區塊模式。

在支援這些方法的服務商上,用 eth_usingBigBlocks 檢查地址模式,用 eth_bigBlockGasPrice 取得大區塊基礎費用。官方 JSON-RPC 參考說明了這兩個方法(存取日期:2026-10-07)。核對所選服務商的方法;BlockVectra 能力以 /v1/chains 為準。上方最小部署面向小型合約,不會變更 Core 帳戶模式。

HyperCore 與 HyperEVM 資料

EVM RPC 用於合約、收據(receipt)與日誌;HyperCore 交易資料和操作使用 Core API。合約可透過預編譯讀取 Core 狀態,透過 CoreWriter 發送操作,接入時參考官方互動指南(存取日期:2026-10-07)。EVM 日誌不能替代 Core 訂單簿或倉位查詢。

HyperEVM 的系統交易(例如 HyperCore 到 HyperEVM 的轉帳)不在標準 eth_getBlockByNumber 回傳中,官方 RPC 透過 eth_getSystemTxsByBlockNumber 與 eth_getSystemTxsByBlockHash 單獨提供(見官方 JSON-RPC 文件,存取日期:2026-10-07)。BlockVectra 目前的 HyperEVM 區塊、交易與 Data API 資料不包含系統交易,需要此類資料時直接使用官方 RPC 的這兩個方法。

處理官方錯誤 10055

HyperEVM 官方指南將 10055 定義為 Core/EVM 邊界錯誤,包括 nonce、餘額不足、重複雜湊與替換交易費用不足(存取日期:2026-10-07)。先讀取廣播 RPC 的錯誤訊息,再決定復原動作:

  • Nonce: 對照 eth_getTransactionCount 與待處理交易,同一部署地址循序提交,並核對下一個 nonce。
  • 餘額: 檢查部署地址的 EVM HYPE 是否足夠支付轉帳金額與 gas。
  • 重複雜湊: 先查詢既有交易和收據,再決定是否提交其他交易。
  • 替換費用: 核對既有 nonce 與費用,按廣播服務商的替換策略提高費用;重複發送相同位元組不會提高費用。

不能僅憑 10055 盲目重試。錯誤與其復原指引另見 BlockVectra 錯誤參考。

官方公共 RPC 限流與 429

Hyperliquid 官方的限流說明規定,rpc.hyperliquid.xyz/evm 每個 IP 每分鐘最多 100 次 EVM JSON-RPC 請求;其 JSON-RPC 文件還規定,eth_getLogs 單次最多查詢 50 個區塊、最多 4 個 topics。存取日期:2026-10-07。

遇到 HTTP 429 時暫停請求,優先遵循 Retry-After(秒數或 HTTP 日期);沒有該回應標頭時採用有次數上限、帶隨機抖動的指數退避,重試同一個尚未成功的區段。降低並行與輪詢頻率,將日誌查詢拆成符合端點上限的小段;分段本身不會消除速率限制,多個用戶端共用一個 IP 時還需協調請求頻率。

使用 BlockVectra 的帶 key 端點時,從 GET /v1/chains 讀取 hyperevm_mainnet 的 max_logs_block_range、methods.allow 與 methods.deny,不要套用官方公共 RPC 的區塊跨度或每分鐘請求數。請求速率另受 key 的 cu_per_sec、burst_cu 與免費方案呼叫頻率約束(見下節);429 時按 error.data.reason 與 retryable 處理,request_exceeds_burst 需要拆小請求,不能原樣退避重試。

BlockVectra 參數與服務規則

BlockVectra 為 HyperEVM 主網提供 JSON-RPC 與 REST 風格的 Data API:

  1. 鏈參數與日誌範圍: 根據公開介面 GET /v1/chains 中 hyperevm_mainnet 的記錄:
    • 鏈識別代號(Slug):hyperevm_mainnet,Chain ID 為 999。
    • max_logs_block_range:以 GET /v1/chains 的 max_logs_block_range 欄位為準。單次 eth_getLogs 請求的區塊跨度(toBlock − fromBlock + 1)不能超過該上限。若跨度超出上限,平台回傳 HTTP 200 與 JSON-RPC 錯誤碼 -32602(eth_getLogs block range too large: max <N> blocks),該錯誤不計費。
    • state_window_blocks:以 GET /v1/chains 的 state_window_blocks 欄位為準。狀態讀取方法(如 eth_call、eth_getBalance)受該欄位宣告的保留區塊範圍約束(為 null 時表示全量保留,不設短期滾動範圍限制)。
    • 方法策略:以 methods.allow 與 methods.deny 欄位為準。常用讀取方法(eth_blockNumber、eth_getLogs、eth_call、eth_getBalance、eth_getBlockByNumber、eth_getTransactionReceipt 等)均開放;過濾和訂閱方法(eth_subscribe、eth_unsubscribe、eth_newFilter、eth_newBlockFilter)已停用,呼叫停用方法回傳 -32601(不計費)。
  2. 免費方案速率限制與升級: 根據 GET /v1/plans 的資料:
    • free.max_calls_per_sec:上限 25 次/秒,帳戶內所有 key、所有鏈和 Data API 共享。
    • 單 key 預設限制:每個 API key 均有 CU 權杖桶(cu_per_sec 補充速率、burst_cu 突發容量——預設 400 CU/s、突發 1,600 CU)。方法按 CU 權重計費。
    • 限額提升:首次付費儲值後,解除帳戶級每秒呼叫上限;每個 key 仍有預設 CU 速率與突發上限。關於目前費率與計費規則,請參閱定價頁。

回填歷史資料:分段 eth_getLogs 與重試判斷

回填歷史事件日誌時,需要將大跨度區間按目標鏈的 max_logs_block_range 上限分塊請求。用戶端的重試策略應檢查錯誤回應中的 retryable 欄位。

錯誤主體中的 retryable 判定

在 BlockVectra 平台上,JSON-RPC 錯誤回應的 error.data 包含 reason、docs_url 以及 retryable(布林值):

  • retryable: true(可重試):例如瞬時過載(overloaded)、免費方案每秒頻次超限(free_plan_call_limit)、節點同步中(node_syncing)或上游服務無法使用(upstream_unavailable)。若 HTTP 回應標頭帶有 Retry-After,應遵循其指定的秒數或 HTTP 日期;否則採用帶抖動的指數退避重試。
  • retryable: false(不可重試):例如區塊跨度超限(-32602 / logs_range_too_large)、請求參數錯誤(invalid_params)、缺少 API key(missing_api_key)或突發容量超限(-32022 / request_exceeds_burst)。此類錯誤盲目重試無法成功,必須修正參數後再發起請求。

未帶 API key 時的真實 401 回應主體結構如下:

{
  "jsonrpc": "2.0",
  "id": null,
  "error": {
    "code": -32024,
    "message": "missing API key: send it in the request path (/v1/{chain}/<api_key>) or in the x-api-key header",
    "data": {
      "reason": "missing_api_key",
      "docs_url": "https://docs.blockvectra.com/en/errors/#missing_api_key",
      "retryable": false
    }
  }
}

使用 Data API 端點取代大範圍 getLogs 掃描

當應用程式需要追蹤某個特定地址的交易歷史或代幣轉移時,透過 eth_getLogs 掃描需要按目標鏈的 max_logs_block_range 逐段發起請求,並解析原始 Transfer 事件日誌。

BlockVectra 的 Data API 針對 hyperevm_mainnet 提供了按地址索引的專用端點,單次查詢可涵蓋最寬 100,000 個區塊並支援游標分頁:

  1. 地址交易清單:GET /v1/data/hyperevm_mainnet/addresses/{address}/transactions
    • 查詢參數:from_block(必填)、to_block(必填)、direction(選填:from, to, any,預設 any)、clamp(選填的布林字串,預設為 false;設為 true 時,區間超出 100,000 塊或高於 as_of_block 會自動截斷,避免回傳 409 錯誤)、limit(選填,最大 500)、cursor(分頁游標)。
  2. 地址代幣轉帳清單:GET /v1/data/hyperevm_mainnet/addresses/{address}/transfers
    • 查詢參數:standard(必填:erc20 或 erc721;erc1155 不支援按地址查詢並回傳 422 no_coverage)、token(選填合約地址)、from_block(必填)、to_block(必填)、direction(選填:in, out, any)、clamp(選填)、limit、cursor。

回應結構說明

兩個端點均使用標準回應結構:

  • data:資料物件陣列。地址交易包含 hash、block_number、block_timestamp、from、to、value、tx_index、gas_limit、gas_used、status 等;代幣轉帳包含 token、standard、from、to、block_number、block_timestamp、tx_hash、tx_index、log_index(ERC-20 包含 amount,ERC-721 包含 token_id)。
  • next_cursor:存在下一頁資料時回傳的不透明游標字串;若為最後一頁則該欄位不存在(非 null)。
  • meta:包含 chain、chain_slug、chain_external_id、as_of_block、safe_block、finalized_block、coverage(full 或 partial)以及 refreshed_at。

程式碼範例:呼叫 Data API

export BLOCKVECTRA_API_KEY="rgw_your_api_key"

# 1. 查詢地址歷史交易(支援 clamp=true 避免 409)
curl -s "https://api.blockvectra.com/v1/data/hyperevm_mainnet/addresses/0x2222222222222222222222222222222222222222/transactions?from_block=0&to_block=50000&clamp=true" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

# 2. 查詢地址 ERC-20 轉帳記錄
curl -s "https://api.blockvectra.com/v1/data/hyperevm_mainnet/addresses/0x2222222222222222222222222222222222222222/transfers?standard=erc20&from_block=0&to_block=50000&clamp=true" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

即時追蹤:輪詢新區塊

使用 HTTP transport 時輪詢新區塊,在 max_logs_block_range 內連續分段讀取事件日誌。僅當 /v1/chains 回傳 ws=true 且 subscriptions 包含所需類型時選擇 WebSocket。推送至 HTTPS 接收端可使用 Webhook 推送。

驗證已部署的 Hello 合約時,將 LOG_ADDRESS 設為其地址。經廣播 RPC 發送 ping(),再用本頁分段指令碼回填收據所在區塊;後續事件從最後成功區段繼續讀取。

cast send "$CONTRACT_ADDRESS" "ping()" --rpc-url "$DEPLOY_RPC_URL" \
  --private-key "$DEPLOYER_PRIVATE_KEY"

輪詢實作邏輯

  1. 定時呼叫輕量讀取方法 eth_blockNumber 取得鏈上最新高度。
  2. 比較目前高度與已處理高度 lastSeenBlock。
  3. 若 currentBlock > lastSeenBlock,將 [lastSeenBlock + 1, currentBlock] 拆成最多 max_logs_block_range 的區段。每段處理成功後才持久化 lastSeenBlock;失敗時重試未完成區段。按 (blockHash, transactionHash, logIndex) 去重,重新連線時回掃重疊區間以核對重組。
  4. viem 的 watchBlockNumber 或 watchBlocks 在 HTTP transport 下原生使用輪詢機制,可透過 pollingInterval 參數自訂輪詢週期(例如 1000 毫秒)。

分段輪詢事件日誌

儲存為 poll-logs.mjs,與 network.mjs、viem-client.mjs 放在一起。設定 BLOCKVECTRA_API_KEY、LOG_ADDRESS 與 FROM_BLOCK 後執行 node poll-logs.mjs。這個有限範例輪詢鏈頭 12 次,間隔五秒,將每個新區間連續分段查詢;錯誤會停止指令碼,不推進失敗區段。

import { isAddress } from 'viem';
import { client } from './viem-client.mjs';
import { chainInfo, allows } from './network.mjs';

const address = process.env.LOG_ADDRESS;
const start = process.env.FROM_BLOCK;
if (!address || !isAddress(address)) throw new Error('Set LOG_ADDRESS');
if (!/^(0x[0-9a-f]+|[0-9]+)$/i.test(start ?? '')) throw new Error('Set FROM_BLOCK');
if (!allows('eth_getLogs')) throw new Error('eth_getLogs requires an available keyed endpoint');
if (!Number.isSafeInteger(chainInfo.max_logs_block_range) || chainInfo.max_logs_block_range <= 0) {
  throw new Error('Invalid max_logs_block_range');
}
const max = BigInt(chainInfo.max_logs_block_range);
let from = BigInt(start);
for (let poll = 0; poll < 12; poll++) {
  const head = await client.getBlockNumber();
  while (from <= head) {
    const to = from + max - 1n < head ? from + max - 1n : head;
    const logs = await client.getLogs({ address, fromBlock: from, toBlock: to });
    console.log(JSON.stringify({ from: from.toString(), to: to.toString(), logs },
      (_, value) => typeof value === 'bigint' ? value.toString() : value));
    from = to + 1n;
  }
  if (poll < 11) await new Promise(resolve => setTimeout(resolve, 5_000));
}

每條輸出記錄一個已完成區段,繼續執行時將 FROM_BLOCK 設為其 to + 1。持久化取用端需一併儲存事件與游標、去重,並按上方步驟核對重組。429 或其他可重試錯誤按有界退避說明處理同一個未完成區段。

相關指南

下一步

最後更新:

本頁目錄