Skip to content

0x0001/multicall

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

About

go multicall3 caller

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors