fix: settle spending max on an affordable balance - #1179
Open
jvsena42 wants to merge 10 commits into
Open
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6 tasks
jvsena42
marked this pull request as ready for review
August 25, 2026 14:43
Greptile SummaryThe PR settles the maximum Savings-to-Spending amount against live LSP fee quotes and adds a confirmation-time affordability guard.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt | Settles the advertised maximum against live order-fee quotes and verifies affordability before creating an LSP order. |
| app/src/test/java/to/bitkit/viewmodels/TransferViewModelTest.kt | Adds focused regression coverage for both fee-curve directions, quote failures, round exhaustion, and confirmation-time funding checks. |
| app/src/main/res/values/strings.xml | Adds the user-facing insufficient-savings error shown when confirmation-time affordability validation fails. |
| changelog.d/next/1179.fixed.md | Documents that maximum Savings-to-Spending transfers now reserve their service fee. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Calculate spendable on-chain balance] --> B[Reserve mining fee]
B --> C[Estimate LSP fee]
C --> D[Derive candidate client balance]
D --> E{Candidate plus quoted fee affordable?}
E -->|Yes| F[Advertise maximum]
E -->|No| G[Reduce candidate by shortfall]
G --> H[Re-quote candidate and channel split]
H --> E
F --> I[User confirms amount]
I --> J[Load current funding budget and quote]
J --> K{Order affordable?}
K -->|Yes| L[Create LSP order]
K -->|No| M[Show insufficient savings error]
Reviews (2): Last reviewed commit: "fix: add a guard on confirm amount check..." | Re-trigger Greptile
jvsena42
marked this pull request as draft
August 25, 2026 14:55
…at the wallet can fund
jvsena42
marked this pull request as ready for review
August 25, 2026 15:03
jvsena42
requested review from
ovitrif and
piotr-iohk
and removed request for
ovitrif and
piotr-iohk
August 25, 2026 16:00
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.
Fixes #899
This PR fixes the maximum Savings → Spending transfer failing with an insufficient funds error.
Description
What the user hit
A user moving their whole savings to spending got "The available funds are insufficient to complete the given operation", and the transfer never went through. They retried, and ended up with two Lightning channels they could receive into but not spend from — a spending balance of 0, after paying fees twice.
Why the transfer failed
Sizing a max transfer takes two fee quotes. The first prices the whole available balance; the second re-prices at the balance left after that fee. The max was then set to the available balance minus the second quote — a balance that quote never actually priced.
The order built at that max can cost more than the user has. Here it was one satoshi: the order came to 265,727 against 265,726 available. That single sat pushed the transfer past the drain shortcut into an ordinary send it could not fund, which surfaced the raw Lightning error above.
Why they ended up with a 0 spending balance
After the failure they left the transfer flow, returned to the wallet home screen, and started the transfer again. The order that followed carried a client balance of zero, which buys receiving capacity only: the channel opens entirely on the LSP's side, the user pays just the liquidity fee, and their spending balance stays 0. They did this twice, paying 1,809 and 2,345 sats, which is why they could receive but not send.
The zero was on screen, not submitted behind a populated field.
satsand the rendered text are always written together inAmountInputViewModel, and a zero amount renders as the placeholder, so the amount screen and then the confirm screen both showed 0 before the swipe.This PR does not change any of that. Continuing at zero is a supported purchase, and
SpendingAmountScreen.kt:265still enables Continue while the amount is 0. What the fix removes is the reason to retry, since the max transfer no longer fails. Two related gaps are tracked separately: Max and ¼ stay tappable while the limits load and will set the amount to zero, and fiat entry with no exchange rate can submit zero behind a typed amount.No funds were lost. Their savings stayed on-chain minus roughly 4,400 sats of fees.
The fix
An earlier attempt capped the max at the balance the fee was quoted for, assuming the second quote can never exceed the first. That assumption does not hold everywhere. The service fee moves with how a channel is split between the client and LSP sides, and the direction differs by deployment:
Where the fee rises, the derived max sits above the quoted balance and capping is enough. Where it falls, it sits below, the cap does nothing, and the order still overshoots — confirmed on staging, where both the old code and the capping attempt produced an order one satoshi over.
Since that direction belongs to the LSP deployment rather than the app, it can differ between environments and change without a client release. So the max is now verified instead of assumed: check that the candidate balance covers its own order fee, and where it does not, re-quote at the shortfall-adjusted balance. This settles within two rounds. Where the fee rises the first check passes immediately and no extra quote is made, so that path keeps its current latency.
The same sizing path backs the hardware wallet transfer limits, so both are covered.
Preview
first-transfer.mp4
second-transfer.mp4
max-spending-reached.mp4
QA Notes
Reproducing the original failure needs the whole on-chain balance transferred at once — the gap only appears at exactly Max.
Regtest cannot reproduce #899. Under the regtest fee behaviour the vulnerable window is about 2 sats wide versus roughly 37 in production, and the regtest channel limits drift on their own —
maxChannelSizeSatmoved 1,388,280 → 1,403,872 within one session, shifting the target. Treat regtest as regression coverage only; the unit tests are the gate for the fix itself.The reliable oracle on device is the log, not the screen.
Estimating order fee for spendingSats=is the available amount, and the order'sfeeSatmust not exceed it:Manual Tests
feeSatis at or below the available amount.regression:Transfer → Spending → tap ¼ → Continue → swipe: still funds normally.Automated Checks
TransferViewModelTest.kt: reproduces the reported production quote pair and asserts the max stays within what the order will cost. Confirmed it fails against master.TransferViewModelTest.kt: covers the opposite fee direction with four live quotes, asserting the settled max funds its own order. Confirmed it fails against the capping attempt, so the two tests cover distinct fee directions rather than overlapping.just compile,just test, andjust lintall pass, with only pre-existing detekt findings in untouched files.