@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/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/aggregate.js +221 -56
- package/dist/aggregate.js.map +1 -1
- package/dist/batcher.d.ts.map +1 -1
- package/dist/batcher.js +9 -4
- package/dist/batcher.js.map +1 -1
- package/dist/erc20.d.ts.map +1 -1
- package/dist/erc20.js +16 -4
- package/dist/erc20.js.map +1 -1
- package/dist/fallback.d.ts +33 -1
- package/dist/fallback.d.ts.map +1 -1
- package/dist/fallback.js +111 -23
- package/dist/fallback.js.map +1 -1
- package/dist/source.js +1 -1
- package/dist/source.js.map +1 -1
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +4 -3
- package/dist/watch.js.map +1 -1
- package/package.json +1 -1
- package/src/aggregate.ts +238 -52
- package/src/batcher.ts +9 -5
- package/src/erc20.ts +17 -4
- package/src/fallback.ts +144 -26
- package/src/source.ts +1 -1
- package/src/watch.ts +6 -3
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
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
return
|
|
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
|
-
|
|
136
|
-
|
|
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
|
|
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
|
|
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
|
|
251
|
-
|
|
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'
|
|
259
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
blockTag: overrides.blockTag,
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return
|
|
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
|
-
|
|
397
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
246
|
+
const nameAt = validTimestamp(value.nameAt)
|
|
247
|
+
if (typeof value.name === 'string' && nameAt !== null) {
|
|
247
248
|
meta.name = value.name
|
|
248
|
-
meta.nameAt =
|
|
249
|
+
meta.nameAt = nameAt
|
|
249
250
|
}
|
|
250
|
-
|
|
251
|
+
const symbolAt = validTimestamp(value.symbolAt)
|
|
252
|
+
if (typeof value.symbol === 'string' && symbolAt !== null) {
|
|
251
253
|
meta.symbol = value.symbol
|
|
252
|
-
meta.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
|