accounts/abi: support inline tuple

This commit is contained in:
mmsqe 2026-01-15 14:16:19 +08:00
parent fd81bb1b9f
commit 7fddaecfd9
No known key found for this signature in database
GPG key ID: 1D6409A9D4025709
2 changed files with 314 additions and 102 deletions

View file

@ -2,6 +2,7 @@ package abi
import (
"encoding/json"
"fmt"
"strings"
"testing"
@ -45,9 +46,113 @@ func normalizeArgument(arg ArgumentMarshaling, isEvent bool) map[string]interfac
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)
@ -78,6 +183,9 @@ func parseHumanReadableABIArray(signatures []string) ([]byte, error) {
}
}
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)
@ -85,6 +193,9 @@ func parseHumanReadableABIArray(signatures []string) ([]byte, error) {
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)
@ -478,6 +589,65 @@ func TestParseHumanReadableABI(t *testing.T) {
}
]`,
},
{
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 {

View file

@ -202,76 +202,110 @@ func parseElementaryType(unescapedSelector string) (string, string, error) {
return parsedType, rest, nil
}
func parseCompositeType(unescapedSelector string) ([]interface{}, string, error) {
if len(unescapedSelector) == 0 || unescapedSelector[0] != '(' {
return nil, "", fmt.Errorf("expected '(', got %c", unescapedSelector[0])
// parseTupleType parses inline tuple syntax like (string denom, uint256 amount)[]
func parseTupleType(params string) ([]ArgumentMarshaling, string, string, error) {
if params == "" || params[0] != '(' {
return nil, "", params, fmt.Errorf("expected '(' at start of tuple")
}
parsedType, rest, err := parseType(unescapedSelector[1:])
if err != nil {
return nil, "", fmt.Errorf("failed to parse type: %v", err)
}
result := []interface{}{parsedType}
for len(rest) > 0 && rest[0] != ')' {
parsedType, rest, err = parseType(rest[1:])
if err != nil {
return nil, "", fmt.Errorf("failed to parse type: %v", err)
rest := params[1:]
rest = skipWhitespace(rest)
if rest[0] == ')' {
rest = rest[1:]
arraySuffix := ""
for len(rest) > 0 && rest[0] == '[' {
endBracket := 1
for endBracket < len(rest) && rest[endBracket] != ']' {
endBracket++
}
if endBracket >= len(rest) {
return nil, "", rest, fmt.Errorf("unclosed array bracket")
}
arraySuffix += rest[:endBracket+1]
rest = rest[endBracket+1:]
}
result = append(result, parsedType)
return []ArgumentMarshaling{}, arraySuffix, rest, nil
}
if len(rest) == 0 || rest[0] != ')' {
return nil, "", fmt.Errorf("expected ')', got '%s'", rest)
}
if len(rest) >= 3 && rest[1] == '[' && rest[2] == ']' {
return append(result, "[]"), rest[3:], nil
}
return result, rest[1:], nil
}
func parseType(unescapedSelector string) (interface{}, string, error) {
if len(unescapedSelector) == 0 {
return nil, "", errors.New("empty type")
}
if unescapedSelector[0] == '(' {
return parseCompositeType(unescapedSelector)
} else {
return parseElementaryType(unescapedSelector)
}
}
var components []ArgumentMarshaling
paramIndex := 0
func assembleArgs(args []interface{}) ([]ArgumentMarshaling, error) {
arguments := make([]ArgumentMarshaling, 0)
for i, arg := range args {
name := fmt.Sprintf("name%d", i)
if s, ok := arg.(string); ok {
arguments = append(arguments, ArgumentMarshaling{
Name: name,
Type: s,
InternalType: s,
Components: nil,
Indexed: false,
})
} else if components, ok := arg.([]interface{}); ok {
subArgs, err := assembleArgs(components)
if err != nil {
return nil, fmt.Errorf("failed to assemble components: %v", err)
for {
rest = skipWhitespace(rest)
var component ArgumentMarshaling
var err error
if rest[0] == '(' {
subComponents, subArraySuffix, newRest, subErr := parseTupleType(rest)
if subErr != nil {
return nil, "", rest, fmt.Errorf("failed to parse nested tuple: %v", subErr)
}
tupleType := "tuple"
if len(subArgs) != 0 && subArgs[len(subArgs)-1].Type == "[]" {
subArgs = subArgs[:len(subArgs)-1]
tupleType = "tuple[]"
rest = newRest
rest = skipWhitespace(rest)
paramName := fmt.Sprintf("param%d", paramIndex)
if len(rest) > 0 && (isAlpha(rest[0]) || isIdentifierSymbol(rest[0])) {
paramName, rest, err = parseIdentifier(rest)
if err != nil {
return nil, "", rest, err
}
}
component = ArgumentMarshaling{
Name: paramName,
Type: "tuple" + subArraySuffix,
Components: subComponents,
}
arguments = append(arguments, ArgumentMarshaling{
Name: name,
Type: tupleType,
InternalType: tupleType,
Components: subArgs,
Indexed: false,
})
} else {
return nil, fmt.Errorf("failed to assemble args: unexpected type %T", arg)
var typeName string
typeName, rest, err = parseElementaryType(rest)
if err != nil {
return nil, "", rest, fmt.Errorf("failed to parse type: %v", err)
}
rest = skipWhitespace(rest)
paramName := fmt.Sprintf("param%d", paramIndex)
if len(rest) > 0 && (isAlpha(rest[0]) || isIdentifierSymbol(rest[0])) {
paramName, rest, err = parseIdentifier(rest)
if err != nil {
return nil, "", rest, err
}
}
component = ArgumentMarshaling{
Name: paramName,
Type: typeName,
}
}
components = append(components, component)
rest = skipWhitespace(rest)
if rest[0] == ')' {
rest = rest[1:]
arraySuffix := ""
for len(rest) > 0 && rest[0] == '[' {
endBracket := 1
for endBracket < len(rest) && rest[endBracket] != ']' {
endBracket++
}
if endBracket >= len(rest) {
return nil, "", rest, fmt.Errorf("unclosed array bracket")
}
arraySuffix += rest[:endBracket+1]
rest = rest[endBracket+1:]
}
return components, arraySuffix, rest, nil
} else if rest[0] == ',' {
rest = rest[1:]
paramIndex++
} else {
return nil, "", rest, fmt.Errorf("expected ',' or ')' in tuple, got '%c'", rest[0])
}
}
return arguments, nil
}
// parseParameterList parses a parameter list with optional indexed flags
@ -287,61 +321,69 @@ func parseParameterList(params string, allowIndexed bool) ([]ArgumentMarshaling,
for params != "" {
params = skipWhitespace(params)
var typeStr interface{}
var arg ArgumentMarshaling
var rest string
var err error
if params[0] == '(' {
typeStr, rest, err = parseCompositeType(params)
} else {
typeStr, rest, err = parseElementaryType(params)
}
if err != nil {
return nil, fmt.Errorf("failed to parse parameter type: %v", err)
}
rest = skipWhitespace(rest)
indexed := false
if allowIndexed {
rest, indexed = parseKeyword(rest, "indexed")
components, arraySuffix, newRest, tupleErr := parseTupleType(params)
if tupleErr != nil {
return nil, fmt.Errorf("failed to parse tuple: %v", tupleErr)
}
rest = newRest
rest = skipWhitespace(rest)
}
paramName := fmt.Sprintf("param%d", paramIndex)
if len(rest) > 0 && (isAlpha(rest[0]) || isIdentifierSymbol(rest[0])) {
var name string
name, rest, err = parseIdentifier(rest)
if err == nil {
paramName = name
indexed := false
if allowIndexed {
rest, indexed = parseKeyword(rest, "indexed")
rest = skipWhitespace(rest)
}
}
if s, ok := typeStr.(string); ok {
arguments = append(arguments, ArgumentMarshaling{
Name: paramName,
Type: s,
Indexed: indexed,
})
} else if components, ok := typeStr.([]interface{}); ok {
subArgs, err := assembleArgs(components)
if err != nil {
return nil, fmt.Errorf("failed to assemble tuple components: %v", err)
paramName := fmt.Sprintf("param%d", paramIndex)
if len(rest) > 0 && (isAlpha(rest[0]) || isIdentifierSymbol(rest[0])) {
paramName, rest, err = parseIdentifier(rest)
if err != nil {
return nil, err
}
}
tupleType := "tuple"
if len(subArgs) != 0 && subArgs[len(subArgs)-1].Type == "[]" {
subArgs = subArgs[:len(subArgs)-1]
tupleType = "tuple[]"
}
arguments = append(arguments, ArgumentMarshaling{
arg = ArgumentMarshaling{
Name: paramName,
Type: tupleType,
Components: subArgs,
Type: "tuple" + arraySuffix,
Components: components,
Indexed: indexed,
})
}
} else {
var typeName string
typeName, rest, err = parseElementaryType(params)
if err != nil {
return nil, fmt.Errorf("failed to parse parameter type: %v", err)
}
rest = skipWhitespace(rest)
indexed := false
if allowIndexed {
rest, indexed = parseKeyword(rest, "indexed")
rest = skipWhitespace(rest)
}
paramName := fmt.Sprintf("param%d", paramIndex)
if len(rest) > 0 && (isAlpha(rest[0]) || isIdentifierSymbol(rest[0])) {
paramName, rest, err = parseIdentifier(rest)
if err == nil && paramName != "" {
rest = skipWhitespace(rest)
}
}
arg = ArgumentMarshaling{
Name: paramName,
Type: typeName,
Indexed: indexed,
}
}
arguments = append(arguments, arg)
rest = skipWhitespace(rest)
if rest == "" {