@1delta/providers
v0.0.54
Published
Resilient EVM provider infrastructure: chain mapping, RPC management, transport creation, and batched multicall with automatic retry/rotation.
Readme
@1delta/providers
Resilient EVM provider infrastructure: chain mapping, RPC management, transport creation, and batched multicall with automatic retry/rotation.
Module Reference
| Module | README | Purpose |
|---|---|---|
| chains/ | chains/README.md | Chain enum → viem Chain mapping, custom chain definitions |
| client/ | client/README.md | PublicClient creation with RPC fallback/rotation |
| rpc/ | rpc/README.md | Default RPC URL pools per chain (LIST_OVERRIDES) |
| transport/ | transport/README.md | HTTP/WebSocket viem transport factory |
| multicall/ | multicall/README.md | Batched multicall with retry, RPC rotation, revert isolation |
| utils/ | utils/README.md | Shared constants and helpers |
Everything is re-exported from the package root via src/index.ts → src/evm.ts.
Building Efficient Batched Multicalls
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.
1. Define the Call Shape
Each call is a plain object:
interface Call {
address: string // target contract
name: string // function name (must match ABI)
params?: any[] // function arguments (alias: args)
}2. Build Calls — Track Lengths per Source
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.
From margin-fetcher/src/flash-liquidity/fetchLiquidity.ts:
let callLengths: { [protocol: string]: number } = {}
let aaveCalls: Call[] = []
// Aave forks: 2 calls per asset (balanceOf + getConfiguration) + 1 premium call
aaveProtocols.forEach((aaveFork) => {
const underlyingsAndATokens = Object.entries(
getAaveStyleProtocolTokenMap(chain, aaveFork),
)
const pool = getAaveTypePoolAddress(chain, aaveFork)
const tokenCalls = underlyingsAndATokens.flatMap(
([underlying, tokens]: [string, any]) => [
{ name: 'balanceOf', address: underlying, params: [tokens.aToken] },
{ name: 'getConfiguration', address: pool, params: [underlying] },
],
)
// Track length so we know how many results belong to this fork
callLengths[aaveFork] = tokenCalls.length + 1
aaveCalls.push(...tokenCalls, {
name: 'FLASHLOAN_PREMIUM_TOTAL',
address: pool,
})
})For uniform sources (same call per asset), a helper keeps it concise:
const buildBalanceCalls = (
forks: { pool: string; address: string }[],
): Call[] => {
const result: Call[] = []
for (const fork of forks) {
callLengths[fork.pool] = unifiedAssets.length
for (const address of unifiedAssets) {
result.push(
address === zeroAddress
? { name: 'getEthBalance', address: MULTICALL_ADDRESS[chain], params: [fork.address] }
: { name: 'balanceOf', address, params: [fork.address] },
)
}
}
return result
}3. Concatenate and Fire
Merge all sub-arrays into one flat call list and execute a single multicall:
const calls = [
...aaveCalls,
...balancerV2Calls,
...morphoCalls,
...balancerV3Calls,
...uniswapV4Calls,
]
const rawResults = await multicallRetryUniversal({
chain,
calls,
abi: FlashAbi, // single ABI covering all function signatures
batchSize: 4096,
allowFailure: true, // failed calls return '0x' instead of throwing
})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.
4. Unwrap Results with Offset Walking
The results array is positionally aligned with the calls array. Use the tracked lengths to slice each source's chunk:
let currentOffset = 0
aaveProtocols.forEach((aave) => {
const callLen = aaveAssets[aave].length * 2 + 1 // 2 calls per asset + 1 premium
// Slice this fork's results
const data = rawResults.slice(currentOffset, currentOffset + callLen)
currentOffset += callLen // advance the cursor
// Last result is the premium call
const fee = data[callLen - 1]
if (typeof fee !== 'bigint') return // skip fork if premium call failed
// Walk interleaved results: [balance, config, balance, config, ...]
aaveAssets[aave].forEach((asset, i) => {
const rawAmount = data[2 * i] // balanceOf result
const config = data[2 * i + 1] // getConfiguration result
if (typeof rawAmount !== 'bigint' || typeof config !== 'bigint') return
// ... process asset
})
})
// Uniform sources are simpler — one result per asset
balancerV2s.forEach((balancer) => {
const data = rawResults.slice(currentOffset, currentOffset + unifiedAssets.length)
currentOffset += unifiedAssets.length
unifiedAssets.forEach((asset, i) => {
const rawAmount = data[i]
if (typeof rawAmount === 'bigint' && rawAmount > 0n) {
// ... process asset
}
})
})5. Handling Failed Calls
With allowFailure: true, failed calls return '0x'. Guard with type checks:
// For bigint returns (balances, configs, etc.)
const isValidResult = (v: any): v is bigint => typeof v === 'bigint'
if (!isValidResult(rawAmount)) return // skip this entryQuick Reference
import { multicallRetryUniversal } from '@1delta/providers'
// Minimal call
const results = await multicallRetryUniversal({
chain: '8453',
calls: [
{ address: tokenAddr, name: 'balanceOf', params: [account] },
{ address: tokenAddr, name: 'decimals' },
],
abi: ERC20_ABI,
})
const [balance, decimals] = resultsFor custom RPCs or factory usage, see multicall/README.md and client/README.md.
