mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-02-26 15:47:21 +00:00
Merge 47d937b3f1 into 2a45272408
This commit is contained in:
commit
fa43e2bc6b
4 changed files with 1431 additions and 75 deletions
701
accounts/abi/human_readable_test.go
Normal file
701
accounts/abi/human_readable_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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