Files
mev-beta/pkg/uniswap/multicall.go

249 lines
6.2 KiB
Go

package uniswap
import (
"context"
"fmt"
"strings"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
// Multicall3 address on Arbitrum
var Multicall3Address = common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11")
// Multicall3 ABI (simplified - only aggregate3 function)
const Multicall3ABI = `[{
"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"
}]`
// Call3 represents a single call in Multicall3
type Call3 struct {
Target common.Address
AllowFailure bool
CallData []byte
}
// Result3 represents the result of a Multicall3 call
type Result3 struct {
Success bool
ReturnData []byte
}
// MulticallBatcher batches multiple RPC calls into a single Multicall3 transaction
type MulticallBatcher struct {
client *ethclient.Client
multicallABI abi.ABI
}
// NewMulticallBatcher creates a new multicall batcher
func NewMulticallBatcher(client *ethclient.Client) (*MulticallBatcher, error) {
parsedABI, err := abi.JSON(strings.NewReader(Multicall3ABI))
if err != nil {
return nil, fmt.Errorf("failed to parse Multicall3 ABI: %w", err)
}
return &MulticallBatcher{
client: client,
multicallABI: parsedABI,
}, nil
}
// ExecuteMulticall executes multiple calls in a single transaction
func (m *MulticallBatcher) ExecuteMulticall(ctx context.Context, calls []Call3) ([]Result3, error) {
if len(calls) == 0 {
return []Result3{}, nil
}
// Pack the aggregate3 call
data, err := m.multicallABI.Pack("aggregate3", calls)
if err != nil {
return nil, fmt.Errorf("failed to pack multicall data: %w", err)
}
// Create the call message
msg := ethereum.CallMsg{
To: &Multicall3Address,
Data: data,
}
// Execute the call
result, err := m.client.CallContract(ctx, msg, nil)
if err != nil {
return nil, fmt.Errorf("failed to execute multicall: %w", err)
}
// Unpack the results
var results []Result3
err = m.multicallABI.UnpackIntoInterface(&results, "aggregate3", result)
if err != nil {
return nil, fmt.Errorf("failed to unpack multicall results: %w", err)
}
return results, nil
}
// BatchPoolDataCalls batches slot0, liquidity, token0, token1, and fee calls for a pool
func (m *MulticallBatcher) BatchPoolDataCalls(ctx context.Context, poolAddress common.Address, poolABI abi.ABI) (
slot0Data []byte,
liquidityData []byte,
token0Data []byte,
token1Data []byte,
feeData []byte,
err error,
) {
// Prepare call data for each function
slot0CallData, err := poolABI.Pack("slot0")
if err != nil {
return nil, nil, nil, nil, nil, fmt.Errorf("failed to pack slot0: %w", err)
}
liquidityCallData, err := poolABI.Pack("liquidity")
if err != nil {
return nil, nil, nil, nil, nil, fmt.Errorf("failed to pack liquidity: %w", err)
}
token0CallData, err := poolABI.Pack("token0")
if err != nil {
return nil, nil, nil, nil, nil, fmt.Errorf("failed to pack token0: %w", err)
}
token1CallData, err := poolABI.Pack("token1")
if err != nil {
return nil, nil, nil, nil, nil, fmt.Errorf("failed to pack token1: %w", err)
}
feeCallData, err := poolABI.Pack("fee")
if err != nil {
return nil, nil, nil, nil, nil, fmt.Errorf("failed to pack fee: %w", err)
}
// Create multicall calls
calls := []Call3{
{Target: poolAddress, AllowFailure: true, CallData: slot0CallData},
{Target: poolAddress, AllowFailure: true, CallData: liquidityCallData},
{Target: poolAddress, AllowFailure: true, CallData: token0CallData},
{Target: poolAddress, AllowFailure: true, CallData: token1CallData},
{Target: poolAddress, AllowFailure: true, CallData: feeCallData},
}
// Execute multicall
results, err := m.ExecuteMulticall(ctx, calls)
if err != nil {
return nil, nil, nil, nil, nil, err
}
// Extract results
if len(results) != 5 {
return nil, nil, nil, nil, nil, fmt.Errorf("expected 5 results, got %d", len(results))
}
// Return raw data (caller will unpack based on success flags)
return results[0].ReturnData,
results[1].ReturnData,
results[2].ReturnData,
results[3].ReturnData,
results[4].ReturnData,
nil
}
// BatchMultiplePoolData batches pool data calls for multiple pools
func (m *MulticallBatcher) BatchMultiplePoolData(ctx context.Context, pools []common.Address, poolABI abi.ABI) (
results map[common.Address]map[string][]byte,
err error,
) {
results = make(map[common.Address]map[string][]byte)
if len(pools) == 0 {
return results, nil
}
// Prepare all calls
var allCalls []Call3
callMap := make(map[int]struct {
poolAddr common.Address
funcName string
})
functions := []string{"slot0", "liquidity", "token0", "token1", "fee"}
callIndex := 0
for _, poolAddr := range pools {
for _, funcName := range functions {
callData, err := poolABI.Pack(funcName)
if err != nil {
// Skip this call if packing fails
continue
}
allCalls = append(allCalls, Call3{
Target: poolAddr,
AllowFailure: true,
CallData: callData,
})
callMap[callIndex] = struct {
poolAddr common.Address
funcName string
}{poolAddr, funcName}
callIndex++
}
}
// Execute multicall
multicallResults, err := m.ExecuteMulticall(ctx, allCalls)
if err != nil {
return nil, fmt.Errorf("failed to execute multicall: %w", err)
}
// Parse results
for i, result := range multicallResults {
if info, ok := callMap[i]; ok {
if _, exists := results[info.poolAddr]; !exists {
results[info.poolAddr] = make(map[string][]byte)
}
if result.Success {
results[info.poolAddr][info.funcName] = result.ReturnData
}
}
}
return results, nil
}