Skip to content

bufrr/multicall-batcher

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

multicall-batcher

Aggregate N Ethereum contract calls into far fewer HTTP round-trips using Multicall3 and Alloy.

How it works

N calls
 ├─ chunked into groups of chunk_size       → fewer, larger eth_calls
 └─ up to max_concurrent chunks per wave    → parallel HTTP requests

Benchmark

5 000 decimals() calls against 1 000 unique addresses, publicnode.com, 10 runs averaged. All approaches use parallel execution:

Approach HTTP reqs Avg time vs fastest
Concurrent eth_call (join_all) 5 000 ~16 000 ms 4.3× slower
Alloy Multicall3 (parallel chunks) 25 ~4 200 ms 1.1× slower
multicall-batcher 25 ~3 700 ms fastest

chunk_size=200, max_concurrent=5.

Run it yourself: cargo run --release --example benchmark

Quick start

[dependencies]
multicall-batcher = { path = "." }
alloy = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["full"] }
use alloy::{primitives::address, sol, sol_types::SolCall};
use multicall_batcher::{Call, MulticallBuilder};

sol! {
    interface IERC20 {
        function balanceOf(address owner) external view returns (uint256);
    }
}

#[tokio::main]
async fn main() -> eyre::Result<()> {
    let mc = MulticallBuilder::new()
        .chunk_size(200)
        .max_concurrent(5)
        .connect("https://ethereum.publicnode.com")
        .await?;

    let usdc = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
    let holders = vec![ /* ... */ ];

    let calls: Vec<Call> = holders.iter().map(|&owner| {
        Call::new(usdc, IERC20::balanceOfCall { owner }.abi_encode())
    }).collect();

    let results = mc.call(calls).await?;

    for r in &results {
        if r.success {
            let bal = alloy::primitives::U256::from_be_slice(&r.return_data[..32]);
            println!("{bal}");
        }
    }
    Ok(())
}

API

MulticallBuilder

Method Default Description
.chunk_size(n) 100 Calls per aggregate3. Keep ≤ 200 for public RPCs.
.max_concurrent(n) 5 Max in-flight aggregate3 calls.
.multicall3(addr) canonical Override for testnets / custom deployments.
.connect(url) Connect and return BatchedMulticall.

Call

Call::new(target, calldata)     // allow_failure = true  (recommended)
Call::strict(target, calldata)  // allow_failure = false (reverts whole batch)

CallResult

pub struct CallResult {
    pub success: bool,
    pub return_data: Bytes,
}

Multicall3 address

0xcA11bde05977b3631167028862bE2a173976CA11 — same on Ethereum, Arbitrum, Base, Optimism, Polygon, and 50+ chains.

Examples

cargo run --example erc20_balances           # 6 balanceOf calls in 1 round-trip
cargo run --release --example benchmark      # full benchmark
cargo run --release --example benchmark_proxy  # proxy throughput test

HTTP server

Language-agnostic JSON API:

cargo run --bin server -- --rpc https://ethereum.publicnode.com --port 3000
curl -X POST http://localhost:3000/call \
  -H "Content-Type: application/json" \
  -d '[{"target":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","calldata":"0x313ce567"}]'

Transparent RPC proxy

Drop-in replacement — intercepts eth_call, batches via Multicall3, forwards everything else:

cargo run --bin proxy -- --rpc https://ethereum.publicnode.com --port 8545

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors