This commit is contained in:
mmsqe 2026-02-24 21:55:16 -08:00 committed by GitHub
commit fa43e2bc6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1431 additions and 75 deletions

View file

@ -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)
})
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)))