Skip to content

Repository files navigation

multicall

A Go library for batch calling Ethereum contracts using Multicall3 and abigen v2.

Features

  • Type-safe contract calls with any abigen v2 binding
  • Generic batch.Add(...) returning Result[T] (Go 1.27)
  • Automatic chunking and concurrent execution
  • Retry of failed calls without re-sending successes
  • Per-result block info (Result.Block) for consistency checks
  • Revert reason decoding
go get github.com/0x0001/multicall

Quick Start

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/0x0001/multicall"
    "github.com/0x0001/multicall/bindings"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/ethclient"
)

func main() {
    client, err := ethclient.Dial("https://ethereum-rpc.publicnode.com")
    if err != nil {
        log.Fatal(err)
    }
    mc := multicall.New(client)
    batch := mc.NewBatch()

    // Define token addresses
    usdtAddr := common.HexToAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7")
    usdcAddr := common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")

    // Create ERC20 codec (generated with abigen --v2)
    erc20 := bindings.NewErc20()

    // Add calls to batch
    usdtName := batch.Add(usdtAddr, erc20.TryPackName, erc20.UnpackName)
    usdcName := batch.Add(usdcAddr, erc20.TryPackName, erc20.UnpackName)

    // Execute the batch
    if err := batch.Execute(context.Background()); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("USDT: %s\n", usdtName.Value)
    fmt.Printf("USDC: %s\n", usdcName.Value)
}

Migrating from v0.0.x

Add, AddWithCallback and AddCall are now methods on *Batch instead of package-level functions (requires a Go 1.27+ toolchain). The old functions still work but are deprecated; rewrite call sites with the bundled fixer:

go run github.com/0x0001/multicall/cmd/multicallfix@latest -fix ./...

Behavior changes: Execute consumes the batch — a second Execute without new calls returns ErrBatchExecuted (ErrBatchInProgress while the first is still running; see Retries), failed chunks are joined with errors.Join, and revert-reason formatting changed. The default underlying method is now tryBlockAndAggregate (same semantics as aggregate3, plus block reporting — see Underlying Method); Result gained a Block field and WithAggregate() is deprecated in favor of WithMethod(MethodAggregate).

Callbacks

AddWithCallback pushes each unpacked result to a callback at Execute time instead of returning a Result:

batch.AddWithCallback(contractAddr,
    erc20.TryPackName,
    erc20.UnpackName,
    func(name string, err error) {
        if err != nil {
            log.Printf("Error: %v", err)
            return
        }
        fmt.Println("Name:", name)
    },
)

batch.Execute(context.Background()) // callbacks invoked

AddWithResult is the callback variant receiving the full Result, Block included.

Callbacks run on their chunk's goroutine, so callbacks of different calls may fire concurrently — guard any state they share. They must not panic: on a single-chunk batch the panic propagates out of Execute (or Retry); on a concurrent chunk it crashes the process.

Generating Bindings for Your Contracts

Use abigen to generate Go bindings for any contract:

# Install abigen
go install github.com/ethereum/go-ethereum/cmd/abigen@latest

# Generate bindings for your contract
abigen \
  --abi ./MyContract.json \
  --pkg mycontract \
  --type MyContract \
  --out mycontract.go \
  --v2

Then instantiate it with mycontract.NewMyContract() and pass its TryPack*/Unpack* methods to batch.Add like any other binding.

Configuration

Customize the Multicall client:

mc := multicall.New(client,
    multicall.WithAddress(customAddress),    // Custom Multicall3 address
    multicall.WithBatchSize(50),             // Max calls per batch (default: 30)
    multicall.WithConcurrency(5),            // Concurrent batches (default: 3)
    multicall.WithAllowFailure(false),       // Fail on individual errors (default: true)
    multicall.WithLogger(myLogger),          // Diagnostics; nil logs nothing (default)
)

Logging

WithLogger wires a diagnostic logger; the default is fully silent. Successful chunks log at debug level (method, call count, block, duration); chunk failures, cancellations and short responses at warn. Adapting log/slog is two one-line methods:

type slogLogger struct{ l *slog.Logger }

func (s slogLogger) Debugf(format string, args ...any) { s.l.Debug(fmt.Sprintf(format, args...)) }
func (s slogLogger) Warnf(format string, args ...any)  { s.l.Warn(fmt.Sprintf(format, args...)) }

mc := multicall.New(client, multicall.WithLogger(slogLogger{slog.Default()}))

Underlying Method

The library sends your calls through one of Multicall's aggregate methods, selected with WithMethod:

mc := multicall.New(client,
    multicall.WithMethod(multicall.MethodBlockAndAggregate),
)
Method Failure semantics Block info Works on
MethodTryBlockAndAggregate (default) failure tolerance via AllowFailure Multicall2+
MethodBlockAndAggregate any failure reverts the whole chunk Multicall2+
MethodAggregate3 per-call tolerance via AllowFailure Multicall3
MethodAggregate any failure reverts the whole chunk number only Multicall1+

The default reports the block each chunk landed on, so every Result carries Block (see below). If the address you configured with WithAddress predates Multicall2, fall back to MethodAggregate — the greatest common denominator every Multicall variant supports.

AllowFailure maps to requireSuccess with MethodTryBlockAndAggregate and to the per-call flag with MethodAggregate3 — but under the library's single global setting, both methods behave identically: true isolates a failed call in its own Result, false reverts the whole chunk. The two methods would only differ for mixed per-call tolerance within one request, which the library has never exposed.

WithAggregate() is deprecated; WithMethod(MethodAggregate) is the exact equivalent, and go fix rewrites old call sites.

Consistency & Failure Semantics

Chunks run concurrently and may land on different blocks. Every Result (and every AddWithResult callback) carries Result.Block — the block its chunk landed on — so consistency is checkable after Execute:

fmt.Println(result.Block.Number, result.Block.Hash)

Calls in the same chunk share one block. To force the whole batch to read a single block, pin it with mc.NewBatchWithOpts(big.NewInt(18000000)).

Block.Number is nil when the call never landed on a block (failed chunk, pack error) or the method reports none (MethodAggregate3).

With the default AllowFailure=true, a failed call surfaces as CallFailedError in its own Result — including the block it failed on — and the rest still succeeds. With AllowFailure=false (or a strict method), one failing call reverts its whole chunk: every Result in it carries that error, and Execute returns it joined.

Advanced Usage

Multicall3 Built-in Methods

Query blockchain state using Multicall3's built-in methods:

import "github.com/0x0001/multicall"

multicallAddr := common.HexToAddress(multicall.DefaultMulticall3Address)

blockNumber := batch.Add(multicallAddr,
    multicall.TryPackGetBlockNumber,
    multicall.UnpackGetBlockNumber,
)

balance := batch.Add(multicallAddr,
    func() ([]byte, error) { return multicall.TryPackGetEthBalance(addr) },
    multicall.UnpackGetEthBalance,
)

batch.Execute(context.Background())
fmt.Println("Block:", blockNumber.Value)
fmt.Println("Balance:", balance.Value)

All Multicall3 view methods are exposed as TryPack*/Unpack* package functions (block number/hash, chain id, basefee, timestamp, ETH balance, …).

Raw Calls (No Decoding)

When you only need success/failure status:

result := batch.AddCall(contractAddr,
    myContract.TryPackMyMethod,
)

batch.Execute(context.Background())

if result.Ok {
    fmt.Println("Success!")
    fmt.Printf("Raw data: %x\n", result.Value)
}

Error Handling

// Check if an error is a call failure
if multicall.IsCallFailed(err) {
    reason := multicall.RevertReason(err)
    fmt.Printf("Call failed: %s\n", reason)
}

// Check individual results
if !result.Ok {
    if multicall.IsCallFailed(result.Err) {
        fmt.Printf("Revert reason: %s\n", multicall.RevertReason(result.Err))
    }
}

Retries

Transient failures — rate limits, timeouts, network blips — are worth re-sending; deterministic reverts usually are not. Retry re-sends exactly the calls that failed the last execution and writes the outcome back into the same Results; the retry policy stays with you. The method docs carry the full semantics — callback re-entrancy, batch lifecycle, block reporting.

License

MIT

About

go multicall3 caller

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages