@1delta/providers 0.0.53 → 0.0.55
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/README.md +188 -0
- package/dist/index.js +40 -9
- package/dist/index.mjs +40 -9
- package/package.json +2 -2
- package/src/chains/README.md +45 -0
- package/src/client/README.md +51 -0
- package/src/multicall/README.md +104 -0
- package/src/multicall/multicall.ts +50 -11
- package/src/rpc/README.md +41 -0
- package/src/transport/README.md +28 -0
- package/src/utils/README.md +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# @1delta/providers
|
|
2
|
+
|
|
3
|
+
Resilient EVM provider infrastructure: chain mapping, RPC management, transport creation, and batched multicall with automatic retry/rotation.
|
|
4
|
+
|
|
5
|
+
## Module Reference
|
|
6
|
+
|
|
7
|
+
| Module | README | Purpose |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `chains/` | [chains/README.md](src/chains/README.md) | Chain enum → viem `Chain` mapping, custom chain definitions |
|
|
10
|
+
| `client/` | [client/README.md](src/client/README.md) | `PublicClient` creation with RPC fallback/rotation |
|
|
11
|
+
| `rpc/` | [rpc/README.md](src/rpc/README.md) | Default RPC URL pools per chain (`LIST_OVERRIDES`) |
|
|
12
|
+
| `transport/` | [transport/README.md](src/transport/README.md) | HTTP/WebSocket viem transport factory |
|
|
13
|
+
| `multicall/` | [multicall/README.md](src/multicall/README.md) | Batched multicall with retry, RPC rotation, revert isolation |
|
|
14
|
+
| `utils/` | [utils/README.md](src/utils/README.md) | Shared constants and helpers |
|
|
15
|
+
|
|
16
|
+
Everything is re-exported from the package root via `src/index.ts → src/evm.ts`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Building Efficient Batched Multicalls
|
|
21
|
+
|
|
22
|
+
The core pattern used across this repo is: **build a flat call array from heterogeneous sources, fire one multicall, then walk the results array using tracked offsets.** This minimizes RPC round-trips by packing as many reads as possible into a single `multicall3` invocation.
|
|
23
|
+
|
|
24
|
+
### 1. Define the Call Shape
|
|
25
|
+
|
|
26
|
+
Each call is a plain object:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
interface Call {
|
|
30
|
+
address: string // target contract
|
|
31
|
+
name: string // function name (must match ABI)
|
|
32
|
+
params?: any[] // function arguments (alias: args)
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Build Calls — Track Lengths per Source
|
|
37
|
+
|
|
38
|
+
The key insight is to **concatenate calls from different protocols into one flat array** while recording how many calls each source contributed, so you can slice the results later.
|
|
39
|
+
|
|
40
|
+
From [`margin-fetcher/src/flash-liquidity/fetchLiquidity.ts`](../margin-fetcher/src/flash-liquidity/fetchLiquidity.ts):
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
let callLengths: { [protocol: string]: number } = {}
|
|
44
|
+
let aaveCalls: Call[] = []
|
|
45
|
+
|
|
46
|
+
// Aave forks: 2 calls per asset (balanceOf + getConfiguration) + 1 premium call
|
|
47
|
+
aaveProtocols.forEach((aaveFork) => {
|
|
48
|
+
const underlyingsAndATokens = Object.entries(
|
|
49
|
+
getAaveStyleProtocolTokenMap(chain, aaveFork),
|
|
50
|
+
)
|
|
51
|
+
const pool = getAaveTypePoolAddress(chain, aaveFork)
|
|
52
|
+
|
|
53
|
+
const tokenCalls = underlyingsAndATokens.flatMap(
|
|
54
|
+
([underlying, tokens]: [string, any]) => [
|
|
55
|
+
{ name: 'balanceOf', address: underlying, params: [tokens.aToken] },
|
|
56
|
+
{ name: 'getConfiguration', address: pool, params: [underlying] },
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Track length so we know how many results belong to this fork
|
|
61
|
+
callLengths[aaveFork] = tokenCalls.length + 1
|
|
62
|
+
|
|
63
|
+
aaveCalls.push(...tokenCalls, {
|
|
64
|
+
name: 'FLASHLOAN_PREMIUM_TOTAL',
|
|
65
|
+
address: pool,
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For uniform sources (same call per asset), a helper keeps it concise:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const buildBalanceCalls = (
|
|
74
|
+
forks: { pool: string; address: string }[],
|
|
75
|
+
): Call[] => {
|
|
76
|
+
const result: Call[] = []
|
|
77
|
+
for (const fork of forks) {
|
|
78
|
+
callLengths[fork.pool] = unifiedAssets.length
|
|
79
|
+
for (const address of unifiedAssets) {
|
|
80
|
+
result.push(
|
|
81
|
+
address === zeroAddress
|
|
82
|
+
? { name: 'getEthBalance', address: MULTICALL_ADDRESS[chain], params: [fork.address] }
|
|
83
|
+
: { name: 'balanceOf', address, params: [fork.address] },
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 3. Concatenate and Fire
|
|
92
|
+
|
|
93
|
+
Merge all sub-arrays into one flat call list and execute a single multicall:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const calls = [
|
|
97
|
+
...aaveCalls,
|
|
98
|
+
...balancerV2Calls,
|
|
99
|
+
...morphoCalls,
|
|
100
|
+
...balancerV3Calls,
|
|
101
|
+
...uniswapV4Calls,
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
const rawResults = await multicallRetryUniversal({
|
|
105
|
+
chain,
|
|
106
|
+
calls,
|
|
107
|
+
abi: FlashAbi, // single ABI covering all function signatures
|
|
108
|
+
batchSize: 4096,
|
|
109
|
+
allowFailure: true, // failed calls return '0x' instead of throwing
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**ABI tip:** When all calls use functions from the same ABI (or a merged superset ABI), pass a single ABI. When calls target different contracts with different ABIs, pass an array of ABIs (one per call) — see [multicall/README.md](src/multicall/README.md).
|
|
114
|
+
|
|
115
|
+
### 4. Unwrap Results with Offset Walking
|
|
116
|
+
|
|
117
|
+
The results array is **positionally aligned** with the calls array. Use the tracked lengths to slice each source's chunk:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
let currentOffset = 0
|
|
121
|
+
|
|
122
|
+
aaveProtocols.forEach((aave) => {
|
|
123
|
+
const callLen = aaveAssets[aave].length * 2 + 1 // 2 calls per asset + 1 premium
|
|
124
|
+
|
|
125
|
+
// Slice this fork's results
|
|
126
|
+
const data = rawResults.slice(currentOffset, currentOffset + callLen)
|
|
127
|
+
currentOffset += callLen // advance the cursor
|
|
128
|
+
|
|
129
|
+
// Last result is the premium call
|
|
130
|
+
const fee = data[callLen - 1]
|
|
131
|
+
if (typeof fee !== 'bigint') return // skip fork if premium call failed
|
|
132
|
+
|
|
133
|
+
// Walk interleaved results: [balance, config, balance, config, ...]
|
|
134
|
+
aaveAssets[aave].forEach((asset, i) => {
|
|
135
|
+
const rawAmount = data[2 * i] // balanceOf result
|
|
136
|
+
const config = data[2 * i + 1] // getConfiguration result
|
|
137
|
+
|
|
138
|
+
if (typeof rawAmount !== 'bigint' || typeof config !== 'bigint') return
|
|
139
|
+
// ... process asset
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Uniform sources are simpler — one result per asset
|
|
144
|
+
balancerV2s.forEach((balancer) => {
|
|
145
|
+
const data = rawResults.slice(currentOffset, currentOffset + unifiedAssets.length)
|
|
146
|
+
currentOffset += unifiedAssets.length
|
|
147
|
+
|
|
148
|
+
unifiedAssets.forEach((asset, i) => {
|
|
149
|
+
const rawAmount = data[i]
|
|
150
|
+
if (typeof rawAmount === 'bigint' && rawAmount > 0n) {
|
|
151
|
+
// ... process asset
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 5. Handling Failed Calls
|
|
158
|
+
|
|
159
|
+
With `allowFailure: true`, failed calls return `'0x'`. Guard with type checks:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// For bigint returns (balances, configs, etc.)
|
|
163
|
+
const isValidResult = (v: any): v is bigint => typeof v === 'bigint'
|
|
164
|
+
|
|
165
|
+
if (!isValidResult(rawAmount)) return // skip this entry
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Quick Reference
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { multicallRetryUniversal } from '@1delta/providers'
|
|
174
|
+
|
|
175
|
+
// Minimal call
|
|
176
|
+
const results = await multicallRetryUniversal({
|
|
177
|
+
chain: '8453',
|
|
178
|
+
calls: [
|
|
179
|
+
{ address: tokenAddr, name: 'balanceOf', params: [account] },
|
|
180
|
+
{ address: tokenAddr, name: 'decimals' },
|
|
181
|
+
],
|
|
182
|
+
abi: ERC20_ABI,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const [balance, decimals] = results
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
For custom RPCs or factory usage, see [multicall/README.md](src/multicall/README.md) and [client/README.md](src/client/README.md).
|
package/dist/index.js
CHANGED
|
@@ -25401,18 +25401,27 @@ function isHttpError(e) {
|
|
|
25401
25401
|
const status = current.status;
|
|
25402
25402
|
if (status && status >= 400) return true;
|
|
25403
25403
|
const details = current.details ?? "";
|
|
25404
|
-
const message = current.message ?? "";
|
|
25404
|
+
const message = current.message ?? current.shortMessage ?? "";
|
|
25405
25405
|
const combined = `${message} ${details}`;
|
|
25406
25406
|
if (HTTP_ERROR_STRINGS.some((s) => combined.includes(s))) return true;
|
|
25407
25407
|
current = current.cause ?? null;
|
|
25408
25408
|
}
|
|
25409
25409
|
}
|
|
25410
25410
|
if (e instanceof Error) {
|
|
25411
|
-
const combined = `${e.message ?? ""} ${e.details ?? ""}`;
|
|
25411
|
+
const combined = `${e.message ?? e.shortMessage ?? ""} ${e.details ?? ""}`;
|
|
25412
25412
|
if (HTTP_ERROR_STRINGS.some((s) => combined.includes(s))) return true;
|
|
25413
25413
|
}
|
|
25414
25414
|
return false;
|
|
25415
25415
|
}
|
|
25416
|
+
function formatViemError(e) {
|
|
25417
|
+
if (e instanceof BaseError2) {
|
|
25418
|
+
const cause = e.walk()?.shortMessage;
|
|
25419
|
+
const short = e.shortMessage;
|
|
25420
|
+
return cause && cause !== short ? `${e.name}: ${short} | cause: ${cause}` : `${e.name}: ${short}`;
|
|
25421
|
+
}
|
|
25422
|
+
if (e instanceof Error) return `${e.name}: ${e.message}`;
|
|
25423
|
+
return String(e);
|
|
25424
|
+
}
|
|
25416
25425
|
function isOutOfGasError(e) {
|
|
25417
25426
|
const msg = e instanceof Error ? (e.message ?? "") + (e.details ?? "") : "";
|
|
25418
25427
|
return msg.includes("out of gas") || msg.includes("gas exhausted");
|
|
@@ -25451,9 +25460,24 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
|
|
|
25451
25460
|
})),
|
|
25452
25461
|
allowFailure
|
|
25453
25462
|
});
|
|
25454
|
-
|
|
25455
|
-
|
|
25456
|
-
|
|
25463
|
+
let resolvedData;
|
|
25464
|
+
if (allowFailure) {
|
|
25465
|
+
const mapped = data.map(
|
|
25466
|
+
({ result, status, error }) => ({
|
|
25467
|
+
ok: status === "success",
|
|
25468
|
+
result: status !== "success" ? "0x" : result,
|
|
25469
|
+
error
|
|
25470
|
+
})
|
|
25471
|
+
);
|
|
25472
|
+
const allFailed = mapped.length > 0 && mapped.every((m) => !m.ok);
|
|
25473
|
+
if (allFailed) {
|
|
25474
|
+
const firstError = mapped[0]?.error;
|
|
25475
|
+
throw firstError ?? new Error("All multicall results failed");
|
|
25476
|
+
}
|
|
25477
|
+
resolvedData = mapped.map((m) => m.result);
|
|
25478
|
+
} else {
|
|
25479
|
+
resolvedData = data;
|
|
25480
|
+
}
|
|
25457
25481
|
if (revertedIndices.size > 0) {
|
|
25458
25482
|
const finalResults = [];
|
|
25459
25483
|
let filteredIndex = 0;
|
|
@@ -25508,7 +25532,7 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
|
|
|
25508
25532
|
}
|
|
25509
25533
|
}
|
|
25510
25534
|
if (isOutOfGasError(e) && batchSize > 1) {
|
|
25511
|
-
if (logErrors) console.debug(e);
|
|
25535
|
+
if (logErrors) console.debug("[multicall] OOG", formatViemError(e));
|
|
25512
25536
|
return await multicallRetry2(
|
|
25513
25537
|
chain,
|
|
25514
25538
|
calls,
|
|
@@ -25525,7 +25549,10 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
|
|
|
25525
25549
|
if (maxSkips > 0) {
|
|
25526
25550
|
if (logErrors) {
|
|
25527
25551
|
const tag = isHttpError(e) ? "HTTP" : "unknown-transient";
|
|
25528
|
-
console.debug(
|
|
25552
|
+
console.debug(
|
|
25553
|
+
`[multicall] ${tag} error on provider ${providerId}, skipping:`,
|
|
25554
|
+
formatViemError(e)
|
|
25555
|
+
);
|
|
25529
25556
|
}
|
|
25530
25557
|
return await multicallRetry2(
|
|
25531
25558
|
chain,
|
|
@@ -25542,10 +25569,14 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
|
|
|
25542
25569
|
}
|
|
25543
25570
|
if (maxRetries === 0) {
|
|
25544
25571
|
if (!allowFailure) throw e;
|
|
25545
|
-
if (logErrors) console.debug(e);
|
|
25572
|
+
if (logErrors) console.debug("[multicall]", formatViemError(e));
|
|
25546
25573
|
return Array(calls.length).fill("0x");
|
|
25547
25574
|
}
|
|
25548
|
-
if (logErrors)
|
|
25575
|
+
if (logErrors)
|
|
25576
|
+
console.debug(
|
|
25577
|
+
`[multicall] retrying on provider ${providerId + 1}:`,
|
|
25578
|
+
formatViemError(e)
|
|
25579
|
+
);
|
|
25549
25580
|
return await multicallRetry2(
|
|
25550
25581
|
chain,
|
|
25551
25582
|
calls,
|
package/dist/index.mjs
CHANGED
|
@@ -11640,18 +11640,27 @@ function isHttpError(e) {
|
|
|
11640
11640
|
const status = current.status;
|
|
11641
11641
|
if (status && status >= 400) return true;
|
|
11642
11642
|
const details = current.details ?? "";
|
|
11643
|
-
const message = current.message ?? "";
|
|
11643
|
+
const message = current.message ?? current.shortMessage ?? "";
|
|
11644
11644
|
const combined = `${message} ${details}`;
|
|
11645
11645
|
if (HTTP_ERROR_STRINGS.some((s) => combined.includes(s))) return true;
|
|
11646
11646
|
current = current.cause ?? null;
|
|
11647
11647
|
}
|
|
11648
11648
|
}
|
|
11649
11649
|
if (e instanceof Error) {
|
|
11650
|
-
const combined = `${e.message ?? ""} ${e.details ?? ""}`;
|
|
11650
|
+
const combined = `${e.message ?? e.shortMessage ?? ""} ${e.details ?? ""}`;
|
|
11651
11651
|
if (HTTP_ERROR_STRINGS.some((s) => combined.includes(s))) return true;
|
|
11652
11652
|
}
|
|
11653
11653
|
return false;
|
|
11654
11654
|
}
|
|
11655
|
+
function formatViemError(e) {
|
|
11656
|
+
if (e instanceof BaseError) {
|
|
11657
|
+
const cause = e.walk()?.shortMessage;
|
|
11658
|
+
const short = e.shortMessage;
|
|
11659
|
+
return cause && cause !== short ? `${e.name}: ${short} | cause: ${cause}` : `${e.name}: ${short}`;
|
|
11660
|
+
}
|
|
11661
|
+
if (e instanceof Error) return `${e.name}: ${e.message}`;
|
|
11662
|
+
return String(e);
|
|
11663
|
+
}
|
|
11655
11664
|
function isOutOfGasError(e) {
|
|
11656
11665
|
const msg = e instanceof Error ? (e.message ?? "") + (e.details ?? "") : "";
|
|
11657
11666
|
return msg.includes("out of gas") || msg.includes("gas exhausted");
|
|
@@ -11690,9 +11699,24 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
|
|
|
11690
11699
|
})),
|
|
11691
11700
|
allowFailure
|
|
11692
11701
|
});
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11702
|
+
let resolvedData;
|
|
11703
|
+
if (allowFailure) {
|
|
11704
|
+
const mapped = data.map(
|
|
11705
|
+
({ result, status, error }) => ({
|
|
11706
|
+
ok: status === "success",
|
|
11707
|
+
result: status !== "success" ? "0x" : result,
|
|
11708
|
+
error
|
|
11709
|
+
})
|
|
11710
|
+
);
|
|
11711
|
+
const allFailed = mapped.length > 0 && mapped.every((m) => !m.ok);
|
|
11712
|
+
if (allFailed) {
|
|
11713
|
+
const firstError = mapped[0]?.error;
|
|
11714
|
+
throw firstError ?? new Error("All multicall results failed");
|
|
11715
|
+
}
|
|
11716
|
+
resolvedData = mapped.map((m) => m.result);
|
|
11717
|
+
} else {
|
|
11718
|
+
resolvedData = data;
|
|
11719
|
+
}
|
|
11696
11720
|
if (revertedIndices.size > 0) {
|
|
11697
11721
|
const finalResults = [];
|
|
11698
11722
|
let filteredIndex = 0;
|
|
@@ -11747,7 +11771,7 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
|
|
|
11747
11771
|
}
|
|
11748
11772
|
}
|
|
11749
11773
|
if (isOutOfGasError(e) && batchSize > 1) {
|
|
11750
|
-
if (logErrors) console.debug(e);
|
|
11774
|
+
if (logErrors) console.debug("[multicall] OOG", formatViemError(e));
|
|
11751
11775
|
return await multicallRetry2(
|
|
11752
11776
|
chain,
|
|
11753
11777
|
calls,
|
|
@@ -11764,7 +11788,10 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
|
|
|
11764
11788
|
if (maxSkips > 0) {
|
|
11765
11789
|
if (logErrors) {
|
|
11766
11790
|
const tag = isHttpError(e) ? "HTTP" : "unknown-transient";
|
|
11767
|
-
console.debug(
|
|
11791
|
+
console.debug(
|
|
11792
|
+
`[multicall] ${tag} error on provider ${providerId}, skipping:`,
|
|
11793
|
+
formatViemError(e)
|
|
11794
|
+
);
|
|
11768
11795
|
}
|
|
11769
11796
|
return await multicallRetry2(
|
|
11770
11797
|
chain,
|
|
@@ -11781,10 +11808,14 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
|
|
|
11781
11808
|
}
|
|
11782
11809
|
if (maxRetries === 0) {
|
|
11783
11810
|
if (!allowFailure) throw e;
|
|
11784
|
-
if (logErrors) console.debug(e);
|
|
11811
|
+
if (logErrors) console.debug("[multicall]", formatViemError(e));
|
|
11785
11812
|
return Array(calls.length).fill("0x");
|
|
11786
11813
|
}
|
|
11787
|
-
if (logErrors)
|
|
11814
|
+
if (logErrors)
|
|
11815
|
+
console.debug(
|
|
11816
|
+
`[multicall] retrying on provider ${providerId + 1}:`,
|
|
11817
|
+
formatViemError(e)
|
|
11818
|
+
);
|
|
11788
11819
|
return await multicallRetry2(
|
|
11789
11820
|
chain,
|
|
11790
11821
|
calls,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1delta/providers",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.55",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"vitest": "^4.0.18",
|
|
21
21
|
"@1delta/chain-registry": "0.0.4",
|
|
22
|
-
"@1delta/data-sdk": "0.0.
|
|
22
|
+
"@1delta/data-sdk": "0.0.22"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"tsup": "^8.5.1",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Chains Module
|
|
2
|
+
|
|
3
|
+
Maps chain identifiers to viem `Chain` objects, including custom chain definitions for networks not yet in viem.
|
|
4
|
+
|
|
5
|
+
## Exported Functions
|
|
6
|
+
|
|
7
|
+
### `getEvmChain(chain: string): Chain`
|
|
8
|
+
|
|
9
|
+
Resolves a chain enum string (from `@1delta/chain-registry`) to a viem `Chain` object.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { getEvmChain } from '@1delta/providers'
|
|
13
|
+
|
|
14
|
+
const chain = getEvmChain('BASE') // returns viem's base chain
|
|
15
|
+
const chain = getEvmChain('MONAD_MAINNET') // returns custom-defined chain
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Throws `"Not in VIEM: <chain>"` if the chain is unrecognized.
|
|
19
|
+
|
|
20
|
+
Supports 80+ chains including Ethereum, Arbitrum, Base, Polygon, Optimism, BSC, Avalanche, Scroll, Linea, Mantle, zkSync, Sonic, Kaia, Blast, and many more. Some viem-native chains get patched with custom `multicall3` contract addresses (e.g. Hemi, Degen, Ink, Story).
|
|
21
|
+
|
|
22
|
+
### Custom Chains
|
|
23
|
+
|
|
24
|
+
Chains not available in viem are defined via `defineChain()` in `customChains.ts`:
|
|
25
|
+
|
|
26
|
+
| Export | Chain | ID |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `katana` | Katana | 747474 |
|
|
29
|
+
| `plasma` | Plasma Mainnet | 9745 |
|
|
30
|
+
| `customChains.artela` | Artela | 11820 |
|
|
31
|
+
| `customChains.botanix` | Botanix | 3637 |
|
|
32
|
+
| `customChains.crossfi` | CrossFi | 4158 |
|
|
33
|
+
| `customChains.GraphLinq` | GraphLinq | 614 |
|
|
34
|
+
| `customChains.hyperEvm` | Hyper EVM | 999 |
|
|
35
|
+
| `customChains.monadMainnet` | Monad | 143 |
|
|
36
|
+
| `customChains.swellchain` | Swellchain | 1923 |
|
|
37
|
+
| `customChains.tacMainnet` | TAC | 239 |
|
|
38
|
+
|
|
39
|
+
All include RPC URLs, block explorers, native currency, and `multicall3` contract addresses.
|
|
40
|
+
|
|
41
|
+
## Adding a New Chain
|
|
42
|
+
|
|
43
|
+
1. If the chain exists in `viem/chains`, add a case to the switch in `chainMapping.ts`.
|
|
44
|
+
2. If the chain does not exist in viem, define it with `defineChain()` in `customChains.ts`, add it to the `customChains` object, then add a case in `chainMapping.ts` referencing it.
|
|
45
|
+
3. Add RPC URLs in `../rpc/rpcOverrides.ts` under `LIST_OVERRIDES`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Client Module
|
|
2
|
+
|
|
3
|
+
Creates viem `PublicClient` instances for any supported EVM chain, with RPC fallback and rotation.
|
|
4
|
+
|
|
5
|
+
## Preferred API
|
|
6
|
+
|
|
7
|
+
Use the `Universal` (object-params) variants — positional-args versions are deprecated.
|
|
8
|
+
|
|
9
|
+
### `getEvmClientUniversal({ chain, rpcId }): PublicClient`
|
|
10
|
+
|
|
11
|
+
Creates a `PublicClient` using default RPCs (from viem chain config + `LIST_OVERRIDES`).
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { getEvmClientUniversal } from '@1delta/providers'
|
|
15
|
+
|
|
16
|
+
const client = getEvmClientUniversal({ chain: 'BASE', rpcId: 0 })
|
|
17
|
+
const block = await client.getBlockNumber()
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `rpcId: 0` uses the viem default RPC or the first override.
|
|
21
|
+
- `rpcId: N` merges override RPCs with viem defaults, deduplicates, and picks index `N`.
|
|
22
|
+
|
|
23
|
+
### `getEvmClientWithCustomRpcsUniversal({ chain, rpcId, customRpcs }): PublicClient`
|
|
24
|
+
|
|
25
|
+
Same as above but lets you inject your own RPC list per chain.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { getEvmClientWithCustomRpcsUniversal } from '@1delta/providers'
|
|
29
|
+
|
|
30
|
+
const client = getEvmClientWithCustomRpcsUniversal({
|
|
31
|
+
chain: 'ETHEREUM_MAINNET',
|
|
32
|
+
rpcId: 0,
|
|
33
|
+
customRpcs: {
|
|
34
|
+
ETHEREUM_MAINNET: ['https://my-rpc.example.com'],
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
When `customRpcs[chain]` is provided, the RPC index wraps around with modulo (`rpcId % list.length`), enabling round-robin rotation across retries.
|
|
40
|
+
|
|
41
|
+
When `customRpcs[chain]` is absent, falls back to the same logic as `getEvmClientUniversal`.
|
|
42
|
+
|
|
43
|
+
## How the Multicall Uses This
|
|
44
|
+
|
|
45
|
+
The multicall module calls `getEvmClientWithCustomRpcsUniversal` internally, incrementing `providerId` on each retry to rotate through available RPCs.
|
|
46
|
+
|
|
47
|
+
## RPC Resolution Order
|
|
48
|
+
|
|
49
|
+
1. If custom RPCs provided for the chain → use those (modulo index).
|
|
50
|
+
2. Else if `rpcId === 0` → viem default RPC, falling back to `LIST_OVERRIDES[chain][0]`.
|
|
51
|
+
3. Else → merge `LIST_OVERRIDES` + viem defaults, deduplicate, clamp index.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Multicall Module
|
|
2
|
+
|
|
3
|
+
Resilient EVM multicall wrapper around viem's `multicall` with automatic retry, RPC rotation, batch splitting, and revert isolation.
|
|
4
|
+
|
|
5
|
+
## Preferred API
|
|
6
|
+
|
|
7
|
+
Use `multicallRetryUniversal` (params object) — the positional-args `multicallRetry` is deprecated.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { multicallRetryUniversal } from '@1delta/providers'
|
|
11
|
+
|
|
12
|
+
const results = await multicallRetryUniversal({
|
|
13
|
+
chain, // chain ID string, e.g. '1', '8453'
|
|
14
|
+
calls, // array of call descriptors (see below)
|
|
15
|
+
abi, // single ABI array, or array-of-ABIs (one per call)
|
|
16
|
+
batchSize, // optional – default 1024
|
|
17
|
+
maxRetries, // optional – default 5
|
|
18
|
+
providerId, // optional – starting RPC index, default 0
|
|
19
|
+
allowFailure, // optional – default false; when true, failed calls return '0x'
|
|
20
|
+
logErrors, // optional – default false
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Call descriptor shape
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
{
|
|
28
|
+
address: string // contract address
|
|
29
|
+
name: string // function name
|
|
30
|
+
args?: any[] // function arguments (alias: `params`)
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Extra fields (e.g. `user` for bookkeeping) are ignored by the multicall but preserved on the input array for post-processing.
|
|
35
|
+
|
|
36
|
+
### ABI modes
|
|
37
|
+
|
|
38
|
+
- **Single ABI** — one ABI array shared by all calls (most common):
|
|
39
|
+
```ts
|
|
40
|
+
{ abi: MY_ABI, calls: [...] }
|
|
41
|
+
```
|
|
42
|
+
- **Per-call ABI** — array of ABIs, one per call (for heterogeneous batches):
|
|
43
|
+
```ts
|
|
44
|
+
{ abi: [ABI_A, ABI_B, ...], calls: [callA, callB, ...] }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Error handling / retry strategy
|
|
48
|
+
|
|
49
|
+
The module classifies errors into three buckets and handles each differently:
|
|
50
|
+
|
|
51
|
+
| Error type | Detection | Action |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| **Contract revert** | viem `ContractFunctionRevertedError` or `execution reverted` in message | Identifies the reverting call(s) by matching address + function + args, excludes them, retries remaining calls. Excluded calls return `'0x'`. |
|
|
54
|
+
| **Out of gas** | `out of gas` / `gas exhausted` in message | Halves `batchSize` and retries (same RPC). |
|
|
55
|
+
| **Transient / HTTP** | Connection errors, rate limits, 4xx/5xx, timeouts | Skips to next RPC (`providerId + 1`) without consuming a retry. After `maxSkips` (6) exhausted, consumes a retry and resets skips. |
|
|
56
|
+
|
|
57
|
+
When all retries are exhausted:
|
|
58
|
+
- `allowFailure: true` → returns `Array(calls.length).fill('0x')`
|
|
59
|
+
- `allowFailure: false` → throws the last error
|
|
60
|
+
|
|
61
|
+
## Factory variant
|
|
62
|
+
|
|
63
|
+
Use `createMulticallRetryUniversal` to inject custom RPC lists:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { createMulticallRetryUniversal } from '@1delta/providers'
|
|
67
|
+
|
|
68
|
+
const multicall = createMulticallRetryUniversal({
|
|
69
|
+
'1': ['https://rpc1.example.com', 'https://rpc2.example.com'],
|
|
70
|
+
'8453': ['https://base-rpc.example.com'],
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const results = await multicall({ chain: '8453', calls, abi })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The default RPC list comes from `LIST_OVERRIDES` in `../rpc/rpcOverrides`.
|
|
77
|
+
|
|
78
|
+
## Usage example
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { multicallRetryUniversal } from '@1delta/providers'
|
|
82
|
+
import { POOL_ABI } from './abi'
|
|
83
|
+
|
|
84
|
+
const calls = users.map((user) => ({
|
|
85
|
+
address: poolAddress,
|
|
86
|
+
name: 'getUserAccountData',
|
|
87
|
+
params: [user],
|
|
88
|
+
}))
|
|
89
|
+
|
|
90
|
+
const results = await multicallRetryUniversal({
|
|
91
|
+
chain: '1',
|
|
92
|
+
calls,
|
|
93
|
+
abi: POOL_ABI,
|
|
94
|
+
allowFailure: true,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
results.forEach((res, i) => {
|
|
98
|
+
if (res === '0x') {
|
|
99
|
+
// call reverted or failed
|
|
100
|
+
} else {
|
|
101
|
+
// res is the decoded return value
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
```
|
|
@@ -71,7 +71,7 @@ function isHttpError(e: unknown): boolean {
|
|
|
71
71
|
const status = current.status as number | undefined
|
|
72
72
|
if (status && status >= 400) return true
|
|
73
73
|
const details: string = current.details ?? ''
|
|
74
|
-
const message: string = current.message ?? ''
|
|
74
|
+
const message: string = current.message ?? current.shortMessage ?? ''
|
|
75
75
|
const combined = `${message} ${details}`
|
|
76
76
|
if (HTTP_ERROR_STRINGS.some((s) => combined.includes(s))) return true
|
|
77
77
|
current = current.cause ?? null
|
|
@@ -80,13 +80,26 @@ function isHttpError(e: unknown): boolean {
|
|
|
80
80
|
|
|
81
81
|
// Fallback: check message + details as strings for non-BaseError errors
|
|
82
82
|
if (e instanceof Error) {
|
|
83
|
-
const combined = `${e.message ?? ''} ${(e as any).details ?? ''}`
|
|
83
|
+
const combined = `${e.message ?? (e as any).shortMessage ?? ''} ${(e as any).details ?? ''}`
|
|
84
84
|
if (HTTP_ERROR_STRINGS.some((s) => combined.includes(s))) return true
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
return false
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** Compact viem/error formatter — avoids dumping full ABI/calldata/args */
|
|
91
|
+
function formatViemError(e: unknown): string {
|
|
92
|
+
if (e instanceof BaseError) {
|
|
93
|
+
const cause = (e.walk() as any)?.shortMessage
|
|
94
|
+
const short = e.shortMessage
|
|
95
|
+
return cause && cause !== short
|
|
96
|
+
? `${e.name}: ${short} | cause: ${cause}`
|
|
97
|
+
: `${e.name}: ${short}`
|
|
98
|
+
}
|
|
99
|
+
if (e instanceof Error) return `${e.name}: ${e.message}`
|
|
100
|
+
return String(e)
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
/** Return true if the multicall ran out of gas (batch too large for the RPC gas limit) */
|
|
91
104
|
function isOutOfGasError(e: unknown): boolean {
|
|
92
105
|
const msg =
|
|
@@ -175,11 +188,30 @@ export function createMulticallRetry(
|
|
|
175
188
|
|
|
176
189
|
// When allowFailure is true, viem returns { result, status } per call.
|
|
177
190
|
// Extract results and convert failures to '0x'.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
182
|
-
|
|
191
|
+
let resolvedData: any[]
|
|
192
|
+
if (allowFailure) {
|
|
193
|
+
const mapped = (data as any[]).map(
|
|
194
|
+
({ result, status, error }: any) => ({
|
|
195
|
+
ok: status === 'success',
|
|
196
|
+
result: status !== 'success' ? '0x' : result,
|
|
197
|
+
error,
|
|
198
|
+
}),
|
|
199
|
+
)
|
|
200
|
+
const allFailed = mapped.length > 0 && mapped.every((m) => !m.ok)
|
|
201
|
+
|
|
202
|
+
// If EVERY call in the batch failed, the cause is almost certainly
|
|
203
|
+
// a transport-level error (429, 502, timeout) that viem caught
|
|
204
|
+
// internally instead of throwing. Re-throw so the retry logic
|
|
205
|
+
// below (skip to next RPC / consume a retry) handles it.
|
|
206
|
+
if (allFailed) {
|
|
207
|
+
const firstError = mapped[0]?.error
|
|
208
|
+
throw firstError ?? new Error('All multicall results failed')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
resolvedData = mapped.map((m) => m.result)
|
|
212
|
+
} else {
|
|
213
|
+
resolvedData = data as any[]
|
|
214
|
+
}
|
|
183
215
|
|
|
184
216
|
if (revertedIndices.size > 0) {
|
|
185
217
|
const finalResults: any[] = []
|
|
@@ -248,7 +280,7 @@ export function createMulticallRetry(
|
|
|
248
280
|
|
|
249
281
|
// Out-of-gas: batch too large for the RPC gas limit — halve batch size and retry
|
|
250
282
|
if (isOutOfGasError(e) && batchSize > 1) {
|
|
251
|
-
if (logErrors) console.debug(e)
|
|
283
|
+
if (logErrors) console.debug('[multicall] OOG', formatViemError(e))
|
|
252
284
|
return await multicallRetry(
|
|
253
285
|
chain,
|
|
254
286
|
calls,
|
|
@@ -269,7 +301,10 @@ export function createMulticallRetry(
|
|
|
269
301
|
if (maxSkips > 0) {
|
|
270
302
|
if (logErrors) {
|
|
271
303
|
const tag = isHttpError(e) ? 'HTTP' : 'unknown-transient'
|
|
272
|
-
console.debug(
|
|
304
|
+
console.debug(
|
|
305
|
+
`[multicall] ${tag} error on provider ${providerId}, skipping:`,
|
|
306
|
+
formatViemError(e),
|
|
307
|
+
)
|
|
273
308
|
}
|
|
274
309
|
return await multicallRetry(
|
|
275
310
|
chain,
|
|
@@ -287,12 +322,16 @@ export function createMulticallRetry(
|
|
|
287
322
|
|
|
288
323
|
if (maxRetries === 0) {
|
|
289
324
|
if (!allowFailure) throw e
|
|
290
|
-
if (logErrors) console.debug(e)
|
|
325
|
+
if (logErrors) console.debug('[multicall]', formatViemError(e))
|
|
291
326
|
return Array(calls.length).fill('0x')
|
|
292
327
|
}
|
|
293
328
|
|
|
294
329
|
// Skips exhausted — consume a retry and rotate RPC
|
|
295
|
-
if (logErrors)
|
|
330
|
+
if (logErrors)
|
|
331
|
+
console.debug(
|
|
332
|
+
`[multicall] retrying on provider ${providerId + 1}:`,
|
|
333
|
+
formatViemError(e),
|
|
334
|
+
)
|
|
296
335
|
|
|
297
336
|
return await multicallRetry(
|
|
298
337
|
chain,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# RPC Module
|
|
2
|
+
|
|
3
|
+
Provides default RPC URL lists per chain used as fallbacks throughout the providers package.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
### `LIST_OVERRIDES: Record<string, string[]>`
|
|
8
|
+
|
|
9
|
+
A mapping of chain identifiers to arrays of public/semi-public RPC URLs. Used as the default RPC pool by the client and multicall modules.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { LIST_OVERRIDES } from '@1delta/providers'
|
|
13
|
+
|
|
14
|
+
// e.g. LIST_OVERRIDES['BASE'] → ['https://mainnet.base.org', 'https://base.drpc.org', ...]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Covered chains include: Ethereum, Base, Polygon, Arbitrum, Optimism, BSC, Avalanche, Scroll, Linea, Mantle, Sonic, Kaia, Blast, Gnosis, Mode, Metis, Fantom, opBNB, Manta, Sei, Monad, HyperEVM, Swellchain, TAC, Botanix, Unichain, and more.
|
|
18
|
+
|
|
19
|
+
### `filterHttpRpcs(rpcs: string[]): string[]`
|
|
20
|
+
|
|
21
|
+
Filters out WebSocket (`wss://`) URLs, returning only HTTP endpoints.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { filterHttpRpcs } from '@1delta/providers'
|
|
25
|
+
|
|
26
|
+
const httpOnly = filterHttpRpcs(['https://rpc.example.com', 'wss://ws.example.com'])
|
|
27
|
+
// → ['https://rpc.example.com']
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Adding RPCs for a New Chain
|
|
31
|
+
|
|
32
|
+
Add an entry to the `LIST_OVERRIDES` object in `rpcOverrides.ts`:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
LIST_OVERRIDES['MY_NEW_CHAIN'] = [
|
|
36
|
+
'https://rpc1.mychain.io',
|
|
37
|
+
'https://rpc2.mychain.io',
|
|
38
|
+
]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Order matters — index 0 is the default RPC used when `rpcId === 0`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Transport Module
|
|
2
|
+
|
|
3
|
+
Creates viem transports (HTTP or WebSocket) from a URL string.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
### `createTransport(url: string, config?: object)`
|
|
8
|
+
|
|
9
|
+
Returns an `http()` or `webSocket()` viem transport based on the URL scheme.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createTransport } from '@1delta/providers'
|
|
13
|
+
|
|
14
|
+
const t1 = createTransport('https://rpc.example.com') // → http transport
|
|
15
|
+
const t2 = createTransport('wss://ws.example.com') // → webSocket transport
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Key detail:** `retryCount` defaults to `0` (no viem-level retries). This is intentional — retry logic is handled at the multicall layer, which rotates RPCs on failure. Allowing viem to retry the same failing RPC would add latency without benefit.
|
|
19
|
+
|
|
20
|
+
You can override this via `config`:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
createTransport('https://rpc.example.com', { retryCount: 3 })
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### `getTransport(url: string)`
|
|
27
|
+
|
|
28
|
+
Convenience wrapper — calls `createTransport(url)` with default config.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Utils Module
|
|
2
|
+
|
|
3
|
+
Internal utilities used by the providers package. All are re-exported from the package root.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
### `DEFAULT_BATCH_SIZE`
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
const DEFAULT_BATCH_SIZE = 1024
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Default number of calls per multicall batch. Used by the multicall module when no `batchSize` is specified.
|
|
14
|
+
|
|
15
|
+
### `trimTrailingSlash(url?: string): string | undefined`
|
|
16
|
+
|
|
17
|
+
Removes a trailing `/` from a URL string. Returns `undefined` if input is falsy. Used to deduplicate RPC URLs that may differ only by a trailing slash.
|
|
18
|
+
|
|
19
|
+
### `deepCompare(a: any, b: any): boolean`
|
|
20
|
+
|
|
21
|
+
Deep equality check for objects and arrays. Used by the multicall module to match reverting calls by comparing function arguments. Note: returns `false` for `bigint` values (by design — bigint args are not compared during revert matching).
|
|
22
|
+
|
|
23
|
+
### `uniq<T>(array: T[]): T[]`
|
|
24
|
+
|
|
25
|
+
Returns a new array with duplicate values removed (via `Set`).
|
|
26
|
+
|
|
27
|
+
### `isArray(value: any): boolean`
|
|
28
|
+
|
|
29
|
+
Wrapper around `Array.isArray`. Used by the multicall module to detect whether the ABI parameter is a single ABI or an array of per-call ABIs.
|