fix(paywall): block Pay when the balance is known to be too low - #79
Open
pedro-pelicioni wants to merge 3 commits into
Open
fix(paywall): block Pay when the balance is known to be too low#79pedro-pelicioni wants to merge 3 commits into
pedro-pelicioni wants to merge 3 commits into
Conversation
`isBalanceInsufficient` takes the raw on-chain balance and the atomic `amount` from the payment requirement. Both are already atomic units of the same asset, so the comparison needs no decimals lookup and no float round-trip — the existing `Number(formatted) < amount` path silently loses precision above 2^53. Returns `null` when the balance is unknown or the amount is unparseable, so callers can tell "not enough" apart from "don't know yet". Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
It returned the formatted display string, which is the wrong shape for deciding whether a payment can go through. Return `bigint | null` instead so callers can compare against the requirement exactly. The value is only consumed inside this package. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
The guard in `handlePayment` only ran while `tokenBalanceFormatted` was still empty. `useStellarBalance` fetches as soon as a wallet connects, so by the time Pay is clickable that string is populated and the guard is skipped entirely. A wallet holding less than the price went straight to signing and only failed later, at settlement, with an error that does not name the cause. Always compare before paying, re-reading the balance first only when it is not known. Disable Pay while the balance is short and say why: the current balance, the Circle faucet on testnet, and an inline check that re-reads it. An unknown balance leaves Pay enabled — a transient RPC failure should not lock someone out of paying. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
This was referenced Aug 12, 2026
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.
fix(paywall): block Pay when the balance is known to be too low
Branch:
fix/74-balance-guardScreenshots:
harness/shots/before-empty-balance.png→harness/shots/after-empty-balance.pngThe bug
handlePaymentinpackages/paywall/src/browser/StellarPaywall.tsxguards on anempty string:
useStellarBalancefetches the balance in auseEffectas soon asaddressbecomes non-null, so by the time the Pay button is clickable
tokenBalanceFormattedis already populated and the whole block is skipped. Aconnected wallet holding less than the price goes straight to signing and only
fails afterwards, at verify/settle, with an error that does not name the cause.
Reproduce
Connect a wallet with 0 USDC to a route priced at $0.51 and press Pay. The
wallet prompts for a signature; the failure arrives from the facilitator.
Rendering the real component against a stubbed balance of
0nconfirms it:disabledPayfalseInsufficient USDCtrueThe change
balance first only when it is not known.
isBalanceInsufficientrather thanNumber(formatted) < amount. Both sides are already atomic units of the sameasset, so the comparison is exact and independent of the asset's decimals. The
old float path also lost precision above 2^53 — there is a test for that case.
refreshBalancenow resolves tobigint | nullinstead of the formattedstring, so callers can make that comparison. It is only consumed inside this
package.
current balance, the Circle faucet link on testnet, and an inline
"check again" that re-reads the balance.
A
nullbalance — never fetched, or the read failed — leaves Pay enabled.Unknown is not treated as insufficient, so a transient RPC blip cannot lock
someone out of paying.
Tests
8 new cases in
utils.test.tscovering unknown balance, unparseable amount,below/exact/above the price, a zero-price requirement, and the 2^53 precision
case.
pnpm test,pnpm typecheckandpnpm lintall pass.Part of #74.