Skip to content

feat: add spark via breez sdk - #1697

Merged
ovitrif merged 140 commits into
masterfrom
feat/breez-spark-lightning
Nov 11, 2026
Merged

ovitrif merged 140 commits into
masterfrom
feat/breez-spark-lightning

Conversation

@ovitrif

@ovitrif ovitrif commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • Transfer to Spending is back, on Spark. While Spark is ready, the suggestion card, Funding and the transfer buttons open the restored Blocktank-era intro, amount, confirm and setting-up screens. Without Spark, the Transfer Unavailable sheet stays.
  • Instant or Later. Confirm shows Network fees, Spending fee, To spending and Total, and two speed rows:
    • Instant, the default: ready in a few minutes, up to the Instant ceiling.
    • Later: after 3 confirmations, about 30 minutes, for less.
  • Setting-up follows the deposit. Instant claims early within the ceiling. Above it, or when repriced, it shows the new cost with Pay and Wait. It says when Instant isn't available, and when Spark hasn't seen the deposit.
  • Hardware wallet Transfer to Spending sends from the device to the Spark deposit address through the same screens.
  • Lightning without channels. Receives and sends go through Spark when no channel can carry the payment, and land in the same received sheet, send screens and activity as channel payments.
  • Spending includes Spark. Its balance counts toward Spending, and its deposits count as in transfer from broadcast on. The most one payment can send is the larger of the channel and Spark balances.
  • Lightning Connections lists Spark first. It shows the balance, deposits with confirmations, receiving as No limit, and a detail screen with the provider, a trust note and the claim fees.
  • Transfer to Savings selection. Select funds lists Spark beside the channels, all on, and moves only what is picked. Spark's withdrawal fee shows only while Spark is selected.
  • Paykit on Spark. A Spark-only wallet publishes a Lightning endpoint and pays and receives Paykit requests and allowances through Spark.
  • Exit data backup. Spark's unilateral exit data backs up to VSS as SPARK_EXIT and is restored right after Spark connects.
  • Short screens. Bottom buttons and swipes stay at the bottom, with content scrolling above them.

Decisions for review

  • SDK: breez_sdk_spark:bindings-android:0.28.0. Spark derives its keys from the wallet's phrase and passphrase on m/8797555', so there is no new phrase to back up.
  • Privacy: real-time data sync, the LNURL server and the default external input parsers are off on every network (realTimeSyncServerUrl, lnurlDomain, useDefaultExternalInputParsers). Nothing goes to Breez's data-sync or breez.tips servers, and no Lightning address is offered.
  • Chain service: mainnet uses Bitkit's Esplora at https://bitkit.to/electrs in place of mempool.space and blockstream.info.
  • API key: mainnet builds without BREEZ_API_KEY keep Spark off. The release workflow fails without it, and both release environments have it.
  • Rail choice: a channel carries a payment when it can, and Spark otherwise. A contact's amountless Paykit invoice goes to Spark unless channel inbound covers 1,000,000 sats.
  • Claim cap: the wallet-wide cap is the recommended fee plus 2 sat/vB, so the SDK never claims early by itself. Only the user's Instant choice does, up to the ceiling: the fastest rate plus 2 sat/vB, times 99 vB, plus 0.2%.
  • Fees are never swapped silently:
    • Confirm is re-priced at the swipe, and a higher total needs another swipe.
    • A repriced claim asks first.
    • A Spark withdrawal goes out only at the fee confirm showed.
  • Send safety:
    • A send with no SDK outcome shows Payment Pending until Spark's record settles it.
    • An invoice is never sent twice.
    • Each send settles from the rail it went out on.
  • Exit data: backed up once Spark has synced, merged with the stored copy before the first upload, and imported without contacting the operators.
  • Dev testing: dev builds use Lightspark's hosted regtest with no key. ESPLORA_URL and two scripts run a dev build on Spark's regtest chain (QA Notes, Setup).

Out of Scope

Risks for review

  • Trust model. Spark is not self-custody in the channel sense. Funds are safe while one of three operators (Lightspark, Breez, Flashnet) deletes old keys, and 2 of 3 sign. The operator code has a per-wallet freeze switch, and Spark's terms let Lightspark suspend access.
  • Exit data size. It can reach several MB, and a large upload to Synonym's VSS is untested.
  • Fees.
    • Sending costs 0.25% plus routing; receiving is free for the user.
    • Withdrawing costs 250 × sat/vB + 750 sats.
    • The claim is a flat fee, the fastest rate times 99 vB, so it weighs on small transfers. On mainnet, a 15,000 sat transfer lost 907 sats, about 6% (CLAIM_TX_SIZE_VBYTES, deposits.rs).
  • Slower Savings to Spending. Blocktank needed 1 confirmation; Spark needs 3, about 30 minutes, unless the provider offers an early claim (Claiming on-chain deposits). That offer is the provider's call per deposit.
  • E2E on regtest. Specs that expect the Transfer Unavailable sheet take the Spark route whenever Lightspark's regtest is reachable from CI.
  • Closed-app receive. A payment received while the app is closed is claimed on the next open, with no push.
  • No BOLT12 on Spark yet.
  • App size. libbreez_sdk_spark_bindings.so adds about 11 MB per ABI (arm64, uncompressed).
  • Dependency. Lightning goes through Lightspark as the Spark service provider, a dependency like Blocktank was.
  • Testing reach. Spark's regtest has no Lightning. Without ESPLORA_URL, Spark never sees a dev build's deposit.

Design

Restored transfer screens, on the latest Bitkit - Handoff page of the Bitkit Figma file (frame names from docs/screens-map.md; the Spark speed rows, Spending fee and setting-up states have no frame yet):

  • Lightning Onboarding From Suggestion Card › Lightning onboarding Intro
  • Transfer to Spending Balance From Savings (Channel Opening) › Transfer to Spending Intro, Transfer to Spending Input, Transfer to Spending Confirm, Transfer to Spending Receiving Capacity (now Learn more)
  • Funds In Transfer › Transfer to Spending Progress 1
  • Transfer to Spending Balance From HW Device › Transfer to Spending Confirm HW, Transfer to Spending Confirmed HW
  • Transfer to Savings (Channel Closure) › Transfer to savings select funds, Transfer all funds to savings review (the Spark row and Spark fee 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.

Transfer to Spending: intro, amount, confirm, Learn More, Later Transfer to Spending: Instant Instant repriced, then Pay
android-spending-later-2x.mp4
android-spending-instant-2x.mp4
android-spending-repriced-2x.mp4
Instant unavailable Receive with no channel Lightning Connections → Spark detail
android-spending-unavailable-2x.mp4
android-receive-nochannel-2x.mp4
android-connections-2x.mp4
Transfer to Savings: Spark only Transfer to Savings: channels only Transfer to Savings: one channel plus Spark
android-savings-spark-2x.mp4
android-savings-channels-2x.mp4
android-savings-both-2x.mp4
Transfer to Savings: nothing selected Transfer to Savings: no Spark balance
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
Spending intro Funding Transfer intro Amount
Confirm, Instant Confirm, Later Learn More (transfer speed)
Funds in transfer
Setting up, waiting for Spark Setting up, adding Setting up, repriced Setting up, Instant unavailable
Setting up, Later Setting up, not seen yet Funds moved
Transfer to Spending from a hardware wallet
Transfer intro Amount Sign with your device Transaction signed
Transfer to Savings
Select funds with Spark Confirm with Spark fee Spark transfer failed
Lightning Connections
Spark row Spark detail
Send

Send: unified invoice Spending can't pay

QA Notes

Setup

  • Dev build (regtest) needs no key (Testing and development). Spark's regtest is Lightspark's own chain, separate from Bitkit's staging regtest, with no Lightning network and no public Electrum server. A dev build runs on it end to end like this:
    1. On the machine running the emulator: 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-estimates at start and /block/<hash>/status for Lightning sync.
    2. Build with 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.0 and use the host's address.
    3. Fund Savings from Lightspark's web faucet, which needs no credentials, or with FAUCET_USERNAME=… FAUCET_PASSWORD=… python3 scripts/spark-regtest-fund.py <Savings address> 100000 through Lightspark's API faucet (credentials issued by Lightspark; Breez's CI uses the same, faucet.rs).
    4. Savings to Spending, the deposit claim (after 1 confirmation on regtest), Spending to Savings and Spark payments then run on regtest. Two such dev builds pay each other's Lightning invoices over Lightning through Spark's provider, as on mainnet.
    • Spark's regtest mempool has answered every plain POST /tx with sendrawtransaction RPC error: {"code":-1}, while POST /v1/txs/package accepted the same transaction. If a Savings send fails that way, forward /tx to the package endpoint (a one-element JSON array) in the proxy.
  • Local Spark regtest stack: instead of Lightspark's hosted regtest, synonymdev/bitkit-docker runs Spark's regtest locally with ./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.
  • Transfer to Savings selection (J10): Spark's regtest has no Lightning peers, so the channel rows were staged with a local hook, not in this PR, that adds two open channels to the Savings screens. The channel cases stop before the swipe; channel closes and the swap are covered by TransferViewModelTest.kt. Channels with no Spark balance was opened through the debug screen link bitkit://screen/savings-availability, since Spending was 0.
  • A dev build without ESPLORA_URL stays 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

  • J1: updated 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.
  • J2: updated transfer/transfer-unavailable-sheet.xml — now needs Spark not running, since a ready Spark replaces the sheet.
  • J3: restored amount-limits/transfer-spending-over-max.xml — the amount pad caps at Max with the Spending Balance Maximum toast.
  • J4: restored amount-limits/transfer-spending-preset-delete.xml — after 25%, Max or a unit switch, each delete removes one digit.
  • J5: restored 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 by SparkFundingViewModelTest.kt.
  • J6: restored hardware-wallet/transfer-to-spending.xml — a paired Trezor transfers into Spark through amount, sign and signed to the setting-up screen.
  • J7: restored 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.
  • J8: restored hardware-wallet/transfer-to-spending-node-warmup.xml — a hardware transfer started during node warm-up reaches the sign screen.
  • J10: new 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.

  • 1. Dev, new wallet with no channels → Receive shows Auto and Spending tabs, and the Spending invoice's payee is Spark's node, not the LDK node — Spark's regtest is not in the journey environment.
  • 2. Dev build with ESPLORA_URL on 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.
  • 10. Dev, Spark with a balance and a deposit in progress → Lightning Connections shows Spark first with its balance and the deposit's confirmations; its detail shows provider, trust note, deposits and Transfer to Savings — Spark state is not in the journey environment.
  • 11. Dev, fund Spark from the regtest faucet to the address on the Spark screen → after 1 confirmation Spending shows the amount — faucet is outside the journey environment.
    1. Retired: it expected a Spark transfer, but Bitkit records only Lightning payments, so dev builds pay each other over Lightning as on mainnet; replaced by 42.
  • 13. Dev build with ESPLORA_URL on 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.
  • 14. Dev, wallet with Spark connected → Backup settings lists Spark exit data as synced; restore the same seed on a fresh install → the log shows Imported Spark exit data before Spark is used, and Spark's balance and deposits match — needs two installs.
  • 21. Build without a Breez API key (Spark off) → Funding and the transfer buttons behave as on master: Manual Setup only and the Transfer Unavailable sheet — any build.
  • 37. Dev on Spark's regtest chain with a Spark balance, Transfer to Savings with Spark selected and the withdrawal failing (Wi-Fi and data off before the swipe) → an error says Spark's balance stayed in Spending and no success screen shows; with channels selected too, it says the connections are still moving — needs a failing Spark withdrawal.
  • 38. Dev on Spark's regtest chain, Transfer to Savings with Spark selected → when the withdrawal reaches Savings, Home shows no Received Bitcoin sheet and Activity lists it as a transfer — Spark's regtest chain.
  • 40. Dev on Spark's regtest chain, Spark balance below its withdrawal fee plus dust → Transfer to Savings with Spark selected shows "Amount is too low to transfer to savings." and the swipe stays off — needs a tiny Spark balance.
  • 41. Dev, wallet with Spark exit data backed up → restore the seed on a fresh install with VSS unreachable during the restore: the restore reports Spark exit data as incomplete; once VSS is back, the next upload logs Merged stored Spark exit data before it replaces the stored backup — needs two installs and control of VSS reachability.
  • 42. Dev, two devices with Spark balances, pay one's Spending invoice from the other → paid over Lightning through Spark's provider, the sender reaches Sent, the receiver gets the received sheet, and both show the activity — two devices.

Automated Checks

  • added 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 state
  • added SparkFundingViewModelTest.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 error
  • added SparkClaimOfferTest.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 declined
  • added SparkServiceTest.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 restore
  • added DeriveBalanceStateUseCaseTest.kt — Spark balance counts toward Spending, caps max send, and pending deposits count as in transfer
  • added PaymentFailureReasonExtTest.kt case — Spark and SDK failures show the existing payment failed copy
  • updated ContentViewTest.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 stack
  • updated PrivatePaykitRepoTest.kt — a private invoice paid through Spark is replaced on the next refresh
  • updated PaykitPaymentProofRepoTest.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 failed
  • updated LightningRepoTest.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 authorization
  • updated BackupRepoTest.kt — SPARK_EXIT uploads 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 started
  • updated EditInvoiceVMTest.kt — Receive asks for no liquidity or CJIT while Spark can receive
  • updated TransferViewModelTest.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 transfer
  • updated NotifyPaymentReceivedHandlerTest.kt — an on-chain receive from a Spark withdrawal is a transfer with no received sheet
  • updated LightningRepoTest.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 deposit
  • updated LightningRepoTest.kt, WalletRepoTest.kt, WalletViewModelTest.kt — construct the new Spark dependency, idle, so existing channel paths are unchanged

QA

Each result at the revision it ran on. Dev builds ran on Android 15 and 16 emulators on Spark's hosted regtest.

  • ✅ J1 spark-transfer-to-spending.xml on 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.
  • ✅ 1. Receive without channels, on 7aa44f6: the Spending invoice's payee was Spark's node with a Spark route hint.
  • ✅ 2 and 13. Transfer to Spending on Spark's chain, on 1adfc47: 20,000 sats with Instant were claimed after 1 confirmation (99 sat standard claim, no early offer), and 10,000 sats with Later showed about 99 Spending fee and were claimed at 99 sats.
  • ✅ 10. Spark in Lightning Connections, on 07a1df7 and a93f3dc.
  • ✅ J10's cases by hand, on b0169b2 and 5edd5a8 with two staged channels: Select funds, partial and empty selections and the Spark fee row as listed; Spark alone swiped reached Funds moved to savings with no channel close, and Savings received 97,711 of 99,901 less the 2,190 fee.
  • ❌ 12. Two devices, on 515a606 with ESPLORA_URL on 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 method lightning, and A paid an 11-sat fee. B's Spark balance was empty before this payment.

case12-spending-invoice-a-pays-b.mp4

  • ❌ 12. Two devices, on b3298a0: A paid B's 1,000-sat Spending invoice as a Spark transfer. Both wallets' Breez SDK records list the payment with method 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: in SparkRepo.kt, recordActivity() and toNodeEvent() handle only PaymentDetails.Lightning, so a payment with Spark details writes no activity and emits no payment event.

case-12-spark-transfer.mp4

  • ✅ 42. Two devices, on 5953744: A paid B's 1,000-sat Spending invoice over Lightning through Spark's provider. Both wallets' Breez SDK records list the payment with method 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

  • ✅ 37. Transfer to Savings with Spark selected and Wi-Fi and data off before the swipe, on 515a606: Transfer Failed said "Your Spark balance could not move to savings and stays in your spending balance.", no success screen showed, and Spending kept its 4,000 sats. ⏭️ The half with channels selected: Spark's regtest has no channels.

case37-spark-withdrawal-offline.mp4

  • ✅ 38. Transfer to Savings of the whole Spark balance, on 515a606: 18,700 sats (20,890 less the 2,190 Spark fee) reached Savings while Funds moved to savings showed, no Received Bitcoin sheet appeared, and Home and Activity listed it as Transfer From Spending.

case38-spark-to-savings-transfer.mp4

  • ⏭️ 14 and 15: run on mainnet only, under Mainnet tests.
  • ⏭️ 21: needs a build without a Breez key.
  • ⏭️ 40 and 41, the funding recovery, the claim states, the per-rail settlement, the pending handling of a Spark send, the Transfer to Savings fee gate and its dust copy, and the SPARK_EXIT merge before the first upload: covered by SparkRepoTest.kt, TransferViewModelTest.kt and BackupRepoTest.kt only. 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> in local.properties. Round 1 ran on a Pixel 10 Pro.

Round 1

  • Transfer to Spending: Spark claimed the deposit after 3 confirmations, on ef7130e (result).
  • Receive over Lightning with no channel and an empty Spark balance, then pay from it two minutes later, on 567e1a8 (result).
  • Transfer to Savings of the Spark balance, on a head before the 27 Oct rebase (result); again on a QA build of 171b250 with master merged (result not posted).
  • Instant claim: the top-up was spendable before its first confirmation, on a QA build of 171b250 with master merged, through the earlier Use now screen (result not posted).
  • Spark's fee estimate read from https://bitkit.to/electrs/fee-estimates, read-only check (result not posted).
  • Wipe and restore on the same phone: the balance and claimed deposits came back, but 0 exit-data leaves were imported because the exit data was restored after Spark had synced; fixed in f96bae9, not rerun (result).
  • Restore on a QA build of 171b250: Spending balance error, from a race between the restore and the network graph reset; the same code is on master, so it's not Spark-specific (result not posted).
  • Paykit request paid over Spark: the payment arrived at once, but the iPhone payer stayed on Payment Pending; fixed in 024b6bb and synonymdev/bitkit-ios@735e972e, not rerun (result).

Round 2 (pending)

On the final heads, with a mainnet build and small amounts.

  • Transfer to Spending with Instant: the setting-up screen says Spark is adding it, then Funds moved to spending before 3 confirmations; when the provider asks more than the ceiling, it shows Instant now costs X with Pay and Wait.
  • Instant repriced: Pay claims at X; Wait lands after 3 confirmations.
  • Instant, leave the setting-up screen at once: the claim still happens within the ceiling.
  • Transfer to Spending with Later: no early claim; the deposit lands after 3 confirmations at about the shown fee.
  • Hardware wallet Transfer to Spending: sign on the device, the setting-up screen follows the deposit, and the transfer lists once under the hardware wallet.
  • Spark ready, no channels: Funding shows Transfer from Savings and Use Other Wallet above Manual Setup, and both transfer entries open the same flow.
  • No channels, receive over Lightning: the received sheet and activity show it.
  • No channels, pay a Lightning invoice: paid through Spark; after Spark's next sync the activity shows Spark's fee, not 0.
  • Receive while the app is closed, then open it: the payment is claimed and shown.
  • Pay an invoice and drop the connection right after the swipe: Payment Pending shows, then it settles as sent or failed; paying the same invoice again sends nothing twice.
  • Transfer to Savings with only a Spark balance: the arrival shows as a transfer From Spending with no received sheet, and the Spark screen lists the withdrawal with its fee.
  • Transfer to Savings with a channel next to Spark: deselecting Spark keeps it in Spending and drops its withdrawal fee.
  • Existing channel: payments within its capacity still go over the channel.
  • Restore on another device: Spark exit data is imported before Spark is used, and the Spark balance and deposits come back.
  • Two Bitkit contacts, one Spark-only: a Paykit request paid over Spark ends on Sent for the payer and paid for the payee; a second Lightning-only request offers Spending and pays.
  • Spark's fee estimate and chain reads go to https://bitkit.to/electrs.

Models used

  • Planning/scoping: claude-opus-5-5 (reasoning: medium), claude-sonnet-5-5 (reasoning: high)
  • Implementation: claude-opus-5-5 (reasoning: medium)
  • Review: 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-5 planning runs were research and audit subagents.

@ovitrif ovitrif added this to the 3.1.0 milestone Oct 26, 2026
@ovitrif ovitrif self-assigned this Oct 26, 2026
@ovitrif
ovitrif force-pushed the feat/breez-spark-lightning branch from bf469f4 to ef7130e Compare October 26, 2026 13:49
@ovitrif
ovitrif force-pushed the feat/breez-spark-lightning branch 4 times, most recently from 01334a9 to 4ed36e0 Compare October 26, 2026 17:20
@ovitrif
ovitrif force-pushed the fix/blocktank-sunset-sheet branch from a1412d5 to a1c333c Compare October 26, 2026 17:23
@ovitrif
ovitrif force-pushed the feat/breez-spark-lightning branch 3 times, most recently from 9982a55 to d964a5f Compare October 26, 2026 22:47
@ovitrif ovitrif changed the title feat: keep lightning through breez sdk on spark feat: add spark via breez sdk Oct 26, 2026
@ovitrif
ovitrif force-pushed the feat/breez-spark-lightning branch from eab67dd to 567e1a8 Compare October 27, 2026 01:46
@ovitrif ovitrif mentioned this pull request Oct 27, 2026
5 of 7 tasks
@ovitrif

ovitrif commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator Author

QA:
Tested on a Pixel 10 Pro, mainnet, by @ovitrif, 26–27 Oct; amounts are sats. Balances, names, addresses and txids are masked.

🟢 Restore Spark exit data
Exit 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 deposit
Transfer 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 pay
With 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 Savings
Transfer 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 Blocktank
On 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 Spark
The 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.

@ovitrif
ovitrif force-pushed the fix/blocktank-sunset-sheet branch from 901ecd8 to 2ba664e Compare October 27, 2026 17:35
@greptile-apps greptile-apps Bot mentioned this pull request Oct 27, 2026
3 tasks
@ovitrif
ovitrif changed the base branch from fix/blocktank-sunset-sheet to master October 28, 2026 08:29
@ovitrif ovitrif removed this from the 3.1.0 milestone Oct 28, 2026
@ovitrif

ovitrif commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

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.

@ovitrif
ovitrif force-pushed the feat/breez-spark-lightning branch from 024b6bb to d7be600 Compare October 28, 2026 14:35
@ovitrif

ovitrif commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

Retargeted this draft to master before merging its base, #1696, so deleting that branch would not close it.

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detekt found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@ovitrif

ovitrif commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

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.

@ovitrif

ovitrif commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 515a606, with fixes for @coreyphillips's recheck 5850123564 and @pwltr's iOS review. Each has a test that failed first.

  • Sync freshness (7962bd4): hasSyncedSince now needs a non-null lastSyncedAt in every case, including the no-time check. So a Synced event without a successful wallet sync no longer makes Spark's list readable, and settleUncertainSend and start() count only a sync that succeeded.
  • lightningSend and a send Spark may still resume (7b247f3): answered in its thread.
  • Spark to Savings across two regtest chains (515a606): Savings funding was already gated on Env.sparkSharesNodeChain, and the withdrawal wasn't. It now skips the send and fails clearly when the chains differ. Mainnet isn't affected.

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 (LightningRepo.kt:1536-1541) and restored on a Spark refusal (:1556-1558), and the node rail only after admitSend succeeds (:1640-1650).

The iOS twins are on synonymdev/bitkit-ios#1128.

@jvsena42, a re-review is needed only because of this push: 50bc48b...515a606

@coreyphillips coreyphillips left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 no lastSyncError and a non-null lastSyncedAt later than the failure. A missing send settles only after such a sync and its invoice's expiry (:370-373), and start() (:189-190) and the Synced handler (:1021) use the same check. In Breez's sync_wallet_internal, last_synced_at is written only when the wallet sync returns Ok, and last_sync_error comes 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 returns Found(PENDING) for a hash that is uncertain or saved and unexpired. It does the same for a FAILED row while the send is uncertain. payInvoice turns that into PaymentPendingException (:297-298), PaykitAllowanceExecutor reads it as PENDING (:160-163), and PaykitPaymentProofRepo keeps the proof (:1319-1337). The settle paths read the raw row through listedLightningSend (: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.xml files: lightning__savings_spark_failed__title, lightning__spending_confirm__error_quote, lightning__spending_confirm__max_changed and lightning__spending_confirm__max_changed_title. Three English strings changed with their translations left as they were: lightning__funding__button1, wallet__receive__cjit and wallet__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 no values-*/strings.xml file (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)

@ovitrif

ovitrif commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

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

@ovitrif

ovitrif commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Ran cases 12, 37 and 38 on 515a606, dev builds with ESPLORA_URL on Spark's hosted regtest, two Android 16 emulators:

  • ❌ 12: A paid B's Spending invoice and both show the activity, but the payment went over Lightning through Spark's provider (Breez SDK method lightning, 11-sat fee), not as a Spark transfer.
  • ✅ 37: with the network off, the Spark withdrawal failed with the "stays in your spending balance" error and no success screen. The half with channels selected is not run, since Spark's regtest has no channels.
  • ✅ 38: the withdrawal reached Savings as a transfer, with no Received Bitcoin sheet.

Results and recordings are in the QA section.

@ovitrif

ovitrif commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed b3298a0 to fix case 12 on regtest: two Spark wallets now pay each other's Lightning invoices as Spark transfers.

  • What failed: case 12 at 515a606 paid over Lightning through Spark's provider (Breez method lightning, 11 sat fee), even though the payer sends with preferSpark.
  • Why: the Breez SDK adds the receiver's Spark address to an invoice only when Config.preferSparkOverLightning is on, and its default is off. Without that address, the payer's preferSpark has nothing to pay to, so it falls back to Lightning.
  • Fix: SparkService.configure turns it on for regtest only, the same rule as prefersSpark. Mainnet stays as it was. SparkServiceTest.kt covers it, and it failed before the fix.
  • Checks: just compile, just test and just lint pass. Case 12 runs again on this head next.

The twin fix is on synonymdev/bitkit-ios#1128.

@coreyphillips @pwltr @jvsena42, a re-review is needed only because of this push: 568e277...b3298a0

@ovitrif

ovitrif commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Case 12 again on b3298a0, two devices: ❌, now on the UI side.

  • The payment went over Spark as intended. Both wallets' Breez SDK records list it with method spark, a 0-sat fee and status completed, and the payee's Spending went from 4,000 to 5,000.
  • Neither wallet shows it. The payer stays on Payment Pending, the payee gets no received sheet, and neither activity list has the payment.
  • Cause: SparkRepo.recordActivity() returns early unless payment.details is PaymentDetails.Lightning, and toNodeEvent() returns null in the same case. A Spark transfer carries Spark details, so no activity is written and no PaymentSuccessful/PaymentReceived event reaches the send and receive sheets. Both need a branch for PaymentDetails.Spark.

Recording and details are in the QA section of the description.

@ovitrif

ovitrif commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 5953744, which reverts b3298a0. Two dev builds go back to paying each other over Lightning through Spark's provider, the path mainnet uses.

  • What the rerun showed: case 12 at b3298a0 paid as a Spark transfer (Breez method spark, fee 0), but neither wallet showed it. The payer stayed on Payment Pending, and the receiver never got the received sheet.
  • Why: Bitkit records activity and emits payment events only for PaymentDetails.Lightning (SparkRepo.recordActivity, toNodeEvent). A Spark transfer carries PaymentDetails.Spark, so it gets neither.
  • The change: preferSparkOverLightning is off again on every network, and SparkServiceTest.kt checks that it stays off. That test fails on b3298a0. The prefersSpark KDoc no longer claims that Spark's regtest has no Lightning.
  • Case 12: retired, because it expected a Spark transfer. Case 42 replaces it and expects a Lightning payment that both wallets record. The ❌ results for 12 stay as history.
  • Checks: just compile, just test and just lint pass. Case 42 runs on this head next.

@coreyphillips @pwltr @jvsena42, a re-review is needed only because of this push: b3298a0...5953744

@ovitrif

ovitrif commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Case 42 on 5953744, two devices: ✅. The payment went over Lightning through Spark's provider (Breez method lightning, 3-sat fee), the payer reached Bitcoin Sent, the payee got the received sheet, and both activity lists show it (Sent 1,003 and Received 1,000). Recording in the QA section of the description.

@pwltr pwltr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@ovitrif

ovitrif commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

@pwltr

High candidate — An older failed Spark attempt can release an unresolved retry after restart.

Addressed in 6cdf592. A retry whose outcome Spark didn't report is now kept in the cache store, not only in memory. start takes it back before Spark connects. The activity sync's trim, the explicit release of failed hashes, lightningSend and earlierLightningSend all treat it as live until a sync after the failure and the invoice's expiry settle it, or a payment event for it arrives. New tests in SparkRepoTest.kt: an uncertain spark retry stays held after a restart while spark lists only its older failure and … after a restart whose first sync fails. Both failed on 5953744 with "A restart released a retry Spark may resume".

Low candidate — A late failure event can clear a newer retry's hold.

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.

Low candidate — A completed withdrawal can leave the old Spending balance displayed.

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 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: earlierNodeSend throws NodeNotStarted (LightningRepo.kt:1587-1590), as coreyphillips asked.
  • hasSyncedSince: needs a non-null lastSyncedAt and 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 at TransferViewModel.kt:415 and :439-444.
  • Amount and fee binding: the send path is unchanged. Savings transfers snapshot channelsToClose and withdrawsSpark at 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), and cacheStore.reset() runs in WipeWalletUseCase:113.
  • perf: each Synced event 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

@ovitrif

ovitrif commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

@jvsena42
This push fixes both LOWs from your review, as proposed and answered in their threads. A Spark send cut off mid-call by process death or cancellation stays held, and QuickPay and Paykit read an uncertain retry as pending. Your three reproductions are in SparkRepoTest.kt and failed first. just compile, just test and just lint pass. The iOS twin is being checked for the same two gaps.

re-review is needed only because of this push: 6cdf592...4f10f8d

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 4f10f8d1f: both LOWs are fixed as proposed, and nothing new. Approving.

  • The uncertain mark is written right before sparkService.send and 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 through settleUncertainSend, which overwrites the mark with the failure time.
  • lightningSendStatuses and attemptedLightningPaymentHashes now read the uncertain set, so QuickPay and Paykit see an uncertain retry as pending.
  • The three new SparkRepoTest cases 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

@ovitrif

ovitrif commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

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 scripts/spark-regtest-esplora.py, both with a Spending balance and no Lightning channel.

  • ✅ 42. route-a made a 1,000-sat invoice on Spending and route-b paid it. Over Lightning through Spark's provider: route-a logs canReceive = false: Found no LN channel usable before it. The sender reached Sent (Sent Spark payment … with status 'COMPLETED', PaymentSuccessful). The receiver got the received sheet (PaymentReceived, 1,000,000 msat, same hash). Both list the activity, and Spending moved 11,131 → 12,131 on route-a. A second invoice paid the same way.
  • ✅ J1, steps 3-10. Transfer from Savings opened the Transfer to Spending intro and then the amount screen. 25% and Max filled 3,659 and 14,635 of ₿14,635. Confirm showed the four tiles with Instant (±326) and Later (±99). Later updated the fee and To spending tiles. Learn more opened Transfer Speed and returned to confirm with Later kept. The intro did not show again.
  • ⏭️ J1, steps 1-2: the Spending suggestion card shows only on a wallet with no Spending balance, and both wallets had one, so the Spending Balance intro and Funding were not run here.
42 sender 42 receiver J1
c42-route-b2x.mp4
c42-route-a2x.mp4
j1-route-a2x.mp4

@ovitrif

ovitrif commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Merged: approved by @jvsena42 at 4f10f8d, required checks green, every review thread answered and resolved, device run in the comment above. The stacked PRs (#1782, #1774, #1754, #1831) now target master.

@pwltr pwltr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@ovitrif

ovitrif commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

@pwltr

High candidate — Require complete Spark history before allowing an uncached channel retry after restore. (SparkRepo.kt:906)

Real: tracked in #1855 and fixed in #1858.

  • After a restore, a payment over channels is refused for invoices dated before the restore until Spark lists its sends. Invoices created after the restore pay normally, even while Spark is down.
  • Installs that never restored aren't held: every Spark Lightning send saves its hash before it goes out, and no released build sent over Spark.
  • The node side had the same gap in reverse: a restore copied the old device's node send list as complete. It's held the same way until the node lists its payments.
  • The iOS twin is fixed in feat: add spark via breez sdk bitkit-ios#1128.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: spark lightning via breez sdk

7 participants