diff --git a/cmd/wallet/balance.go b/cmd/wallet/balance.go index 89049e1..af26eca 100644 --- a/cmd/wallet/balance.go +++ b/cmd/wallet/balance.go @@ -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"` diff --git a/cmd/wallet/flexstring_test.go b/cmd/wallet/flexstring_test.go new file mode 100644 index 0000000..feedad2 --- /dev/null +++ b/cmd/wallet/flexstring_test.go @@ -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) + } + } +} diff --git a/cmd/wallet/tokens.go b/cmd/wallet/tokens.go index e453d90..f7115e7 100644 --- a/cmd/wallet/tokens.go +++ b/cmd/wallet/tokens.go @@ -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 {