multicall

package module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Apr 19, 2026 License: MIT Imports: 11 Imported by: 0

README

multicall

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

Features

  • Type-safe contract calls with abigen v2 generated code
  • Generic Result[T] for compile-time type safety, no type assertions needed
  • Works with any contract - compatible with all abigen v2 bindings
  • Automatic chunking - large batches are split automatically
  • Concurrent execution - chunks run in parallel for better performance
  • Simple API - just pass TryPack* and Unpack* functions
  • Revert reason decoding for debugging failed calls
go get github.com/0x0001/multicall

Quick Start

package main

import (
    "context"
    "fmt"
    "log"

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

func main() {
    client, _ := ethclient.Dial("https://eth.llamarpc.com")
    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 := multicall.Add(batch, usdtAddr, erc20.TryPackName, erc20.UnpackName)
    usdcName := multicall.Add(batch, usdcAddr, erc20.TryPackName, erc20.UnpackName)

    // Execute all calls in a single RPC request
    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)
}

Usage Patterns

The library provides two patterns for handling multicall results.

Pattern 1: Result-Based (Add)

Get a Result reference and read it after Execute():

result := multicall.Add(batch, contractAddr,
    erc20.TryPackName,
    erc20.UnpackName,
)

batch.Execute(context.Background())
fmt.Println(result.Value) // access the result
Pattern 2: Callback-Based (AddWithCallback)

Provide a callback that receives the result automatically:

multicall.AddWithCallback(batch, 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()) // callback invoked automatically

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 use it with multicall:

import "github.com/user/project/mycontract"

contract := mycontract.NewMyContract()

result := multicall.Add(batch, contractAddr,
    contract.TryPackMyMethod,
    contract.UnpackMyMethod,
)

batch.Execute(context.Background())
fmt.Println(result.Value)

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)
)
Aggregate Method

By default, the library uses aggregate3 which returns per-call success/failure status. Use WithAggregate() to switch to the aggregate method, which reverts the entire batch if any single call fails:

mc := multicall.New(client, multicall.WithAggregate())

Note: WithAggregate is incompatible with AllowFailureaggregate does not support per-call failure tolerance. A warning is logged if both are used together.

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 := multicall.Add(batch, multicallAddr,
    multicall.TryPackGetBlockNumber,
    multicall.UnpackGetBlockNumber,
)

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

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

Available built-in methods: TryPackGetBlockNumber, TryPackGetChainId, TryPackGetBasefee, TryPackGetCurrentBlockTimestamp, TryPackGetEthBalance, TryPackGetBlockHash, TryPackGetLastBlockHash, and their corresponding Unpack* functions.

Query at Specific Block Height
batch := mc.NewBatchWithOpts(big.NewInt(18000000))
// ... add calls
batch.Execute(context.Background())
Raw Calls (No Decoding)

When you only need success/failure status:

result := multicall.AddCall(batch, 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))
    }
}

License

MIT

Documentation

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 (
	// 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
)
View Source
var (
	// ErrResultMissing result count does not match call count
	ErrResultMissing = errors.New("multicall: result missing from response")
)

Functions

func AddWithCallback

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 Suitable for batch operations, no need to process results in an external loop

Usage:

for _, p := range pairs {
    p := p
    multicall.AddWithCallback(batch, p.Address,
        pair.TryPackGetReserves,
        pair.UnpackGetReserves,
        func(r GetReservesOutput, err error) {
            if err != nil { return }
            p.Reserve0 = r.Reserve0
            p.Reserve1 = r.Reserve1
        },
    )
}
batch.Execute(ctx)

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) Execute

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

Execute executes all calls in the batch, automatically chunked and concurrent Errors are dispatched to each Result/callback via resolve; return value only reflects chunk-level RPC errors

func (*Batch) Len

func (b *Batch) Len() int

Len returns the number of calls in the current batch

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
	AllowFailure bool           // Whether to allow individual call failures by default
	Aggregate    bool           // Use aggregate instead of aggregate3 (reverts on any failure)
}

Config client configuration

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

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 new batch, allowing block number specification

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 added in v0.0.2

func WithAggregate() Option

WithAggregate uses the aggregate method instead of aggregate3. Aggregate reverts the entire batch if any single call fails.

func WithAllowFailure

func WithAllowFailure(allow bool) Option

WithAllowFailure sets whether to allow individual call failures by default

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

type Result

type Result[T any] struct {
	Value T
	Err   error
	Ok    bool // Whether the call succeeded
}

Result generic result container, read after Execute

func Add

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 pack: abigen v2 TryPack* method (or wrapper closure) unpack: abigen v2 Unpack* method

Usage:

r := multicall.Add(batch, pairAddr, pair.TryPackGetReserves, pair.UnpackGetReserves)
batch.Execute(ctx)
fmt.Println(r.Value.Reserve0)

func AddCall

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

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