diff --git a/go.mod b/go.mod index 8bfd6a4..e7db85f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.20 require ( github.com/coinbase/rosetta-sdk-go v0.8.2 github.com/ethereum/go-ethereum v1.13.8 - github.com/holiman/uint256 v1.2.4 github.com/neilotoole/errgroup v0.1.6 github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.17.0 @@ -49,6 +48,7 @@ require ( github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect + github.com/holiman/uint256 v1.2.4 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/klauspost/compress v1.15.15 // indirect diff --git a/services/construction/contract_call_data.go b/services/construction/contract_call_data.go index 743dd91..6aeb7bc 100644 --- a/services/construction/contract_call_data.go +++ b/services/construction/contract_call_data.go @@ -29,6 +29,8 @@ import ( "golang.org/x/crypto/sha3" ) +const NoMethodSig = "NO-METHOD-SIG" + // ConstructContractCallDataGeneric constructs the data field of a transaction. // The methodArgs can be already in ABI encoded format in case of a single string // It can also be passed in as a slice of args, which requires further encoding. @@ -38,8 +40,14 @@ func ConstructContractCallDataGeneric(methodSig string, methodArgs interface{}) return nil, err } + // preprocess method args for fallback pattern contract call + args, err := preprocessArgs(methodSig, methodArgs) + if err != nil { + return nil, err + } + // switch on the type of the method args. method args can come in from json as either a string or list of strings - switch methodArgs := methodArgs.(type) { + switch methodArgs := args.(type) { // case 0: no method arguments, return the selector case nil: return data, nil @@ -65,6 +73,7 @@ func ConstructContractCallDataGeneric(methodSig string, methodArgs interface{}) } strList = append(strList, strVal) } + return encodeMethodArgsStrings(data, methodSig, strList) // case 3: method args are encoded as a list of strings, which will be decoded @@ -80,6 +89,28 @@ func ConstructContractCallDataGeneric(methodSig string, methodArgs interface{}) } } +// preprocessArgs converts methodArgs to a string value if methodSig is an empty string. +// We are calling a contract written with fallback pattern, which has no method signature. +func preprocessArgs(methodSig string, methodArgs interface{}) (interface{}, error) { + if methodSig == "" || methodSig == NoMethodSig { + switch args := methodArgs.(type) { + case []interface{}: + if len(args) == 1 { + if argStr, ok := args[0].(string); ok { + return argStr, nil + } + return nil, fmt.Errorf("failed to convert method arg \"%T\" to string", args[0]) + } + case []string: + if len(args) == 1 { + return args[0], nil + } + } + } + + return methodArgs, nil +} + // encodeMethodArgsStrings constructs the data field of a transaction for a list of string args. // It attempts to first convert the string arg to it's corresponding type in the method signature, // and then performs abi encoding to the converted args list and construct the data. @@ -185,6 +216,11 @@ func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []str // contractCallMethodID calculates the first 4 bytes of the method // signature for function call on contract func contractCallMethodID(methodSig string) ([]byte, error) { + if methodSig == "" || methodSig == NoMethodSig { + // contract call without method signature (fallback pattern) + return []byte{}, nil + } + fnSignature := []byte(methodSig) hash := sha3.NewLegacyKeccak256() if _, err := hash.Write(fnSignature); err != nil { diff --git a/services/construction/contract_call_data_test.go b/services/construction/contract_call_data_test.go index 3ab2a68..e3bafde 100644 --- a/services/construction/contract_call_data_test.go +++ b/services/construction/contract_call_data_test.go @@ -51,6 +51,16 @@ func TestConstruction_ContractCallData(t *testing.T) { methodArgs: []interface{}{"bool abc", "0x0000000000000000000000000000000000000000", "true"}, expectedResponse: "0x60d7a2780000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008626f6f6c20616263000000000000000000000000000000000000000000000000", }, + "happy path: method sig is an empty string and args is a list of interface": { + methodSig: "", + methodArgs: []interface{}{"0xabcde12345"}, + expectedResponse: "0xabcde12345", + }, + "happy path: method sig is NO-METHOD-SIG and args is a list of interface": { + methodSig: NoMethodSig, + methodArgs: []interface{}{"0xaabbcc112233"}, + expectedResponse: "0xaabbcc112233", + }, "error: case string: invalid method args hex data": { methodSig: "attest((bytes32,(address,uint64,bool,bytes32,bytes,uint256)))", methodArgs: "!!!", @@ -75,3 +85,63 @@ func TestConstruction_ContractCallData(t *testing.T) { }) } } + +func TestConstruction_preprocessArgs(t *testing.T) { + tests := map[string]struct { + methodSig string + methodArgs interface{} + + expectedResponse interface{} + expectedError error + }{ + "happy path: method sig is function name": { + methodSig: "withdraw(address,uint256,uint32,bytes)", + methodArgs: []interface{}{ + "0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22", + "32941055343948244352", + "0", + "0x"}, + expectedResponse: []interface{}{ + "0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22", + "32941055343948244352", + "0", + "0x"}, + }, + "happy path: method sig is empty and args is nil": { + methodSig: "", + methodArgs: nil, + expectedResponse: nil, + }, + "happy path: method sig is NO-METHOD-SIG and args is a single string": { + methodSig: NoMethodSig, + methodArgs: "0x12345", + expectedResponse: "0x12345", + }, + "happy path: method sig is empty and args is a list of interface": { + methodSig: "", + methodArgs: []interface{}{"0xabcde"}, + expectedResponse: "0xabcde", + }, + "happy path: method sig is NO-METHOD-SIG and args is a list of strings": { + methodSig: NoMethodSig, + methodArgs: []string{"0x1a2b3c"}, + expectedResponse: "0x1a2b3c", + }, + "unhappy path: args is a list of interface and cannot be converted to strings": { + methodSig: "", + methodArgs: []interface{}{34567}, + expectedError: errors.New("failed to convert method arg \"int\" to string"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + argsReturned, err := preprocessArgs(test.methodSig, test.methodArgs) + if err != nil { + assert.EqualError(t, err, test.expectedError.Error()) + } else { + assert.Equal(t, test.expectedResponse, argsReturned) + } + }) + } +}