mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-04-03 00:25:56 +00:00
* 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
437 lines
11 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|