diff --git a/go.sum b/go.sum index b946e8c..dc2f64e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/0xsequence/ethkit v1.27.7 h1:0g2CWWS+OYdhf6gfDlAjU566UIoiGWhZICjijPZtMjA= -github.com/0xsequence/ethkit v1.27.7/go.mod h1:rv0FAIyEyN0hhwGefbduAz4ujmyjyJXhCd6a0/yF3tk= github.com/0xsequence/go-ethauth v0.13.0 h1:ZaqFEEqy574A2b1P7vjpcy5tb4W/izn+A3swwOYi9wA= github.com/0xsequence/go-ethauth v0.13.0/go.mod h1:f3kx39S9F+W+qvZEB6bkKKbpUstmyB7goUntO3wvlhg= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= diff --git a/go.work.sum b/go.work.sum index 48ed467..d7e2566 100644 --- a/go.work.sum +++ b/go.work.sum @@ -251,6 +251,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -390,6 +391,7 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= diff --git a/intents/intent_data_transaction_contract_abi.go b/intents/intent_data_transaction_contract_abi.go index 5ce1e74..dfcb256 100644 --- a/intents/intent_data_transaction_contract_abi.go +++ b/intents/intent_data_transaction_contract_abi.go @@ -1,16 +1,7 @@ package intents import ( - "encoding/json" - "errors" - "fmt" - "reflect" - "strings" - "github.com/0xsequence/ethkit/ethcoder" - "github.com/0xsequence/ethkit/go-ethereum/accounts/abi" - "github.com/0xsequence/ethkit/go-ethereum/common" - "github.com/davecgh/go-spew/spew" ) type contractCallType struct { @@ -19,259 +10,13 @@ type contractCallType struct { Args []any `json:"args"` } +// EncodeContractCall encodes a contract call as a hex encoded calldata. +// NOTE: see ethcoder.EncodeContractCall for more details. func EncodeContractCall(data *contractCallType) (string, error) { - data.Abi = strings.TrimSpace(data.Abi) - - // Get the method from the abi - method, _, argTypes, err := getMethodFromAbi(data.Abi, data.Func) - if err != nil { - return "", err - } - - // Prepare the arguments, which may be nested - argStringValues, err := prepareContractCallArgs(data.Args) - if err != nil { - return "", err - } - - spew.Dump("method", method) - spew.Dump("enc", argStringValues) - spew.Dump("argTypes", argTypes) - - // Encode the method call - // TODO: pass argTypes so we dont have to decode again... we can just copy what we do inside of `ethcoder.AbiEncodeMethodCalldataFromStringValuesAny` - // res, err := ethcoder.AbiEncodeMethodCalldataFromStringValuesAny(method, enc) - // if err != nil { - // return "", err - // } - - // TODO: we need to fix up and update ethcoder.AbiEncodeMethodCalldata to upgrade the method parsing functions... etc... - - // TODO: we are calling ParseEventDef multiple times.. - - argValues, err := ethcoder.ABIUnmarshalStringValuesAny(argTypes, argStringValues) - if err != nil { - return "", err - } - // return AbiEncodeMethodCalldata(methodExpr, argValues) - - var mabi abi.ABI - var methodName string - - if len(data.Abi) > 0 && strings.Contains(data.Abi, "(") && data.Abi[len(data.Abi)-1] == ')' { - abiSig, err := ethcoder.ParseABISignature(data.Abi) - if err != nil { - return "", err - } - mabi, err = ethcoder.EventDefToABI(abiSig, true) - if err != nil { - return "", err - } - methodName = abiSig.Name - spew.Dump(mabi) - spew.Dump(argValues) - } else { - mabi, err = abi.JSON(strings.NewReader(data.Abi)) - if err != nil { - return "", err - } - methodName = data.Func - } - - args, err := packableArgValues(mabi, methodName, argValues) - if err != nil { - return "", err - } - - packed, err := mabi.Pack(methodName, args...) - if err != nil { - return "", err - } - - return "0x" + common.Bytes2Hex(packed), nil -} - -func packableArgValues(mabi abi.ABI, method string, argValues []any) ([]any, error) { - m, ok := mabi.Methods[method] - if !ok { - return nil, errors.New("method not found in abi") - } - - if len(m.Inputs) != len(argValues) { - return nil, errors.New("method inputs length does not match arg values length") - } - - fmt.Println("$$$$$$$$$$$$$$$$$$$") - spew.Dump(m.Inputs) - - out := make([]any, len(argValues)) - - for i, input := range m.Inputs { - isTuple := false - typ := input.Type.String() - if len(typ) >= 2 && typ[0] == '(' && typ[len(typ)-1] == ')' { - isTuple = true - } - - if !isTuple { - out[i] = argValues[i] - } else { - // build struct for the tuple, as that is what the geth abi encoder expects - // NOTE: in future we could fork or modify it if we want to avoid the need for this, - // as it means decoding tuples will be more intensive the necessary. - - spew.Dump(input) - - fields := []reflect.StructField{} - - v, ok := argValues[i].([]any) - if !ok { - vv, ok := argValues[i].([]string) - if !ok { - return nil, errors.New("tuple arg values must be an array") - } - v = make([]any, len(vv)) - for j, x := range vv { - v[j] = x - } - } - - for j, vv := range v { - fields = append(fields, reflect.StructField{ - Name: fmt.Sprintf("Name%d", j), - Type: reflect.TypeOf(vv), - }) - } - - structType := reflect.StructOf(fields) - instance := reflect.New(structType).Elem() - - for j, vv := range v { - instance.Field(j).Set(reflect.ValueOf(vv)) - } - - spew.Dump(instance.Interface()) - - out[i] = instance.Interface() - } - } - - return out, nil -} - -func prepareContractCallArgs(args []any) ([]any, error) { - var err error - out := make([]any, len(args)) - - for i, arg := range args { - switch arg := arg.(type) { - case string, []string, []any: - out[i] = arg - - case map[string]interface{}: - nst := arg - - var funcName string - if v, ok := nst["func"].(string); ok { - funcName = v - } - - args, ok := nst["args"].([]interface{}) - if !ok { - return nil, fmt.Errorf("nested encode expects the 'args' field to be an array") - } - - abi, ok := nst["abi"].(string) - if !ok { - return nil, fmt.Errorf("nested encode expects an 'abi' field") - } - - out[i], err = EncodeContractCall(&contractCallType{ - Abi: abi, - Func: funcName, - Args: args, - }) - if err != nil { - return nil, err - } - - default: - return nil, fmt.Errorf("abi encoding fail due to invalid arg type, '%T'", arg) - } - } - - return out, nil -} - -// The abi may be a: -// - already encoded method abi: transferFrom(address,address,uint256) -// - already encoded named method: transferFrom(address from,address to,uint256 val) -// - an array of function abis: "[{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"_orderId\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"_maxCost\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_fees\",\"type\":\"address\"}],\"name\":\"fillOrKillOrder\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"}]" -// - or a single function abi: "{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"_orderId\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"_maxCost\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_fees\",\"type\":\"address\"}],\"name\":\"fillOrKillOrder\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"}" -// And it must always return it encoded, like this: -// - transferFrom(address,address,uint256) -// making sure that the method matches the returned one -func getMethodFromAbi(abi string, method string) (string, []string, []string, error) { - // - // First attempt to parse `abi` string as a plain method abi - // ie. transferFrom(address,address,uint256) - // - - // Handle the case for already encoded method abi. - // NOTE: we do not need the know the `method` argument here. - abi = strings.TrimSpace(abi) - if len(abi) > 0 && strings.Contains(abi, "(") && abi[len(abi)-1] == ')' { - // NOTE: even though the ethcoder function is `ParseEventDef`, designed for event type parsing - // the abi format for a single function structure is the same, so it works. Perhaps we will rename - // `ParseEventDef` in the future, or just add another method with a different name. - abiSig, err := ethcoder.ParseABISignature(abi) - if err != nil { - return "", nil, nil, err - } - return abiSig.Signature, abiSig.ArgNames, abiSig.ArgTypes, nil + callDef := ethcoder.ContractCallDef{ + ABI: data.Abi, + Func: data.Func, + Args: data.Args, } - - // - // If above didn't work, attempt to parse `abi` string as - // a JSON object of the full abi definition - // - - type FunctionAbi struct { - Name string `json:"name"` - Type string `json:"type"` - Inputs []struct { - InternalType string `json:"internalType"` - Name string `json:"name"` - Type string `json:"type"` - } `json:"inputs"` - } - - // Handle array of function abis and single function abi - var abis []FunctionAbi - if strings.HasPrefix(abi, "[") { - if err := json.Unmarshal([]byte(abi), &abis); err != nil { - return "", nil, nil, err - } - } else { - var singleAbi FunctionAbi - if err := json.Unmarshal([]byte(abi), &singleAbi); err != nil { - return "", nil, nil, err - } - abis = append(abis, singleAbi) - } - - // Find the correct method and encode it - for _, fnAbi := range abis { - if fnAbi.Name == method { - var paramTypes []string - order := make([]string, len(fnAbi.Inputs)) - for i, input := range fnAbi.Inputs { - paramTypes = append(paramTypes, input.Type) - order[i] = input.Name - } - return method + "(" + strings.Join(paramTypes, ",") + ")", order, paramTypes, nil - } - } - - return "", nil, nil, fmt.Errorf("method not found in abi") + return ethcoder.EncodeContractCall(callDef) } diff --git a/intents/intent_data_transaction_contract_abi_test.go b/intents/intent_data_transaction_contract_abi_test.go deleted file mode 100644 index 045820b..0000000 --- a/intents/intent_data_transaction_contract_abi_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package intents - -import ( - "testing" - - "github.com/0xsequence/ethkit/ethcoder" - "github.com/0xsequence/ethkit/go-ethereum/common" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetMethodFromABI(t *testing.T) { - // From ABI, alone, in array - res, argNames, argTypes, err := getMethodFromAbi(`[{"name":"transfer","type":"function","inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}]}]`, "transfer") - assert.Nil(t, err) - - require.Equal(t, "transfer(address,uint256)", res) - require.Equal(t, []string{"_to", "_value"}, argNames) - require.Equal(t, []string{"address", "uint256"}, argTypes) - - // From ABI, alone, as object - res, order, _, err := getMethodFromAbi(`{"name":"transfer","type":"function","inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}]}`, "transfer") - assert.Nil(t, err) - - assert.Equal(t, "transfer(address,uint256)", res) - assert.Equal(t, []string{"_to", "_value"}, order) - - // From ABI, with many args - res, order, _, err = getMethodFromAbi(`[{"inputs":[{"internalType":"bytes32","name":"_orderId","type":"bytes32"},{"internalType":"uint256","name":"_maxCost","type":"uint256"},{"internalType":"address[]","name":"_fees","type":"address[]"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"fillOrKillOrder","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_val","type":"uint256"},{"internalType":"string","name":"_data","type":"string"}],"name":"notExpired","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[],"name":"otherMethods","outputs":[],"stateMutability":"nonpayable","type":"function"}]`, "fillOrKillOrder") - assert.Nil(t, err) - - assert.Equal(t, "fillOrKillOrder(bytes32,uint256,address[],bytes)", res) - assert.Equal(t, []string{"_orderId", "_maxCost", "_fees", "_data"}, order) - - res, order, _, err = getMethodFromAbi(`[{"inputs":[{"internalType":"bytes32","name":"_orderId","type":"bytes32"},{"internalType":"uint256","name":"_maxCost","type":"uint256"},{"internalType":"address[]","name":"_fees","type":"address[]"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"fillOrKillOrder","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_val","type":"uint256"},{"internalType":"string","name":"_data","type":"string"}],"name":"notExpired","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[],"name":"otherMethods","outputs":[],"stateMutability":"nonpayable","type":"function"}]`, "notExpired") - assert.Nil(t, err) - - assert.Equal(t, "notExpired(uint256,string)", res) - assert.Equal(t, []string{"_val", "_data"}, order) - - res, order, _, err = getMethodFromAbi(`[{"inputs":[{"internalType":"bytes32","name":"_orderId","type":"bytes32"},{"internalType":"uint256","name":"_maxCost","type":"uint256"},{"internalType":"address[]","name":"_fees","type":"address[]"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"fillOrKillOrder","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_val","type":"uint256"},{"internalType":"string","name":"_data","type":"string"}],"name":"notExpired","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[],"name":"otherMethods","outputs":[],"stateMutability":"nonpayable","type":"function"}]`, "otherMethods") - assert.Nil(t, err) - - assert.Equal(t, "otherMethods()", res) - assert.Equal(t, []string{}, order) - - // From plain method, without named args - res, order, _, err = getMethodFromAbi(`transfer(address,uint256)`, "transfer") - assert.Nil(t, err) - - assert.Equal(t, "transfer(address,uint256)", res) - assert.Equal(t, []string{"arg1", "arg2"}, order) - - // From plain method, with named args - res, order, _, err = getMethodFromAbi(`transfer(address _to,uint256 _value, bytes _mas)`, "transfer") - assert.Nil(t, err) - - assert.Equal(t, "transfer(address,uint256,bytes)", res) - assert.Equal(t, []string{"_to", "_value", "_mas"}, order) - - // Mixed plain method should return nil order - res, order, _, err = getMethodFromAbi(`transfer(address _to,uint256, bytes _mas)`, "transfer") - - assert.Nil(t, err) - assert.Equal(t, "transfer(address,uint256,bytes)", res) - assert.Equal(t, []string{"_to", "arg2", "_mas"}, order) -} - -func TestEncodeContractCall(t *testing.T) { - // Encode simple transferFrom, not named - res, err := EncodeContractCall(&contractCallType{ - Abi: `[{"name":"transferFrom","type":"function","inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}]}]`, - Func: "transferFrom", - Args: []any{"0x0dc9603d4da53841C1C83f3B550C6143e60e0425", "0x0dc9603d4da53841C1C83f3B550C6143e60e0425", "100"}, - }) - require.Nil(t, err) - require.Equal(t, "0x23b872dd0000000000000000000000000dc9603d4da53841c1c83f3b550c6143e60e04250000000000000000000000000dc9603d4da53841c1c83f3b550c6143e60e04250000000000000000000000000000000000000000000000000000000000000064", res) - - // Encode simple transferFrom, selector - res, err = EncodeContractCall(&contractCallType{ - Abi: `transferFrom(address,address,uint256)`, - Args: []any{"0x0dc9603d4da53841C1C83f3B550C6143e60e0425", "0x0dc9603d4da53841C1C83f3B550C6143e60e0425", "100"}, - }) - require.Nil(t, err) - require.Equal(t, "0x23b872dd0000000000000000000000000dc9603d4da53841c1c83f3b550c6143e60e04250000000000000000000000000dc9603d4da53841c1c83f3b550c6143e60e04250000000000000000000000000000000000000000000000000000000000000064", res) - - // Encode simple transferFrom, named - // res, err = EncodeContractCall(&contractCallType{ - // Abi: `[{"name":"transferFrom","type":"function","inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}]}]`, - // Func: "transferFrom", - // Args: json.RawMessage(`{"_from": "0x0dc9603d4da53841C1C83f3B550C6143e60e0425", "_value": "100", "_to": "0x0dc9603d4da53841C1C83f3B550C6143e60e0425"}`), - // }) - // require.Nil(t, err) - // require.Equal(t, res, "0x23b872dd0000000000000000000000000dc9603d4da53841c1c83f3b550c6143e60e04250000000000000000000000000dc9603d4da53841c1c83f3b550c6143e60e04250000000000000000000000000000000000000000000000000000000000000064") - - // Encode simple transferFrom, not named, passed as function - res, err = EncodeContractCall(&contractCallType{ - Abi: `transferFrom(address,address,uint256)`, - Args: []any{"0x13915b1ea28Fd2E8197c88ff9D2422182E83bf25", "0x4Ad47F1611c78C824Ff3892c4aE1CC04637D6462", "5192381927398174182391237"}, - }) - require.Nil(t, err) - require.Equal(t, "0x23b872dd00000000000000000000000013915b1ea28fd2e8197c88ff9d2422182e83bf250000000000000000000000004ad47f1611c78c824ff3892c4ae1cc04637d6462000000000000000000000000000000000000000000044b87969b06250e50bdc5", res) - - // Encode simple transferFrom, named, passed as function - // res, err = EncodeContractCall(&contractCallType{ - // Abi: `transferFrom(address _from,address _to,uint256 _value)`, - // Func: "transferFrom", - // Args: json.RawMessage(`{"_from": "0x13915b1ea28Fd2E8197c88ff9D2422182E83bf25", "_value": "5192381927398174182391237", "_to": "0x4Ad47F1611c78C824Ff3892c4aE1CC04637D6462"}`), - // }) - // require.Nil(t, err) - // require.Equal(t, res, "0x23b872dd00000000000000000000000013915b1ea28fd2e8197c88ff9d2422182e83bf250000000000000000000000004ad47f1611c78c824ff3892c4ae1cc04637d6462000000000000000000000000000000000000000000044b87969b06250e50bdc5") - - // // Encode nested bytes, passed as function - // nestedEncodeType1 := &contractCallType{ - // Abi: `transferFrom(uint256)`, - // Args: []any{"481923749816926378123"}, - // } - - // nestedEncodeType2 := &contractCallType{ - // Abi: `hola(string)`, - // Args: []any{"mundo"}, - // } - - // net1jsn, err := json.Marshal(nestedEncodeType1) - // require.Nil(t, err) - - // net2jsn, err := json.Marshal(nestedEncodeType2) - // require.Nil(t, err) - - arg2, err := ethcoder.AbiEncodeMethodCalldataFromStringValues("transferFrom(uint256)", []string{"481923749816926378123"}) - require.NoError(t, err) - - arg3, err := ethcoder.AbiEncodeMethodCalldataFromStringValues("hola(string)", []string{"mundo"}) - require.NoError(t, err) - - arg2Hex := "0x" + common.Bytes2Hex(arg2) - arg3Hex := "0x" + common.Bytes2Hex(arg3) - - res, err = EncodeContractCall(&contractCallType{ - Abi: "caller(address,bytes,bytes)", - Func: "caller", - Args: []any{"0x13915b1ea28Fd2E8197c88ff9D2422182E83bf25", arg2Hex, arg3Hex}, - }) - require.Nil(t, err) - require.Equal(t, "0x8b6701df00000000000000000000000013915b1ea28fd2e8197c88ff9d2422182e83bf25000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000002477a11f7e00000000000000000000000000000000000000000000001a2009191df61e988b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000646ce8ea55000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000056d756e646f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", res) - - // Fail passing named args to non-named abi - // res, err = EncodeContractCall(&contractCallType{ - // Abi: `transferFrom(address,uint256)`, - // Func: "transferFrom", - // Args: json.RawMessage(`{"_from": "0x13915b1ea28Fd2E8197c88ff9D2422182E83bf25", "_value": "5192381927398174182391237", "_to": "0x4Ad47F1611c78C824Ff3892c4aE1CC04637D6462"}`), - // }) - // assert.NotNil(t, err) - - // Accept passing ordened args to named abi - res, err = EncodeContractCall(&contractCallType{ - Abi: `transferFrom(address _from,address _to,uint256 _value)`, - Func: "transferFrom", - Args: []any{"0x13915b1ea28Fd2E8197c88ff9D2422182E83bf25", "0x4Ad47F1611c78C824Ff3892c4aE1CC04637D6462", "9"}, - }) - require.Nil(t, err) - require.Equal(t, "0x23b872dd00000000000000000000000013915b1ea28fd2e8197c88ff9d2422182e83bf250000000000000000000000004ad47f1611c78c824ff3892c4ae1cc04637d64620000000000000000000000000000000000000000000000000000000000000009", res) - - // ... - res, err = EncodeContractCall(&contractCallType{ - Abi: `fillOrder(uint256 orderId, uint256 maxCost, address[] fees, (uint256 a, address b) extra)`, - Args: []any{ - "48774435471364917511246724398022004900255301025912680232738918790354204737320", - "1000000000000000000", - []string{"0x8541D65829f98f7D71A4655cCD7B2bB8494673bF"}, - []string{"123456789", "0x1231f65f29f98e7D71A4655cCD7B2bc441211feb"}, - }, - }) - require.Nil(t, err) - require.Equal(t, "0x326a62086bd55a2877890bd58871eefe886770a7734077a74981910a75d7b1f044b5bf280000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000075bcd150000000000000000000000001231f65f29f98e7d71a4655ccd7b2bc441211feb00000000000000000000000000000000000000000000000000000000000000010000000000000000000000008541d65829f98f7d71a4655ccd7b2bb8494673bf", res) -} diff --git a/intents/intent_data_transaction_delayed_abi.go b/intents/intent_data_transaction_delayed_abi.go index a8707d7..9abfc98 100644 --- a/intents/intent_data_transaction_delayed_abi.go +++ b/intents/intent_data_transaction_delayed_abi.go @@ -3,6 +3,7 @@ package intents import ( "encoding/json" "fmt" + "strings" "github.com/0xsequence/ethkit/ethcoder" "github.com/0xsequence/ethkit/go-ethereum/common" @@ -18,7 +19,7 @@ type delayedEncodeType struct { // Deprecated: use EncodeContractCall instead func EncodeDelayedABI(data *delayedEncodeType) (string, error) { // Get the method from the abi - method, order, _, err := getMethodFromAbi(data.Abi, data.Func) + _, method, order, _, err := getMethodFromAbi(data.Abi, data.Func) if err != nil { return "", err } @@ -96,3 +97,78 @@ func EncodeDelayedABI(data *delayedEncodeType) (string, error) { return "", err } + +// The abi may be a: +// - already encoded method abi: transferFrom(address,address,uint256) +// - already encoded named method: transferFrom(address from,address to,uint256 val) +// - an array of function abis: "[{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"_orderId\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"_maxCost\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_fees\",\"type\":\"address\"}],\"name\":\"fillOrKillOrder\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"}]" +// - or a single function abi: "{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"_orderId\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"_maxCost\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_fees\",\"type\":\"address\"}],\"name\":\"fillOrKillOrder\",\"outputs\":[],\"stateMutability\":\"view\",\"type\":\"function\"}" +// And it must always return it encoded, like this: +// - transferFrom(address,address,uint256) +// making sure that the method matches the returned one +func getMethodFromAbi(abiSigOrJSON string, method string) (*ethcoder.ABISignature, string, []string, []string, error) { + // + // First attempt to parse `abiSigOrJSON` string as a plain method abi + // ie. transferFrom(address,address,uint256) + // + + // Handle the case for already encoded method abi. + // NOTE: we do not need the know the `method` argument here. + abiSigOrJSON = strings.TrimSpace(abiSigOrJSON) + if len(abiSigOrJSON) > 0 && strings.Contains(abiSigOrJSON, "(") && abiSigOrJSON[len(abiSigOrJSON)-1] == ')' { + // NOTE: even though the ethcoder function is `ParseEventDef`, designed for event type parsing + // the abi format for a single function structure is the same, so it works. Perhaps we will rename + // `ParseEventDef` in the future, or just add another method with a different name. + abiSig, err := ethcoder.ParseABISignature(abiSigOrJSON) + if err != nil { + return nil, "", nil, nil, err + } + + // NOTE: we only return the abiSig if the `abiSigOrJSON` is an abi signature + return &abiSig, abiSig.Signature, abiSig.ArgNames, abiSig.ArgTypes, nil + } + + // + // If above didn't work, attempt to parse `abi` string as + // a JSON object of the full abi definition + // + + type FunctionAbi struct { + Name string `json:"name"` + Type string `json:"type"` + Inputs []struct { + InternalType string `json:"internalType"` + Name string `json:"name"` + Type string `json:"type"` + } `json:"inputs"` + } + + // Handle array of function abis and single function abi + var abis []FunctionAbi + if strings.HasPrefix(abiSigOrJSON, "[") { + if err := json.Unmarshal([]byte(abiSigOrJSON), &abis); err != nil { + return nil, "", nil, nil, err + } + } else { + var singleAbi FunctionAbi + if err := json.Unmarshal([]byte(abiSigOrJSON), &singleAbi); err != nil { + return nil, "", nil, nil, err + } + abis = append(abis, singleAbi) + } + + // Find the correct method and encode it + for _, fnAbi := range abis { + if fnAbi.Name == method { + var paramTypes []string + order := make([]string, len(fnAbi.Inputs)) + for i, input := range fnAbi.Inputs { + paramTypes = append(paramTypes, input.Type) + order[i] = input.Name + } + return nil, method + "(" + strings.Join(paramTypes, ",") + ")", order, paramTypes, nil + } + } + + return nil, "", nil, nil, fmt.Errorf("method not found in abi") +} diff --git a/intents/intent_data_transaction_delayed_abi_test.go b/intents/intent_data_transaction_delayed_abi_test.go index 7e6cf53..bd39c72 100644 --- a/intents/intent_data_transaction_delayed_abi_test.go +++ b/intents/intent_data_transaction_delayed_abi_test.go @@ -4,9 +4,67 @@ import ( "encoding/json" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestGetMethodFromABI(t *testing.T) { + // From ABI, alone, in array + _, res, argNames, argTypes, err := getMethodFromAbi(`[{"name":"transfer","type":"function","inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}]}]`, "transfer") + assert.Nil(t, err) + + require.Equal(t, "transfer(address,uint256)", res) + require.Equal(t, []string{"_to", "_value"}, argNames) + require.Equal(t, []string{"address", "uint256"}, argTypes) + + // From ABI, alone, as object + _, res, order, _, err := getMethodFromAbi(`{"name":"transfer","type":"function","inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}]}`, "transfer") + assert.Nil(t, err) + + assert.Equal(t, "transfer(address,uint256)", res) + assert.Equal(t, []string{"_to", "_value"}, order) + + // From ABI, with many args + _, res, order, _, err = getMethodFromAbi(`[{"inputs":[{"internalType":"bytes32","name":"_orderId","type":"bytes32"},{"internalType":"uint256","name":"_maxCost","type":"uint256"},{"internalType":"address[]","name":"_fees","type":"address[]"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"fillOrKillOrder","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_val","type":"uint256"},{"internalType":"string","name":"_data","type":"string"}],"name":"notExpired","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[],"name":"otherMethods","outputs":[],"stateMutability":"nonpayable","type":"function"}]`, "fillOrKillOrder") + assert.Nil(t, err) + + assert.Equal(t, "fillOrKillOrder(bytes32,uint256,address[],bytes)", res) + assert.Equal(t, []string{"_orderId", "_maxCost", "_fees", "_data"}, order) + + _, res, order, _, err = getMethodFromAbi(`[{"inputs":[{"internalType":"bytes32","name":"_orderId","type":"bytes32"},{"internalType":"uint256","name":"_maxCost","type":"uint256"},{"internalType":"address[]","name":"_fees","type":"address[]"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"fillOrKillOrder","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_val","type":"uint256"},{"internalType":"string","name":"_data","type":"string"}],"name":"notExpired","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[],"name":"otherMethods","outputs":[],"stateMutability":"nonpayable","type":"function"}]`, "notExpired") + assert.Nil(t, err) + + assert.Equal(t, "notExpired(uint256,string)", res) + assert.Equal(t, []string{"_val", "_data"}, order) + + _, res, order, _, err = getMethodFromAbi(`[{"inputs":[{"internalType":"bytes32","name":"_orderId","type":"bytes32"},{"internalType":"uint256","name":"_maxCost","type":"uint256"},{"internalType":"address[]","name":"_fees","type":"address[]"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"fillOrKillOrder","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_val","type":"uint256"},{"internalType":"string","name":"_data","type":"string"}],"name":"notExpired","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[],"name":"otherMethods","outputs":[],"stateMutability":"nonpayable","type":"function"}]`, "otherMethods") + assert.Nil(t, err) + + assert.Equal(t, "otherMethods()", res) + assert.Equal(t, []string{}, order) + + // From plain method, without named args + _, res, order, _, err = getMethodFromAbi(`transfer(address,uint256)`, "transfer") + assert.Nil(t, err) + + assert.Equal(t, "transfer(address,uint256)", res) + assert.Equal(t, []string{"arg1", "arg2"}, order) + + // From plain method, with named args + _, res, order, _, err = getMethodFromAbi(`transfer(address _to,uint256 _value, bytes _mas)`, "transfer") + assert.Nil(t, err) + + assert.Equal(t, "transfer(address,uint256,bytes)", res) + assert.Equal(t, []string{"_to", "_value", "_mas"}, order) + + // Mixed plain method should return nil order + _, res, order, _, err = getMethodFromAbi(`transfer(address _to,uint256, bytes _mas)`, "transfer") + + assert.Nil(t, err) + assert.Equal(t, "transfer(address,uint256,bytes)", res) + assert.Equal(t, []string{"_to", "arg2", "_mas"}, order) +} + func TestEncodeDelayedABI(t *testing.T) { // Encode simple transferFrom, not named res, err := EncodeDelayedABI(&delayedEncodeType{ diff --git a/intents/intent_data_transaction_ext.go b/intents/intent_data_transaction_ext.go index e75510b..0d03076 100644 --- a/intents/intent_data_transaction_ext.go +++ b/intents/intent_data_transaction_ext.go @@ -11,7 +11,6 @@ import ( "github.com/0xsequence/ethkit/go-ethereum/core/types" "github.com/0xsequence/go-sequence" "github.com/0xsequence/go-sequence/relayer/proto" - "github.com/davecgh/go-spew/spew" ) func (p *IntentDataSendTransaction) chainID() (*big.Int, error) { @@ -215,8 +214,6 @@ func (p *IntentDataSendTransaction) ExpectedValuesFor(txRaw *json.RawMessage) (* } nst.Args = tx.Data.Args - spew.Dump("$$$WEE$$", nst) - encoded, err := EncodeContractCall(nst) if err != nil { return nil, err diff --git a/intents/intent_data_transaction_ext_test.go b/intents/intent_data_transaction_ext_test.go index c612d3c..55689b9 100644 --- a/intents/intent_data_transaction_ext_test.go +++ b/intents/intent_data_transaction_ext_test.go @@ -314,7 +314,7 @@ func TestRecoverTransactionIntent(t *testing.T) { require.False(t, ok) } -// NOTE: DelayedEncode is deprecated, see TestRecoverTransactionIntent below which uses contractCall +// NOTE: DelayedEncode is deprecated, see TestRecoverTransactionIntent below which uses contractCall type func TestDelayedEncodeRecoverTransactionIntent(t *testing.T) { data := `{ "version": "1",