@1delta/providers 0.0.41 → 0.0.43
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/dist/{_esm-NMEXBKYZ.mjs → _esm-BLSSJTMS.mjs} +33 -33
- package/dist/{ccip-3Y4YD27I.mjs → ccip-P6QKYDYG.mjs} +1 -1
- package/dist/{chunk-7BYOX3YW.mjs → chunk-GGVKF6RL.mjs} +175 -129
- package/dist/index.d.mts +582 -160
- package/dist/index.d.ts +582 -160
- package/dist/index.js +1424 -1193
- package/dist/index.mjs +815 -640
- package/package.json +5 -6
- package/src/chains/chainMapping.ts +314 -0
- package/src/chains/customChains.ts +202 -0
- package/src/client/client.ts +110 -0
- package/src/evm.ts +7 -1067
- package/src/multicall/multicall.ts +250 -0
- package/src/rpc/rpcOverrides.ts +248 -0
- package/src/transport/transport.ts +17 -0
- package/src/utils/utils.ts +55 -0
- package/test/contract.ts +204 -0
- package/test/multicallRetry.test.ts +808 -0
- package/test/multicallRetry.testUtils.ts +181 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { Chain } from '@1delta/chain-registry'
|
|
3
|
+
import { multicallRetryUniversal } from '../src/multicall/multicall'
|
|
4
|
+
import { testContractAddress } from './contract'
|
|
5
|
+
import {
|
|
6
|
+
encodeMulticallResponse,
|
|
7
|
+
createMockFetch,
|
|
8
|
+
type FetchCallHistory,
|
|
9
|
+
} from './multicallRetry.testUtils'
|
|
10
|
+
|
|
11
|
+
describe('multicallRetry - Revert Handling', () => {
|
|
12
|
+
const chain = Chain.ETHEREUM_MAINNET
|
|
13
|
+
const overrides = {
|
|
14
|
+
[Chain.ETHEREUM_MAINNET]: [
|
|
15
|
+
'https://eth.llamarpc.com',
|
|
16
|
+
'https://api.zan.top/eth-mainnet',
|
|
17
|
+
'https://rpc.flashbots.net/fast',
|
|
18
|
+
'https://rpc.owlracle.info/eth/70d38ce1826c4a60bb2a8e05a6c8b20f',
|
|
19
|
+
'https://eth.merkle.io',
|
|
20
|
+
'https://eth-mainnet.nodereal.io/v1/1659dfb40aa24bbb8153a677b98064d7',
|
|
21
|
+
'wss://eth-mainnet.nodereal.io/ws/v1/1659dfb40aa24bbb8153a677b98064d7',
|
|
22
|
+
'https://rpc.payload.de',
|
|
23
|
+
'https://ethereum-rpc.publicnode.com',
|
|
24
|
+
'https://eth-mainnet.g.alchemy.com/v2/demo',
|
|
25
|
+
'https://go.getblock.io/aefd01aa907c4805ba3c00a9e5b48c6b',
|
|
26
|
+
'https://rpc.flashbots.net',
|
|
27
|
+
'https://public-eth.nownodes.io',
|
|
28
|
+
'https://ethereum-json-rpc.stakely.io',
|
|
29
|
+
'https://eth.blockrazor.xyz',
|
|
30
|
+
'https://eth.drpc.org',
|
|
31
|
+
'https://ethereum.public.blockpi.network/v1/rpc/public',
|
|
32
|
+
'https://ethereum-public.nodies.app',
|
|
33
|
+
'https://0xrpc.io/eth',
|
|
34
|
+
],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
it('handle single revert, return 0x for revert', async () => {
|
|
38
|
+
const calls = [
|
|
39
|
+
{
|
|
40
|
+
address: testContractAddress.address,
|
|
41
|
+
name: 'ownerOf',
|
|
42
|
+
args: [543322n],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
address: testContractAddress.address,
|
|
46
|
+
name: 'name',
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const results = await multicallRetryUniversal({
|
|
51
|
+
chain,
|
|
52
|
+
calls,
|
|
53
|
+
abi: testContractAddress.abi,
|
|
54
|
+
batchSize: 4096,
|
|
55
|
+
maxRetries: 3,
|
|
56
|
+
providerId: 0,
|
|
57
|
+
allowFailure: true,
|
|
58
|
+
logErrors: false,
|
|
59
|
+
overrdies: overrides,
|
|
60
|
+
})
|
|
61
|
+
expect(results).toHaveLength(2)
|
|
62
|
+
expect(results[0]).toBe('0x')
|
|
63
|
+
expect(results[1]).not.toBe('0x')
|
|
64
|
+
expect(typeof results[1]).toBe('string')
|
|
65
|
+
}, 30000)
|
|
66
|
+
|
|
67
|
+
it('should handle multiple reverts', async () => {
|
|
68
|
+
const calls = [
|
|
69
|
+
{
|
|
70
|
+
address: testContractAddress.address,
|
|
71
|
+
name: 'ownerOf',
|
|
72
|
+
args: [543322n],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
address: testContractAddress.address,
|
|
76
|
+
name: 'name',
|
|
77
|
+
args: [],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
address: testContractAddress.address,
|
|
81
|
+
name: 'ownerOf',
|
|
82
|
+
args: [999999n],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
address: testContractAddress.address,
|
|
86
|
+
name: 'symbol',
|
|
87
|
+
args: [],
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
const results = await multicallRetryUniversal({
|
|
92
|
+
chain,
|
|
93
|
+
calls,
|
|
94
|
+
abi: testContractAddress.abi,
|
|
95
|
+
batchSize: 4096,
|
|
96
|
+
maxRetries: 3,
|
|
97
|
+
providerId: 0,
|
|
98
|
+
allowFailure: true,
|
|
99
|
+
logErrors: false,
|
|
100
|
+
overrdies: overrides,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(results).toHaveLength(4)
|
|
104
|
+
expect(results[0]).toBe('0x')
|
|
105
|
+
expect(results[1]).not.toBe('0x')
|
|
106
|
+
expect(results[2]).toBe('0x')
|
|
107
|
+
expect(results[3]).not.toBe('0x')
|
|
108
|
+
}, 30000)
|
|
109
|
+
|
|
110
|
+
it('should handle duplicate reverted calls', async () => {
|
|
111
|
+
const calls = [
|
|
112
|
+
{
|
|
113
|
+
address: testContractAddress.address,
|
|
114
|
+
name: 'ownerOf',
|
|
115
|
+
args: [543322n],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
address: testContractAddress.address,
|
|
119
|
+
name: 'name',
|
|
120
|
+
args: [],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
address: testContractAddress.address,
|
|
124
|
+
name: 'ownerOf',
|
|
125
|
+
args: [543322n],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
address: testContractAddress.address,
|
|
129
|
+
name: 'symbol',
|
|
130
|
+
args: [],
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
const results = await multicallRetryUniversal({
|
|
135
|
+
chain,
|
|
136
|
+
calls,
|
|
137
|
+
abi: testContractAddress.abi,
|
|
138
|
+
batchSize: 4096,
|
|
139
|
+
maxRetries: 3,
|
|
140
|
+
providerId: 0,
|
|
141
|
+
allowFailure: true,
|
|
142
|
+
logErrors: false,
|
|
143
|
+
overrdies: overrides,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
expect(results).toHaveLength(4)
|
|
147
|
+
expect(results[0]).toBe('0x')
|
|
148
|
+
expect(results[1]).not.toBe('0x')
|
|
149
|
+
expect(results[2]).toBe('0x')
|
|
150
|
+
expect(results[3]).not.toBe('0x')
|
|
151
|
+
}, 30000)
|
|
152
|
+
|
|
153
|
+
it('should handle all calls reverting', async () => {
|
|
154
|
+
const calls = [
|
|
155
|
+
{
|
|
156
|
+
address: testContractAddress.address,
|
|
157
|
+
name: 'ownerOf',
|
|
158
|
+
args: [543322n],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
address: testContractAddress.address,
|
|
162
|
+
name: 'ownerOf',
|
|
163
|
+
args: [999999n],
|
|
164
|
+
},
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
const results = await multicallRetryUniversal({
|
|
168
|
+
chain,
|
|
169
|
+
calls,
|
|
170
|
+
abi: testContractAddress.abi,
|
|
171
|
+
batchSize: 4096,
|
|
172
|
+
maxRetries: 3,
|
|
173
|
+
providerId: 0,
|
|
174
|
+
allowFailure: true,
|
|
175
|
+
logErrors: false,
|
|
176
|
+
overrdies: overrides,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
expect(results).toHaveLength(2)
|
|
180
|
+
expect(results[0]).toBe('0x')
|
|
181
|
+
expect(results[1]).toBe('0x')
|
|
182
|
+
}, 30000)
|
|
183
|
+
|
|
184
|
+
it('should handle all calls succeeding', async () => {
|
|
185
|
+
const calls = [
|
|
186
|
+
{
|
|
187
|
+
address: testContractAddress.address,
|
|
188
|
+
name: 'name',
|
|
189
|
+
args: [],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
address: testContractAddress.address,
|
|
193
|
+
name: 'symbol',
|
|
194
|
+
args: [],
|
|
195
|
+
},
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
const results = await multicallRetryUniversal({
|
|
199
|
+
chain,
|
|
200
|
+
calls,
|
|
201
|
+
abi: testContractAddress.abi,
|
|
202
|
+
batchSize: 4096,
|
|
203
|
+
maxRetries: 3,
|
|
204
|
+
providerId: 0,
|
|
205
|
+
allowFailure: true,
|
|
206
|
+
logErrors: false,
|
|
207
|
+
overrdies: overrides,
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
expect(results).toHaveLength(2)
|
|
211
|
+
expect(results[0]).not.toBe('0x')
|
|
212
|
+
expect(results[1]).not.toBe('0x')
|
|
213
|
+
expect(typeof results[0]).toBe('string')
|
|
214
|
+
expect(typeof results[1]).toBe('string')
|
|
215
|
+
}, 30000)
|
|
216
|
+
|
|
217
|
+
it('should maintain correct order of results with reverts', async () => {
|
|
218
|
+
const calls = [
|
|
219
|
+
{
|
|
220
|
+
address: testContractAddress.address,
|
|
221
|
+
name: 'name',
|
|
222
|
+
args: [],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
address: testContractAddress.address,
|
|
226
|
+
name: 'ownerOf',
|
|
227
|
+
args: [543322n],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
address: testContractAddress.address,
|
|
231
|
+
name: 'symbol',
|
|
232
|
+
args: [],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
address: testContractAddress.address,
|
|
236
|
+
name: 'ownerOf',
|
|
237
|
+
args: [999999n],
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
address: testContractAddress.address,
|
|
241
|
+
name: 'totalSupply',
|
|
242
|
+
args: [],
|
|
243
|
+
},
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
const results = await multicallRetryUniversal({
|
|
247
|
+
chain,
|
|
248
|
+
calls,
|
|
249
|
+
abi: testContractAddress.abi,
|
|
250
|
+
batchSize: 4096,
|
|
251
|
+
maxRetries: 3,
|
|
252
|
+
providerId: 0,
|
|
253
|
+
allowFailure: true,
|
|
254
|
+
logErrors: false,
|
|
255
|
+
overrdies: overrides,
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
expect(results).toHaveLength(5)
|
|
259
|
+
expect(results[0]).not.toBe('0x')
|
|
260
|
+
expect(results[1]).toBe('0x')
|
|
261
|
+
expect(results[2]).not.toBe('0x')
|
|
262
|
+
expect(results[3]).toBe('0x')
|
|
263
|
+
expect(results[4]).not.toBe('0x')
|
|
264
|
+
}, 30000)
|
|
265
|
+
|
|
266
|
+
it('should handle duplicates and multiple reverts', async () => {
|
|
267
|
+
const calls = [
|
|
268
|
+
{
|
|
269
|
+
address: testContractAddress.address,
|
|
270
|
+
name: 'name',
|
|
271
|
+
args: [],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
address: testContractAddress.address,
|
|
275
|
+
name: 'ownerOf',
|
|
276
|
+
args: [543322n],
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
address: testContractAddress.address,
|
|
280
|
+
name: 'ownerOf',
|
|
281
|
+
args: [543322n],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
address: testContractAddress.address,
|
|
285
|
+
name: 'symbol',
|
|
286
|
+
args: [],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
address: testContractAddress.address,
|
|
290
|
+
name: 'ownerOf',
|
|
291
|
+
args: [999999n],
|
|
292
|
+
},
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
const results = await multicallRetryUniversal({
|
|
296
|
+
chain,
|
|
297
|
+
calls,
|
|
298
|
+
abi: testContractAddress.abi,
|
|
299
|
+
batchSize: 4096,
|
|
300
|
+
maxRetries: 3,
|
|
301
|
+
providerId: 0,
|
|
302
|
+
allowFailure: true,
|
|
303
|
+
logErrors: false,
|
|
304
|
+
overrdies: overrides,
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
expect(results).toHaveLength(5)
|
|
308
|
+
expect(results[0]).not.toBe('0x')
|
|
309
|
+
expect(results[1]).toBe('0x')
|
|
310
|
+
expect(results[2]).toBe('0x')
|
|
311
|
+
expect(results[3]).not.toBe('0x')
|
|
312
|
+
expect(results[4]).toBe('0x')
|
|
313
|
+
}, 30000)
|
|
314
|
+
|
|
315
|
+
it('should work with allowFailure=false for non-revert errors', async () => {
|
|
316
|
+
const calls = [
|
|
317
|
+
{
|
|
318
|
+
address: testContractAddress.address,
|
|
319
|
+
name: 'name',
|
|
320
|
+
args: [],
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
address: testContractAddress.address,
|
|
324
|
+
name: 'symbol',
|
|
325
|
+
args: [],
|
|
326
|
+
},
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
const results = await multicallRetryUniversal({
|
|
330
|
+
chain,
|
|
331
|
+
calls,
|
|
332
|
+
abi: testContractAddress.abi,
|
|
333
|
+
batchSize: 4096,
|
|
334
|
+
maxRetries: 3,
|
|
335
|
+
providerId: 0,
|
|
336
|
+
allowFailure: false,
|
|
337
|
+
logErrors: false,
|
|
338
|
+
overrdies: overrides,
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
expect(results).toHaveLength(2)
|
|
342
|
+
expect(results[0]).not.toBe('0x')
|
|
343
|
+
expect(results[1]).not.toBe('0x')
|
|
344
|
+
}, 30000)
|
|
345
|
+
|
|
346
|
+
it('should handle revert with allowFailure=false', async () => {
|
|
347
|
+
const calls = [
|
|
348
|
+
{
|
|
349
|
+
address: testContractAddress.address,
|
|
350
|
+
name: 'ownerOf',
|
|
351
|
+
args: [543322n],
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
address: testContractAddress.address,
|
|
355
|
+
name: 'name',
|
|
356
|
+
args: [],
|
|
357
|
+
},
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
const results = await multicallRetryUniversal({
|
|
361
|
+
chain,
|
|
362
|
+
calls,
|
|
363
|
+
abi: testContractAddress.abi,
|
|
364
|
+
batchSize: 4096,
|
|
365
|
+
maxRetries: 3,
|
|
366
|
+
providerId: 0,
|
|
367
|
+
allowFailure: false,
|
|
368
|
+
logErrors: false,
|
|
369
|
+
overrdies: overrides,
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
expect(results).toHaveLength(2)
|
|
373
|
+
expect(results[0]).toBe('0x')
|
|
374
|
+
expect(results[1]).not.toBe('0x')
|
|
375
|
+
}, 30000)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
describe('multicallRetry - HTTP and JSON-RPC Error Retry', () => {
|
|
379
|
+
const chain = Chain.ETHEREUM_MAINNET
|
|
380
|
+
const rpc1 = 'https://rpc1.test.com'
|
|
381
|
+
const rpc2 = 'https://rpc2.test.com'
|
|
382
|
+
const mockName = 'TestToken'
|
|
383
|
+
const mockSymbol = 'TEST'
|
|
384
|
+
|
|
385
|
+
let originalFetch: typeof fetch
|
|
386
|
+
let fetchCallHistory: FetchCallHistory[]
|
|
387
|
+
|
|
388
|
+
beforeEach(() => {
|
|
389
|
+
fetchCallHistory = []
|
|
390
|
+
originalFetch = globalThis.fetch
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
afterEach(() => {
|
|
394
|
+
globalThis.fetch = originalFetch
|
|
395
|
+
fetchCallHistory = []
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('should retry on JSON-RPC error object from first RPC', async () => {
|
|
399
|
+
const calls = [
|
|
400
|
+
{
|
|
401
|
+
address: testContractAddress.address,
|
|
402
|
+
name: 'name',
|
|
403
|
+
args: [],
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
address: testContractAddress.address,
|
|
407
|
+
name: 'symbol',
|
|
408
|
+
args: [],
|
|
409
|
+
},
|
|
410
|
+
]
|
|
411
|
+
|
|
412
|
+
const mockName = 'TestToken'
|
|
413
|
+
const mockSymbol = 'TEST'
|
|
414
|
+
const successResult = encodeMulticallResponse(
|
|
415
|
+
calls,
|
|
416
|
+
testContractAddress.abi,
|
|
417
|
+
[mockName, mockSymbol],
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
globalThis.fetch = createMockFetch(
|
|
421
|
+
{ [rpc1]: 'jsonrpc-error', [rpc2]: 'success' },
|
|
422
|
+
successResult,
|
|
423
|
+
fetchCallHistory,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
const results = await multicallRetryUniversal({
|
|
427
|
+
chain,
|
|
428
|
+
calls,
|
|
429
|
+
abi: testContractAddress.abi,
|
|
430
|
+
batchSize: 4096,
|
|
431
|
+
maxRetries: 3,
|
|
432
|
+
providerId: 0,
|
|
433
|
+
allowFailure: true,
|
|
434
|
+
logErrors: false,
|
|
435
|
+
overrdies: { [chain]: [rpc1, rpc2] },
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
expect(results).toHaveLength(2)
|
|
439
|
+
expect(results[0]).toBe(mockName)
|
|
440
|
+
expect(results[1]).toBe(mockSymbol)
|
|
441
|
+
expect(fetchCallHistory.length).toBe(2)
|
|
442
|
+
expect(fetchCallHistory[0].url).toBe(rpc1)
|
|
443
|
+
expect(fetchCallHistory[1].url).toBe(rpc2)
|
|
444
|
+
}, 30000)
|
|
445
|
+
|
|
446
|
+
it('should retry on HTTP 429 from first RPC', async () => {
|
|
447
|
+
const calls = [
|
|
448
|
+
{
|
|
449
|
+
address: testContractAddress.address,
|
|
450
|
+
name: 'name',
|
|
451
|
+
args: [],
|
|
452
|
+
},
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
const successResult = encodeMulticallResponse(
|
|
456
|
+
calls,
|
|
457
|
+
testContractAddress.abi,
|
|
458
|
+
[mockName],
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
globalThis.fetch = createMockFetch(
|
|
462
|
+
{ [rpc1]: 'http-429', [rpc2]: 'success' },
|
|
463
|
+
successResult,
|
|
464
|
+
fetchCallHistory,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
const results = await multicallRetryUniversal({
|
|
468
|
+
chain,
|
|
469
|
+
calls,
|
|
470
|
+
abi: testContractAddress.abi,
|
|
471
|
+
batchSize: 4096,
|
|
472
|
+
maxRetries: 3,
|
|
473
|
+
providerId: 0,
|
|
474
|
+
allowFailure: true,
|
|
475
|
+
logErrors: false,
|
|
476
|
+
overrdies: { [chain]: [rpc1, rpc2] },
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
expect(results).toHaveLength(1)
|
|
480
|
+
expect(results[0]).toBe(mockName)
|
|
481
|
+
expect(fetchCallHistory[0].url).toBe(rpc1)
|
|
482
|
+
expect(fetchCallHistory[1].url).toBe(rpc2)
|
|
483
|
+
}, 30000)
|
|
484
|
+
|
|
485
|
+
it('should retry on HTTP 5xx from first RPC', async () => {
|
|
486
|
+
const calls = [
|
|
487
|
+
{
|
|
488
|
+
address: testContractAddress.address,
|
|
489
|
+
name: 'symbol',
|
|
490
|
+
args: [],
|
|
491
|
+
},
|
|
492
|
+
]
|
|
493
|
+
|
|
494
|
+
const successResult = encodeMulticallResponse(
|
|
495
|
+
calls,
|
|
496
|
+
testContractAddress.abi,
|
|
497
|
+
[mockSymbol],
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
globalThis.fetch = createMockFetch(
|
|
501
|
+
{ [rpc1]: 'http-503', [rpc2]: 'success' },
|
|
502
|
+
successResult,
|
|
503
|
+
fetchCallHistory,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
const results = await multicallRetryUniversal({
|
|
507
|
+
chain,
|
|
508
|
+
calls,
|
|
509
|
+
abi: testContractAddress.abi,
|
|
510
|
+
batchSize: 4096,
|
|
511
|
+
maxRetries: 3,
|
|
512
|
+
providerId: 0,
|
|
513
|
+
allowFailure: true,
|
|
514
|
+
logErrors: false,
|
|
515
|
+
overrdies: { [chain]: [rpc1, rpc2] },
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
expect(results).toHaveLength(1)
|
|
519
|
+
expect(results[0]).toBe(mockSymbol)
|
|
520
|
+
expect(fetchCallHistory[0].url).toBe(rpc1)
|
|
521
|
+
expect(fetchCallHistory[1].url).toBe(rpc2)
|
|
522
|
+
}, 30000)
|
|
523
|
+
|
|
524
|
+
it('should return 0x array when all retries are used and allowFailure=true', async () => {
|
|
525
|
+
const calls = [
|
|
526
|
+
{
|
|
527
|
+
address: testContractAddress.address,
|
|
528
|
+
name: 'name',
|
|
529
|
+
args: [],
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
address: testContractAddress.address,
|
|
533
|
+
name: 'symbol',
|
|
534
|
+
args: [],
|
|
535
|
+
},
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
globalThis.fetch = createMockFetch(
|
|
539
|
+
{ [rpc1]: 'always-fail', [rpc2]: 'always-fail' },
|
|
540
|
+
undefined,
|
|
541
|
+
fetchCallHistory,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
const results = await multicallRetryUniversal({
|
|
545
|
+
chain,
|
|
546
|
+
calls,
|
|
547
|
+
abi: testContractAddress.abi,
|
|
548
|
+
batchSize: 4096,
|
|
549
|
+
maxRetries: 3,
|
|
550
|
+
providerId: 0,
|
|
551
|
+
allowFailure: true,
|
|
552
|
+
logErrors: false,
|
|
553
|
+
overrdies: { [chain]: [rpc1, rpc2] },
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
expect(results).toHaveLength(2)
|
|
557
|
+
expect(results[0]).toBe('0x')
|
|
558
|
+
expect(results[1]).toBe('0x')
|
|
559
|
+
expect(fetchCallHistory.length).toBe(4) // 1 initial call + 3 retries
|
|
560
|
+
}, 30000)
|
|
561
|
+
|
|
562
|
+
it('should throw when all retries are used and allowFailure=false', async () => {
|
|
563
|
+
const calls = [
|
|
564
|
+
{
|
|
565
|
+
address: testContractAddress.address,
|
|
566
|
+
name: 'name',
|
|
567
|
+
args: [],
|
|
568
|
+
},
|
|
569
|
+
]
|
|
570
|
+
|
|
571
|
+
globalThis.fetch = createMockFetch(
|
|
572
|
+
{ [rpc1]: 'always-fail', [rpc2]: 'always-fail' },
|
|
573
|
+
undefined,
|
|
574
|
+
fetchCallHistory,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
await expect(
|
|
578
|
+
multicallRetryUniversal({
|
|
579
|
+
chain,
|
|
580
|
+
calls,
|
|
581
|
+
abi: testContractAddress.abi,
|
|
582
|
+
batchSize: 4096,
|
|
583
|
+
maxRetries: 0,
|
|
584
|
+
providerId: 0,
|
|
585
|
+
allowFailure: false,
|
|
586
|
+
logErrors: false,
|
|
587
|
+
overrdies: { [chain]: [rpc1, rpc2] },
|
|
588
|
+
}),
|
|
589
|
+
).rejects.toThrow()
|
|
590
|
+
}, 30000)
|
|
591
|
+
|
|
592
|
+
it('should retry multiple times across all RPCs', async () => {
|
|
593
|
+
const calls = [
|
|
594
|
+
{
|
|
595
|
+
address: testContractAddress.address,
|
|
596
|
+
name: 'name',
|
|
597
|
+
args: [],
|
|
598
|
+
},
|
|
599
|
+
]
|
|
600
|
+
|
|
601
|
+
const successResult = encodeMulticallResponse(
|
|
602
|
+
calls,
|
|
603
|
+
testContractAddress.abi,
|
|
604
|
+
[mockName],
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
const rpc3 = 'https://rpc3.test.com'
|
|
608
|
+
globalThis.fetch = createMockFetch(
|
|
609
|
+
{
|
|
610
|
+
[rpc1]: 'http-503',
|
|
611
|
+
[rpc2]: 'http-503',
|
|
612
|
+
[rpc3]: 'success',
|
|
613
|
+
},
|
|
614
|
+
successResult,
|
|
615
|
+
fetchCallHistory,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
const results = await multicallRetryUniversal({
|
|
619
|
+
chain,
|
|
620
|
+
calls,
|
|
621
|
+
abi: testContractAddress.abi,
|
|
622
|
+
batchSize: 4096,
|
|
623
|
+
maxRetries: 2,
|
|
624
|
+
providerId: 0,
|
|
625
|
+
allowFailure: true,
|
|
626
|
+
logErrors: false,
|
|
627
|
+
overrdies: { [chain]: [rpc1, rpc2, rpc3] },
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
expect(results).toHaveLength(1)
|
|
631
|
+
expect(results[0]).toBe(mockName)
|
|
632
|
+
expect(fetchCallHistory.some((call) => call.url.includes(rpc3))).toBe(true)
|
|
633
|
+
}, 30000)
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
describe('multicallRetry - WebSocket Transport Error Retry', () => {
|
|
637
|
+
const chain = Chain.ETHEREUM_MAINNET
|
|
638
|
+
const wsRpc1 = 'wss://rpc1.test.com'
|
|
639
|
+
const wsRpc2 = 'wss://rpc2.test.com'
|
|
640
|
+
const httpRpc1 = 'https://rpc1.test.com'
|
|
641
|
+
const httpRpc2 = 'https://rpc2.test.com'
|
|
642
|
+
const mockName = 'TestToken'
|
|
643
|
+
const mockSymbol = 'TEST'
|
|
644
|
+
|
|
645
|
+
let originalFetch: typeof fetch
|
|
646
|
+
let fetchCallHistory: FetchCallHistory[]
|
|
647
|
+
|
|
648
|
+
beforeEach(() => {
|
|
649
|
+
fetchCallHistory = []
|
|
650
|
+
originalFetch = globalThis.fetch
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
afterEach(() => {
|
|
654
|
+
globalThis.fetch = originalFetch
|
|
655
|
+
fetchCallHistory = []
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
it('should retry on WebSocket connection failure and switch to HTTP RPC', async () => {
|
|
659
|
+
const calls = [
|
|
660
|
+
{
|
|
661
|
+
address: testContractAddress.address,
|
|
662
|
+
name: 'name',
|
|
663
|
+
args: [],
|
|
664
|
+
},
|
|
665
|
+
]
|
|
666
|
+
|
|
667
|
+
const successResult = encodeMulticallResponse(
|
|
668
|
+
calls,
|
|
669
|
+
testContractAddress.abi,
|
|
670
|
+
[mockName],
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
globalThis.fetch = createMockFetch(
|
|
674
|
+
{ [httpRpc2]: 'success' },
|
|
675
|
+
successResult,
|
|
676
|
+
fetchCallHistory,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
const results = await multicallRetryUniversal({
|
|
680
|
+
chain,
|
|
681
|
+
calls,
|
|
682
|
+
abi: testContractAddress.abi,
|
|
683
|
+
batchSize: 4096,
|
|
684
|
+
maxRetries: 2,
|
|
685
|
+
providerId: 0,
|
|
686
|
+
allowFailure: true,
|
|
687
|
+
logErrors: false,
|
|
688
|
+
overrdies: { [chain]: [wsRpc1, httpRpc2] },
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
expect(results).toHaveLength(1)
|
|
692
|
+
expect(results[0]).toBe(mockName)
|
|
693
|
+
expect(fetchCallHistory.some((call) => call.url.includes(httpRpc2))).toBe(
|
|
694
|
+
true,
|
|
695
|
+
)
|
|
696
|
+
}, 30000)
|
|
697
|
+
|
|
698
|
+
it('should retry across multiple WebSocket RPCs and fallback to HTTP', async () => {
|
|
699
|
+
const calls = [
|
|
700
|
+
{
|
|
701
|
+
address: testContractAddress.address,
|
|
702
|
+
name: 'symbol',
|
|
703
|
+
args: [],
|
|
704
|
+
},
|
|
705
|
+
]
|
|
706
|
+
|
|
707
|
+
const successResult = encodeMulticallResponse(
|
|
708
|
+
calls,
|
|
709
|
+
testContractAddress.abi,
|
|
710
|
+
[mockSymbol],
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
globalThis.fetch = createMockFetch(
|
|
714
|
+
{ [httpRpc2]: 'success' },
|
|
715
|
+
successResult,
|
|
716
|
+
fetchCallHistory,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
const results = await multicallRetryUniversal({
|
|
720
|
+
chain,
|
|
721
|
+
calls,
|
|
722
|
+
abi: testContractAddress.abi,
|
|
723
|
+
batchSize: 4096,
|
|
724
|
+
maxRetries: 2,
|
|
725
|
+
providerId: 0,
|
|
726
|
+
allowFailure: true,
|
|
727
|
+
logErrors: false,
|
|
728
|
+
overrdies: { [chain]: [wsRpc1, wsRpc2, httpRpc2] },
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
expect(results).toHaveLength(1)
|
|
732
|
+
expect(results[0]).toBe(mockSymbol)
|
|
733
|
+
expect(fetchCallHistory.some((call) => call.url.includes(httpRpc2))).toBe(
|
|
734
|
+
true,
|
|
735
|
+
)
|
|
736
|
+
}, 30000)
|
|
737
|
+
|
|
738
|
+
it('should handle mixed WebSocket and HTTP RPCs with HTTP failure', async () => {
|
|
739
|
+
const calls = [
|
|
740
|
+
{
|
|
741
|
+
address: testContractAddress.address,
|
|
742
|
+
name: 'name',
|
|
743
|
+
args: [],
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
address: testContractAddress.address,
|
|
747
|
+
name: 'symbol',
|
|
748
|
+
args: [],
|
|
749
|
+
},
|
|
750
|
+
]
|
|
751
|
+
|
|
752
|
+
const successResult = encodeMulticallResponse(
|
|
753
|
+
calls,
|
|
754
|
+
testContractAddress.abi,
|
|
755
|
+
[mockName, mockSymbol],
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
globalThis.fetch = createMockFetch(
|
|
759
|
+
{ [httpRpc1]: 'http-429', [httpRpc2]: 'success' },
|
|
760
|
+
successResult,
|
|
761
|
+
fetchCallHistory,
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
const results = await multicallRetryUniversal({
|
|
765
|
+
chain,
|
|
766
|
+
calls,
|
|
767
|
+
abi: testContractAddress.abi,
|
|
768
|
+
batchSize: 4096,
|
|
769
|
+
maxRetries: 3,
|
|
770
|
+
providerId: 0,
|
|
771
|
+
allowFailure: true,
|
|
772
|
+
logErrors: false,
|
|
773
|
+
overrdies: { [chain]: [wsRpc1, httpRpc1, httpRpc2] },
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
expect(results).toHaveLength(2)
|
|
777
|
+
expect(results[0]).toBe(mockName)
|
|
778
|
+
expect(results[1]).toBe(mockSymbol)
|
|
779
|
+
expect(fetchCallHistory.some((call) => call.url.includes(httpRpc2))).toBe(
|
|
780
|
+
true,
|
|
781
|
+
)
|
|
782
|
+
}, 30000)
|
|
783
|
+
|
|
784
|
+
it('should handle all WebSocket RPCs failing and return 0x array', async () => {
|
|
785
|
+
const calls = [
|
|
786
|
+
{
|
|
787
|
+
address: testContractAddress.address,
|
|
788
|
+
name: 'name',
|
|
789
|
+
args: [],
|
|
790
|
+
},
|
|
791
|
+
]
|
|
792
|
+
|
|
793
|
+
const results = await multicallRetryUniversal({
|
|
794
|
+
chain,
|
|
795
|
+
calls,
|
|
796
|
+
abi: testContractAddress.abi,
|
|
797
|
+
batchSize: 4096,
|
|
798
|
+
maxRetries: 1,
|
|
799
|
+
providerId: 0,
|
|
800
|
+
allowFailure: true,
|
|
801
|
+
logErrors: false,
|
|
802
|
+
overrdies: { [chain]: [wsRpc1, wsRpc2] },
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
expect(results).toHaveLength(1)
|
|
806
|
+
expect(results[0]).toBe('0x')
|
|
807
|
+
}, 30000)
|
|
808
|
+
})
|