Repository navigation
feat: add spark via breez sdk - #1128
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
jvsena42
left a comment
There was a problem hiding this comment.
Re-review of the 36 own commits since 7ce626fe0 (head e467a2ff1): one HIGH inline (pwltr's candidate, now reproduced), so requesting changes on that alone.
Checked and clean:
- Fee the swipe approved (
23e71bdf5):recheckSparkWithdrawFeereturns the fee shown when the swipe began; a re-pricing during the recheck refuses the commit;withdrawAllchecks the prepared fee against that cap beforesendPayment(SparkManager.swift:813-817). - Pre-restore invoices (
199729d81,18946a0f0):- Both rails go through
settledEarlierAttempt. - With no Spark row, or Spark unreadable, a pre-restore unexpired invoice is refused on either rail.
- A node-paid row returns paid without re-sending.
sparkRestoredAtis cleared on a failed restore and on wipe.
- Both rails go through
- Re-route on source change (
d1efd4f39): a stale answer is dropped byisCurrent(generation, source, endpoints). A leg is paid from the swipe-timeserviceLegwithmaxFeeSats = debit − delivered. - Contact withdrawal during an exit (
9fab00f9f): refused up front, and the send runs insidespends.run, so an exit starting during prepare refuses it and the journal, proof and allowance are released. - Generation guards (
5176fa0c1,437a97913,f0d06d9bc,1af56fc9d,e1dbb0b9b):validGenerationis nil wheneverisRedoRebuild, and the guard runs beforesaveandbroadcastReady. - Payout confirmation:
markOnchainActivityAsConfirmedre-reads on the core queue, so the stale-upsert issue I raised on Android doesn't exist here. The Electrum fallback is fixed in #1304. - Exit funding after restore: the forward scan is fixed in #1305. A quote taken before recovery runs still reuses the restored index; I put a failing test for that on Greptile's #1305 thread.
The 0.28.2 bump in #1306 changes how the exit driver sees a lost CPFP race (.redo instead of deadCpfpChildren); a regtest exit run on 0.28.2 is worth doing before it lands.
Device gate: not re-run for this delta; the finding is unit-reproduced and the fix should land first. Journeys to re-run after it: transfer/spark-emergency-exit.xml, settings/app-status-emergency-exit.xml (updated in c07e4ce38), paykit-spark-route.xml. perf: recoverFunding adds 20+ sequential Esplora reads after a restore, and each refresh starts an Electrum address read per unconfirmed contact payout.
Reviewer model: Claude Fable 5.2 (claude-fable-5-2), Claude Opus 5.5 (claude-opus-5-5)
Reasoning effort: not exposed
jvsena42
left a comment
There was a problem hiding this comment.
Approving at 71f103fca. The HIGH is fixed as suggested: payDirectly re-checks isCurrent after both the LNURL and BOLT11 decodes before writing anything, the caller maps that to .stale, and the new PaykitRouteTests case pins it. The other commit since my review is the #1304 merge, which I approved there.
Device gate: not re-run for this delta (journeys to re-run once the stack is up: transfer/spark-emergency-exit.xml, settings/app-status-emergency-exit.xml, paykit-spark-route.xml); the delta's fixes are covered by unit tests. CI: Run Tests and build-local still pending at this head.
jvsena42
left a comment
There was a problem hiding this comment.
Device gate result: approving, nothing above LOW.
Device gate: 71f103fca — journeys: 3 passed (settings/app-status-spark-row.xml, transfer/spark-emergency-exit.xml 12/12, settings/app-status-emergency-exit.xml); perf: not relevant; l10n: no UI text changed.
- Setup: iPhone 17 simulator on the bitkit-docker
sparkprofile, withSPARK_LOCAL_CONFIG_B64, local Electrum andSPARK_CHAIN_SERVICE_URLpointed at the local chain. Fresh wallet with no channel: Spending 199 865, Savings 500 000. - The exit quote was 192 512 sats over 6 leaves (fees 12 122, fee funding 22 025, 7 353 stays on Spark), paid from savings (fee 422). The funding tx confirmed and the exit reads "In progress".
- Lightning Connection reads "Using Spark" outside the exit and "No open connections" during it, under the Emergency Exit row.
⚠️ Not run:paykit-spark-route.xml, which needs the Paykit fixture stack.
Observation, not a finding: the Spark and Emergency Exit rows in App Status had no tap target in the accessibility snapshot, so they were opened by touching the row text.
…forward fix: find exit funding past a restored counter
pwltr
left a comment
There was a problem hiding this comment.
Reviewed fe3505b.
Medium candidate — Close returned withdrawals that fail without payout details. In Bitkit/Managers/SparkManager.swift:1152, failed-payment cleanup depends on withdrawal details, while the unlisted-payment expiry path requires no recorded payment ID. A returned withdrawal with an ID that later fails without details can therefore retain its journal indefinitely, blocking subsequent Spark contact payments and leaving pending UI or request proofs unresolved. Handle a matched terminal failed status independently of payout details while preserving unknown or pending journals. The application trace supports this candidate; the combined SDK scenario remains unconfirmed.
Review coverage is partial: complete diffs were read for 53 of 187 changed paths. This is not a completed whole-change review.
Review details
General review
Model: GPT-6.1 Sol
Reasoning effort: high
chore: bump breez sdk to 0.28.2
pwltr
left a comment
There was a problem hiding this comment.
Reviewed bbf02ca, which merges #1306. It bumps the Breez Spark SDK to 0.28.2 (the project pin and Package.resolved agree), and the exit driver now waits one check before rebuilding over a step that just read as unverified. It also ends a funded unverified ask without rebuilding once its steps read again. I found nothing new.
Review details
General review
Model: Claude Opus 5.5
Reasoning effort: high
|
QA for bbf02ca: Tested on an iPhone 17 Pro 2x simulator (iOS 27.0) against Spark's hosted regtest, with Breez SDK 0.28.2 (#1306) and fresh dev wallets.
Full in-app emergency exit, running: the exit started from the 12,288 above in 2 leaves, with 11,481 fee funding in 18 parts from another wallet. Progress shows 17 of its 22 exit transactions published, with no rebuild so far. The last refund is claimable at height 3,709,145, about 30 hours after the start; the result goes here at Done. exit-start2x.mp4 |
|
Rerun on bitkit-docker's local stack with the paykit-rs 0.1.1-rc8 fixtures from synonymdev/bitkit-docker#38, on an iPhone 17 Pro 2x simulator against Spark's hosted regtest.
The same journey passes on Android at 2f18eb69b, with the same route, fee and result. ios-journey2x.mp4 |






Closes #1239
Closes #1206
Closes #1242
Closes #1244
Closes #1260
Closes #1261
Closes #1285
Closes #1296
Twin: synonymdev/bitkit-android#1697
This PR keeps Lightning in Bitkit now that Blocktank no longer sells channels. On top of the Blocktank sunset (#1127), a Breez SDK on Spark wallet carries Lightning whenever no channel can. Existing channels keep working until they close, and ldk-node keeps running the on-chain wallet.
The emergency exit is in #1217 and the Paykit route over Spark in #1191, both stacked on this PR.
Description
What this PR does
SPARK_EXITand is restored right after Spark connects.Decisions for review
breez-sdk-spark-swift0.28.0, exact. Spark derives its keys from the wallet's phrase and passphrase onm/8797555', so there is no new phrase to back up. It runs independently of LDK, so a stopped node doesn't take Spending down.realTimeSyncServerUrl,lnurlDomain,useDefaultExternalInputParsers). Nothing goes to Breez's data-sync orbreez.tipsservers, and no Lightning address is offered.https://bitkit.to/electrsin place of mempool.space and blockstream.info.BREEZ_API_KEYkeep Spark off. The key comes from a gitignoredSecrets.xcconfig(README, Build secrets), and a Release-only build phase fails without it (Docs/RELEASE.md).Syncedcan come before its payment history syncs, so it doesn't end this; the user is asked for a new invoice. The refusal can end earlier once bug: synced fires before payment history has synced breez/spark-sdk#1314 gives a payment-history sync signal.ESPLORA_URLpath, since Spark's regtest chain lacks the/fee-estimatescall ldk-node needs (QA Notes, Setup).Out of Scope
SPARK_EXITupload when the exit data is unchanged.SPARK_EXITuploads go through: bug: unimportable spark exit data blocks later backups #1268 (review thread).Risks for review
250 × sat/vB + 750sats.CLAIM_TX_SIZE_VBYTES,deposits.rs).Design
N/A — no design available. Figma has no Spark or unlimited-receiving state for the Lightning Connections header, so No limit uses the spending figure's style; @aldertnl, a design for this state is still open.
UI pending design
Design for the Spark 3.2 screens was requested in the #design-ux Spark 3.2 thread, so some screens may change. The exit UI polish list is not part of this PR.
Preview
Recordings at 2x on b4f3e04 (the Later recording on 803bc6b), on an iPhone 17 Pro 2x simulator (iOS 27) in Bitkit's dark theme. Every recording is staged on a debug build with a local hook that is not in this PR: the Spark balance, the channel rows, the claim states after the swipe (Instant, Later, repriced and Pay, unavailable), the Spark withdrawal, and the tap that plays the swipe-to-confirm (simulator drags fail on iOS 27).
ios-spending-later2x.mp4
ios-spending-instant-2x.mp4
ios-repriced-2x.mp4
ios-unavailable-2x.mp4
ios-receive-2x.mp4
ios-connections-2x.mp4
ios-savings-spark-2x.mp4
ios-savings-channels-2x.mp4
ios-savings-both-2x.mp4
ios-savings-none-2x.mp4
ios-savings-nospark-2x.mp4
Screens
Every screen this PR adds or changes, on an iPhone 17 Pro 2x simulator (iOS 27) in Bitkit's dark theme, rendered with stubbed state by a local hook that is not in this PR; the amount, confirm, setting-up and Transfer to Savings shots are on 803bc6b.
Transfer to Spending
Funds in transfer
Transfer to Spending from a hardware wallet
Transfer to Savings
Lightning Connections
Send
Send: unified invoice Spending can't pay
QA Notes
Setup
SPARK_CHAIN_SERVICE_URLset to the regtest's Esplora URL, so Spark reads Bitkit's chain; without it the transfer buttons show the Transfer Unavailable sheet.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.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 fee that costs no more at the swipe pays at once. A higher fee at the swipe is manual test 24.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". Unlike Android, a picked selection shows "Transfer all" in place of "Advanced", so reaching the list again takes "Transfer all" first.Manual Tests
Mainnet cases are under Mainnet tests. The Emergency exit tests (25 to 33) are in #1217.
SPARK_CHAIN_SERVICE_URLset to the regtest's Esplora URL, fund Savings, Transfer to Spending → amount max is the on-chain budget, confirm shows Network fees, Spending fee, To spending and Total with the Instant and Later rows and no Advanced, swipe broadcasts to the Spark deposit address — same.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 early-claim cap it clears, 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 once; the duplicate-payment check and the Paykit proof fail closed when Spark can't list its payments, a retried invoice settles from its live attempt, the sendable estimate doesn't overflow, a Spark failure then a channel retry and a channel failure then a Spark retry both end in success with no failure reaching the send screen, and a settled activity keeps its status against a stale failure; while the node can't list its payments, an invoice it sent is not paid through Spark, Paykit reads the attempt as unknown and QuickPay waits; insufficient funds inside the Spark send is a refusal; the send sheet waits for a channel when Spark covers the amount but not its feePaykitPaymentProofServiceTests.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 skipped; exit data not yet imported blocks the upload, an unreadable remote backup backs off, and the latest backup time skips the exit dataSavingsSelectionTests.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, committing resets the pick to all, and so does entering the flowSparkConfigTests.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, the fee timeout returns at its deadline while the SDK call ignores cancellation, Close instead waits for the fee and for the swipe's re-price, 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 withdrawnSparkPaymentSafetyTests.swift— a settled Spark payment is recorded and its node event raised even when the refresh after it fails; a sent withdrawal succeeds when the refresh after it fails and fails on Spark's failed status; a withdrawal fee above the shown one is refused, the swipe waits for the fee, a higher fee at the swipe is shown and asks again, and the withdrawal sends with the shown fee as its cap; a deposit at or below the claim ceiling is refused; SDK errors show app copy; a send must fit its quoted feeChannel-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 instead of leaving "Payment Pending". Android does the same.LightningService.emitChannelFreeRailEvent: Spark events that arrive before the node's event callback is set are kept and delivered once it is.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.QA
Each result at the revision it ran on (✅ passed, ❌ failed, ⏭️ not run). Tested on an iPhone 17 Pro simulator (iOS 27.0) against Spark's hosted regtest.
0eed9c2aon 0.27.2.SPARK_CHAIN_SERVICE_URL, both transfer entries open the sheet and its Learn More and Continue work, while Funding and Receive show the Spark-ready screens.bitkit-docker, not part of the hosted regtest setup.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.
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.
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.
🟢 Pay a Spending invoice between two Spark wallets, Paykit contacts on Spark, Transfer to Spending over Max
Pay a Spending invoice between two Spark wallets
12 at 5eab42d: wallet B's Spending invoice, paid from wallet A for 1,000 sats, sent from A's Spark balance (19,901 to 18,898) and showed Bitcoin Sent; B showed Received Instant Bitcoin and both activity lists have the payment.
c12-2x.mp4
Paykit contacts on Spark
34 at 5eab42d: two Spark-only wallets with Bitkit profiles, saved as each other's contacts. After a cold start of B, paying B from A's contact screen offered Spending with a Lightning invoice (Instant, 1 day expiry) and 500 sats went through Spark. A 300 sat payment request from B reached A, and A paid it: Bitcoin Sent on A, not Payment Pending, and both contacts' activity lists the two payments.
c34-2x.mp4
Transfer to Spending over Max
J3 at 5eab42d, launched with
SPARK_CHAIN_SERVICE_URLon the staging Esplora: Savings › Transfer To Spending opened the intro, then the amount screen with Available 39,431. Nine taps on 9 snapped the amount to 39,431 with the Spending Balance Maximum toast, and delete took it to 3,943. The toast isn't in the accessibility tree, so the recording shows it.j32x.mp4
🟠 Transfer Unavailable sheet with Spark running
Transfer Unavailable sheet with Spark running
J2's first steps at 5eab42d, on a build launched without
SPARK_CHAIN_SERVICE_URL: Savings › Transfer To Spending and Spending › Transfer From Savings opened the Transfer Unavailable sheet with Manual Setup Required, Learn More opened blog.bitkit.to and Continue opened Manual Setup. Spark was connected, so Funding listed Transfer From Savings and Use Other Wallet above Manual Setup and the journey stopped there.j22x.mp4
🟢 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.
🟢 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
Unit and UI suites are CI's selection; "not driven" means not run on a device.
0eed9c2a: the unit and UI suites 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 was not rerun.SparkConfigTestschecks that Spark connects with no data-sync server and no LNURL domain on every network.58fa5fe1, Paykit on Spark: the Debug device build passes, andChannelFreeRailTests,PaykitPaymentProofServiceTests,PaykitAllowanceExecutorTests,SparkLightningSendTestsandSparkTransferFlowTestspass on an iPhone 17 simulator. Not driven.d20fc7c8, restored flow: 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). Not driven.4ac08000: the simulator build and the Spark unit tests pass locally; SwiftFormat and validate pass in CI (run).92a03765: the mainnet Release build for device, SwiftFormat and the unit suite pass (run).Device runs on the earlier flow, at the revisions they ran on:
4b58c22andf79c9f5, Spark's regtest: new wallet without channels; the log showsConnected 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.aed1f9fbuilds for the simulator (arm64).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.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.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.Mainnet tests (run by the author)
These run on real devices with mainnet funds, by the author. Reviewers and review bots don't need to run them, and they don't block review.
Setup: a mainnet build with
BREEZ_API_KEYinSecrets.xcconfig(copySecrets.xcconfig.template). Round 1 ran on an iPhone.Round 1
Round 2 (pending)
On the final heads, with a mainnet build and small amounts.
https://bitkit.to/electrs.Models used
claude-opus-5-5(reasoning:medium),claude-sonnet-5-5(reasoning:high)claude-opus-5-5(reasoning:medium)claude-sonnet-5-5(reasoning:high)Taken from the run logs of every session and subagent that planned, wrote or reviewed this PR, which record the model and reasoning effort per turn. The
claude-sonnet-5-5planning runs were research and audit subagents.