go-ethereum/accounts/abi/multicall/api.go
2024-09-09 11:08:42 -04:00

181 lines
12 KiB
Go

package multicall
import (
"context"
"errors"
"fmt"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
type MulticallClientOptions struct {
// the address of the multicall3 contract. if unset, defaults to the usual address.
// for sensible values, see: https://www.multicall3.com/deployments
address *common.Address
// optional: if set, will batch requests to the node. Reasonable values for this include 2048 / 4096 byte chunks.
maxBatchSizeBytes *uint64
}
/**
* To use multicall, you will typically-
* - setup a client with `multicall.NewClient()`
* - use the autogenerated bindings on your client (or the manual `multicall.Describe()` to describe calls you want to make)
* - send the calls with `multicall.Perform*()` -- using 3, 4, or many for a variable number of requests. This will dispatch your requests,
* and depending on your settings, may break into multiple eth_call requests.
*/
func NewClient(ctx context.Context, eth *ethclient.Client, opts *MulticallClientOptions) (*MulticallClient, error) {
if eth == nil {
return nil, errors.New("no ethclient passed")
}
// taken from: https://www.multicall3.com/
parsed, err := abi.JSON(strings.NewReader(`[{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"aggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes[]","name":"returnData","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bool","name":"allowFailure","type":"bool"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call3[]","name":"calls","type":"tuple[]"}],"name":"aggregate3","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bool","name":"allowFailure","type":"bool"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call3Value[]","name":"calls","type":"tuple[]"}],"name":"aggregate3Value","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"blockAndAggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"getBasefee","outputs":[{"internalType":"uint256","name":"basefee","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"name":"getBlockHash","outputs":[{"internalType":"bytes32","name":"blockHash","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBlockNumber","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getChainId","outputs":[{"internalType":"uint256","name":"chainid","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockCoinbase","outputs":[{"internalType":"address","name":"coinbase","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockDifficulty","outputs":[{"internalType":"uint256","name":"difficulty","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockGasLimit","outputs":[{"internalType":"uint256","name":"gaslimit","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockTimestamp","outputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getEthBalance","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastBlockHash","outputs":[{"internalType":"bytes32","name":"blockHash","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"requireSuccess","type":"bool"},{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"tryAggregate","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bool","name":"requireSuccess","type":"bool"},{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"tryBlockAndAggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"}]`))
if err != nil {
return nil, fmt.Errorf("error parsing multicall abi: %s", err.Error())
}
contractAddress := func() common.Address {
if opts.address == nil {
// also taken from: https://www.multicall3.com/ -- it's deployed at the same addr on most chains
return common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11")
}
return *opts.address
}()
return &MulticallClient{MaxBatchSize: opts.maxBatchSizeBytes, Context: ctx, ABI: &parsed, Contract: bind.NewBoundContract(contractAddress, parsed, eth, eth, eth)}, nil
}
func Describe[T any](contractAddress common.Address, abi abi.ABI, deserialize func([]byte) (T, error), method string, params ...interface{}) *MultiCallMetaData[T] {
callData, err := abi.Pack(method, params...)
if err != nil {
panic(fmt.Errorf("error packing multicall: %s", err.Error()))
}
return &MultiCallMetaData[T]{
Address: contractAddress,
Data: callData,
Deserialize: deserialize,
}
}
func Perform[A any, B any](mc MulticallClient, a *MultiCallMetaData[A], b *MultiCallMetaData[B]) (*A, *B, error) {
res, err := doMultiCall(mc, a.Raw(), b.Raw())
if err != nil {
return nil, nil, fmt.Errorf("error performing multicall: %s", err.Error())
}
return any(res[0].Value).(*A), any(res[1].Value).(*B), nil
}
func Perform3[A any, B any, C any](mc MulticallClient, a *MultiCallMetaData[A], b *MultiCallMetaData[B], c *MultiCallMetaData[C]) (*A, *B, *C, error) {
res, err := doMultiCall(mc, a.Raw(), b.Raw(), c.Raw())
if err != nil {
return nil, nil, nil, fmt.Errorf("error performing multicall: %s", err.Error())
}
return any(res[0].Value).(*A), any(res[1].Value).(*B), any(res[2].Value).(*C), nil
}
func Perform4[A any, B any, C any, D any](mc MulticallClient, a *MultiCallMetaData[A], b *MultiCallMetaData[B], c *MultiCallMetaData[C], d *MultiCallMetaData[D]) (*A, *B, *C, *D, error) {
res, err := doMultiCall(mc, a.Raw(), b.Raw(), c.Raw(), d.Raw())
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("error performing multicall: %s", err.Error())
}
return any(res[0].Value).(*A), any(res[1].Value).(*B), any(res[2].Value).(*C), any(res[3].Value).(*D), nil
}
func Perform5[A any, B any, C any, D any, E any](mc MulticallClient, a *MultiCallMetaData[A], b *MultiCallMetaData[B], c *MultiCallMetaData[C], d *MultiCallMetaData[D], e *MultiCallMetaData[E]) (*A, *B, *C, *D, *E, error) {
res, err := doMultiCall(mc, a.Raw(), b.Raw(), c.Raw(), d.Raw(), e.Raw())
if err != nil {
return nil, nil, nil, nil, nil, fmt.Errorf("error performing multicall: %s", err.Error())
}
return any(res[0].Value).(*A), any(res[1].Value).(*B), any(res[2].Value).(*C), any(res[3].Value).(*D), any(res[4].Value).(*E), nil
}
func PerformMany[A any](mc MulticallClient, requests ...*MultiCallMetaData[A]) (*[]A, error) {
res, err := doMultiCall(mc, mapArray(requests, func(mc *MultiCallMetaData[A], index uint64) RawMulticall {
return mc.Raw()
})...)
if err != nil {
return nil, fmt.Errorf("multicall failed: %s", err.Error())
}
// unwind results
unwoundResults := mapArray(res, func(d DeserializedMulticall3Result, i uint64) A {
// force these back to A
return any(d.Value).(A)
})
return &unwoundResults, nil
}
func doMultiCall(mc MulticallClient, calls ...RawMulticall) ([]DeserializedMulticall3Result, error) {
typedCalls := make([]ParamMulticall3Call3, len(calls))
for i, call := range calls {
typedCalls[i] = ParamMulticall3Call3{
Target: call.Address,
AllowFailure: true,
CallData: call.Data,
}
}
chunkedCalls := func() [][]ParamMulticall3Call3 {
if mc.MaxBatchSize == nil {
// no batching
return [][]ParamMulticall3Call3{
typedCalls,
}
}
return chunkCalls(typedCalls, *mc.MaxBatchSize)
}()
var results = make([]interface{}, len(calls))
var totalResults = 0
for _, multicalls := range chunkedCalls {
var res []interface{}
// we can't use the generated abi, as we want an eth_call simulation (and aggregate3 is payable, which triggers an eth_sendTransaction in abigen)
err := mc.Contract.Call(&bind.CallOpts{}, &res, "aggregate3", multicalls)
if err != nil {
return nil, fmt.Errorf("aggregate3 failed: %s", err)
}
multicallResults := *abi.ConvertType(res[0], new([]Multicall3Result)).(*[]Multicall3Result)
for i := 0; i < len(multicallResults); i++ {
results[totalResults+i] = multicallResults[i]
}
totalResults += len(multicallResults)
}
return mapArray(calls, func(call RawMulticall, i uint64) DeserializedMulticall3Result {
res := results[i].(Multicall3Result)
if res.Success {
if res.ReturnData != nil {
val, err := call.Deserialize(res.ReturnData)
if err != nil {
return DeserializedMulticall3Result{
Value: err,
Success: false,
}
} else {
return DeserializedMulticall3Result{
Value: val,
Success: res.Success,
}
}
} else {
return DeserializedMulticall3Result{
Value: errors.New("no data returned"),
Success: false,
}
}
} else {
return DeserializedMulticall3Result{
Success: false,
Value: errors.New("call failed"),
}
}
}), nil
}