@w3lib/evm-batch-call 0.3.0 → 0.4.1

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 (103) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +98 -35
  3. package/contracts/DeploylessMulticall3.sol +5 -0
  4. package/dist/aggregate.d.ts +20 -1
  5. package/dist/aggregate.d.ts.map +1 -1
  6. package/dist/aggregate.js +222 -32
  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 +64 -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 +133 -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 +49 -4
  33. package/dist/fallback.d.ts.map +1 -1
  34. package/dist/fallback.js +239 -37
  35. package/dist/fallback.js.map +1 -1
  36. package/dist/index.d.ts +9 -10
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +6 -7
  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 +9 -0
  48. package/dist/owner.d.ts.map +1 -1
  49. package/dist/owner.js +11 -2
  50. package/dist/owner.js.map +1 -1
  51. package/dist/provider.d.ts +55 -29
  52. package/dist/provider.d.ts.map +1 -1
  53. package/dist/provider.js +140 -111
  54. package/dist/provider.js.map +1 -1
  55. package/dist/shortcuts.d.ts +34 -53
  56. package/dist/shortcuts.d.ts.map +1 -1
  57. package/dist/shortcuts.js +41 -91
  58. package/dist/shortcuts.js.map +1 -1
  59. package/dist/source.d.ts +4 -2
  60. package/dist/source.d.ts.map +1 -1
  61. package/dist/source.js +35 -6
  62. package/dist/source.js.map +1 -1
  63. package/dist/subpaths/nft.d.ts +46 -0
  64. package/dist/subpaths/nft.d.ts.map +1 -0
  65. package/dist/subpaths/nft.js +55 -0
  66. package/dist/subpaths/nft.js.map +1 -0
  67. package/dist/subpaths/owner.d.ts +33 -0
  68. package/dist/subpaths/owner.d.ts.map +1 -0
  69. package/dist/subpaths/owner.js +34 -0
  70. package/dist/subpaths/owner.js.map +1 -0
  71. package/dist/tron.d.ts +2 -0
  72. package/dist/tron.d.ts.map +1 -1
  73. package/dist/tron.js +5 -0
  74. package/dist/tron.js.map +1 -1
  75. package/dist/util.d.ts +5 -0
  76. package/dist/util.d.ts.map +1 -1
  77. package/dist/util.js +23 -0
  78. package/dist/util.js.map +1 -1
  79. package/dist/watch.d.ts +25 -0
  80. package/dist/watch.d.ts.map +1 -0
  81. package/dist/watch.js +106 -0
  82. package/dist/watch.js.map +1 -0
  83. package/package.json +19 -10
  84. package/src/aggregate.ts +253 -34
  85. package/src/batcher.ts +73 -9
  86. package/src/call.ts +42 -2
  87. package/src/chains.ts +15 -0
  88. package/src/deployless.ts +1 -1
  89. package/src/erc20.ts +184 -8
  90. package/src/events.ts +42 -0
  91. package/src/fallback.ts +287 -38
  92. package/src/index.ts +19 -61
  93. package/src/multicall.ts +34 -2
  94. package/src/nft.ts +5 -0
  95. package/src/owner.ts +11 -2
  96. package/src/provider.ts +225 -124
  97. package/src/shortcuts.ts +76 -110
  98. package/src/source.ts +35 -8
  99. package/src/subpaths/nft.ts +107 -0
  100. package/src/subpaths/owner.ts +69 -0
  101. package/src/tron.ts +7 -0
  102. package/src/util.ts +27 -0
  103. package/src/watch.ts +152 -0
package/src/aggregate.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { AbiCoder, Interface, concat, dataSlice, id, isError, type Provider as EthersProvider } from 'ethers'
2
2
 
3
- import type { CallRequest, RawResult } from './call.js'
3
+ import { encodeUint256, type CallRequest, type RawResult } from './call.js'
4
4
  import { DEPLOYLESS_MULTICALL3_BYTECODE } from './deployless.js'
5
5
  import { isExecutionError } from './errors.js'
6
+ import { AllNodesFailedError, FallbackRpc, InvalidResultError } from './fallback.js'
6
7
  import { MULTICALL3_ADDRESS, type Multicall } from './multicall.js'
8
+ import { isTronChain } from './tron.js'
7
9
 
8
10
  /**
9
11
  * 只用到 ethers Provider 的这几个方法:必需 call / getBalance;getLogs / getBlockNumber 可选(只有增量扫描 Transfer 事件时用到)。
@@ -16,13 +18,55 @@ export type BlockTag = number | bigint | string
16
18
  export interface CallOverrides {
17
19
  blockTag?: BlockTag
18
20
  from?: string
21
+ /**
22
+ * 最低区块:节点落后于这个区块时(如交易刚确认、公共节点还没跟上)不用它的结果——
23
+ * 改为按这个区块重查(落后的节点会报错、自动换节点),所有节点都落后时稍等重试,最多约 10 秒。
24
+ * 只对最新状态的查询生效(指定了 blockTag 时忽略)
25
+ */
26
+ minBlock?: number
27
+ /** 取消查询:已取消时不发请求;执行中取消时立即 reject(合并在一起的底层请求继续完成,结果丢弃) */
28
+ signal?: AbortSignal
19
29
  }
20
30
 
31
+ /**
32
+ * 主币余额的读取方式:
33
+ * - 'contract':和其他调用放在同一次 eth_call 里(Multicall3.getEthBalance / deployless 合约里的 BALANCE)
34
+ * - 'rpc':单独用 eth_getBalance(与 eth_call 同时发出)。用于合约里读主币余额不可靠的链(如 BALANCE 恒为 0)
35
+ * - { erc20 }:主币其实是一个 ERC20 合约,改调它的 balanceOf
36
+ */
37
+ export type NativeBalanceMode = 'contract' | 'rpc' | { erc20: string }
38
+
39
+ const balanceOfInterface = new Interface(['function balanceOf(address owner) view returns (uint256)'])
40
+
21
41
  export const multicall3Interface = new Interface([
22
42
  'function aggregate3((address target, bool allowFailure, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[] returnData)',
23
43
  'function getEthBalance(address addr) view returns (uint256 balance)',
44
+ 'function getBlockNumber() view returns (uint256 blockNumber)',
24
45
  ])
25
46
 
47
+ /** 区块号查询(minBlock 检查用):目标在执行时换成 multicall 地址 */
48
+ const BLOCK_NUMBER_REQUEST: CallRequest = {
49
+ target: MULTICALL3_ADDRESS,
50
+ allowFailure: true,
51
+ callData: multicall3Interface.encodeFunctionData('getBlockNumber'),
52
+ blockNumber: true,
53
+ }
54
+
55
+ /**
56
+ * Arbitrum 系的链(Arbitrum One / Nova、Orbit 链如 Robinhood Chain)在合约里 block.number 是 L1 区块号,
57
+ * L2 区块号要从 ArbSys 预编译合约(0x64)的 arbBlockNumber() 读。区块号查询时顺带读它:有返回值就用它,
58
+ * 其他链上 0x64 没有代码、返回空数据,忽略
59
+ */
60
+ const ARB_BLOCK_NUMBER_REQUEST: CallRequest = {
61
+ target: '0x0000000000000000000000000000000000000064',
62
+ allowFailure: true,
63
+ callData: '0xa3b1b31d', // arbBlockNumber()
64
+ }
65
+
66
+ /** minBlock:所有节点都落后时的重试间隔和总时长 */
67
+ const MIN_BLOCK_RETRY_INTERVAL = 1000
68
+ const MIN_BLOCK_DEADLINE = 10_000
69
+
26
70
  const CALL3_TUPLE = 'tuple(address target, bool allowFailure, bytes callData)[]'
27
71
  const RESULT_TUPLE = 'tuple(bool success, bytes returnData)[]'
28
72
  /** error Aggregate3Result((bool,bytes)[]) */
@@ -32,7 +76,45 @@ const AGGREGATE3_RESULT_SELECTOR = id('Aggregate3Result((bool,bytes)[])').slice(
32
76
  * 判定为无效的 multicall 地址(无代码 / 不是 Multicall3),按 chainId+address 记在模块级,
33
77
  * 这样业务里每次 new Provider 也只会多试一次。
34
78
  */
35
- const unusableMulticalls = new Set<string>()
79
+ const unusableMulticalls = new Map<string, number>()
80
+
81
+ /** 判定不可用后多久重新尝试合约(节点偶发返回空数据时不至于整个会话都不用 Multicall3) */
82
+ const UNUSABLE_TTL = 10 * 60 * 1000
83
+ /** 单节点时连续返回空数据 / 无法解码的次数(多节点时由多个节点一致的结果确认,见 aggregateRawChunk) */
84
+ const invalidStrikes = new Map<string, number>()
85
+
86
+ /**
87
+ * eth_call 并解码 aggregate3 的结果:通过 FallbackRpc 时空数据 / 无法解码的结果换下一个节点(解码只做一次,结果复用);
88
+ * 其他 Provider 不支持校验参数,取到结果后自己校验
89
+ */
90
+ async function callAggregate3(provider: EthersLikeProvider, tx: Parameters<EthersLikeProvider['call']>[0]): Promise<RawResult[]> {
91
+ const decoded = new Map<string, RawResult[]>()
92
+ const accept = (data: string) => {
93
+ const results = decodeAggregate3Data(data)
94
+ if (results) {
95
+ decoded.set(data, results)
96
+ }
97
+ return results !== null
98
+ }
99
+ const data = provider instanceof FallbackRpc ? await provider.call(tx, { accept }) : await provider.call(tx)
100
+ const results = decoded.get(data) ?? decodeAggregate3Data(data)
101
+ if (!results) {
102
+ throw new InvalidResultError(data)
103
+ }
104
+ return results
105
+ }
106
+
107
+ /** aggregate3 返回数据的解码结果;空数据或无法解码时为 null */
108
+ function decodeAggregate3Data(data: string): RawResult[] | null {
109
+ if (!data || data === '0x') {
110
+ return null
111
+ }
112
+ try {
113
+ return decodeAggregate3(multicall3Interface.decodeFunctionResult('aggregate3', data)[0])
114
+ } catch {
115
+ return null
116
+ }
117
+ }
36
118
 
37
119
  export interface AggregateContext {
38
120
  provider: EthersLikeProvider
@@ -40,6 +122,8 @@ export interface AggregateContext {
40
122
  /** null 表示直接走 deployless */
41
123
  multicall: Multicall | null
42
124
  chunkSize: number
125
+ /** 主币余额的读取方式,默认 'contract' */
126
+ nativeBalance?: NativeBalanceMode
43
127
  }
44
128
 
45
129
  /**
@@ -55,64 +139,207 @@ export async function aggregate(
55
139
  if (requests.length === 0) {
56
140
  return []
57
141
  }
142
+ const mode = ctx.nativeBalance ?? 'contract'
143
+ if (mode !== 'contract' && requests.some((req) => req.ethBalanceOf !== undefined)) {
144
+ return typeof mode === 'object' ? aggregate(ctx, requests.map((req) => toErc20Balance(req, mode.erc20)), overrides) : aggregateWithRpcBalances(ctx, requests, overrides)
145
+ }
58
146
  const size = Math.max(1, ctx.chunkSize)
147
+ const run = overrides.minBlock !== undefined && isLatest(overrides.blockTag) ? aggregateChunkAtLeast : aggregateChunk
59
148
  if (requests.length <= size) {
60
- return aggregateChunk(ctx, requests, overrides)
149
+ return run(ctx, requests, overrides)
61
150
  }
62
151
  const chunks: CallRequest[][] = []
63
152
  for (let i = 0; i < requests.length; i += size) {
64
153
  chunks.push(requests.slice(i, i + size))
65
154
  }
66
- const results = await Promise.all(chunks.map((chunk) => aggregateChunk(ctx, chunk, overrides)))
155
+ const results = await Promise.all(chunks.map((chunk) => run(ctx, chunk, overrides)))
67
156
  return results.flat()
68
157
  }
69
158
 
70
- async function aggregateChunk(
159
+ /**
160
+ * minBlock:先按最新状态查,同一次 eth_call 里顺带读区块号;节点落后时按 minBlock 指定区块重查——
161
+ * 落后的节点对未来区块报 “header not found” 等错误,FallbackRpc 换节点(不冷却);所有节点都落后时稍等重试。
162
+ * 重查得到的是 minBlock 那个区块的状态(已包含刚确认的交易)。Tron 只能查最新状态,落后时等节点跟上再查。
163
+ * overrides.signal 取消后不再重试
164
+ */
165
+ async function aggregateChunkAtLeast(ctx: AggregateContext, requests: CallRequest[], overrides: CallOverrides): Promise<RawResult[]> {
166
+ const minBlock = overrides.minBlock as number
167
+ // Tron 节点只能查最新状态(不支持按区块号查询)
168
+ const canPinBlock = !isTronChain(ctx.chainId)
169
+ return retryUntilCaughtUp(overrides, async () => {
170
+ const results = await aggregateChunk(ctx, [...requests, BLOCK_NUMBER_REQUEST], overrides)
171
+ if ((blockOf(results[results.length - 1]) ?? -1n) >= BigInt(minBlock)) {
172
+ return results.slice(0, -1)
173
+ }
174
+ if (canPinBlock) {
175
+ // 按区块号重查时同样带上区块号并校验:有的节点对未来区块不报错、直接返回最新状态(如 HyperEVM),不能当成 minBlock 的结果
176
+ const pinned = await aggregateChunk(ctx, [...requests, BLOCK_NUMBER_REQUEST], { ...overrides, blockTag: minBlock })
177
+ if ((blockOf(pinned[pinned.length - 1]) ?? -1n) >= BigInt(minBlock)) {
178
+ return pinned.slice(0, -1)
179
+ }
180
+ }
181
+ throw new Error(`Node is behind minBlock ${minBlock}`)
182
+ })
183
+ }
184
+
185
+ /** 节点落后时的重试:确定性错误(合约执行结果)直接抛;其余错误每秒重试,最多约 10 秒;signal 取消后停止 */
186
+ async function retryUntilCaughtUp<T>(overrides: CallOverrides, run: () => Promise<T>): Promise<T> {
187
+ const deadline = Date.now() + MIN_BLOCK_DEADLINE
188
+ for (;;) {
189
+ overrides.signal?.throwIfAborted()
190
+ try {
191
+ return await run()
192
+ } catch (err) {
193
+ if (isExecutionError(err) || Date.now() + MIN_BLOCK_RETRY_INTERVAL > deadline) {
194
+ throw err
195
+ }
196
+ }
197
+ await sleep(MIN_BLOCK_RETRY_INTERVAL, overrides.signal)
198
+ }
199
+ }
200
+
201
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
202
+ return new Promise((resolve, reject) => {
203
+ const timer = setTimeout(() => {
204
+ signal?.removeEventListener('abort', onAbort)
205
+ resolve()
206
+ }, ms)
207
+ const onAbort = () => {
208
+ clearTimeout(timer)
209
+ reject(signal?.reason)
210
+ }
211
+ signal?.addEventListener('abort', onAbort, { once: true })
212
+ })
213
+ }
214
+
215
+ /** 区块号查询的结果;失败时为 null */
216
+ function blockOf(result: RawResult | undefined): bigint | null {
217
+ return result?.success && result.returnData !== '0x' ? BigInt(result.returnData) : null
218
+ }
219
+
220
+ /** 主币余额请求改成主币 ERC20 合约的 balanceOf(返回值同样是 uint256,解码方式不变) */
221
+ function toErc20Balance(req: CallRequest, erc20: string): CallRequest {
222
+ if (req.ethBalanceOf === undefined) {
223
+ return req
224
+ }
225
+ return { target: erc20, allowFailure: req.allowFailure, callData: balanceOfInterface.encodeFunctionData('balanceOf', [req.ethBalanceOf]) }
226
+ }
227
+
228
+ /**
229
+ * 主币余额单独走 eth_getBalance,结果按原顺序合并;节点错误照常抛出。
230
+ * 需要区块号(withBlock / minBlock)时,先执行其余请求并读出实际区块号,再在这个区块上读主币余额——
231
+ * 主币和代币余额、blockNumber 一致,minBlock 的等待 / 换节点也由其余请求负责;否则两者同时发出
232
+ */
233
+ async function aggregateWithRpcBalances(ctx: AggregateContext, requests: CallRequest[], overrides: CallOverrides): Promise<RawResult[]> {
234
+ const others = requests.filter((req) => req.ethBalanceOf === undefined)
235
+ const owners = requests.filter((req) => req.ethBalanceOf !== undefined).map((req) => req.ethBalanceOf as string)
236
+ const contractCtx: AggregateContext = { ...ctx, nativeBalance: 'contract' }
237
+ let otherResults: RawResult[]
238
+ let balances: bigint[]
239
+ // Tron 节点只能查最新状态,不能按区块号读余额
240
+ if (isLatest(overrides.blockTag) && !isTronChain(ctx.chainId) && (overrides.minBlock !== undefined || others.some((req) => req.blockNumber))) {
241
+ const results = await aggregate(contractCtx, [...others, BLOCK_NUMBER_REQUEST], overrides)
242
+ const block = blockOf(results.pop())
243
+ otherResults = results
244
+ const blockTag = block === null ? overrides.blockTag : Number(block)
245
+ const read = () => Promise.all(owners.map((owner) => ctx.provider.getBalance(owner, blockTag)))
246
+ // 只有 minBlock 才等待重试(读的区块可能来自比当前节点更新的节点);只要 withBlock 时出错照常抛出
247
+ balances = await (overrides.minBlock !== undefined ? retryUntilCaughtUp(overrides, read) : read())
248
+ } else {
249
+ ;[otherResults, balances] = await Promise.all([
250
+ aggregate(contractCtx, others, overrides),
251
+ Promise.all(owners.map((owner) => ctx.provider.getBalance(owner, overrides.blockTag))),
252
+ ])
253
+ }
254
+ let i = 0
255
+ let j = 0
256
+ return requests.map((req) =>
257
+ req.ethBalanceOf === undefined ? (otherResults[i++] as RawResult) : { success: true, returnData: encodeUint256(balances[j++] as bigint) },
258
+ )
259
+ }
260
+
261
+ /** 执行一批请求;其中有区块号查询时顺带读 Arbitrum 的 L2 区块号,并用它替换(见 ARB_BLOCK_NUMBER_REQUEST) */
262
+ async function aggregateChunk(ctx: AggregateContext, requests: CallRequest[], overrides: CallOverrides): Promise<RawResult[]> {
263
+ if (!requests.some((req) => req.blockNumber)) {
264
+ return aggregateRawChunk(ctx, requests, overrides)
265
+ }
266
+ const results = await aggregateRawChunk(ctx, [...requests, ARB_BLOCK_NUMBER_REQUEST], overrides)
267
+ const arb = results.pop() as RawResult
268
+ if (arb.success && arb.returnData.length === 66) {
269
+ requests.forEach((req, i) => {
270
+ if (req.blockNumber) {
271
+ results[i] = arb
272
+ }
273
+ })
274
+ }
275
+ return results
276
+ }
277
+
278
+ async function aggregateRawChunk(
71
279
  ctx: AggregateContext,
72
280
  requests: CallRequest[],
73
281
  overrides: CallOverrides,
74
282
  ): Promise<RawResult[]> {
75
283
  const multicall = ctx.multicall
76
284
  if (multicall && isUsable(ctx.chainId, multicall, overrides.blockTag)) {
285
+ const key = cacheKey(ctx.chainId, multicall.address)
77
286
  const results = await callContract(ctx, multicall, requests, overrides)
78
287
  if (Array.isArray(results)) {
288
+ invalidStrikes.delete(key)
79
289
  return results
80
290
  }
81
- // 只有“最新状态下地址上没有可用合约”才记缓存。
291
+ // 只有“最新状态下地址上没有可用合约”才记缓存(10 分钟后重新尝试):
292
+ // - 'invalid'(返回空数据 / 无法解码)要确认过才记:多个节点结果一致,或单节点连续 2 次——节点偶发返回一次 0x 不算
82
293
  // - revert 无数据('reverted')有歧义:可能是 out of gas / 节点 gas 上限、子调用失败而钱包丢了 revert 数据,
83
294
  // 这些情况地址本身是好的,只对这一次退回 deployless,不记缓存
84
295
  // - 历史区块查到无代码可能只是那时还没部署,不能推及 latest
85
- if (results === 'invalid' && isLatest(overrides.blockTag)) {
86
- unusableMulticalls.add(cacheKey(ctx.chainId, multicall.address))
296
+ if (results !== 'reverted' && isLatest(overrides.blockTag)) {
297
+ const strikes = (invalidStrikes.get(key) ?? 0) + 1
298
+ if (results === 'invalid-confirmed' || strikes >= 2) {
299
+ unusableMulticalls.set(key, Date.now() + UNUSABLE_TTL)
300
+ invalidStrikes.delete(key)
301
+ } else {
302
+ invalidStrikes.set(key, strikes)
303
+ }
87
304
  }
88
305
  }
89
306
  return callDeployless(ctx, requests, overrides)
90
307
  }
91
308
 
92
309
  /**
93
- * 'invalid':地址上没有代码或返回无法解码;'reverted':执行 revert 且没有 revert 数据。
94
- * 两种情况都退回 deployless。
310
+ * 'invalid':返回空数据(地址上没有代码)或无法解码——经 FallbackRpc 时会先换节点重试;
311
+ * 所有节点(2 个以上)都这样时为 'invalid-confirmed'。'reverted':执行 revert 且没有 revert 数据。
312
+ * 这几种情况都退回 deployless。
95
313
  */
96
314
  async function callContract(
97
315
  ctx: AggregateContext,
98
316
  multicall: Multicall,
99
317
  requests: CallRequest[],
100
318
  overrides: CallOverrides,
101
- ): Promise<RawResult[] | 'invalid' | 'reverted'> {
319
+ ): Promise<RawResult[] | 'invalid' | 'invalid-confirmed' | 'reverted'> {
102
320
  const calls = requests.map((req) => ({
103
- target: req.ethBalanceOf ? multicall.address : req.target,
321
+ target: req.ethBalanceOf || req.blockNumber ? multicall.address : req.target,
104
322
  allowFailure: req.allowFailure,
105
323
  callData: req.callData,
106
324
  }))
107
- let data: string
108
325
  try {
109
- data = await ctx.provider.call({
326
+ // 地址上没有代码时 eth_call 返回 0x;空数据 / 无法解码的结果换下一个节点(节点偶发出错时别的节点能给出正确结果)
327
+ return await callAggregate3(ctx.provider, {
110
328
  to: multicall.address,
111
329
  data: multicall3Interface.encodeFunctionData('aggregate3', [calls]),
112
330
  blockTag: overrides.blockTag,
113
331
  from: overrides.from,
114
332
  })
115
333
  } catch (err) {
334
+ if (err instanceof InvalidResultError) {
335
+ return 'invalid'
336
+ }
337
+ // 有节点返回空数据、其余节点出错(超时、限频等):合约可能不存在,退回 deployless;
338
+ // 只有 2 个以上节点都返回空数据才算确认,记缓存
339
+ if (err instanceof AllNodesFailedError && err.errors.some((e) => e.error instanceof InvalidResultError)) {
340
+ const invalid = err.errors.filter((e) => e.error instanceof InvalidResultError).length
341
+ return invalid >= 2 && invalid === err.errors.length ? 'invalid-confirmed' : 'invalid'
342
+ }
116
343
  // aggregate3 自己的失败(allowFailure=false 的子调用失败)会带 "Multicall3: call failed" 的 revert 数据;
117
344
  // 执行层面 revert 且没有数据,基本就是这个地址上不是 Multicall3。
118
345
  // 限流、节点缺状态等错误 ethers 也会包装成 CALL_EXCEPTION,那些要原样抛出,不能把地址判成无效
@@ -121,15 +348,6 @@ async function callContract(
121
348
  }
122
349
  throw err
123
350
  }
124
- // 地址上没有代码时 eth_call 返回 0x
125
- if (!data || data === '0x') {
126
- return 'invalid'
127
- }
128
- try {
129
- return decodeAggregate3(multicall3Interface.decodeFunctionResult('aggregate3', data)[0])
130
- } catch {
131
- return 'invalid'
132
- }
133
351
  }
134
352
 
135
353
  /**
@@ -157,20 +375,16 @@ async function callDeploylessChunk(
157
375
  const args = AbiCoder.defaultAbiCoder().encode(
158
376
  [CALL3_TUPLE],
159
377
  [
160
- requests.map(({ target, allowFailure, callData, ethBalanceOf }) => ({
161
- target: ethBalanceOf ? MULTICALL3_ADDRESS : target,
378
+ requests.map(({ target, allowFailure, callData, ethBalanceOf, blockNumber }) => ({
379
+ target: ethBalanceOf || blockNumber ? MULTICALL3_ADDRESS : target,
162
380
  allowFailure,
163
381
  callData,
164
382
  })),
165
383
  ],
166
384
  )
167
- let data: string
168
385
  try {
169
- data = await ctx.provider.call({
170
- data: concat([DEPLOYLESS_MULTICALL3_BYTECODE, args]),
171
- blockTag: overrides.blockTag,
172
- from: overrides.from,
173
- })
386
+ // 返回空数据 / 无法解码(节点不支持合约创建式 eth_call,或偶发出错)时换下一个节点
387
+ return await callAggregate3(ctx.provider, { data: concat([DEPLOYLESS_MULTICALL3_BYTECODE, args]), blockTag: overrides.blockTag, from: overrides.from })
174
388
  } catch (err) {
175
389
  const revertData = (err as { data?: string | null }).data
176
390
  if (isError(err, 'CALL_EXCEPTION') && revertData?.startsWith(AGGREGATE3_RESULT_SELECTOR)) {
@@ -188,7 +402,6 @@ async function callDeploylessChunk(
188
402
  }
189
403
  throw err
190
404
  }
191
- return decodeAggregate3(multicall3Interface.decodeFunctionResult('aggregate3', data)[0])
192
405
  }
193
406
 
194
407
  /** EIP-3860 initcode 上限 49152 字节,留出余量 */
@@ -228,8 +441,13 @@ function decodeAggregate3(raw: ReadonlyArray<readonly [boolean, string]>): RawRe
228
441
  }
229
442
 
230
443
  function isUsable(chainId: number, multicall: Multicall, blockTag?: BlockTag): boolean {
231
- if (unusableMulticalls.has(cacheKey(chainId, multicall.address))) {
232
- return false
444
+ const key = cacheKey(chainId, multicall.address)
445
+ const until = unusableMulticalls.get(key)
446
+ if (until !== undefined) {
447
+ if (Date.now() < until) {
448
+ return false
449
+ }
450
+ unusableMulticalls.delete(key)
233
451
  }
234
452
  if (blockTag === 'earliest') {
235
453
  return false
@@ -254,4 +472,5 @@ function cacheKey(chainId: number, address: string): string {
254
472
  /** 测试用:清掉“地址不可用”的缓存 */
255
473
  export function resetMulticallCache(): void {
256
474
  unusableMulticalls.clear()
475
+ invalidStrikes.clear()
257
476
  }
package/src/batcher.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { decodeCall, encodeCall, type Call, type CallRequest, type RawResult } from './call.js'
1
+ import { decodeCall, encodeCall, encodeFailureReason, settleResult, type Call, type CallRequest, type RawResult, type Settled } from './call.js'
2
2
  import type { CallOverrides } from './aggregate.js'
3
3
  import { CallFailedError } from './errors.js'
4
4
 
@@ -13,6 +13,10 @@ type Runner = (requests: CallRequest[], overrides: CallOverrides) => Promise<Raw
13
13
 
14
14
  interface Pending {
15
15
  call: Call
16
+ /** 调用方的 signal(没有时这个调用方不会取消,整批请求不能中断) */
17
+ signal?: AbortSignal
18
+ /** true:失败时 resolve 失败原因(Settled),不 reject;节点错误照常 reject */
19
+ settle: boolean
16
20
  resolve: (value: unknown) => void
17
21
  reject: (reason: unknown) => void
18
22
  }
@@ -23,6 +27,27 @@ interface Queue {
23
27
  timer: ReturnType<typeof setTimeout> | null
24
28
  }
25
29
 
30
+ /**
31
+ * 整批请求的 signal:所有调用方都传了 signal、并且都取消后才取消(如 minBlock 的等待重试随之停止);
32
+ * 有调用方没传 signal 时为 undefined(整批照常完成)。请求结束后调用 dispose,移除挂在调用方 signal 上的监听
33
+ * (调用方可能用一个长期存在的 signal 发起很多次查询)
34
+ */
35
+ function combinedSignal(entries: readonly Pending[]): { signal: AbortSignal | undefined; dispose: () => void } {
36
+ const signals = [...new Set(entries.map((entry) => entry.signal))]
37
+ if (!signals.length || signals.some((signal) => signal === undefined)) {
38
+ return { signal: undefined, dispose: () => {} }
39
+ }
40
+ const controller = new AbortController()
41
+ const check = () => {
42
+ if (signals.every((signal) => signal?.aborted)) {
43
+ controller.abort(signals[0]?.reason)
44
+ }
45
+ }
46
+ signals.forEach((signal) => signal?.addEventListener('abort', check, { once: true }))
47
+ check()
48
+ return { signal: controller.signal, dispose: () => signals.forEach((signal) => signal?.removeEventListener('abort', check)) }
49
+ }
50
+
26
51
  /**
27
52
  * DataLoader 式的自动合并:各处独立调用 `provider.call(x)`,
28
53
  * 落在同一个收集窗口、且 blockTag/from 相同的调用会合成一次 aggregate3。
@@ -41,21 +66,49 @@ export class Batcher {
41
66
  }
42
67
 
43
68
  load<T>(call: Call, overrides: CallOverrides = {}): Promise<T> {
44
- const key = `${String(overrides.blockTag ?? 'latest')}|${overrides.from ?? ''}`
69
+ const [key, queue] = this.#queue(overrides)
70
+ const promise = new Promise<T>((resolve, reject) => {
71
+ queue.entries.push({ call, signal: overrides.signal, settle: false, resolve: resolve as (value: unknown) => void, reject })
72
+ })
73
+ this.#schedule(key, queue)
74
+ return promise
75
+ }
76
+
77
+ /**
78
+ * 一组调用一起入队(不会被 maxSize 拆到不同批次),逐条返回成功的值或失败原因;
79
+ * 只有节点错误(整批失败)时 reject。balances / tokens / allowances 用它参与自动合并。
80
+ */
81
+ loadSettled(calls: readonly Call[], overrides: CallOverrides = {}): Promise<Settled[]> {
82
+ if (!calls.length) {
83
+ return Promise.resolve([])
84
+ }
85
+ const [key, queue] = this.#queue(overrides)
86
+ const promises = calls.map(
87
+ (call) =>
88
+ new Promise<Settled>((resolve, reject) => {
89
+ queue.entries.push({ call, signal: overrides.signal, settle: true, resolve: resolve as (value: unknown) => void, reject })
90
+ }),
91
+ )
92
+ this.#schedule(key, queue)
93
+ return Promise.all(promises)
94
+ }
95
+
96
+ #queue(overrides: CallOverrides): [string, Queue] {
97
+ const key = `${String(overrides.blockTag ?? 'latest')}|${overrides.from ?? ''}|${overrides.minBlock ?? ''}`
45
98
  let queue = this.#queues.get(key)
46
99
  if (!queue) {
47
100
  queue = { overrides, entries: [], timer: null }
48
101
  this.#queues.set(key, queue)
49
102
  }
50
- const promise = new Promise<T>((resolve, reject) => {
51
- queue.entries.push({ call, resolve: resolve as (value: unknown) => void, reject })
52
- })
103
+ return [key, queue]
104
+ }
105
+
106
+ #schedule(key: string, queue: Queue): void {
53
107
  if (queue.entries.length >= this.#maxSize) {
54
108
  this.#flush(key)
55
109
  } else if (!queue.timer) {
56
110
  queue.timer = setTimeout(() => this.#flush(key), this.#wait)
57
111
  }
58
- return promise
59
112
  }
60
113
 
61
114
  #flush(key: string): void {
@@ -80,11 +133,15 @@ export class Batcher {
80
133
  try {
81
134
  request = encodeCall(entry.call, true)
82
135
  } catch (err) {
83
- entry.reject(err)
136
+ if (entry.settle) {
137
+ entry.resolve({ ok: false, reason: encodeFailureReason(err) } satisfies Settled)
138
+ } else {
139
+ entry.reject(err)
140
+ }
84
141
  requestIndex.push(-1)
85
142
  continue
86
143
  }
87
- const dedupeKey = `${request.ethBalanceOf ? 'eth' : request.target.toLowerCase()}|${request.callData}`
144
+ const dedupeKey = `${request.ethBalanceOf ? 'eth' : request.blockNumber ? 'block' : request.target.toLowerCase()}|${request.callData}`
88
145
  let index = indexByKey.get(dedupeKey)
89
146
  if (index === undefined) {
90
147
  index = requests.push(request) - 1
@@ -98,11 +155,14 @@ export class Batcher {
98
155
  }
99
156
 
100
157
  let results: RawResult[]
158
+ const combined = combinedSignal(entries)
101
159
  try {
102
- results = await this.#run(requests, overrides)
160
+ results = await this.#run(requests, { ...overrides, signal: combined.signal })
103
161
  } catch (err) {
104
162
  entries.forEach((entry, i) => requestIndex[i] !== -1 && entry.reject(err))
105
163
  return
164
+ } finally {
165
+ combined.dispose()
106
166
  }
107
167
 
108
168
  entries.forEach((entry, i) => {
@@ -111,6 +171,10 @@ export class Batcher {
111
171
  return
112
172
  }
113
173
  const result = results[index] as RawResult
174
+ if (entry.settle) {
175
+ entry.resolve(settleResult(entry.call, result))
176
+ return
177
+ }
114
178
  if (!result.success) {
115
179
  entry.reject(new CallFailedError(entry.call, result.returnData))
116
180
  return
package/src/call.ts CHANGED
@@ -32,9 +32,10 @@ export interface Call<T = any> {
32
32
  /**
33
33
  * 内部标记:
34
34
  * - ethBalance:原生币余额查询(见 Provider.getEthBalance)
35
+ * - blockNumber:当前区块号(Multicall3.getBlockNumber;deployless 合约里直接返回 block.number)
35
36
  * - stringOrBytes32:string 返回值,解码失败时按 bytes32 解析(MKR 等老代币的 symbol / name)
36
37
  */
37
- kind?: 'ethBalance' | 'stringOrBytes32'
38
+ kind?: 'ethBalance' | 'blockNumber' | 'stringOrBytes32'
38
39
  /** 仅用于类型推断 */
39
40
  readonly __result?: T
40
41
  }
@@ -52,13 +53,51 @@ export interface RawResult {
52
53
  returnData: string
53
54
  }
54
55
 
56
+ /**
57
+ * 单条调用的失败原因:
58
+ * - invalid-address:合约地址或参数里的地址非法
59
+ * - invalid-argument:其他参数编码失败(个数不对、数值超出范围等)
60
+ * - no-contract:调用成功但返回空数据,多半是目标地址上没有合约
61
+ * - reverted:执行 revert
62
+ * - decode-failed:有返回数据但无法按 ABI 解码(如合约不是预期的标准)
63
+ * - not-configured:没有发请求的信息缺失,如主币不在内置链信息表里、也没有配置 nativeName / nativeSymbol
64
+ */
65
+ export type FailureReason = 'invalid-address' | 'invalid-argument' | 'no-contract' | 'reverted' | 'decode-failed' | 'not-configured'
66
+
67
+ /** 单条调用的结果:成功的值,或失败原因 */
68
+ export type Settled<T = any> = { ok: true; value: T } | { ok: false; reason: FailureReason }
69
+
70
+ /** 按 multicall 的原始结果判断成功 / 失败原因 */
71
+ export function settleResult<T>(call: Call, result: RawResult): Settled<T> {
72
+ if (!result.success) {
73
+ return { ok: false, reason: 'reverted' }
74
+ }
75
+ // 没有返回值的函数成功时本来就返回空数据,照常解码;有返回值却拿到空数据,多半是地址上没有合约
76
+ if ((!result.returnData || result.returnData === '0x') && call.outputs.length > 0) {
77
+ return { ok: false, reason: 'no-contract' }
78
+ }
79
+ try {
80
+ return { ok: true, value: decodeCall<T>(call, result.returnData) }
81
+ } catch {
82
+ return { ok: false, reason: 'decode-failed' }
83
+ }
84
+ }
85
+
86
+ /** 参数编码失败的原因:地址非法,还是其他参数问题 */
87
+ export function encodeFailureReason(err: unknown): FailureReason {
88
+ const e = err as { code?: unknown; argument?: unknown; message?: unknown } | null
89
+ return /address/i.test(String(e?.argument ?? '')) || /address/i.test(String(e?.message ?? '')) ? 'invalid-address' : 'invalid-argument'
90
+ }
91
+
55
92
  /** 发给 aggregate3 前的单条请求 */
56
93
  export interface CallRequest {
57
94
  target: string
58
95
  allowFailure: boolean
59
96
  callData: string
60
- /** 原生币余额查询:有 multicall 合约时走 getEthBalance,deployless 时改走 eth_getBalance */
97
+ /** 原生币余额查询:默认有 multicall 合约时走 getEthBalance、deployless 时用 BALANCE;其他读取方式见 NativeBalanceMode */
61
98
  ethBalanceOf?: string
99
+ /** 区块号查询:目标换成实际的 multicall 地址(deployless 时由合约内部处理) */
100
+ blockNumber?: boolean
62
101
  }
63
102
 
64
103
  // inputs → name → outputs → Interface。同一份 ABI 生成的 Call 共享 inputs/outputs 数组(见 contract.ts 的 ABI 缓存),
@@ -100,6 +139,7 @@ export function encodeCall(call: Call, allowFailure: boolean): CallRequest {
100
139
  allowFailure,
101
140
  callData: iface.encodeFunctionData(fragment, params),
102
141
  ethBalanceOf: call.kind === 'ethBalance' ? toEvmAddress(params[0] as string) : undefined,
142
+ blockNumber: call.kind === 'blockNumber' ? true : undefined,
103
143
  }
104
144
  }
105
145
 
package/src/chains.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { NativeBalanceMode } from './aggregate.js'
2
+
1
3
  /**
2
4
  * 内置链的主币信息(balances() / tokens() 里主币的 name / symbol / 精度)。
3
5
  * 来源:https://chainid.network/chains.json 的 nativeCurrency;999 在该表里与 Wanchain 测试网撞号,
@@ -44,3 +46,16 @@ export const NATIVE_CURRENCIES: Readonly<Record<number, NativeCurrency>> = {
44
46
  export function getNativeCurrency(chainId: number): NativeCurrency | undefined {
45
47
  return NATIVE_CURRENCIES[chainId]
46
48
  }
49
+
50
+ /**
51
+ * 合约里读主币余额不可靠的链,以及应改用的读取方式(ProviderConfig.nativeBalance 的默认值)。
52
+ * 新链可用 `pnpm check:rpc` 比对“合约里读到的主币余额”和 eth_getBalance 来发现。
53
+ */
54
+ export const NATIVE_BALANCE_MODES: Readonly<Record<number, NativeBalanceMode>> = {
55
+ // Anubis:Multicall3.getEthBalance 和 deployless 里的 BALANCE 对任何地址都返回 0,eth_getBalance 正常
56
+ 6714: 'rpc',
57
+ }
58
+
59
+ export function getNativeBalanceMode(chainId: number): NativeBalanceMode {
60
+ return NATIVE_BALANCE_MODES[chainId] ?? 'contract'
61
+ }
package/src/deployless.ts CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  /** constructor((address target, bool allowFailure, bytes callData)[] calls) — 以 aggregate3 的返回格式带回结果 */
4
4
  export const DEPLOYLESS_MULTICALL3_BYTECODE =
5
- '0x608060405260405161060f38038061060f8339810160408190526100229161036c565b80516000816001600160401b0381111561003e5761003e6102da565b60405190808252806020026020018201604052801561008457816020015b60408051808201909152600081526060602082015281526020019060019003908161005c5790505b50905060005b828110156102875760008482815181106100a6576100a6610507565b602090810291909101015160408101518151919250906001600160a01b031673ca11bde05977b3631167028862be2a173976ca111480156100e8575080516024145b801561010c5750631348c07360e21b6101008261051d565b6001600160e01b031916145b1561017e5760248101516040805180820182526001815290516001600160a01b039290921691602080830191610149918531910190815260200190565b60405160208183030381529060405281525085858151811061016d5761016d610507565b60200260200101819052505061027d565b60008083600001516001600160a01b03168360405161019d919061055b565b6000604051808303816000865af19150503d80600081146101da576040519150601f19603f3d011682016040523d82523d6000602084013e6101df565b606091505b5091509150811580156101f457508360200151155b156102465760405162461bcd60e51b815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064015b60405180910390fd5b604051806040016040528083151581526020018281525086868151811061026f5761026f610507565b602002602001018190525050505b505060010161008a565b5060008160405160200161029b9190610577565b6040516020818303038152906040529050616000815111156102d25781604051633649f05160e01b815260040161023d9190610577565b805160208201f35b634e487b7160e01b600052604160045260246000fd5b604051606081016001600160401b0381118282101715610312576103126102da565b60405290565b604051601f8201601f191681016001600160401b0381118282101715610340576103406102da565b604052919050565b60005b8381101561036357818101518382015260200161034b565b50506000910152565b60006020828403121561037e57600080fd5b81516001600160401b0381111561039457600080fd5b8201601f810184136103a557600080fd5b80516001600160401b038111156103be576103be6102da565b8060051b6103ce60208201610318565b918252602081840181019290810190878411156103ea57600080fd5b6020850192505b838310156104fc5782516001600160401b0381111561040f57600080fd5b85016060818a03601f1901121561042557600080fd5b61042d6102f0565b60208201516001600160a01b038116811461044757600080fd5b81526040820151801515811461045c57600080fd5b602082015260608201516001600160401b0381111561047a57600080fd5b60208184010192505089601f83011261049257600080fd5b81516001600160401b038111156104ab576104ab6102da565b6104be601f8201601f1916602001610318565b8181528b60208386010111156104d357600080fd5b6104e4826020830160208701610348565b604083015250835250602092830192909101906103f1565b979650505050505050565b634e487b7160e01b600052603260045260246000fd5b805160208201516001600160e01b0319811691906004821015610554576001600160e01b0319600483900360031b81901b82161692505b5050919050565b6000825161056d818460208701610348565b9190910192915050565b6000602082016020835280845180835260408501915060408160051b86010192506020860160005b8281101561060257603f198786030184528151805115158652602081015190506040602087015280518060408801526105df816060890160208501610348565b601f01601f1916959095016060019450602093840193919091019060010161059f565b5092969550505050505056fe'
5
+ '0x60806040526040516106c13803806106c18339810160408190526100229161041e565b80516000816001600160401b0381111561003e5761003e61038c565b60405190808252806020026020018201604052801561008457816020015b60408051808201909152600081526060602082015281526020019060019003908161005c5790505b50905060005b828110156103395760008482815181106100a6576100a66105b9565b602090810291909101015160408101518151919250906001600160a01b031673ca11bde05977b3631167028862be2a173976ca111480156100e8575080516024145b801561010c5750631348c07360e21b610100826105cf565b6001600160e01b031916145b1561017e5760248101516040805180820182526001815290516001600160a01b039290921691602080830191610149918531910190815260200190565b60405160208183030381529060405281525085858151811061016d5761016d6105b9565b60200260200101819052505061032f565b81516001600160a01b031673ca11bde05977b3631167028862be2a173976ca111480156101ac575080516004145b80156101d057506310b2ec5760e21b6101c4826105cf565b6001600160e01b031916145b15610230576040518060400160405280600115158152602001436040516020016101fc91815260200190565b604051602081830303815290604052815250848481518110610220576102206105b9565b602002602001018190525061032f565b60008083600001516001600160a01b03168360405161024f919061060d565b6000604051808303816000865af19150503d806000811461028c576040519150601f19603f3d011682016040523d82523d6000602084013e610291565b606091505b5091509150811580156102a657508360200151155b156102f85760405162461bcd60e51b815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064015b60405180910390fd5b6040518060400160405280831515815260200182815250868681518110610321576103216105b9565b602002602001018190525050505b505060010161008a565b5060008160405160200161034d9190610629565b6040516020818303038152906040529050616000815111156103845781604051633649f05160e01b81526004016102ef9190610629565b805160208201f35b634e487b7160e01b600052604160045260246000fd5b604051606081016001600160401b03811182821017156103c4576103c461038c565b60405290565b604051601f8201601f191681016001600160401b03811182821017156103f2576103f261038c565b604052919050565b60005b838110156104155781810151838201526020016103fd565b50506000910152565b60006020828403121561043057600080fd5b81516001600160401b0381111561044657600080fd5b8201601f8101841361045757600080fd5b80516001600160401b038111156104705761047061038c565b8060051b610480602082016103ca565b9182526020818401810192908101908784111561049c57600080fd5b6020850192505b838310156105ae5782516001600160401b038111156104c157600080fd5b85016060818a03601f190112156104d757600080fd5b6104df6103a2565b60208201516001600160a01b03811681146104f957600080fd5b81526040820151801515811461050e57600080fd5b602082015260608201516001600160401b0381111561052c57600080fd5b60208184010192505089601f83011261054457600080fd5b81516001600160401b0381111561055d5761055d61038c565b610570601f8201601f19166020016103ca565b8181528b602083860101111561058557600080fd5b6105968260208301602087016103fa565b604083015250835250602092830192909101906104a3565b979650505050505050565b634e487b7160e01b600052603260045260246000fd5b805160208201516001600160e01b0319811691906004821015610606576001600160e01b0319600483900360031b81901b82161692505b5050919050565b6000825161061f8184602087016103fa565b9190910192915050565b6000602082016020835280845180835260408501915060408160051b86010192506020860160005b828110156106b457603f198786030184528151805115158652602081015190506040602087015280518060408801526106918160608901602085016103fa565b601f01601f19169590950160600194506020938401939190910190600101610651565b5092969550505050505056fe'