Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading