You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: channel balances move to spark at low fees - #1774
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).
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.
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.
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
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
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.
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
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.LightningRepo.payInvoiceOverLdkandestimateLdkRoutingFeesForAmount, so the move always goes over the channels and never falls back to Spark; the existingshouldPayWithSparkrouting is unchanged.Out of Scope
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.
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):
f-a1-sheet.mp4
f-a1b-once.mp4
f-a4-too-large.mp4
Moving the rest, closing, and a wallet without a channel (2x):
f-a2-move.mp4
f-a3-close.mp4
f-a5-no-channel.mp4
QA Notes
Setup
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.SparkService, the stack's Spark config (from the stack'sspark-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.Journeys
move-to-spark.xml— sheet once, entry button, move with routing fee, close afterwards, nothing for a wallet without a channelManual Tests
Automated Checks
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 readyAppViewModelSendFlowTest.kt— wires the new sheet and repositoryEvidence
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 lintandjust testpass.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 lintandjust testpass.Rebased onto the rebuilt Spark base
ab1b09d14(without the emergency exit) at967292c09:just compile,just lintandjust testpass. The device runs above ran before this rebase; the Move to Spark code is the same apart from checkingisReadywhere it checkedisUsable, which included the removed exit state.Gaps
Models used
claude-opus-5-5(reasoning:medium)claude-opus-5-5(reasoning:medium)