@anywalletkit/history-evm

中文文档
0.1.11发布于 2026/11/03Apache-2.0TypeScriptESM + CJS构建来源已验证近 30 天下载 1950

Activity history (HistorySource) for every EVM chain the SDK supports: Etherscan v2, Blockscout PRO and Alchemy Transfers API (API keys) with Blockscout and Routescan keyless fallbacks, merged into one ChainTx per hash with a documented spam heuristic.

原始使用文档
文档目录9 节
  1. Install
  2. Options
  3. Chain coverage (verified with curl on 2026-10-14)
  4. Alchemy backend ( backend: 'alchemy' )
  5. What a ChainTx contains
  6. Pagination
  7. Spam rule
  8. Adding a chain
  9. Tests

@anywalletkit/history-evm

npm

HistorySource for every EVM chain the SDK supports. One source per chain (chain = "eip155:<id>"), backed by Etherscan v2 when you have an API key, Blockscout PRO and Alchemy (Transfers API) with theirs, and keyless fallbacks (Blockscout /api/v2, Routescan's Etherscan-format mirror) where a public instance exists. Explorer rows are merged into one ChainTx per hash: top-level transaction, ERC-20 transfers and internal native transfers, with fee, status, call selector and a documented spam flag.

Install

npm install @anywalletkit/history-evm @anywalletkit/core
# or: pnpm add @anywalletkit/history-evm @anywalletkit/core  ·  yarn add @anywalletkit/history-evm @anywalletkit/core

On npm: @anywalletkit/history-evm, published from the release workflow with provenance.

import { createEvmHistory } from '@anywalletkit/history-evm'

const history = createEvmHistory({ chainId: 8453, etherscanApiKey: process.env.ETHERSCAN_API_KEY })
const page = await history.listActivity('0x8AbE813577E47415A3CD0Bbc68c7ABCa2c4c55bA', { limit: 50 })
for (const tx of page.items) console.log(tx.hash, tx.status, tx.transfers.length, tx.spam ? 'spam' : '')
const older = await history.listActivity(address, { cursor: page.cursor })
const one = await history.getTransaction(page.items[0].hash, address)
history.explorerTxUrl(one.hash) // https://basescan.org/tx/0x…

Options

optiondefaultnotes
chainId—1, 8453, 42161, 10, 137, 56, 43114, 100, 130, 143, 999, 146, 80094, 59144, 534352, 324, 42220, 9745, 4326, 4663, 480, 1329, 57073, 98866, 196
etherscanApiKeynoneWithout it Etherscan is never called: since 2026 api.etherscan.io/v2 answers Missing/Invalid API Key to keyless requests.
etherscanUrlhttps://api.etherscan.io/v2/apiPoint at a proxy that adds the key server-side.
blockscoutProApiKeynoneBlockscout PRO multichain key (proapi_…; free tier 100K credits/day, 5 rps, one key for every Blockscout chain). Etherscan-format with chain_id=; tried after Etherscan, so a free Etherscan key + a free PRO key together cover Base, Arbitrum, Polygon, Optimism, Gnosis, Robinhood, Arc, Scroll, zkSync, Ink, Plume without a paid plan. Register at docs.blockscout.com/devs/pro-api.
blockscoutProUrl / blockscoutProRateLimithttps://api.blockscout.com/v2/api / 5Override the endpoint (proxy) / the per-key limiter.
alchemyApiKeynoneAlchemy key (free tier: 30M CU/month, no card). Enables the alchemy backend: alchemy_getAssetTransfers lists + one JSON-RPC batch of eth_getTransactionByHash/eth_getTransactionReceipt per page. The only free source on Base and BSC; also a second source for Monad, Berachain, Linea, HyperEVM and Robinhood, which otherwise depend on Etherscan alone. Tried after Etherscan and Blockscout PRO. Every network must be enabled for the app in the Alchemy dashboard (see below).
alchemyNetworkALCHEMY_NETWORKS[chainId] when the Transfers API is documented for the chainSlug override (base-mainnet). Passing it also forces the backend on for a chain the table lists as undocumented (Sonic, Plasma, MegaETH, Sei, Plume, X Layer).
alchemyUrlhttps://{network}.g.alchemy.com/v2/{key}Host template for a proxy; {network} / {key} substituted.
alchemyComputeUnitsPerSecond300Alchemy compute units per second, shared per key across chains. Each call is charged its published cost (Transfers 120, receipt 20, tx 17, block 16, else 26) and a JSON-RPC batch the sum of its members, so concurrent chains on one key stay under the free tier's 500 CU/s. A rate-limit answer is retried up to three times with backoff.
blockscoutUrlbuilt-in tableAny Blockscout /api/v2 root; adds or overrides the chain's entry.
etherscanCompatUrlbuilt-in table (Routescan)Keyless Etherscan-format endpoint.
fetchglobalThis.fetchInject for tests / custom transports.
rateLimit3 rps with a keyEtherscan requests per second (free tier 3, Lite 5, Standard 10). One token bucket per key is shared by every source in the process (25 chains do not multiply the quota). Blockscout: 5 rps per host, Routescan: 2 rps.
prefer'etherscan''blockscout' tries Blockscout first to save Etherscan quota; 'alchemy' tries Alchemy first (Base, where it is the only free one, or to spend Alchemy CU instead of Etherscan calls).
timeoutMs15 000Per request (AbortSignal.timeout when the runtime has it).

Backend order: Etherscan v2 (key + chain in the v2 chainlist) → Blockscout PRO (key) → Alchemy (key + documented Transfers API, or alchemyNetwork) → Routescan → Blockscout. prefer moves Blockscout or Alchemy first. A failing backend is retried once on rate limiting (HTTP 429/5xx, Max rate limit reached), then the next backend is tried. Plan / key refusals (Free API access is not supported for this chain, Missing/Invalid API Key, Community Free API Limit reached, unsupported chainid, Blockscout PRO Unauthorized / Featured chain, Alchemy <NETWORK> is not enabled for this app / HTTP 401 / 403 / Unsupported method) skip straight to the fallback. When nothing is left the source throws HistoryUnavailableError with chain, reason and one line per attempts. Chains in NO_EXPLORER_CHAIN_IDS return an empty page with a note instead.

The Etherscan v2 chainlist (/v2/chainlist) is fetched once per process and cached; when it cannot be fetched the table verified on 2026-10-14 is used.

Chain coverage (verified with curl on 2026-10-14)

With a Blockscout PRO key (verified live with a free-tier key on 2026-10-14): served on 1, 10, 100, 130, 534352, 42220, 4326, 4663, 480, 57073, 98866, 5042 and 42161 (Arbitrum). "Featured" chains need the Builder plan ($49/mo): 8453 Base, 137 Polygon, 324 zkSync ({"error":"Featured chain … requires one of the following plans: Builder, Business, Pro"} → classified unavailable, falls through). "Network not supported": 56, 43114, 143, 999, 146, 80094, 59144, 9745, 1329, 196, 5000, 747474, 2818, 14. Net effect with a free Etherscan key + a free PRO key: everything has a free source except Base (paid Etherscan or Blockscout Builder), BSC, X Layer and Morph — add a free Alchemy key (section below) and Base and BSC are covered too; only X Layer (and Sonic / Plasma / MegaETH / Sei / Plume, where Alchemy has no Transfers API) stay on Etherscan alone.

Probe used for Blockscout: GET <root>/api/v2/addresses/0x8AbE813577E47415A3CD0Bbc68c7ABCa2c4c55bA/transactions and /api/v2/transactions, kept only when both answer 200 JSON.

chainidEtherscan v2keyless fallbacknotes
Ethereum1yes (free plan ok)Blockscout eth.blockscout.com, Routescan
Base8453yes, paid plan onlynonebase.blockscout.com is behind a Cloudflare JS challenge (403)
Arbitrum One42161yes (free plan ok)nonearbitrum.blockscout.com 403 (Cloudflare)
OP Mainnet10yes, paid plan onlyBlockscout explorer.optimism.io
Polygon137yes (free plan ok)nonepolygon.blockscout.com 403 (Cloudflare)
BNB Smart Chain56yes, paid plan onlynoneno public Blockscout found
Avalanche C-Chain43114yes, paid plan onlyRoutescan
Gnosis100yes, paid plan onlyBlockscout-compatible gnosisscan.iognosis.blockscout.com redirects there
Unichain130yesBlockscout unichain.blockscout.com
Monad143yesnone
HyperEVM999yes (free plan ok)none
Sonic146yesnone
Berachain80094yesnoneberatrail.io (Routescan UI) answers 202, no JSON
Linea59144yesnoneexplorer.linea.build has no /api/v2
Scroll534352no (not in chainlist)Blockscout-compatible scrollscan.comscroll.blockscout.com redirects there
zkSync Era324noBlockscout zksync.blockscout.com
Celo42220yes, community quotaBlockscout celo.blockscout.com
Plasma9745yesRoutescan
MegaETH4326yesBlockscout megaeth.blockscout.com
Robinhood Chain4663yes (free plan ok)none
World Chain480yesBlockscout worldchain-mainnet.explorer.alchemy.com
Sei1329yesnoneseitrace.com has its own API
Ink57073noBlockscout explorer.inkonchain.com
Plume98866noBlockscout explorer.plume.org
X Layer196nononeonly OKLink (own API, key); empty page + note

"free plan ok" = a free-tier key returned rows (or No transactions found) that day; "paid plan only" = Free API access is not supported for this chain. Chains marked "yes" without a qualifier were served but the test wallet has no history there, so the plan tier could not be told apart. Without a key, the chains with "none" in the fallback column throw HistoryUnavailableError (no keyless explorer; pass etherscanApiKey).

Alchemy backend (backend: 'alchemy')

alchemy_getAssetTransfers is called twice per page — fromAddress and toAddress — with category: ['external', 'erc20'] (+ 'internal' on the chains in ALCHEMY_INTERNAL_CHAIN_IDS), order: 'desc', withMetadata: true (block timestamps), excludeZeroValue: false (zero-value address-poisoning transfers are what the spam rule flags; Alchemy hides them by default) and maxCount = limit. Transfers are de-duplicated by uniqueId, grouped by hash, and the Transfers API's gaps are filled from the same endpoint: every unique hash of the page gets eth_getTransactionByHash + eth_getTransactionReceipt in one JSON-RPC batch (≤ 20 requests per HTTP call, one retry) → from, to, value, input → call.selector, status, fee = gasUsed × effectiveGasPrice. Blocks whose transfers carried no metadata.blockTimestamp get an eth_getBlockByNumber in that batch.

Merge rules (mirroring the Etherscan txlist + tokentx + txlistinternal merge): erc20 rows → token transfers (symbol = asset, decimals = rawContract.decimal, amount = rawContract.value); internal rows → native transfers; external rows are the transaction's own value and are already the tx row's nativeValue — they add no second native transfer. Only when the node has no such tx any more (dropped / reorged, null from the batch) is the external row turned into a bare tx row (status: confirmed, no fee) so the item still renders. ERC-721 / 1155 / special-NFT rows are skipped (v1). getTransaction(hash) = the same batch for one hash + ERC-20 Transfer logs of the receipt + eth_getBlockByNumber for the time.

Pagination is by block, not by pageKey. Measured on base-mainnet on 2026-10-14: a request that carries a pageKey ignores toBlock; an unknown or expired pageKey (TTL 10 minutes) silently restarts from the first page; a malformed one is HTTP 400 Invalid pageKey format. None of that fits a cursor that is resumed hours later or re-fetched with a narrower window, so the Alchemy lists page exactly like the Etherscan ones — toBlock = w − 1 from the window cursor, next is a marker — and an Alchemy cursor never expires.

Budget. 120 CU per Transfers call, 15 CU per eth_getTransactionByHash, 15 CU per eth_getTransactionReceipt: a 50-item page ≈ 2 × 120 + 50 × 30 ≈ 1 740 CU, so the free 30M CU/month is ≈ 17 000 pages (≈ 570 a day); a getTransaction ≈ 45–60 CU. Throughput on the free tier is 500 CU/s, hence the 4 rps advice above.

Gotcha: enable the network in the Alchemy app. A new app has only the networks you tick in the dashboard (Apps → your app → Networks). Every other slug answers HTTP 403 {"error":{"code":-32600,"message":"BNB_MAINNET is not enabled for this app. Visit this page to enable the network: …"}} — classified unavailable, no retry, next backend. Verified 2026-10-14 with a free key: eth-mainnet and base-mainnet answered (including the internal category on both); every other slug below answered "not enabled" until ticked.

Slug table (ALCHEMY_NETWORKS; verified 2026-10-14 by eth_chainId on every slug — an existing slug answers the chain id or "not enabled", a non-existent one does not resolve). "Transfers API" = the chain's docs page https://www.alchemy.com/rpc/<chain>.md lists it under "Supported on …" (ALCHEMY_TRANSFERS_CHAIN_IDS); "internal" = the internal category per docs ("available only on Ethereum Mainnet, Polygon Mainnet, Base Mainnet, Arc Mainnet, and Arc Testnet"; ALCHEMY_INTERNAL_CHAIN_IDS).

chainidslugTransfers API documentedinternal category
Ethereum1eth-mainnetyesyes (verified live)
Base8453base-mainnetyesyes (verified live)
Arbitrum One42161arb-mainnetyesno
OP Mainnet10opt-mainnetyesno
Polygon PoS137polygon-mainnetyes (/rpc/matic.md)yes (docs)
BNB Smart Chain56bnb-mainnetyesno
Avalanche C-Chain43114avax-mainnetyesno
Gnosis100gnosis-mainnetyesno
Unichain130unichain-mainnetyesno
Monad143monad-mainnetyesno
HyperEVM999hyperliquid-mainnetyesno
Sonic146sonic-mainnetno ("Not currently supported")no
Berachain80094berachain-mainnetyesno
Linea59144linea-mainnetyesno
Scroll534352scroll-mainnetyesno
zkSync Era324zksync-mainnetyesno
Celo42220celo-mainnetyesno
Plasma9745plasma-mainnetnono
MegaETH4326megaeth-mainnetnono
Robinhood Chain4663robinhood-mainnetyesno
World Chain480worldchain-mainnetyesno
Sei1329sei-mainnetnono
Ink57073ink-mainnetyesno
Plume98866plume-mainnetno (no Alchemy docs page)no
X Layer196xlayer-mainnetnono
Arc5042arc-mainnetyesyes (docs)

Only Ethereum and Base were exercised end to end (the test key had only those two networks enabled); the other rows are slug + documentation checks. Chains marked "no" are not tried unless alchemyNetwork is passed.

What a ChainTx contains

  • time from timeStamp (Etherscan) / ISO timestamp (Blockscout) / metadata.blockTimestamp (Alchemy, else eth_getBlockByNumber).
  • status: failed when isError = 1 or txreceipt_status = 0 (Blockscout status = error, receipt status = 0x0), else confirmed.
  • from / to / nativeValue from the transaction row. A hash the explorer only lists as a token transfer to the address (incoming ERC-20) has no transaction row: from/to are then the transfer's, raw.tx is absent.
  • fee = gasUsed × gasPrice (Blockscout fee.value; Alchemy and the Etherscan proxy path: receipt gasUsed × effectiveGasPrice).
  • call = { to, selector (4 bytes, lowercase), data } when the input is not 0x.
  • transfers: the native value (when > 0), every ERC-20 row (standard: 'erc20', symbol/decimals from the explorer or the swap2 asset catalog), every internal native transfer with value > 0. ERC-721 / ERC-1155 rows are skipped in v1.
  • block as bigint, raw = { source, tx, tokens, internal, spam?, spamReason? } with the untouched explorer rows (Alchemy: raw.tx = { tx, receipt } JSON-RPC objects, raw.tokens / raw.internal the Transfers API rows).
  • EvmChainTx.spam mirrors raw.spam.

getTransaction(hash, address) uses Etherscan's proxy module or Alchemy's JSON-RPC (eth_getTransactionByHash + eth_getTransactionReceipt; ERC-20 Transfer logs become transfers; no internal transfers, neither has traces) or Blockscout /api/v2/transactions/<hash> + /token-transfers + /internal-transactions. Returns undefined for an unknown hash.

Pagination

Explorer lists (transactions, token transfers, internal transfers) paginate independently, so the source pages by block window: each page fetches one explorer page of every list and emits only rows above the highest cut block among the lists that still have more rows. Everything emitted is complete across all three lists, so the merge by hash is exact. The cursor (base64 JSON: { v, b: backend, w: exclusive upper block, lists }) carries Blockscout's next_page_params per list; Etherscan lists simply re-query with endblock = w - 1, Alchemy lists with toBlock = w - 1. A cursor from one backend resumes on another by block when the first one fails. limit (default 50, max 100) is the explorer page size, so a page can hold more hashes than limit when token/internal rows add hashes of their own. Degenerate case: when one list has more than limit rows in a single block, that block is emitted once and the remainder skipped.

since is applied client-side on time (explorers page by block, not time): rows older than since are dropped and the page has no cursor.

Spam rule

Never applied to a transaction the address itself sent. A tx with at least one token row is flagged (raw.spamReason) when:

  1. zero-value — every movement in the tx is zero (zero-value transferFrom address poisoning, even with a catalog token such as USDT);
  2. zero-value-unknown-token — a token the catalog does not know moved 0;
  3. bulk-airdrop — 20 or more token rows in one tx and at least one unknown token;
  4. symbol-impersonation — an unknown token whose symbol contains non-ASCII characters (USDΤ) or whose ASCII form equals a catalog symbol on this chain (fake "USDT" at another contract).

"Catalog" = @anywalletkit/swap2-assets (ccChain('eip155:<id>').assets).

Adding a chain

  1. Add the id to SUPPORTED_CHAIN_IDS and its explorer to EXPLORER_TX_BASE in src/chains.ts.
  2. curl https://api.etherscan.io/v2/chainlist — if listed, add to ETHERSCAN_V2_CHAIN_IDS_FALLBACK.
  3. Probe a Blockscout root (/api/v2/transactions must be 200 JSON) → BLOCKSCOUT_URLS; Routescan (https://api.routescan.io/v2/network/mainnet/evm/<id>/etherscan/api?module=account&action=txlist&address=…) → ETHERSCAN_COMPAT_URLS.
  4. Alchemy: curl -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' https://<slug>.g.alchemy.com/v2/<key> → ALCHEMY_NETWORKS; https://www.alchemy.com/rpc/<chain>.md lists "Transfers API" → ALCHEMY_TRANSFERS_CHAIN_IDS (and ALCHEMY_INTERNAL_CHAIN_IDS when the internal category is documented).
  5. Nothing answers → NO_EXPLORER_CHAIN_IDS.
  6. Update the tables above with the date, run pnpm --filter @anywalletkit/history-evm test (the chain-table tests enforce that every id is covered and has an Alchemy slug).

Tests

pnpm --filter @anywalletkit/history-evm test — no network: fixtures in tests/fixtures/ are real Etherscan v2 (chainid=1, captured with a key — keyless capture is no longer possible) and eth.blockscout.com responses for the public test wallet 0x8AbE813577E47415A3CD0Bbc68c7ABCa2c4c55bA, plus a Routescan sample for Avalanche and synthetic rows for a 3-leg swap, a failed tx, a 20-row airdrop and a fake-USDT dust transfer. Alchemy fixtures (alchemy-base-*.json, base-mainnet, same wallet, captured with a free key on 2026-10-14): the fromAddress / toAddress Transfers pages (incl. internal rows, an Aave supply and withdraw, inbound-via-internal payments and non-ASCII spoofed tokens), the tx + receipt batch for every hash in them (calldata longer than 2 KB and receipts with many logs trimmed, marked _inputTrimmed / _logsTrimmed), and the is not enabled for this app envelope. LIVE_HISTORY=1 (and optionally ETHERSCAN_API_KEY / ALCHEMY_API_KEY) enables the live checks.

Hermes-safe: fetch only, guarded AbortSignal.timeout, own base64 (no Buffer/btoa), no URL in src/.

版本与发布记录

@anywalletkit/history-evm 共发布 11 个版本,首次发布于 2026/10/25;过去 12 个月发布了 11 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.1.11latest2026/11/035 天前
0.1.102026/10/309 天前
0.1.92026/10/2811 天前
0.1.82026/10/2712 天前
0.1.72026/10/2612 天前
0.1.62026/10/2613 天前

关于 @anywalletkit/history-evm 的常见问题

@anywalletkit/history-evm 是什么?

Activity history (HistorySource) for every EVM chain the SDK supports: Etherscan v2, Blockscout PRO and Alchemy Transfers API (API keys) with Blockscout and Routescan keyless fallbacks, merged into one ChainTx per hash with a documented spam heuristic.

如何安装 @anywalletkit/history-evm?

在项目目录运行 npm install @anywalletkit/history-evm,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@anywalletkit/core(0.8.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@anywalletkit/history-evm 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@anywalletkit/history-evm 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@anywalletkit/history-evm 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@anywalletkit/history-evm 可以商用吗?

@anywalletkit/history-evm 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@anywalletkit/history-evm 的下载量有多少?

根据 npm 官方下载统计,@anywalletkit/history-evm 在 2026/10/06 – 2026/11/04 这 30 天内被下载 1,950 次,日均约 65 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@anywalletkit/history-evm 还在维护吗?

最新版本 0.1.11 发布于 2026/11/03(5 天前);至今共发布 11 个版本,其中过去 12 个月发布了 11 个;首次发布于 2026/10/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。