@1delta/providers 0.0.53 → 0.0.54
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 +24 -6
- package/dist/index.mjs +24 -6
- 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 +30 -8
- 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,14 +25401,14 @@ 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;
|
|
@@ -25451,9 +25451,24 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
|
|
|
25451
25451
|
})),
|
|
25452
25452
|
allowFailure
|
|
25453
25453
|
});
|
|
25454
|
-
|
|
25455
|
-
|
|
25456
|
-
|
|
25454
|
+
let resolvedData;
|
|
25455
|
+
if (allowFailure) {
|
|
25456
|
+
const mapped = data.map(
|
|
25457
|
+
({ result, status, error }) => ({
|
|
25458
|
+
ok: status === "success",
|
|
25459
|
+
result: status !== "success" ? "0x" : result,
|
|
25460
|
+
error
|
|
25461
|
+
})
|
|
25462
|
+
);
|
|
25463
|
+
const allFailed = mapped.length > 0 && mapped.every((m) => !m.ok);
|
|
25464
|
+
if (allFailed) {
|
|
25465
|
+
const firstError = mapped[0]?.error;
|
|
25466
|
+
throw firstError ?? new Error("All multicall results failed");
|
|
25467
|
+
}
|
|
25468
|
+
resolvedData = mapped.map((m) => m.result);
|
|
25469
|
+
} else {
|
|
25470
|
+
resolvedData = data;
|
|
25471
|
+
}
|
|
25457
25472
|
if (revertedIndices.size > 0) {
|
|
25458
25473
|
const finalResults = [];
|
|
25459
25474
|
let filteredIndex = 0;
|
|
@@ -25525,7 +25540,10 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
|
|
|
25525
25540
|
if (maxSkips > 0) {
|
|
25526
25541
|
if (logErrors) {
|
|
25527
25542
|
const tag = isHttpError(e) ? "HTTP" : "unknown-transient";
|
|
25528
|
-
console.debug(
|
|
25543
|
+
console.debug(
|
|
25544
|
+
`[multicall] ${tag} error on provider ${providerId}, skipping`,
|
|
25545
|
+
e
|
|
25546
|
+
);
|
|
25529
25547
|
}
|
|
25530
25548
|
return await multicallRetry2(
|
|
25531
25549
|
chain,
|
package/dist/index.mjs
CHANGED
|
@@ -11640,14 +11640,14 @@ 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;
|
|
@@ -11690,9 +11690,24 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
|
|
|
11690
11690
|
})),
|
|
11691
11691
|
allowFailure
|
|
11692
11692
|
});
|
|
11693
|
-
|
|
11694
|
-
|
|
11695
|
-
|
|
11693
|
+
let resolvedData;
|
|
11694
|
+
if (allowFailure) {
|
|
11695
|
+
const mapped = data.map(
|
|
11696
|
+
({ result, status, error }) => ({
|
|
11697
|
+
ok: status === "success",
|
|
11698
|
+
result: status !== "success" ? "0x" : result,
|
|
11699
|
+
error
|
|
11700
|
+
})
|
|
11701
|
+
);
|
|
11702
|
+
const allFailed = mapped.length > 0 && mapped.every((m) => !m.ok);
|
|
11703
|
+
if (allFailed) {
|
|
11704
|
+
const firstError = mapped[0]?.error;
|
|
11705
|
+
throw firstError ?? new Error("All multicall results failed");
|
|
11706
|
+
}
|
|
11707
|
+
resolvedData = mapped.map((m) => m.result);
|
|
11708
|
+
} else {
|
|
11709
|
+
resolvedData = data;
|
|
11710
|
+
}
|
|
11696
11711
|
if (revertedIndices.size > 0) {
|
|
11697
11712
|
const finalResults = [];
|
|
11698
11713
|
let filteredIndex = 0;
|
|
@@ -11764,7 +11779,10 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
|
|
|
11764
11779
|
if (maxSkips > 0) {
|
|
11765
11780
|
if (logErrors) {
|
|
11766
11781
|
const tag = isHttpError(e) ? "HTTP" : "unknown-transient";
|
|
11767
|
-
console.debug(
|
|
11782
|
+
console.debug(
|
|
11783
|
+
`[multicall] ${tag} error on provider ${providerId}, skipping`,
|
|
11784
|
+
e
|
|
11785
|
+
);
|
|
11768
11786
|
}
|
|
11769
11787
|
return await multicallRetry2(
|
|
11770
11788
|
chain,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1delta/providers",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.54",
|
|
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.21"
|
|
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,7 +80,7 @@ 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
|
|
|
@@ -175,11 +175,30 @@ export function createMulticallRetry(
|
|
|
175
175
|
|
|
176
176
|
// When allowFailure is true, viem returns { result, status } per call.
|
|
177
177
|
// Extract results and convert failures to '0x'.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
)
|
|
182
|
-
|
|
178
|
+
let resolvedData: any[]
|
|
179
|
+
if (allowFailure) {
|
|
180
|
+
const mapped = (data as any[]).map(
|
|
181
|
+
({ result, status, error }: any) => ({
|
|
182
|
+
ok: status === 'success',
|
|
183
|
+
result: status !== 'success' ? '0x' : result,
|
|
184
|
+
error,
|
|
185
|
+
}),
|
|
186
|
+
)
|
|
187
|
+
const allFailed = mapped.length > 0 && mapped.every((m) => !m.ok)
|
|
188
|
+
|
|
189
|
+
// If EVERY call in the batch failed, the cause is almost certainly
|
|
190
|
+
// a transport-level error (429, 502, timeout) that viem caught
|
|
191
|
+
// internally instead of throwing. Re-throw so the retry logic
|
|
192
|
+
// below (skip to next RPC / consume a retry) handles it.
|
|
193
|
+
if (allFailed) {
|
|
194
|
+
const firstError = mapped[0]?.error
|
|
195
|
+
throw firstError ?? new Error('All multicall results failed')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
resolvedData = mapped.map((m) => m.result)
|
|
199
|
+
} else {
|
|
200
|
+
resolvedData = data as any[]
|
|
201
|
+
}
|
|
183
202
|
|
|
184
203
|
if (revertedIndices.size > 0) {
|
|
185
204
|
const finalResults: any[] = []
|
|
@@ -269,7 +288,10 @@ export function createMulticallRetry(
|
|
|
269
288
|
if (maxSkips > 0) {
|
|
270
289
|
if (logErrors) {
|
|
271
290
|
const tag = isHttpError(e) ? 'HTTP' : 'unknown-transient'
|
|
272
|
-
console.debug(
|
|
291
|
+
console.debug(
|
|
292
|
+
`[multicall] ${tag} error on provider ${providerId}, skipping`,
|
|
293
|
+
e,
|
|
294
|
+
)
|
|
273
295
|
}
|
|
274
296
|
return await multicallRetry(
|
|
275
297
|
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.
|