@w3lib/evm-batch-call 0.4.0 → 0.4.2

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.
package/src/aggregate.ts CHANGED
@@ -3,6 +3,7 @@ import { AbiCoder, Interface, concat, dataSlice, id, isError, type Provider as E
3
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, NodeBehindError, isBehind } from './fallback.js'
6
7
  import { MULTICALL3_ADDRESS, type Multicall } from './multicall.js'
7
8
  import { isTronChain } from './tron.js'
8
9
 
@@ -75,7 +76,124 @@ const AGGREGATE3_RESULT_SELECTOR = id('Aggregate3Result((bool,bytes)[])').slice(
75
76
  * 判定为无效的 multicall 地址(无代码 / 不是 Multicall3),按 chainId+address 记在模块级,
76
77
  * 这样业务里每次 new Provider 也只会多试一次。
77
78
  */
78
- 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(
91
+ provider: EthersLikeProvider,
92
+ tx: Parameters<EthersLikeProvider['call']>[0],
93
+ requests: readonly CallRequest[],
94
+ atLeast?: bigint,
95
+ ): Promise<RawResult[]> {
96
+ const decoded = new Map<string, RawResult[]>()
97
+ // deployless 结果超过 24KB 时通过 revert Aggregate3Result 带回:同样校验区块号,落后时换成 NodeBehindError(FallbackRpc 换节点)
98
+ const inspectError = (err: unknown) => {
99
+ const results = atLeast === undefined ? null : revertedResults(err)
100
+ if (results) {
101
+ assertAtLeast(requests, results, atLeast as bigint)
102
+ }
103
+ }
104
+ const accept = (data: string) => {
105
+ const results = decodeAggregate3Data(data)
106
+ if (!results) {
107
+ return false
108
+ }
109
+ // minBlock:结果里的区块号早于要求时节点落后,抛 NodeBehindError 让 FallbackRpc 换下一个节点
110
+ if (atLeast !== undefined) {
111
+ assertAtLeast(requests, results, atLeast)
112
+ }
113
+ decoded.set(data, results)
114
+ return true
115
+ }
116
+ let data: string
117
+ if (provider instanceof FallbackRpc) {
118
+ data = await provider.call(tx, { accept, inspectError })
119
+ } else {
120
+ // 其他 Provider 不支持校验参数:取到结果后自己校验(同样包括区块号)
121
+ try {
122
+ data = await provider.call(tx)
123
+ } catch (err) {
124
+ inspectError(err)
125
+ throw err
126
+ }
127
+ if (!accept(data)) {
128
+ throw new InvalidResultError(data)
129
+ }
130
+ }
131
+ const results = decoded.get(data) ?? decodeAggregate3Data(data)
132
+ if (!results) {
133
+ throw new InvalidResultError(data)
134
+ }
135
+ return results
136
+ }
137
+
138
+ /** 已解析过的 revert 结果(同一个错误在 inspectError 和 callDeploylessChunk 里各用一次,只解析一次) */
139
+ const revertedCache = new WeakMap<object, RawResult[] | null>()
140
+
141
+ /** deployless 通过 revert Aggregate3Result(...) 带回的结果;不是这种 revert、或数据无法解码时为 null(保留原来的错误) */
142
+ function revertedResults(err: unknown): RawResult[] | null {
143
+ const revertData = (err as { data?: string | null } | null)?.data
144
+ if (!isError(err, 'CALL_EXCEPTION') || !revertData?.startsWith(AGGREGATE3_RESULT_SELECTOR)) {
145
+ return null
146
+ }
147
+ const cached = revertedCache.get(err)
148
+ if (cached !== undefined) {
149
+ return cached
150
+ }
151
+ let results: RawResult[] | null
152
+ try {
153
+ results = decodeAggregate3(AbiCoder.defaultAbiCoder().decode([RESULT_TUPLE], dataSlice(revertData, 4))[0])
154
+ } catch {
155
+ results = null
156
+ }
157
+ revertedCache.set(err, results)
158
+ return results
159
+ }
160
+
161
+ /** ArbSys.arbBlockNumber() 的结果(Arbitrum 系的 L2 区块号);其他链上 0x64 没有代码、返回空数据,为 null */
162
+ function l2BlockOf(result: RawResult | undefined): bigint | null {
163
+ return result?.success && result.returnData.length === 66 ? BigInt(result.returnData) : null
164
+ }
165
+
166
+ /**
167
+ * minBlock 校验:这次 eth_call 里附带的区块号查询(BLOCK_NUMBER_REQUEST,Arbitrum 上用一起附带的 L2 区块号)早于要求时,
168
+ * 抛 NodeBehindError(FallbackRpc 换下一个节点)。每次 eth_call 都附带了这两条(deployless 切片时每片各自附带)
169
+ */
170
+ function assertAtLeast(requests: readonly CallRequest[], results: readonly RawResult[], atLeast: bigint): void {
171
+ const blockIndex = requests.lastIndexOf(BLOCK_NUMBER_REQUEST)
172
+ if (blockIndex === -1) {
173
+ return
174
+ }
175
+ const arbIndex = requests.lastIndexOf(ARB_BLOCK_NUMBER_REQUEST)
176
+ assertBlock((arbIndex === -1 ? null : l2BlockOf(results[arbIndex])) ?? blockOf(results[blockIndex]), atLeast)
177
+ }
178
+
179
+ /** 区块号不早于 minBlock,否则节点落后(读不到区块号也按落后处理) */
180
+ function assertBlock(block: bigint | null, atLeast: bigint): void {
181
+ if (block === null || block < atLeast) {
182
+ throw new NodeBehindError(atLeast, block ?? -1n)
183
+ }
184
+ }
185
+
186
+ /** aggregate3 返回数据的解码结果;空数据或无法解码时为 null */
187
+ function decodeAggregate3Data(data: string): RawResult[] | null {
188
+ if (!data || data === '0x') {
189
+ return null
190
+ }
191
+ try {
192
+ return decodeAggregate3(multicall3Interface.decodeFunctionResult('aggregate3', data)[0])
193
+ } catch {
194
+ return null
195
+ }
196
+ }
79
197
 
80
198
  export interface AggregateContext {
81
199
  provider: EthersLikeProvider
@@ -127,18 +245,54 @@ async function aggregateChunkAtLeast(ctx: AggregateContext, requests: CallReques
127
245
  const minBlock = overrides.minBlock as number
128
246
  // Tron 节点只能查最新状态(不支持按区块号查询)
129
247
  const canPinBlock = !isTronChain(ctx.chainId)
248
+ const atLeast = BigInt(minBlock)
249
+ let pinned = false
130
250
  return retryUntilCaughtUp(overrides, async () => {
131
- const results = await aggregateChunk(ctx, [...requests, BLOCK_NUMBER_REQUEST], overrides)
132
- if ((blockOf(results[results.length - 1]) ?? -1n) >= BigInt(minBlock)) {
133
- return results.slice(0, -1)
251
+ // 区块号校验在 FallbackRpc 里做:落后的节点(结果的区块号早于 minBlock)当场换下一个节点,跟上的节点直接给出最新状态
252
+ try {
253
+ return withoutBlock(await aggregateChunk(ctx, [...requests, BLOCK_NUMBER_REQUEST], overrides, atLeast))
254
+ } catch (err) {
255
+ // 有节点落后(其余节点可能因限频、超时等失败)时,按区块号重查一次;之后只等最新状态跟上,避免每秒把所有节点问两遍
256
+ if (!canPinBlock || pinned || !isBehindFailure(err)) {
257
+ throw err
258
+ }
134
259
  }
135
- if (canPinBlock) {
136
- return aggregateChunk(ctx, requests, { ...overrides, blockTag: minBlock })
260
+ // 按区块号重查:落后的节点对未来区块报错或返回较旧区块,同样校验区块号、换节点;仍然都落后时由 retryUntilCaughtUp 稍等重试。
261
+ // 确认节点都落后后不再重查(只等最新状态跟上);因限频、超时等失败时下次照常重查
262
+ try {
263
+ return withoutBlock(await aggregateChunk(ctx, [...requests, BLOCK_NUMBER_REQUEST], { ...overrides, blockTag: minBlock }, atLeast))
264
+ } catch (err) {
265
+ // 所有节点都落后才不再重查;有节点只是限频、超时等失败时下次照常重查
266
+ if (isAllBehind(err)) {
267
+ pinned = true
268
+ }
269
+ throw err
137
270
  }
138
- throw new Error(`Node is behind minBlock ${minBlock}`)
139
271
  })
140
272
  }
141
273
 
274
+ /** 去掉附带的区块号查询(区块号已在每次 eth_call 时由 assertAtLeast 校验过) */
275
+ function withoutBlock(results: RawResult[]): RawResult[] {
276
+ return results.slice(0, -1)
277
+ }
278
+
279
+ /**
280
+ * 是否有节点落后:结果早于 minBlock(NodeBehindError),或对按区块号的查询报“区块不存在”(header not found 等);
281
+ * 多节点时其中有节点是这样(其余可能是限频、超时等)
282
+ */
283
+ function isBehindFailure(err: unknown): boolean {
284
+ return isBehindError(err) || (err instanceof AllNodesFailedError && err.errors.some((e) => isBehindError(e.error)))
285
+ }
286
+
287
+ /** 所有节点都落后(单节点时就是它落后) */
288
+ function isAllBehind(err: unknown): boolean {
289
+ return isBehindError(err) || (err instanceof AllNodesFailedError && err.errors.every((e) => isBehindError(e.error)))
290
+ }
291
+
292
+ function isBehindError(err: unknown): boolean {
293
+ return err instanceof NodeBehindError || isBehind(err)
294
+ }
295
+
142
296
  /** 节点落后时的重试:确定性错误(合约执行结果)直接抛;其余错误每秒重试,最多约 10 秒;signal 取消后停止 */
143
297
  async function retryUntilCaughtUp<T>(overrides: CallOverrides, run: () => Promise<T>): Promise<T> {
144
298
  const deadline = Date.now() + MIN_BLOCK_DEADLINE
@@ -199,6 +353,8 @@ async function aggregateWithRpcBalances(ctx: AggregateContext, requests: CallReq
199
353
  const block = blockOf(results.pop())
200
354
  otherResults = results
201
355
  const blockTag = block === null ? overrides.blockTag : Number(block)
356
+ // 已知限制:eth_getBalance 不返回区块号,没法校验。读余额的节点如果忽略区块参数、又比给出 blockTag 的节点落后,
357
+ // 主币余额可能比代币余额早几个块(仍不早于 minBlock 的节点才会给出 blockTag 之后的区块)
202
358
  const read = () => Promise.all(owners.map((owner) => ctx.provider.getBalance(owner, blockTag)))
203
359
  // 只有 minBlock 才等待重试(读的区块可能来自比当前节点更新的节点);只要 withBlock 时出错照常抛出
204
360
  balances = await (overrides.minBlock !== undefined ? retryUntilCaughtUp(overrides, read) : read())
@@ -216,13 +372,13 @@ async function aggregateWithRpcBalances(ctx: AggregateContext, requests: CallReq
216
372
  }
217
373
 
218
374
  /** 执行一批请求;其中有区块号查询时顺带读 Arbitrum 的 L2 区块号,并用它替换(见 ARB_BLOCK_NUMBER_REQUEST) */
219
- async function aggregateChunk(ctx: AggregateContext, requests: CallRequest[], overrides: CallOverrides): Promise<RawResult[]> {
375
+ async function aggregateChunk(ctx: AggregateContext, requests: CallRequest[], overrides: CallOverrides, atLeast?: bigint): Promise<RawResult[]> {
220
376
  if (!requests.some((req) => req.blockNumber)) {
221
- return aggregateRawChunk(ctx, requests, overrides)
377
+ return aggregateRawChunk(ctx, requests, overrides, atLeast)
222
378
  }
223
- const results = await aggregateRawChunk(ctx, [...requests, ARB_BLOCK_NUMBER_REQUEST], overrides)
379
+ const results = await aggregateRawChunk(ctx, [...requests, ARB_BLOCK_NUMBER_REQUEST], overrides, atLeast)
224
380
  const arb = results.pop() as RawResult
225
- if (arb.success && arb.returnData.length === 66) {
381
+ if (l2BlockOf(arb) !== null) {
226
382
  requests.forEach((req, i) => {
227
383
  if (req.blockNumber) {
228
384
  results[i] = arb
@@ -236,48 +392,74 @@ async function aggregateRawChunk(
236
392
  ctx: AggregateContext,
237
393
  requests: CallRequest[],
238
394
  overrides: CallOverrides,
395
+ atLeast?: bigint,
239
396
  ): Promise<RawResult[]> {
240
397
  const multicall = ctx.multicall
241
398
  if (multicall && isUsable(ctx.chainId, multicall, overrides.blockTag)) {
242
- const results = await callContract(ctx, multicall, requests, overrides)
399
+ const key = cacheKey(ctx.chainId, multicall.address)
400
+ const results = await callContract(ctx, multicall, requests, overrides, atLeast)
243
401
  if (Array.isArray(results)) {
402
+ invalidStrikes.delete(key)
244
403
  return results
245
404
  }
246
- // 只有“最新状态下地址上没有可用合约”才记缓存。
405
+ // 只有“最新状态下地址上没有可用合约”才记缓存(10 分钟后重新尝试):
406
+ // - 'invalid'(返回空数据 / 无法解码)要确认过才记:多个节点结果一致,或单节点连续 2 次——节点偶发返回一次 0x 不算
247
407
  // - revert 无数据('reverted')有歧义:可能是 out of gas / 节点 gas 上限、子调用失败而钱包丢了 revert 数据,
248
408
  // 这些情况地址本身是好的,只对这一次退回 deployless,不记缓存
249
409
  // - 历史区块查到无代码可能只是那时还没部署,不能推及 latest
250
- if (results === 'invalid' && isLatest(overrides.blockTag)) {
251
- unusableMulticalls.add(cacheKey(ctx.chainId, multicall.address))
410
+ if (results !== 'reverted' && isLatest(overrides.blockTag)) {
411
+ const strikes = (invalidStrikes.get(key) ?? 0) + 1
412
+ if (results === 'invalid-confirmed' || strikes >= 2) {
413
+ unusableMulticalls.set(key, Date.now() + UNUSABLE_TTL)
414
+ invalidStrikes.delete(key)
415
+ } else {
416
+ invalidStrikes.set(key, strikes)
417
+ }
252
418
  }
253
419
  }
254
- return callDeployless(ctx, requests, overrides)
420
+ return callDeployless(ctx, requests, overrides, atLeast)
255
421
  }
256
422
 
257
423
  /**
258
- * 'invalid':地址上没有代码或返回无法解码;'reverted':执行 revert 且没有 revert 数据。
259
- * 两种情况都退回 deployless。
424
+ * 'invalid':返回空数据(地址上没有代码)或无法解码——经 FallbackRpc 时会先换节点重试;
425
+ * 所有节点(2 个以上)都这样时为 'invalid-confirmed'。'reverted':执行 revert 且没有 revert 数据。
426
+ * 这几种情况都退回 deployless。
260
427
  */
261
428
  async function callContract(
262
429
  ctx: AggregateContext,
263
430
  multicall: Multicall,
264
431
  requests: CallRequest[],
265
432
  overrides: CallOverrides,
266
- ): Promise<RawResult[] | 'invalid' | 'reverted'> {
433
+ atLeast?: bigint,
434
+ ): Promise<RawResult[] | 'invalid' | 'invalid-confirmed' | 'reverted'> {
267
435
  const calls = requests.map((req) => ({
268
436
  target: req.ethBalanceOf || req.blockNumber ? multicall.address : req.target,
269
437
  allowFailure: req.allowFailure,
270
438
  callData: req.callData,
271
439
  }))
272
- let data: string
273
440
  try {
274
- data = await ctx.provider.call({
275
- to: multicall.address,
276
- data: multicall3Interface.encodeFunctionData('aggregate3', [calls]),
277
- blockTag: overrides.blockTag,
278
- from: overrides.from,
279
- })
441
+ // 地址上没有代码时 eth_call 返回 0x;空数据 / 无法解码的结果换下一个节点(节点偶发出错时别的节点能给出正确结果)
442
+ return await callAggregate3(
443
+ ctx.provider,
444
+ { to: multicall.address, data: multicall3Interface.encodeFunctionData('aggregate3', [calls]), blockTag: overrides.blockTag, from: overrides.from },
445
+ requests,
446
+ atLeast,
447
+ )
280
448
  } catch (err) {
449
+ if (err instanceof InvalidResultError) {
450
+ return 'invalid'
451
+ }
452
+ // 有节点只是落后(minBlock):不能据此判断合约不存在,按落后处理(由 minBlock 的等待重试负责),不记次数
453
+ // (只看 minBlock 校验得出的 NodeBehindError;“区块不存在”的报错和空数据混在一起时照常退回 deployless)
454
+ if (atLeast !== undefined && (err instanceof NodeBehindError || (err instanceof AllNodesFailedError && err.errors.some((e) => e.error instanceof NodeBehindError)))) {
455
+ throw err
456
+ }
457
+ // 有节点返回空数据、其余节点出错(超时、限频等):合约可能不存在,退回 deployless;
458
+ // 只有 2 个以上节点都返回空数据才算确认,记缓存
459
+ if (err instanceof AllNodesFailedError && err.errors.some((e) => e.error instanceof InvalidResultError)) {
460
+ const invalid = err.errors.filter((e) => e.error instanceof InvalidResultError).length
461
+ return invalid >= 2 && invalid === err.errors.length ? 'invalid-confirmed' : 'invalid'
462
+ }
281
463
  // aggregate3 自己的失败(allowFailure=false 的子调用失败)会带 "Multicall3: call failed" 的 revert 数据;
282
464
  // 执行层面 revert 且没有数据,基本就是这个地址上不是 Multicall3。
283
465
  // 限流、节点缺状态等错误 ethers 也会包装成 CALL_EXCEPTION,那些要原样抛出,不能把地址判成无效
@@ -286,15 +468,6 @@ async function callContract(
286
468
  }
287
469
  throw err
288
470
  }
289
- // 地址上没有代码时 eth_call 返回 0x
290
- if (!data || data === '0x') {
291
- return 'invalid'
292
- }
293
- try {
294
- return decodeAggregate3(multicall3Interface.decodeFunctionResult('aggregate3', data)[0])
295
- } catch {
296
- return 'invalid'
297
- }
298
471
  }
299
472
 
300
473
  /**
@@ -308,9 +481,10 @@ async function callDeployless(
308
481
  ctx: AggregateContext,
309
482
  requests: CallRequest[],
310
483
  overrides: CallOverrides,
484
+ atLeast?: bigint,
311
485
  ): Promise<RawResult[]> {
312
486
  const chunks = splitByInitcodeSize(requests)
313
- const results = await Promise.all(chunks.map((chunk) => callDeploylessChunk(ctx, chunk, overrides)))
487
+ const results = await Promise.all(chunks.map((chunk) => callDeploylessChunk(ctx, chunk, overrides, atLeast)))
314
488
  return results.flat()
315
489
  }
316
490
 
@@ -318,42 +492,48 @@ async function callDeploylessChunk(
318
492
  ctx: AggregateContext,
319
493
  requests: CallRequest[],
320
494
  overrides: CallOverrides,
495
+ atLeast?: bigint,
321
496
  ): Promise<RawResult[]> {
497
+ // minBlock:每片都附带区块号查询并各自校验——只校验带区块号的那一片的话,其他片可能来自落后节点、混进旧数据
498
+ const sent =
499
+ atLeast === undefined
500
+ ? requests
501
+ : [
502
+ ...requests,
503
+ ...(requests.includes(BLOCK_NUMBER_REQUEST) ? [] : [BLOCK_NUMBER_REQUEST]),
504
+ ...(requests.includes(ARB_BLOCK_NUMBER_REQUEST) ? [] : [ARB_BLOCK_NUMBER_REQUEST]),
505
+ ]
322
506
  const args = AbiCoder.defaultAbiCoder().encode(
323
507
  [CALL3_TUPLE],
324
508
  [
325
- requests.map(({ target, allowFailure, callData, ethBalanceOf, blockNumber }) => ({
509
+ sent.map(({ target, allowFailure, callData, ethBalanceOf, blockNumber }) => ({
326
510
  target: ethBalanceOf || blockNumber ? MULTICALL3_ADDRESS : target,
327
511
  allowFailure,
328
512
  callData,
329
513
  })),
330
514
  ],
331
515
  )
332
- let data: string
333
516
  try {
334
- data = await ctx.provider.call({
335
- data: concat([DEPLOYLESS_MULTICALL3_BYTECODE, args]),
336
- blockTag: overrides.blockTag,
337
- from: overrides.from,
338
- })
517
+ // 返回空数据 / 无法解码(节点不支持合约创建式 eth_call,或偶发出错)时换下一个节点
518
+ const results = await callAggregate3(ctx.provider, { data: concat([DEPLOYLESS_MULTICALL3_BYTECODE, args]), blockTag: overrides.blockTag, from: overrides.from }, sent, atLeast)
519
+ return results.slice(0, requests.length)
339
520
  } catch (err) {
340
- const revertData = (err as { data?: string | null }).data
341
- if (isError(err, 'CALL_EXCEPTION') && revertData?.startsWith(AGGREGATE3_RESULT_SELECTOR)) {
342
- // 结果超过 24KB,合约通过 revert Aggregate3Result(...) 带回
343
- return decodeAggregate3(AbiCoder.defaultAbiCoder().decode([RESULT_TUPLE], dataSlice(revertData, 4))[0])
521
+ // 结果超过 24KB,合约通过 revert Aggregate3Result(...) 带回(区块号已在 callAggregate3 里校验)
522
+ const reverted = revertedResults(err)
523
+ if (reverted) {
524
+ return reverted.slice(0, requests.length)
344
525
  }
345
526
  // 有的钱包 / 中间层会丢掉 revert 数据,大结果拿不回来:对半拆开重试,小批量能正常 return
346
527
  if (requests.length > 1 && isExecutionError(err) && !hasRevertData(err)) {
347
528
  const mid = Math.ceil(requests.length / 2)
348
529
  const [a, b] = await Promise.all([
349
- callDeploylessChunk(ctx, requests.slice(0, mid), overrides),
350
- callDeploylessChunk(ctx, requests.slice(mid), overrides),
530
+ callDeploylessChunk(ctx, requests.slice(0, mid), overrides, atLeast),
531
+ callDeploylessChunk(ctx, requests.slice(mid), overrides, atLeast),
351
532
  ])
352
533
  return [...a, ...b]
353
534
  }
354
535
  throw err
355
536
  }
356
- return decodeAggregate3(multicall3Interface.decodeFunctionResult('aggregate3', data)[0])
357
537
  }
358
538
 
359
539
  /** EIP-3860 initcode 上限 49152 字节,留出余量 */
@@ -393,8 +573,13 @@ function decodeAggregate3(raw: ReadonlyArray<readonly [boolean, string]>): RawRe
393
573
  }
394
574
 
395
575
  function isUsable(chainId: number, multicall: Multicall, blockTag?: BlockTag): boolean {
396
- if (unusableMulticalls.has(cacheKey(chainId, multicall.address))) {
397
- return false
576
+ const key = cacheKey(chainId, multicall.address)
577
+ const until = unusableMulticalls.get(key)
578
+ if (until !== undefined) {
579
+ if (Date.now() < until) {
580
+ return false
581
+ }
582
+ unusableMulticalls.delete(key)
398
583
  }
399
584
  if (blockTag === 'earliest') {
400
585
  return false
@@ -419,4 +604,5 @@ function cacheKey(chainId: number, address: string): string {
419
604
  /** 测试用:清掉“地址不可用”的缓存 */
420
605
  export function resetMulticallCache(): void {
421
606
  unusableMulticalls.clear()
607
+ invalidStrikes.clear()
422
608
  }
package/src/batcher.ts CHANGED
@@ -29,12 +29,13 @@ interface Queue {
29
29
 
30
30
  /**
31
31
  * 整批请求的 signal:所有调用方都传了 signal、并且都取消后才取消(如 minBlock 的等待重试随之停止);
32
- * 有调用方没传 signal 时返回 undefined(整批照常完成)
32
+ * 有调用方没传 signal 时为 undefined(整批照常完成)。请求结束后调用 dispose,移除挂在调用方 signal 上的监听
33
+ * (调用方可能用一个长期存在的 signal 发起很多次查询)
33
34
  */
34
- function combinedSignal(entries: readonly Pending[]): AbortSignal | undefined {
35
+ function combinedSignal(entries: readonly Pending[]): { signal: AbortSignal | undefined; dispose: () => void } {
35
36
  const signals = [...new Set(entries.map((entry) => entry.signal))]
36
37
  if (!signals.length || signals.some((signal) => signal === undefined)) {
37
- return undefined
38
+ return { signal: undefined, dispose: () => {} }
38
39
  }
39
40
  const controller = new AbortController()
40
41
  const check = () => {
@@ -44,7 +45,7 @@ function combinedSignal(entries: readonly Pending[]): AbortSignal | undefined {
44
45
  }
45
46
  signals.forEach((signal) => signal?.addEventListener('abort', check, { once: true }))
46
47
  check()
47
- return controller.signal
48
+ return { signal: controller.signal, dispose: () => signals.forEach((signal) => signal?.removeEventListener('abort', check)) }
48
49
  }
49
50
 
50
51
  /**
@@ -154,11 +155,14 @@ export class Batcher {
154
155
  }
155
156
 
156
157
  let results: RawResult[]
158
+ const combined = combinedSignal(entries)
157
159
  try {
158
- results = await this.#run(requests, { ...overrides, signal: combinedSignal(entries) })
160
+ results = await this.#run(requests, { ...overrides, signal: combined.signal })
159
161
  } catch (err) {
160
162
  entries.forEach((entry, i) => requestIndex[i] !== -1 && entry.reject(err))
161
163
  return
164
+ } finally {
165
+ combined.dispose()
162
166
  }
163
167
 
164
168
  entries.forEach((entry, i) => {
package/src/erc20.ts CHANGED
@@ -243,13 +243,15 @@ export function importTokenMetaCache(snapshot: unknown): void {
243
243
  const key = `${rawKey.slice(0, separator)}:${rawKey.slice(separator + 1).toLowerCase()}`
244
244
  const meta: CachedMeta = {}
245
245
  if (Number.isInteger(value.decimals) && (value.decimals as number) >= 0 && (value.decimals as number) <= 255) meta.decimals = value.decimals
246
- if (typeof value.name === 'string' && typeof value.nameAt === 'number') {
246
+ const nameAt = validTimestamp(value.nameAt)
247
+ if (typeof value.name === 'string' && nameAt !== null) {
247
248
  meta.name = value.name
248
- meta.nameAt = value.nameAt
249
+ meta.nameAt = nameAt
249
250
  }
250
- if (typeof value.symbol === 'string' && typeof value.symbolAt === 'number') {
251
+ const symbolAt = validTimestamp(value.symbolAt)
252
+ if (typeof value.symbol === 'string' && symbolAt !== null) {
251
253
  meta.symbol = value.symbol
252
- meta.symbolAt = value.symbolAt
254
+ meta.symbolAt = symbolAt
253
255
  }
254
256
  if (value.standard === 'ERC721' || value.standard === 'ERC1155' || value.standard === null) meta.standard = value.standard
255
257
  if (Object.keys(meta).length && !tokenMetaCache.has(key)) {
@@ -261,6 +263,17 @@ export function importTokenMetaCache(snapshot: unknown): void {
261
263
  }
262
264
  }
263
265
 
266
+ /**
267
+ * 导入的写入时间:只接受有限的数值;晚于当前时间的(时钟不准、数据被改过)截到当前时间——否则永不过期。
268
+ * 不合格时为 null(丢弃 name / symbol,之后重新查)
269
+ */
270
+ function validTimestamp(value: unknown): number | null {
271
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
272
+ return null
273
+ }
274
+ return Math.min(value, Date.now())
275
+ }
276
+
264
277
  /** persistTokenMetaCache 的存储:localStorage / sessionStorage 可以直接传,也可以自己实现 */
265
278
  export interface TokenMetaStore {
266
279
  getItem(key: string): string | null