From 628cd18810ecfed0368e321fae280ef71d267f59 Mon Sep 17 00:00:00 2001 From: Mael Regnery Date: Fri, 24 Apr 2026 11:28:54 +0200 Subject: [PATCH 1/3] fix: make the contract load case-insensitive (#10) --- pkg/storage/abi/local.go | 9 +++++---- pkg/storage/contract/local.go | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/storage/abi/local.go b/pkg/storage/abi/local.go index 389ff1b..0d6d912 100644 --- a/pkg/storage/abi/local.go +++ b/pkg/storage/abi/local.go @@ -6,6 +6,7 @@ package abi import ( "os" "path/filepath" + "strings" ) type Local struct { @@ -26,12 +27,12 @@ func NewLocal(storePath string) (*Local, error) { // Write writes the ABI data for a given contract ID. func (l *Local) Write(id string, data string) error { - return os.WriteFile(filepath.Join(l.path, id), []byte(data), 0644) + return os.WriteFile(filepath.Join(l.path, strings.ToLower(id)), []byte(data), 0644) } // Read reads the ABI data for a given contract ID. func (l *Local) Read(id string) (string, error) { - data, err := os.ReadFile(filepath.Join(l.path, id)) + data, err := os.ReadFile(filepath.Join(l.path, strings.ToLower(id))) if err != nil { return "", err } @@ -40,10 +41,10 @@ func (l *Local) Read(id string) (string, error) { } func (l *Local) Delete(id string) error { - return os.Remove(filepath.Join(l.path, id)) + return os.Remove(filepath.Join(l.path, strings.ToLower(id))) } // GetPath returns the file path of the ABI for a given contract ID. func (l *Local) GetPath(id string) string { - return filepath.Join(l.path, id) + return filepath.Join(l.path, strings.ToLower(id)) } diff --git a/pkg/storage/contract/local.go b/pkg/storage/contract/local.go index 6ea4a3f..ff7d7ed 100644 --- a/pkg/storage/contract/local.go +++ b/pkg/storage/contract/local.go @@ -10,6 +10,7 @@ import ( "iter" "os" "path/filepath" + "strings" ) var ( @@ -40,6 +41,7 @@ func NewLocal(storePath string) (*Local, error) { // Add adds a contract func (l *Local) Add(address string, meta []byte) error { + address = strings.ToLower(address) c, err := l.getContracts() if err != nil { return fmt.Errorf("loading contracts: %w", err) @@ -65,6 +67,7 @@ func (l *Local) Add(address string, meta []byte) error { // Get returns a contract info func (l *Local) Get(address string) ([]byte, error) { + address = strings.ToLower(address) c, err := l.getContracts() if err != nil { return nil, fmt.Errorf("loading contracts: %w", err) @@ -84,6 +87,7 @@ func (l *Local) Get(address string) ([]byte, error) { // Update replaces the metadata for an existing contract. func (l *Local) Update(address string, meta []byte) error { + address = strings.ToLower(address) c, err := l.getContracts() if err != nil { return fmt.Errorf("loading contracts: %w", err) @@ -109,6 +113,7 @@ func (l *Local) Update(address string, meta []byte) error { // Delete deletes a contract func (l *Local) Delete(address string) error { + address = strings.ToLower(address) c, err := l.getContracts() if err != nil { return fmt.Errorf("loading contracts: %w", err) From f411c3c270f8bdfc808c73c9bb31a9a55bd3a8ec Mon Sep 17 00:00:00 2001 From: Mael Regnery Date: Fri, 24 Apr 2026 11:29:23 +0200 Subject: [PATCH 2/3] fix: normalize solidy types (#11) --- internal/contract/call.go | 8 +- internal/contract/format.go | 178 ++++++++++++++++++++++++++++++ internal/contract/format_test.go | 181 +++++++++++++++++++++++++++++++ 3 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 internal/contract/format.go create mode 100644 internal/contract/format_test.go diff --git a/internal/contract/call.go b/internal/contract/call.go index 3593ce5..67856de 100644 --- a/internal/contract/call.go +++ b/internal/contract/call.go @@ -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) } @@ -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 } diff --git a/internal/contract/format.go b/internal/contract/format.go new file mode 100644 index 0000000..d7f9b1a --- /dev/null +++ b/internal/contract/format.go @@ -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 "" + } + 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 "" + } + rv = rv.Elem() + } + + switch rv.Kind() { + case reflect.Slice: + if rv.IsNil() { + return "" + } + 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 + } +} diff --git a/internal/contract/format_test.go b/internal/contract/format_test.go new file mode 100644 index 0000000..3599f1b --- /dev/null +++ b/internal/contract/format_test.go @@ -0,0 +1,181 @@ +// Copyright 2025 MqllR. All rights reserved. +// SPDX-License-Identifier: MIT + +package contract + +import ( + "encoding/json" + "math/big" + "strings" + "testing" +) + +// ---- formatValue ---- + +func TestFormatValue_Bytes(t *testing.T) { + b := []byte{0x00, 0xe0, 0xf7, 0x83} + got := formatValue(b) + want := "0x00e0f783" + if got != want { + t.Errorf("formatValue([]byte): got %q, want %q", got, want) + } +} + +func TestFormatValue_EmptyBytes(t *testing.T) { + got := formatValue([]byte{}) + want := "0x" + if got != want { + t.Errorf("formatValue([]byte{}): got %q, want %q", got, want) + } +} + +func TestFormatValue_FixedBytes4(t *testing.T) { + b := [4]byte{0xde, 0xad, 0xbe, 0xef} + got := formatValue(b) + want := "0xdeadbeef" + if got != want { + t.Errorf("formatValue([4]byte): got %q, want %q", got, want) + } +} + +func TestFormatValue_FixedBytes32(t *testing.T) { + var b [32]byte + b[31] = 0x01 + got := formatValue(b) + want := "0x" + strings.Repeat("00", 31) + "01" + if got != want { + t.Errorf("formatValue([32]byte): got %q, want %q", got, want) + } +} + +func TestFormatValue_Uint256(t *testing.T) { + n := big.NewInt(12345) + got := formatValue(n) + want := "12345" + if got != want { + t.Errorf("formatValue(*big.Int): got %q, want %q", got, want) + } +} + +func TestFormatValue_Bool(t *testing.T) { + if got := formatValue(true); got != "true" { + t.Errorf("formatValue(true): got %q, want %q", got, "true") + } + if got := formatValue(false); got != "false" { + t.Errorf("formatValue(false): got %q, want %q", got, "false") + } +} + +func TestFormatValue_SliceOfInts(t *testing.T) { + s := []uint32{1, 2, 3} + got := formatValue(s) + want := "[1 2 3]" + if got != want { + t.Errorf("formatValue([]uint32): got %q, want %q", got, want) + } +} + +func TestFormatValue_StructWithBytes(t *testing.T) { + type call struct { + Target string + AllowFail bool + CallData []byte + } + v := call{ + Target: "0xABCD", + AllowFail: true, + CallData: []byte{0x01, 0x02}, + } + got := formatValue(v) + want := "{0xABCD true 0x0102}" + if got != want { + t.Errorf("formatValue(struct): got %q, want %q", got, want) + } +} + +func TestFormatValue_SliceOfStructsWithBytes(t *testing.T) { + type call struct { + Data []byte + } + s := []call{{Data: []byte{0xAA}}, {Data: []byte{0xBB}}} + got := formatValue(s) + want := "[{0xaa} {0xbb}]" + if got != want { + t.Errorf("formatValue([]struct): got %q, want %q", got, want) + } +} + +func TestFormatValue_Nil(t *testing.T) { + got := formatValue(nil) + want := "" + if got != want { + t.Errorf("formatValue(nil): got %q, want %q", got, want) + } +} + +// ---- normalizeForJSON ---- + +func TestNormalizeForJSON_Bytes(t *testing.T) { + b := []byte{0x00, 0xe0} + got := normalizeForJSON(b) + want := "0x00e0" + if got != want { + t.Errorf("normalizeForJSON([]byte): got %v, want %v", got, want) + } +} + +func TestNormalizeForJSON_FixedBytes(t *testing.T) { + b := [2]byte{0xca, 0xfe} + got := normalizeForJSON(b) + want := "0xcafe" + if got != want { + t.Errorf("normalizeForJSON([2]byte): got %v, want %v", got, want) + } +} + +func TestNormalizeForJSON_SliceOfBytes(t *testing.T) { + s := [][]byte{{0x01}, {0x02}} + got := normalizeForJSON(s) + b, err := json.Marshal(got) + if err != nil { + t.Fatalf("json.Marshal: %v", err) + } + want := `["0x01","0x02"]` + if string(b) != want { + t.Errorf("normalizeForJSON([][]byte) JSON: got %s, want %s", b, want) + } +} + +func TestNormalizeForJSON_StructWithBytes(t *testing.T) { + type row struct { + Addr string + Data []byte + } + v := row{Addr: "0xABCD", Data: []byte{0xde, 0xad}} + got := normalizeForJSON(v) + + m, ok := got.(map[string]interface{}) + if !ok { + t.Fatalf("expected map, got %T", got) + } + if m["Data"] != "0xdead" { + t.Errorf("Data field: got %v, want 0xdead", m["Data"]) + } + if m["Addr"] != "0xABCD" { + t.Errorf("Addr field: got %v, want 0xABCD", m["Addr"]) + } +} + +func TestNormalizeForJSON_Nil(t *testing.T) { + if got := normalizeForJSON(nil); got != nil { + t.Errorf("normalizeForJSON(nil): got %v, want nil", got) + } +} + +func TestNormalizeForJSON_BigInt(t *testing.T) { + n := big.NewInt(999) + got := normalizeForJSON(n) + if got != n { + t.Errorf("normalizeForJSON(*big.Int): got %v, want %v", got, n) + } +} From 0ed409d5db29047b50e6dc06fe97fb2ce9dca5fc Mon Sep 17 00:00:00 2001 From: Mael Regnery Date: Fri, 24 Apr 2026 15:56:16 +0200 Subject: [PATCH 3/3] fix: display function outputs for abi view command (#12) --- AGENTS.md | 2 +- README.md | 14 ++++++---- cmd/abitool/abi/view.go | 6 ++++- internal/contract/display.go | 8 ++++-- pkg/abiparser/print.go | 52 ++++++++++++++++++++++++++++-------- 5 files changed, 62 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8c2c292..3ca5208 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/README.md b/README.md index f98f1ee..b896a37 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/cmd/abitool/abi/view.go b/cmd/abitool/abi/view.go index a43421b..099b27b 100644 --- a/cmd/abitool/abi/view.go +++ b/cmd/abitool/abi/view.go @@ -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) @@ -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. diff --git a/internal/contract/display.go b/internal/contract/display.go index 16e413b..52367e0 100644 --- a/internal/contract/display.go +++ b/internal/contract/display.go @@ -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")) } diff --git a/pkg/abiparser/print.go b/pkg/abiparser/print.go index 3ad536b..be712aa 100644 --- a/pkg/abiparser/print.go +++ b/pkg/abiparser/print.go @@ -31,6 +31,7 @@ var ( tableMutDefaultStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6272A4")) tableNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#F8F8F2")) tableInputStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#8BE9FD")) + tableOutputStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#50FA7B")) ) type PrettyPrinter struct { @@ -78,21 +79,28 @@ func (p *PrettyPrinter) Print() (string, error) { } // TablePrinter prints the ABI in a table format. -// The columns are: Type, Name, Inputs, StateMutability +// The columns are: Type, Name, Inputs, Outputs, Selector/Topic, StateMutability type TablePrinter struct { - a *ABI - WithInputNames bool + a *ABI + WithInputNames bool + WithOutputNames bool } -type tableOptions func(*TablePrinter) +type TableOption func(*TablePrinter) -func WithInputNames() tableOptions { +func WithInputNames() TableOption { return func(tp *TablePrinter) { tp.WithInputNames = true } } -func NewTablePrinter(abi *ABI, opts ...tableOptions) *TablePrinter { +func WithOutputNames() TableOption { + return func(tp *TablePrinter) { + tp.WithOutputNames = true + } +} + +func NewTablePrinter(abi *ABI, opts ...TableOption) *TablePrinter { table := &TablePrinter{a: abi} for _, opt := range opts { @@ -109,14 +117,15 @@ func (p *TablePrinter) Print() (string, error) { var b bytes.Buffer - headers := []string{"Type", "Name", "Inputs", "Selector/Topic", "StateMutability"} - colWidths := []int{len(headers[0]), len(headers[1]), len(headers[2]), len(headers[3]), len(headers[4])} + headers := []string{"Type", "Name", "Inputs", "Outputs", "Selector/Topic", "StateMutability"} + colWidths := []int{len(headers[0]), len(headers[1]), len(headers[2]), len(headers[3]), len(headers[4]), len(headers[5])} // Collect raw (unstyled) rows to measure column widths. type rawRow struct { typ string name string inputs string + outputs string selector string mut string } @@ -145,10 +154,11 @@ func (p *TablePrinter) Print() (string, error) { typ: string(element.Type), name: element.Name, inputs: p.formatInputTypes(element.Inputs), + outputs: p.formatOutputTypes(element.Outputs), selector: id, mut: string(element.StateMutability), } - cells := []string{r.typ, r.name, r.inputs, r.selector, r.mut} + cells := []string{r.typ, r.name, r.inputs, r.outputs, r.selector, r.mut} for i, c := range cells { if len(c) > colWidths[i] { colWidths[i] = len(c) @@ -184,8 +194,9 @@ func (p *TablePrinter) Print() (string, error) { b.WriteString(cell(styledType(r.typ), colWidths[0])) b.WriteString(cell(tableNameStyle.Render(r.name), colWidths[1])) b.WriteString(cell(tableInputStyle.Render(r.inputs), colWidths[2])) - b.WriteString(cell(styledSelector(r.selector), colWidths[3])) - b.WriteString(cell(styledMutability(r.mut), colWidths[4])) + b.WriteString(cell(tableOutputStyle.Render(r.outputs), colWidths[3])) + b.WriteString(cell(styledSelector(r.selector), colWidths[4])) + b.WriteString(cell(styledMutability(r.mut), colWidths[5])) b.WriteByte('\n') } @@ -243,3 +254,22 @@ func (p *TablePrinter) formatInputTypes(inputs []Input) string { return fmt.Sprintf("(%s)", strings.Join(types, ",")) } + +func (p *TablePrinter) formatOutputTypes(outputs []Output) string { + types := []string{} + + for _, out := range outputs { + if p.WithOutputNames && out.Name != "" { + types = append(types, fmt.Sprintf("%s %s", out.Name, out.Type)) + continue + } + + types = append(types, out.Type) + } + + if len(types) == 0 { + return "" + } + + return fmt.Sprintf("(%s)", strings.Join(types, ",")) +}