feat(send): max amount button on the amount screen - #689
Open
islandbitcoin wants to merge 5 commits into
Open
Conversation
Adds a MAX chip beside the balance in the amount input screen's header (issue #512). Outlined green when available, solid green while the entered amount is the computed max, back to outlined the moment the user edits, greyed out on a zero balance. Theme colors only. Max rules, computed in the send flow (the amount screen stays free of payment knowledge via an optional maxAmountButton prop): - intraledger / Flash-to-Flash: full wallet balance, no API call; note says there is no fee between Flash accounts - external invoice / LNURL: balance minus the fee estimate (BTC wallet via fetchBreezFee, USD wallet via the existing fee probes); note states the reserved fee - LNURL destinations clamp to the receiver's maxSendable (reuses the payRequest bounds from #680) - fee estimate failure or timeout fills the full balance and lets the existing pre-validation surface the typed fee error — the tap never blocks The currency toggle keeps the same underlying amount, so the chip stays solid across toggles. i18n: new AmountInputScreen keys in en, English placeholders appended to all 23 locale files for the next translation batch. Tests: computeMaxSendAmount unit suite (12 cases) and an AmountInputScreen component suite for the chip states (6 cases). Fixes #512 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fAxSGEsL2LsMx1uCjAzHH
- BTC-wallet intraledger sends settle via LNURL-pay with a real Breez fee: route them through the fee path (effectiveMaxPaymentType) instead of filling the full balance with a false no-fee note, which dead-ended every BTC-wallet MAX-to-Flash send at confirm time - exclude onchain from the chip: its fee estimate needs the not-yet- selected fee speed and the flow already has its own send-all Max - guard NaN balances: disable the chip while wallets are still loading and treat non-finite balances as zero in computeMaxSendAmount, so the number pad can never fill the literal string NaN - drop stale in-flight max computations: any edit (key press, clear, currency toggle) invalidates the pending fee fetch so a slow resolve can no longer overwrite the user's typed amount or revert a currency toggle; the chip now shows a computing state while the fetch runs - show no note when the reserved fee is zero (a $0.00 reservation note is nonsense) - extract the decision logic into pure functions (maxChipSupports- PaymentType, effectiveMaxPaymentType, resolveRecipientCap, noteForResult) and unit-test them, plus component tests for the computing state and stale-resolve races Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fAxSGEsL2LsMx1uCjAzHH
fetchBreezFee bounds-validates the probe amount against the receiver's LUD-06 limits before preparing, so probing at the full balance was guaranteed to fail whenever the recipient cap binds — the fee came back null and MAX filled the cap-clamped amount with no fee headroom. In the band where balance - cap < fee, confirm-time validation then dead-ended with "amount exceeds balance (amount + fee)". computeMaxSendAmount now derives the probe amount (balance clamped to the recipient cap) and passes it to fetchFee; the details screen probes Breez/IBEX at that amount. Both regimes now yield a confirm-safe MAX: min(balance - fee, cap). Adds unit tests for the probe amount and the cap-within-fee-distance-of-balance band. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fAxSGEsL2LsMx1uCjAzHH
…overdraw On-device repro (iPhone, USD cash wallet, zero-amount invoice): the API reports USD balances in FRACTIONAL cents (109.9346 = $1.099346 at IBEX); MAX offered it un-floored, downstream rounding produced a $1.10 invoice, and IBEX rejected the send — 'insufficient balance. Current Balance: 1.099346 ... invoice amount: 1.100000'. computeMaxSendAmount now floors the spendable balance, the fee-adjusted result, and the recipient cap to whole minor units (no-op for integer sat balances), and a sub-cent balance counts as zero. Four regression tests pin the repro numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fAxSGEsL2LsMx1uCjAzHH
Contributor
Author
|
Device-testing follow-up: MAX on a fractional-cent USD balance ($1.099346) produced a $1.10 invoice and an IBEX insufficient-balance rejection. Fixed in the latest commit — the max computation now floors balance, fee-adjusted result, and recipient cap to whole minor units (sats unaffected), with regression tests pinning the exact repro numbers. Full suite 360/360 green. |
The pad can hold a different currency than the wallet (JMD display over a USD wallet). The fill converted the wallet-unit max into the pad currency and floored there — but the send path converts the pad amount BACK to wallet units and rounds, so a forward-rounding conversion (or a rate tick) can still round-trip above the computed max and overdraw. The fill now steps the pad amount down until its round-trip back to wallet units stays within the computed max. Component test pins the class with a coarse rounding converter (109-sat max, 10-sats-per-cent: naive fill 11 cents → 110 sats overdraw; guarded fill 10 cents). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fAxSGEsL2LsMx1uCjAzHH
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.
Summary
Implements the approved design for a MAX button on Lightning sends: a compact chip in the amount screen's balance header, immediately right of the balance value.
Fixes #512
UX
colors.primary) — no hardcoded hex.Max rules
fetchBreezFee(reuses the resolvedpayRequestfrom #680, no second LNURL round-trip)useIbexFee), probed at the full balancemin(computed max, maxSendable)— reuses the LUD-06 bounds validation shipped in #680; applies to BTC-wallet intraledger sends too since they settle via LNURL-payArchitecture
amount-input-screenstays a generic component with zero payment knowledge. It gains an optionalmaxAmountButton?: { disabled?: boolean; compute: () => Promise<{ amount, note? } | null> }prop — when absent (all other usages of the amount screen), no chip renders and nothing changes.The computation lives in the send flow:
app/screens/send-bitcoin-screen/max-send-amount.ts— pure, dependency-free max rules (computeMaxSendAmount), unit-testable under plain jest (same pattern asfee-errors.tsfrom fix(send): surface real BTC fee errors and validate LNURL receive limits #680).send-bitcoin-details-screen.tsxbuilds the config from the payment detail (intraledger vs external vs LNURL, balances, resolvedreceiverPayRequest/receiverLimits) and threads it:DetailAmountNote→AmountInput→AmountInputModal→AmountInputScreen→AmountInputScreenUI.The filled amount is floored in the primary display currency so it never converts back to more than the computed max in wallet units.
i18n
Four new
AmountInputScreenkeys inapp/i18n/en/index.ts(max,maxNoteIntraledger,maxNoteFeeReserved,maxNoteRecipientCap). English placeholders appended to all 23 locale files underraw-i18n/translations/for the next translation batch; generatedi18n-types.tsandraw-i18n/source/en.jsoncommitted. No literal$before interpolations — currency symbols arrive in the interpolated values.Tests
__tests__/utils/max-send-amount.spec.ts— 12 unit cases: intraledger = balance; external = balance − fee; LNURL clamp (binding, non-binding, on intraledger, on the fallback); fee null / throw / timeout fallbacks; zero balance; fee ≥ balance; negative/NaN fee.__tests__/components/amount-input-screen-max-chip.spec.tsx— 6 component cases in the house style of the fix(send): surface real BTC fee errors and validate LNURL receive limits #680DetailAmountNotetests: no chip without the prop; outlined → tap fills the display-currency amount, goes solid, shows the note; editing returns it to outlined and hides the note; solid across currency toggle; greyed + inert at zero balance.Checks run locally:
yarn test57 suites / 332 tests green,yarn tsc:checkclean, eslint clean on changed lines,yarn check:translation-driftgreen,yarn update-translationsidempotent (generated files committed).🤖 Generated with Claude Code
https://claude.ai/code/session_015fAxSGEsL2LsMx1uCjAzHH