diff --git a/README.md b/README.md index a5eb2ea..1c3d59b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ go get github.com/forta-network/go-multicall (See other examples under the `examples` directory!) +#### Multicall ```go package main @@ -91,3 +92,67 @@ func main() { } } ``` + +#### SingleCall +```go +package main + +import ( + "context" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/forta-network/go-multicall" +) + +const ( + APIURL = "https://cloudflare-eth.com" + ERC20ABI = `[ + { + "constant":true, + "inputs":[ + { + "name":"tokenOwner", + "type":"address" + } + ], + "name":"balanceOf", + "outputs":[ + { + "name":"balance", + "type":"uint256" + } + ], + "payable":false, + "stateMutability":"view", + "type":"function" + } + ]` +) + +func main() { + caller, err := multicall.Dial(context.Background(), APIURL) + if err != nil { + panic(err) + } + + contract, err := multicall.NewContract(ERC20ABI, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") + if err != nil { + panic(err) + } + + single, err := caller.CallSingle(nil, + contract.NewCall( + nil, + "balanceOf", + common.HexToAddress("0xcEe284F754E854890e311e3280b767F80797180d"), // Arbitrum One gateway + ).Name("Arbitrum One gateway balance").SetExtend(map[string]string{ + "account": "0xcEe284F754E854890e311e3280b767F80797180d", + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + })) + if err != nil { + return + } + + fmt.Println(single.CallName, ":", single.UnpackResult()[0].(*big.Int), single.Extend) +} +``` diff --git a/call.go b/call.go index b89a9e4..8929516 100644 --- a/call.go +++ b/call.go @@ -42,6 +42,7 @@ type Call struct { CallName string Contract *Contract Method string + Extend any Inputs []any Outputs any CanFail bool @@ -67,6 +68,18 @@ func (call *Call) Name(name string) *Call { return call } +func (call *Call) SetExtend(ext any) *Call { + call.Extend = ext + return call +} + +func (call *Call) UnpackResult() []interface{} { + if call.Outputs == nil { + return nil + } + return call.Outputs.([]interface{}) +} + // AllowFailure sets if the call is allowed to fail. This helps avoiding a revert // when one of the calls in the array fails. func (call *Call) AllowFailure() *Call { @@ -76,19 +89,24 @@ func (call *Call) AllowFailure() *Call { // Unpack unpacks and converts EVM outputs and sets struct fields. func (call *Call) Unpack(b []byte) error { + out, err := call.Contract.ABI.Unpack(call.Method, b) + if err != nil { + return fmt.Errorf("failed to unpack '%s' outputs: %v", call.Method, err) + } + if call.Outputs == nil { + call.Outputs = out + return nil + } + t := reflect.ValueOf(call.Outputs) if t.Kind() == reflect.Pointer { t = t.Elem() } + if t.Kind() != reflect.Struct { return errors.New("outputs type is not a struct") } - out, err := call.Contract.ABI.Unpack(call.Method, b) - if err != nil { - return fmt.Errorf("failed to unpack '%s' outputs: %v", call.Method, err) - } - fieldCount := t.NumField() for i := 0; i < fieldCount; i++ { field := t.Field(i) diff --git a/caller.go b/caller.go index eb2e10d..4c04d2b 100644 --- a/caller.go +++ b/caller.go @@ -3,6 +3,7 @@ package multicall import ( "context" "fmt" + "log" "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -47,6 +48,19 @@ func Dial(ctx context.Context, rawUrl string, multicallAddr ...string) (*Caller, // Call makes multicalls. func (caller *Caller) Call(opts *bind.CallOpts, calls ...*Call) ([]*Call, error) { + return caller.calls(opts, calls...) +} + +func (caller *Caller) CallSingle(opts *bind.CallOpts, call *Call) (*Call, error) { + + calls, err := caller.calls(opts, call) + if err != nil { + return call, fmt.Errorf("CallSingle failed: %v", err) + } + return calls[0], nil +} + +func (caller *Caller) calls(opts *bind.CallOpts, calls ...*Call) ([]*Call, error) { var multiCalls []contract_multicall.Multicall3Call3 for i, call := range calls { @@ -70,6 +84,10 @@ func (caller *Caller) Call(opts *bind.CallOpts, calls ...*Call) ([]*Call, error) call := calls[i] // index always matches call.Failed = !result.Success if err := call.Unpack(result.ReturnData); err != nil { + if call.CanFail { + log.Println(fmt.Errorf("failed to unpack call outputs at index [%d]: %v", i, err)) + continue + } return calls, fmt.Errorf("failed to unpack call outputs at index [%d]: %v", i, err) } } @@ -86,7 +104,7 @@ func (caller *Caller) CallChunked(opts *bind.CallOpts, chunkSize int, cooldown t time.Sleep(cooldown) } - chunk, err := caller.Call(opts, chunk...) + chunk, err := caller.calls(opts, chunk...) if err != nil { return calls, fmt.Errorf("call chunk [%d] failed: %v", i, err) }