multicall

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 12 Imported by: 0

README

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

Documentation

Overview

Package multicall batches Ethereum contract calls through the Multicall3 contract, collapsing many eth_call round trips into a few.

Create a client over any RPC backend, add typed calls to a Batch using abigen v2 pack/unpack functions, then Execute:

mc := multicall.New(client)
batch := mc.NewBatch()
name := batch.Add(tokenAddr, erc20.TryPackName, erc20.UnpackName)
if err := batch.Execute(ctx); err != nil {
	return err
}
fmt.Println(name.Value)

Calls are chunked by BatchSize and sent concurrently (Concurrency). Individual call failures surface in each call's Result or callback; Execute's error only joins chunk-level RPC failures and ctx.Err() when cancelled. Concurrent chunks may observe different blocks: every Result reports the block its chunk landed on (Result.Block), and pinning a block with NewBatchWithOpts forces the whole batch onto one block.

Execute consumes the batch — a second call returns ErrBatchExecuted (ErrBatchInProgress while one is in flight). Retry re-sends the calls that failed, writing the outcome back into the same Results.

Index

Constants

View Source
const DefaultMulticall3Address = "0xcA11bde05977b3631167028862bE2a173976CA11"

DefaultMulticall3Address is the standard deployment address for Multicall3 contract (universal across all EVM chains)

Variables

View Source
var (
	// ErrResultMissing result count does not match call count
	ErrResultMissing = errors.New("multicall: result missing from response")
	// ErrBatchInProgress is returned by Execute and Retry when another
	// Execute or Retry on the same batch has not returned yet.
	ErrBatchInProgress = errors.New("multicall: batch execution already in progress")
	// ErrBatchExecuted is returned by Execute on a batch that already ran
	// and received no new calls since: the calls are consumed. Retry
	// re-sends the failed ones instead.
	ErrBatchExecuted = errors.New("multicall: batch already executed")
)
View Source
var (
	// TryPack functions - encode method calls into calldata (return error on failure)
	TryPackGetBasefee                = codec.TryPackGetBasefee
	TryPackGetBlockHash              = codec.TryPackGetBlockHash
	TryPackGetBlockNumber            = codec.TryPackGetBlockNumber
	TryPackGetChainId                = codec.TryPackGetChainId
	TryPackGetCurrentBlockCoinbase   = codec.TryPackGetCurrentBlockCoinbase
	TryPackGetCurrentBlockDifficulty = codec.TryPackGetCurrentBlockDifficulty
	TryPackGetCurrentBlockGasLimit   = codec.TryPackGetCurrentBlockGasLimit
	TryPackGetCurrentBlockTimestamp  = codec.TryPackGetCurrentBlockTimestamp
	TryPackGetEthBalance             = codec.TryPackGetEthBalance
	TryPackGetLastBlockHash          = codec.TryPackGetLastBlockHash

	// Unpack functions - decode result data into Go types
	UnpackGetBasefee                = codec.UnpackGetBasefee
	UnpackGetBlockHash              = codec.UnpackGetBlockHash
	UnpackGetBlockNumber            = codec.UnpackGetBlockNumber
	UnpackGetChainId                = codec.UnpackGetChainId
	UnpackGetCurrentBlockCoinbase   = codec.UnpackGetCurrentBlockCoinbase
	UnpackGetCurrentBlockDifficulty = codec.UnpackGetCurrentBlockDifficulty
	UnpackGetCurrentBlockGasLimit   = codec.UnpackGetCurrentBlockGasLimit
	UnpackGetCurrentBlockTimestamp  = codec.UnpackGetCurrentBlockTimestamp
	UnpackGetEthBalance             = codec.UnpackGetEthBalance
	UnpackGetLastBlockHash          = codec.UnpackGetLastBlockHash
)

Functions

func AddWithCallback deprecated

func AddWithCallback[T any](
	b *Batch,
	addr common.Address,
	pack func() ([]byte, error),
	unpack func([]byte) (T, error),
	callback func(T, error),
)

AddWithCallback adds a call to the batch, automatically dispatches results via callback on Execute.

Deprecated: Use (*Batch).AddWithCallback instead; migrate with cmd/multicallfix (see the shim note above).

func IsCallFailed

func IsCallFailed(err error) bool

IsCallFailed checks if error is an individual call failure

func RevertReason

func RevertReason(err error) string

RevertReason extracts revert reason from error, returns empty string if not CallFailedError

Types

type Batch

type Batch struct {
	// contains filtered or unexported fields
}

Batch represents a batch of pending calls

func (*Batch) Add added in v0.1.0

func (b *Batch) Add[T any](
	addr common.Address,
	pack func() ([]byte, error),
	unpack func([]byte) (T, error),
) *Result[T]

Add adds a call to the batch and returns a Result reference, readable after Execute. pack is an abigen v2 TryPack* method, unpack its Unpack*. If pack itself fails, the returned Result already carries the error — no RPC is issued for it and Execute skips it.

func (*Batch) AddCall added in v0.1.0

func (b *Batch) AddCall(
	addr common.Address,
	pack func() ([]byte, error),
) *Result[[]byte]

AddCall adds a raw call without decoding, only cares about success/failure

func (*Batch) AddWithCallback added in v0.1.0

func (b *Batch) AddWithCallback[T any](
	addr common.Address,
	pack func() ([]byte, error),
	unpack func([]byte) (T, error),
	callback func(T, error),
)

AddWithCallback adds a call whose callback receives the unpacked result (or error) when the batch executes. Callbacks may fire concurrently and must not panic; see AddWithResult for the contract. If pack itself fails, callback runs synchronously before AddWithCallback returns instead of waiting for Execute, and no RPC is issued for the call.

func (*Batch) AddWithResult added in v0.1.0

func (b *Batch) AddWithResult[T any](
	addr common.Address,
	pack func() ([]byte, error),
	unpack func([]byte) (T, error),
	callback func(Result[T]),
)

AddWithResult adds a call whose callback receives the full Result — including Block, the block the chunk landed on — when the batch executes.

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

If pack itself fails, callback runs synchronously before AddWithResult returns with the error already set, and no RPC is issued for the call.

func (*Batch) Execute

func (b *Batch) Execute(ctx context.Context) error

Execute executes all calls in the batch, chunked and concurrent. It consumes the batch: calling Execute again before adding new calls returns ErrBatchExecuted (ErrBatchInProgress while the first Execute is still running). Calls that failed — the whole chunk on an RPC error, individual reverts otherwise — stay eligible for Retry.

The returned error joins chunk-level RPC errors — and ctx.Err() when cancelled — while individual call failures surface in each call's Result or callback instead.

func (*Batch) Len

func (b *Batch) Len() int

Len returns the number of calls in the current batch

func (*Batch) Retry added in v0.1.0

func (b *Batch) Retry(ctx context.Context) error

Retry re-executes the calls that failed the last execution — chunk-level RPC errors and individual call failures alike — writing the outcome back into the same Result/callback values; successful calls are not re-sent. The failed subset is re-chunked under the current BatchSize, and a block number pinned at batch creation keeps applying. With nothing to retry (never executed, or everything succeeded) Retry is a no-op returning nil.

Retrying re-invokes the callbacks of the failed calls, so callbacks must be re-entrant. The retry policy — attempts, backoff, which errors are worth it — stays with the caller:

err := batch.Execute(ctx)
for attempt := 0; shouldRetry(err) && attempt < 3; attempt++ {
	time.Sleep(backoff(attempt))
	err = batch.Retry(ctx)
}

Like Execute, Retry returns ErrBatchInProgress while another Execute or Retry is in flight, and joins chunk-level errors otherwise.

type BlockInfo added in v0.1.0

type BlockInfo struct {
	// Number is the block number; nil when unavailable — the chunk's RPC
	// failed, the call never went on chain (pack error), or the
	// configured Method does not report blocks.
	Number *big.Int
	// Hash is the block hash; zero when the Method reports only the number.
	Hash [32]byte
}

BlockInfo describes the block a chunk of calls landed in, as reported by the Multicall contract.

type CallFailedError

type CallFailedError struct {
	Reason string // Decoded revert reason
	Data   []byte // Raw return data
}

CallFailedError error for individual call failure, includes revert reason (if any)

func (*CallFailedError) Error

func (e *CallFailedError) Error() string

type Caller

type Caller interface {
	CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error)
}

Caller is the underlying RPC call interface, implemented by *ethclient.Client

type Config

type Config struct {
	Address     common.Address // Multicall3 contract address
	BatchSize   int            // Max calls per batch
	Concurrency int            // Concurrent batch count
	// Method selects the underlying contract method; the zero value is
	// MethodTryBlockAndAggregate.
	Method Method
	// AllowFailure (default true) sets the failure tolerance of the
	// configured method: it maps to requireSuccess with
	// MethodTryBlockAndAggregate and to the per-call flag with
	// MethodAggregate3. When true, a failed call surfaces as
	// CallFailedError in its own Result while the rest succeeds. When
	// false, one failing call reverts its whole chunk and every Result in
	// it carries that error. Ignored by MethodBlockAndAggregate and
	// MethodAggregate, which are strict by nature.
	AllowFailure bool
	// Logger receives diagnostics; nil logs nothing (see Logger).
	Logger Logger
}

Config client configuration

type Logger added in v0.1.0

type Logger interface {
	Debugf(format string, args ...any)
	Warnf(format string, args ...any)
}

Logger is the minimal logging interface the library writes diagnostics to; wire it to any logging backend via WithLogger. Events are logged sparingly: successful chunks at debug level, failures, cancellations and malformed responses at warn level. Errors are always returned as values too, so logging is purely a diagnostic aid.

type Method added in v0.1.0

type Method int

Method selects the Multicall contract method used under the hood.

const (
	// MethodTryBlockAndAggregate is the default. Failure tolerance is
	// configurable via AllowFailure, and every Result reports the block
	// its chunk landed on. Supported by Multicall2 and later.
	MethodTryBlockAndAggregate Method = iota
	// MethodBlockAndAggregate reverts the whole chunk if any call fails,
	// and reports the block in every Result. Supported by Multicall2 and
	// later.
	MethodBlockAndAggregate
	// MethodAggregate3 is Multicall3's per-call allowFailure method,
	// without block reporting; AllowFailure sets the per-call flag.
	MethodAggregate3
	// MethodAggregate is the greatest common denominator across
	// Multicall1/2/3: any failing call reverts the whole chunk. It
	// reports the block number, not its hash. Use it when the configured
	// address predates Multicall2.
	MethodAggregate
)

func (Method) String added in v0.1.0

func (m Method) String() string

String returns the contract method name.

type Multicall

type Multicall struct {
	// contains filtered or unexported fields
}

Multicall client

func New

func New(caller Caller, opts ...Option) *Multicall

New creates a Multicall client. Non-positive BatchSize or Concurrency falls back to the defaults.

func (*Multicall) NewBatch

func (mc *Multicall) NewBatch() *Batch

NewBatch creates a new batch

func (*Multicall) NewBatchWithOpts

func (mc *Multicall) NewBatchWithOpts(blockNumber *big.Int) *Batch

NewBatchWithOpts creates a batch whose calls all execute at the given block number, so concurrent chunks observe consistent state; nil means latest.

type Option

type Option func(*Config)

Option configuration option

func WithAddress

func WithAddress(addr common.Address) Option

WithAddress sets custom Multicall3 contract address

func WithAggregate deprecated added in v0.0.2

func WithAggregate() Option

WithAggregate uses aggregate instead of the default tryBlockAndAggregate.

Deprecated: WithMethod(MethodAggregate) is the exact equivalent; consider WithMethod(MethodBlockAndAggregate) to additionally get block info in every Result.

func WithAllowFailure

func WithAllowFailure(allow bool) Option

WithAllowFailure sets the default per-call failure tolerance; see Config.AllowFailure for what it means per Method.

func WithBatchSize

func WithBatchSize(size int) Option

WithBatchSize sets max calls per batch

func WithConcurrency

func WithConcurrency(n int) Option

WithConcurrency sets concurrent batch count

func WithLogger added in v0.1.0

func WithLogger(l Logger) Option

WithLogger sets the logger the library writes diagnostics to; see Logger. nil (the default) logs nothing.

func WithMethod added in v0.1.0

func WithMethod(m Method) Option

WithMethod selects the underlying Multicall contract method; see Method.

type Result

type Result[T any] struct {
	Value T
	Err   error
	Ok    bool // whether the call succeeded (exactly Err == nil)
	// Block is the block its chunk landed in; see BlockInfo for when it
	// is zero. A failed call still reports its block.
	Block BlockInfo
}

Result generic result container, read after Execute.

func Add deprecated

func Add[T any](
	b *Batch,
	addr common.Address,
	pack func() ([]byte, error),
	unpack func([]byte) (T, error),
) *Result[T]

Add adds a call to the batch, returns a Result reference.

Deprecated: Use (*Batch).Add instead; migrate with cmd/multicallfix (see the shim note above).

func AddCall deprecated

func AddCall(
	b *Batch,
	addr common.Address,
	pack func() ([]byte, error),
) *Result[[]byte]

AddCall adds a raw call without decoding, only cares about success/failure.

Deprecated: Use (*Batch).AddCall instead.

Directories

Path Synopsis
examples
builtin command
Package main demonstrates using Multicall3 built-in methods These methods are available as package-level functions for convenience.
Package main demonstrates using Multicall3 built-in methods These methods are available as package-level functions for convenience.
callback-based command
Package main demonstrates Pattern 2: Callback-Based usage of multicall Add calls with a callback function that automatically receives results during Execute().
Package main demonstrates Pattern 2: Callback-Based usage of multicall Add calls with a callback function that automatically receives results during Execute().
result-based command
Package main demonstrates Pattern 1: Result-Based usage of multicall Add calls and get a Result reference.
Package main demonstrates Pattern 1: Result-Based usage of multicall Add calls and get a Result reference.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL