fix(wallet): accept numeric chainId in balance and token responses - #87
Conversation
479190e to
31f8ca2
Compare
|
Context for reviewers — this was found during a fresh Linux/WSL2 onboarding run on v0.13.1 (2026-08-05). Full teardown with repro steps and all findings: https://github.com/Makabeez/carrydesk/blob/main/onboarding-teardown.md Five other friction points hit during the same session (not all PRs, some just doc gaps):
Items 1, 3, and 4 are covered in the companion doc PR: #88. SSH marketplace blocker: KeeperHub/claude-plugins#5. Also amended since the initial push: dropped |
31f8ca2 to
024b16a
Compare
suisuss
left a comment
There was a problem hiding this comment.
Welcome - first PR here. Briefly how we review: the diff gets read independently of the description first, then the two are compared. Conventions are in the repo's contributing guide.
What this changes
Three files. cmd/wallet/balance.go adds type FlexibleString string with a pointer-receiver UnmarshalJSON: if the first byte is " it delegates to json.Unmarshal into a string, otherwise it assigns the raw bytes verbatim and returns nil. ChainBalance.ChainID and Token.ChainID change from string to FlexibleString. cmd/wallet/flexstring_test.go is new and table-tests the two chainId shapes for both structs.
Does it match the description
Matches. The stated problem is real - on main, kh w balance and kh w tokens fail with json: cannot unmarshal number into Go struct field ... of type string when the API returns chainId as a number, and this fixes that.
Blocking
-
cmd/wallet/balance.go:36- the else branch accepts every non-string JSON value, not just numbers, becauseencoding/jsonhandsUnmarshalJSONthe complete next value. JSONnullhas first byten, soChainIDbecomes the four-character string"null". -> The API returns{"chainId":null,...}andkh w tokensprintsnullin the CHAIN column, while--jsonemits"chainId": "null". Onmainthe same input leaves the field as"", becauseencoding/jsonskipsnullfor a plainstring. That is a regression, and it is also contrary to theUnmarshalercontract, which asks thatUnmarshalJSON([]byte("null"))be a no-op. -> Return early without assigning whenstring(b) == "null". -
cmd/wallet/balance.go:35-37- the same branch swallows objects, arrays and booleans.{"chainId":{"x":1}}returns nil withChainIDset to{"x":1};[1,2]andtruelikewise. -> A version-skewed or malformed response that previously produced a clear decode error now prints{"x":1}in the CHAIN column and hands it tojqconsumers as a chain id. -> Accept only a JSON number: checkb[0]is-or a digit, or unmarshal intojson.Numberand take itsString(). That keeps the fix and restores the error for everything else.
Mechanical - actionable as-is
cmd/wallet/flexstring_test.go:9- the comment// current API - panics before this fixis wrong. Onmainthe decode returns a*json.UnmarshalTypeError; nothing panics. Worth correcting since it is the only description of the bug in the test.cmd/wallet/flexstring_test.go- the table covers the two shapes that work and none of the ones above. Cases fornull, an object and a bool would have caught both blockers.cmd/wallet/balance.go:24- there is noMarshalJSON, so--jsonnow always emitschainIdas a string even when the input was a number. Not a regression againstmainfor numeric input, since that errored out entirely, but it locks in a string-only output contract that disagrees withkh chain list --json, which emits a number - a divergencedocs/kh_chain_list.md:10-13already calls out as a footgun.kh w tokens --json | jq 'select(.chainId == 11155111)'will match nothing.cmd/wallet/balance.go:36-strings.TrimSpaceis dead on this path;encoding/jsonpasses the literal with no surrounding whitespace.cmd/wallet/flexstring_test.go:1- declaredpackage walletwhile every other test in the directory ispackage wallet_test. Both compile; matching the neighbours is the smaller surprise.
Needs a decision
- Should
FlexibleStringalso marshal back as a number, or is string-out the intended CLI contract? (a) AddMarshalJSONemitting a bare number when the value is numeric, costs a little complexity and makes--jsonoutput shape depend on input; (b) keep string-out and alignkh chain list --jsonto match, costs a breaking change on that command; (c) accept the divergence and document it, costs thejqfootgun staying. This is the fix-selection question behind the third mechanical item.
Verdict
Changes requested - the numeric case is fixed correctly, but the else branch is wider than the problem and turns null and malformed values into printable chain ids.
The auth label on this PR is an automated file-path heuristic firing on "tokens"; nothing credential-related is touched here.
kh w balance and kh w tokens fail against the current API with: json: cannot unmarshal number into Go struct field ChainBalance.balances.chainId of type string The API returns chainId as a JSON number; the structs expect a string. Adds FlexibleString, which unmarshals either shape, so older responses keep working. Reproducible on v0.13.1 with and without --json/--chain.
024b16a to
867d754
Compare
|
Full onboarding teardown now published: https://github.com/Makabeez/carrydesk/blob/main/onboarding-teardown.md |
|
Confirmed still reproducible on v0.14.0 (just released): |
What changed since the last review
Also dropped from this push per the contributor's comment: the BlockingNone remaining. Mechanical - actionable as-isNone remaining. Needs a decision
VerdictBoth blockers from the prior review are fixed with tests covering the failure cases; the only open item is the string-vs-number output contract, which is a maintainer call, not a defect. |
|
Decision: add |
Problem
kh w balanceandkh w tokenscrash on v0.13.1 against the current API:The API now returns
chainIdas a JSON number;ChainBalanceandTokenboth declare it asstring. Go'sencoding/jsonhard-errors on this mismatch. Reproducible on every invocation, with and without--jsonor--chain.Fix
Adds
FlexibleString— a type alias forstringwith a customUnmarshalJSONthat accepts both a quoted string and a bare number, normalising both tostring. No behaviour change for callers that already receive a stringchainId.Changed files:
cmd/wallet/balance.go—FlexibleStringdefinition +ChainBalance.ChainIDtype changecmd/wallet/tokens.go—Token.ChainIDtype change (reusesFlexibleString)cmd/wallet/flexstring_test.go— new: failing-before / passing-after test covering both JSON shapesbalance_test.go,tokens_test.go—gofmtalignment fixes (widened type name shifted column)Test
Discovered during a fresh Linux/WSL2 onboarding run on v0.13.1 (2026-08-05).