From 7fddaecfd9c80a2b0e9ff8827e9900f26eaf8ed6 Mon Sep 17 00:00:00 2001 From: mmsqe Date: Thu, 15 Jan 2026 14:16:19 +0800 Subject: [PATCH] accounts/abi: support inline tuple --- accounts/abi/human_readable_test.go | 170 +++++++++++++++++++ accounts/abi/selector_parser.go | 246 ++++++++++++++++------------ 2 files changed, 314 insertions(+), 102 deletions(-) diff --git a/accounts/abi/human_readable_test.go b/accounts/abi/human_readable_test.go index 52e3045637..cd00d36999 100644 --- a/accounts/abi/human_readable_test.go +++ b/accounts/abi/human_readable_test.go @@ -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 { diff --git a/accounts/abi/selector_parser.go b/accounts/abi/selector_parser.go index cec41e0d8b..ba92aaa87b 100644 --- a/accounts/abi/selector_parser.go +++ b/accounts/abi/selector_parser.go @@ -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 == "" {