@w3lib/evm-batch-call 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +184 -30
  3. package/contracts/DeploylessMulticall3.sol +5 -0
  4. package/dist/aggregate.d.ts +25 -3
  5. package/dist/aggregate.d.ts.map +1 -1
  6. package/dist/aggregate.js +143 -5
  7. package/dist/aggregate.js.map +1 -1
  8. package/dist/batcher.d.ts +6 -1
  9. package/dist/batcher.d.ts.map +1 -1
  10. package/dist/batcher.js +59 -9
  11. package/dist/batcher.js.map +1 -1
  12. package/dist/call.d.ts +27 -2
  13. package/dist/call.d.ts.map +1 -1
  14. package/dist/call.js +22 -0
  15. package/dist/call.js.map +1 -1
  16. package/dist/chains.d.ts +7 -0
  17. package/dist/chains.d.ts.map +1 -1
  18. package/dist/chains.js +11 -0
  19. package/dist/chains.js.map +1 -1
  20. package/dist/deployless.d.ts +1 -1
  21. package/dist/deployless.d.ts.map +1 -1
  22. package/dist/deployless.js +1 -1
  23. package/dist/deployless.js.map +1 -1
  24. package/dist/erc20.d.ts +70 -4
  25. package/dist/erc20.d.ts.map +1 -1
  26. package/dist/erc20.js +121 -5
  27. package/dist/erc20.js.map +1 -1
  28. package/dist/events.d.ts +25 -0
  29. package/dist/events.d.ts.map +1 -0
  30. package/dist/events.js +25 -0
  31. package/dist/events.js.map +1 -0
  32. package/dist/fallback.d.ts +54 -3
  33. package/dist/fallback.d.ts.map +1 -1
  34. package/dist/fallback.js +273 -21
  35. package/dist/fallback.js.map +1 -1
  36. package/dist/index.d.ts +10 -8
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +7 -5
  39. package/dist/index.js.map +1 -1
  40. package/dist/multicall.d.ts.map +1 -1
  41. package/dist/multicall.js +33 -2
  42. package/dist/multicall.js.map +1 -1
  43. package/dist/nft.d.ts +5 -0
  44. package/dist/nft.d.ts.map +1 -1
  45. package/dist/nft.js +5 -0
  46. package/dist/nft.js.map +1 -1
  47. package/dist/owner.d.ts +145 -0
  48. package/dist/owner.d.ts.map +1 -0
  49. package/dist/owner.js +510 -0
  50. package/dist/owner.js.map +1 -0
  51. package/dist/provider.d.ts +63 -18
  52. package/dist/provider.d.ts.map +1 -1
  53. package/dist/provider.js +166 -100
  54. package/dist/provider.js.map +1 -1
  55. package/dist/scan.d.ts +59 -0
  56. package/dist/scan.d.ts.map +1 -0
  57. package/dist/scan.js +245 -0
  58. package/dist/scan.js.map +1 -0
  59. package/dist/shortcuts.d.ts +34 -35
  60. package/dist/shortcuts.d.ts.map +1 -1
  61. package/dist/shortcuts.js +41 -73
  62. package/dist/shortcuts.js.map +1 -1
  63. package/dist/source.d.ts +6 -2
  64. package/dist/source.d.ts.map +1 -1
  65. package/dist/source.js +49 -6
  66. package/dist/source.js.map +1 -1
  67. package/dist/subpaths/nft.d.ts +46 -0
  68. package/dist/subpaths/nft.d.ts.map +1 -0
  69. package/dist/subpaths/nft.js +55 -0
  70. package/dist/subpaths/nft.js.map +1 -0
  71. package/dist/subpaths/owner.d.ts +33 -0
  72. package/dist/subpaths/owner.d.ts.map +1 -0
  73. package/dist/subpaths/owner.js +34 -0
  74. package/dist/subpaths/owner.js.map +1 -0
  75. package/dist/tron.d.ts +2 -0
  76. package/dist/tron.d.ts.map +1 -1
  77. package/dist/tron.js +5 -0
  78. package/dist/tron.js.map +1 -1
  79. package/dist/util.d.ts +5 -0
  80. package/dist/util.d.ts.map +1 -1
  81. package/dist/util.js +23 -0
  82. package/dist/util.js.map +1 -1
  83. package/dist/watch.d.ts +25 -0
  84. package/dist/watch.d.ts.map +1 -0
  85. package/dist/watch.js +105 -0
  86. package/dist/watch.js.map +1 -0
  87. package/package.json +19 -10
  88. package/src/aggregate.ts +177 -9
  89. package/src/batcher.ts +69 -9
  90. package/src/call.ts +42 -2
  91. package/src/chains.ts +15 -0
  92. package/src/deployless.ts +1 -1
  93. package/src/erc20.ts +171 -8
  94. package/src/events.ts +42 -0
  95. package/src/fallback.ts +306 -22
  96. package/src/index.ts +20 -27
  97. package/src/multicall.ts +34 -2
  98. package/src/nft.ts +5 -0
  99. package/src/owner.ts +660 -0
  100. package/src/provider.ts +255 -112
  101. package/src/scan.ts +293 -0
  102. package/src/shortcuts.ts +76 -88
  103. package/src/source.ts +51 -8
  104. package/src/subpaths/nft.ts +107 -0
  105. package/src/subpaths/owner.ts +69 -0
  106. package/src/tron.ts +7 -0
  107. package/src/util.ts +27 -0
  108. package/src/watch.ts +149 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### 不兼容的改动
6
+
7
+ - 代币信息缓存与 solana-batch-call 对齐:最多 5 万个代币(超出淘汰最早的),name / symbol 缓存 1 小时后重查(可升级合约可能改名);decimals 仍永久缓存
8
+ - 单节点也经过 `FallbackRpc`(统一触发 onRequest 事件),`provider.rpc` 始终是 `FallbackRpc`,底层节点在 `rpc.nodes[0]`;错误仍是节点的原始错误
9
+ - NFT 查询和资产列表移到子路径,不用时打包不再带上(拆出后约少 15KB;只用 `getBalances` 时 minify 后约 44KB,含本版本新增功能,不含 ethers):
10
+ - `@w3lib/evm-batch-call/nft`:`getNftCollections` / `getNftBalances` / `getNftOwners` / `getNftTokenUris` / `getErc1155Balances`、`ERC721_ABI` / `ERC1155_ABI` 及相关类型
11
+ - `@w3lib/evm-batch-call/owner`:`getOwnerTokens`、代币来源(`metamaskTokenList` / `alchemy` / `combine` …)、`defillamaPrices`、`transferScan` / `keyValueScanStorage` 等
12
+ - 删除 `Provider` 上的 `nftCollections` / `nftBalances` / `nftOwners` / `nftTokenUris` / `erc1155Balances` / `ownerTokens` 方法,改用子路径里接收 Provider 的同名函数:`multi.nftOwners(items)` → `nftOwners(multi, items)`
13
+ - `balances` / `tokens` / `allowances` 的子调用改走自动合并队列(见下),与同一时刻的其他查询合并成一次 `eth_call`;结果不变,但请求会推迟到 `batch.wait` 收集窗口结束(默认同一 tick)
14
+
15
+ ### 新增
16
+
17
+ - `nativeBalance` 配置:`'contract'`(默认)| `'rpc'`(主币单独走 `eth_getBalance`)| `{ erc20 }`(主币改调 ERC20 的 `balanceOf`),对 `balances`、`getEthBalance`、`all` / `call` / `staticCall` 都生效;内置表 `NATIVE_BALANCE_MODES` 收录合约里读主币余额恒为 0 的链(Anubis 6714 默认 `'rpc'`)
18
+ - `pnpm check:rpc` 比对每条链“合约里读到的主币余额”(Multicall3 / deployless)与 `eth_getBalance`,不一致且没有配置时报错
19
+ - `balances` / `allowances`(及 `getBalances` / `getAllowances`)的 `decimals: false` 选项:只查余额 / 额度,结果不带 `decimals` / `formatted`(返回类型 `RawTokenBalance` / `RawTokenAllowance`);没有 `decimals` 的合约(ERC721)也能拿到余额
20
+ - `balances` / `tokens` / `allowances` 的失败项带 `error`(`invalid-address` / `invalid-argument` / `no-contract` / `reverted` / `decode-failed` / `not-configured`)和 `errorField`(哪一项没读到)
21
+ - `balances` / `tokens` / `allowances` 参与自动合并:不同组件同时发起的查询、单条 `await` 调用合成一次 `eth_call` 并去重;一次调用的子调用不会被 `batch.maxSize` 拆开
22
+ - `fallback.stallTimeout`:当前节点超过这个时间未返回时同时请求下一个节点,先成功的胜出(与 ethers `FallbackProvider` 的 stallTimeout 相同,没有首次请求前的全节点同步);默认 0 不启用
23
+ - 快捷函数:`[window.ethereum, ...urls]` 这类钱包 + URL 混合列表也复用 Provider 实例;`provider` 参数可以直接传 `Provider` 实例
24
+ - 所有节点都失败时抛 `AllNodesFailedError`,带每个节点的标识(URL 只取 host)和失败原因;单节点时仍抛原始错误
25
+ - 超时与出错分开计:超时只换节点、不冷却,同一节点连续 3 次超时才冷却;节点冷却状态按节点(URL / 对象)全局共享,Provider 重建后仍有效
26
+ - `onRequest(listener)`:订阅所有节点请求(节点、方法、耗时、成败、第几个节点),用于观察故障切换和耗时
27
+ - 所有查询支持 `signal`(AbortSignal):已取消时不发请求,查询中取消立即 reject
28
+ - `withBlock`:`balances` / `tokens` / `allowances` 的结果带读取时的区块号(同一次 `eth_call` 里读出;deployless 合约新增对 `getBlockNumber()` 的支持)
29
+ - `minBlock`:节点落后于指定区块时按该区块重查、自动换节点(落后不进入冷却),所有节点都落后时稍等重试(约 10 秒,可用 signal 取消),用于交易刚确认后刷新余额;Tron 上等节点跟上后查最新状态
30
+ - `getMultiBalances` / `multiBalances`:多个钱包一次查,同一条链上合成一次 multicall
31
+ - `watchBalances`:轮询余额,多处订阅同一份数据时合并成一份轮询,只在余额变化时通知
32
+ - 代币信息缓存可持久化:`persistTokenMetaCache(localStorage)`、`exportTokenMetaCache` / `importTokenMetaCache`
33
+ - `formatUnits`(与 ethers 同名);`formatAmount` 保留为别名并标记 deprecated
34
+
35
+ - Multicall3 地址表补充 23 条链(2026-10 逐条在链上实测 aggregate3 可用):Cronos、Velas、Unichain、Monad、opBNB、Boba、KCC、Astar、Stable、HyperEVM、Core、Morph、Robinhood Chain、IoTeX、Plasma、Mode、Ink、Linea、Berachain、Scroll、Winchain、Oasis Emerald、Aurora。其中 Oasis Emerald、Aurora 不支持 deployless,原来在这两条链上批量查询会失败
36
+ - `pnpm check:rpc` 检查每条链的批量调用方式:内置表里的 Multicall3 能否调用 aggregate3、deployless 能否执行,两种都不可用时报错
37
+
38
+ ### 修复
39
+
40
+ - Mantle、Blast 的 Multicall3 地址(来自 ethcall 的表)调用 aggregate3 会 revert,改为标准地址(原来会自动退回 deployless,结果正确但多一次失败的请求)
41
+ - Arbitrum 系的链(Arbitrum One、Robinhood Chain 等)在合约里 `block.number` 是 L1 区块号:`withBlock` / `minBlock` 改为读 ArbSys 的 `arbBlockNumber()`,得到 L2 区块号
42
+ - `all<any>(calls)` / `tryAll<any>(calls)` 显式写 any 时返回 `any[]`,可以按数组解构(原来命中第一个重载,返回值不能解构)
43
+
44
+ ### 文档
45
+
46
+ - 零地址默认按主币处理,在 README 中单独醒目说明,并给出只保留 `0xeeee…eeee` 的配置
47
+
48
+ ## 0.3.0
49
+
50
+ ### 新增
51
+
52
+ - `Provider.getBlockNumber()` / `Provider.getLogs()`;多节点时 getLogs / getBlockNumber 失败不影响 call / getBalance 的节点顺序,getLogs 全部失败抛 `GetLogsError`(带各节点的错误)
53
+ - 资产列表 `getOwnerTokens` / `ownerTokens`:从代币来源发现候选代币,multicall 在链上核对余额,返回持有的代币
54
+ - 代币来源可替换:`metamaskTokenList`、`coingeckoTokenList`、`tokenList(url)`、`staticTokens`、`alchemy({ apiKey })`、`nodereal({ apiKey })`,以及 `firstAvailable` / `combine` 组合;默认 MetaMask 列表 → CoinGecko 列表(免费免 Key)
55
+ - 可选价格:`prices: true` 用 DefiLlama,或传自定义价格源;`minUsd` 过滤
56
+ - 增量扫描开关 `scanTransfers`(默认关闭):从第一次调用开始扫描转入的 ERC20 Transfer 事件,补充列表里没有的新代币;进度可持久化(`keyValueScanStorage` 或自定义存储),自动适配节点的区块范围上限与限频;`timeBudget` 限制每次调用的扫描时间,`confirmations` 避开链重组
57
+ - 局限性见 README「资产列表」;结果不保证完整、准确,因此标记为 `@deprecated`(不推荐使用)
58
+
3
59
  ## 0.2.0
4
60
 
5
61
  ### 不兼容的改动
package/README.md CHANGED
@@ -13,22 +13,15 @@ pnpm add @w3lib/evm-batch-call ethers
13
13
  常用的批量查询都有现成函数,一次请求查完一批,结果都是字符串 / 数字 / 布尔,可以直接 `JSON.stringify`:
14
14
 
15
15
  ```ts
16
- import {
17
- NATIVE_TOKEN,
18
- getAllowances,
19
- getBalances,
20
- getErc1155Balances,
21
- getNftBalances,
22
- getNftCollections,
23
- getNftOwners,
24
- getNftTokenUris,
25
- getTokens,
26
- } from '@w3lib/evm-batch-call'
16
+ import { NATIVE_TOKEN, getAllowances, getBalances, getTokens } from '@w3lib/evm-batch-call'
17
+ import { getErc1155Balances, getNftBalances, getNftCollections, getNftOwners, getNftTokenUris } from '@w3lib/evm-batch-call/nft' // NFT 在子路径
27
18
 
28
19
  // 余额:主币 + 代币,带 decimals 换算
29
20
  await getBalances(user, [NATIVE_TOKEN, USDT], { chainId: 56 })
30
21
  // [{ token: NATIVE_TOKEN, native: true, balance: '1500000000000000000', decimals: 18, formatted: '1.5', success: true }, ...]
31
22
  await getBalances(user, [NATIVE_TOKEN, USDT], { chainId: 56, symbol: true }) // 额外返回 symbol
23
+ await getBalances(user, [BAYC, USDT], { chainId: 1, decimals: false }) // 只查余额,不查 decimals(没有 decimals 的合约也能查)
24
+ // [{ token: BAYC, native: false, balance: '128', success: true }, ...]
32
25
 
33
26
  // 代币详情,字段可选:name / symbol / decimals / totalSupply(默认前三个)
34
27
  await getTokens([USDT, NATIVE_TOKEN], { chainId: 56, fields: ['symbol', 'decimals', 'totalSupply'] })
@@ -46,25 +39,53 @@ await getNftTokenUris([{ contract: BAYC, tokenId: 1 }], { chainId: 1, ipfsGatewa
46
39
  await getErc1155Balances(user, [{ contract: ITEMS, tokenId: 7 }], { chainId: 137 }) // ERC1155 余额
47
40
  ```
48
41
 
49
- | 函数 | Provider 方法 | 说明 |
42
+ | 函数 | 传 Provider 实例的写法 | 说明 |
50
43
  | --- | --- | --- |
51
- | `getBalances(owner, tokens, opts)` | `balances` | 主币 + 代币余额;`symbol: true` 额外返回 symbol |
52
- | `getTokens(tokens, opts)` | `tokens` | ERC20 详情,`fields` 选择返回字段,结果类型随之收窄 |
53
- | `getAllowances(owner, spender, tokens, opts)` | `allowances` | 授权额度;`unlimited` 表示额度 ≥ uint96 最大值(覆盖 MaxUint256 及 UNI / COMP 这类截断为 uint96 的代币) |
54
- | `getNftCollections(contracts, opts)` | `nftCollections` | NFT 集合 standard / name / symbol / totalSupply,`fields` 可选 |
55
- | `getNftBalances(owner, contracts, opts)` | `nftBalances` | ERC721 持有数量 |
56
- | `getNftOwners(items, opts)` | `nftOwners` | ERC721 持有人,可混合多个集合;不存在的 tokenId 为 null;Tron 链上返回 T 开头的地址 |
57
- | `getNftTokenUris(items, opts)` | `nftTokenUris` | 元数据地址:自动兼容 ERC721 `tokenURI` 与 ERC1155 `uri`(`{id}` 按规范替换),`ipfsGateway` 转换 `ipfs://`;首次查询顺带识别集合标准并缓存,之后只发对应的调用 |
58
- | `getErc1155Balances(owner, items, opts)` | `erc1155Balances` | ERC1155 余额 |
44
+ | `getBalances(owner, tokens, opts)` | `multi.balances(owner, tokens, opts)` | 主币 + 代币余额;`symbol: true` 额外返回 symbol;`decimals: false` 只查余额 |
45
+ | `getTokens(tokens, opts)` | `multi.tokens(tokens, opts)` | ERC20 详情,`fields` 选择返回字段,结果类型随之收窄 |
46
+ | `getAllowances(owner, spender, tokens, opts)` | `multi.allowances(...)` | 授权额度;`unlimited` 表示额度 ≥ uint96 最大值(覆盖 MaxUint256 及 UNI / COMP 这类截断为 uint96 的代币);`decimals: false` 只查额度 |
47
+ | `getNftCollections(contracts, opts)` | `nftCollections(multi, contracts, opts)` | **子路径 `/nft`**。NFT 集合 standard / name / symbol / totalSupply,`fields` 可选 |
48
+ | `getNftBalances(owner, contracts, opts)` | `nftBalances(multi, owner, contracts)` | **`/nft`**。ERC721 持有数量 |
49
+ | `getNftOwners(items, opts)` | `nftOwners(multi, items)` | **`/nft`**。ERC721 持有人,可混合多个集合;不存在的 tokenId 为 null;Tron 链上返回 T 开头的地址 |
50
+ | `getNftTokenUris(items, opts)` | `nftTokenUris(multi, items, opts)` | **`/nft`**。元数据地址:自动兼容 ERC721 `tokenURI` 与 ERC1155 `uri`(`{id}` 按规范替换),`ipfsGateway` 转换 `ipfs://`;首次查询顺带识别集合标准并缓存,之后只发对应的调用 |
51
+ | `getErc1155Balances(owner, items, opts)` | `erc1155Balances(multi, owner, items)` | **`/nft`**。ERC1155 余额 |
52
+ | `getOwnerTokens(owner, opts)` | `ownerTokens(multi, owner, opts)` | **子路径 `/owner`**。资产列表:地址持有的代币(**不推荐使用**,结果不保证完整,见[资产列表](#资产列表)) |
53
+
54
+ **子路径**:NFT 查询在 `@w3lib/evm-batch-call/nft`,资产列表(含代币来源、价格源、Transfer 增量扫描)在 `@w3lib/evm-batch-call/owner`。不用这些功能时打包不会带上它们的代码(拆出后约少 15KB:只用 `getBalances` 时 minify 后约 44KB、gzip 约 16KB,均不含 ethers)。
59
55
 
60
56
  共同的规则:
61
57
 
62
- - **主币**:`0xeeee…eeee`(导出为 `NATIVE_TOKEN`,不区分大小写)和零地址按主币处理,不发合约调用;name / symbol / 精度来自内置链信息表(`NATIVE_CURRENCIES`),可用 `nativeTokens` / `nativeName` / `nativeSymbol` / `nativeDecimals` 配置
63
- - **缓存**:decimals / symbol / name / NFT 标准不会变,查过一次后按链缓存(各函数共用),之后只查会变的数据;已知精度可以直接传 `{ address, decimals }`(主币也适用,优先于内置配置)
64
- - **失败**:单项失败(非合约地址、非法地址、不存在的 tokenId 等)不影响其他项,该项 `success: false`;整个请求失败(节点不可用等)才会抛错
65
- - **数值**:原始数值是最小单位的十进制字符串,需要计算时用 `BigInt(x)`;`formatted` 保留全部有效小数位、不四舍五入,整数不带小数点(单独格式化可用 `formatAmount(value, decimals)`)
58
+ - **主币**:`0xeeee…eeee`(导出为 `NATIVE_TOKEN`,不区分大小写)按主币处理,不发合约调用;name / symbol / 精度来自内置链信息表(`NATIVE_CURRENCIES`),可用 `nativeTokens` / `nativeName` / `nativeSymbol` / `nativeDecimals` 配置
59
+ - **⚠ 零地址 `0x0000…0000` 默认也按主币处理**(返回主币余额,不会去调零地址上的 `balanceOf`)。如果你的数据里零地址代表别的含义,传 `nativeTokens: [NATIVE_TOKEN]` 只保留 `0xeeee…eeee`
60
+ - **主币余额的读取方式**:默认和合约调用放在同一次 `eth_call` 里。有的链在合约里读主币余额恒为 0、不报错(如 Anubis),内置表 `NATIVE_BALANCE_MODES` 已为这类链改用 `eth_getBalance`;其他链可用 `nativeBalance: 'rpc'` 或 `{ erc20: '0x…' }` 配置,见[主币余额与 deployless](#主币余额与-deployless)
61
+ - **缓存**:查过一次后按链缓存(各函数共用),之后只查会变的数据。decimals / NFT 标准永久缓存;name / symbol 在可升级合约上可能改,各自缓存 1 小时;最多 5 万个代币,超出淘汰最早的(与 solana-batch-call 一致)。已知精度可以直接传 `{ address, decimals }`(主币也适用,优先于内置配置)。缓存可以持久化,见[缓存持久化](#缓存持久化)
62
+ - **失败**:单项失败(非合约地址、非法地址、不存在的 tokenId 等)不影响其他项,该项 `success: false`;整个请求失败(节点不可用等)才会抛错。`getBalances` / `getTokens` / `getAllowances` 的失败项还带 `error`(`invalid-address` 地址非法、`invalid-argument` 其他参数编码失败、`no-contract` 地址上没有合约、`reverted` 执行 revert、`decode-failed` 返回值无法解码、`not-configured` 没发请求的信息缺失,如主币不在内置链信息表里)和 `errorField`(哪一项没读到,如 `balance` / `decimals`),可以按原因分别处理(如地址非法报错、没有合约按 0)
63
+ - **取消**:所有查询都可以传 `signal`(`AbortController`):已取消时不发请求;查询中取消时立即 reject,合并在一起的底层请求继续完成、结果丢弃。切换代币时不必再手动比对、丢弃旧结果
64
+ - **只查余额**:`decimals: false` 时不查 decimals,结果不带 `decimals` / `formatted`(类型随之变化)。适合没有 `decimals` 的合约(ERC721 的 `balanceOf`)、或只需要原始数值的场景,冷启动时子调用数减半
65
+ - **数值**:原始数值是最小单位的十进制字符串,需要计算时用 `BigInt(x)`;`formatted` 保留全部有效小数位、不四舍五入,整数不带小数点(单独格式化可用 `formatUnits(value, decimals)`;旧名 `formatAmount` 仍可用,已标记 deprecated)
66
66
  - **老代币**:symbol / name 返回 bytes32 的代币(MKR 等)也能解析
67
67
 
68
+ ### 多个钱包、轮询、刚成交后刷新
69
+
70
+ ```ts
71
+ import { getMultiBalances, watchBalances } from '@w3lib/evm-batch-call'
72
+
73
+ // 多个钱包一次查:同一条链上合成一次 multicall
74
+ const [a, b] = await getMultiBalances([{ owner: walletA, tokens: [NATIVE_TOKEN, USDT] }, { owner: walletB, tokens: [USDT] }], { chainId: 56 })
75
+
76
+ // 轮询:多处订阅同一个钱包的同一组代币时合并成一份轮询(代币顺序无关),只在余额变化时通知;返回取消订阅的函数
77
+ const stop = watchBalances(user, [NATIVE_TOKEN, USDT], { chainId: 56, interval: 10_000, onChange: (list, previous) => render(list) })
78
+
79
+ // 交易刚确认:要求读到的数据不早于交易所在区块(公共节点可能还落后一两个块)
80
+ const receipt = await tx.wait()
81
+ await getBalances(user, tokens, { chainId: 56, minBlock: receipt.blockNumber })
82
+ await getBalances(user, tokens, { chainId: 56, withBlock: true }) // 每项带 blockNumber:读取时的区块号
83
+ ```
84
+
85
+ - **`withBlock`**:区块号与数据在同一次 `eth_call` 里读出(Multicall3 的 `getBlockNumber`;deployless 合约里直接取 `block.number`),不多发请求。`balances` / `tokens` / `allowances` 及对应函数都支持
86
+ - **`minBlock`**:先按最新状态查并顺带读区块号;节点落后时按 `minBlock` 指定区块重查——落后的节点对未来区块报错,自动换下一个节点(落后不算故障,不进入冷却);所有节点都落后时每秒重试,最多约 10 秒后抛错,传了 `signal` 时取消后立即停止。重查得到的是 `minBlock` 那个区块的状态(已包含刚确认的交易)。只对最新状态的查询生效(指定了 `blockTag` 时忽略)。Tron 节点只能查最新状态,落后时等节点跟上再查。`nativeBalance: 'rpc'` 时主币余额在批量查询实际读取的区块上用 `eth_getBalance` 读取,与代币余额、`blockNumber` 一致
87
+ - **`watchBalances`**:相同的节点参数 + 钱包 + 代币 + 选项共用一份轮询(多处订阅时取最小的 `interval`),新订阅者立即拿到最近一次的结果;查询失败调用 `onError`,轮询继续;上一次查询没完成时不会重叠发起;最后一个订阅者取消后停止
88
+
68
89
  ### 节点参数
69
90
 
70
91
  以上函数的节点参数相同,`chainId` 和 `provider` 至少传一个:
@@ -82,7 +103,8 @@ await getBalances(user, tokens, { chainId: 56, blockTag: 'pending' })
82
103
  - **chainId 自动识别**:EVM 节点 / 钱包用 `eth_chainId`,ethers Provider 用 `getNetwork()`,Tron 读创世区块哈希的最后 4 字节(与 TronGrid 的 `eth_chainId` 一致);只传 URL 时先按 EVM 识别,失败再按 Tron 识别,所以 Tron 节点 URL 也不用传 chainId
83
104
  - **识别结果缓存**:同时发起的识别共用一个请求,失败或超时不缓存(下次重试)。URL 按地址缓存;tronWeb 按当前连接的节点地址缓存(TronLink 切网络后自动重新识别);EIP-1193 钱包按对象缓存并监听 `chainChanged`,切链时失效,不支持事件监听的钱包不缓存。`clearChainIdCache()` 可手动清空
84
105
  - **链校验**:传入的节点会确认它实际所在的链与 chainId 一致,不一致时报 `NETWORK_ERROR`,多节点时自动换下一个节点。所以 `[钱包, 公共节点]` 这类组合、配错了节点 URL、或用户自己创建的 `BrowserProvider` 在别的链上,都不会把另一条链的数据当成这条链的返回。校验通过已配置好的节点连接进行(带 Tron apiKey 等),与实际请求并行发出、不增加延迟,结果缓存,每个节点只多一次请求;内置公共节点不校验
85
- - **Provider 复用**:同一个 chainId + 配置 + 节点多次调用会复用同一个 Provider(共享连接和合并队列,最多缓存 32 组,超出淘汰最早的);只传钱包时每次按钱包当前的链创建,切链后自动按新链查询。`chainId` 为 `null` / `''` 时视为未传
106
+ - **Provider 复用**:同一个 chainId + 配置 + 节点多次调用会复用同一个 Provider(共享连接和合并队列,最多缓存 32 组,超出淘汰最早的);`[window.ethereum, ...urls]` 这类钱包 + URL 混合列表按“钱包对象 + URL”复用。只传钱包(没传 chainId)时每次按钱包当前的链创建,切链后自动按新链查询。`chainId` 为 `null` / `''` 时视为未传
107
+ - **直接传 Provider 实例**:`{ provider: multi }` 时直接用这个实例(chainId 和配置以实例为准,同时再传会报错),与其他直接调用 `multi` 的代码共享合并队列
86
108
  - 其余 Provider 配置(`fallback`、`nativeSymbol` 等,见[配置](#配置))也可以直接放进这个参数
87
109
 
88
110
  ### 合约调用
@@ -126,14 +148,104 @@ const router = multi.contract(routerAddress, RouterABI)
126
148
  const amountOut = await router.swap.staticCall(params, { value, from: user })
127
149
  ```
128
150
 
151
+ ## 资产列表
152
+
153
+ > **不推荐使用**(已标记 `@deprecated`):`getOwnerTokens` / `ownerTokens` 只能尽力而为,结果不保证完整、准确,局限性见下文。已知要查哪些代币时请用 `getBalances`;传 `alchemy` / `nodereal` 来源能查全,但同样受第三方服务的额度和可用性约束,需要可靠的完整持仓请直接接入索引服务的接口或自己的索引。
154
+
155
+ EVM 链上**没有办法只靠节点列出一个地址持有的全部代币**(链上没有“某地址持有哪些代币”的索引)。`getOwnerTokens` 的做法是:先从“代币来源”拿到候选代币,再用 multicall 在链上核对这些代币的余额,返回有余额的部分。
156
+
157
+ ```ts
158
+ import { alchemy, getOwnerTokens, nodereal, tokenList } from '@w3lib/evm-batch-call/owner'
159
+
160
+ await getOwnerTokens(user, { chainId: 56 }) // 默认:免费公开代币列表
161
+ await getOwnerTokens(user, { chainId: 1, prices: true, minUsd: 1 }) // 带美元价值,按价值排序,过滤 < $1 的代币
162
+ await getOwnerTokens(user, { chainId: 56, source: alchemy({ apiKey: KEY }) }) // 用 Alchemy 查全部历史持仓(需要 Key)
163
+ // [
164
+ // { token: NATIVE_TOKEN, native: true, symbol: 'BNB', formatted: '1.5', price: 600, value: 900, source: 'native', … },
165
+ // { token: '0x…', native: false, symbol: 'CAKE', formatted: '120', price: 2, value: 240, source: 'metamask', logo: '…', … },
166
+ // ]
167
+ ```
168
+
169
+ ### 代币来源
170
+
171
+ | 来源 | 需要 Key | 能发现什么 | 说明 |
172
+ | --- | --- | --- | --- |
173
+ | `metamaskTokenList()`(默认首选) | 否 | MetaMask 汇总的代币列表(Ethereum 约 9500 个、BSC 约 2 万个),默认只用至少 3 家来源收录的 | MetaMask 自用接口,非对外承诺的公开 API |
174
+ | `coingeckoTokenList()`(默认备用) | 否 | CoinGecko 收录的代币(Ethereum 约 6000 个、BSC 约 4500 个) | 覆盖 26 条链,见 `COINGECKO_PLATFORMS` |
175
+ | `tokenList(url)` | 否 | 任意 Uniswap Token List 格式的列表(自己维护的、PancakeSwap 等) | |
176
+ | `staticTokens(list)` | 否 | 固定的代币地址 | 如之前发现过、存下来的代币 |
177
+ | `alchemy({ apiKey })` | **是** | 地址的全部历史持仓 | 19 条链,见 `ALCHEMY_NETWORKS`;可用 `urls: { [chainId]: url }` 按链覆盖地址 |
178
+ | `nodereal({ apiKey })` | **是** | 地址的全部历史持仓 | 仅 BSC、Ethereum;同样支持 `urls` |
179
+
180
+ - 默认来源是 `firstAvailable(metamaskTokenList(), coingeckoTokenList())`:MetaMask 列表不支持这条链、为空或请求失败时,用 CoinGecko 列表
181
+ - 来源可以组合:`firstAvailable(a, b)` 依次尝试,`combine(a, b)` 合并去重(单个来源出错不影响其他来源),例如 `combine(defaultTokenSource(), staticTokens(savedTokens))`
182
+ - **Key 只在调用时传入**:库里没有内置任何 Key,`alchemy()` / `nodereal()` 不传 `apiKey` 会直接报错,不传就不能用这两个来源。出错信息里不会包含 Key。在浏览器里使用时 Key 会暴露给用户,建议在服务端调用,或使用对方控制台里的域名白名单
183
+ - 不管代币从哪个来源发现,**余额和精度都由本库在链上用 multicall 核对**(来源返回的余额不使用,来源给错的精度会被链上的值纠正),链上读不到精度的合约(不是正常的 ERC20)不返回
184
+ - 主币用 Provider 配置的 `nativeTokens` 里的第一个地址查询(默认 `0xeeee…eeee`),名称 / symbol 同样遵循 `nativeName` / `nativeSymbol` 配置;列表里出现的主币映射地址(如 Polygon 的 `0x…1010`、zkSync 的 `0x…800A`)会被排除,不会把主币算两次
185
+ - 公开代币列表按 URL 缓存 1 小时;`clearTokenListCache()` 可手动清空
186
+
187
+ ### 增量扫描(可选)
188
+
189
+ 代币列表发现不了的新代币(刚发行、未被收录),可以用 `scanTransfers` 开关补充:开启后,从**第一次调用开始**,每次调用都往后扫描新区块里转给该地址的 ERC20 `Transfer` 事件,把发现的代币合并进来(结果的 `source` 为 `'transfers'`)。默认关闭。
190
+
191
+ ```ts
192
+ import { getOwnerTokens, keyValueScanStorage } from '@w3lib/evm-batch-call/owner'
193
+
194
+ await getOwnerTokens(user, { chainId: 1, scanTransfers: true }) // 进度存在内存里
195
+ await getOwnerTokens(user, { chainId: 1, scanTransfers: { storage: keyValueScanStorage(localStorage) } }) // 刷新页面后接着扫
196
+ await getOwnerTokens(user, { chainId: 1, scanTransfers: { lookbackBlocks: 20_000 } }) // 第一次额外往回扫一段
197
+ ```
198
+
199
+ | 选项 | 默认 | 说明 |
200
+ | --- | --- | --- |
201
+ | `storage` | 内存 | 扫描进度(起始区块、已扫到的区块、发现的代币)。内存存储在进程 / 页面结束后丢失;`keyValueScanStorage(localStorage)` 或自己实现 `{ get, set }`(如存数据库)可以持久化 |
202
+ | `lookbackBlocks` | 0 | 第一次调用时往回扫的区块数(0 表示只从现在开始) |
203
+ | `blockRange` | 1000 | 单次 `eth_getLogs` 的最大区块范围。节点有上限时按报错里的上限自动缩小(报错里没写上限就减半试),记下的上限 1 天后失效再重新试;偶发错误导致缩小后,连续成功会逐步放大回去 |
204
+ | `maxRequests` | 20 | 每次调用最多发多少个 `eth_getLogs`;落后很多时分多次追上 |
205
+ | `timeBudget` | 5000 | 每次调用的扫描时间预算(毫秒),超过后不再发新请求,留到下次调用 |
206
+ | `confirmations` | 5 | 只扫到最新区块往前这么多个区块,避开链重组和节点之间的高度差(否则可能漏掉刚出的区块) |
207
+
208
+ 出错时的处理:所有节点都明确不开放 `eth_getLogs` 时暂停 10 分钟;能用的节点只是限频 / 超时时不缩小范围,退避后重试(连续 3 次失败留到下次调用);有节点报范围超限时缩小范围。存储里的进度格式不对(如旧版本留下的)会被丢弃,重新开始。
209
+
210
+ 也可以单独当作代币来源使用:`combine(defaultTokenSource(), transferScan({ storage }))`;`getTransferScanState(chainId, owner, storage)` 读取扫描进度。
211
+
212
+ **增量扫描的局限**:
213
+
214
+ - **只能发现开始扫描之后收到的代币**(以及 `lookbackBlocks` 范围内的)。更早收到、又不在代币列表里的代币仍然查不到;需要完整历史请用 `alchemy` / `nodereal`
215
+ - **需要节点支持 `eth_getLogs`**,免费公共节点限制很多:BSC 上内置节点只有 blockrazor 可用,且每次最多 25 个区块(会自动适配),连续请求还会被限频;节点完全不支持时会暂停 10 分钟再试。持续使用建议传自己的节点
216
+ - **进度默认只在内存里**:页面刷新、服务重启后会从头开始(重新记下当前区块),需要持久化请传 `storage`
217
+ - **会增加 `getOwnerTokens` 的耗时**:扫描和余额查询一起等待,每次调用通常多几百毫秒到 `timeBudget`(默认 5 秒),最坏再加上一个请求的超时时间(节点超时默认 10 秒)。对耗时敏感时可以调小 `timeBudget` / `maxRequests`,或单独用 `transferScan` 在后台定期扫描、展示时只读进度
218
+ - **很久没调用时追赶较慢**:每次最多 `maxRequests` 个请求、`timeBudget` 时间(如 BSC 上 20 × 25 = 500 个区块),落后很多时需要多次调用才能追上
219
+ - **最新几个区块里收到的代币要等下一次调用**(`confirmations`,默认 5 个区块)
220
+ - 扫描到的代币不经过任何列表筛选,**大部分会是空投垃圾币**,建议配合 `prices` + `minUsd` 使用
221
+ - 只看转入(`to` 为该地址)的 ERC20 `Transfer` 事件;不发标准 `Transfer` 事件的代币发现不了;Tron 不支持
222
+
223
+ ### 价格
224
+
225
+ `prices: true` 用 DefiLlama 查美元价格(免费、不需要 Key),结果带 `price` / `value`,并按价值从高到低排序;`minUsd` 只保留价值不低于它的代币(没有价格的代币也会被去掉,主币始终保留)。只传 `minUsd` 时会自动开启 DefiLlama 价格;与 `prices: false` 同时使用会报错。也可以传自己的价格源:`prices: { name, prices: async ({ chainId, tokens, native }) => Map }`。
226
+
227
+ ### 局限性
228
+
229
+ 使用前请了解:
230
+
231
+ 1. **默认来源只能发现公开列表里的代币**。刚发行、还没被列表收录的代币(如新上线的 meme 币)、小众代币查不到;需要完整持仓时,传 `alchemy({ apiKey })` 或 `nodereal({ apiKey })`
232
+ 2. **默认来源依赖第三方免费服务**(MetaMask、CoinGecko 的列表,DefiLlama 的价格):它们可能限流、变更格式或停止服务;MetaMask 列表接口不是对外承诺的公开 API。生产环境建议组合多个来源,或使用自己维护的列表(`tokenList(url)`)
233
+ 3. **列表里也有垃圾币和零头**。默认只用至少 3 家来源收录的代币来减少垃圾币,但仍会有不少价值很低的代币;需要干净的列表时开启 `prices` 并设置 `minUsd`
234
+ 4. **价格不一定可信**。流动性很差的代币价格可能严重虚高(实测有空投代币被估值数十万美元),默认已过滤 DefiLlama 置信度低于 0.9 的价格,但展示总资产时仍建议使用自己的价格源或再做校验
235
+ 5. **需要查几千到两万个代币的余额**,耗时几秒:实测 Ethereum(约 9500 个代币)首次约 11 秒、之后约 6 秒,BSC(约 4800 个)首次约 8 秒、之后约 4 秒(首次要在链上核对持有代币的精度,之后有缓存)。大量使用时建议缓存结果;列表很大时也会占用较多节点请求
236
+ 6. **只包含 ERC20 代币**,不含 NFT(NFT 请用 `getNftBalances` 等);Tron 链没有默认来源,需要自己传 `source`
237
+ 7. 使用 Alchemy / NodeReal 时受它们的免费额度和限频约束,翻页次数默认最多 20 页(2000 个代币),可用 `maxPages` 调整
238
+ 8. 开启 `scanTransfers` 时另有局限,见上面「增量扫描」
239
+
129
240
  ## 功能
130
241
 
131
- - **常用查询**:余额、代币详情、授权额度、NFT(集合 / 持有 / 元数据地址 / ERC1155 余额),一个函数一次请求
242
+ - **常用查询**:余额、代币详情、授权额度、NFT(集合 / 持有 / 元数据地址 / ERC1155 余额,子路径 `/nft`),一个函数一次请求
243
+ - **资产列表**(不推荐使用,尽力而为,子路径 `/owner`):`getOwnerTokens`,免费公开代币列表或带 Key 的索引服务发现代币,multicall 核对余额,可选 DefiLlama 价格
132
244
  - **批量读取**:`all` / `tryAll` / `tryEach`,N 条读调用合成一次 `eth_call`;支持数组或对象输入
133
- - **自动合并**:绑定合约直接 `await`,或 `provider.call(x)`,同一收集窗口内的调用合并成一次请求并去重
245
+ - **自动合并**:绑定合约直接 `await`、`provider.call(x)`,以及 `balances` / `tokens` / `allowances`,同一收集窗口内的调用合并成一次请求并去重
134
246
  - **预执行**:`staticCall` / `staticCallAll` / `method.staticCall`,带 `from` / `value` 模拟交易,返回解析好的 revert 原因
135
247
  - **主币余额合并**:`getEthBalance` 与合约调用在同一次 `eth_call` 里——无论链上有没有 Multicall3
136
- - **任意链可用**:内置 Multicall3 地址表;表里没有、地址无效、或查询早于部署区块时,自动改用 deployless(链上无需部署任何合约)
248
+ - **任意链可用**:内置 Multicall3 地址表;表里没有、地址无效、或查询早于部署区块时,自动改用 deployless(链上无需部署任何合约)。少数链不支持 deployless(合约创建式 `eth_call`,如 Oasis Emerald、Aurora),这些链已在地址表里,只能查合约部署之后的状态;`pnpm check:rpc` 会检查内置节点表里每条链至少有一种批量方式可用
137
249
  - **备用节点**:传多个节点自动故障切换;不传节点使用内置公共节点表(34 条 EVM 链 + Tron)
138
250
  - **chainId 可选**:传了节点就能自动识别 chainId,结果缓存,钱包切链自动失效
139
251
  - **Tron**:走 Tron HTTP API,T 开头地址可以直接用
@@ -154,10 +266,40 @@ new Provider(56, DEFAULT_RPC_URLS[56], { fallback: { timeout: 8000, cooldown: 60
154
266
 
155
267
  多节点时按顺序使用:出错或超时(默认 10s)换下一个节点,出错的节点在 `cooldown`(默认 30s)内排到最后。与 ethers 的 `FallbackProvider` 相比,没有首次请求前对所有节点的同步,一个卡住的节点不会拖住第一次查询;合约 revert 等确定性结果不会拿去别的节点重试。
156
268
 
269
+ **超时与出错分开计**:出错(连不上、5xx、限频等)的节点进入冷却;超时只换下一个节点、不冷却,同一节点连续 3 次超时才冷却——冷启动的大批量查询偶尔慢,不会把健康的节点踢到后面。冷却状态按节点(URL / 对象)全局共享,Provider 重建后刚失败过的节点仍排在后面。
270
+
271
+ **所有节点都失败**时抛 `AllNodesFailedError`,`err.errors` 是每个节点的标识和失败原因(`[{ node, error }]`,按尝试顺序),message 里也逐个列出;节点标识只取 URL 的 host,不含 path / query 里的 API Key。单节点时抛原始错误。
272
+
273
+ **可观测性**:`onRequest(listener)` 订阅所有节点请求(全局,返回取消订阅的函数),每次对节点的请求一条事件:
274
+
275
+ ```ts
276
+ import { onRequest } from '@w3lib/evm-batch-call'
277
+
278
+ const off = onRequest(({ node, method, ms, ok, error, attempt }) => {
279
+ // node:节点 host(钱包为 'wallet');method:call / getBalance / getLogs …;attempt > 0 表示换过节点
280
+ console.log(`${node} ${method} ${ms}ms ${ok ? 'ok' : 'failed'}${attempt ? ` (第 ${attempt + 1} 个节点)` : ''}`)
281
+ })
282
+ ```
283
+
284
+ **慢节点**:`fallback.stallTimeout`(毫秒,默认 0 关闭)设置后,当前节点超过这个时间还没返回就同时请求下一个节点,原请求不取消,谁先成功用谁(与 `FallbackProvider` 的 stallTimeout 语义相同);`timeout` 才是真正放弃。节点正常但偶尔较慢时,用 `{ stallTimeout: 2000, timeout: 10_000 }` 比把 `timeout` 设短更好:大批量查询本来就要几秒,`timeout` 太短会把正常的慢请求判成超时、反复重试。只影响 `eth_call` / `eth_getBalance`。
285
+
157
286
  ethers 会把 `eth_call` 的**任何** JSON-RPC 错误(限流、`header not found` 等)都包装成 `CALL_EXCEPTION`,库内用 `isExecutionError(err)` 区分“合约执行结果”和“节点问题”,只有后者会触发切换。
158
287
 
159
288
  内置节点表 `DEFAULT_RPC_URLS`(按 chainId)/ `DEFAULT_TRON_HOSTS` 中的节点都能在浏览器里直连:放行跨域、支持 batch 请求、chainId 正确、不需要 API Key。公共节点随时可能下线或限流,生产环境建议传入自己的节点,内置表作为备用。
160
289
 
290
+ ## 缓存持久化
291
+
292
+ decimals 永远不会变,但页面刷新后缓存就没了,冷启动的大批量查询要顺带查精度,耗时能差好几倍。可以把缓存持久化:
293
+
294
+ ```ts
295
+ import { exportTokenMetaCache, importTokenMetaCache, persistTokenMetaCache } from '@w3lib/evm-batch-call'
296
+
297
+ persistTokenMetaCache(localStorage) // 调用时读入,之后有更新就防抖(1 秒)写回,默认最多 5000 条;返回停止函数
298
+ // 或自己存:IndexedDB、服务端……
299
+ await save(JSON.stringify(exportTokenMetaCache()))
300
+ importTokenMetaCache(JSON.parse(await load())) // 格式不对的数据忽略;name / symbol 仍按写入时间 1 小时过期
301
+ ```
302
+
161
303
  ## 批量读取
162
304
 
163
305
  ```ts
@@ -173,6 +315,7 @@ await multi.all(calls, { blockTag: 40_000_000 }) //
173
315
  ```
174
316
 
175
317
  - 单返回值的函数直接返回该值;多返回值返回 ethers `Result`(可按下标或名字取);整数统一是 `bigint`
318
+ - 结果类型按输入推断(元组保留各项类型);ethcall 风格的 `all<T>(calls)` 返回 `T[]`,`all<any>(calls)` 返回 `any[]`
176
319
  - `tryAll` 中出现非法地址(如后端给的脏数据)只让该条为 `null`,不会拖垮整批
177
320
  - `Contract` 暴露 ABI 里的**所有**函数(不只 view/pure),重载函数用完整签名访问:`c['getAmount(uint256,address)'](...)`
178
321
 
@@ -191,6 +334,7 @@ const b = multi.erc20(tokenB).balanceOf(user)
191
334
  - `wait`:收集窗口,默认 `0`(同一事件循环 tick);`maxSize`:达到即发出,默认 500
192
335
  - 单条失败只 reject 自己(`CallFailedError`,`err.reason` 是 revert 原因)
193
336
  - blockTag / from 不同的调用分开发
337
+ - `balances` / `tokens` / `allowances` 的子调用也进同一个队列:不同组件同时查余额时合成一次 `eth_call`,相同的 `balanceOf` / `decimals` 只请求一次;一次调用的子调用不会被 `maxSize` 拆开
194
338
  - 合并发生在**同一个 Provider 实例**内,应按链缓存 Provider,不要每次 `new`
195
339
  - 绑定合约的 Call 只在第一次 `await` 时才发请求,传给 `all` / `tryAll` 不会额外发请求
196
340
  - 末位参数可以带 overrides:`await pair.getReserves({ blockTag: 18_000_000 })`。传给 `all` / `tryAll` 时以批次的 overrides 为准(一批只能在同一个区块、同一个 from 下执行)
@@ -218,11 +362,20 @@ for (const r of results) {
218
362
 
219
363
  ## 主币余额与 deployless
220
364
 
221
- `getEthBalance` 在两种模式下都和合约调用在同一次 `eth_call` 里:
365
+ `getEthBalance`(以及 `balances` 里的主币)默认在两种模式下都和合约调用在同一次 `eth_call` 里:
222
366
 
223
367
  - 有 Multicall3:调合约自带的 `getEthBalance`
224
368
  - deployless:使用本库的 [DeploylessMulticall3](contracts/DeploylessMulticall3.sol),构造函数里直接用 `BALANCE` 读取
225
369
 
370
+ **有的链在合约里读不到正确的主币余额**:比如 Anubis(6714)的主币其实是一个 ERC20,`getEthBalance` 和 `BALANCE` 对任何地址都返回 0、不报错,只有 `eth_getBalance` 正确。这时 `balances()` 会返回 `success: true, balance: '0'`,调用方察觉不到。用 `nativeBalance` 配置读取方式:
371
+
372
+ ```ts
373
+ new Provider(6714, url, { nativeBalance: 'rpc' }) // 主币单独走 eth_getBalance(与 eth_call 同时发出)
374
+ new Provider(chainId, url, { nativeBalance: { erc20: '0x…' } }) // 主币改调这个 ERC20 的 balanceOf,仍在同一次 eth_call 里
375
+ ```
376
+
377
+ 内置表 `NATIVE_BALANCE_MODES` 收录了已知的这类链(目前是 Anubis),默认按表处理。`pnpm check:rpc` 会对每条链比对“合约里读到的主币余额”和 `eth_getBalance`,新链有这个问题时会报出来。
378
+
226
379
  deployless 还修正了 ethcall 自带字节码的两个上限问题(在 BSC 上实测,约 150 条 `balanceOf` 起就会失败):
227
380
 
228
381
  | 限制 | 现象 | 处理 |
@@ -276,12 +429,13 @@ new Provider(chainId, nodes?, { // 或 new Provider(nodes, { … })
276
429
  deployless: false, // true:强制走 deployless
277
430
  chunkSize: 500, // 合约模式单次 eth_call 最多打包的调用数
278
431
  batch: { wait: 0, maxSize: 500 }, // 自动合并参数
279
- fallback: { timeout: 10_000, cooldown: 30_000 }, // 节点超时 / 出错节点冷却时间
432
+ fallback: { timeout: 10_000, cooldown: 30_000, stallTimeout: 0 }, // 节点超时 / 出错节点冷却时间(超时连续 3 次才冷却)/ 慢节点提前请求下一个
280
433
  tron: { apiKey, minInterval: 200 }, // 用 URL 创建 TronProvider 时的参数
281
434
  nativeTokens: [NATIVE_TOKEN, ZeroAddress], // 视为主币的地址(默认值;ZeroAddress 来自 ethers)
282
435
  nativeDecimals: 18, // 主币精度 / symbol / 名称,默认按内置链信息表(NATIVE_CURRENCIES)
283
436
  nativeSymbol: 'BNB',
284
437
  nativeName: 'BNB',
438
+ nativeBalance: 'contract', // 主币余额读取方式:'contract' | 'rpc' | { erc20 },默认按内置表 NATIVE_BALANCE_MODES
285
439
  })
286
440
  ```
287
441
 
@@ -7,6 +7,7 @@ pragma solidity 0.8.30;
7
7
  * 与 ethcall 自带的 deployless 字节码相比:
8
8
  * - 对 `MULTICALL3.getEthBalance(addr)` 的调用在构造函数里直接用 BALANCE 读取,
9
9
  * 主币余额和合约调用在同一次 eth_call 里完成(链上没有 Multicall3 时也一样)
10
+ * - 对 `MULTICALL3.getBlockNumber()` 的调用直接返回 block.number(与 Multicall3 相同),用于确认读取时的区块
10
11
  * - 结果超过 EIP-170 的 24KB 时,合约创建会因 "max code size exceeded" 失败,
11
12
  * 此时改为通过 revert Aggregate3Result(...) 带回(revert 数据没有大小限制)
12
13
  *
@@ -28,6 +29,8 @@ contract DeploylessMulticall3 {
28
29
  address private constant MULTICALL3 = 0xcA11bde05977b3631167028862bE2a173976CA11;
29
30
  /// getEthBalance(address)
30
31
  bytes4 private constant GET_ETH_BALANCE = 0x4d2301cc;
32
+ /// getBlockNumber()
33
+ bytes4 private constant GET_BLOCK_NUMBER = 0x42cbb15c;
31
34
  /// EIP-170
32
35
  uint256 private constant MAX_CODE_SIZE = 24576;
33
36
 
@@ -46,6 +49,8 @@ contract DeploylessMulticall3 {
46
49
  account := and(mload(add(data, 36)), 0xffffffffffffffffffffffffffffffffffffffff)
47
50
  }
48
51
  results[i] = Result(true, abi.encode(account.balance));
52
+ } else if (c.target == MULTICALL3 && data.length == 4 && bytes4(data) == GET_BLOCK_NUMBER) {
53
+ results[i] = Result(true, abi.encode(block.number));
49
54
  } else {
50
55
  (bool success, bytes memory ret) = c.target.call(data);
51
56
  if (!success && !c.allowFailure) {
@@ -1,13 +1,33 @@
1
1
  import { Interface, type Provider as EthersProvider } from 'ethers';
2
- import type { CallRequest, RawResult } from './call.js';
2
+ import { type CallRequest, type RawResult } from './call.js';
3
3
  import { type Multicall } from './multicall.js';
4
- /** 只用到 ethers Provider 的这两个方法,JsonRpcProvider / BrowserProvider 都满足 */
5
- export type EthersLikeProvider = Pick<EthersProvider, 'call' | 'getBalance'>;
4
+ /**
5
+ * 只用到 ethers Provider 的这几个方法:必需 call / getBalance;getLogs / getBlockNumber 可选(只有增量扫描 Transfer 事件时用到)。
6
+ * ethers 的 JsonRpcProvider / BrowserProvider 都满足
7
+ */
8
+ export type EthersLikeProvider = Pick<EthersProvider, 'call' | 'getBalance'> & Partial<Pick<EthersProvider, 'getLogs' | 'getBlockNumber'>>;
6
9
  export type BlockTag = number | bigint | string;
7
10
  export interface CallOverrides {
8
11
  blockTag?: BlockTag;
9
12
  from?: string;
13
+ /**
14
+ * 最低区块:节点落后于这个区块时(如交易刚确认、公共节点还没跟上)不用它的结果——
15
+ * 改为按这个区块重查(落后的节点会报错、自动换节点),所有节点都落后时稍等重试,最多约 10 秒。
16
+ * 只对最新状态的查询生效(指定了 blockTag 时忽略)
17
+ */
18
+ minBlock?: number;
19
+ /** 取消查询:已取消时不发请求;执行中取消时立即 reject(合并在一起的底层请求继续完成,结果丢弃) */
20
+ signal?: AbortSignal;
10
21
  }
22
+ /**
23
+ * 主币余额的读取方式:
24
+ * - 'contract':和其他调用放在同一次 eth_call 里(Multicall3.getEthBalance / deployless 合约里的 BALANCE)
25
+ * - 'rpc':单独用 eth_getBalance(与 eth_call 同时发出)。用于合约里读主币余额不可靠的链(如 BALANCE 恒为 0)
26
+ * - { erc20 }:主币其实是一个 ERC20 合约,改调它的 balanceOf
27
+ */
28
+ export type NativeBalanceMode = 'contract' | 'rpc' | {
29
+ erc20: string;
30
+ };
11
31
  export declare const multicall3Interface: Interface;
12
32
  export interface AggregateContext {
13
33
  provider: EthersLikeProvider;
@@ -15,6 +35,8 @@ export interface AggregateContext {
15
35
  /** null 表示直接走 deployless */
16
36
  multicall: Multicall | null;
17
37
  chunkSize: number;
38
+ /** 主币余额的读取方式,默认 'contract' */
39
+ nativeBalance?: NativeBalanceMode;
18
40
  }
19
41
  /**
20
42
  * 执行一批请求,结果与 requests 一一对应。
@@ -1 +1 @@
1
- {"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../src/aggregate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,SAAS,EAAkC,KAAK,QAAQ,IAAI,cAAc,EAAE,MAAM,QAAQ,CAAA;AAE7G,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAGvD,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAEnE,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,YAAY,CAAC,CAAA;AAE5E,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;AAE/C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,eAAO,MAAM,mBAAmB,WAG9B,CAAA;AAaF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,4BAA4B;IAC5B,SAAS,EAAE,SAAS,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,WAAW,EAAE,EACvB,SAAS,GAAE,aAAkB,GAC5B,OAAO,CAAC,SAAS,EAAE,CAAC,CActB;AA0LD,uBAAuB;AACvB,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C"}
1
+ {"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../src/aggregate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,SAAS,EAAkC,KAAK,QAAQ,IAAI,cAAc,EAAE,MAAM,QAAQ,CAAA;AAE7G,OAAO,EAAiB,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAA;AAG3E,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAGnE;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,gBAAgB,CAAC,CAAC,CAAA;AAE1I,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;AAE/C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yDAAyD;IACzD,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,KAAK,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAItE,eAAO,MAAM,mBAAmB,WAI9B,CAAA;AAoCF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,4BAA4B;IAC5B,SAAS,EAAE,SAAS,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,8BAA8B;IAC9B,aAAa,CAAC,EAAE,iBAAiB,CAAA;CAClC;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,WAAW,EAAE,EACvB,SAAS,GAAE,aAAkB,GAC5B,OAAO,CAAC,SAAS,EAAE,CAAC,CAmBtB;AA6SD,uBAAuB;AACvB,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C"}