Repository navigation
Conversation
|
Pushed |
|
Added to the 3.2.0 milestone: Spark via Breez SDK is the 3.2.0 highlight, and this PR stacks on #1128. |
A spending-only wallet pays a Paykit contact whose only endpoint is on-chain through a Pubky Swaps provider found among its contacts. Behind PUBKY_SWAPS_ROUTES on regtest debug builds. Uses bitkit-core 0.8.0-swaps.2.
ffba7c7 to
0eed9c2
Compare
f376aa7 to
438d6ae
Compare
|
Force-pushed: rebuilt on #1128's new head |
Moves to bitkit-core 0.8.0-spark.3 and its `paykitRoutePayment`, and removes the Pubky Swaps code: the build flags, provider quotes, commit and claim, and their journey and changelog fragment. - On the amount screen, every bitcoin contact payment asks the router how to pay from the selected source: Spending offers all of the contact's bitcoin endpoints and a Spark leg to its on-chain ones, Savings only the on-chain endpoints. Direct pays the endpoint it picks, a Spark leg opens Confirm with Route: Spark and its fee, and none stays on the amount screen with the app's copy, its reason going to the log only. - An on-chain-only contact opens with Spending as a source while Spark can carry the payment, whether or not Savings covers it. - A Spark withdrawal to a contact is journaled before it is sent, settles from Spark's payment list on its event or on the next start, and is recorded as a sent on-chain activity for the contact, confirmed once Spark completes it. A second one waits while the first is open.
- A payment request's Confirm, which opens with fixed terms and no amount screen, asks the router too: Direct switches the send to the endpoint it picks. - An on-chain request can be paid from Spending through a Spark withdrawal. Its on-chain proof is started before Spark sends and completed with the withdrawal's txid, the request's payment check runs right before the send, and the withdrawal journal completes or drops the proof when the app stopped mid-send.
- A Spark leg to a contact's on-chain address now counts the L1 broadcast fee Spark's prepare reports, so Confirm shows the whole fee, and the withdrawal refuses a fee above the one Confirm showed. - A withdrawal to a contact is never marked as a transfer to Savings, so its activity stays a send to the contact.
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Ready for review: Paykit payments to contacts work when the balance sits in Spark. It's stacked on #1128 and builds on bitkit-core 0.8.0-spark.3, a prerelease of synonymdev/bitkit-core#193. Every bitcoin Paykit payment goes through the core router with the contact's full endpoint list. On-chain endpoints and requests can be paid from Spark. The QA notes list each Paykit flow with its result on Spark-only wallets and the revision it ran on. Flows we couldn't stage are marked ⏭️ with the reason; the on-chain Spark withdrawal after the fee fix is covered by unit tests only. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0a47cc8d38
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
[Critical impact] This PR reaches Bitcoin contact payments that can send funds through Spark. Fix request approval and withdrawal recovery before merging. Findings
SummaryRoutes Bitcoin contact payments through
Review was based on source inspection. No iOS build, device run, or visual capture was performed in this Linux environment. Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Bitcoin contact payment] --> B[Choose Spending or Savings]
B --> C[paykitRoutePayment]
C --> D[Direct]
C --> E[Service: Spark]
C --> F[None]
D --> G[Pay chosen endpoint]
E --> H[Review amount and fee]
H --> I[Write withdrawal journal]
I --> J[Send through Spark]
J --> K[Save proof and sent activity]
F --> L[Show payment error]
Reviews (1) · Last reviewed commit: "fix: charge the spark leg its broadcast ..." · Reviewed by Greptile |
|
Triage: in 3.2.0. This PR closed no issue, so I filed #1244 to carry the milestone, moved 3.2.0 from this PR to that issue and added |
talosmachina
left a comment
There was a problem hiding this comment.
1 finding (1 P1). Routes Paykit contact payments through bitkit-core's router and pays an on-chain-only contact from Spending with a Spark withdrawal, journaled so an interrupted one settles from Spark's list. Reviewed 0a47cc8, full tier, statically: iOS does not build on this reviewer's Linux host.
What I checked
Read: the PR diff of SparkManager.swift, PaykitRouteService.swift, PaykitPaymentProofService.swift, SendConfirmationView.swift, plus PaykitPaymentProofService.reconcile and failOnchainPayment
Compared with the Android twin: here a definite pre-send refusal and a synced list without the withdrawal both fail the started proof, which Android does not do yet
CI: SwiftFormat green, Run Tests still running at review time
Not done: the cold-reader pass on this finding did not finish in time, so it rests on my own trace
Merge confidence: 3/5, one P1.
Reviewer model: claude-opus-5-5
Reasoning effort: Not exposed (the runner reports no effort setting)
Keep the withdrawal journal until the proof, activity and tags are saved, match a recovered withdrawal to its own attempt, prepare and release a request on the Spark leg like a direct send, tell a pre-send refusal from the send itself, pick a request's source by what Savings covers, reroute on a source switch, and use the leg's amount and fee in Confirm warnings.
Settle the journal after an uncertain send only when that pass's sync succeeded, and close an attempt Spark never returned nor listed as unsent only after a 10-minute expiry.
Closes #1244
Twin: synonymdev/bitkit-android#1754
Dependency:
This PR routes every bitcoin Paykit contact payment through bitkit-core's Paykit router, so a wallet whose money sits in Spark can pay any contact, including one who only accepts on-chain bitcoin.
Description
0.8.0-spark.3and itspaykitRoutePayment, which takes the contact's endpoints, what the wallet can spend over Lightning (Spark included) and on chain, and service legs, and returns Direct, Service or None.Route: Sparkand the leg's fee; the swipe withdraws exactly the amount to the contact's address.Design
N/A — no design available.
Preview
A wallet whose spending balance is held on Spark pays a contact who accepts only on-chain bitcoin. The router picks a Spark withdrawal, the confirm screen shows
Route: Spark, and the contact receives exactly 20,000 sats on Spark's regtest chain (2x speed). Recorded before the router change, on bitkit-core0.8.0-swaps.3.ios-spark-j12x.mp4
Device runs on two regtest iPhone 17 simulators (iOS 26.5) with Spark on Lightspark's hosted regtest, one recording per case at 2x speed; flow numbers match Manual Tests.
1 and 11: A pays contact B over B's private endpoint through the router; 18: B's app closed while A pays, then relaunched.
route-1191-f1-a2x.mp4
route-1191-f18-a2x.mp4
route-1191-f18-b2x.mp4
5 and 13: B requests 150 and A pays it from Spark; reject: B dismisses A's 2,000 request and A sees Rejected.
route-1191-f13-b2x.mp4
route-1191-f5-a2x.mp4
route-1191-f5-reject-b2x.mp4
route-1191-f5-reject-a2x.mp4
7 and 15: B's allowance auto-pays A's 300 request and keeps the 2,000 one manual; ended: after B ends it, A's 200 request stays manual.
route-1191-f7-b2x.mp4
route-1191-f15-a2x.mp4
route-1191-f7-ended-b2x.mp4
route-1191-f7-ended-a2x.mp4
6 and 14: a daily subscription each way, first period paid from Spark, surviving a relaunch.
route-1191-f6-a2x.mp4
route-1191-f14-b2x.mp4
route-1191-f6-b2x.mp4
route-1191-f14-a2x.mp4
8: a $0.20 request paid at its 264-sat quote; 17: B turns contact payments off (A can't pay) and on again (A pays).
route-1191-f8-a2x.mp4
route-1191-f8-b2x.mp4
route-1191-f17-b2x.mp4
route-1191-f17-a2x.mp4
9: an amount above the balance can't be entered, and the sendable maximum pays with fee room; 10: a kill right after the swipe pays nothing twice.
route-1191-f9-a2x.mp4
route-1191-f9-max-a2x.mp4
route-1191-f9-max2-a2x.mp4
route-1191-f10-a2x.mp4
QA Notes
Setup
main:docker compose up -d, then./pubky-marketplace upand./pubky-marketplace seed, thendocker compose --profile marketplace --profile payment-requests up -d --no-build fixture-issuer rc56-peer.xcodebuild build -project Bitkit.xcodeproj -scheme Bitkit -configuration Debug -destination 'platform=iOS Simulator,name=<simulator>' 'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) E2E_BUILD' E2E_BACKEND=local E2E_NETWORK=regtest E2E_HOMESERVER_PUBKY=$(./pubky-marketplace info | jq -r .homeserver_z32), installed on a simulator on the same Mac as bitkit-docker.bitkit://contact?pubky=<pubky>, fromcurl 127.0.0.1:23012/health).fixture-issuer, which publishes onlybtc-regtest-p2wpkh.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
Paykit flows with the money in Spark, against the Paykit v0.1 specs (pubky/paykit-rs
cb8cba5). Wallet state a is Spark only with no channel; state b is a channel too small for the amount with Spark covering the rest. Results are under Test results.Test results
Two regtest iPhone 17 simulators (iOS 26.5) at 771b250, and at 0a47cc8 for 5 reject, 6, 7 ended, 8, 9 within 5 sats, 14 and 17, RouteIosA and RouteIosB, each other's contacts, Spark on Lightspark's hosted regtest, funded with about 2,000-3,000 sats each. Each payment was 100-300 sats. All results are in state a; state b is ⏭️ for every flow, since the setup has no channel.
PaykitRouteTests), the leg debits the broadcast fee, and a contact withdrawal stays a send. J1 passed earlier, before the router change.PaykitPaymentProofServiceTests(the withdrawal txid becomes the on-chain proof; a withdrawal that never went out drops its proof). ✅ reject: B dismissed RouteIosA's 2,000 request from the Payment Requests sheet; A's request reads Rejected, nothing paid. ⏭️ payee cancel before payment: iOS can't cancel a sent one-time request (see 16). ⏭️ expired: the shortest request expiry is 1 hour, not staged.amount_outside_range), nothing paid. After B ended the allowance ("Ended · $0.23 paid automatically"), A's 200 request stayed manual.PrivatePaykitAddressReservationStore), rotated once used, never Spark's static deposit address, which can't be rotated and needs a claim. The spec defines only the endpoint's value format (Payment Endpoint Identifier), not which wallet holds it, so this matches it.Evidence
0a47cc8d(route head): the unit and UI suites pass on an iOS 27.0 simulator; the Spark leg's broadcast fee and the contact withdrawal's activity are unit-tested. The on-chain Spark leg is not driven on a device (see 3).db348780(route head53a7648f): the unit and UI suites ofunit-tests.ymlpass on an iOS 27.0 simulator, includingPaykitRouteTests, which runs bitkit-core's real router for Direct over Lightning from Spending, Direct on-chain from Savings, the Spark leg to an on-chain-only contact, and None; SwiftFormat shows no new findings and the translation check passes. Not yet driven on a device.0.8.0-swaps.3with the swap route still in: J1 on a regtest iPhone simulator (iOS 26.5), at27569ae(now4677e4f2) on bitkit-core0.8.0-swaps.3:Route: Sparkand the leg's fee;49780e91…is confirmed on Lightspark's regtest chain and pays exactly 20,000 sats to the contact's address.Automated Checks
unit-tests.ymlon an iOS 27.0 simulator ata2a34000: pass; SwiftFormat shows no new findings and the translation check passesunit-tests.ymlon an iOS 26.5 simulator at27569ae: passes apart from three date-boundPaykitPaymentProofServiceTestscases thatmasterfixed in421e152PaykitPaymentProofServiceTests.swift: a Spark withdrawal's txid completes and submits the on-chain proof, completing it again is a no-op, and one that never went out drops its started proofPaykitRouteTests.swift: the leg's debit includes the L1 broadcast fee, a contact withdrawal is not marked as a transfer, what the router is asked from Spending and from Savings, the real router's Direct, Service and None outcomes, when a Spark leg is offered, the None copy's balance check, a journaled contact withdrawal matches only its own attempt, the journal stays open until every local record is saved, the leg prepares a request before its proof is started and frees it on a failure before the send, when a source switch on Confirm reroutes, when a request starts on Spending, that a refused send releases its own proof while an older journal is open, and that an unlisted withdrawal is dropped only after the expirySparkManagertalks to the Breez SDK directlyMainnet 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.