-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
4 changed files
with
277 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters