@1delta/providers 0.0.52 → 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 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
@@ -25099,10 +25099,13 @@ var LIST_OVERRIDES = {
25099
25099
  "https://avax-rpc.gateway.pokt.network"
25100
25100
  ],
25101
25101
  [import_chain_registry2.Chain.CELO_MAINNET]: [
25102
+ "https://celo-mainnet.gateway.tatum.io",
25103
+ "https://celo-json-rpc.stakely.io",
25102
25104
  "https://forno.celo.org",
25103
25105
  "https://1rpc.io/celo",
25104
25106
  "https://celo.drpc.org",
25105
- "https://celo-rpc.publicnode.com"
25107
+ "https://celo-rpc.publicnode.com",
25108
+ "https://rpc.ankr.com/celo"
25106
25109
  ],
25107
25110
  [import_chain_registry2.Chain.BLAST]: [
25108
25111
  "https://rpc.blast.io",
@@ -25198,6 +25201,10 @@ var LIST_OVERRIDES = {
25198
25201
  "https://tac.therpc.io",
25199
25202
  "https://ws.rpc.tac.build"
25200
25203
  ],
25204
+ [import_chain_registry2.Chain.BOTANIX_MAINNET]: [
25205
+ "https://rpc.botanixlabs.com",
25206
+ "https://rpc.ankr.com/botanix_mainnet"
25207
+ ],
25201
25208
  [import_chain_registry2.Chain.UNICHAIN]: [
25202
25209
  "https://mainnet.unichain.org",
25203
25210
  "https://rpc.sentio.xyz/unichain-mainnet",
@@ -25394,14 +25401,14 @@ function isHttpError(e) {
25394
25401
  const status = current.status;
25395
25402
  if (status && status >= 400) return true;
25396
25403
  const details = current.details ?? "";
25397
- const message = current.message ?? "";
25404
+ const message = current.message ?? current.shortMessage ?? "";
25398
25405
  const combined = `${message} ${details}`;
25399
25406
  if (HTTP_ERROR_STRINGS.some((s) => combined.includes(s))) return true;
25400
25407
  current = current.cause ?? null;
25401
25408
  }
25402
25409
  }
25403
25410
  if (e instanceof Error) {
25404
- const combined = `${e.message ?? ""} ${e.details ?? ""}`;
25411
+ const combined = `${e.message ?? e.shortMessage ?? ""} ${e.details ?? ""}`;
25405
25412
  if (HTTP_ERROR_STRINGS.some((s) => combined.includes(s))) return true;
25406
25413
  }
25407
25414
  return false;
@@ -25444,9 +25451,24 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
25444
25451
  })),
25445
25452
  allowFailure
25446
25453
  });
25447
- const resolvedData = allowFailure ? data.map(
25448
- ({ result, status }) => status !== "success" ? "0x" : result
25449
- ) : data;
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
+ }
25450
25472
  if (revertedIndices.size > 0) {
25451
25473
  const finalResults = [];
25452
25474
  let filteredIndex = 0;
@@ -25518,7 +25540,10 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
25518
25540
  if (maxSkips > 0) {
25519
25541
  if (logErrors) {
25520
25542
  const tag = isHttpError(e) ? "HTTP" : "unknown-transient";
25521
- console.debug(`[multicall] ${tag} error on provider ${providerId}, skipping`, e);
25543
+ console.debug(
25544
+ `[multicall] ${tag} error on provider ${providerId}, skipping`,
25545
+ e
25546
+ );
25522
25547
  }
25523
25548
  return await multicallRetry2(
25524
25549
  chain,
package/dist/index.mjs CHANGED
@@ -11338,10 +11338,13 @@ var LIST_OVERRIDES = {
11338
11338
  "https://avax-rpc.gateway.pokt.network"
11339
11339
  ],
11340
11340
  [Chain2.CELO_MAINNET]: [
11341
+ "https://celo-mainnet.gateway.tatum.io",
11342
+ "https://celo-json-rpc.stakely.io",
11341
11343
  "https://forno.celo.org",
11342
11344
  "https://1rpc.io/celo",
11343
11345
  "https://celo.drpc.org",
11344
- "https://celo-rpc.publicnode.com"
11346
+ "https://celo-rpc.publicnode.com",
11347
+ "https://rpc.ankr.com/celo"
11345
11348
  ],
11346
11349
  [Chain2.BLAST]: [
11347
11350
  "https://rpc.blast.io",
@@ -11437,6 +11440,10 @@ var LIST_OVERRIDES = {
11437
11440
  "https://tac.therpc.io",
11438
11441
  "https://ws.rpc.tac.build"
11439
11442
  ],
11443
+ [Chain2.BOTANIX_MAINNET]: [
11444
+ "https://rpc.botanixlabs.com",
11445
+ "https://rpc.ankr.com/botanix_mainnet"
11446
+ ],
11440
11447
  [Chain2.UNICHAIN]: [
11441
11448
  "https://mainnet.unichain.org",
11442
11449
  "https://rpc.sentio.xyz/unichain-mainnet",
@@ -11633,14 +11640,14 @@ function isHttpError(e) {
11633
11640
  const status = current.status;
11634
11641
  if (status && status >= 400) return true;
11635
11642
  const details = current.details ?? "";
11636
- const message = current.message ?? "";
11643
+ const message = current.message ?? current.shortMessage ?? "";
11637
11644
  const combined = `${message} ${details}`;
11638
11645
  if (HTTP_ERROR_STRINGS.some((s) => combined.includes(s))) return true;
11639
11646
  current = current.cause ?? null;
11640
11647
  }
11641
11648
  }
11642
11649
  if (e instanceof Error) {
11643
- const combined = `${e.message ?? ""} ${e.details ?? ""}`;
11650
+ const combined = `${e.message ?? e.shortMessage ?? ""} ${e.details ?? ""}`;
11644
11651
  if (HTTP_ERROR_STRINGS.some((s) => combined.includes(s))) return true;
11645
11652
  }
11646
11653
  return false;
@@ -11683,9 +11690,24 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
11683
11690
  })),
11684
11691
  allowFailure
11685
11692
  });
11686
- const resolvedData = allowFailure ? data.map(
11687
- ({ result, status }) => status !== "success" ? "0x" : result
11688
- ) : data;
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
+ }
11689
11711
  if (revertedIndices.size > 0) {
11690
11712
  const finalResults = [];
11691
11713
  let filteredIndex = 0;
@@ -11757,7 +11779,10 @@ function createMulticallRetry(customRpcs = LIST_OVERRIDES) {
11757
11779
  if (maxSkips > 0) {
11758
11780
  if (logErrors) {
11759
11781
  const tag = isHttpError(e) ? "HTTP" : "unknown-transient";
11760
- console.debug(`[multicall] ${tag} error on provider ${providerId}, skipping`, e);
11782
+ console.debug(
11783
+ `[multicall] ${tag} error on provider ${providerId}, skipping`,
11784
+ e
11785
+ );
11761
11786
  }
11762
11787
  return await multicallRetry2(
11763
11788
  chain,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1delta/providers",
3
- "version": "0.0.52",
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.17"
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
- const resolvedData = allowFailure
179
- ? (data as any[]).map(({ result, status }: any) =>
180
- status !== 'success' ? '0x' : result,
181
- )
182
- : data
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(`[multicall] ${tag} error on provider ${providerId}, skipping`, e)
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`.
@@ -175,10 +175,13 @@ export const LIST_OVERRIDES: Record<string, string[]> = {
175
175
  'https://avax-rpc.gateway.pokt.network',
176
176
  ],
177
177
  [Chain.CELO_MAINNET]: [
178
+ 'https://celo-mainnet.gateway.tatum.io',
179
+ 'https://celo-json-rpc.stakely.io',
178
180
  'https://forno.celo.org',
179
181
  'https://1rpc.io/celo',
180
182
  'https://celo.drpc.org',
181
183
  'https://celo-rpc.publicnode.com',
184
+ 'https://rpc.ankr.com/celo',
182
185
  ],
183
186
  [Chain.BLAST]: [
184
187
  'https://rpc.blast.io',
@@ -274,6 +277,10 @@ export const LIST_OVERRIDES: Record<string, string[]> = {
274
277
  'https://tac.therpc.io',
275
278
  'https://ws.rpc.tac.build',
276
279
  ],
280
+ [Chain.BOTANIX_MAINNET]: [
281
+ 'https://rpc.botanixlabs.com',
282
+ 'https://rpc.ankr.com/botanix_mainnet',
283
+ ],
277
284
  [Chain.UNICHAIN]: [
278
285
  'https://mainnet.unichain.org',
279
286
  'https://rpc.sentio.xyz/unichain-mainnet',
@@ -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.