Skip to content

Commit

Permalink
Add transaction data decoding support (#124)
Browse files Browse the repository at this point in the history
### TL;DR
Added transaction data decoding functionality for transactions when a function signature is provided.

### What changed?
- Introduced new ABI parsing utilities to handle function signatures
- Added support for decoding transaction input data using parsed ABI
- Created a new `DecodedTransaction` type that includes the original transaction data along with decoded parameters
- Enhanced transaction handlers to decode transaction data when a function signature is provided

### How to test?
1. Make a request to `/{chainId}/transactions/{contractAddress}/{signature}`
2. Verify that the response includes decoded transaction data with:
   - Function name
   - Function signature
   - Decoded input parameters as key-value pairs
3. Test with various function signatures, including those with complex parameter types (tuples, arrays)
4. Verify that regular transaction endpoints continue to work without decoded data

### Why make this change?
To improve transaction data readability by automatically decoding the raw input data into human-readable format when the function signature is known. This makes it easier for users to understand the actual parameters passed in contract interactions without having to manually decode the transaction data.
  • Loading branch information
iuwqyir authored Dec 6, 2024
2 parents cdb050b + f996013 commit cdef621
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 cdef621

Please sign in to comment.