Repository navigation
Conversation
|
Pushed |
Regtest APKDownload bitkit-dev-debug universal APK (expires in 30 days). |
|
Added to the 3.2.0 milestone: Spark via Breez SDK is the 3.2.0 highlight, and this PR stacks on #1697. |
ab34526 to
ab1b09d
Compare
Rebuilt on the split Spark head; carries the Pubky Swaps route (#1732) as it was merged into this branch.
c08023a to
f00fb41
Compare
# Conflicts: # gradle/libs.versions.toml
|
@coreyphillips your two blocking threads are fixed in d1b899a: a Savings payment to a mixed contact now sends on-chain without fetching from its LNURL. 5042f41 had only fixed the Continue check, not the swipe.
Fixed in #1858, where this code comes from (it merged to
Fixed in b43fae9. A Direct LNURL pick clears @jvsena42 both LOWs are fixed here (d1b899a, 315cd03 and ba94a58); see their threads. This push also ports the fixes from the iOS twin's latest review (synonymdev/bitkit-ios#1191):
Each fix has a test that failed first. Compile, the full unit tests, detekt and lint pass. re-review is needed only because of this push: 5042f41...ba94a58 |
|
CI failed at ba94a58 because
Compile, the full unit tests (5,470), detekt and lint pass. re-review is needed only because of this push: ba94a58...1f1a42e |
jvsena42
left a comment
There was a problem hiding this comment.
Re-review of the eight own commits since 5042f416f (head 1f1a42e0f): my three earlier LOWs are fixed, two new LOWs inline, approving.
- No LNURL fetch for an on-chain pick (
d1b899a97):isLnurlPaynow needspayMethod == LIGHTNINGand no service leg (AppViewModel.kt:6604-6605). - Confirm layout (
315cd03bf,ba94a584e):paysLnurldrives the title and layout, so an on-chain pick showsOnChainDetails(address, miner fee, speed) and a leg showsLightningDetailswith the Spark route and fee. That also covers the master behaviour in my out-of-scope note. - bitkit-core rc14 (
1f1a42e0f):PaykitRouteReposetsproviders = emptyList()andfeeRateSatVb = 0. The app calls onlypaykitRoutePayment, which never returnsSwap, and aSwapwould be refused with theNonebranch (:4859), paying nothing. - Fee-rise close (
6370c373f): breez spark-sdk 0.28.0 raises thatInvalidInputbeforespark_wallet.withdraw, so the journal closes with nothing in flight. a5022fea8,eb6e8e1ef,b43fae99a, mergef2654b086: no issues.
The new settling string is English only, which is the Spark translation deferral agreed in #1840.
Both LOWs are display-only. They can be fixed here or moved to a follow-up issue, your choice; please say which on each thread.
Device gate: no device check needed for this delta; the reproductions are Robolectric and unit tests. CI: build-and-test, lint and detekt pass; the ui-tests and e2e shards were still running.
Reviewer model: Claude Fable 5.2 (claude-fable-5-2), Claude Opus 5.5 (claude-opus-5-5)
Reasoning effort: not exposed
|
Pushed 3815386: jvsena42's two LOWs from the last review, each with a test that failed first.
Tests: compile, detekt, lint and unit tests pass. Re-review is needed only because of this push: 1f1a42e...3815386 |
pwltr
left a comment
There was a problem hiding this comment.
Reviewed 3815386.
Medium candidate — Keep withdrawal recording protected from a stale sync snapshot. In app/src/main/java/to/bitkit/repositories/SparkRepo.kt:783-809, the in-flight flag clears before the returned payment is recorded. For an attempt older than the ten-minute grace, a concurrent sync can use a snapshot taken before the successful send, clear its journal, and leave recordWithdrawal at :1444-1451 with neither a journal nor an existing activity. The successful contact withdrawal can then go unrecorded. Keep protection through response recording and reject stale snapshots before negative settlement.
The previously described duplicate-payment consequence is not established: recovery reads a fresh payment list, and the SDK 0.28.0 storage-before-return behavior described in the discussion would preserve that payment. The remaining candidate concerns the activity/journal race; its aged-send interleaving still 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
Spark lists a withdrawal completed once its provider accepts the payout, before the payout transaction is broadcast or mined (spark-sdk 0.28.0, crates/spark/src/ssp/graphql/models.rs:187-209 and crates/breez-sdk/core/src/models/adaptors.rs:339-347). A contact withdrawal's activity now stays unconfirmed until Electrum lists its payout output in a block.
A sync whose payment list was read during a withdrawal's send, or before the send ended, no longer drops the journal of an aged withdrawal, so the withdrawal Spark returned is always recorded.
|
Pushed e0221f9 with three fixes to Spark withdrawals. Each new test failed before its fix. Compile, detekt and all 5,479 unit tests pass.
Re-review is needed only because of this push: 3815386...e0221f9 |
|
Reran the failed CI, Lint and E2E jobs on e0221f9. They failed when GitHub's artifact upload returned an HTML error page ("Failed to CreateArtifact"), during the GitHub outage. The unit tests that ran had all passed. |
# Conflicts: # app/src/main/java/to/bitkit/repositories/SparkRepo.kt
|
Pushed c220ce5: master (with synonymdev/bitkit-android#1858) merged in. The one conflict is in Re-review is needed only because of this push: e0221f9...c220ce5 |
Master's restore tests stubbed SparkService.send with one matcher, and this branch gives send an idempotency key, so Mockito refused the stubs once both met.
|
Pushed b7c748f, which fixes the 9 Re-review is needed only because of this push: c220ce5...b7c748f |
Closes #1807
Closes #1850
Twin: synonymdev/bitkit-ios#1191
Dependency:
This PR routes every bitcoin Paykit contact payment through bitkit-core's Paykit router, so a wallet whose spending balance sits on Spark pays contacts directly over Lightning, or through a Spark withdrawal to a contact who only accepts on-chain bitcoin.
Description
paykitRoutePayment) how to pay every bitcoin contact payment: on the amount screen's Continue, when a payment request's Confirm opens with its fixed terms, and when the source is switched on Confirm. The router gets the contact's endpoints and what the selected source can spend:Directpays the endpoint it names: Lightning (bolt11 first, then LNURL) or the on-chain address.Serviceopens Confirm withRoute: Sparkand the leg's fee in the fee row. The swipe withdraws exactly the amount to the contact's address, so the contact receives exactly the amount entered.Noneshows "Insufficient balance" when the amount is above what the source can spend, else "Can't pay this contact" (translated in every locale). The router's reason goes to the log only.SparkRepo.withdrawalRouteLeg). The withdrawal sends only at the debit Confirm showed: a quote that rose since fails with a fee-changed error, and nothing is sent.0.8.0-spark.3, a prerelease of feat: pay contacts via spark bitkit-core#193. No build flag.What it needs to ship
Out of Scope
Design
N/A — no design available.
Preview
Two regtest Android emulators (896×1994, dark mode) with Spark on Lightspark's hosted regtest and a local Pubky homeserver, one recording per flow at 2x speed; A is Alice and B is Bob, each other's contacts, both Spark only with no channel.
1 and 2: A pays B 300 over B's private endpoint through the router; then B's app is closed while A pays 250, and B's relaunch shows it received from Alice.
1754-p1-a2x.mp4
1754-p1-b2x.mp4
1754-p2-a2x.mp4
1754-p2-b2x.mp4
3: A pays the on-chain-only fixture contact 2,000 through a Spark withdrawal (
Route: Spark, 2,190 fee); 4: A requests 200 and B pays it from Spark.1754-p3-a2x.mp4
1754-p4-a2x.mp4
1754-p4-b2x.mp4
Flow 4 runs with the roles the other way round from the iOS twin: here A requests and B pays.
5: A requests 2,000, B (the payer) dismisses it from Payment Requests, and A's request reads Rejected.
1754-p5-b2x.mp4
1754-p5-a2x.mp4
6: B's allowance ($1 a payment) auto-pays A's 300 request from Spark and keeps the 2,000 one manual; ended: after B ends it, A's 200 request stays manual.
1754-p6-b2x.mp4
1754-p6-a2x.mp4
1754-p6e-b2x.mp4
1754-p6e-a2x.mp4
7: B proposes a 100-sat daily subscription, A subscribes and pays the first period from Spark, and it is still active after A relaunches; 8: B requests $0.20 and A pays it at its quote from Spark.
1754-p7-a2x.mp4
1754-p7-b2x.mp4
1754-p8-a2x.mp4
1754-p8-b2x.mp4
9: B turns private contact payments off (A can't pay) and on again (A pays 100); 10: an amount above the balance can't be entered, and the sendable maximum pays; 11: A is killed right after the swipe, and nothing is paid twice after the relaunch.
1754-p9-b2x.mp4
1754-p9-a2x.mp4
1754-p10-a2x.mp4
1754-p10-b2x.mp4
1754-p11-a2x.mp4
1754-p11-b2x.mp4
Screens
Dev debug build on a regtest Android emulator (896×1994 at 320 dpi), dark mode, Spark on Lightspark's hosted regtest; every state was reached in the flows above, with no debug hooks.
Confirm
Activity
Spark screen
QA Notes
Setup
mainwith the marketplace and payment-requests profiles; itsfixture-issueris the on-chain-only contact (J1, Preview 3).E2E=true E2E_BACKEND=local E2E_LOCAL_HOST=127.0.0.1 E2E_HOMESERVER_PUBKY=$(./pubky-marketplace info | jq -r .homeserver_z32), withadb reverse6286, 6287, 6288, 15411, 15412, 60001, 23735, 23000 and 23010.bitkit://contact?pubky=<pubky>).Journeys
paykit-spark-route.xml— a wallet whose spending balance is on Spark pays an on-chain-only contact through a Spark withdrawal, and the contact receives exactly 20,000 satsManual Tests
Two contacts as above, both Spark only (no channel); either one can play payer or payee.
Route: Sparkwith the withdrawal's fee, the withdrawal pays exactly the amount to the payee's address, and the payee's request shows "Proof submitted".Evidence
Dev debug builds on the staging backend, on two regtest Android emulators (API 35), Spark on Lightspark's hosted regtest, no channel. Both wallets were Spark only, except in cases 10 and 11, where the payer's Savings was funded with
./lsp POST /regtest/chain/deposit. Each line names the head it ran at.1cbb4b6c9: 300 sats paid; the payer showed Sent 301 (fee 1) to the contact with its preimage, and the payee showed Received 300 from the contact. Through the router, Direct bolt11: 120 sats atbd755b839, and 110 sats at25b40e0fdon bitkit-core0.8.0-spark.3.case 1
case 1 on spark.3
1cbb4b6c9: after the payee's relaunch the payer opened list version 5 after 0 and paid 150 sats.f11-rotate2x.mp4
1cbb4b6c9: the force-stopped payee received 250 sats; on relaunch Home showed Received 250 from the contact and no duplicate rows.case 3 payer
case 3 payee
1cbb4b6c9: a 200-sat request was paid, and the payee showed "Proof submitted".case 4 payee
case 4 payer
bd755b839: both sides showed Rejected.f5-reject-payer2x.mp4
bd755b839: the 300-sat request was paid automatically ("Paid automatically $0.23"); a 2,000-sat request was kept manual ("the allowance limit is reached"); after End Allowance, a 200-sat request was presented for manual payment.case 6 payer
case 6 payee
1cbb4b6c9: with 4,473 sendable, 6,000 and 44,731 could not be entered; 4,473 was paid, quoted at ± ₿ 12.f9-above2x.mp4
bd755b839: two kills before Spark dispatched left the balance unchanged and the request payable; a kill after dispatch is Flow 10 below.case 8
case 8 relaunch
25b40e0fd: the first 100-sat period was paid from Spark and its proof queued; after a force-stop and relaunch the payer listed the subscription as Active, renewing the next day. Later periods not observed yet.25b40e0fd: from Savings, the router paid the payee's on-chain endpoint 15,000 sats at Normal (₿ 141); the payee showed Received 15,000 from the contact in Savings.f4-onchain2x.mp4
47a1484a8: switching to Spending showedRoute: Spark; Spark's withdrawal365dc1df…paid exactly 2,000 sats to the payee's address and confirmed on Lightspark's chain (the payee's node runs on staging's chain, so its Savings doesn't show it); the proof was delivered and the payee showed "Proof submitted"; the payer's activity showed the contact. Confirm quoted ± ₿ 750 while Spark charged 2,190, and the activity read as a transfer:47a1484a8fixes both.f5-onchain-payer2x.mp4
47a1484a8: Confirm opened from Spending at Instant (± ₿ 1); 200 sats sent and received.f5-router-request22x.mp4
47a1484a8: Spark refused the send before it left ("Spark could not prepare the payment"), the payment-failed screen showed, and after reconnecting no payment had been made.f9-offline2x.mp4
J1 at
28a63f8on bitkit-core0.8.0-swaps.3, before the router: Spark's withdrawal56263be8…paid exactly 20,000 sats to the contact. Not rerun since.Flow 3 at
0f5d7e531, on a dev build withE2E_BACKEND=localandESPLORA_URLon Spark's chain, payer with Spark and Savings: the payee is bitkit-docker'sfixture-issuer, which publishes only abtc-regtest-p2wpkhendpoint, since Android has no Lightning-only sharing toggle. From Spending, Confirm showedRoute: Sparkat ± ₿ 2,190. The swipe sent 2,000 sats, Spending dropped from 14,901 to 10,711 (2,000 plus the 2,190 shown), and the activity reads "Sent to" the contact with a 2,190 fee, not a transfer. Spark's withdrawal413332e8…paid exactly 2,000 sats to the payee's address.flow3-onchain-contact-via-spark.mp4
0f5d7e531, same build: bitkit-docker'src56-peerrequested 2,000 sats on-chain, the payer paid it from Spending through Spark (Route: Spark), and the app was force-stopped the moment it loggedSent Spark withdrawal. After the relaunch, the new process submitted the request's proof and the request reads "Proof submitted" (ProofSubmittedon the peer). Spending was debited once, from 10,711 to 6,521, and Spark's withdrawalf5556e33…is the only transaction to the payee's address. The 2,000-sat requests shown after the relaunch are two other requests, still open. ⏭️ The allowance: allowance auto-pay pays only Lightning invoices (PaykitAllowanceExecutor), so an allowance never reaches a Spark withdrawal.flow10-kill-after-dispatch.mp4
flow10-relaunch.mp4
Paykit flows with a Spark balance
Flows from the v0.1 specs at pubky/paykit-rs
cb8cba5. State a is Spark only, with no channel. State b is a channel too small for the amount, with Spark covering the rest. The case numbers above give the evidence.LightningRepoTest,PaykitAllowanceRepoTest).0.8.0-spark.3; ⏭️ public endpoint, because both test wallets hold a private link, which is paid first.Automated Checks
AppViewModelSendFlowTestcases: router Direct Lightning, Direct on-chain from Savings, Service leg, None with a short balance and None with an unpayable contact; the Spark leg runs only from Spending; a request's Confirm pays the endpoint the router picks; an on-chain request paid through the Spark leg starts and completes its proof, and a failed leg releases an unsent proof; a source switch on Confirm reroutes; Spark leg fees count in the warnings; Confirm's tags reach the withdrawal; an on-chain request Savings can't cover goes to SparkPaykitPaymentStateBackupTestcase: a Spark withdrawal proof keeps its rail through backupPaykitPaymentProofRepoTestcases: an on-chain proof completed with a Spark withdrawal's txid, one settled from Spark's payment list on reconcile, and one released when its withdrawal never sentSparkRepoTestcases: a withdrawal quoted as a route leg, no leg the balance cannot cover, a withdrawal recorded with its contact and fee, a second withdrawal refused while one is unsettled, a journaled withdrawal settled or dropped on start, a withdrawal refused right before it sends or above the confirmed debit, the journal kept when the activity fails to save, an earlier withdrawal never settling a new attempt, tags on a recovered withdrawal, a contact withdrawal not marked as a transfer, and kept off the Spark screen's transfersjust compile,just test,just lintand the androidTest compile on bitkit-core0.8.0-spark.3: passMainnet 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.
Round 1
None yet: the flows above ran on regtest.
Round 2 (pending)
On the final heads, two mainnet wallets that are each other's Paykit contacts, small amounts.
Models used
claude-opus-5-5(reasoning:medium),claude-sonnet-5-5(reasoning:high)claude-opus-5-5(reasoning:medium)Taken from the run logs of every session and subagent that planned or wrote this PR, which record the model and reasoning effort per turn. The
claude-sonnet-5-5planning runs were research and spec-mapping subagents. No independent agent review was run on this PR.