Repository navigation
Conversation
aed1f9f to
82d6014
Compare
bb09eeb to
bf0e9cd
Compare
9d5e276 to
1883357
Compare
856799e to
7897f16
Compare
4534525 to
a9aa36f
Compare
|
QA: 🟢 Pay a Lightning invoice from iOSThe iPhone paid a plain Lightning invoice from the Pixel through Spark with no channel, and the Pixel received it at once. Ran on a9aa36f.
🟢 Restore Spark exit dataExit data backs up as its own category, and after a wipe the iPhone imported 16 leaves of Spark exit data before using Spark; its balance and claimed deposit came back. Ran on a9aa36f.
🟢 Move Spending to SavingsTransfer to Savings withdrew the Spark balance for a 1,470 sat fee and 4,917 sats arrived in Savings; it was listed as a plain received payment, fixed in 93aec43 to show as a transfer. Ran on a9aa36f.
🟢 Claim a depositSpark claimed the 7,500 sat deposit after 3 confirmations, keeping 198 sats, and Spending showed 7,302. Ran on an earlier head of this PR, before the 27 Oct rebase. 🟢 Transfer to SpendingTransfer to Spending sent 9,632 sats to Spark and Spending showed it as an incoming transfer. The confirm screen estimated a 99 sat claim fee where claims so far took 198 to 297; 7388559 now shows the most Spark may keep. Ran on f25edfe, a build of #1145 on an earlier head of this PR.
🔴 Pay a Paykit request over SparkThe iPhone paid the Pixel's 204 sat Paykit request over Spark and it arrived at once, but the iPhone stayed on Payment Pending: the payment settled inside the send call, before the send screen started watching for it. Fixed in 735e972 and synonymdev/bitkit-android@024b6bb85, not rerun on a device yet. Ran on 56d7adf, a build of #1145 on 9ddd96b. An earlier attempt kept loading for minutes while Paykit's shared state stayed locked; since a5d94ed the sheet stops at once and says the contact's payment details aren't ready, and the lock itself is a Paykit issue reported separately.
|
6104b95 to
23b8b66
Compare
|
Removed from the 3.1.0 milestone: this draft is the deliverable of a finished Spark experiment and does not ship in 3.1.0. |
|
Retargeted this draft to master before merging its base, #1127, so deleting that branch would not close it. |
735e972 to
a77cb76
Compare
|
Rebuilt this branch on master after the Blocktank sunset (#1127). Master removed the old Transfer to Spending flow, so Spark now has its own amount, confirm and deposit screens, reached from Funding and the transfer buttons while Spark is ready. The deposit screen offers Breez's early claim at the fee the provider quotes for that deposit. The history was rewritten into new commits, so the shas under Evidence predate the rebuild. This PR stays a draft. |
3808d9d to
92a0376
Compare
|
Pushed ae8f1b3: the Spark connection row on Lightning Connections now opens its detail from a tap anywhere on the row. Its button uses the plain style, which hit-tests only drawn content, so taps in the empty middle between the name and the chevron, or between the amounts, did nothing. Exact-point taps on an iOS 26.5 simulator: on 3eb00ec those two points did nothing while the name and the bar opened the detail; with the fix all four open it. The same gap on master's pending rows is #1202. |
Spending keeps Lightning without channels through a Spark wallet: transfer to spending funds it from Savings or another wallet, Lightning payments and Paykit run on it, and transfer to savings picks channels, Spark or both. Breez data sync and the Lightning address server are off, and the Spark exit data is backed up.
ffba7c7 to
0eed9c2
Compare
|
Force-pushed: this PR is now one commit, |






Twin: synonymdev/bitkit-android#1697
This PR keeps Lightning in Bitkit after Blocktank stopped selling channels: on top of the Blocktank sunset (#1127), Spark becomes the easy option where Transfer to Spending now leads to manual setup. Savings to Spending funds a Breez SDK on Spark wallet, and Lightning sends and receives fall back to it whenever no channel can carry the payment. Existing channels keep working until they close, and ldk-node keeps running the on-chain wallet.
Emergency exit, leaving Spark on-chain without its operators, moved to #1217, stacked on this PR.
Description
Transfer to Spending on Spark
SparkTransferFlowmodel drives the screens; the Spark-only amount, confirm and deposit screens are gone.HwSendCoordinator.signAndBroadcastalready signs and broadcasts an on-chain send from the device to any address for Send, so the device pays the Spark deposit address, recorded as a transfer in the hardware wallet's activity.Spark wallet and Lightning
breez-sdk-spark-swift0.27.2, exact) withSparkManager, so that a Spark wallet starts beside the node from the same recovery phrase and passphrase. Spark derives its own keys onm/8797555', so there is no new phrase to back up.realTimeSyncServerUrl = nil) and the LNURL server (lnurlDomain = nil) in Spark's config on every network, so the app sends no traffic to Breez's data-sync orbreez.tipsLNURL servers, and no Lightning address is offered (aa01843).LightningServicethat only the app sets, so the notification extension never loads Breez.BREEZ_API_KEYfrom an optional, gitignoredSecrets.xcconfigthrough a newBitkit.xcconfigbase config; copySecrets.xcconfig.templateand paste the key (README, Build secrets). Without the key, mainnet builds keep Spark off; regtest builds use Lightspark's hosted regtest, which needs no key. TestFlight is archived locally (Docs/RELEASE.md §2), so the releaser adds the file first; a Release-only build phase fails the build when the key is empty, so Spark can't ship switched off, and Docs/RELEASE.md and the release command carry the step.Paykit on Spark
Exit data backup
SPARK_EXIT, encrypted like the other backups. It is exported again when Spark connects and whenever the SDK reportsUnilateralExitStateChanged, and on restore it is imported right after Spark connects, without contacting the operators, so a wallet restored on a new device can still exit on-chain while the operators are unreachable. The category shows in Backup settings only where Spark runs.Small screens
Out of Scope
Risks for review
SPARK_EXIT; upstream VSS accepts request bodies up to 1 GiB by default, but a large wallet's upload against Synonym's VSS is untested.250 × sat/vB + 750sats.CLAIM_TX_SIZE_VBYTES,deposits.rs), so it is a flat cost that weighs heavily on small transfers. The confirm screen shows it as Spending fee and shows what reaches Spending after it.Design
N/A — no design available. Figma has no Spark or unlimited-receiving state for the Lightning Connections header, so No limit uses the same style and arrow as the spending figure beside it; @aldertnl, a design for this state is still open.
Preview
Transfer to Spending on a regtest simulator build of f63e613, with Savings funded on Bitkit's staging regtest. Spark never sees a staging-chain deposit, so the Instant claim states (adding, repriced, unavailable) are staged with a local hook that is not in this PR; Later and "hasn't seen" are the real app.
t1-transfer2x.mp4
t2-intro-instant2x.mp4
t3-intro-repriced2x.mp4
Setting-up after 5 minutes when Spark never lists the deposit, and Instant unavailable (staged).
Case 1 on an earlier regtest simulator build without channels (4b58c22): the Spending invoice comes from Spark.
ios-receive-spark2x.mp4
Lightning Connections and the Spark detail on f63e613, with 99,901 sats from Lightspark's regtest faucet: the claimed deposit with its 99 sat claim fee and Transfer To Savings.
c1-connections2x.mp4
Transfer to Savings selection at 674f0da against Spark's hosted regtest, with channels to Blocktank's staging regtest node: the Advanced list with every row on and with nothing selected, a partial pick, and both a channel and Spark.
1-default-and-nothing-selected2x.mp4
2-partial-one-channel2x.mp4
3-both-channel-and-spark2x.mp4
The single-item cases: Spark alone with no channels, and two channels with no Spark balance.
4-spark-only-no-channels2x.mp4
5-channels-only-no-spark2x.mp4
UI pass on an iPhone SE (375 pt) and an iPhone 17 simulator, every changed screen rendered with stubbed state by a local hook that is not in this PR. On the SE, the Sign with your device headline was cut to one line, and the Spark detail's buttons scrolled away with its content. Content now scrolls above the buttons, which stay at the bottom. These Spark detail shots predate the split, so they still show Emergency exit, which is now in #1217.
The same screens after, on the SE.
And on the iPhone 17, where the device picture sits centred in the space under the speed rows.
The Spark failure headline now fits two lines on the SE.
The Learn More headline now accents one word, like master's two-line headlines, and its text scrolls above the Spending balance bar.
Screens
Every screen the PR adds or changes, on an iPhone 17 simulator in Bitkit's dark theme, rendered with stubbed state by a local hook that is not in this PR.
Transfer to Spending
Transfer to Spending from a hardware wallet
Transfer to Savings
Lightning Connections
The Spark detail shot predates the split and still shows Emergency exit, now in #1217.
QA Notes
Setup
BREEZ_API_KEYinSecrets.xcconfig(copySecrets.xcconfig.template), and small amounts.https://regtest-mempool.us-west-2.sparkinfra.net/api(the SDK's own regtest default,sdk_builder.rs). It lacks/fee-estimates, which ldk-node needs to start, so a dev build cannot point its node at Spark's chain as-is.Journeys
transfer/spark-transfer-to-spending.xml— with Spark ready, the transfer buttons and Funding open the spending intro, then the amount and confirm screens; the confirm shows the four tiles and the Instant and Later rows, and Learn More opens the Spark copy. Clarified for the restored flow; earlier results ran on the Spark-only screens.transfer/transfer-unavailable-sheet.xml— now needs Spark not running, since a ready Spark replaces the sheet.amount-limits/transfer-spending-over-max.xml— the amount over Max snaps to it with the toast, and delete still works.transfer/spending-confirm-amount-change.xml— a changed amount re-prices the confirm screen, and a cheaper fee at the swipe pays at once.hardware-wallet/transfer-to-spending.xml— the device pays the Spark deposit address and the transfer lists once in the hardware wallet's activity.hardware-wallet/transfer-to-spending-over-max.xml— the hardware amount caps on both entries.hardware-wallet/passphrase-transfer-to-spending.xml— a hidden wallet asks for its passphrase again after the session drops, and refuses a wrong one.transfer/savings-select-funds.xml— each channel and Spark are rows on Select funds, all on by default; the confirm amount and "Spark fee" follow the pick, and nothing selected disables Continue. Allow about 20 s for "Spark fee". Adapted from Android: a picked selection shows "Transfer all" in place of "Advanced", so reaching the list again takes "Transfer all" first.Manual Tests
Imported Spark exit databefore Spark is used, and Spark's balance and deposits match — needs two installs.Automated Checks
SparkTransferFlowTests.swift— the claim decision (Instant within and over the ceiling, Standard, automatic, Declined, Later), the claim outcomes, the poll timeout, a listed-then-gone deposit, Waiting that keeps polling, Pay after a reprice, Wait after a reprice, and the quote's tilesAppCacheDataTests.swift— the transfer intro flags round-trip through the backup againSparkClaimOfferTests.swift— a quote maps to the standard claim, an automatic early claim within the cap, or a choice above the cap or with automatic claiming off, and each claim outcome maps to claimed, waiting, repriced or declinedChannelFreeRailTests.swift— rail events before the node callback are delivered, Paykit proofs and private Paykit sets read Spark payments, QuickPay reconciles them, a Spark send honours the routing fee limit, a fee over the limit or a failed estimate is a refusal before sending, Spark's sendable amount leaves room for its fee andcanSenduses it, and Spark becoming ready triggers the Paykit refresh oncePaykitPaymentProofServiceTests.swift— a Spark refusal before sending, bare or wrapped, ends the payment as failed and removes its proofSparkLightningSendTests.swift— a Lightning invoice with no channels opens the payment when Spark can pay, is rejected without Spark, and a Spark send returns before completionSparkTransferActivityTests.swift— Spark deposits and withdrawals are marked as transfers, failed ones are not, and withdrawals are listed with the fee Spark keptSparkExitBackupTests.swift—SPARK_EXITuploads the exported state under its own key, a restored state is staged until Spark connects, and one from another network is skippedSavingsSelectionTests.swift— every channel and Spark move by default, Spark alone or channels alone, a deselected Spark is not withdrawn and Spark alone closes no channel, a partial channel pick, a channel no longer ready is dropped, nothing selected is empty, and committing resets the pick to allSparkConfigTests.swift— Spark connects with no Breez data-sync server and no LNURL domain on every networkSparkWithdrawalTests.swift— a withdrawal quote holds for the same address and balance until 30 s before it expires and an estimate never does, the fee is priced once per balance to one address and again for a new balance, a slow quote gives up and prices again next time, a failed Spark withdrawal shows as failed and names whether channels moved, and a withdrawal's receipt is matched by its txidSavingsSwapTests.swift— the commit takes whether Spark is withdrawnChannel-only audit
Every place that assumed channels, checked for a Spark wallet with no channels and compared with Android:
AppViewModelscan check: rejected every Lightning invoice without channels before considering Spark; now pays through Spark when its balance covers the amount, and the toast counts Spark's balance.SendSheetinsufficient toasts: count Spark's balance.SendSheetchannel wait: no longer waits for a reconnecting channel when Spark can pay the amount.SparkManagersend: returns as soon as the payment starts, so its completion event reaches the send screen and Paykit settlement; it showed "Payment Pending" before. Same fix on Android.LightningService.emitChannelFreeRailEvent: Spark events that arrive before the node's event callback is set are kept and delivered once it is; they were dropped.PrivatePaykitService+Invoicessettled and attempted payment sets: include Spark payments, so a private invoice Spark received isn't reused and a contact invoice Spark paid isn't paid again.PaykitLightningPaymentProofLookup: settles a Paykit proof from Spark's record when its completion event was missed, also before the node lists its payments.QuickPayPaymentCoordinatorreconcile: includes Spark payments.SparkManageractivity: recorded through the same path as LDK payments, so a Paykit contact and seen state survive Spark syncs.TransferViewModelswap to Savings: also withdraws Spark's balance when Spark is selected, as the confirm screen counts it.PaykitAllowanceManager.lightningReady: Spark ready counts as ready.LightningService.sendon Spark: enforces the caller's routing fee limit (Paykit auto-pay's 1%) against Spark's fee estimate.LnurlPayAmount: caps and shows the larger rail balance, not the sum of both.AppStatusHelper.channelsStatus: a ready Spark wallet shows the Lightning connection as ready.GiftLoading: gift claims open a Blocktank channel, which Blocktank no longer sells.nodeLifecycleStategates on send and receive: the node runs beside Spark, as on Android.WalletViewModelbalance derivation while the node isn't up: keeps the last balances, which include Spark.maxSendLightningSats, receive invoice creation and tabs, Paykit Lightning endpoint publishing, the Spending card transfer button, the LNURL withdraw invoice.preferSpark) has no Lightning details, so it maps to no event; mainnet sends are Lightning payments.Test results
QA:
Tested on an iPhone 17 Pro simulator (iOS 27.0) against Spark's hosted regtest.
Device tests skipped⚠️ :
🟢 Transfer to Spending with Later, Transfer to Spending with Instant, Claim states after the swipe
Transfer to Spending with Later
J1 and 2 at f63e613: Savings › Transfer To Spending opened the Spending intro, the amount screen's 25%, Max and unit toggle worked, the confirm screen showed the four tiles with Instant and Later, Learn More showed the Spark copy, and the swipe ended on setting-up with Speed: Later and the 3-confirmation text; recording under Preview.
Transfer to Spending with Instant
The Spend suggestion card opened the Transfer intro, then Funding with Transfer from Savings, Use Other Wallet and Manual Setup; with Instant, setting-up waited for Spark and after 5 minutes said Spark hasn't seen the transfer, since the deposit went to Bitkit's staging chain; recording and still under Preview.
Claim states after the swipe
With the claim offer staged by a local hook, Instant showed Spark adding it and then the success screen, a repriced offer showed Instant now costs ₿ 812 with Wait and Pay and Pay ended on success, and a declined one showed Instant isn't available; recordings and still under Preview.
🟢 Fund Spark from the faucet
Fund Spark from the faucet
10 and 11 at f63e613: 100,000 sats from Lightspark's regtest faucet to the address on the Spark screen arrived as 99,901 after a 99 sat claim fee, and Lightning Connections and the Spark detail listed them; recording under Preview.
🟢 Default and nothing selected, Partial, Channel and Spark, Spark only, Channels only
Default and nothing selected
With two channels and 99,901 sats on Spark, the confirm screen showed 535,993 sats with a 2,190 sat Spark fee and Advanced. The list showed Connection 1, Connection 2 and Spark, all on; with all three off, the total read 0 and Continue was disabled.
Partial
With Connection 2 and Spark off, the confirm showed only Connection 1's 186,780 sats, no Spark fee, and Transfer all. After the swipe, Spending went from 535,425 to 348,929: Connection 1 closed, the other channel and Spark's 99,901 stayed.
Channel and Spark
The remaining channel and Spark, all selected, showed 349,213 sats with the Spark fee; the swipe closed the channel and withdrew Spark, and Spending read 0.
Spark only
With Spark refunded from the faucet and no channels, the confirm showed 99,901 sats with the Spark fee and no Advanced button; the swipe withdrew it and Spending read 0.
Channels only
With two new channels and no Spark balance, the confirm showed Advanced and no Spark fee, the list had only the two connections, and the swipe closed both.
Evidence
0eed9c2a: the emergency exit moved to feat: spark emergency exit #1217, and this PR is now one commit on master. The unit and UI suites (CI's selection) pass on an iPhone 17 Pro simulator (iOS 27.0), SwiftFormat shows no new findings and the translation check passes; J1 passes on an iPhone 17 Pro simulator (iOS 26.5) against Spark's hosted regtest with Savings funded on Bitkit's staging regtest. J8 needs a Spark balance from Lightspark's web faucet and channels, and was not rerun.SparkConfigTestschecks that Spark connects with no data-sync server and no LNURL domain on every network.58fa5fe1: the Debug device build passes, andChannelFreeRailTests,PaykitPaymentProofServiceTests,PaykitAllowanceExecutorTests,SparkLightningSendTestsandSparkTransferFlowTestspass on an iPhone 17 simulator. Not driven on a device.d20fc7c8: the Debug device build passes, andSparkTransferFlowTests,AppCacheDataTestsandHwWalletVendorPresentationTestspass on an iPhone 17 simulator; SwiftFormat and the translation check pass locally. Previews cover every confirm state (Instant, Later, loading) and every setting-up state (looking, adding, done, repriced, Instant unavailable, Later, not seen); the screens were not driven on a device.4ac08000(6d32091a), no rebase, so stacked branches keep their ancestry: one follow-up fix inBackupServicefor master's class-based subscription store; the simulator build and the Spark unit tests pass locally; SwiftFormat and validate pass in CI (run). Draft CI skips the build and unit suite.92a03765: the mainnet Release build for device, SwiftFormat and the unit suite pass (run). The rebuilt flow and early claims (16 to 19) are not yet driven. Results below ran on the earlier flow before the rebuild; their revisions are kept as run.Debug simulator build of
4b58c22andf79c9f5(before the rebase onto55b8a47), iOS simulator, Spark's regtest:1: new wallet without channels; the log shows
Connected Spark wallet on 'regtest'. Receive shows Auto and Spending with a Lightning QR whose invoice names an explicit payee and a route hint to0202f87a…bb76, which an LDK node without channels does not add.The rebased head
aed1f9fbuilds for the simulator (arm64).10 on
34dfdef(nowbb09eeb): Lightning Connections listed Spark first with its balance and the pending deposit with live confirmations; the detail showed provider, trust note, pending and claimed deposits with the claim fee, and Transfer To Savings.10 and 2 on
5c3b6f7(nowcbc59e9): the Spark row and detail drew a full purple bar with no receiving side, the header read Receiving capacity No limit, and a 20,000 sat Spark transfer confirm showed network fee 458, Claim fee 99, To spending 19,901 and Total 20,458, with no background toggle, Learn More or Advanced.Transfer to Savings selection at 674f0da (35, J8's cases): the five recordings in Preview, on an iPhone 17 Pro simulator (iOS 27.0) with Spark on its hosted regtest and channels on Blocktank's staging regtest. At f01de3b the unit suite (CI's selection) passes on the same simulator, with SwiftFormat and the translation check.
Still open
a9aa36fd: the unit suite passes exceptSendConfirmationLifecycleTests.testCustomFeeWalletSwitchSurvivesFeeScreenNavigation, which fails the same way on the base branch at77b01a4a. Not yet driven on a device.8f6de109: built for the simulator only, not driven; the twin Android change was driven on an emulator.Models used
claude-opus-5-5(reasoning:low)claude-opus-5-5(reasoning:low)claude-sonnet-5-5(reasoning:high)