Repository navigation
feat: add spark via breez sdk - #1697
Conversation
bf469f4 to
ef7130e
Compare
01334a9 to
4ed36e0
Compare
a1412d5 to
a1c333c
Compare
9982a55 to
d964a5f
Compare
eab67dd to
567e1a8
Compare
|
QA: 🟢 Restore Spark exit dataExit data backs up as its own category. After a wipe and restore, the Pixel's Spark balance and claimed deposits came back, but the log counted 0 imported leaves because the exit data was restored after Spark had already synced. Ran on 567e1a8; the order is fixed in f96bae9, and a rerun is pending.
🟢 Claim a depositTransfer to Spending: Spark claimed each deposit after 3 confirmations, keeping 297 sats of 15,000 and 198 of 2,500. Ran on an earlier head of this PR, before the 27 Oct rebase. 🟢 Receive with zero balance, then payWith no channel and a 0 Spark balance, the Pixel received 1,024 sats over Lightning and paid 128 from them two minutes later: Lightning received into Spark is spendable at once. The send's activity showed a fee of 0, fixed in 972e5ec. Ran on 567e1a8.
🟢 Move Spending to SavingsTransfer to Savings withdrew 12,973 sats from Spark for a 1,470 sat fee; the withdrawal showed in Savings unconfirmed after 22 seconds and confirmed after 31 minutes. Ran on an earlier head of this PR, before the 27 Oct rebase; a later run on a build of #1710 lists the withdrawal as a Transfer from Spending.
🟢 Fees against BlocktankOn the same wallet, a 15,000 sat Transfer to Spending cost 907 sats in all through Spark (6.0%) and 1,584 through Blocktank (10.6%). Moving back to Savings cost 1,470 sats through Spark, where Blocktank channel closes cost 183 to 1,113. 🔴 Pay a Paykit request over SparkThe iPhone paid this Pixel's 204 sat Paykit request over Spark and it arrived at once, but the iPhone stayed on Payment Pending; fixed in 024b6bb and synonymdev/bitkit-ios@735e972e, not rerun on a device yet. An earlier attempt failed because this Pixel republished a private invoice already paid through Spark, fixed in 972e5ec.
|
901ecd8 to
2ba664e
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. |
024b6bb to
d7be600
Compare
|
Retargeted this draft to master before merging its base, #1696, so deleting that branch would not close it. |
There was a problem hiding this comment.
detekt found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
|
Rebuilt this branch on master after the Blocktank sunset (#1696). 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. |
cab0f89 to
fa49b5a
Compare
|
Pushed 515a606, with fixes for @coreyphillips's recheck 5850123564 and @pwltr's iOS review. Each has a test that failed first.
The Out of Scope list now links the translations follow-up, #1840. pwltr's rail-before-admission candidate doesn't apply on Android: the Spark rail is recorded only after admission ( The iOS twins are on synonymdev/bitkit-ios#1128. @jvsena42, a re-review is needed only because of this push: 50bc48b...515a606 |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 3 new commits (7962bd4, 7b247f3, 515a606). 1 blocking finding the reviewers disagree about.
addressed
- Uncertain Spark send is declared failed after a sync that never reached Spark (app/src/main/java/to/bitkit/repositories/SparkRepo.kt:340-350). Fixed:
hasSyncedSince(SparkRepo.kt:403-410) now needs nolastSyncErrorand a non-nulllastSyncedAtlater than the failure. A missing send settles only after such a sync and its invoice's expiry (:370-373), andstart()(:189-190) and the Synced handler (:1021) use the same check. In Breez'ssync_wallet_internal,last_synced_atis written only when the wallet sync returns Ok, andlast_sync_errorcomes from that same result, so a sync that never reached Spark no longer counts. I confirmed this by reading the code. I could not run the tests, because the build's private packages were unavailable here. - lightningSend still reads an unlisted, unexpired Spark send as never made (app/src/main/java/to/bitkit/repositories/SparkRepo.kt:820-829). Fixed: When Spark lists no row,
lightningSend(SparkRepo.kt:824-835) now returnsFound(PENDING)for a hash that is uncertain or saved and unexpired. It does the same for a FAILED row while the send is uncertain.payInvoiceturns that intoPaymentPendingException(:297-298),PaykitAllowanceExecutorreads it as PENDING (:160-163), andPaykitPaymentProofRepokeeps the proof (:1319-1337). The settle paths read the raw row throughlistedLightningSend(:371, :1051), so the hold cannot block its own settlement. I confirmed this by reading the code against the tests at SparkRepoTest.kt:1502-1533, without running them. The case where Spark cannot list its payments is filed separately below.
still open
- New and changed strings are missing from every translation (app/src/main/res/values/strings.xml:376). Four new translatable strings are absent from all 16
values-*/strings.xmlfiles:lightning__savings_spark_failed__title,lightning__spending_confirm__error_quote,lightning__spending_confirm__max_changedandlightning__spending_confirm__max_changed_title. Three English strings changed with their translations left as they were:lightning__funding__button1,wallet__receive__cjitandwallet__receive_onboarding_description. The last one changed meaning, so its translations still say "on the Lightning Network". The repository rule asks for translations in the same PR. The four keys are still in novalues-*/strings.xmlfile (a grep found 0 of 15), and they show in SavingsProgressScreen.kt:307, SparkFundingViewModel.kt:501-502 and SavingsConfirmScreen.kt:404. The reply moves them to #1840 through Out of Scope. That defers the point rather than fixing or rebutting it, so accepting the deferral is a maintainer's call.
the reviewers disagree, your call
- Spark retry skips the send hold while Spark can't list its payments (app/src/main/java/to/bitkit/repositories/SparkRepo.kt:829-834)
|
Pushed 568e277, with the fix for @coreyphillips's blocking thread in 5850968332: a Spark retry no longer skips the send hold while Spark can't list its payments yet. Details are in the thread. The translations stay in #1840. That's my call as maintainer, so the Spark PRs don't carry them. @coreyphillips @jvsena42, a re-review is needed only because of this push: 515a606...568e277 |
|
Ran cases 12, 37 and 38 on 515a606, dev builds with
Results and recordings are in the QA section. |
|
Pushed b3298a0 to fix case 12 on regtest: two Spark wallets now pay each other's Lightning invoices as Spark transfers.
The twin fix is on synonymdev/bitkit-ios#1128. @coreyphillips @pwltr @jvsena42, a re-review is needed only because of this push: 568e277...b3298a0 |
|
Case 12 again on b3298a0, two devices: ❌, now on the UI side.
Recording and details are in the QA section of the description. |
|
Pushed 5953744, which reverts b3298a0. Two dev builds go back to paying each other over Lightning through Spark's provider, the path mainnet uses.
@coreyphillips @pwltr @jvsena42, a re-review is needed only because of this push: b3298a0...5953744 |
|
Case 42 on 5953744, two devices: ✅. The payment went over Lightning through Spark's provider (Breez method |
pwltr
left a comment
There was a problem hiding this comment.
Reviewed 5953744.
High candidate — An older failed Spark attempt can release an unresolved retry after restart. In app/src/main/java/to/bitkit/repositories/SparkRepo.kt:983, startup pruning treats the older FAILED row as final because the newer retry's uncertainty is only in memory. Both pruning and the later explicit removal discard its persisted, unexpired hash, including after a failed initial sync. Either rail can then dispatch again while Spark may still resume the interrupted retry. The restart tests cover an empty listing, while the older-failure test retains in-memory uncertainty; their combination remains uncovered. Preserve evidence of the unresolved attempt across restart and do not release it solely because an older attempt failed. Duplicate settlement has not been demonstrated.
Low candidate — A late failure event can clear a newer retry's hold. app/src/main/java/to/bitkit/repositories/SparkRepo.kt:1049 removes uncertainty by hash without identifying the attempt. If an older failure arrives after an uncertain retry, the older FAILED row becomes retryable again. SDK delivery of that ordering remains to be established.
Low candidate — A completed withdrawal can leave the old Spending balance displayed. app/src/main/java/to/bitkit/repositories/SparkRepo.kt:715 retains the previous shown balance whenever Spark reports zero with a sync error, even after a successful withdrawal. Sending still uses the actual balance. The successful-withdrawal-plus-sync-error combination needs confirmation.
Review details
Quick review
Model: Claude Opus 5.5
Reasoning effort: high
General review
Model: GPT-6.1 Sol
Reasoning effort: high
Combined review
Model: GPT-6.1 Sol
Reasoning effort: high
Addressed in 6cdf592. A retry whose outcome Spark didn't report is now kept in the cache store, not only in memory.
out of scope: tracked in #1848, because whether the SDK can deliver an earlier attempt's failure after a later send call on the same hash returned is unestablished. Clearing the hold only on non-failure events would keep every genuinely failed uncertain retry pending until its invoice expires.
out of scope: tracked in #1849, because it affects only the shown balance until the next good sync. Sending uses Spark's actual balance. The iOS twin has the same restart gap; its fix goes to synonymdev/bitkit-ios#1128. re-review is needed only because of this push: 5953744...6cdf592 |
jvsena42
left a comment
There was a problem hiding this comment.
Re-review of the 19 commits since my approval at 477e9a0e9 (head 6cdf59220): two LOWs inline, approving.
Checked and clean:
- Send admission and the rail record (
cc942e70a,eb434dc8f): the rail is recorded at admission, and the earlier rail is restored when Spark sent nothing (LightningRepo.kt:1540-1558). - Kept LDK hash while the node cannot list:
earlierNodeSendthrowsNodeNotStarted(LightningRepo.kt:1587-1590), as coreyphillips asked. hasSyncedSince: needs a non-nulllastSyncedAtand no sync error. Settling a missing send needs a sync after the failure and the invoice's expiry.- Double
sparkWithdrawJob(my earlier out-of-scope note): fixed atTransferViewModel.kt:415and:439-444. - Amount and fee binding: the send path is unchanged. Savings transfers snapshot
channelsToCloseandwithdrawsSparkat job start, and an active withdrawal is joined, not repeated. - Spark to Savings on mismatched regtest chains: refused (
TransferViewModel.kt:465); both checks hold on mainnet. - Wipe: the in-memory map is cleared (
SparkRepo.kt:219), andcacheStore.reset()runs inWipeWalletUseCase:113. - perf: each
Syncedevent lists payments once per uncertain hash (SparkRepo.kt:1044), and uncertain hashes now live until invoice expiry. That only matters while a send is held; nothing to measure for the common path.
Both LOWs are in the Spark send bookkeeping this PR adds. The iOS twin has a MEDIUM in the same area that I'll post there. These can be fixed here or moved to a follow-up issue, your choice. Please say which on each thread.
coreyphillips's translations thread (strings.xml:376) is deferred to #1840, as before.
Device gate: not re-run for this delta. The earlier run covered the Spark transfer and send journeys. This delta changes send-outcome bookkeeping that only a kill or a dropped connection mid-send reaches, and SparkRepoTest covers it. CI is green at 6cdf59220.
Reviewer model: Claude Fable 5.2 (claude-fable-5-2), Claude Opus 5.5 (claude-opus-5-5)
Reasoning effort: not exposed
|
@jvsena42 re-review is needed only because of this push: 6cdf592...4f10f8d |
jvsena42
left a comment
There was a problem hiding this comment.
Re-review of 4f10f8d1f: both LOWs are fixed as proposed, and nothing new. Approving.
- The uncertain mark is written right before
sparkService.sendand cleared on every outcome that returns: expiry, insufficient funds, and a returned send. A send cut off by a kill or a cancellation stays held. A failure still goes throughsettleUncertainSend, which overwrites the mark with the failure time. lightningSendStatusesandattemptedLightningPaymentHashesnow read the uncertain set, so QuickPay and Paykit see an uncertain retry as pending.- The three new
SparkRepoTestcases are the reproductions from my threads.
Device gate: not re-run, as before; this is send bookkeeping covered by SparkRepoTest.
Reviewer model: Claude Opus 5.5 (claude-opus-5-5)
Reasoning effort: not exposed
|
Device run at 4f10f8d: case 42 passes; J1 passes on the steps a funded wallet can reach. Setup: two dev-build emulators (Android 16) on Spark's regtest through
|
pwltr
left a comment
There was a problem hiding this comment.
Reviewed 4f10f8d.
High candidate — Require complete Spark history before allowing an uncached channel retry after restore. In app/src/main/java/to/bitkit/repositories/SparkRepo.kt:906, unavailable history falls back to the locally saved send hashes. A restore from missing or stale metadata can lack an unexpired invoice previously paid through Spark. Before Spark's first successful sync, that hash returns no earlier send, allowing LightningRepo.payInvoiceWithLdk to dispatch it through usable channels. The restart and cancellation fixes preserve known hashes; they do not establish completeness of restored metadata. Require authoritative history or a restore-safe completeness guarantee before admitting this retry. The source supports duplicate dispatch; actual duplicate settlement has not been demonstrated.
This static assessment is partial: changed tests, resources and journeys were not exhaustively reviewed.
Review details
General review
Model: GPT-6.1 Sol
Reasoning effort: high
Combined review
Model: GPT-6.1 Sol
Reasoning effort: high
Real: tracked in #1855 and fixed in #1858.
|





Closes #1805
Twin: synonymdev/bitkit-ios#1128
This PR keeps Lightning in Bitkit now that Blocktank no longer sells channels. On top of the Blocktank sunset (#1696), 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 #1782 and the Paykit route over Spark in #1754, both stacked on this PR.
Description
What this PR does
SPARK_EXITand is restored right after Spark connects.Decisions for review
breez_sdk_spark:bindings-android:0.28.0. Spark derives its keys from the wallet's phrase and passphrase onm/8797555', so there is no new phrase to back up.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 release workflow fails without it, and both release environments have it.ESPLORA_URLand two scripts run a dev build on Spark's regtest chain (QA Notes, Setup).Out of Scope
Risks for review
250 × sat/vB + 750sats.CLAIM_TX_SIZE_VBYTES,deposits.rs).libbreez_sdk_spark_bindings.soadds about 11 MB per ABI (arm64, uncompressed).ESPLORA_URL, Spark never sees a dev build's deposit.Design
Restored transfer screens, on the latest
Bitkit - Handoffpage of the Bitkit Figma file (frame names fromdocs/screens-map.md; the Spark speed rows, Spending fee and setting-up states have no frame yet):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
Transfer to Spending, Receive and Transfer to Savings on 7c8b0ee, on an Android emulator at 896×1994 (2 px per dp) in dark mode, at 2x speed. Every recording is staged on a debug build with local hooks that are not in this PR: the Spark balance, the claim states after the swipe (Instant, Later, repriced and Pay, unavailable), the Spark withdrawal and its fee, and the channel rows on Transfer to Savings.
android-spending-later-2x.mp4
android-spending-instant-2x.mp4
android-spending-repriced-2x.mp4
android-spending-unavailable-2x.mp4
android-receive-nochannel-2x.mp4
android-connections-2x.mp4
android-savings-spark-2x.mp4
android-savings-channels-2x.mp4
android-savings-both-2x.mp4
android-savings-nothing-2x.mp4
android-savings-nospark-2x.mp4
Screens
Every screen the PR adds or changes, on the same emulator in dark mode; the amount, confirm, setting-up, hardware wallet amount and Transfer to Savings shots are on bdf3584. Staged with local debug-only hooks that are not in this PR: the Spark balance, every setting-up state, the channel rows on Transfer to Savings, the Spark fee and the Spark transfer failure. The hardware wallet screens use a Trezor emulator on a local regtest chain.
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
python3 scripts/spark-regtest-esplora.py --port 3999. It fronts Spark's regtest mempool API (the SDK's own regtest default,sdk_builder.rs) and adds the two Esplora calls it lacks and ldk-node needs:/fee-estimatesat start and/block/<hash>/statusfor Lightning sync.ESPLORA_URL=http://10.0.2.2:3999 ./gradlew assembleDevDebug, so the node's chain source is Esplora on Spark's chain instead of Electrum on Blocktank's staging chain. For a phone, run the proxy with--bind 0.0.0.0and use the host's address.FAUCET_USERNAME=… FAUCET_PASSWORD=… python3 scripts/spark-regtest-fund.py <Savings address> 100000through Lightspark's API faucet (credentials issued by Lightspark; Breez's CI uses the same,faucet.rs).POST /txwithsendrawtransaction RPC error: {"code":-1}, whilePOST /v1/txs/packageaccepted the same transaction. If a Savings send fails that way, forward/txto the package endpoint (a one-element JSON array) in the proxy../spark(docs/spark-regtest.md, pinned at cdd294a5e). Pointing a dev build at it needs a build setting that comes in a follow-up PR; until then the docs say how to patch the settings in by hand. J5 uses this stack.TransferViewModelTest.kt. Channels with no Spark balance was opened through the debug screen linkbitkit://screen/savings-availability, since Spending was 0.ESPLORA_URLstays on Blocktank's staging chain: Spark start-up and invoices work, and Savings to Spending shows the Transfer Unavailable sheet, as Spark could not see its deposit.Journeys
transfer/spark-transfer-to-spending.xml— with Spark ready, the suggestion card opens the Spending Balance intro and Funding, Transfer from Savings opens the Transfer to Spending intro and the amount screen; 25% and Max fill the amount, confirm shows the four tiles with Instant and Later, Later updates the tiles, Learn more opens and returns; the intros show once.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 pad caps at Max with the Spending Balance Maximum toast.amount-limits/transfer-spending-preset-delete.xml— after 25%, Max or a unit switch, each delete removes one digit.transfer/spending-confirm-amount-change.xml— a UTXO that confirms while confirm is open re-prices the transfer at the swipe; a price that costs no more pays on that swipe with no Fees changed toast. A higher price asking for another swipe is covered bySparkFundingViewModelTest.kt.hardware-wallet/transfer-to-spending.xml— a paired Trezor transfers into Spark through amount, sign and signed to the setting-up screen.hardware-wallet/passphrase-transfer-to-spending.xml— a hidden wallet signs with its live session, re-prompts after the session drops, and refuses a wrong passphrase.hardware-wallet/transfer-to-spending-node-warmup.xml— a hardware transfer started during node warm-up reaches the sign screen.transfer/savings-select-funds.xml— Select funds lists Spark and each channel, all on; confirm shows only what is selected, with Spark fee only while Spark is selected; Transfer all restores everything; nothing selected disables Continue; stops before the swipe.Manual Tests
Mainnet cases are under Mainnet tests.
ESPLORA_URLon Spark's regtest chain, fund Savings, Transfer to Spending → amount max is the on-chain budget, confirm shows the network fee, the Spending fee for the chosen speed and no Advanced, swipe broadcasts to the Spark deposit address — same.ESPLORA_URLon Spark's regtest chain, Savings funded from the faucet → Transfer to Spending; after 1 confirmation Spark claims the deposit and Spending shows it; Lightning Connections lists it as claimed — Spark's regtest chain.Imported Spark exit databefore Spark is used, and Spark's balance and deposits match — needs two installs.Merged stored Spark exit databefore it replaces the stored backup — needs two installs and control of VSS reachability.Automated Checks
SparkRepoTest.kt— Instant claims early within the ceiling and reaches Claimed, asks above it, shows the repriced fee from a repriced claim, pays a repriced claim at the new fee, is unavailable with only the standard claim or a declined claim, says Spark has not seen a deposit never listed, and Later makes no claim call; a Later deposit reads as claimed once Spark claims it; a restored exit backup is imported once Spark connects, one from another network is skipped, a restore whose exit data Spark rejects fails and keeps it staged, and the exit backup waits for Spark's first sync; a send recorded with fee 0 is re-recorded with the fee a later sync reports; a Spark send refused before it leaves (fee over the limit, past its deadline) is a typed refusal while a failure inside the send is pending, and ends as failed once Spark has synced and holds no record of it; an invoice in flight, pending or paid is not sent again; a payment event settles even when the balance read fails; a Max send keeps the routing fee Spark quotes for its whole balance; a withdrawal is listed with its fee, pays the quote its fee estimate priced while it holds and asks again once it expired or the address changed, never leaves above the confirmed fee or when it would leave dust, succeeds when the refresh after it fails, and its arrival is marked a transfer by the txid its send returned; a wipe fails when Spark's storage is not deleted and clears Spark's stateSparkFundingViewModelTest.kt— Max is Savings less the network fee and 25% a quarter of it, Max is 0 without Savings, Continue refuses an amount under the Instant fee, prices and opens confirm, the swipe asks again when the fee rose and refuses when Savings no longer cover it, sends at the quoted fee rates and follows the claim at the chosen speed, a hardware transfer prices from the device and hands signing a transfer request, and a funding signed but not accepted opens its recovery instead of an errorSparkClaimOfferTest.kt— 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 declinedSparkServiceTest.kt— Lightning sends return before completion; the config turns off Breez data sync, the Lightning address server and the default external input parsers on every network; the Later fee prices the mature claim at the half-hour rate (198 sats at 2 sat/vB); connect is refused during a wallet wipe; wipe deletes Spark's storage and a staged exit data restoreDeriveBalanceStateUseCaseTest.kt— Spark balance counts toward Spending, caps max send, and pending deposits count as in transferPaymentFailureReasonExtTest.ktcase — Spark and SDK failures show the existing payment failed copyContentViewTest.kt— the spending start route shows the intro until seen, for Savings and hardware wallets, and setting up drops the confirm screen from the back stackPrivatePaykitRepoTest.kt— a private invoice paid through Spark is replaced on the next refreshPaykitPaymentProofRepoTest.kt,PaykitAllowancePayerTest.kt,PaykitAllowanceRepoTest.kt,AppViewModelSendFlowTest.kt— a Lightning proof settles from Spark when its event was missed and is removed when Spark failed the send, a send neither rail knows was never made, a Spark send is never read as unsent before Spark has synced, Spark becoming ready or syncing settles open allowance attempts and republishes the Paykit endpoints, Spark sends keep the deadline and fee limit, a receive recorded without its event gets its contact, the send limit leaves room for Spark's service fee, and a Spark retry decides a Lightning payment the node failedLightningRepoTest.kt,AppViewModelSendFlowTest.kt— an earlier Spark failure never settles a retry the node is paying, a Spark retry of an invoice the node failed settles it, and the node's earlier failure never overwrites the Spark retry's success; a Spark send is refused while a wallet restore is pending, or when one begins during authorizationBackupRepoTest.kt—SPARK_EXITuploads the exported state and restores it, defers without uploading until Spark has connected and synced, merges the stored backup into Spark before the first upload, never replaces a stored backup it could not read, reports a failed restore as incomplete, and uploads when Spark was ready and synced before observing startedEditInvoiceVMTest.kt— Receive asks for no liquidity or CJIT while Spark can receiveTransferViewModelTest.kt— a selection moves only the picked channels and Spark: Spark only closes no channel and doesn't wait for a peer, channels only leave Spark untouched, both move both; a swap skips Spark when it is deselected and is sized by the selected channels; a quote left from a channel selection never swaps a Spark-only transfer; Spark's fee is priced once per balance and gives up after 15 s; a failed Spark withdrawal is reported while the selected channels still close; Spark is never withdrawn at a fee the user was not shown, and is withdrawn once per confirmed transferNotifyPaymentReceivedHandlerTest.kt— an on-chain receive from a Spark withdrawal is a transfer with no received sheetLightningRepoTest.kt,SparkRepoTest.kt— Spark waits for the node's first payment listing when the node's earlier sends were never kept, that listing keeps its pending and paid sends, and Wait clears the early-claim cap again while another claim holds the depositLightningRepoTest.kt,WalletRepoTest.kt,WalletViewModelTest.kt— construct the new Spark dependency, idle, so existing channel paths are unchangedQA
Each result at the revision it ran on. Dev builds ran on Android 15 and 16 emulators on Spark's hosted regtest.
spark-transfer-to-spending.xmlon 4e22b9c, Breez SDK 0.28.0: passed, with Savings funded with 100,000 sats on staging regtest; also passed on ab1b09d, and by hand on 1adfc47.ESPLORA_URLon Spark's chain: A paid B's amountless Spending invoice 4,000 sats and both show the activity (A: Sent 4,011, B: Received 4,000), but not as a Spark transfer. Both wallets' Breez SDK records list the payment with methodlightning, and A paid an 11-sat fee. B's Spark balance was empty before this payment.case12-spending-invoice-a-pays-b.mp4
spark, a 0-sat fee and status completed, and B's Spending went from 4,000 to 5,000. Neither wallet shows the payment: A stayed on Payment Pending ("taking a bit longer than expected"), B showed no received sheet, and neither activity list has it after 4 minutes and a refresh. Cause: inSparkRepo.kt,recordActivity()andtoNodeEvent()handle onlyPaymentDetails.Lightning, so a payment with Spark details writes no activity and emits no payment event.case-12-spark-transfer.mp4
lightning, completed, and A paid a 3-sat fee. A reached Bitcoin Sent and lists Sent 1,003, and B showed Received Instant Bitcoin and lists Received 1,000.case-42-lightning-through-spark.mp4
case37-spark-withdrawal-offline.mp4
case38-spark-to-savings-transfer.mp4
SPARK_EXITmerge before the first upload: covered bySparkRepoTest.kt,TransferViewModelTest.ktandBackupRepoTest.ktonly. The default external input parsers change is not run on a device either.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_KEY=<key>inlocal.properties. Round 1 ran on a Pixel 10 Pro.Round 1
https://bitkit.to/electrs/fee-estimates, read-only check (result not posted).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.