From d3ab46da597afeaffc0aa3b19fb7efa93cfb62ab Mon Sep 17 00:00:00 2001 From: iuwqyir Date: Fri, 6 Dec 2024 01:13:54 +0200 Subject: [PATCH] decode transaction inputs --- internal/common/abi.go | 167 +++++++++++++++++++++ internal/common/transaction.go | 42 ++++++ internal/common/transaction_test.go | 49 ++++++ internal/handlers/transactions_handlers.go | 24 ++- 4 files changed, 277 insertions(+), 5 deletions(-) create mode 100644 internal/common/abi.go create mode 100644 internal/common/transaction_test.go diff --git a/internal/common/abi.go b/internal/common/abi.go new file mode 100644 index 0000000..909423e --- /dev/null +++ b/internal/common/abi.go @@ -0,0 +1,167 @@ +package common + +import ( + "fmt" + "regexp" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" +) + +func ConstructFunctionABI(signature string) (*abi.Method, error) { + regex := regexp.MustCompile(`^(\w+)\((.*)\)$`) + matches := regex.FindStringSubmatch(strings.TrimSpace(signature)) + if len(matches) != 3 { + return nil, fmt.Errorf("invalid event signature format") + } + + functionName := matches[1] + params := matches[2] + + inputs, err := parseParamsToAbiArguments(params) + if err != nil { + return nil, fmt.Errorf("failed to parse params to abi arguments '%s': %v", params, err) + } + + function := abi.NewMethod(functionName, functionName, abi.Function, "", false, false, inputs, nil) + + return &function, nil +} + +func parseParamsToAbiArguments(params string) (abi.Arguments, error) { + paramList := splitParams(strings.TrimSpace(params)) + var inputs abi.Arguments + for idx, param := range paramList { + arg, err := parseParamToAbiArgument(param, fmt.Sprintf("%d", idx)) + if err != nil { + return nil, fmt.Errorf("failed to parse param to arg '%s': %v", param, err) + } + inputs = append(inputs, *arg) + } + return inputs, nil +} + +/** + * Splits a string of parameters into a list of parameters + */ +func splitParams(params string) []string { + var result []string + depth := 0 + current := "" + for _, r := range params { + switch r { + case ',': + if depth == 0 { + result = append(result, strings.TrimSpace(current)) + current = "" + continue + } + case '(': + depth++ + case ')': + depth-- + } + current += string(r) + } + if strings.TrimSpace(current) != "" { + result = append(result, strings.TrimSpace(current)) + } + return result +} + +func parseParamToAbiArgument(param string, fallbackName string) (*abi.Argument, error) { + argName, paramType, err := getArgNameAndType(param, fallbackName) + if err != nil { + return nil, fmt.Errorf("failed to get arg name and type '%s': %v", param, err) + } + if isTuple(paramType) { + argType, err := marshalTupleParamToArgumentType(paramType) + if err != nil { + return nil, fmt.Errorf("failed to marshal tuple: %v", err) + } + return &abi.Argument{ + Name: argName, + Type: argType, + }, nil + } else { + argType, err := abi.NewType(paramType, paramType, nil) + if err != nil { + return nil, fmt.Errorf("failed to parse type '%s': %v", paramType, err) + } + return &abi.Argument{ + Name: argName, + Type: argType, + }, nil + } +} + +func getArgNameAndType(param string, fallbackName string) (name string, paramType string, err error) { + if isTuple(param) { + lastParenIndex := strings.LastIndex(param, ")") + if lastParenIndex == -1 { + return "", "", fmt.Errorf("invalid tuple format") + } + if len(param)-1 == lastParenIndex { + return fallbackName, param, nil + } + paramsEndIdx := lastParenIndex + 1 + if strings.HasPrefix(param[paramsEndIdx:], "[]") { + paramsEndIdx = lastParenIndex + 3 + } + return strings.TrimSpace(param[paramsEndIdx:]), param[:paramsEndIdx], nil + } else { + tokens := strings.Fields(param) + if len(tokens) == 1 { + return fallbackName, strings.TrimSpace(tokens[0]), nil + } + return strings.TrimSpace(tokens[len(tokens)-1]), strings.Join(tokens[:len(tokens)-1], " "), nil + } +} + +func isTuple(param string) bool { + return strings.HasPrefix(param, "(") +} + +func marshalTupleParamToArgumentType(paramType string) (abi.Type, error) { + typ := "tuple" + isSlice := strings.HasSuffix(paramType, "[]") + strippedParamType := strings.TrimPrefix(paramType, "(") + if isSlice { + strippedParamType = strings.TrimSuffix(strippedParamType, "[]") + typ = "tuple[]" + } + strippedParamType = strings.TrimSuffix(strippedParamType, ")") + components, err := marshalParamArguments(strippedParamType) + if err != nil { + return abi.Type{}, fmt.Errorf("failed to marshal tuple: %v", err) + } + return abi.NewType(typ, typ, components) +} + +func marshalParamArguments(param string) ([]abi.ArgumentMarshaling, error) { + paramList := splitParams(param) + components := []abi.ArgumentMarshaling{} + for idx, param := range paramList { + argName, paramType, err := getArgNameAndType(param, fmt.Sprintf("field%d", idx)) + if err != nil { + return nil, fmt.Errorf("failed to get arg name and type '%s': %v", param, err) + } + if isTuple(paramType) { + subComponents, err := marshalParamArguments(paramType[1 : len(paramType)-1]) + if err != nil { + return nil, fmt.Errorf("failed to marshal tuple: %v", err) + } + components = append(components, abi.ArgumentMarshaling{ + Type: "tuple", + Name: argName, + Components: subComponents, + }) + } else { + components = append(components, abi.ArgumentMarshaling{ + Type: paramType, + Name: argName, + }) + } + } + return components, nil +} diff --git a/internal/common/transaction.go b/internal/common/transaction.go index 3c4afd9..68e0ea6 100644 --- a/internal/common/transaction.go +++ b/internal/common/transaction.go @@ -1,7 +1,12 @@ package common import ( + "encoding/hex" "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/rs/zerolog/log" ) type Transaction struct { @@ -35,3 +40,40 @@ type Transaction struct { LogsBloom *string `json:"logs_bloom"` Status *uint64 `json:"status"` } + +type DecodedTransactionData struct { + Name string `json:"name"` + Signature string `json:"signature"` + Inputs map[string]interface{} `json:"inputs"` +} + +type DecodedTransaction struct { + Transaction + Decoded DecodedTransactionData `json:"decodedData"` +} + +func (t *Transaction) Decode(functionABI *abi.Method) *DecodedTransaction { + decodedData, err := hex.DecodeString(strings.TrimPrefix(t.Data, "0x")) + if err != nil { + log.Debug().Msgf("failed to decode transaction data: %v", err) + return &DecodedTransaction{Transaction: *t} + } + + if len(decodedData) < 4 { + log.Debug().Msg("Data too short to contain function selector") + return &DecodedTransaction{Transaction: *t} + } + inputData := decodedData[4:] + decodedInputs := make(map[string]interface{}) + err = functionABI.Inputs.UnpackIntoMap(decodedInputs, inputData) + if err != nil { + log.Warn().Msgf("failed to decode function parameters: %v, signature: %s", err, functionABI.Sig) + } + return &DecodedTransaction{ + Transaction: *t, + Decoded: DecodedTransactionData{ + Name: functionABI.RawName, + Signature: functionABI.Sig, + Inputs: decodedInputs, + }} +} diff --git a/internal/common/transaction_test.go b/internal/common/transaction_test.go new file mode 100644 index 0000000..a4e38b3 --- /dev/null +++ b/internal/common/transaction_test.go @@ -0,0 +1,49 @@ +package common + +import ( + "math/big" + "testing" + + gethCommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" +) + +func TestDecodeTransaction(t *testing.T) { + transaction := Transaction{ + Data: "0x095ea7b3000000000000000000000000971add32ea87f10bd192671630be3be8a11b862300000000000000000000000000000000000000000000010df58ac64e49b91ea0", + } + + abi, err := ConstructFunctionABI("approve(address _spender, uint256 _value)") + assert.NoError(t, err) + decodedTransaction := transaction.Decode(abi) + + assert.Equal(t, "approve", decodedTransaction.Decoded.Name) + assert.Equal(t, gethCommon.HexToAddress("0x971add32Ea87f10bD192671630be3BE8A11b8623"), decodedTransaction.Decoded.Inputs["_spender"]) + expectedValue := big.NewInt(0) + expectedValue.SetString("4979867327953494417056", 10) + assert.Equal(t, expectedValue, decodedTransaction.Decoded.Inputs["_value"]) + + transaction2 := Transaction{ + Data: "0x27c777a9000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000672c0c60302aafae8a36ffd8c12b32f1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c680000000000000000000000000000734d56da60852a03e2aafae8a36ffd8c12b32f10000000000000000000000000000000000000000000000000000000000000000", + } + abi2, err := ConstructFunctionABI("allocatedWithdrawal((bytes,uint256,uint256,uint256,uint256,address) _withdrawal)") + assert.NoError(t, err) + decodedTransaction2 := transaction2.Decode(abi2) + + assert.Equal(t, "allocatedWithdrawal", decodedTransaction2.Decoded.Name) + withdrawal := decodedTransaction2.Decoded.Inputs["_withdrawal"].(struct { + Field0 []uint8 `json:"field0"` + Field1 *big.Int `json:"field1"` + Field2 *big.Int `json:"field2"` + Field3 *big.Int `json:"field3"` + Field4 *big.Int `json:"field4"` + Field5 gethCommon.Address `json:"field5"` + }) + + assert.Equal(t, []uint8{}, withdrawal.Field0) + assert.Equal(t, "123", withdrawal.Field1.String()) + assert.Equal(t, "1730940000", withdrawal.Field2.String()) + assert.Equal(t, "21786436819914608908212656341824591317420268878283544900672692017070052737024", withdrawal.Field3.String()) + assert.Equal(t, "1000000000000000", withdrawal.Field4.String()) + assert.Equal(t, "0x0734d56DA60852A03e2Aafae8a36FFd8c12B32f1", withdrawal.Field5.Hex()) +} diff --git a/internal/handlers/transactions_handlers.go b/internal/handlers/transactions_handlers.go index 2198c3c..7120383 100644 --- a/internal/handlers/transactions_handlers.go +++ b/internal/handlers/transactions_handlers.go @@ -3,6 +3,7 @@ package handlers import ( "net/http" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/crypto" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" @@ -56,7 +57,7 @@ type TransactionModel struct { // @Failure 500 {object} api.Error // @Router /{chainId}/transactions [get] func GetTransactions(c *gin.Context) { - handleTransactionsRequest(c, "", "") + handleTransactionsRequest(c, "", "", nil) } // @Summary Get transactions by contract @@ -81,7 +82,7 @@ func GetTransactions(c *gin.Context) { // @Router /{chainId}/transactions/{to} [get] func GetTransactionsByContract(c *gin.Context) { to := c.Param("to") - handleTransactionsRequest(c, to, "") + handleTransactionsRequest(c, to, "", nil) } // @Summary Get transactions by contract and signature @@ -109,10 +110,14 @@ func GetTransactionsByContractAndSignature(c *gin.Context) { to := c.Param("to") signature := c.Param("signature") strippedSignature := common.StripPayload(signature) - handleTransactionsRequest(c, to, strippedSignature) + functionABI, err := common.ConstructFunctionABI(signature) + if err != nil { + log.Debug().Err(err).Msgf("Unable to construct function ABI for %s", signature) + } + handleTransactionsRequest(c, to, strippedSignature, functionABI) } -func handleTransactionsRequest(c *gin.Context, contractAddress, signature string) { +func handleTransactionsRequest(c *gin.Context, contractAddress, signature string, functionABI *abi.Method) { chainId, err := api.GetChainId(c) if err != nil { api.BadRequestErrorHandler(c, err) @@ -187,7 +192,16 @@ func handleTransactionsRequest(c *gin.Context, contractAddress, signature string api.InternalErrorHandler(c) return } - queryResult.Data = transactionsResult.Data + if functionABI != nil { + decodedTransactions := []*common.DecodedTransaction{} + for _, transaction := range transactionsResult.Data { + decodedTransaction := transaction.Decode(functionABI) + decodedTransactions = append(decodedTransactions, decodedTransaction) + } + queryResult.Data = decodedTransactions + } else { + queryResult.Data = transactionsResult.Data + } queryResult.Meta.TotalItems = len(transactionsResult.Data) }