Skip to content

Commit f996013

Browse files
committed
decode transaction inputs
1 parent cdb050b commit f996013

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)