Multicall is not a service - it provides effectful functions (aggregate3, BalanceResolver)
that depend on TransportService. Unlike services which use Context.Tag, these are simply
Effect functions you call directly.
Quick Start
import { Effect } from 'effect'
import { aggregate3, HttpTransport } from 'voltaire-effect'
const program = Effect.gen(function* () {
const results = yield* aggregate3([
{ target: '0x6B175474E89094C44Da98b954EecdEfaE6E286AB', callData: '0x70a08231...' },
{ target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', callData: '0x70a08231...' }
])
return results
}).pipe(
Effect.provide(HttpTransport('https://eth.llamarpc.com'))
)
MulticallCall Type
interface MulticallCall {
/** Target contract address to call */
readonly target: `0x${string}`
/** ABI-encoded function call data */
readonly callData: `0x${string}`
/** Whether this call is allowed to fail without reverting the batch */
readonly allowFailure?: boolean
}
MulticallResult
interface MulticallResult {
/** Whether the call succeeded */
readonly success: boolean
/** The return data from the call (empty if failed) */
readonly returnData: `0x${string}`
}
Success/Failure Handling
const results = yield* aggregate3([
{ target: tokenA, callData: balanceOfData, allowFailure: true },
{ target: tokenB, callData: balanceOfData, allowFailure: true }
])
for (const result of results) {
if (result.success) {
const balance = decodeBalance(result.returnData)
console.log('Balance:', balance)
} else {
console.log('Call failed')
}
}
Error Handling
import { MulticallError } from 'voltaire-effect'
program.pipe(
Effect.catchTag('MulticallError', (e) => {
console.error('Multicall failed:', e.message)
if (e.failedCalls) {
console.error('Failed call indices:', e.failedCalls)
}
return Effect.succeed([])
})
)
MulticallError
class MulticallError extends Data.TaggedError("MulticallError")<{
/** Human-readable error message */
readonly message: string
/** Indices of calls that failed (if applicable) */
readonly failedCalls?: readonly number[]
/** Underlying error that caused the failure */
readonly cause?: unknown
}>
ERC-20 Balance Checking
Batch multiple balanceOf calls for different tokens:
import { Effect } from 'effect'
import { encodeParameters } from '@tevm/voltaire/Abi'
import { aggregate3, HttpTransport } from 'voltaire-effect'
const BALANCE_OF_SELECTOR = '0x70a08231'
const encodeBalanceOf = (account: `0x${string}`): `0x${string}` => {
const encoded = encodeParameters([{ type: 'address' }], [account])
return `${BALANCE_OF_SELECTOR}${Buffer.from(encoded).toString('hex')}` as `0x${string}`
}
const checkBalances = Effect.gen(function* () {
const account = '0x1234567890123456789012345678901234567890'
const tokens = [
'0x6B175474E89094C44Da98b954EecdEfaE6E286AB', // DAI
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
'0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT
] as const
const results = yield* aggregate3(
tokens.map(token => ({
target: token,
callData: encodeBalanceOf(account),
allowFailure: true
}))
)
return results.map((r, i) => ({
token: tokens[i],
balance: r.success ? BigInt(r.returnData) : 0n
}))
}).pipe(
Effect.provide(HttpTransport('https://eth.llamarpc.com'))
)
Custom Multicall Address
For chains where Multicall3 is deployed at a different address, pass an override:
const results = yield* aggregate3(
calls,
'latest',
'0x0000000000000000000000000000000000000000'
)
Request Configuration
Use Effect-native helpers for timeout and retry:
import { withTimeout, withRetrySchedule, withoutCache } from 'voltaire-effect'
import { Schedule } from 'effect'
// Multicall with timeout
const results = yield* aggregate3(calls).pipe(
withTimeout("10 seconds")
)
// Retry with exponential backoff
const resultsWithRetry = yield* aggregate3(calls).pipe(
withRetrySchedule(
Schedule.exponential("500 millis").pipe(
Schedule.jittered,
Schedule.compose(Schedule.recurs(5))
)
)
)
// Bypass cache for fresh data
const freshResults = yield* aggregate3(calls).pipe(withoutCache)
TransportService Dependency
aggregate3 is an Effect function (not a service) that depends on TransportService:
import { Effect } from 'effect'
import { aggregate3, HttpTransport } from 'voltaire-effect'
const program = aggregate3([{ target, callData }]).pipe(
Effect.provide(HttpTransport('https://eth.llamarpc.com'))
)
Function Signature
aggregate3 is an Effect function, not a service method:
const aggregate3: (
calls: readonly MulticallCall[],
blockTag?: BlockTag,
multicallAddress?: `0x${string}`
) => Effect.Effect<readonly MulticallResult[], MulticallError, TransportService>
Multicall3 Contract
The helper uses the Multicall3 contract deployed at 0xcA11bde05977b3631167028862bE2a173976CA11 on Ethereum mainnet and most EVM-compatible chains.
Either for Result Handling
Transform MulticallResult to Either for ergonomic partial failure handling:
import { Effect, Either } from 'effect'
import { aggregate3, HttpTransport } from 'voltaire-effect'
const program = Effect.gen(function* () {
const results = yield* aggregate3(calls)
// Transform to Either<Error, Data>
const eitherResults = results.map((r, i) =>
r.success
? Either.right(r.returnData)
: Either.left({ index: i, message: 'Call failed' })
)
// Exhaustive handling with Either.match
const processed = eitherResults.map(r =>
Either.match(r, {
onLeft: (err) => ({ data: null, error: err.message }),
onRight: (data) => ({ data: BigInt(data), error: null })
})
)
// Filter and extract
const successes = eitherResults.filter(Either.isRight).map(r => r.right)
const failures = eitherResults.filter(Either.isLeft).map(r => r.left)
return { processed, successes, failures }
}).pipe(
Effect.provide(HttpTransport('https://eth.llamarpc.com'))
)
Either API Reference
| Method | Signature | Description |
|---|
Either.right | <A>(value: A) => Either<never, A> | Create success |
Either.left | <E>(error: E) => Either<E, never> | Create failure |
Either.isRight | (e: Either<E, A>) => e is Right<A> | Type guard |
Either.isLeft | (e: Either<E, A>) => e is Left<E> | Type guard |
Either.match | (e, { onLeft, onRight }) => B | Exhaustive match |
Either.map | (e, f: A => B) => Either<E, B> | Map success |
Either.mapLeft | (e, f: E => E2) => Either<E2, A> | Map error |
Either.getOrElse | (e, default: () => A) => A | Extract with fallback |
See Also