Skip to content

feat: pay contacts via spark - #1191

Open
ovitrif wants to merge 21 commits into
feat/breez-spark-lightningfrom
feat/paykit-spark-route
Open

ovitrif wants to merge 21 commits into
feat/breez-spark-lightningfrom
feat/paykit-spark-route

Conversation

@ovitrif

@ovitrif ovitrif commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • Moves to bitkit-core 0.8.0-spark.3 and its paykitRoutePayment, 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.
  • Removes the Pubky Swaps route: its build flags, provider quotes, commit and claim, journey and changelog fragment. Pubky Swaps comes back later as its own draft on top.
  • On the amount screen's Continue, every bitcoin contact payment asks the router from the selected source. USDT and hardware wallet payments skip it.
    • Spending offers all of the contact's bitcoin endpoints, with a Spark withdrawal leg quoted for each on-chain one when a Spark leg is offered.
    • Savings offers only the on-chain endpoints.
    • Direct pays the endpoint the router picks, switching the send to it when the scan opened another one.
    • Service opens Confirm with Route: Spark and the leg's fee; the swipe withdraws exactly the amount to the contact's address.
    • None stays on the amount screen. It shows "Insufficient balance" when the amount is above what was given to the router, otherwise "Can't pay this contact" / "This contact has no payment method Bitkit can use right now." (English in every locale for now). The router's reason goes to the log only.
  • A Spark leg is offered while Spark is ready with a sendable balance, for a payment the user starts to a contact with an on-chain endpoint that Lightning can't pay directly. Payment requests get one too (below). A contact that also lists USDT, whose scan asks for an asset first, gets it as well.
  • An on-chain-only contact opens with Spending as a source while Spark can carry the payment, whether or not Savings covers it. Spending is the default when Savings holds no more than dust.
  • Switching the source on Confirm asks the router again, for any contact payment, so a Spark leg never pays a send moved to Savings.
  • A request's Confirm picks its source the way the amount screen would: Savings when it can send the amount after the on-chain fee, otherwise Spending, where the Spark leg can pay it.
  • A payment request's Confirm, which opens with fixed terms and no amount screen, asks the router too, and an on-chain request the wallet can't pay directly 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 is prepared like a direct send's (a refusal before Spark's send, told by the send itself rather than by another withdrawal's journal, releases it for another try), its 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 counts Spark's L1 broadcast fee as well as its own, so Confirm shows the whole fee, and the withdrawal refuses a fee above the one Confirm showed. Confirm's amount and fee warnings use the leg's amount and fee, and the tags picked there are saved on the withdrawal's activity.
  • A Spark withdrawal to a contact is journaled before it is sent. It settles from Spark's payment list, on its payment event or on the next start, and is recorded as a sent on-chain activity for the contact with Spark's fee, confirmed once Spark completes it. The journal records Spark's payment id once Spark returns it, and settles only from that payment, or from a withdrawal to the same address and amount that wasn't listed before the send. It stays open until the activity, its tags and the request's proof are all saved, so a failed write is retried. A second one waits while the first is open, and one Spark never returned is closed as unsent, releasing the request, only when a sync that succeeded still doesn't list it 10 minutes after the send. A withdrawal still being sent is never settled.

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-core 0.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.

1, payer A 18, payer A 18, payee B 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.

13, payee B 5, payer A Reject, payer B Reject, payee A
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.

7, allower B 15, payee A Ended, allower B Ended, payee A
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.

6, subscriber A 14, creator B 6, subscriber B 14, creator A
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).

8, payer A 8, payee B 17, payee B 17, payer A
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.

9, above balance 9, maximum 9, maximum sent 10, kill
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

  • Pubky stack: synonymdev/bitkit-docker main: docker compose up -d, then ./pubky-marketplace up and ./pubky-marketplace seed, then docker compose --profile marketplace --profile payment-requests up -d --no-build fixture-issuer rc56-peer.
  • App: 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.
  • Spark: dev builds run Spark on Lightspark's hosted regtest, which needs no key and runs its own chain. The node's chain does not matter here: Savings stays empty.
  • Wallet: a new wallet with no channels; a Pubky profile on the local homeserver; fund Spending by sending 50,000 sats from Lightspark's web regtest faucet to the deposit address on Settings › Advanced › Lightning Connections › Spark, which Spark claims after one confirmation; save the fixture-issuer as a contact (bitkit://contact?pubky=<pubky>, from curl 127.0.0.1:23012/health).
  • Contact: bitkit-docker's fixture-issuer, which publishes only btc-regtest-p2wpkh.

Journeys

  • J1: new 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 sats

Manual 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.

  • 1. Pay a contact's Lightning endpoint with an amount: private reserved endpoint, public endpoint.
  • 2. Pay an amountless Lightning endpoint.
  • 3. Pay a contact with only an on-chain endpoint through a Spark withdrawal.
  • 4. Pay a contact with both endpoints: Lightning when Spark covers it, else on-chain.
  • 5. Payment requests as payer: pay over Lightning and on-chain, reject, cancel before payment, refuse an expired one.
  • 6. Recurring requests and subscriptions paid from Spark, surviving restarts.
  • 7. Allowances: within the limit auto-pays from Spark, over it goes manual, an ended one doesn't pay.
  • 8. Payment conversion: a fiat request paid at its quote and deadline.
  • 9. Balance edges: above Spark plus channel refused, within 5 sats of Spark leaves fee room, Spark unavailable fails clearly.
  • 10. Kill the app mid-payment: settles once, no double pay.
  • 11. Spark-backed Lightning endpoint as payee: public and private, rotated after each payment, republished after relaunch.
  • 12. On-chain endpoint as payee.
  • 13. As payee, send a request (Lightning and on-chain); the contact pays, the proof is matched.
  • 14. Recurring payments received into Spark.
  • 15. Allowances as payee: the contact auto-pays our request into Spark.
  • 16. Cancel a request we sent.
  • 17. Endpoint toggles remove and republish the Spark-backed endpoint.
  • 18. Payee app closed while paid: linked on reopen, no duplicates.
  • 19. Spark-native endpoint.

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.

  • ✅ 1, private: A paid RouteIosB 200; the router picked Direct over Lightning on private list v1; A Sent 200 (fee 1), B Received from RouteIosA, linked. ⏭️ public endpoint: Bitkit pays a saved contact only over the private path (with B's private sharing off and public on, A's payment was refused), and paying B's pubky by hand opens the saved contact; it needs a payer that hasn't saved the contact, not staged so the test contacts stay linked.
  • ✅ 2: the contact's private endpoint is an amountless Spark invoice; covered by 1, 13 and 18.
  • ⏭️ 3: a Spark withdrawal costs about 4,200 sats with its broadcast fee, more than the wallets hold. Unit-tested: the router picks the Spark leg (PaykitRouteTests), the leg debits the broadcast fee, and a contact withdrawal stays a send. J1 passed earlier, before the router change.
  • ✅ 4, partly: with RouteIosB accepting Lightning and on-chain, the router picked Lightning (1). ⏭️ the on-chain fallback: same cost as 3.
  • ✅ 5, Lightning: A's Payment Request confirm opened from Spending, the router paid it Direct over Lightning, and B's request reads "Proof submitted". ⏭️ on-chain from Spark: same cost as 3; unit-tested in 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.
  • ✅ 6: a daily subscription each way, first period paid from Spark (A paid B's 120, B paid A's 110); after both apps relaunched, A shows it Active and B shows 1 subscriber, 1 payment. Later periods were not waited for.
  • ✅ 7: B gave RouteIosA $1 a payment, $10 a month; A requested 300 and B paid it from Spark automatically ("Paid automatically $0.23"); a 2,000 request (above $1 = 1,318 sats) stayed manual (amount_outside_range), nothing paid. After B ended the allowance ("Ended · $0.23 paid automatically"), A's 200 request stayed manual.
  • ✅ 8: B requested $0.20; A's confirm showed it quoted at 264 sats and paid it from Spark; B received 264. Fixed-rate conversion only; Bitkit issues no per-period quotes.
  • ✅ 9, above the balance: typing 9999 against an Available of 2,531 stayed at 999; nothing sent. ✅ within 5 sats: with 2,470 in Spark, 2,466 can't be entered; the Available 2,454 paid with a 7-sat fee, leaving 9. ⏭️ Spark unavailable: the iOS simulator has no device-scoped offline switch, and the host's networking stays untouched.
  • ✅ 10, partly: A killed 0.3 s after the swipe; the kill landed before Spark dispatched: no new Sent row, Spending unchanged, no double pay. ⏭️ a kill after dispatch: Spark sends finish in under a second.
  • ✅ 11, private: B's private endpoint rotated after each payment, A opening it on list versions 1, 2 and then 5, each landing in Spark and linked. ⏭️ public: sharing off.
  • 12, from the code (no on-chain receive was run): Bitkit publishes a node Savings address as each contact's reserved on-chain endpoint (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.
  • ✅ 13, Lightning: B sent RouteIosA a 150-sat request and A paid it; B's request reads "Proof submitted" and Spending went up 150. ⏭️ on-chain: same cost as 3.
  • ✅ 14: the payee side of 6, each wallet receiving the other's first period into Spark.
  • ✅ 15: the payee side of 7; A's 300 request was auto-paid into A's Spark.
  • ⏭️ 16: not supported on iOS on either rail, tracked separately. Only subscriptions have a cancel.
  • ✅ 17: iOS has no Lightning-only toggle, so the contact-payments toggle was used: with B's sharing off, A got "Unable To Pay Contact"; turned back on, A paid B 100 on a new private list about 25 s later.
  • ✅ 18: with B's app terminated, A paid RouteIosB 100; on relaunch B showed Received from RouteIosA, linked, with no duplicate rows.
  • 19: not in spec. Payment Endpoint Identifier §5.2 lists no Spark rail, and §8 covers extensions; nothing was invented.

Evidence

  • Fixes at 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).
  • At db348780 (route head 53a7648f): the unit and UI suites of unit-tests.yml pass on an iOS 27.0 simulator, including PaykitRouteTests, 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.
  • Earlier, before the router change, on bitkit-core 0.8.0-swaps.3 with the swap route still in: J1 on a regtest iPhone simulator (iOS 26.5), at 27569ae (now 4677e4f2) on bitkit-core 0.8.0-swaps.3:
    • the amount screen offered only Spending; the confirm screen showed Route: Spark and the leg's fee;
    • Bitcoin Sent showed ₿ 20,000;
    • Spark's withdrawal 49780e91… is confirmed on Lightspark's regtest chain and pays exactly 20,000 sats to the contact's address.

Automated Checks

  • ran the unit and UI suites of unit-tests.yml on an iOS 27.0 simulator at a2a34000: pass; SwiftFormat shows no new findings and the translation check passes
  • ran the unit test selection of unit-tests.yml on an iOS 26.5 simulator at 27569ae: passes apart from three date-bound PaykitPaymentProofServiceTests cases that master fixed in 421e152
  • ran SwiftFormat lint on the changed files: clean
  • updated PaykitPaymentProofServiceTests.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 proof
  • added PaykitRouteTests.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 expiry
  • no unit test drives the Spark leg's quote or send: SparkManager talks to the Breez SDK directly

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.

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.

  • Spark only, no channel: pay a contact's Lightning endpoint, both ways; Activity shows the contact and the fee.
  • A channel too small for the amount: the payment goes from Spark.
  • A contact with only an on-chain endpoint: paid through a Spark withdrawal, the fee includes the on-chain fee, and Activity shows the contact.
  • A payment request paid from Spark, over Lightning and on-chain: the payee's request shows paid and the proof matches.
  • An allowance: a request within it auto-pays from Spark, and the ledger shows it.
  • Receive: a contact pays our Spark-backed endpoint while the app is closed; on reopen it's linked to the contact and the endpoint has rotated.
  • Kill the payer mid-payment: on relaunch it settles once, with no double pay.
  • An amount above Spark plus the channel: refused with the app's copy, and nothing is spent.

Models used

  • Planning/scoping: claude-opus-5-5 (reasoning: medium), claude-sonnet-5-5 (reasoning: high)
  • Implementation: claude-opus-5-5 (reasoning: medium)
  • Review: Not performed

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-5 planning runs were research and spec-mapping subagents. No independent agent review was run on this PR.

@ovitrif

ovitrif commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 9d8ca9f: adds the paykit-spark-route.xml journey (J1), ported from synonymdev/bitkit-android#1754. Ran it on an iOS 26.5 simulator against Lightspark's hosted regtest. The router picked a Spark withdrawal, and 49780e91… pays exactly 20,000 sats to the contact. The recording, setup and evidence are in the description.

@ovitrif ovitrif added this to the 3.2.0 milestone Nov 3, 2026
@ovitrif

ovitrif commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator Author

Added to the 3.2.0 milestone: Spark via Breez SDK is the 3.2.0 highlight, and this PR stacks on #1128.

@ovitrif ovitrif changed the title feat: pay on-chain contacts from spark feat: pay contacts via spark Nov 3, 2026
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.
@ovitrif
ovitrif force-pushed the feat/breez-spark-lightning branch from ffba7c7 to 0eed9c2 Compare November 5, 2026 02:10
@ovitrif
ovitrif force-pushed the feat/paykit-spark-route branch from f376aa7 to 438d6ae Compare November 5, 2026 02:10
@ovitrif

ovitrif commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

Force-pushed: rebuilt on #1128's new head 0eed9c2a, with the same route changes (old head kept as backup/paykit-spark-route-20261105). The unit and UI suites pass at 438d6ae5; the description is updated.

@ovitrif ovitrif mentioned this pull request Nov 6, 2026
5 of 44 tasks
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.
@ovitrif
ovitrif marked this pull request as ready for review November 7, 2026 02:15
@ovitrif
ovitrif requested review from a team, ben-kaufman and pwltr and removed request for a team November 7, 2026 02:15
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-07T02:19:10.177148Z 0a47cc8 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ovitrif

ovitrif commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator Author

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 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".

Comment thread Bitkit/Managers/SparkManager.swift Resolved
Comment thread Bitkit/ViewModels/AppViewModel.swift Resolved
@greptile-apps

greptile-apps Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 2/5

[Critical impact] This PR reaches Bitcoin contact payments that can send funds through Spark.

Fix request approval and withdrawal recovery before merging.

Findings

  1. P1 Spark requests fail before sending ▶
  2. P1 Paid withdrawals lose their records ▶
  3. P1 Recovery reuses an earlier payment ▶
  4. P2 Selected payment tags disappear ▶
  5. P2 Large-payment warning is skipped ▶

Summary

Routes Bitcoin contact payments through paykitRoutePayment and adds Spark withdrawals to on-chain contacts.

  • Bitcoin contact payments use the route the selected wallet can reach.
  • Spark contact withdrawals reconnect to their send records after a restart.
  • Spark withdrawals can complete on-chain payment requests.

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]
Loading

Reviews (1) · Last reviewed commit: "fix: charge the spark leg its broadcast ..." · Reviewed by Greptile

Comment thread Bitkit/Managers/SparkManager.swift Resolved
Comment thread Bitkit/Managers/SparkManager.swift Resolved
@ovitrif ovitrif removed this from the 3.2.0 milestone Nov 7, 2026
@ovitrif

ovitrif commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator Author

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 Closes at the top of the body. The issue is on the project at 👀 Review. Twin: synonymdev/bitkit-android#1754 (issue synonymdev/bitkit-android#1807).

Comment thread Bitkit/ViewModels/AppViewModel.swift Resolved

@talosmachina talosmachina 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.

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.
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.

3 participants