From de42b15f748798d611bb7877d78bbf0f29194d98 Mon Sep 17 00:00:00 2001 From: "jiangchuan.he" Date: Sat, 23 Sep 2023 14:18:06 -0700 Subject: [PATCH 1/4] enable arbitrary contract call --- server/service/contract_call_data.go | 181 ++++++++++++++++++++ server/service/contract_call_data_test.go | 63 +++++++ server/service/service_construction.go | 126 ++++++++++++-- server/service/service_construction_test.go | 42 +++++ server/service/types.go | 65 ++++++- 5 files changed, 453 insertions(+), 24 deletions(-) create mode 100644 server/service/contract_call_data.go create mode 100644 server/service/contract_call_data_test.go diff --git a/server/service/contract_call_data.go b/server/service/contract_call_data.go new file mode 100644 index 0000000..100a540 --- /dev/null +++ b/server/service/contract_call_data.go @@ -0,0 +1,181 @@ +package service + +import ( + "encoding/hex" + "errors" + "fmt" + "log" + "math/big" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "golang.org/x/crypto/sha3" +) + +// 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. +func constructContractCallDataGeneric(methodSig string, methodArgs interface{}) ([]byte, error) { + data, err := contractCallMethodID(methodSig) + 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) { + // case 0: no method arguments, return the selector + case nil: + return data, nil + + // case 1: method args are pre-compiled ABI data. decode the hex and create the call data directly + case string: + methodArgs = strings.TrimPrefix(methodArgs, "0x") + b, decErr := hex.DecodeString(methodArgs) + if decErr != nil { + return nil, fmt.Errorf("error decoding method args hex data: %w", decErr) + } + return append(data, b...), nil + + // case 2: method args are a list of interface{} which will be converted to string before encoding + case []interface{}: + var strList []string + for i, genericVal := range methodArgs { + strVal, isStrVal := genericVal.(string) + if !isStrVal { + return nil, fmt.Errorf("invalid method_args type at index %d: %T (must be a string)", + i, genericVal, + ) + } + strList = append(strList, strVal) + } + return encodeMethodArgsStrings(data, methodSig, strList) + + // case 3: method args are encoded as a list of strings, which will be decoded + case []string: + return encodeMethodArgsStrings(data, methodSig, methodArgs) + + // case 4: there is no known way to decode the method args + default: + return nil, fmt.Errorf( + "invalid method_args type, accepted values are []string and hex-encoded string."+ + " type received=%T value=%#v", methodArgs, methodArgs, + ) + } +} + +// 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. +func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []string) ([]byte, error) { + arguments := abi.Arguments{} + var argumentsData []interface{} + + var data []byte + data = append(data, methodID...) + + const split = 2 + splitSigByLeadingParenthesis := strings.Split(methodSig, "(") + if len(splitSigByLeadingParenthesis) < split { + return data, nil + } + splitSigByTrailingParenthesis := strings.Split(splitSigByLeadingParenthesis[1], ")") + if len(splitSigByTrailingParenthesis) < 1 { + return data, nil + } + splitSigByComma := strings.Split(splitSigByTrailingParenthesis[0], ",") + + if len(splitSigByComma) != len(methodArgs) { + return nil, errors.New("invalid method arguments") + } + + for i, v := range splitSigByComma { + typed, _ := abi.NewType(v, v, nil) + argument := abi.Arguments{ + { + Type: typed, + }, + } + + arguments = append(arguments, argument...) + var argData interface{} + const base = 10 + switch { + case v == "address": + { + argData = common.HexToAddress(methodArgs[i]) + } + case v == "uint32": + { + u64, err := strconv.ParseUint(methodArgs[i], 10, 32) + if err != nil { + log.Fatal(err) + } + argData = uint32(u64) + } + case strings.HasPrefix(v, "uint") || strings.HasPrefix(v, "int"): + { + value := new(big.Int) + value.SetString(methodArgs[i], base) + argData = value + } + case v == "bytes32": + { + value := [32]byte{} + bytes, err := hexutil.Decode(methodArgs[i]) + if err != nil { + log.Fatal(err) + } + copy(value[:], bytes) + argData = value + } + case strings.HasPrefix(v, "bytes"): + { + // No fixed size set as it would make it an "array" instead + // of a "slice" when encoding. We want it to be a slice. + value := []byte{} + bytes, err := hexutil.Decode(methodArgs[i]) + if err != nil { + log.Fatal(err) + } + copy(value[:], bytes) // nolint:gocritic + argData = value + } + case strings.HasPrefix(v, "string"): + { + argData = methodArgs[i] + } + case strings.HasPrefix(v, "bool"): + { + value, err := strconv.ParseBool(methodArgs[i]) + if err != nil { + log.Fatal(err) + } + argData = value + } + } + argumentsData = append(argumentsData, argData) + } + + abiEncodeData, err := arguments.PackValues(argumentsData) + if err != nil { + return nil, fmt.Errorf("failed to encode arguments: %w", err) + } + + data = append(data, abiEncodeData...) + return data, nil +} + +// contractCallMethodID calculates the first 4 bytes of the method +// signature for function call on contract +func contractCallMethodID(methodSig string) ([]byte, error) { + fnSignature := []byte(methodSig) + hash := sha3.NewLegacyKeccak256() + if _, err := hash.Write(fnSignature); err != nil { + return nil, err + } + + return hash.Sum(nil)[:4], nil +} diff --git a/server/service/contract_call_data_test.go b/server/service/contract_call_data_test.go new file mode 100644 index 0000000..06688f6 --- /dev/null +++ b/server/service/contract_call_data_test.go @@ -0,0 +1,63 @@ +package service + +import ( + "errors" + "fmt" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/assert" +) + +func TestConstruction_ContractCallData(t *testing.T) { + tests := map[string]struct { + methodSig string + methodArgs interface{} + + expectedResponse string + expectedError error + }{ + "happy path: nil args": { + methodSig: "deposit()", + methodArgs: nil, + expectedResponse: "0xd0e30db0", + }, + "happy path: single string arg": { + methodSig: "attest((bytes32,(address,uint64,bool,bytes32,bytes,uint256)))", + methodArgs: "0x00000000000000000000000000000000000000000000000000000000000000201cdb5651ea836ecc9be70d044e2cf7a416e5257ec8d954deb9d09a66a8264b8e000000000000000000000000000000000000000000000000000000000000004000000000000000000000000026c58c5095c8fac99e518ee951ba8f56d3c75e8e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001", + expectedResponse: "0xf17325e700000000000000000000000000000000000000000000000000000000000000201cdb5651ea836ecc9be70d044e2cf7a416e5257ec8d954deb9d09a66a8264b8e000000000000000000000000000000000000000000000000000000000000004000000000000000000000000026c58c5095c8fac99e518ee951ba8f56d3c75e8e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001", + }, + "happy path: list of string args": { + methodSig: "register(string,address,bool)", + methodArgs: []string{"bool abc", "0x0000000000000000000000000000000000000000", "true"}, + expectedResponse: "0x60d7a2780000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008626f6f6c20616263000000000000000000000000000000000000000000000000", + }, + "happy path: list of non string args": { + methodSig: "register(string,address,bool)", + methodArgs: []interface{}{"bool abc", "0x0000000000000000000000000000000000000000", "true"}, + expectedResponse: "0x60d7a2780000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008626f6f6c20616263000000000000000000000000000000000000000000000000", + }, + "error: case string: invalid method args hex data": { + methodSig: "attest((bytes32,(address,uint64,bool,bytes32,bytes,uint256)))", + methodArgs: "!!!", + expectedError: errors.New("error decoding method args hex data: encoding/hex: invalid byte: U+0021 '!'"), + }, + "error: case []interface: ": { + methodSig: "register(string,address,bool)", + methodArgs: []interface{}{"bool abc", "0x0000000000000000000000000000000000000000", true}, + expectedError: errors.New("invalid method_args type at index 2: bool (must be a string)"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + bytes, err := constructContractCallDataGeneric(test.methodSig, test.methodArgs) + if err != nil { + fmt.Println(err) + assert.EqualError(t, err, test.expectedError.Error()) + } else { + assert.Equal(t, test.expectedResponse, hexutil.Encode(bytes)) + } + }) + } +} diff --git a/server/service/service_construction.go b/server/service/service_construction.go index eca52f4..d83e53c 100644 --- a/server/service/service_construction.go +++ b/server/service/service_construction.go @@ -98,6 +98,8 @@ func (s ConstructionService) ConstructionMetadata( if input.GasLimit == nil { if input.Currency == nil || utils.Equal(input.Currency, mapper.FlareCurrency) { gasLimit, err = s.getNativeTransferGasLimit(ctx, input.To, input.From, input.Value) + } else if len(input.ContractAddress) > 0 { + gasLimit, err = s.getGenericContractCallGasLimit(ctx, input.ContractAddress, input.From, input.Data) } else { gasLimit, err = s.getErc20TransferGasLimit(ctx, input.To, input.From, input.Value, input.Currency) } @@ -110,9 +112,12 @@ func (s ConstructionService) ConstructionMetadata( } metadata := &metadata{ - Nonce: nonce, - GasPrice: gasPrice, - GasLimit: gasLimit, + Nonce: nonce, + GasPrice: gasPrice, + GasLimit: gasLimit, + MethodSignature: input.MethodSignature, + MethodArgs: input.MethodArgs, + Data: input.Data, } metadataMap, err := marshalJSONMap(metadata) @@ -279,16 +284,25 @@ func (s ConstructionService) ConstructionParse( var opMethod string var value *big.Int var toAddressHex string - // Erc20 transfer if len(tx.Data) != 0 { - toAddress, amountSent, err := parseErc20TransferData(tx.Data) - if err != nil { - return nil, wrapError(errInvalidInput, err) + methodID := hexutil.Encode(tx.Data[:4]) + tokenTransferMethodID := hexutil.Encode(getMethodID(transferFnSignature)) + if methodID == tokenTransferMethodID { + // Erc20 transfer + toAddress, amountSent, err := parseErc20TransferData(tx.Data) + if err != nil { + return nil, wrapError(errInvalidInput, err) + } + + value = amountSent + opMethod = mapper.OpErc20Transfer + toAddressHex = toAddress.Hex() + } else { + // Generic contract call + value = tx.Value + opMethod = mapper.OpCall + toAddressHex = tx.To } - - value = amountSent - opMethod = mapper.OpErc20Transfer - toAddressHex = toAddress.Hex() } else { value = tx.Value opMethod = mapper.OpCall @@ -386,7 +400,7 @@ func (s ConstructionService) ConstructionPayloads( ctx context.Context, req *types.ConstructionPayloadsRequest, ) (*types.ConstructionPayloadsResponse, *types.Error) { - operationDescriptions, err := s.CreateOperationDescription(req.Operations) + operationDescriptions, err := s.CreateOperationDescription(req.Operations, req.Metadata) if err != nil { return nil, wrapError(errInvalidInput, err) } @@ -431,6 +445,9 @@ func (s ConstructionService) ConstructionPayloads( if utils.Equal(fromCurrency, mapper.FlareCurrency) { transferData = []byte{} sendToAddress = ethcommon.HexToAddress(checkTo) + } else if len(metadata.MethodSignature) > 0 { + transferData = metadata.Data + sendToAddress = ethcommon.HexToAddress(checkTo) } else { contract, ok := fromCurrency.Metadata[mapper.ContractAddressMetadata].(string) if !ok { @@ -489,7 +506,7 @@ func (s ConstructionService) ConstructionPreprocess( ctx context.Context, req *types.ConstructionPreprocessRequest, ) (*types.ConstructionPreprocessResponse, *types.Error) { - operationDescriptions, err := s.CreateOperationDescription(req.Operations) + operationDescriptions, err := s.CreateOperationDescription(req.Operations, req.Metadata) if err != nil { return nil, wrapError(errInvalidInput, err) } @@ -562,6 +579,24 @@ func (s ConstructionService) ConstructionPreprocess( preprocessOptions.Nonce = bigObj } + if v, ok := req.Metadata["method_signature"]; ok { + methodSigStringObj := v.(string) + if !ok { + return nil, wrapError( + errCallInvalidMethod, + fmt.Errorf("%s is not a valid signature string", v), + ) + } + data, err := constructContractCallDataGeneric(methodSigStringObj, req.Metadata["method_args"]) + if err != nil { + return nil, wrapError(errCallInvalidParams, err) + } + preprocessOptions.ContractAddress = checkTo + preprocessOptions.Data = data + preprocessOptions.MethodSignature = methodSigStringObj + preprocessOptions.MethodArgs = req.Metadata["method_args"] + } + marshaled, err := marshalJSONMap(preprocessOptions) if err != nil { return nil, wrapError(errInternalError, err) @@ -610,6 +645,7 @@ func (s ConstructionService) ConstructionSubmit( func (s ConstructionService) CreateOperationDescription( operations []*types.Operation, + metadata map[string]interface{}, ) ([]*parser.OperationDescription, error) { if len(operations) != 2 { return nil, fmt.Errorf("invalid number of operations") @@ -625,8 +661,22 @@ func (s ConstructionService) CreateOperationDescription( return nil, fmt.Errorf("currency info doesn't match between the operations") } + if _, ok := metadata["method_signature"]; ok && utils.Equal(currency, mapper.FlareCurrency) { + const base = 10 + i := new(big.Int) + i.SetString(operations[0].Amount.Value, base) + j := new(big.Int) + j.SetString(operations[1].Amount.Value, base) + if i.Cmp(big.NewInt(0)) == 0 { + if j.Cmp(big.NewInt(0)) != 0 { + return nil, fmt.Errorf("for contract call both values should be zero") + } + } + return s.createOperationDescription(currency, mapper.OpCall, true), nil + } + if utils.Equal(currency, mapper.FlareCurrency) { - return s.createOperationDescription(currency, mapper.OpCall), nil + return s.createOperationDescription(currency, mapper.OpCall, false), nil } // ERC-20s must have contract address in metadata @@ -634,13 +684,41 @@ func (s ConstructionService) CreateOperationDescription( return nil, fmt.Errorf("contractAddress must be populated in currency metadata") } - return s.createOperationDescription(currency, mapper.OpErc20Transfer), nil + return s.createOperationDescription(currency, mapper.OpErc20Transfer, false), nil } func (s ConstructionService) createOperationDescription( currency *types.Currency, opType string, + isContractCall bool, ) []*parser.OperationDescription { + if isContractCall { + return []*parser.OperationDescription{ + { + Type: opType, + Account: &parser.AccountDescription{ + Exists: true, + }, + Amount: &parser.AmountDescription{ + Exists: true, + Sign: parser.AnyAmountSign, + Currency: currency, + }, + }, + { + Type: opType, + Account: &parser.AccountDescription{ + Exists: true, + }, + Amount: &parser.AmountDescription{ + Exists: true, + Sign: parser.AnyAmountSign, + Currency: currency, + }, + }, + } + } + return []*parser.OperationDescription{ // Send { @@ -750,3 +828,21 @@ func getMethodID(signature string) []byte { hash.Write(bytes) return hash.Sum(nil)[:4] } + +func (s ConstructionService) getGenericContractCallGasLimit( + ctx context.Context, + toAddress string, + fromAddress string, + data []byte, +) (uint64, error) { + contractAddress := ethcommon.HexToAddress(toAddress) + gasLimit, err := s.client.EstimateGas(ctx, interfaces.CallMsg{ + From: ethcommon.HexToAddress(fromAddress), + To: &contractAddress, + Data: data, + }) + if err != nil { + return 0, err + } + return gasLimit, nil +} diff --git a/server/service/service_construction_test.go b/server/service/service_construction_test.go index 07782d8..e341839 100644 --- a/server/service/service_construction_test.go +++ b/server/service/service_construction_test.go @@ -852,4 +852,46 @@ func TestPreprocessMetadata(t *testing.T) { }, }, metadataResponse) }) + + t.Run("generic contract call flow", func(t *testing.T) { + contractCallIntent := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"},"amount":{"value":"0","currency":{"symbol":"FLR","decimals":18}}},{"operation_identifier":{"index":1},"type":"CALL","account":{"address":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d"},"amount":{"value":"0","currency":{"symbol":"FLR","decimals":18}}}]` + service := ConstructionService{ + config: &Config{Mode: ModeOnline}, + client: client, + } + + currency := &types.Currency{Symbol: defaultSymbol, Decimals: defaultDecimals} + client.On( + "ContractInfo", + common.HexToAddress(defaultContractAddress), + true, + ).Return( + currency, + nil, + ).Once() + var ops []*types.Operation + assert.NoError(t, json.Unmarshal([]byte(contractCallIntent), &ops)) + requestMetadata := map[string]interface{}{ + "bridge_unwrap": false, + "method_signature": `deploy(bytes32,address,address,address,address)`, + "method_args": []string{"0x3100000000000000000000000000000000000000000000000000000000000000", "0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5", "0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6", "0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5", "0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6"}, + } + + preprocessResponse, err := service.ConstructionPreprocess( + ctx, + &types.ConstructionPreprocessRequest{ + NetworkIdentifier: networkIdentifier, + Operations: ops, + Metadata: requestMetadata, + }, + ) + assert.Nil(t, err) + optionsRaw := `{"from":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x0", "currency":{"symbol":"FLR","decimals":18}, "contract_address": "0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d", "method_signature": "deploy(bytes32,address,address,address,address)", "data": "0xb0d78b753100000000000000000000000000000000000000000000000000000000000000000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6", "method_args":["0x3100000000000000000000000000000000000000000000000000000000000000","0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5","0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6","0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5","0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6"] }` + var opt options + assert.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) + assert.Equal(t, &types.ConstructionPreprocessResponse{ + Options: forceMarshalMap(t, &opt), + }, preprocessResponse) + + }) } diff --git a/server/service/types.go b/server/service/types.go index d9949a5..6643505 100644 --- a/server/service/types.go +++ b/server/service/types.go @@ -21,6 +21,10 @@ type options struct { GasLimit *big.Int `json:"gas_limit,omitempty"` Nonce *big.Int `json:"nonce,omitempty"` Currency *types.Currency `json:"currency,omitempty"` + ContractAddress string `json:"contract_address,omitempty"` + MethodSignature string `json:"method_signature,omitempty"` + MethodArgs interface{} `json:"method_args,omitempty"` + Data []byte `json:"data,omitempty"` } type optionsWire struct { @@ -32,6 +36,10 @@ type optionsWire struct { GasLimit string `json:"gas_limit,omitempty"` Nonce string `json:"nonce,omitempty"` Currency *types.Currency `json:"currency,omitempty"` + ContractAddress string `json:"contract_address,omitempty"` + MethodSignature string `json:"method_signature,omitempty"` + MethodArgs interface{} `json:"method_args,omitempty"` + Data string `json:"data,omitempty"` } func (o *options) MarshalJSON() ([]byte, error) { @@ -40,6 +48,9 @@ func (o *options) MarshalJSON() ([]byte, error) { To: o.To, SuggestedFeeMultiplier: o.SuggestedFeeMultiplier, Currency: o.Currency, + ContractAddress: o.ContractAddress, + MethodSignature: o.MethodSignature, + MethodArgs: o.MethodArgs, } if o.Value != nil { ow.Value = hexutil.EncodeBig(o.Value) @@ -53,6 +64,9 @@ func (o *options) MarshalJSON() ([]byte, error) { if o.Nonce != nil { ow.Nonce = hexutil.EncodeBig(o.Nonce) } + if len(o.Data) > 0 { + ow.Data = hexutil.Encode(o.Data) + } return json.Marshal(ow) } @@ -66,6 +80,9 @@ func (o *options) UnmarshalJSON(data []byte) error { o.To = ow.To o.SuggestedFeeMultiplier = ow.SuggestedFeeMultiplier o.Currency = ow.Currency + o.ContractAddress = ow.ContractAddress + o.MethodSignature = ow.MethodSignature + o.MethodArgs = ow.MethodArgs if len(ow.Value) > 0 { value, err := hexutil.DecodeBig(ow.Value) @@ -99,28 +116,47 @@ func (o *options) UnmarshalJSON(data []byte) error { o.Nonce = nonce } + if len(ow.Data) > 0 { + data, err := hexutil.Decode(ow.Data) + if err != nil { + return err + } + o.Data = data + } + return nil } type metadata struct { - Nonce uint64 `json:"nonce"` - GasPrice *big.Int `json:"gas_price"` - GasLimit uint64 `json:"gas_limit"` + Nonce uint64 `json:"nonce"` + GasPrice *big.Int `json:"gas_price"` + GasLimit uint64 `json:"gas_limit"` + MethodSignature string `json:"method_signature,omitempty"` + MethodArgs interface{} `json:"method_args,omitempty"` + Data []byte `json:"data,omitempty"` } type metadataWire struct { - Nonce string `json:"nonce"` - GasPrice string `json:"gas_price"` - GasLimit string `json:"gas_limit"` + Nonce string `json:"nonce"` + GasPrice string `json:"gas_price"` + GasLimit string `json:"gas_limit"` + MethodSignature string `json:"method_signature,omitempty"` + MethodArgs interface{} `json:"method_args,omitempty"` + Data string `json:"data,omitempty"` } func (m *metadata) MarshalJSON() ([]byte, error) { mw := &metadataWire{ - Nonce: hexutil.Uint64(m.Nonce).String(), - GasPrice: hexutil.EncodeBig(m.GasPrice), - GasLimit: hexutil.Uint64(m.GasLimit).String(), + Nonce: hexutil.Uint64(m.Nonce).String(), + GasPrice: hexutil.EncodeBig(m.GasPrice), + GasLimit: hexutil.Uint64(m.GasLimit).String(), + MethodSignature: m.MethodSignature, + MethodArgs: m.MethodArgs, } + if len(m.Data) > 0 { + mw.Data = hexutil.Encode(m.Data) + } return json.Marshal(mw) } @@ -130,6 +166,9 @@ func (m *metadata) UnmarshalJSON(data []byte) error { return err } + m.MethodSignature = mw.MethodSignature + m.MethodArgs = mw.MethodArgs + gasPrice, err := hexutil.DecodeBig(mw.GasPrice) if err != nil { return err @@ -148,6 +187,14 @@ func (m *metadata) UnmarshalJSON(data []byte) error { } m.Nonce = nonce + if len(mw.Data) > 0 { + mwData, err := hexutil.Decode(mw.Data) + if err != nil { + return err + } + m.Data = mwData + } + return nil } From 1cfde63e86781d71a24f5570420d413c3136ca5b Mon Sep 17 00:00:00 2001 From: "jiangchuan.he" Date: Sat, 23 Sep 2023 16:05:54 -0700 Subject: [PATCH 2/4] add unit test --- server/service/service_construction.go | 20 +++++--- server/service/service_construction_test.go | 54 +++++++++++++++++++++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/server/service/service_construction.go b/server/service/service_construction.go index d83e53c..b972d55 100644 --- a/server/service/service_construction.go +++ b/server/service/service_construction.go @@ -97,9 +97,11 @@ func (s ConstructionService) ConstructionMetadata( var gasLimit uint64 if input.GasLimit == nil { if input.Currency == nil || utils.Equal(input.Currency, mapper.FlareCurrency) { - gasLimit, err = s.getNativeTransferGasLimit(ctx, input.To, input.From, input.Value) - } else if len(input.ContractAddress) > 0 { - gasLimit, err = s.getGenericContractCallGasLimit(ctx, input.ContractAddress, input.From, input.Data) + if utils.Equal(input.Currency, mapper.FlareCurrency) && len(input.ContractAddress) > 0 { + gasLimit, err = s.getGenericContractCallGasLimit(ctx, input.ContractAddress, input.From, input.Data) + } else { + gasLimit, err = s.getNativeTransferGasLimit(ctx, input.To, input.From, input.Value) + } } else { gasLimit, err = s.getErc20TransferGasLimit(ctx, input.To, input.From, input.Value, input.Currency) } @@ -443,11 +445,13 @@ func (s ConstructionService) ConstructionPayloads( var transferData []byte var sendToAddress ethcommon.Address if utils.Equal(fromCurrency, mapper.FlareCurrency) { - transferData = []byte{} - sendToAddress = ethcommon.HexToAddress(checkTo) - } else if len(metadata.MethodSignature) > 0 { - transferData = metadata.Data - sendToAddress = ethcommon.HexToAddress(checkTo) + if len(metadata.MethodSignature) == 0 { + transferData = []byte{} + sendToAddress = ethcommon.HexToAddress(checkTo) + } else { + transferData = metadata.Data + sendToAddress = ethcommon.HexToAddress(checkTo) + } } else { contract, ok := fromCurrency.Metadata[mapper.ContractAddressMetadata].(string) if !ok { diff --git a/server/service/service_construction_test.go b/server/service/service_construction_test.go index e341839..ea14ed7 100644 --- a/server/service/service_construction_test.go +++ b/server/service/service_construction_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "encoding/json" + "github.com/ethereum/go-ethereum/common/hexutil" "math/big" "testing" @@ -893,5 +894,58 @@ func TestPreprocessMetadata(t *testing.T) { Options: forceMarshalMap(t, &opt), }, preprocessResponse) + data, _ := hexutil.Decode("0xb0d78b753100000000000000000000000000000000000000000000000000000000000000000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6000000000000000000000000323e3ab04a3795ad79cc92378fcdb0a0aec51ba500000000000000000000000014e37c2e9cd255404bd35b4542fd9ccaa070aed6") + metadata := &metadata{ + GasPrice: big.NewInt(1000000000), + GasLimit: 21_001, + Nonce: 0, + Data: data, + MethodSignature: "deploy(bytes32,address,address,address,address)", + MethodArgs: []string{"0x3100000000000000000000000000000000000000000000000000000000000000", "0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5", "0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6", "0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5", "0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6"}, + } + + client.On( + "SuggestGasPrice", + ctx, + ).Return( + big.NewInt(1000000000), + nil, + ).Once() + to := common.HexToAddress("0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d") + client.On( + "EstimateGas", + ctx, + interfaces.CallMsg{ + From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), + To: &to, + Data: data, + }, + ).Return( + uint64(21001), + nil, + ).Once() + client.On( + "NonceAt", + ctx, + common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), + (*big.Int)(nil), + ).Return( + uint64(0), + nil, + ).Once() + metadataResponse, err := service.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: forceMarshalMap(t, &opt), + }) + assert.Nil(t, err) + assert.Equal(t, &types.ConstructionMetadataResponse{ + Metadata: forceMarshalMap(t, metadata), + SuggestedFee: []*types.Amount{ + { + Value: "21001000000000", + Currency: mapper.FlareCurrency, + }, + }, + }, metadataResponse) }) } From 099215835cecdfba20d497a7b2bba07dfb5a6d7e Mon Sep 17 00:00:00 2001 From: "jiangchuan.he" Date: Mon, 25 Sep 2023 14:59:58 -0700 Subject: [PATCH 3/4] support docker build on M1 --- server/Dockerfile.arm64 | 115 ++++++++++++++++++++++++++++++++++++++++ server/Makefile | 8 +++ 2 files changed, 123 insertions(+) create mode 100644 server/Dockerfile.arm64 diff --git a/server/Dockerfile.arm64 b/server/Dockerfile.arm64 new file mode 100644 index 0000000..e8f236d --- /dev/null +++ b/server/Dockerfile.arm64 @@ -0,0 +1,115 @@ +# ------------------------------------------------------------------------------ +# Build go-flare +# ------------------------------------------------------------------------------ +FROM arm64v8/golang:1.18.5 AS flare + +WORKDIR /app + +ARG GO_FLARE_VERSION=v1.7.1806 +ARG GO_FLARE_REPO=https://github.com/flare-foundation/go-flare + +RUN git clone --branch "$GO_FLARE_VERSION" "${GO_FLARE_REPO}" . + +RUN apt update && \ + apt -y install rsync +RUN cd avalanchego && \ + ./scripts/build.sh + +# ------------------------------------------------------------------------------ +# Build flare-rosetta +# ------------------------------------------------------------------------------ +FROM arm64v8/golang:1.18.5 AS rosetta + +ARG ROSETTA_SRC=https://github.com/flare-foundation/flare-rosetta/archive/refs/heads/main.zip +ARG ROSETTA_SRC_ZIP_SUBFOLDER=flare-rosetta-main + +WORKDIR /tmp + +RUN apt update -y && apt install unzip -y +ADD ${ROSETTA_SRC} /tmp/rosetta-source +RUN if [ ! -d "/tmp/rosetta-source" ]; then \ + unzip "/tmp/rosetta-source" && \ + mv /tmp/${ROSETTA_SRC_ZIP_SUBFOLDER} /app; \ + else \ + mv /tmp/rosetta-source /app; \ + fi + +WORKDIR /app/server + +RUN \ + GO_VERSION=$(go version | awk {'print $3'}) \ + GIT_COMMIT=main \ + go mod download && \ + make setup && \ + make build + +# ------------------------------------------------------------------------------ +# Target container for running the rosetta server +# ------------------------------------------------------------------------------ +FROM arm64v8/ubuntu:20.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# Install dependencies +RUN apt-get update -y && \ + apt -y upgrade && \ + apt -y install curl jq netcat dnsutils moreutils + +WORKDIR /app + +ENV HTTP_HOST=0.0.0.0 \ + HTTP_PORT=9650 \ + STAKING_PORT=9651 \ + DB_DIR=/data \ + DB_TYPE=leveldb \ + LOG_DIR=/app/flare/logs \ + MODE=online \ + START_ROSETTA_SERVER_AFTER_BOOTSTRAP=false \ + AUTOCONFIGURE_BOOTSTRAP_ENDPOINT="" \ + AUTOCONFIGURE_BOOTSTRAP_ENDPOINT_RETRY=0 \ + STAKING_ENABLED="true" \ + YES_I_REALLY_KNOW_WHAT_I_AM_DOING="false" + + + +# Intentionally empty, so env vars are evalueted at runtime (script entrypoint_flare.sh) instead of build time +# Default: /app/conf/$NETWORK_ID +ENV CHAIN_CONFIG_DIR="" +# Default: warn +ENV LOG_LEVEL="" + +# ROSETTA ENV VARS +ENV ROSETTA_FLARE_ENDPOINT=http://127.0.0.1:9650 +# Intentionally empty, so env vars are evalueted at runtime (script entrypoint_rosetta.sh) instead of build time +# Default: /app/conf/$NETWORK_ID/server-config.json +ENV ROSETTA_CONFIG_PATH="" + +# Install flare +COPY --from=flare /app/avalanchego/build /app/flare/build + +# Install rosetta server +COPY --from=rosetta /app/server /app/rosetta-server + +# Install node and rosetta configs +COPY --from=rosetta /app/server/rosetta-cli-conf /app/conf + +# Install entrypoints +COPY --from=rosetta /app/server/docker/entrypoint_flare.sh /app/entrypoint_flare.sh +COPY --from=rosetta /app/server/docker/entrypoint_rosetta.sh /app/entrypoint_rosetta.sh +COPY --from=rosetta /app/server/docker/entrypoint_main.sh /app/entrypoint_main.sh +COPY --from=rosetta /app/server/docker/healthcheck.sh /app/healthcheck.sh + +# Copy testnet configs +COPY --from=flare /app/avalanchego/staking/local /app/flare/staking/local +COPY --from=flare /app/config/localflare /app/flare/config/localflare + +RUN chmod +x entrypoint_flare.sh entrypoint_rosetta.sh entrypoint_main.sh + +EXPOSE ${HTTP_PORT} +EXPOSE ${STAKING_PORT} +EXPOSE 8080 + +HEALTHCHECK --interval=5s --timeout=5s --retries=5 --start-period=15s CMD bash /app/healthcheck.sh + +ENTRYPOINT ["/bin/bash"] +CMD ["/app/entrypoint_main.sh"] diff --git a/server/Makefile b/server/Makefile index a489843..00e9d7b 100644 --- a/server/Makefile +++ b/server/Makefile @@ -31,6 +31,14 @@ docker-build: -f Dockerfile \ . +docker-build-arm64: + docker build \ + --build-arg AVALANCHE_VERSION=${AVALANCHE_VERSION} \ + --build-arg ROSETTA_VERSION=${GIT_COMMIT} \ + -t ${DOCKER_TAG} \ + -f Dockerfile.arm64 \ + . + # Start the Testnet in ONLINE mode run-testnet: docker run \ From d7f6ef0f132453368138a38edb1cdbb6c4dd1b1b Mon Sep 17 00:00:00 2001 From: "jiangchuan.he" Date: Wed, 11 Oct 2023 14:54:39 -0700 Subject: [PATCH 4/4] fix --- server/service/contract_call_data.go | 3 + server/service/service_construction.go | 4 + server/service/service_construction_test.go | 97 ++++++++++++++++++++- 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/server/service/contract_call_data.go b/server/service/contract_call_data.go index 100a540..891d4ff 100644 --- a/server/service/contract_call_data.go +++ b/server/service/contract_call_data.go @@ -155,7 +155,10 @@ func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []str } argData = value } + default: + return nil, errors.New(fmt.Sprintf("invalid argument type:%s", v)) } + argumentsData = append(argumentsData, argData) } diff --git a/server/service/service_construction.go b/server/service/service_construction.go index b972d55..8b0c6f2 100644 --- a/server/service/service_construction.go +++ b/server/service/service_construction.go @@ -525,6 +525,10 @@ func (s ConstructionService) ConstructionPreprocess( return nil, wrapError(errInvalidInput, "unclear intent") } + if len(matches) != 2 { + return nil, wrapError(errInvalidInput, "Must have only two operations") + } + fromOp, _ := matches[0].First() fromAddress := fromOp.Account.Address toOp, amount := matches[1].First() diff --git a/server/service/service_construction_test.go b/server/service/service_construction_test.go index ea14ed7..2844700 100644 --- a/server/service/service_construction_test.go +++ b/server/service/service_construction_test.go @@ -4,7 +4,6 @@ import ( "context" "encoding/hex" "encoding/json" - "github.com/ethereum/go-ethereum/common/hexutil" "math/big" "testing" @@ -14,6 +13,7 @@ import ( "github.com/coinbase/rosetta-sdk-go/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/assert" ) @@ -873,7 +873,6 @@ func TestPreprocessMetadata(t *testing.T) { var ops []*types.Operation assert.NoError(t, json.Unmarshal([]byte(contractCallIntent), &ops)) requestMetadata := map[string]interface{}{ - "bridge_unwrap": false, "method_signature": `deploy(bytes32,address,address,address,address)`, "method_args": []string{"0x3100000000000000000000000000000000000000000000000000000000000000", "0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5", "0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6", "0x323e3ab04a3795ad79cc92378fcdb0a0aec51ba5", "0x14e37c2e9cd255404bd35b4542fd9ccaa070aed6"}, } @@ -948,4 +947,98 @@ func TestPreprocessMetadata(t *testing.T) { }, }, metadataResponse) }) + + t.Run("generic contract call flow with encoded args", func(t *testing.T) { + contractCallIntent := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"},"amount":{"value":"0","currency":{"symbol":"FLR","decimals":18}}},{"operation_identifier":{"index":1},"type":"CALL","account":{"address":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d"},"amount":{"value":"0","currency":{"symbol":"FLR","decimals":18}}}]` + service := ConstructionService{ + config: &Config{Mode: ModeOnline}, + client: client, + } + + currency := &types.Currency{Symbol: defaultSymbol, Decimals: defaultDecimals} + client.On( + "ContractInfo", + common.HexToAddress(defaultContractAddress), + true, + ).Return( + currency, + nil, + ).Once() + var ops []*types.Operation + assert.NoError(t, json.Unmarshal([]byte(contractCallIntent), &ops)) + requestMetadata := map[string]interface{}{ + "method_signature": `cloneRandomDepositWallets(bytes32[])`, + "method_args": "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001", + } + + preprocessResponse, err := service.ConstructionPreprocess( + ctx, + &types.ConstructionPreprocessRequest{ + NetworkIdentifier: networkIdentifier, + Operations: ops, + Metadata: requestMetadata, + }, + ) + assert.Nil(t, err) + optionsRaw := `{"from":"0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x0", "currency":{"symbol":"FLR","decimals":18}, "contract_address": "0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d", "method_signature": "cloneRandomDepositWallets(bytes32[])", "data": "0x84dc7baa000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001", "method_args":"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001" }` + var opt options + assert.NoError(t, json.Unmarshal([]byte(optionsRaw), &opt)) + assert.Equal(t, &types.ConstructionPreprocessResponse{ + Options: forceMarshalMap(t, &opt), + }, preprocessResponse) + + data, _ := hexutil.Decode("0x84dc7baa000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001") + metadata := &metadata{ + GasPrice: big.NewInt(1000000000), + GasLimit: 21_001, + Nonce: 0, + Data: data, + MethodSignature: "cloneRandomDepositWallets(bytes32[])", + MethodArgs: "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001", + } + + client.On( + "SuggestGasPrice", + ctx, + ).Return( + big.NewInt(1000000000), + nil, + ).Once() + to := common.HexToAddress("0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d") + client.On( + "EstimateGas", + ctx, + interfaces.CallMsg{ + From: common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), + To: &to, + Data: data, + }, + ).Return( + uint64(21001), + nil, + ).Once() + client.On( + "NonceAt", + ctx, + common.HexToAddress("0xe3a5B4d7f79d64088C8d4ef153A7DDe2B2d47309"), + (*big.Int)(nil), + ).Return( + uint64(0), + nil, + ).Once() + metadataResponse, err := service.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ + NetworkIdentifier: networkIdentifier, + Options: forceMarshalMap(t, &opt), + }) + assert.Nil(t, err) + assert.Equal(t, &types.ConstructionMetadataResponse{ + Metadata: forceMarshalMap(t, metadata), + SuggestedFee: []*types.Amount{ + { + Value: "21001000000000", + Currency: mapper.FlareCurrency, + }, + }, + }, metadataResponse) + }) }