@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 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
- const resolvedData = allowFailure ? data.map(
25455
- ({ result, status }) => status !== "success" ? "0x" : result
25456
- ) : data;
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(`[multicall] ${tag} error on provider ${providerId}, skipping`, e);
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) console.debug(e);
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
- const resolvedData = allowFailure ? data.map(
11694
- ({ result, status }) => status !== "success" ? "0x" : result
11695
- ) : data;
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(`[multicall] ${tag} error on provider ${providerId}, skipping`, e);
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) console.debug(e);
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.53",
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.17"
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
- const resolvedData = allowFailure
179
- ? (data as any[]).map(({ result, status }: any) =>
180
- status !== 'success' ? '0x' : result,
181
- )
182
- : data
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(`[multicall] ${tag} error on provider ${providerId}, skipping`, e)
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) console.debug(e)
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.