go-ethereum/accounts/abi/human_readable_test.go
mmsqe bf541d1dfd
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
2026-01-15 20:03:55 +08:00

437 lines
11 KiB
Go

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