Skip to content

feat: pay onchain address from spending balance#1101

Draft
coreyphillips wants to merge 2 commits into
feat/savings-swapfrom
feat/send-swap
Draft

feat: pay onchain address from spending balance#1101
coreyphillips wants to merge 2 commits into
feat/savings-swapfrom
feat/send-swap

Conversation

@coreyphillips

Copy link
Copy Markdown
Contributor

Description

Stacked on feat/savings-swap. Targets that branch, not master.

Sending to an onchain address with an amount larger than the Savings balance currently dead-ends on an "insufficient savings" toast, even when the Spending balance could cover it. This adds a fallback: pay the address out of Spending through a Boltz reverse swap, using the recipient's address as the swap claim address so Boltz's payout lands on them directly.

The review screen stays as it is apart from one extra item. "From" reads Spending, and Confirming in gives way to a single Service fee cell covering Boltz's percentage plus both miner fees, mirroring SavingsConfirmScreen.

  • New SwapRepo owns the swap business logic and returns Result<T>: quoting, bounds, and the pay-then-await-claim orchestration. TransferViewModel is left on its existing direct BoltzService use.
  • boltzCreateReverseSwap prices from the Lightning invoice amount, while a send starts from what the recipient must receive, so the quote inverts Boltz's forward formula. It uses ceil on the percentage fee to match ReverseFees::boltz in boltz-client, not the roundToLong the savings quote uses. The service fee shown is invoiceSat - recipientSat.
  • Boltz reports the amount it will lock before anything is paid, so a swap that would short the recipient is abandoned before payInvoice is ever called.
  • SendMethod gains a SWAP value. The amount screen caps input at what a swap can deliver, and the send falls back to Savings automatically once the amount fits there again.
  • Gated behind the existing isSavingsSwapEnabled dev toggle via BoltzService.isSwapEnabled(), so nothing changes for users yet.

Not in scope, worth a follow-up:

  • The payment shows in Activity as a Lightning SENT row for the gross amount with no recipient address; LightningActivity has no address field. A synthetic onchain row keyed on the claim txid would never confirm, since ldk-node never sees that transaction.
  • BoltzSwap carries no intent field, so a send swap and a savings swap are indistinguishable in history.
  • The claim fee rate is the global one boltzStartSwapUpdates was started with, so the send has no per-payment speed choice. The Fee and speed cell is not editable for a swap.

QA Notes

Manual QA is mainnet only: Env.isSwapSupported is network == Network.BITCOIN, and Boltz's mainnet reverse minimum is around 50k sat. Use a wallet with an open channel holding well above that and a near-empty onchain balance, then enable Dev Settings -> Enable Savings Swap.

  1. Scan an external onchain address and enter an amount above Savings but within Spending. The amount screen should accept it and show Spending as available.
  2. Review should read "From: Spending" with a single Service fee cell next to Fee and speed. Swipe to pay.
  3. Verify on an explorer that the claim transaction pays the recipient at least the amount entered. Dev Settings -> Swaps lists the swap and its claim txid.
  4. Regression, dev flag off: an over-balance onchain send must still show InsufficientSavingsToast and go nowhere. Normal in-balance onchain sends and normal Lightning sends must be unchanged.

Tests: new SwapRepoTest.kt covers the inverse fee math (exact round-trip, ceil boundary, min/max limits, spendable balance, routing), the pre-payment abort gate, and the claimed/error/timeout outcomes. AppViewModelSendFlowTest.kt gains coverage for switching into and out of a swap send, rejecting amounts a swap cannot deliver, and dispatching the confirm to SwapRepo.

Ran ./gradlew compileDevDebugKotlin testDevDebugUnitTest detekt --rerun-tasks, all green.

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.

1 participant