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
27 changes: 26 additions & 1 deletion cmd/wallet/balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,34 @@ type BalancesResponse struct {
Balances []ChainBalance `json:"balances"`
}

// FlexibleString holds a value the API has returned as either a JSON string
// or a JSON number, normalising both to string.
type FlexibleString string

func (f *FlexibleString) UnmarshalJSON(b []byte) error {
// Per the Unmarshaler contract, null is a no-op.
if string(b) == "null" {
return nil
}
if len(b) > 0 && b[0] == '"' {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
*f = FlexibleString(s)
return nil
}
var n json.Number
if err := json.Unmarshal(b, &n); err != nil {
return err
}
*f = FlexibleString(n.String())
return nil
}

// ChainBalance holds the balance for a single chain.
type ChainBalance struct {
ChainID string `json:"chainId"`
ChainID FlexibleString `json:"chainId"`
ChainName string `json:"chainName"`
NativeBalance string `json:"nativeBalance"`
Tokens []TokenBalance `json:"tokens"`
Expand Down
49 changes: 49 additions & 0 deletions cmd/wallet/flexstring_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package wallet_test

import (
"encoding/json"
"testing"

"github.com/keeperhub/cli/cmd/wallet"
)

func TestFlexibleStringAcceptsBothShapes(t *testing.T) {
// success cases: number and string both normalise to the same string value.
for _, tc := range []struct{ in, want string }{
{`{"chainId":11155111}`, "11155111"}, // current API — returned *json.UnmarshalTypeError before this fix
{`{"chainId":"11155111"}`, "11155111"}, // legacy API — must keep working
{`{"chainId":null}`, ""}, // null is a no-op; zero value preserved
} {
var cb wallet.ChainBalance
if err := json.Unmarshal([]byte(tc.in), &cb); err != nil {
t.Fatalf("ChainBalance %s: %v", tc.in, err)
}
if string(cb.ChainID) != tc.want {
t.Fatalf("ChainBalance %s: got %q want %q", tc.in, cb.ChainID, tc.want)
}

var tok wallet.Token
if err := json.Unmarshal([]byte(tc.in), &tok); err != nil {
t.Fatalf("Token %s: %v", tc.in, err)
}
if string(tok.ChainID) != tc.want {
t.Fatalf("Token %s: got %q want %q", tc.in, tok.ChainID, tc.want)
}
}

// error cases: non-string, non-number JSON values must return an error.
for _, bad := range []string{
`{"chainId":{"x":1}}`,
`{"chainId":true}`,
`{"chainId":[1,2]}`,
} {
var cb wallet.ChainBalance
if err := json.Unmarshal([]byte(bad), &cb); err == nil {
t.Fatalf("ChainBalance %s: expected error, got nil", bad)
}
var tok wallet.Token
if err := json.Unmarshal([]byte(bad), &tok); err == nil {
t.Fatalf("Token %s: expected error, got nil", bad)
}
}
}
10 changes: 5 additions & 5 deletions cmd/wallet/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ type TokensResponse struct {

// Token holds metadata for a single supported token.
type Token struct {
ChainID string `json:"chainId"`
TokenAddress string `json:"tokenAddress"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Decimals int `json:"decimals"`
ChainID FlexibleString `json:"chainId"`
TokenAddress string `json:"tokenAddress"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Decimals int `json:"decimals"`
}

func NewTokensCmd(f *cmdutil.Factory) *cobra.Command {
Expand Down