feat: pay onchain address from spending balance - #645
Draft
coreyphillips wants to merge 2 commits into
Draft
Conversation
Sending to an onchain address for more than the savings balance currently dead-ends on an "insufficient savings" toast, even when spending could cover it. Add a fallback that pays 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. Ports bitkit-android #1101. - New SendSwapService owns the swap business logic: quoting, bounds, and the pay-then-await-claim orchestration. TransferViewModel keeps its own direct BoltzService use for the transfer to savings. - 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 ceils the percentage fee to match ReverseFees in boltz-client, not the round-to-nearest the savings quote uses. - Boltz reports the amount it will lock before anything is paid, so a swap that would short the recipient is abandoned before the invoice is paid. The hold invoice amount is checked against the quote, and the recipient address is re-validated for network, before paying. - isSwapSend is orthogonal to selectedWalletToPayFrom, which stays onchain: the recipient is still an address and the whole flow keys off the onchain invoice. - The amount screen raises its cap to what a swap can deliver so an amount above savings can be entered, then falls back to savings automatically once the amount fits there again while keeping the bounds so it can be raised again. - The review screen reads "From: Spending" with a single Service fee cell in place of "Confirming in", mirroring the savings confirm screen, and skips coin selection since a swap spends no utxos. - Per-keystroke pricing is sequence guarded so a slow re-price cannot overwrite the current quote, and an unpriceable swap surfaces an error instead of wedging the review screen. - Gated behind the existing isSwapEnabled dev flag, 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; the claim tx paying the recipient is one ldk-node never sees. - 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 the updates stream was started with, so the send has no per-payment speed choice and the fee cell is not editable. Tests cover the inverse fee math (round-trip, ceil boundary, min/max limits, spendable balance, degenerate fee schedules), the savings-versus-swap resolution including keeping bounds on revert, and the swaps-disabled fallback.
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.
Description
Stacked on
feat/savings-swap. Targets that branch, notmaster.Sending to an onchain address for 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.
Ports bitkit-android #1101. The review screen stays as it is apart from one change: "From" reads Spending, and "Confirming in" gives way to a single Service fee cell, mirroring
SavingsConfirmView.SendSwapServiceowns the swap business logic: quoting, bounds, and the pay-then-await-claim orchestration.TransferViewModelkeeps its own directBoltzServiceuse for the transfer to savings.boltzCreateReverseSwapprices from the Lightning invoice amount, while a send starts from what the recipient must receive, so the quote here inverts Boltz's forward formula. It ceils the percentage fee to matchReverseFeesin boltz-client, not the round-to-nearest the savings quote uses. The service fee shown isinvoiceSat - deliverSat.isSwapSendis orthogonal toselectedWalletToPayFrom, which stays.onchain: the recipient is still an address and the whole send flow keys off the onchain invoice. This avoids a thirdWalletTypecase that ~50 non-exhaustive== .lightningsites would silently mishandle.isSwapEnableddev toggle (mainnet plus "Enable Savings Swap" in Dev Settings), so nothing changes for users yet. No changelog fragment, matching how the savings swap was gated inad93ba38.Not in scope, worth a follow-up:
BoltzSwapcarries no intent field, so a send swap and a savings swap are indistinguishable in history.boltzStartSwapUpdateswas 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.isSwapSupportedisnetwork == .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 -> Swaps -> Enable Savings Swap.Tests: new
SendSwapTests.swiftcovers the inverse fee math (exact round-trip, ceil boundary, min/max limits, spendable balance, a range of fee schedules, a degenerate non-finite schedule), the savings-versus-swap resolution including keeping bounds on revert, and the swaps-disabled fallback.Ran the
BitkitTestsunit suite: all green apart from the two pre-existingUtxoSelectionTestscases that depend on the Blocktank regtest faucet (currently returning 404 for everyone, unrelated to this change).