A Go library for batch calling Ethereum contracts using Multicall3 and abigen v2.
- 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*andUnpack*functions - Revert reason decoding for debugging failed calls
go get github.com/0x0001/multicallpackage 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)
}The library provides two patterns for handling multicall results.
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 resultProvide 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 automaticallyUse 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 \
--v2Then 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)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)
)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 AllowFailure — aggregate does not support per-call failure tolerance. A warning is logged if both are used together.
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.
batch := mc.NewBatchWithOpts(big.NewInt(18000000))
// ... add calls
batch.Execute(context.Background())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)
}// 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))
}
}MIT