mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-02-26 15:47:21 +00:00
accounts/abi, cmd/abigen: add human-readable ABI support
* to provide intuitive way to define contract interfaces compared to verbose JSON * inspired by: https://github.com/yihuang/go-abi * for more spec: https://abitype.dev/api/human
This commit is contained in:
parent
494908a852
commit
bf541d1dfd
3 changed files with 922 additions and 26 deletions
437
accounts/abi/human_readable_test.go
Normal file
437
accounts/abi/human_readable_test.go
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
package abi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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
|
||||
}
|
||||
|
||||
// parseHumanReadableABIArray processes multiple human-readable ABI signatures
|
||||
// and returns a JSON array. Comments and empty lines are skipped.
|
||||
func parseHumanReadableABIArray(signatures []string) ([]byte, error) {
|
||||
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"
|
||||
|
||||
if name, ok := result["name"]; ok {
|
||||
normalized["name"] = name
|
||||
}
|
||||
if inputs, ok := result["inputs"].([]ArgumentMarshaling); ok {
|
||||
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 {
|
||||
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: "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"
|
||||
}
|
||||
]`,
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -19,14 +19,82 @@ package abi
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type EventMarshaling struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Inputs []ArgumentMarshaling `json:"inputs"`
|
||||
Anonymous bool `json:"anonymous"`
|
||||
}
|
||||
|
||||
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
|
||||
type ABIMarshaling map[string]interface{}
|
||||
|
||||
// ParseHumanReadableABI parses a human-readable ABI signature into a JSON-compatible map
|
||||
func ParseHumanReadableABI(signature string) (ABIMarshaling, error) {
|
||||
signature = skipWhitespace(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 +130,32 @@ 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
|
||||
}
|
||||
|
||||
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:]
|
||||
|
|
@ -123,10 +211,15 @@ func parseType(unescapedSelector string) (interface{}, string, error) {
|
|||
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})
|
||||
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 {
|
||||
|
|
@ -137,7 +230,13 @@ func assembleArgs(args []interface{}) ([]ArgumentMarshaling, error) {
|
|||
subArgs = subArgs[:len(subArgs)-1]
|
||||
tupleType = "tuple[]"
|
||||
}
|
||||
arguments = append(arguments, ArgumentMarshaling{name, tupleType, tupleType, subArgs, false})
|
||||
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)
|
||||
}
|
||||
|
|
@ -145,33 +244,311 @@ func assembleArgs(args []interface{}) ([]ArgumentMarshaling, error) {
|
|||
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.
|
||||
// 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
|
||||
|
||||
for params != "" {
|
||||
params = skipWhitespace(params)
|
||||
|
||||
var typeStr interface{}
|
||||
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")
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
tupleType := "tuple"
|
||||
if len(subArgs) != 0 && subArgs[len(subArgs)-1].Type == "[]" {
|
||||
subArgs = subArgs[:len(subArgs)-1]
|
||||
tupleType = "tuple[]"
|
||||
}
|
||||
arguments = append(arguments, ArgumentMarshaling{
|
||||
Name: paramName,
|
||||
Type: tupleType,
|
||||
Components: subArgs,
|
||||
Indexed: indexed,
|
||||
})
|
||||
}
|
||||
|
||||
rest = skipWhitespace(rest)
|
||||
|
||||
if rest == "" {
|
||||
break
|
||||
}
|
||||
if rest[0] == ',' {
|
||||
rest = rest[1:]
|
||||
params = rest
|
||||
paramIndex++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return arguments, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue