Skip to content

Commit

Permalink
decode transaction inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
iuwqyir committed Dec 6, 2024
1 parent 69139c0 commit d3ab46d
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 5 deletions.
167 changes: 167 additions & 0 deletions internal/common/abi.go
Original file line number Diff line number Diff line change
@@ -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
}
42 changes: 42 additions & 0 deletions internal/common/transaction.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
}}
}
49 changes: 49 additions & 0 deletions internal/common/transaction_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
24 changes: 19 additions & 5 deletions internal/handlers/transactions_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

Expand Down

0 comments on commit d3ab46d

Please sign in to comment.