Skip to content

Commit cdef621

Browse files
authored
Add transaction data decoding support (#124)
### 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.
2 parents cdb050b + f996013 commit cdef621

File tree

4 files changed

+277
-5
lines changed

4 files changed

+277
-5
lines changed

internal/common/abi.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package common
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
8+
"github.com/ethereum/go-ethereum/accounts/abi"
9+
)
10+
11+
func ConstructFunctionABI(signature string) (*abi.Method, error) {
12+
regex := regexp.MustCompile(`^(\w+)\((.*)\)$`)
13+
matches := regex.FindStringSubmatch(strings.TrimSpace(signature))
14+
if len(matches) != 3 {
15+
return nil, fmt.Errorf("invalid event signature format")
16+
}
17+
18+
functionName := matches[1]
19+
params := matches[2]
20+
21+
inputs, err := parseParamsToAbiArguments(params)
22+
if err != nil {
23+
return nil, fmt.Errorf("failed to parse params to abi arguments '%s': %v", params, err)
24+
}
25+
26+
function := abi.NewMethod(functionName, functionName, abi.Function, "", false, false, inputs, nil)
27+
28+
return &function, nil
29+
}
30+
31+
func parseParamsToAbiArguments(params string) (abi.Arguments, error) {
32+
paramList := splitParams(strings.TrimSpace(params))
33+
var inputs abi.Arguments
34+
for idx, param := range paramList {
35+
arg, err := parseParamToAbiArgument(param, fmt.Sprintf("%d", idx))
36+
if err != nil {
37+
return nil, fmt.Errorf("failed to parse param to arg '%s': %v", param, err)
38+
}
39+
inputs = append(inputs, *arg)
40+
}
41+
return inputs, nil
42+
}
43+
44+
/**
45+
* Splits a string of parameters into a list of parameters
46+
*/
47+
func splitParams(params string) []string {
48+
var result []string
49+
depth := 0
50+
current := ""
51+
for _, r := range params {
52+
switch r {
53+
case ',':
54+
if depth == 0 {
55+
result = append(result, strings.TrimSpace(current))
56+
current = ""
57+
continue
58+
}
59+
case '(':
60+
depth++
61+
case ')':
62+
depth--
63+
}
64+
current += string(r)
65+
}
66+
if strings.TrimSpace(current) != "" {
67+
result = append(result, strings.TrimSpace(current))
68+
}
69+
return result
70+
}
71+
72+
func parseParamToAbiArgument(param string, fallbackName string) (*abi.Argument, error) {
73+
argName, paramType, err := getArgNameAndType(param, fallbackName)
74+
if err != nil {
75+
return nil, fmt.Errorf("failed to get arg name and type '%s': %v", param, err)
76+
}
77+
if isTuple(paramType) {
78+
argType, err := marshalTupleParamToArgumentType(paramType)
79+
if err != nil {
80+
return nil, fmt.Errorf("failed to marshal tuple: %v", err)
81+
}
82+
return &abi.Argument{
83+
Name: argName,
84+
Type: argType,
85+
}, nil
86+
} else {
87+
argType, err := abi.NewType(paramType, paramType, nil)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to parse type '%s': %v", paramType, err)
90+
}
91+
return &abi.Argument{
92+
Name: argName,
93+
Type: argType,
94+
}, nil
95+
}
96+
}
97+
98+
func getArgNameAndType(param string, fallbackName string) (name string, paramType string, err error) {
99+
if isTuple(param) {
100+
lastParenIndex := strings.LastIndex(param, ")")
101+
if lastParenIndex == -1 {
102+
return "", "", fmt.Errorf("invalid tuple format")
103+
}
104+
if len(param)-1 == lastParenIndex {
105+
return fallbackName, param, nil
106+
}
107+
paramsEndIdx := lastParenIndex + 1
108+
if strings.HasPrefix(param[paramsEndIdx:], "[]") {
109+
paramsEndIdx = lastParenIndex + 3
110+
}
111+
return strings.TrimSpace(param[paramsEndIdx:]), param[:paramsEndIdx], nil
112+
} else {
113+
tokens := strings.Fields(param)
114+
if len(tokens) == 1 {
115+
return fallbackName, strings.TrimSpace(tokens[0]), nil
116+
}
117+
return strings.TrimSpace(tokens[len(tokens)-1]), strings.Join(tokens[:len(tokens)-1], " "), nil
118+
}
119+
}
120+
121+
func isTuple(param string) bool {
122+
return strings.HasPrefix(param, "(")
123+
}
124+
125+
func marshalTupleParamToArgumentType(paramType string) (abi.Type, error) {
126+
typ := "tuple"
127+
isSlice := strings.HasSuffix(paramType, "[]")
128+
strippedParamType := strings.TrimPrefix(paramType, "(")
129+
if isSlice {
130+
strippedParamType = strings.TrimSuffix(strippedParamType, "[]")
131+
typ = "tuple[]"
132+
}
133+
strippedParamType = strings.TrimSuffix(strippedParamType, ")")
134+
components, err := marshalParamArguments(strippedParamType)
135+
if err != nil {
136+
return abi.Type{}, fmt.Errorf("failed to marshal tuple: %v", err)
137+
}
138+
return abi.NewType(typ, typ, components)
139+
}
140+
141+
func marshalParamArguments(param string) ([]abi.ArgumentMarshaling, error) {
142+
paramList := splitParams(param)
143+
components := []abi.ArgumentMarshaling{}
144+
for idx, param := range paramList {
145+
argName, paramType, err := getArgNameAndType(param, fmt.Sprintf("field%d", idx))
146+
if err != nil {
147+
return nil, fmt.Errorf("failed to get arg name and type '%s': %v", param, err)
148+
}
149+
if isTuple(paramType) {
150+
subComponents, err := marshalParamArguments(paramType[1 : len(paramType)-1])
151+
if err != nil {
152+
return nil, fmt.Errorf("failed to marshal tuple: %v", err)
153+
}
154+
components = append(components, abi.ArgumentMarshaling{
155+
Type: "tuple",
156+
Name: argName,
157+
Components: subComponents,
158+
})
159+
} else {
160+
components = append(components, abi.ArgumentMarshaling{
161+
Type: paramType,
162+
Name: argName,
163+
})
164+
}
165+
}
166+
return components, nil
167+
}

internal/common/transaction.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package common
22

33
import (
4+
"encoding/hex"
45
"math/big"
6+
"strings"
7+
8+
"github.com/ethereum/go-ethereum/accounts/abi"
9+
"github.com/rs/zerolog/log"
510
)
611

712
type Transaction struct {
@@ -35,3 +40,40 @@ type Transaction struct {
3540
LogsBloom *string `json:"logs_bloom"`
3641
Status *uint64 `json:"status"`
3742
}
43+
44+
type DecodedTransactionData struct {
45+
Name string `json:"name"`
46+
Signature string `json:"signature"`
47+
Inputs map[string]interface{} `json:"inputs"`
48+
}
49+
50+
type DecodedTransaction struct {
51+
Transaction
52+
Decoded DecodedTransactionData `json:"decodedData"`
53+
}
54+
55+
func (t *Transaction) Decode(functionABI *abi.Method) *DecodedTransaction {
56+
decodedData, err := hex.DecodeString(strings.TrimPrefix(t.Data, "0x"))
57+
if err != nil {
58+
log.Debug().Msgf("failed to decode transaction data: %v", err)
59+
return &DecodedTransaction{Transaction: *t}
60+
}
61+
62+
if len(decodedData) < 4 {
63+
log.Debug().Msg("Data too short to contain function selector")
64+
return &DecodedTransaction{Transaction: *t}
65+
}
66+
inputData := decodedData[4:]
67+
decodedInputs := make(map[string]interface{})
68+
err = functionABI.Inputs.UnpackIntoMap(decodedInputs, inputData)
69+
if err != nil {
70+
log.Warn().Msgf("failed to decode function parameters: %v, signature: %s", err, functionABI.Sig)
71+
}
72+
return &DecodedTransaction{
73+
Transaction: *t,
74+
Decoded: DecodedTransactionData{
75+
Name: functionABI.RawName,
76+
Signature: functionABI.Sig,
77+
Inputs: decodedInputs,
78+
}}
79+
}

internal/common/transaction_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package common
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
gethCommon "github.com/ethereum/go-ethereum/common"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestDecodeTransaction(t *testing.T) {
12+
transaction := Transaction{
13+
Data: "0x095ea7b3000000000000000000000000971add32ea87f10bd192671630be3be8a11b862300000000000000000000000000000000000000000000010df58ac64e49b91ea0",
14+
}
15+
16+
abi, err := ConstructFunctionABI("approve(address _spender, uint256 _value)")
17+
assert.NoError(t, err)
18+
decodedTransaction := transaction.Decode(abi)
19+
20+
assert.Equal(t, "approve", decodedTransaction.Decoded.Name)
21+
assert.Equal(t, gethCommon.HexToAddress("0x971add32Ea87f10bD192671630be3BE8A11b8623"), decodedTransaction.Decoded.Inputs["_spender"])
22+
expectedValue := big.NewInt(0)
23+
expectedValue.SetString("4979867327953494417056", 10)
24+
assert.Equal(t, expectedValue, decodedTransaction.Decoded.Inputs["_value"])
25+
26+
transaction2 := Transaction{
27+
Data: "0x27c777a9000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000672c0c60302aafae8a36ffd8c12b32f1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038d7ea4c680000000000000000000000000000734d56da60852a03e2aafae8a36ffd8c12b32f10000000000000000000000000000000000000000000000000000000000000000",
28+
}
29+
abi2, err := ConstructFunctionABI("allocatedWithdrawal((bytes,uint256,uint256,uint256,uint256,address) _withdrawal)")
30+
assert.NoError(t, err)
31+
decodedTransaction2 := transaction2.Decode(abi2)
32+
33+
assert.Equal(t, "allocatedWithdrawal", decodedTransaction2.Decoded.Name)
34+
withdrawal := decodedTransaction2.Decoded.Inputs["_withdrawal"].(struct {
35+
Field0 []uint8 `json:"field0"`
36+
Field1 *big.Int `json:"field1"`
37+
Field2 *big.Int `json:"field2"`
38+
Field3 *big.Int `json:"field3"`
39+
Field4 *big.Int `json:"field4"`
40+
Field5 gethCommon.Address `json:"field5"`
41+
})
42+
43+
assert.Equal(t, []uint8{}, withdrawal.Field0)
44+
assert.Equal(t, "123", withdrawal.Field1.String())
45+
assert.Equal(t, "1730940000", withdrawal.Field2.String())
46+
assert.Equal(t, "21786436819914608908212656341824591317420268878283544900672692017070052737024", withdrawal.Field3.String())
47+
assert.Equal(t, "1000000000000000", withdrawal.Field4.String())
48+
assert.Equal(t, "0x0734d56DA60852A03e2Aafae8a36FFd8c12B32f1", withdrawal.Field5.Hex())
49+
}

internal/handlers/transactions_handlers.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package handlers
33
import (
44
"net/http"
55

6+
"github.com/ethereum/go-ethereum/accounts/abi"
67
"github.com/ethereum/go-ethereum/crypto"
78
"github.com/gin-gonic/gin"
89
"github.com/rs/zerolog/log"
@@ -56,7 +57,7 @@ type TransactionModel struct {
5657
// @Failure 500 {object} api.Error
5758
// @Router /{chainId}/transactions [get]
5859
func GetTransactions(c *gin.Context) {
59-
handleTransactionsRequest(c, "", "")
60+
handleTransactionsRequest(c, "", "", nil)
6061
}
6162

6263
// @Summary Get transactions by contract
@@ -81,7 +82,7 @@ func GetTransactions(c *gin.Context) {
8182
// @Router /{chainId}/transactions/{to} [get]
8283
func GetTransactionsByContract(c *gin.Context) {
8384
to := c.Param("to")
84-
handleTransactionsRequest(c, to, "")
85+
handleTransactionsRequest(c, to, "", nil)
8586
}
8687

8788
// @Summary Get transactions by contract and signature
@@ -109,10 +110,14 @@ func GetTransactionsByContractAndSignature(c *gin.Context) {
109110
to := c.Param("to")
110111
signature := c.Param("signature")
111112
strippedSignature := common.StripPayload(signature)
112-
handleTransactionsRequest(c, to, strippedSignature)
113+
functionABI, err := common.ConstructFunctionABI(signature)
114+
if err != nil {
115+
log.Debug().Err(err).Msgf("Unable to construct function ABI for %s", signature)
116+
}
117+
handleTransactionsRequest(c, to, strippedSignature, functionABI)
113118
}
114119

115-
func handleTransactionsRequest(c *gin.Context, contractAddress, signature string) {
120+
func handleTransactionsRequest(c *gin.Context, contractAddress, signature string, functionABI *abi.Method) {
116121
chainId, err := api.GetChainId(c)
117122
if err != nil {
118123
api.BadRequestErrorHandler(c, err)
@@ -187,7 +192,16 @@ func handleTransactionsRequest(c *gin.Context, contractAddress, signature string
187192
api.InternalErrorHandler(c)
188193
return
189194
}
190-
queryResult.Data = transactionsResult.Data
195+
if functionABI != nil {
196+
decodedTransactions := []*common.DecodedTransaction{}
197+
for _, transaction := range transactionsResult.Data {
198+
decodedTransaction := transaction.Decode(functionABI)
199+
decodedTransactions = append(decodedTransactions, decodedTransaction)
200+
}
201+
queryResult.Data = decodedTransactions
202+
} else {
203+
queryResult.Data = transactionsResult.Data
204+
}
191205
queryResult.Meta.TotalItems = len(transactionsResult.Data)
192206
}
193207

0 commit comments

Comments
 (0)