feat(buy): tell a player they are short of NIM before they open the w… - #17
Merged
Merged
Conversation
…allet A player without enough NIM found out from the wallet, after committing to a payment dialog — and `sendNimWithData` reports whatever comes back as "The payment was declined", which is the wrong sentence: they did not decline, they could not pay. The panel now says so first, with the amount. ## Where the check hangs, and why it matters The mini-app SDK has no balance method — the Nimiq provider exposes accounts, signing, consensus and payments, and nothing that reports a balance (verified against the installed @nimiq/mini-app-sdk@0.1.0 type definitions, not from memory). So the balance comes from a node, via `getAccountByAddress`, through the same RPC client settlement already trusts. `NIMIQ_RPC_URL` is server-side, hence the new route. Reading a balance needs an address, and `listAccounts()` raises a native confirmation. Nimiq's own guidance is that confirmation-requiring calls must be separated by clear user intent rather than queued, so the address prompt rides the GET NIM PRICE tap and the payment stays on the PAY tap — two dialogs, two taps, the price on screen in between. Doing both inside the pay tap would have queued two dialogs, which is the documented anti-pattern. ## Best-effort by construction The check can only ever add a warning. A declined prompt, an unreachable node, a browser with no provider to ask — all leave the shortfall unknown, which renders as nothing and leaves the button exactly as it was. Only a shortfall that was actually measured disables paying, because the wallet, not this endpoint, is the authority on what a player can afford. `/api/nim/balance` answers 503 rather than failing the quote. Balance is returned as a bigint so it is compared against the quoted Luna without either side passing through a float, matching every other amount here. ## Verification lint clean, `tsc --noEmit` clean, 1035 tests pass (17 new). Mutation-tested: accepting a negative balance from the node turns 1 red; dropping the address shape check turns 6 red. The address validation is checked before any request leaves the server, so the endpoint cannot be used to forward arbitrary strings to a third-party RPC. Not verified automatically: the `listAccounts()` prompt and its declined path only exist inside Nimiq Pay, so the two-dialog sequence needs a look on a real device before this is trusted to behave well. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pa7dcWrqWwkwZE2mZWSG1P
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
…allet
A player without enough NIM found out from the wallet, after committing to a payment dialog — and
sendNimWithDatareports whatever comes back as "The payment was declined", which is the wrong sentence: they did not decline, they could not pay. The panel now says so first, with the amount.Where the check hangs, and why it matters
The mini-app SDK has no balance method — the Nimiq provider exposes accounts, signing, consensus and payments, and nothing that reports a balance (verified against the installed @nimiq/mini-app-sdk@0.1.0 type definitions, not from memory). So the balance comes from a node, via
getAccountByAddress, through the same RPC client settlement already trusts.NIMIQ_RPC_URLis server-side, hence the new route.Reading a balance needs an address, and
listAccounts()raises a native confirmation. Nimiq's own guidance is that confirmation-requiring calls must be separated by clear user intent rather than queued, so the address prompt rides the GET NIM PRICE tap and the payment stays on the PAY tap — two dialogs, two taps, the price on screen in between. Doing both inside the pay tap would have queued two dialogs, which is the documented anti-pattern.Best-effort by construction
The check can only ever add a warning. A declined prompt, an unreachable node, a browser with no provider to ask — all leave the shortfall unknown, which renders as nothing and leaves the button exactly as it was. Only a shortfall that was actually measured disables paying, because the wallet, not this endpoint, is the authority on what a player can afford.
/api/nim/balanceanswers 503 rather than failing the quote.Balance is returned as a bigint so it is compared against the quoted Luna without either side passing through a float, matching every other amount here.
Verification
lint clean,
tsc --noEmitclean, 1035 tests pass (17 new).Mutation-tested: accepting a negative balance from the node turns 1 red; dropping the address shape check turns 6 red. The address validation is checked before any request leaves the server, so the endpoint cannot be used to forward arbitrary strings to a third-party RPC.
Not verified automatically: the
listAccounts()prompt and its declined path only exist inside Nimiq Pay, so the two-dialog sequence needs a look on a real device before this is trusted to behave well.Claude-Session: https://claude.ai/code/session_01Pa7dcWrqWwkwZE2mZWSG1P
The hole, and the fix
What this does NOT do / residual risk
Judgement calls
Issues
Closes #
Refs #
Stacking / conflicts
Verification evidence
Remaining ops steps
Checklist
mainlint/typecheck/testpass locally on the current headClosesissue — all met.env.example/ examples / error strings updated for the world this createsmain, lockfile regenerated (never hand-resolved)money-path-checklist.mdrun; payout logic compared against the other side