Skip to content

fix: settle spending max on an affordable balance - #1179

Open
jvsena42 wants to merge 10 commits into
masterfrom
fix/899-max-transfer-to-spending
Open

fix: settle spending max on an affordable balance#1179
jvsena42 wants to merge 10 commits into
masterfrom
fix/899-max-transfer-to-spending

Conversation

@jvsena42

@jvsena42 jvsena42 commented Aug 24, 2026

Copy link
Copy Markdown
Member

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. sats and the rendered text are always written together in AmountInputViewModel, 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:265 still 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:

Environment Client balance Service fee Direction
Production 261,561 → 265,726 3,072 → 3,109 fee rises with client balance
Staging / regtest 264,680 → 266,478 1,800 → 1,798 fee falls with client balance

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 — maxChannelSizeSat moved 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's feeSat must not exceed it:

adb shell "run-as to.bitkit.dev cat files/logs/<newest>.log" \
  | grep -E "Estimating order fee|Buying channel|BT confirm|Failed to prepare"

Manual Tests

  • 1. Transfer → Spending → tap Max → Continue → swipe: order funds, and the logged feeSat is at or below the available amount.
  • 2. regression: Transfer → Spending → tap ¼ → Continue → swipe: still funds normally.

Automated Checks

  • Unit test added in TransferViewModelTest.kt: reproduces the reported production quote pair and asserts the max stays within what the order will cost. Confirmed it fails against master.
  • Unit test added in 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.
  • Local just compile, just test, and just lint all pass, with only pre-existing detekt findings in untouched files.

jvsena42 and others added 2 commits August 24, 2026 15:07
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jvsena42 jvsena42 self-assigned this Aug 25, 2026
@jvsena42 jvsena42 changed the title fix: cap spending max at quoted lsp fee balance fix: verify max transfer against a live fee quote Aug 25, 2026
@jvsena42 jvsena42 changed the title fix: verify max transfer against a live fee quote fix: settle spending max on an affordable balance Aug 25, 2026
@jvsena42
jvsena42 marked this pull request as ready for review August 25, 2026 14:43
@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown

Greptile Summary

The PR settles the maximum Savings-to-Spending amount against live LSP fee quotes and adds a confirmation-time affordability guard.

  • Re-quotes candidate balances against the channel split used for order creation.
  • Rejects confirmed amounts that exceed the current funding budget.
  • Adds coverage for rising and falling LSP fee curves, unavailable quotes, and unaffordable orders.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

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]
Loading

Reviews (2): Last reviewed commit: "fix: add a guard on confirm amount check..." | Re-trigger Greptile

Comment thread app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
@jvsena42
jvsena42 marked this pull request as draft August 25, 2026 14:55
@jvsena42
jvsena42 marked this pull request as ready for review August 25, 2026 15:03
@jvsena42
jvsena42 requested review from ovitrif and piotr-iohk and removed request for ovitrif and piotr-iohk August 25, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Unable to transfer max amount to spending

1 participant