Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ These are exported via the `elementBadge(el abiparser.Element) string` helper in

### Table output (non-TUI)

The `TablePrinter` in `pkg/abiparser/print.go` also uses Lip Gloss for the static `abi view --output table` command. Follow the same column-sizing pattern and the same colour palette. The `table*Style` vars there mirror the TUI palette — keep them in sync if colours change.
The `TablePrinter` in `pkg/abiparser/print.go` uses Lip Gloss for the static `abi view` command (default output is `table`; use `--output json` for JSON). Columns: **Type**, **Name**, **Inputs**, **Outputs**, **Selector/Topic**, **StateMutability**. Options `--with-input-name` and `--with-output-name` expand parameter names. Follow the same column-sizing pattern and the same colour palette. The `table*Style` vars there mirror the TUI palette — keep them in sync if colours change.

## Running / building

Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,14 @@ abitool abi list --chainid 84532
# 0x808456652fdb597867f38412077A9182bf77359F ⚠ FiatTokenProxy true
# 0xd74cc5d436923b8ba2c179b4bCA2841D8A52C5B5 FiatTokenV2_2 true

# View as coloured table with selectors
abitool abi view -o table 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
# View as coloured table with selectors (default)
abitool abi view 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48

# Show parameter names too
abitool abi view -o table --with-input-name 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
abitool abi view --with-input-name --with-output-name 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48

# View as JSON
abitool abi view -o json 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48

# Filter to events only
abitool abi view -o table -t event 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
Expand Down Expand Up @@ -247,9 +250,10 @@ abitool encode --output json 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 transfer

| Flag | Default | Description |
|---|---|---|
| `-o, --output` | `json` | Output format: `json` or `table` |
| `-o, --output` | `table` | Output format: `json` or `table` |
| `-t, --type` | `all` | Filter by type: `all`, `function`, `event`, `constructor`, `fallback`, `receive` |
| `--with-input-name` | `false` | Show parameter names in table output |
| `--with-input-name` | `false` | Show input parameter names in table output |
| `--with-output-name` | `false` | Show output parameter names in table output |

### `rpc` flags

Expand Down
6 changes: 5 additions & 1 deletion cmd/abitool/abi/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (
)

func init() {
ViewCmd.Flags().StringP("output", "o", "json", "Output format: json or table")
ViewCmd.Flags().StringP("output", "o", "table", "Output format: json or table")
ViewCmd.Flags().StringP("type", "t", "all", "Filter by function type: all, function, event, constructor, fallback, receive")
ViewCmd.Flags().Bool("with-input-name", false, "Display input parameter names in table output")
ViewCmd.Flags().Bool("with-output-name", false, "Display output parameter names in table output")

if err := viper.BindPFlag("abi-view-output", ViewCmd.Flags().Lookup("output")); err != nil {
panic(err)
Expand All @@ -24,6 +25,9 @@ func init() {
if err := viper.BindPFlag("abi-view-with-intput-name", ViewCmd.Flags().Lookup("with-input-name")); err != nil {
panic(err)
}
if err := viper.BindPFlag("abi-view-with-output-name", ViewCmd.Flags().Lookup("with-output-name")); err != nil {
panic(err)
}
}

// viewCmd displays a contract's ABI by its address.
Expand Down
8 changes: 6 additions & 2 deletions internal/contract/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,11 @@ func writeResult(out io.Writer, outputs []abiparser.Output, values []interface{}
}

if asJSON {
b, err := json.MarshalIndent(values, "", " ")
normalized := make([]interface{}, len(values))
for i, v := range values {
normalized[i] = normalizeForJSON(v)
}
b, err := json.MarshalIndent(normalized, "", " ")
if err != nil {
return fmt.Errorf("marshaling result to JSON: %w", err)
}
Expand All @@ -198,7 +202,7 @@ func writeResult(out io.Writer, outputs []abiparser.Output, values []interface{}
if i < len(outputs) && outputs[i].Name != "" {
label = outputs[i].Name
}
_, err := fmt.Fprintf(out, "%s: %v\n", label, v)
_, err := fmt.Fprintf(out, "%s: %s\n", label, formatValue(v))
if err != nil {
return err
}
Expand Down
8 changes: 6 additions & 2 deletions internal/contract/display.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,14 @@ func Print(abi *abiparser.ABI) (string, error) {
case "json":
abiPrinter = abiparser.NewPrettyPrinter(&filtered)
case "table":
abiPrinter = abiparser.NewTablePrinter(&filtered)
var opts []abiparser.TableOption
if viper.GetBool("abi-view-with-intput-name") {
abiPrinter = abiparser.NewTablePrinter(&filtered, abiparser.WithInputNames())
opts = append(opts, abiparser.WithInputNames())
}
if viper.GetBool("abi-view-with-output-name") {
opts = append(opts, abiparser.WithOutputNames())
}
abiPrinter = abiparser.NewTablePrinter(&filtered, opts...)
default:
return "", fmt.Errorf("unsupported ABI print format: %s", viper.GetString("abi-print-format"))
}
Expand Down
178 changes: 178 additions & 0 deletions internal/contract/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright 2025 MqllR. All rights reserved.
// SPDX-License-Identifier: MIT

package contract

import (
"encoding/hex"
"fmt"
"reflect"
"strings"
)

// jsonMarshaler and textMarshaler are local aliases to avoid importing
// encoding/json and encoding in the same file as their consumers.
type jsonMarshaler interface {
MarshalJSON() ([]byte, error)
}

type textMarshaler interface {
MarshalText() ([]byte, error)
}

// formatValue returns a human-friendly string representation of a decoded ABI
// value. []byte and [N]byte values are rendered as 0x-prefixed hex strings.
// Structs (tuples), slices, and arrays are rendered recursively.
func formatValue(v interface{}) string {
if v == nil {
return "<nil>"
}
return formatReflect(reflect.ValueOf(v))
}

func formatReflect(rv reflect.Value) string {
// If the value (or its pointer) implements Stringer, use it directly.
// This must happen before pointer dereferencing so that types like *big.Int
// (which has a pointer-receiver String()) are caught here.
if rv.CanInterface() {
if s, ok := rv.Interface().(fmt.Stringer); ok {
return s.String()
}
}

// Dereference pointers.
for rv.Kind() == reflect.Ptr {
if rv.IsNil() {
return "<nil>"
}
rv = rv.Elem()
}

switch rv.Kind() {
case reflect.Slice:
if rv.IsNil() {
return "<nil>"
}
if rv.Type().Elem().Kind() == reflect.Uint8 {
return "0x" + hex.EncodeToString(rv.Bytes())
}
parts := make([]string, rv.Len())
for i := range parts {
parts[i] = formatReflect(rv.Index(i))
}
return "[" + strings.Join(parts, " ") + "]"

case reflect.Array:
if rv.Type().Elem().Kind() == reflect.Uint8 {
b := make([]byte, rv.Len())
for i := range b {
b[i] = byte(rv.Index(i).Uint())
}
return "0x" + hex.EncodeToString(b)
}
parts := make([]string, rv.Len())
for i := range parts {
parts[i] = formatReflect(rv.Index(i))
}
return "[" + strings.Join(parts, " ") + "]"

case reflect.Struct:
t := rv.Type()
var parts []string
for i := 0; i < rv.NumField(); i++ {
if !t.Field(i).IsExported() {
continue
}
parts = append(parts, formatReflect(rv.Field(i)))
}
return "{" + strings.Join(parts, " ") + "}"

default:
if rv.CanInterface() {
return fmt.Sprintf("%v", rv.Interface())
}
return fmt.Sprintf("%v", rv)
}
}

// normalizeForJSON recursively transforms decoded ABI values so that
// json.Marshal produces human-friendly output:
// - []byte → 0x-prefixed hex string
// - [N]byte → 0x-prefixed hex string
// - structs → map[string]interface{} (preserving ABI field names)
// - slices / arrays → []interface{} with normalized elements
// - everything else → passed through unchanged
func normalizeForJSON(v interface{}) interface{} {
if v == nil {
return nil
}
return normalizeReflect(reflect.ValueOf(v))
}

func normalizeReflect(rv reflect.Value) interface{} {
// If the value knows how to marshal itself for JSON, leave it alone.
// This handles *big.Int (MarshalJSON), common.Address (MarshalText), etc.
if rv.CanInterface() {
v := rv.Interface()
if _, ok := v.(jsonMarshaler); ok {
return v
}
if _, ok := v.(textMarshaler); ok {
return v
}
}

// Dereference pointers.
for rv.Kind() == reflect.Ptr {
if rv.IsNil() {
return nil
}
rv = rv.Elem()
}

switch rv.Kind() {
case reflect.Slice:
if rv.IsNil() {
return nil
}
if rv.Type().Elem().Kind() == reflect.Uint8 {
return "0x" + hex.EncodeToString(rv.Bytes())
}
result := make([]interface{}, rv.Len())
for i := range result {
result[i] = normalizeReflect(rv.Index(i))
}
return result

case reflect.Array:
if rv.Type().Elem().Kind() == reflect.Uint8 {
b := make([]byte, rv.Len())
for i := range b {
b[i] = byte(rv.Index(i).Uint())
}
return "0x" + hex.EncodeToString(b)
}
result := make([]interface{}, rv.Len())
for i := range result {
result[i] = normalizeReflect(rv.Index(i))
}
return result

case reflect.Struct:
t := rv.Type()
m := make(map[string]interface{})
for i := 0; i < rv.NumField(); i++ {
if !t.Field(i).IsExported() {
continue
}
m[t.Field(i).Name] = normalizeReflect(rv.Field(i))
}
return m

default:
if rv.CanInterface() {
return rv.Interface()
}
return nil
}
}
Loading
Loading