Repository navigation
fix: quote the exact sat amount to USD-wallet recipients (1.5.3) - #17
Merged
Merged
Conversation
A sats donation to a recipient whose default wallet is USD was converted to
USD cents before invoicing, so the donor was asked for MORE than they entered.
Two errors stacked:
1. Rounding to a whole cent. 1000 sats x 0.077936 c/sat = 77.936c -> 78c.
A cent is ~13 sats at current rates, so this alone moves the total.
2. The stablesats dealer spread. Blink prices those 78c back into sats for
the payer at the immediate-buy price: 1007 sats, ~0.7% above mid.
Donors default to the sats selector, so this applied to every sats donation to
a USD-wallet account.
Blink has a mutation for exactly this case:
lnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipient takes an amount in
satoshis against a USD wallet and credits the recipient its value at creation,
so the donor pays the sats they entered and the recipient still receives USD.
It is public like the other on-behalf-of mutations and carries the same
5-minute expiry (an exchange rate is attached to the amount).
handleDonate now decides the invoice DENOMINATION as well as the amount, and
createInvoice takes an optional 4th argument (amountUnit: 'sats' | 'cents')
that selects the mutation. Omitting it keeps the previous behaviour, so the
existing call shape still works. The sats path also no longer needs
fetchExchangeRate('USD'), removing a network round-trip and its
"USD exchange rate not available" failure mode.
Unchanged: fiat donations still invoice in cents, BTC-wallet recipients are
untouched, the self-custodial (Spark) LNURL path is untouched (Spark is
BTC-only and always invoiced sats), and the memo, public API and DOM are the
same. convertToUsdCents() is left intact and still characterized by
widget-currency.spec.js; it is simply no longer on the sats invoice path, and
a comment at the call site records why.
Verified against the live API on all three paths:
USD wallet + 1000 sats -> BtcDenominated mutation, amount "1000"
-> lnbc10u1... (exactly 1000 sats, was ~1007)
USD wallet + $1 -> cent mutation, amount "100" -> 1291 sats
BTC wallet + 1234 sats -> sats mutation, expiresIn 15 -> 1234 sats
blink-dev-bot
approved these changes
Sep 14, 2026
blink-dev-bot
left a comment
Collaborator
There was a problem hiding this comment.
Consolidated Multi-Model Review
Findings
No consensus functional findings. The new USD-wallet + sats path selects the live public lnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipient mutation with a SatAmount, preserves the exact entered sat amount, and removes the unnecessary exchange-rate fetch. Fiat-to-USD and all BTC-wallet paths remain intact; the optional fourth createInvoice argument preserves the historical three-argument behavior.
Diverging opinions
- GPT recommended
COMMENTbecause of low-risk coverage gaps, two whitespace-only lines, and the lack of an independently reproduced paid smoke test. Kimi and GLM recommendedAPPROVE. I verified the live GraphQL mutation/input contract, confirmed the PR author documented live API checks for all three routes, and found none of the concerns merge-blocking, so the reconciled disposition isAPPROVE.
Test Coverage Gaps
- The focused new spec passed locally: 10 tests covering all wallet/donor-currency combinations, no-rate-fetch behavior, all three mutations, exact variables, expiry values, backward compatibility, and unsupported currency.
- Remote CI is green on this exact head: Unit tests and ESLint both passed.
- The exact-head local full suite ran all 166 tests successfully but exited 1 because Vitest reported two unhandled asynchronous
ReferenceError: document is not definedexceptions from timers in pre-existingtests/widget-callbacks.spec.js:27. Therefore the local full-suite command is not green, despite the assertions passing. - No coverage provider is configured, so changed-line/branch coverage was assessed manually. The substantive changed branches are covered; low-risk omissions are fractional-sat rounding and the analytics
widget_versionliteral. A real paid smoke was not independently repeated during this review; the PR body records live checks for all three affected routes.
Code Smells
js/blink-pay-button.js:1940and:1949add trailing whitespace, causinggit diff --checkto fail. This is cosmetic and non-blocking.createInvoice(walletId, amount, currency, amountUnit)has two stringly-typed denomination inputs that could be mismatched by a future direct caller. The only production caller passes constants consistently, and focused tests cover every current combination, so this is a low-risk maintainability concern rather than a defect.- No additional material SOLID-specific issues were found.
Reviewer notes
- Reviewer A —
openai-codex/gpt-5.6-sol: no functional defects; recommended comment-level follow-up for coverage and whitespace. - Reviewer B —
kimi-coding/kimi-k3: no functional defects; approved. - Reviewer C —
zai/glm-5.3-flash: no functional defects; approved.
Coverage: full multi-model (3 distinct models)
Summary
Disposition: APPROVE. Reviewed head f69b0a3adae495dc22ae56a9b1e600d98685f312. No model substitutions.
🤖 Multi-model review (gpt-5.6-sol + kimi-k3 + glm-5.3-flash) via @blink-dev-bot
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.
The bug
A sats donation to a recipient whose default wallet is USD was converted to USD cents before invoicing, so the donor was asked for more than they entered. Since the currency selector defaults to sats, this applied to every sats donation to a USD-wallet account.
A donor entering
1000was quoted 1007 sats. Two errors stack:The fix
Blink has a mutation for exactly this case:
Its own schema description: "Returns a lightning invoice denominated in satoshis... When invoice is paid the equivalent value at invoice creation will be credited to a USD wallet. Expires after 'expiresIn' or 5 minutes." The donor pays the sats they entered; the recipient still receives USD. It is public like the other on-behalf-of mutations (the resolver takes no auth context), and keeps the same 5-minute expiry.
handleDonatenow decides the invoice denomination as well as the amount, andcreateInvoicetakes an optional 4th argument (amountUnit: 'sats' | 'cents') that selects the mutation:lnInvoiceCreateOnBehalfOfRecipientlnUsdInvoiceCreateOnBehalfOfRecipientlnUsdInvoiceBtcDenominatedCreateOnBehalfOfRecipientOmitting
amountUnitkeeps the previous behaviour, so the existing call shape still works.The sats path also no longer needs
fetchExchangeRate('USD')— one fewer network round-trip, and its "USD exchange rate not available" failure mode disappears.Unchanged: fiat donations still invoice in cents; BTC-wallet recipients are untouched; the self-custodial (Spark) LNURL path is untouched (Spark is BTC-only and always invoiced sats); memo, public API and DOM are identical.
convertToUsdCents()is left intact and still characterized bywidget-currency.spec.js— it is simply no longer on the sats invoice path, and a comment at the call site records why.Verification
Live against
api.blink.svon all three paths (viadebug-test.html, GraphQL bodies captured):amount...BtcDenominated..."1000"lnbc10u1...= exactly 1000 sats (was ~1007)LnUsdInvoiceCreate..."100"centsLnInvoiceCreate..."1234",expiresIn: 15Automated:
npm testgreen - 166 tests, 13 new intests/widget-invoice-denomination.spec.jscovering all four wallet/currency combinations at thehandleDonatelevel (including "no exchange-rate lookup happens for sats -> USD wallet", the regression guard) and all three mutations at thecreateInvoicelevel, plus the legacy 3-argument call.npm run lintreports 0 errors (2 pre-existing warnings, untouched code).Prettier:
js/blink-pay-button.jsalready failsnpm run formatonmain(11 files repo-wide do), so it is deliberately not reformatted here - that would bury a 70-line fix in a 2.8k-line diff. The new spec file is prettier-clean.Notes for the reviewer
widget_versionbumped to 1.5.3 per AGENTS.md.Math.round(1 * 0.0779) = 0cents produced an API error, now a 1-sat invoice is attempted directly.