diff --git a/accounts/abi/human_readable_test.go b/accounts/abi/human_readable_test.go new file mode 100644 index 0000000000..cd00d36999 --- /dev/null +++ b/accounts/abi/human_readable_test.go @@ -0,0 +1,701 @@ +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) + }) + } +} diff --git a/accounts/abi/selector_parser.go b/accounts/abi/selector_parser.go index b8ddd7d656..9ab411e9d2 100644 --- a/accounts/abi/selector_parser.go +++ b/accounts/abi/selector_parser.go @@ -19,14 +19,107 @@ package abi import ( "errors" "fmt" + "strings" ) +// SelectorMarshaling represents a parsed function signature with its components. type SelectorMarshaling struct { + Name string `json:"name"` + Type string `json:"type"` + Inputs []ArgumentMarshaling `json:"inputs"` + Outputs []ArgumentMarshaling `json:"outputs,omitempty"` + StateMutability string `json:"stateMutability,omitempty"` + Anonymous bool `json:"anonymous,omitempty"` +} + +// EventMarshaling represents a parsed event signature. +type EventMarshaling struct { + Name string `json:"name"` + Type string `json:"type"` + Inputs []ArgumentMarshaling `json:"inputs"` + Anonymous bool `json:"anonymous"` +} + +// ErrorMarshaling represents a parsed error signature. +type ErrorMarshaling struct { Name string `json:"name"` Type string `json:"type"` Inputs []ArgumentMarshaling `json:"inputs"` } +// ABIMarshaling is a union type that can represent any ABI element. +// It stores the parsed ABI fields such as "name", "type", "inputs", "outputs", +// "stateMutability", and "anonymous" as key-value pairs. +type ABIMarshaling map[string]any + +// ParseHumanReadableABI parses a human-readable ABI signature into a JSON-compatible map. +// It supports functions, events, errors, constructors, fallback, and receive functions. +// +// Examples: +// +// "function transfer(address to, uint256 amount)" +// "function balanceOf(address) view returns (uint256)" +// "event Transfer(address indexed from, address indexed to, uint256 value)" +// "constructor(address owner) payable" +func ParseHumanReadableABI(signature string) (ABIMarshaling, error) { + signature = skipWhitespace(signature) + + if strings.HasPrefix(signature, "constructor") { + return ParseConstructor(signature) + } + + if strings.HasPrefix(signature, "fallback") { + return ParseFallback(signature) + } + + if strings.HasPrefix(signature, "receive") { + return ParseReceive(signature) + } + + if strings.HasPrefix(signature, "event ") || (strings.Contains(signature, "(") && strings.Contains(signature, "indexed")) { + event, err := ParseEvent(signature) + if err != nil { + return nil, err + } + result := make(ABIMarshaling) + result["name"] = event.Name + result["type"] = event.Type + result["inputs"] = event.Inputs + result["anonymous"] = event.Anonymous + return result, nil + } + + if strings.HasPrefix(signature, "error ") { + errSig, err := ParseError(signature) + if err != nil { + return nil, err + } + result := make(ABIMarshaling) + result["name"] = errSig.Name + result["type"] = errSig.Type + result["inputs"] = errSig.Inputs + return result, nil + } + + if strings.HasPrefix(signature, "struct ") { + return nil, fmt.Errorf("struct definitions not supported, use inline tuple syntax") + } + + fn, err := ParseSelector(signature) + if err != nil { + return nil, err + } + result := make(ABIMarshaling) + result["name"] = fn.Name + result["type"] = fn.Type + result["inputs"] = fn.Inputs + if len(fn.Outputs) > 0 { + result["outputs"] = fn.Outputs + } + result["stateMutability"] = fn.StateMutability + return result, nil +} + func isDigit(c byte) bool { return c >= '0' && c <= '9' } @@ -62,12 +155,49 @@ func parseIdentifier(unescapedSelector string) (string, string, error) { return parseToken(unescapedSelector, true) } +func skipWhitespace(s string) string { + i := 0 + for i < len(s) && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') { + i++ + } + return s[i:] +} + +// parseKeyword checks if the string starts with a keyword followed by whitespace or special char +func parseKeyword(s string, keyword string) (string, bool) { + s = skipWhitespace(s) + if !strings.HasPrefix(s, keyword) { + return s, false + } + rest := s[len(keyword):] + if len(rest) > 0 && (isAlpha(rest[0]) || isDigit(rest[0]) || isIdentifierSymbol(rest[0])) { + return s, false + } + return skipWhitespace(rest), true +} + +// normalizeType normalizes bare int/uint to int256/uint256, including arrays +func normalizeType(typeName string) string { + if typeName == "int" { + return "int256" + } + if typeName == "uint" { + return "uint256" + } + if strings.HasPrefix(typeName, "int[") { + return "int256" + typeName[3:] + } + if strings.HasPrefix(typeName, "uint[") { + return "uint256" + typeName[4:] + } + return typeName +} + func parseElementaryType(unescapedSelector string) (string, string, error) { parsedType, rest, err := parseToken(unescapedSelector, false) if err != nil { return "", "", fmt.Errorf("failed to parse elementary type: %v", err) } - // handle arrays for len(rest) > 0 && rest[0] == '[' { parsedType = parsedType + string(rest[0]) rest = rest[1:] @@ -81,97 +211,540 @@ func parseElementaryType(unescapedSelector string) (string, string, error) { parsedType = parsedType + string(rest[0]) rest = rest[1:] } + parsedType = normalizeType(parsedType) 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 { - // generate dummy name to avoid unmarshal issues - name := fmt.Sprintf("name%d", i) - if s, ok := arg.(string); ok { - arguments = append(arguments, ArgumentMarshaling{name, s, s, nil, 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, tupleType, tupleType, subArgs, 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) + + switch rest[0] { + case ')': + 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 + case ',': + rest = rest[1:] + paramIndex++ + default: + return nil, "", rest, fmt.Errorf("expected ',' or ')' in tuple, got '%c'", rest[0]) } } +} + +// parseParameterList parses a parameter list with optional indexed flags +func parseParameterList(params string, allowIndexed bool) ([]ArgumentMarshaling, error) { + params = skipWhitespace(params) + if params == "" { + return []ArgumentMarshaling{}, nil + } + + var arguments []ArgumentMarshaling + paramIndex := 0 + +loop: + for params != "" { + params = skipWhitespace(params) + + var arg ArgumentMarshaling + var rest string + var err error + + if params[0] == '(' { + components, arraySuffix, newRest, tupleErr := parseTupleType(params) + if tupleErr != nil { + return nil, fmt.Errorf("failed to parse tuple: %v", tupleErr) + } + rest = newRest + 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 { + return nil, err + } + } + + arg = ArgumentMarshaling{ + Name: paramName, + 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 == "" { + break + } + switch rest[0] { + case ',': + rest = rest[1:] + params = rest + paramIndex++ + default: + break loop + } + } + return arguments, nil } -// ParseSelector converts a method selector into a struct that can be JSON encoded -// and consumed by other functions in this package. -// Note, although uppercase letters are not part of the ABI spec, this function -// still accepts it as the general format is valid. +// ParseEvent parses an event signature into EventMarshaling +func ParseEvent(unescapedSelector string) (EventMarshaling, error) { + unescapedSelector = skipWhitespace(unescapedSelector) + + rest, _ := parseKeyword(unescapedSelector, "event") + + name, rest, err := parseIdentifier(rest) + if err != nil { + return EventMarshaling{}, fmt.Errorf("failed to parse event name: %v", err) + } + + rest = skipWhitespace(rest) + + if len(rest) == 0 || rest[0] != '(' { + return EventMarshaling{}, fmt.Errorf("expected '(' after event name") + } + rest = rest[1:] + + parenCount := 1 + paramEnd := 0 + for i := 0; i < len(rest); i++ { + if rest[i] == '(' { + parenCount++ + } else if rest[i] == ')' { + parenCount-- + if parenCount == 0 { + paramEnd = i + break + } + } + } + + if parenCount != 0 { + return EventMarshaling{}, fmt.Errorf("unbalanced parentheses in event signature") + } + + paramsStr := rest[:paramEnd] + arguments, err := parseParameterList(paramsStr, true) + if err != nil { + return EventMarshaling{}, fmt.Errorf("failed to parse event parameters: %v", err) + } + + rest = skipWhitespace(rest[paramEnd+1:]) + _, anonymous := parseKeyword(rest, "anonymous") + + return EventMarshaling{ + Name: name, + Type: "event", + Inputs: arguments, + Anonymous: anonymous, + }, nil +} + +// ParseError parses an error signature into ErrorMarshaling +func ParseError(unescapedSelector string) (ErrorMarshaling, error) { + unescapedSelector = skipWhitespace(unescapedSelector) + + rest, _ := parseKeyword(unescapedSelector, "error") + + name, rest, err := parseIdentifier(rest) + if err != nil { + return ErrorMarshaling{}, fmt.Errorf("failed to parse error name: %v", err) + } + + rest = skipWhitespace(rest) + + if len(rest) == 0 || rest[0] != '(' { + return ErrorMarshaling{}, fmt.Errorf("expected '(' after error name") + } + rest = rest[1:] + + parenCount := 1 + paramEnd := 0 + for i := 0; i < len(rest); i++ { + if rest[i] == '(' { + parenCount++ + } else if rest[i] == ')' { + parenCount-- + if parenCount == 0 { + paramEnd = i + break + } + } + } + + if parenCount != 0 { + return ErrorMarshaling{}, fmt.Errorf("unbalanced parentheses in error signature") + } + + paramsStr := rest[:paramEnd] + arguments, err := parseParameterList(paramsStr, false) + if err != nil { + return ErrorMarshaling{}, fmt.Errorf("failed to parse error parameters: %v", err) + } + + return ErrorMarshaling{ + Name: name, + Type: "error", + Inputs: arguments, + }, nil +} + +// ParseConstructor parses a constructor signature +func ParseConstructor(signature string) (ABIMarshaling, error) { + signature = skipWhitespace(signature) + + rest, _ := parseKeyword(signature, "constructor") + rest = skipWhitespace(rest) + + if len(rest) == 0 || rest[0] != '(' { + return nil, fmt.Errorf("expected '(' after constructor keyword") + } + rest = rest[1:] + + parenCount := 1 + paramEnd := 0 + for i := 0; i < len(rest); i++ { + if rest[i] == '(' { + parenCount++ + } else if rest[i] == ')' { + parenCount-- + if parenCount == 0 { + paramEnd = i + break + } + } + } + + if parenCount != 0 { + return nil, fmt.Errorf("unbalanced parentheses in constructor signature") + } + + paramsStr := rest[:paramEnd] + arguments, err := parseParameterList(paramsStr, false) + if err != nil { + return nil, fmt.Errorf("failed to parse constructor parameters: %v", err) + } + + rest = skipWhitespace(rest[paramEnd+1:]) + + stateMutability := "nonpayable" + if newRest, found := parseKeyword(rest, "payable"); found { + stateMutability = "payable" + rest = newRest + } + + result := make(ABIMarshaling) + result["type"] = "constructor" + result["inputs"] = arguments + result["stateMutability"] = stateMutability + return result, nil +} + +// ParseFallback parses a fallback function signature (no parameters allowed). +func ParseFallback(signature string) (ABIMarshaling, error) { + signature = skipWhitespace(signature) + + rest, _ := parseKeyword(signature, "fallback") + rest = skipWhitespace(rest) + + if len(rest) == 0 || rest[0] != '(' { + return nil, fmt.Errorf("expected '(' after fallback keyword") + } + rest = rest[1:] + + if len(rest) == 0 || rest[0] != ')' { + return nil, fmt.Errorf("fallback function cannot have parameters") + } + rest = skipWhitespace(rest[1:]) + + stateMutability := "nonpayable" + if newRest, found := parseKeyword(rest, "payable"); found { + stateMutability = "payable" + rest = newRest + } + + result := make(ABIMarshaling) + result["type"] = "fallback" + result["stateMutability"] = stateMutability + return result, nil +} + +// ParseReceive parses a receive function signature (no parameters, always payable) +func ParseReceive(signature string) (ABIMarshaling, error) { + signature = skipWhitespace(signature) + + rest, _ := parseKeyword(signature, "receive") + rest = skipWhitespace(rest) + + if len(rest) == 0 || rest[0] != '(' { + return nil, fmt.Errorf("expected '(' after receive keyword") + } + rest = rest[1:] + + if len(rest) == 0 || rest[0] != ')' { + return nil, fmt.Errorf("receive function cannot have parameters") + } + rest = skipWhitespace(rest[1:]) + + stateMutability := "payable" + if newRest, found := parseKeyword(rest, "payable"); found { + rest = newRest + } + + result := make(ABIMarshaling) + result["type"] = "receive" + result["stateMutability"] = stateMutability + return result, nil +} + func ParseSelector(unescapedSelector string) (SelectorMarshaling, error) { - name, rest, err := parseIdentifier(unescapedSelector) + unescapedSelector = skipWhitespace(unescapedSelector) + + rest, _ := parseKeyword(unescapedSelector, "function") + rest = skipWhitespace(rest) + + name, rest, err := parseIdentifier(rest) if err != nil { return SelectorMarshaling{}, fmt.Errorf("failed to parse selector '%s': %v", unescapedSelector, err) } - args := []interface{}{} - if len(rest) >= 2 && rest[0] == '(' && rest[1] == ')' { - rest = rest[2:] - } else { - args, rest, err = parseCompositeType(rest) - if err != nil { - return SelectorMarshaling{}, fmt.Errorf("failed to parse selector '%s': %v", unescapedSelector, err) + + rest = skipWhitespace(rest) + + if len(rest) == 0 || rest[0] != '(' { + return SelectorMarshaling{}, fmt.Errorf("expected '(' after function name") + } + rest = rest[1:] + + parenCount := 1 + paramEnd := 0 + for i := 0; i < len(rest); i++ { + if rest[i] == '(' { + parenCount++ + } else if rest[i] == ')' { + parenCount-- + if parenCount == 0 { + paramEnd = i + break + } } } + + if parenCount != 0 { + return SelectorMarshaling{}, fmt.Errorf("unbalanced parentheses in function signature") + } + + paramsStr := rest[:paramEnd] + fakeArgs, err := parseParameterList(paramsStr, false) + if err != nil { + return SelectorMarshaling{}, fmt.Errorf("failed to parse input parameters: %v", err) + } + + rest = skipWhitespace(rest[paramEnd+1:]) + + stateMutability := "nonpayable" + if newRest, found := parseKeyword(rest, "view"); found { + stateMutability = "view" + rest = newRest + } else if newRest, found := parseKeyword(rest, "pure"); found { + stateMutability = "pure" + rest = newRest + } else if newRest, found := parseKeyword(rest, "payable"); found { + stateMutability = "payable" + rest = newRest + } + + rest = skipWhitespace(rest) + + var outputs []ArgumentMarshaling + if newRest, found := parseKeyword(rest, "returns"); found { + rest = skipWhitespace(newRest) + if len(rest) == 0 || rest[0] != '(' { + return SelectorMarshaling{}, fmt.Errorf("expected '(' after returns keyword") + } + + parenCount := 1 + paramEnd := 1 + for i := 1; i < len(rest); i++ { + if rest[i] == '(' { + parenCount++ + } else if rest[i] == ')' { + parenCount-- + if parenCount == 0 { + paramEnd = i + break + } + } + } + + if parenCount != 0 { + return SelectorMarshaling{}, fmt.Errorf("unbalanced parentheses in returns clause") + } + + returnsStr := rest[1:paramEnd] + outputs, err = parseParameterList(returnsStr, false) + if err != nil { + return SelectorMarshaling{}, fmt.Errorf("failed to parse returns: %v", err) + } + + rest = skipWhitespace(rest[paramEnd+1:]) + } + + rest = skipWhitespace(rest) + if stateMutability == "nonpayable" { + if newRest, found := parseKeyword(rest, "view"); found { + stateMutability = "view" + rest = newRest + } else if newRest, found := parseKeyword(rest, "pure"); found { + stateMutability = "pure" + rest = newRest + } else if newRest, found := parseKeyword(rest, "payable"); found { + stateMutability = "payable" + rest = newRest + } + } + + rest = skipWhitespace(rest) + if len(rest) > 0 { return SelectorMarshaling{}, fmt.Errorf("failed to parse selector '%s': unexpected string '%s'", unescapedSelector, rest) } - // Reassemble the fake ABI and construct the JSON - fakeArgs, err := assembleArgs(args) - if err != nil { - return SelectorMarshaling{}, fmt.Errorf("failed to parse selector: %v", err) - } - - return SelectorMarshaling{name, "function", fakeArgs}, nil + return SelectorMarshaling{ + Name: name, + Type: "function", + Inputs: fakeArgs, + Outputs: outputs, + StateMutability: stateMutability, + Anonymous: false, + }, nil } diff --git a/accounts/abi/selector_parser_test.go b/accounts/abi/selector_parser_test.go index 6cb0ae0e70..72f25f7536 100644 --- a/accounts/abi/selector_parser_test.go +++ b/accounts/abi/selector_parser_test.go @@ -28,13 +28,13 @@ func TestParseSelector(t *testing.T) { mkType := func(types ...interface{}) []ArgumentMarshaling { var result []ArgumentMarshaling for i, typeOrComponents := range types { - name := fmt.Sprintf("name%d", i) + name := fmt.Sprintf("param%d", i) if typeName, ok := typeOrComponents.(string); ok { - result = append(result, ArgumentMarshaling{name, typeName, typeName, nil, false}) + result = append(result, ArgumentMarshaling{name, typeName, "", nil, false}) } else if components, ok := typeOrComponents.([]ArgumentMarshaling); ok { - result = append(result, ArgumentMarshaling{name, "tuple", "tuple", components, false}) + result = append(result, ArgumentMarshaling{name, "tuple", "", components, false}) } else if components, ok := typeOrComponents.([][]ArgumentMarshaling); ok { - result = append(result, ArgumentMarshaling{name, "tuple[]", "tuple[]", components[0], false}) + result = append(result, ArgumentMarshaling{name, "tuple[]", "", components[0], false}) } else { log.Fatalf("unexpected type %T", typeOrComponents) } diff --git a/cmd/abigen/main.go b/cmd/abigen/main.go index c82358be49..8539fb6846 100644 --- a/cmd/abigen/main.go +++ b/cmd/abigen/main.go @@ -17,6 +17,8 @@ package main import ( + "bufio" + "bytes" "encoding/json" "fmt" "io" @@ -24,6 +26,7 @@ import ( "regexp" "strings" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/abigen" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common/compiler" @@ -71,6 +74,10 @@ var ( Name: "v2", Usage: "Generates v2 bindings", } + humanABIFlag = &cli.StringFlag{ + Name: "human-abi", + Usage: "Path to human-readable ABI file (one signature per line), - for STDIN", + } ) var app = flags.NewApp("Ethereum ABI wrapper code generator") @@ -87,18 +94,19 @@ func init() { outFlag, aliasFlag, v2Flag, + humanABIFlag, } app.Action = generate } func generate(c *cli.Context) error { - flags.CheckExclusive(c, abiFlag, jsonFlag) // Only one source can be selected. + flags.CheckExclusive(c, abiFlag, jsonFlag, humanABIFlag) // Only one source can be selected. if c.String(pkgFlag.Name) == "" { utils.Fatalf("No destination package specified (--pkg)") } - if c.String(abiFlag.Name) == "" && c.String(jsonFlag.Name) == "" { - utils.Fatalf("Either contract ABI source (--abi) or combined-json (--combined-json) are required") + if c.String(abiFlag.Name) == "" && c.String(jsonFlag.Name) == "" && c.String(humanABIFlag.Name) == "" { + utils.Fatalf("Either contract ABI source (--abi), combined-json (--combined-json), or human-readable ABI (--human-abi) are required") } // If the entire solidity code was specified, build and bind based on that var ( @@ -137,6 +145,45 @@ func generate(c *cli.Context) error { } bins = append(bins, string(bin)) + kind := c.String(typeFlag.Name) + if kind == "" { + kind = c.String(pkgFlag.Name) + } + types = append(types, kind) + } else if c.String(humanABIFlag.Name) != "" { + // Load human-readable ABI and convert to JSON + var ( + humanABI []byte + err error + ) + input := c.String(humanABIFlag.Name) + if input == "-" { + humanABI, err = io.ReadAll(os.Stdin) + } else { + humanABI, err = os.ReadFile(input) + } + if err != nil { + utils.Fatalf("Failed to read human-readable ABI: %v", err) + } + + jsonABI, err := convertHumanReadableABI(string(humanABI)) + if err != nil { + utils.Fatalf("Failed to parse human-readable ABI: %v", err) + } + + abis = append(abis, jsonABI) + + var bin []byte + if binFile := c.String(binFlag.Name); binFile != "" { + if bin, err = os.ReadFile(binFile); err != nil { + utils.Fatalf("Failed to read input bytecode: %v", err) + } + if strings.Contains(string(bin), "//") { + utils.Fatalf("Contract has additional library references, please use other mode(e.g. --combined-json) to catch library infos") + } + } + bins = append(bins, string(bin)) + kind := c.String(typeFlag.Name) if kind == "" { kind = c.String(pkgFlag.Name) @@ -234,6 +281,41 @@ func generate(c *cli.Context) error { return nil } +func convertHumanReadableABI(humanABI string) (string, error) { + var abiElements []abi.ABIMarshaling + + scanner := bufio.NewScanner(strings.NewReader(humanABI)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if line == "" || strings.HasPrefix(line, "//") || strings.HasPrefix(line, "#") { + continue + } + + element, err := abi.ParseHumanReadableABI(line) + if err != nil { + return "", fmt.Errorf("failed to parse signature '%s': %v", line, err) + } + + if element != nil { + abiElements = append(abiElements, element) + } + } + + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("failed to read human-readable ABI: %v", err) + } + + var buf bytes.Buffer + encoder := json.NewEncoder(&buf) + encoder.SetIndent("", " ") + if err := encoder.Encode(abiElements); err != nil { + return "", fmt.Errorf("failed to encode JSON ABI: %v", err) + } + + return buf.String(), nil +} + func main() { log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelInfo, true)))