Skip to content

feat: add boltz savings swap#632

Open
coreyphillips wants to merge 7 commits into
masterfrom
feat/savings-swap
Open

feat: add boltz savings swap#632
coreyphillips wants to merge 7 commits into
masterfrom
feat/savings-swap

Conversation

@coreyphillips

@coreyphillips coreyphillips commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

This PR adds a swap-based path for transferring spending balance to savings, so Lightning funds can move on-chain through a Boltz reverse swap instead of only by closing a channel. It ports the equivalent Android feature (synonymdev/bitkit-android#1081) to iOS, including its later refinements.

Description

  • Adds a Boltz reverse swap flow to the transfer-to-savings journey. The confirm screen shows a live quote (amount to savings, network fee, service fee, and the amount you will receive) and lets the user proceed with a swap or fall back to closing the channel.
  • The flow is gated behind a new "Enable Savings Swap" toggle under Dev Settings while its UI is being finalised, so nothing changes for users until it is switched on. Dev Settings also gets a Swaps section grouping that toggle with the existing swaps list.
  • Swaps are offered on mainnet only. Boltz's testnet deployment is deprecated and regtest expects a local backend, so on every other network, and whenever the dev toggle is off, the journey behaves exactly as it did before this PR: the swipe closes the channel, with no quote, fees, slider, or extra close action, and the updates stream never starts.
  • Closing a channel is the default and a priced quote is the only thing that upgrades a transfer to a swap. Without one, for any reason (unsupported network, Boltz unreachable, or an amount below the swap minimum), the swipe closes the channel rather than doing nothing. The quote fetch is bounded so a hanging Boltz request cannot leave the swipe stuck loading.
  • Holds back a small reserve for Lightning routing fees so the hold invoice can actually be paid.
  • Waits for the on-chain claim during the progress step. If the claim has not landed within the wait window (30s), the screen shows a "settling on-chain" state so the transfer reads as committed and completing in the background rather than as a false success.
  • Resumes any pending swaps when the wallet starts and refreshes balances automatically once a swap lands on-chain. The updates stream is managed in the wallet view model: it only starts where swaps are supported and enabled, retries with backoff until it starts (a paid swap needs the stream to broadcast its claim), stops with the node, and restarts on the next wallet start. The progress screen also ensures the stream is running before starting a swap.
  • Claims reverse swaps as soon as their lockup hits the mempool (acceptZeroConf) instead of waiting for a confirmation, for faster settlement.
  • Adds a Swaps section under Dev Settings to list swaps and claim a reverse swap, backed by a new service that wraps the Boltz module. The manual claim is a recovery tool, so it is offered for any unclaimed reverse swap that has not reached a terminal state, including a swap still marked created locally. A stalled updates stream can leave the cached status behind while the funds are already claimable on-chain, and the chain is the source of truth.
  • Adds a reusable continuous amount slider component used by the swap amount controls, and an optional loading state on the swipe-to-confirm button so it cannot be swiped while a quote is still being fetched.
  • Bumps bitkit-core to 0.5.2, which adds the Boltz module this flow depends on and the acceptZeroConf option on the updates stream.

Linked Issues/Tasks

Android counterpart: synonymdev/bitkit-android#1081

Screenshot / Video

Insert relevant screenshot / recording

QA Notes

Manual Tests (swap path needs mainnet plus Dev Settings → Enable Savings Swap)

  • 1. With a spending balance, Transfer to Savings → Savings Confirm: quote shows amount to savings, network fee, service fee, and you'll receive.
  • 2. Savings Confirm → drag the amount slider: quote recomputes for the new amount.
  • 3. Savings Confirm with a balance below the swap minimum: no quote or fees show and the swipe closes the channel instead, with the separate close action hidden.
  • 4a. Savings Confirm → swipe to confirm → Savings Progress: completes after the on-chain claim, or shows the "settling on-chain" state if the claim is still landing.
    • 4b. After claim: savings balance reflects the swapped funds without a manual sync.
  • 5. regression: Savings Confirm → Close channel instead: falls back to the channel-close path.
  • 6. regression: Start the app with a pending swap: swap resumes and balance updates once it lands on-chain.
  • 7a. Dev Settings → Swaps → tap a swap → Swap Details: Claim shows for any unclaimed reverse swap that is not in a terminal state, including one still marked created locally, the recovery case.
    • 7b. Swap Details of a claimed, expired, refunded, or failed swap, or any submarine swap: no Claim button.
  • 8. regression: On a regtest or testnet build, Transfer to Savings → Savings Confirm: no quote, fees, slider, or close-instead action, and the swipe closes the channel as before.
  • 9. regression: With Dev Settings → Enable Savings Swap off, Transfer to Savings → Savings Confirm: no quote, fees, slider, or close-instead action, and the swipe closes the channel as before.
  • 10. Dev Settings → Swaps section: toggling Enable Savings Swap persists across an app restart.

Automated Checks

  • Unit tests added: savings swap quote fee breakdown, claimable status gating with permissive recovery semantics, the swap gate (unsupported off mainnet, off without the dev flag, and the dev flag alone not enabling it off mainnet), and the transfer resolving to the channel-close path without a quote, in BitkitTests/SavingsSwapTests.swift.
  • Ran locally against bitkit-core 0.5.2: simulator Debug build succeeds and SavingsSwapTests pass.
  • No changelog fragment: the flow is behind a dev toggle, so it is not user-facing yet.

@coreyphillips coreyphillips self-assigned this Jul 16, 2026
coreyphillips and others added 2 commits July 17, 2026 09:00
Bring the iOS savings-swap flow to parity with bitkit-android #1081:

- Bump bitkit-core 0.5.1 -> 0.5.2 and pass acceptZeroConf so reverse swaps
  claim once the lockup hits the mempool
- Make BoltzSwap.isClaimable permissive so the manual-claim recovery tool
  stays reachable when the updates stream stalls
- Add a settling progress state instead of showing success while the on-chain
  claim is still landing
- Move the updates-stream orchestration into WalletViewModel with retry/backoff
  and ensure it is running before starting a swap
- Shorten the claim wait to 30s and let the swipe close the channel when the
  amount is below the swap minimum
@coreyphillips
coreyphillips requested a review from piotr-iohk July 20, 2026 17:39
@coreyphillips
coreyphillips marked this pull request as ready for review July 20, 2026 17:39
@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a Boltz reverse-swap path for moving Lightning funds to on-chain savings. The main changes are:

  • Live swap quotes with network and service fee details.
  • A bounded claim wait with a background settling state.
  • Automatic swap resumption, claim handling, and balance refresh.
  • Manual swap inspection and claim recovery in Dev Settings.
  • A reusable continuous amount slider and swap-focused tests.
  • An update to bitkit-core 0.5.2.

Confidence Score: 5/5

This looks safe to merge.

  • The payment call returns after initiation, so it does not delay the bounded claim wait.
  • Swap events are subscribed before payment starts.
  • No blocking issue related to the settlement timeout remains in the updated code.

Important Files Changed

Filename Overview
Bitkit/ViewModels/TransferViewModel.swift Adds reverse-swap quotes, payment initiation, and a bounded wait for claim events.
Bitkit/ViewModels/WalletViewModel.swift Adds swap-update startup, retry, shutdown, and balance refresh handling.
Bitkit/Services/BoltzService.swift Wraps the Boltz API and forwards swap lifecycle events through asynchronous streams.
Bitkit/Views/Transfer/SavingsConfirmView.swift Adds swap quotes, amount selection, and the channel-close fallback.
Bitkit/Views/Transfer/SavingsProgressView.swift Runs the selected transfer path and shows pending claims as settling on-chain.
BitkitTests/SavingsSwapTests.swift Covers quote calculations and manual claim eligibility.

Reviews (2): Last reviewed commit: "refactor: simplify savings swap payment ..." | Re-trigger Greptile

Comment thread Bitkit/ViewModels/TransferViewModel.swift
Paying a Boltz hold invoice only settles once the swap is claimed on-chain,
so awaiting the payment before starting the claim timeout could leave the
progress screen spinning when a lockup is delayed. Run the hold-invoice
payment in a background task and drive the outcome off the bounded claim
wait, which now runs concurrently. A timeout means the transfer is settling
in the background; a payment that fails to initiate is still surfaced.
…g send

LDK returns a payment id as soon as a bolt11 payment is initiated; it does
not block until a hold invoice settles, so awaiting the payment before the
bounded claim wait does not gate the claim timeout. Restore the direct
structure so a payment that fails to initiate surfaces immediately instead
of after the claim timeout, and document the semantics so the ordering is
not mistaken for a stuck flow.
@piotr-iohk
piotr-iohk marked this pull request as draft July 21, 2026 06:08
@piotr-iohk
piotr-iohk marked this pull request as ready for review July 21, 2026 06:08
Ports the latest bitkit-android #1081 refinement. Closing a channel is now
the default and a priced quote is the only thing that upgrades a transfer to
a swap, so the swipe always commits the transfer instead of going inert when
no quote is available.

Gate swaps to mainnet: Boltz's testnet deployment is deprecated and regtest
expects a local backend, so on every other network the confirm screen shows
no quote, fees, slider, or close-instead action and the swipe closes the
channel exactly as it did before swaps existed.

Skip the limits fetch entirely where swaps are unsupported, bound it to 15s
elsewhere so a hanging Boltz request cannot leave the swipe stuck loading,
and drop the quote error and amount-too-low state now that every failure
simply leaves the quote nil. Also skip starting the swap updates stream on
unsupported networks, and add an optional loading state to the swipe button
so it cannot be swiped past a quote that is still being fetched.
Ports the bitkit-android #1081 dev gate. The savings swap UI is not final
yet, so the flow now stays off until "Enable Savings Swap" is switched on
under Dev Settings. With it off the confirm screen fetches no quote and
resolves to the channel-close path, and the swap updates stream never
starts, so the journey is exactly what shipped before swaps existed.

Groups the toggle with the existing swaps list under a new Swaps section in
Dev Settings, and drops the changelog fragment now that the flow is not
user-facing.
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