Skip to content

feat: channel balances move to spark at low fees - #1774

Draft
ovitrif wants to merge 6 commits into
feat/breez-spark-lightningfrom
feat/move-to-spark
Draft

ovitrif wants to merge 6 commits into
feat/breez-spark-lightningfrom
feat/move-to-spark

Conversation

@ovitrif

@ovitrif ovitrif commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1768

Twin: synonymdev/bitkit-ios#1212
Dependency: #1697

This PR adds Move to Spark: a one-time sheet and an action that pays the balance of a Lightning channel to the user's own Spark wallet over the channel, then offers to close the emptied channel. Blocktank is closing its channels, and moving the balance over Lightning costs only the routing fee, where a close followed by a deposit costs on-chain and claim fees and 3 confirmations. It stacks on #1697 and stays a draft until that merges.

Description

  • Adds MoveToSparkRepo, which quotes the move against a new amountless Spark invoice: it starts from what the channels can send, leaves room for the routing fee LDK estimates, and caps the payment's routing fee at the fee shown.
  • Adds LightningRepo.payInvoiceOverLdk and estimateLdkRoutingFeesForAmount, so the move always goes over the channels and never falls back to Spark; the existing shouldPayWithSpark routing is unchanged.
  • Adds the Move to Spark screen: quote, swipe to move, then "Moved to Spark" with Close Connection, which opens the existing cooperative close; after a partial move it offers Move The Rest instead.
  • Shows "Amount too large" with a Move-half button when a payment cannot route, down to a 1,000 sat minimum, so a large balance can move in parts.
  • Adds a timed sheet that tells channel holders Blocktank is closing its connections and opens Move to Spark; it shows once, at the top priority, and is checked again once channels and Spark have loaded after a cold start.
  • Adds a Move To Spark button to Spending and Lightning Connections while a channel can still send at least 1,000 sats to a usable Spark wallet.
  • Keeps the move's own send and receive from raising the usual payment sent, received or failed notices, since the screen reports the outcome.
  • Fits the smallest screens: button labels stay on one line at 360 dp and on an iPhone SE (Later / Move Now, Cancel / Move Half, Later / Move Rest, Later / Close), the buttons sit at the bottom with the illustration taking the flexible height, and headlines put the white words first and the shorter accent last, as master's screens do.

Out of Scope

  • Activity: the move shows as one received payment, since the LDK send and the Spark receive share a payment hash; how a move should appear in Activity is left to the design pass.
  • Design: an AI-made first version from existing components; a Figma design and designer review follow.

Design

N/A — no design available.

Screens

Pixel 6 emulator at 360 dp width, in flow order. Bitkit draws dark only, its system bars included, and no screen here shows a keyboard or system sheet.

Post-update sheet Spending entry Connections entry Finding a route
Too large Confirm: amount and fee Moving Done: part moved
Done: close offer Close confirmation After the close Could not move

Preview

The recordings below predate the small-screen pass, so they show earlier labels and headlines; the Screens section shows the current ones.

Sheet, too large and a partial move on the local Spark regtest (2x):

Sheet on Home, Later Not again after relaunch Too large, half, Move The Rest
f-a1-sheet.mp4
f-a1b-once.mp4
f-a4-too-large.mp4

Moving the rest, closing, and a wallet without a channel (2x):

Move the rest from Spending Close afterwards No channel, no sheet
f-a2-move.mp4
f-a3-close.mp4
f-a5-no-channel.mp4

QA Notes

Setup

  • A local Spark regtest: spark-sdk regtest/local (docker compose -f regtest/local/docker-compose.yml up), which runs the operators, the SSP with its Lightning node, an LNURL server, data sync, electrs and a chain API.
  • A dev build pointed at that stack. This PR has no switch for it; the tested build carried a local-only patch that sets, in SparkService, the stack's Spark config (from the stack's spark-config.json), its chain service, LNURL domain and sync URL, and sets Electrum to the stack's electrs. Missing prerequisite for reviewers: that patch, or a shared way to point dev builds at a local Spark stack.
  • In the app, open Lightning Connections, Manual Setup, and open a channel to the stack's SSP Lightning node (the node URI the stack prints) from a funded Savings balance; wait for it to confirm.

Journeys

  • J1: new move-to-spark.xml — sheet once, entry button, move with routing fee, close afterwards, nothing for a wallet without a channel

Manual Tests

  • 1. With a route that cannot carry the balance, open Move To Spark → "Amount too large" offers to move half; repeating it moves a part that routes, and Done offers Move The Rest — needs a route limit the local stack cannot set (tested with a local route-limit hook)

Automated Checks

  • added MoveToSparkRepoTest.kt — quote fits the routing fee under the channel limit, grows back once the real fee is known, keeps a smaller target, maps no route to too large, refuses with nothing to move or Spark not ready
  • updated AppViewModelSendFlowTest.kt — wires the new sheet and repository

Evidence

Tested at 927472a4e (this PR's feature commit; later commits change only the journey text), dev regtest build with the local-only patch from Setup, Pixel 6 API 35 emulator, one 400,000 sat channel to the SSP:

  • J1 sheet: on a cold launch the sheet shows on Home; Later closes it, and after a relaunch it does not show again.

  • 1: with a local 150,000 sat route-limit hook, ₿ 392,993 and ₿ 196,496 showed "Amount too large"; ₿ 98,248 moved with a ₿ 0 routing fee, and Done offered Move The Rest.

  • J1 move: without the hook, Move To Spark in Spending moved the rest, ₿ 296,719, with a ₿ 0 routing fee (direct channel to the SSP); Done offered Close Connection.

  • J1 close: Close Connection, then Close, returned to Lightning Connections with no open connection, and Spark held ₿ 394,967, the sum of both moves.

  • J1 no channel: a new wallet showed no Move To Spark button and no sheet.

  • The Lightning Connections entry button was driven on an earlier build of this change: it moved ₿ 77,967 and closed.

  • just compile, just lint and just test pass.

  • Small-screen pass at 62526d3bf: the sheet, too large, confirm, partial and full Done screens captured at 360 dp width; every button label stays on one line and the buttons sit at the bottom. just compile, just lint and just test pass.

  • Rebased onto the rebuilt Spark base ab1b09d14 (without the emergency exit) at 967292c09: just compile, just lint and just test pass. The device runs above ran before this rebase; the Move to Spark code is the same apart from checking isReady where it checked isUsable, which included the removed exit state.

Gaps

  • Only direct channels to the SSP were tested; a multi-hop route with a real routing fee was not set up locally.
  • Activity shows the move as a single received payment (see Out of Scope).
  • Mainnet runs on phones come at polish time.

Models used

  • Planning/scoping: claude-opus-5-5 (reasoning: medium)
  • Implementation: claude-opus-5-5 (reasoning: medium)
  • Review: Not performed

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 967292c (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

@ovitrif

ovitrif commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 95f993e89: the Move To Spark button in Lightning Connections now sits below the open channels instead of above the Spark section, next to the channel it moves and clear of the Spark row and detail that the Spark exit work is changing on the base branch. Layout only; compile and lint pass.

@ovitrif

ovitrif commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 84f49451a..62526d3bf, a small-screen pass on the Move to Spark screens:

  • Button labels stay on one line at 360 dp: Later / Move Now, Cancel / Move Half, Later / Move Rest, Later / Close (the amount left the button; Close replaces Close Connection).
  • Bottom buttons sit at the screen bottom; the illustration takes the flexible height and shrinks so the sheet copy stays whole.
  • Headlines put the white words first and the shorter accent last, as master's screens do, with the purple accent of the Lightning screens.
  • All 15 locales follow the same headline shape and short labels.

Screenshots at 360 dp are in Preview. just compile, just lint and just test pass.

@ovitrif
ovitrif force-pushed the feat/breez-spark-lightning branch from ab34526 to ab1b09d Compare November 5, 2026 02:00
@ovitrif
ovitrif force-pushed the feat/move-to-spark branch from 62526d3 to 967292c Compare November 5, 2026 02:10
@ovitrif

ovitrif commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto the rebuilt Spark base ab1b09d14, which drops the emergency exit, and force-pushed with a lease (this PR is a draft): 62526d3bf → 967292c09.

  • ContentView and LightningConnectionsViewModel conflicts kept the new base's lines and only the Move to Spark route, entry and canMoveToSpark additions.
  • 967292c09 adapts to the removed exit: Spark readiness is SparkState.isReady (the removed isUsable also excluded a running exit), and the screen formats amounts itself instead of using the exit's helper.

just compile, just lint and just test pass. The device runs and screens in the description were taken before the rebase on the same Move to Spark code.

This branch has not been deployed

No deployments
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