mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-04-30 21:32:55 +00:00
701 lines
17 KiB
Go
701 lines
17 KiB
Go
package abi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// normalizeArgument converts ArgumentMarshaling to a JSON-compatible map.
|
|
// Auto-generated parameter names like "param0", "param1" are converted to empty strings.
|
|
func normalizeArgument(arg ArgumentMarshaling, isEvent bool) map[string]interface{} {
|
|
name := arg.Name
|
|
if strings.HasPrefix(name, "param") && len(name) > 5 {
|
|
isParamN := true
|
|
for _, c := range name[5:] {
|
|
if c < '0' || c > '9' {
|
|
isParamN = false
|
|
break
|
|
}
|
|
}
|
|
if isParamN {
|
|
name = ""
|
|
}
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"name": name,
|
|
"type": arg.Type,
|
|
}
|
|
if arg.InternalType != "" {
|
|
result["internalType"] = arg.InternalType
|
|
}
|
|
if len(arg.Components) > 0 {
|
|
components := make([]map[string]interface{}, len(arg.Components))
|
|
for i, comp := range arg.Components {
|
|
components[i] = normalizeArgument(comp, isEvent)
|
|
}
|
|
result["components"] = components
|
|
}
|
|
if isEvent {
|
|
result["indexed"] = arg.Indexed
|
|
}
|
|
return result
|
|
}
|
|
|
|
// parseStructDefinitions extracts and parses struct definitions from signatures
|
|
func parseStructDefinitions(signatures []string) (map[string][]ArgumentMarshaling, error) {
|
|
structs := make(map[string][]ArgumentMarshaling)
|
|
|
|
for _, sig := range signatures {
|
|
sig = skipWhitespace(sig)
|
|
if !strings.HasPrefix(sig, "struct ") {
|
|
continue
|
|
}
|
|
|
|
rest := sig[7:]
|
|
rest = skipWhitespace(rest)
|
|
|
|
nameEnd := 0
|
|
for nameEnd < len(rest) && (isAlpha(rest[nameEnd]) || isDigit(rest[nameEnd]) || rest[nameEnd] == '_') {
|
|
nameEnd++
|
|
}
|
|
if nameEnd == 0 {
|
|
return nil, fmt.Errorf("invalid struct definition: missing name")
|
|
}
|
|
|
|
structName := rest[:nameEnd]
|
|
rest = skipWhitespace(rest[nameEnd:])
|
|
|
|
if len(rest) == 0 || rest[0] != '{' {
|
|
return nil, fmt.Errorf("invalid struct definition: expected '{'")
|
|
}
|
|
rest = rest[1:]
|
|
|
|
closeBrace := strings.Index(rest, "}")
|
|
if closeBrace == -1 {
|
|
return nil, fmt.Errorf("invalid struct definition: missing '}'")
|
|
}
|
|
|
|
fieldsStr := rest[:closeBrace]
|
|
fields := strings.Split(fieldsStr, ";")
|
|
|
|
var components []ArgumentMarshaling
|
|
for _, field := range fields {
|
|
field = skipWhitespace(field)
|
|
if field == "" {
|
|
continue
|
|
}
|
|
|
|
parts := strings.Fields(field)
|
|
if len(parts) < 2 {
|
|
return nil, fmt.Errorf("invalid struct field: %s", field)
|
|
}
|
|
|
|
typeName := parts[0]
|
|
fieldName := parts[1]
|
|
|
|
components = append(components, ArgumentMarshaling{
|
|
Name: fieldName,
|
|
Type: typeName,
|
|
})
|
|
}
|
|
|
|
structs[structName] = components
|
|
}
|
|
|
|
return structs, nil
|
|
}
|
|
|
|
// expandStructReferences replaces struct references with tuple types
|
|
func expandStructReferences(args []ArgumentMarshaling, structs map[string][]ArgumentMarshaling) error {
|
|
for i := range args {
|
|
baseType := args[i].Type
|
|
arraySuffix := ""
|
|
|
|
for strings.HasSuffix(baseType, "]") {
|
|
bracketIdx := strings.LastIndex(baseType, "[")
|
|
if bracketIdx == -1 {
|
|
break
|
|
}
|
|
arraySuffix = baseType[bracketIdx:] + arraySuffix
|
|
baseType = baseType[:bracketIdx]
|
|
}
|
|
|
|
if structComponents, ok := structs[baseType]; ok {
|
|
expandedComponents := make([]ArgumentMarshaling, len(structComponents))
|
|
copy(expandedComponents, structComponents)
|
|
|
|
if err := expandStructReferences(expandedComponents, structs); err != nil {
|
|
return err
|
|
}
|
|
|
|
args[i].Type = "tuple" + arraySuffix
|
|
args[i].InternalType = "struct " + baseType + arraySuffix
|
|
args[i].Components = expandedComponents
|
|
} else if len(args[i].Components) > 0 {
|
|
if err := expandStructReferences(args[i].Components, structs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseHumanReadableABIArray processes multiple human-readable ABI signatures
|
|
// and returns a JSON array. Comments and empty lines are skipped.
|
|
func parseHumanReadableABIArray(signatures []string) ([]byte, error) {
|
|
structs, err := parseStructDefinitions(signatures)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var results []map[string]interface{}
|
|
for _, sig := range signatures {
|
|
sig = skipWhitespace(sig)
|
|
if sig == "" || strings.HasPrefix(sig, "//") {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(sig, "struct ") {
|
|
continue
|
|
}
|
|
result, err := ParseHumanReadableABI(sig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resultType := result["type"]
|
|
normalized := map[string]interface{}{
|
|
"type": resultType,
|
|
}
|
|
isEvent := resultType == "event"
|
|
isFunction := resultType == "function"
|
|
isConstructor := resultType == "constructor"
|
|
isFallback := resultType == "fallback"
|
|
isReceive := resultType == "receive"
|
|
|
|
if !isConstructor && !isFallback && !isReceive {
|
|
if name, ok := result["name"]; ok {
|
|
normalized["name"] = name
|
|
}
|
|
}
|
|
if inputs, ok := result["inputs"].([]ArgumentMarshaling); ok {
|
|
if err := expandStructReferences(inputs, structs); err != nil {
|
|
return nil, err
|
|
}
|
|
normInputs := make([]map[string]interface{}, len(inputs))
|
|
for i, inp := range inputs {
|
|
normInputs[i] = normalizeArgument(inp, isEvent)
|
|
}
|
|
normalized["inputs"] = normInputs
|
|
}
|
|
if outputs, ok := result["outputs"].([]ArgumentMarshaling); ok {
|
|
if err := expandStructReferences(outputs, structs); err != nil {
|
|
return nil, err
|
|
}
|
|
normOutputs := make([]map[string]interface{}, len(outputs))
|
|
for i, out := range outputs {
|
|
normOutputs[i] = normalizeArgument(out, false)
|
|
}
|
|
normalized["outputs"] = normOutputs
|
|
} else if isFunction {
|
|
normalized["outputs"] = []map[string]interface{}{}
|
|
}
|
|
if stateMutability, ok := result["stateMutability"]; ok {
|
|
normalized["stateMutability"] = stateMutability
|
|
}
|
|
if anonymous, ok := result["anonymous"]; ok {
|
|
normalized["anonymous"] = anonymous
|
|
}
|
|
|
|
results = append(results, normalized)
|
|
}
|
|
return json.Marshal(results)
|
|
}
|
|
|
|
func TestParseHumanReadableABI(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []string
|
|
expected string
|
|
hasError bool
|
|
}{
|
|
{
|
|
name: "simple function",
|
|
input: []string{"function transfer(address to, uint256 amount)"},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "transfer",
|
|
"inputs": [
|
|
{"name": "to", "type": "address"},
|
|
{"name": "amount", "type": "uint256"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "function with view and returns",
|
|
input: []string{"function balanceOf(address account) view returns (uint256)"},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "balanceOf",
|
|
"inputs": [
|
|
{"name": "account", "type": "address"}
|
|
],
|
|
"outputs": [
|
|
{"name": "", "type": "uint256"}
|
|
],
|
|
"stateMutability": "view"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "function with payable",
|
|
input: []string{"function deposit() payable"},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "deposit",
|
|
"inputs": [],
|
|
"outputs": [],
|
|
"stateMutability": "payable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "event with indexed parameters",
|
|
input: []string{"event Transfer(address indexed from, address indexed to, uint256 value)"},
|
|
expected: `[
|
|
{
|
|
"type": "event",
|
|
"name": "Transfer",
|
|
"inputs": [
|
|
{"name": "from", "type": "address", "indexed": true},
|
|
{"name": "to", "type": "address", "indexed": true},
|
|
{"name": "value", "type": "uint256", "indexed": false}
|
|
],
|
|
"anonymous": false
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "constructor",
|
|
input: []string{"constructor(address owner, uint256 initialSupply)"},
|
|
expected: `[
|
|
{
|
|
"type": "constructor",
|
|
"inputs": [
|
|
{"name": "owner", "type": "address"},
|
|
{"name": "initialSupply", "type": "uint256"}
|
|
],
|
|
"stateMutability": "nonpayable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "constructor payable",
|
|
input: []string{"constructor(address owner) payable"},
|
|
expected: `[
|
|
{
|
|
"type": "constructor",
|
|
"inputs": [
|
|
{"name": "owner", "type": "address"}
|
|
],
|
|
"stateMutability": "payable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "fallback function",
|
|
input: []string{"fallback()"},
|
|
expected: `[
|
|
{
|
|
"type": "fallback",
|
|
"stateMutability": "nonpayable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "receive function",
|
|
input: []string{"receive() payable"},
|
|
expected: `[
|
|
{
|
|
"type": "receive",
|
|
"stateMutability": "payable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "multiple functions",
|
|
input: []string{
|
|
"function transfer(address to, uint256 amount)",
|
|
"function balanceOf(address account) view returns (uint256)",
|
|
},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "transfer",
|
|
"inputs": [
|
|
{"name": "to", "type": "address"},
|
|
{"name": "amount", "type": "uint256"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
},
|
|
{
|
|
"type": "function",
|
|
"name": "balanceOf",
|
|
"inputs": [
|
|
{"name": "account", "type": "address"}
|
|
],
|
|
"outputs": [
|
|
{"name": "", "type": "uint256"}
|
|
],
|
|
"stateMutability": "view"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "function with arrays",
|
|
input: []string{"function batchTransfer(address[] recipients, uint256[] amounts)"},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "batchTransfer",
|
|
"inputs": [
|
|
{"name": "recipients", "type": "address[]"},
|
|
{"name": "amounts", "type": "uint256[]"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "function with fixed arrays",
|
|
input: []string{"function getBalances(address[10] accounts) view returns (uint256[10])"},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "getBalances",
|
|
"inputs": [
|
|
{"name": "accounts", "type": "address[10]"}
|
|
],
|
|
"outputs": [
|
|
{"name": "", "type": "uint256[10]"}
|
|
],
|
|
"stateMutability": "view"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "function with bytes types",
|
|
input: []string{"function setData(bytes32 key, bytes value)"},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "setData",
|
|
"inputs": [
|
|
{"name": "key", "type": "bytes32"},
|
|
{"name": "value", "type": "bytes"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "function with small integers",
|
|
input: []string{"function smallIntegers(uint8 u8, uint16 u16, uint32 u32, uint64 u64, int8 i8, int16 i16, int32 i32, int64 i64)"},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "smallIntegers",
|
|
"inputs": [
|
|
{"name": "u8", "type": "uint8"},
|
|
{"name": "u16", "type": "uint16"},
|
|
{"name": "u32", "type": "uint32"},
|
|
{"name": "u64", "type": "uint64"},
|
|
{"name": "i8", "type": "int8"},
|
|
{"name": "i16", "type": "int16"},
|
|
{"name": "i32", "type": "int32"},
|
|
{"name": "i64", "type": "int64"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "function with non-standard small integers",
|
|
input: []string{"function nonStandardIntegers(uint24 u24, uint48 u48, uint72 u72, uint96 u96, uint120 u120, int24 i24, int36 i36, int48 i48, int72 i72, int96 i96, int120 i120)"},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "nonStandardIntegers",
|
|
"inputs": [
|
|
{"name": "u24", "type": "uint24"},
|
|
{"name": "u48", "type": "uint48"},
|
|
{"name": "u72", "type": "uint72"},
|
|
{"name": "u96", "type": "uint96"},
|
|
{"name": "u120", "type": "uint120"},
|
|
{"name": "i24", "type": "int24"},
|
|
{"name": "i36", "type": "int36"},
|
|
{"name": "i48", "type": "int48"},
|
|
{"name": "i72", "type": "int72"},
|
|
{"name": "i96", "type": "int96"},
|
|
{"name": "i120", "type": "int120"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "comments and empty lines",
|
|
input: []string{
|
|
"// This is a comment",
|
|
"",
|
|
"function transfer(address to, uint256 amount)",
|
|
"",
|
|
"// Another comment",
|
|
"function balanceOf(address account) view returns (uint256)",
|
|
},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "transfer",
|
|
"inputs": [
|
|
{"name": "to", "type": "address"},
|
|
{"name": "amount", "type": "uint256"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
},
|
|
{
|
|
"type": "function",
|
|
"name": "balanceOf",
|
|
"inputs": [
|
|
{"name": "account", "type": "address"}
|
|
],
|
|
"outputs": [
|
|
{"name": "", "type": "uint256"}
|
|
],
|
|
"stateMutability": "view"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "function with nested dynamic arrays",
|
|
input: []string{
|
|
"function processNestedArrays(uint256[][] matrix, address[][2][] deepArray)",
|
|
},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "processNestedArrays",
|
|
"inputs": [
|
|
{"name": "matrix", "type": "uint256[][]"},
|
|
{"name": "deepArray", "type": "address[][2][]"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "function with mixed fixed and dynamic arrays",
|
|
input: []string{
|
|
"function processMixedArrays(uint256[5] fixedArray, address[] dynamicArray, bytes32[3][] fixedDynamicArray)",
|
|
},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "processMixedArrays",
|
|
"inputs": [
|
|
{"name": "fixedArray", "type": "uint256[5]"},
|
|
{"name": "dynamicArray", "type": "address[]"},
|
|
{"name": "fixedDynamicArray", "type": "bytes32[3][]"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "function with deeply nested mixed arrays",
|
|
input: []string{
|
|
"function deepNestedArrays(uint256[][] complexArray, address[][] mixedArray)",
|
|
},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "deepNestedArrays",
|
|
"inputs": [
|
|
{"name": "complexArray", "type": "uint256[][]"},
|
|
{"name": "mixedArray", "type": "address[][]"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "int and uint without explicit sizes normalize to 256 bits",
|
|
input: []string{
|
|
"function testIntUint(int value1, uint value2)",
|
|
"function testArrays(int[] values1, uint[10] values2)",
|
|
"function testMixed(int value1, uint value2, int8 value3, uint256 value4)",
|
|
},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "testIntUint",
|
|
"inputs": [
|
|
{"name": "value1", "type": "int256"},
|
|
{"name": "value2", "type": "uint256"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
},
|
|
{
|
|
"type": "function",
|
|
"name": "testArrays",
|
|
"inputs": [
|
|
{"name": "values1", "type": "int256[]"},
|
|
{"name": "values2", "type": "uint256[10]"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
},
|
|
{
|
|
"type": "function",
|
|
"name": "testMixed",
|
|
"inputs": [
|
|
{"name": "value1", "type": "int256"},
|
|
{"name": "value2", "type": "uint256"},
|
|
{"name": "value3", "type": "int8"},
|
|
{"name": "value4", "type": "uint256"}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "nested tuple in return",
|
|
input: []string{
|
|
"function communityPool() view returns ((string denom, uint256 amount)[] coins)",
|
|
},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "communityPool",
|
|
"inputs": [],
|
|
"outputs": [
|
|
{
|
|
"name": "coins",
|
|
"type": "tuple[]",
|
|
"components": [
|
|
{"name": "denom", "type": "string"},
|
|
{"name": "amount", "type": "uint256"}
|
|
]
|
|
}
|
|
],
|
|
"stateMutability": "view"
|
|
}
|
|
]`,
|
|
},
|
|
{
|
|
name: "function with struct arrays and nested arrays",
|
|
input: []string{
|
|
"struct DataPoint { uint256 value; string label; }",
|
|
"function processData(DataPoint[][] dataMatrix, DataPoint[5][] fixedDataArray)",
|
|
},
|
|
expected: `[
|
|
{
|
|
"type": "function",
|
|
"name": "processData",
|
|
"inputs": [
|
|
{
|
|
"name": "dataMatrix",
|
|
"type": "tuple[][]",
|
|
"internalType": "struct DataPoint[][]",
|
|
"components": [
|
|
{"name": "value", "type": "uint256"},
|
|
{"name": "label", "type": "string"}
|
|
]
|
|
},
|
|
{
|
|
"name": "fixedDataArray",
|
|
"type": "tuple[5][]",
|
|
"internalType": "struct DataPoint[5][]",
|
|
"components": [
|
|
{"name": "value", "type": "uint256"},
|
|
{"name": "label", "type": "string"}
|
|
]
|
|
}
|
|
],
|
|
"outputs": [],
|
|
"stateMutability": "nonpayable"
|
|
}
|
|
]`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := parseHumanReadableABIArray(tt.input)
|
|
if tt.hasError {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
var expectedJSON interface{}
|
|
err = json.Unmarshal([]byte(tt.expected), &expectedJSON)
|
|
require.NoError(t, err)
|
|
|
|
var actualJSON interface{}
|
|
err = json.Unmarshal(result, &actualJSON)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, expectedJSON, actualJSON)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseHumanReadableABI_Errors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []string
|
|
}{
|
|
{
|
|
name: "invalid function format",
|
|
input: []string{"function invalid format"},
|
|
},
|
|
{
|
|
name: "invalid array size",
|
|
input: []string{"function test(uint256[invalid] arr) returns (bool)"},
|
|
},
|
|
{
|
|
name: "unrecognized line",
|
|
input: []string{"invalid line format"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := parseHumanReadableABIArray(tt.input)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
}
|