Repository navigation
Conversation
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.
|
Pushed
|
|
Pushed
|
|
Pushed |
The send succeeds once bitkit-core broadcasts the claim, and the claim is advanced in the background until it confirms. A failed hold payment stops the wait at once, a failed advance is retried, tags and the contact land on the hold payment, a Direct answer moves to Savings, quotes for a replaced send or another amount are dropped, configured providers come first, and restore shows the Savings notice only for swaps rebuilt now.
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. |
|
@coreyphillips one more version step: bitkit-core |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 2 new commits (bd19f96, 76f8374). Nothing blocking remains.
still open
- Background claim advancing stops after an hour, not at confirmation (Bitkit/Services/PubkySwapsRouteService.swift:21). The PR says the claim keeps advancing in the background until it confirms, but the loop stops after 60 minutes.
confirmationTimeoutis 60 minutes (PubkySwapsRouteService.swift:21), and the detached loop inawaitClaimthen logs and exits. The claim fee is a fixed 2 sat/vB with no bump, so a claim that drops out of mempools after that hour is never rebroadcast. The provider settles the hold invoice as soon as it sees the claim. If the claim then does not confirm before the lock'stimeout_block_height, the provider can also refund its lockup. The user pays and the contact receives nothing. This cannot happen today: the flag is regtest-only and bitkit-core refuses mainnet commits. It does need to change before the flag goes, and the description currently says otherwise. No app code changed since 4bee733.confirmationTimeoutis still 60 minutes (PubkySwapsRouteService.swift:28), andadvanceInBackgroundstill stops there. The description now says "up to an hour" and #1262 tracks the behaviour, but the doc comments at :25 and :135 still say the claim is rebroadcast until it confirms. - Swap restore runs once and is skipped if the wallet start fails (Bitkit/AppScene.swift:1268).
restorePubkySwaps()runs right afterstartWallet(completingBackupRestore: true)(AppScene.swift:1268), whether or not the node actually started.startWalletreturns early when offline and swallows start errors. In that caserestoreSwapsthrows (no node fornewAddress/listPayments), and the error is only logged.retryWalletStart(AppScene.swift:1300) and later wallet starts never call it again. In those cases the swap J2 covers is never claimed and waits for the provider's timeout instead. Funds return through that timeout, so this is not urgent. A retry once the node is running would close the gap. AppScene.swift:1267-1268 still callsrestorePubkySwaps()once, whateverstartWalletdid.retryWalletStart(:1301-1310) still never calls it. Deferring it to #1263 tracks the gap without closing it. - Old swap results overwrite an edited payment in the same sheet (Bitkit/Views/Wallets/Send/SendConfirmationView.swift:1381-1382). While a 20,000-sat commit waits, the user can return to Amount and continue with 30,000 sats in the same sheet. The unchanged presentation ID accepts the old result: success opens Bitcoin Sent displaying the new draft amount, while failure clears the replacement quote. Both branches reproduced with the extracted payment method and a suspended commit. The quote check in
isSwapRouteResultCurrent(SendConfirmationView.swift:1372-1379) settles the failure branch, and the case where the new quote has already landed. One path remains: Q1'ssendreturns after Continue on Amount but before Q2 lands. Q1 is then still the active quote, so Pending (:1422) and Bitcoin Sent showwallet.sendAmountSats, which SendAmountView.swift:377 has already set to the new amount. I traced this but did not run it. - Swap records survive a successful wallet wipe (Bitkit.xcodeproj/project.pbxproj:1244). After a routed payment, the in-app wallet wipe leaves its swap records behind. The pinned release writes contracts, destination addresses, amounts and signed claims to
core/pubky_swaps/*.json. Itswipe_all_databases()clears only activity and Blocktank tables, whileAppResetdeletes only LDK storage. Swap history and any saved claim preimage therefore survive a successful wipe. The new pin'swipe_all_databases(bitkit-core src/lib.rs:2080 at 4f3e215) still clears only the activity and Blocktank stores, and AppReset is unchanged. Swap records still survive a wipe. Tracked in bitkit-core#197. - Going back to Amount during the swap commit can still pay a second swap (Bitkit/Views/Wallets/Send/SendConfirmationView.swift:1406-1424). 4bee733 removes the way back to Amount once the hold invoice is paid. Before that point, during
commit,decodeandsend(SendConfirmationView.swift:1406-1415), the user can still go back, request a fresh quote and pay a second swap. WhileisSubmittingPaymentis true, the header back button (showBackButton: !navigationPath.isEmpty, :336) and the amount tap (navigateToAmount, gated only bycanEditAmount, :253) both stay live. On Amount, Continue startsquoteSwapRoute, which asks every contact for an offer, so it usually takes longer than the commit to one provider. What follows, in order: 1. Q1'ssendreturns while the user is on Amount.isSendCurrent()still passes because Q1 is still the active quote, so.pendingis appended on top of Amount (:1422). 2.performSwapRoutedPaymentreturns, which releasesisSubmittingPayment. 3. Q2 arrives. Its guard (SendAmountView.swift:474-476) still passes, because nothing cleared Q1. It stores Q2 and appends.confirmon top of Pending (:409). 4. That Confirm's swipe is enabled. A swipe commits and pays a second swap, so the contact is paid twice. If Q2 lands before Q1'ssendreturns, the outcome is the same and quieter.isSendCurrent()fails, so Q1 is paid with no navigation and no toast. Q2's swipe unlocks as soon as Q1's call returns. I confirmed this by tracing the code above, andSwipeButtonruns its action in an unstructuredTaskthat is not cancelled when Confirm is popped. I did not run it: there is no provider or simulator here. It is non-blocking only because the route is behind the regtest debug flag. Two possible fixes: - Hide back and amount editing on Confirm while a swap payment is submitting. - HavequoteSwapRoutedrop its result when a payment is submitting or when the path no longer ends at Amount. The reply defers this to #1266 and does not dispute it.canEditAmount(SendConfirmationView.swift:253-260) andshowBackButton(:336) still ignoreisSubmittingPayment, so going back during the commit can still lead to a second swap. - Fast hold-payment failures are missed before pending registration (Bitkit/Views/Wallets/Send/SendConfirmationView.swift:1415-1424). A hold payment that fails before submission returns now opens Pending without a failure resolution. The failure listener is installed in
driveSwapClaimafter the awaited send, and pending registration also happens afterward. I reproduced this with the extracted methods by deliveringpaymentFailedduring submission: HEAD retained the failed hash without a resolution, while d2621c6 opened Failure. The reply defers this to #1267 and agrees it is a regression.send(SendConfirmationView.swift:1415) still runs before bothaddPendingPaymentHash(:1420) and thepaymentFailedlistener indriveSwapClaim(:1463).
jvsena42
left a comment
There was a problem hiding this comment.
Delta review of bd19f96 and 76f8374 (bitkit-core 0.8.0-swaps.9 → 0.8.0-swaps.11): no findings. No Swift changed.
- What the core diff adds:
store::create, which refuses a swap whose id is already saved. It runs incommit_routebefore the app pays the hold invoice, so a refusal moves no funds and lands on the existing "refused commit → Retry from Amount" path. Inrestore_swapsa known record stillcontinues beforecreate. - What it leaves alone:
SwapRecordand its JSON, key derivation, hold-invoice validation and claim logic. So swaps that testers saved under swaps.9 still restore. - Other core changes: the API breaks are all in wake, which the app doesn't call. The swap FFI signatures are unchanged.
- Twin note: Android #1732 still pins
swaps.9. Noswaps.10orswaps.11Android artifact is published yet, so the twins differ on this refusal until one is.
The device gate is still pending.
Reviewer model: Claude Fable 5.1 (claude-fable-5-1), Claude Opus 5.5 (claude-opus-5-5)
Reasoning effort: not exposed
|
@coreyphillips moved bitkit-core from the |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 1 new commit (d57e2bc). Nothing blocking remains.
still open
- Background claim advancing stops after an hour, not at confirmation (Bitkit/Services/PubkySwapsRouteService.swift:21). The PR says the claim keeps advancing in the background until it confirms, but the loop stops after 60 minutes.
confirmationTimeoutis 60 minutes (PubkySwapsRouteService.swift:21), and the detached loop inawaitClaimthen logs and exits. The claim fee is a fixed 2 sat/vB with no bump, so a claim that drops out of mempools after that hour is never rebroadcast. The provider settles the hold invoice as soon as it sees the claim. If the claim then does not confirm before the lock'stimeout_block_height, the provider can also refund its lockup. The user pays and the contact receives nothing. This cannot happen today: the flag is regtest-only and bitkit-core refuses mainnet commits. It does need to change before the flag goes, and the description currently says otherwise. The description now says the claim advances for up to an hour, which matches the code, and #1262 tracks the rest. The code is unchanged:confirmationTimeoutis still 60 minutes (PubkySwapsRouteService.swift:28), and the detached loop at :150-153 still stops there with a fixed 2 sat/vB claim, so this still needs fixing before the flag goes. - Swap restore runs once and is skipped if the wallet start fails (Bitkit/AppScene.swift:1268).
restorePubkySwaps()runs right afterstartWallet(completingBackupRestore: true)(AppScene.swift:1268), whether or not the node actually started.startWalletreturns early when offline and swallows start errors. In that caserestoreSwapsthrows (no node fornewAddress/listPayments), and the error is only logged.retryWalletStart(AppScene.swift:1300) and later wallet starts never call it again. In those cases the swap J2 covers is never claimed and waits for the provider's timeout instead. Funds return through that timeout, so this is not urgent. A retry once the node is running would close the gap. AppScene.swift:1267-1268 still callsrestorePubkySwaps()once afterstartWallet(completingBackupRestore: true), whether or not the node started.retryWalletStart(:1301) still never calls it. Tracking it in #1263 defers the point but does not fix or rebut it. - Old swap results overwrite an edited payment in the same sheet (Bitkit/Views/Wallets/Send/SendConfirmationView.swift:1381-1382). While a 20,000-sat commit waits, the user can return to Amount and continue with 30,000 sats in the same sheet. The unchanged presentation ID accepts the old result: success opens Bitcoin Sent displaying the new draft amount, while failure clears the replacement quote. Both branches reproduced with the extracted payment method and a suspended commit. Claude still sees it: The quote-ID check (SendConfirmationView.swift:1367-1376, 1393-1400) helps only once Q2 is stored.
onContinuesetswallet.sendAmountSatsto the new amount (SendAmountView.swift:377) beforequoteSwapRoutestores Q2, so Q1 still passesisSendCurrent()in that window. Q1's success therefore pushes Pending, and later Bitcoin Sent, with the edited amount. Q1's failure nils the route's quote (:1443), which makes Q2'sapp.paykitSwapRoute == routeguard drop the replacement quote silently. - Swap records survive a successful wallet wipe (Bitkit.xcodeproj/project.pbxproj:1244). After a routed payment, the in-app wallet wipe leaves its swap records behind. The pinned release writes contracts, destination addresses, amounts and signed claims to
core/pubky_swaps/*.json. Itswipe_all_databases()clears only activity and Blocktank tables, whileAppResetdeletes only LDK storage. Swap history and any saved claim preimage therefore survive a successful wipe. I checked rc14 (8fbf1d75):wipe_all_databases()in src/lib.rs:2092 still wipes only the activity and Blocktank databases. The swap store opened at lib.rs:524 is untouched, so the records still survive a wipe. This is tracked in synonymdev/bitkit-core#197, not fixed. - Going back to Amount during the swap commit can still pay a second swap (Bitkit/Views/Wallets/Send/SendConfirmationView.swift:1406-1424). 4bee733 removes the way back to Amount once the hold invoice is paid. Before that point, during
commit,decodeandsend(SendConfirmationView.swift:1406-1415), the user can still go back, request a fresh quote and pay a second swap. WhileisSubmittingPaymentis true, the header back button (showBackButton: !navigationPath.isEmpty, :336) and the amount tap (navigateToAmount, gated only bycanEditAmount, :253) both stay live. On Amount, Continue startsquoteSwapRoute, which asks every contact for an offer, so it usually takes longer than the commit to one provider. What follows, in order: 1. Q1'ssendreturns while the user is on Amount.isSendCurrent()still passes because Q1 is still the active quote, so.pendingis appended on top of Amount (:1422). 2.performSwapRoutedPaymentreturns, which releasesisSubmittingPayment. 3. Q2 arrives. Its guard (SendAmountView.swift:474-476) still passes, because nothing cleared Q1. It stores Q2 and appends.confirmon top of Pending (:409). 4. That Confirm's swipe is enabled. A swipe commits and pays a second swap, so the contact is paid twice. If Q2 lands before Q1'ssendreturns, the outcome is the same and quieter.isSendCurrent()fails, so Q1 is paid with no navigation and no toast. Q2's swipe unlocks as soon as Q1's call returns. I confirmed this by tracing the code above, andSwipeButtonruns its action in an unstructuredTaskthat is not cancelled when Confirm is popped. I did not run it: there is no provider or simulator here. It is non-blocking only because the route is behind the regtest debug flag. Two possible fixes: - Hide back and amount editing on Confirm while a swap payment is submitting. - HavequoteSwapRoutedrop its result when a payment is submitting or when the path no longer ends at Amount. Neither gate changed.showBackButton: !navigationPath.isEmpty(SendConfirmationView.swift:336) andcanEditAmount(:253) still ignoreisSubmittingPayment.quoteSwapRoute(SendAmountView.swift:474-476) still accepts Q2 while Q1 is the active quote, so Q2's Confirm can be pushed over Pending. Tracked in #1266, not fixed. - Fast hold-payment failures are missed before pending registration (Bitkit/Views/Wallets/Send/SendConfirmationView.swift:1415-1424). A hold payment that fails before submission returns now opens Pending without a failure resolution. The failure listener is installed in
driveSwapClaimafter the awaited send, and pending registration also happens afterward. I reproduced this with the extracted methods by deliveringpaymentFailedduring submission: HEAD retained the failed hash without a resolution, while d2621c6 opened Failure.send(bolt11:)is still awaited at SendConfirmationView.swift:1415, beforeaddPendingPaymentHash(:1419) and before thepaymentFailedlistener indriveSwapClaim(:1463). A failure delivered during the send is still missed. Tracked in #1267, not fixed.
jvsena42
left a comment
There was a problem hiding this comment.
Delta review of d57e2bc1b (bitkit-core 0.8.0-swaps.11 → 0.8.0-rc14): no findings.
v0.8.0-swaps.11is an ancestor ofv0.8.0-rc14, and the swaps merge (core#182) has a tree identical to swaps.11. So no swap code is dropped, and swaps saved under swaps.11 restore as before.- rc14 adds only regtest basic auth (core#201, #203): a trailing
auth: String? = nilon the regtest calls. TheCoreServicecallers compile unchanged, andbuild-localpasses. - The
Package.swiftchecksum matches the rc14BitkitCore.xcframework.zip.
coreyphillips/pubky-swap f45e204) against bitkit-docker's LND and the marketplace stack. I'll run it on a later pass.
Reviewer model: Claude Fable 5.2 (claude-fable-5-2), Claude Opus 5.5 (claude-opus-5-5)
Reasoning effort: not exposed
# Conflicts: # Bitkit.xcodeproj/project.pbxproj # Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
|
Merged |
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 42 new commits (5aafcfd, 75fe13b, 265389e, and 39 more). Nothing blocking remains.
still open
- Background claim advancing stops after an hour, not at confirmation (Bitkit/Services/PubkySwapsRouteService.swift:21). The PR says the claim keeps advancing in the background until it confirms, but the loop stops after 60 minutes.
confirmationTimeoutis 60 minutes (PubkySwapsRouteService.swift:21), and the detached loop inawaitClaimthen logs and exits. The claim fee is a fixed 2 sat/vB with no bump, so a claim that drops out of mempools after that hour is never rebroadcast. The provider settles the hold invoice as soon as it sees the claim. If the claim then does not confirm before the lock'stimeout_block_height, the provider can also refund its lockup. The user pays and the contact receives nothing. This cannot happen today: the flag is regtest-only and bitkit-core refuses mainnet commits. It does need to change before the flag goes, and the description currently says otherwise.confirmationTimeoutis still 60 minutes (PubkySwapsRouteService.swift:28), and the detached loop at :149-160 still logs and exits. The description now says "up to an hour" and defers the rest to #1262, which settles the description mismatch. The doc comments at :25 and :133-135 still promise rebroadcasts "until it confirms", and the behaviour itself is unchanged. - Swap restore runs once and is skipped if the wallet start fails (Bitkit/AppScene.swift:1268).
restorePubkySwaps()runs right afterstartWallet(completingBackupRestore: true)(AppScene.swift:1268), whether or not the node actually started.startWalletreturns early when offline and swallows start errors. In that caserestoreSwapsthrows (no node fornewAddress/listPayments), and the error is only logged.retryWalletStart(AppScene.swift:1300) and later wallet starts never call it again. In those cases the swap J2 covers is never claimed and waits for the provider's timeout instead. Funds return through that timeout, so this is not urgent. A retry once the node is running would close the gap.restorePubkySwaps()is still called once, right afterstartWallet(completingBackupRestore:)(AppScene.swift:1268), andretryWalletStart(:1301-1310) still does not call it. This is deferred to #1263, which is open; no code changed. - Old swap results overwrite an edited payment in the same sheet (Bitkit/Views/Wallets/Send/SendConfirmationView.swift:1381-1382). While a 20,000-sat commit waits, the user can return to Amount and continue with 30,000 sats in the same sheet. The unchanged presentation ID accepts the old result: success opens Bitcoin Sent displaying the new draft amount, while failure clears the replacement quote. Both branches reproduced with the extracted payment method and a suspended commit. Part of this is fixed. The quote-ID check (SendConfirmationView.swift:1372-1378, used at :1394-1400) stops an old result from navigating or clearing the quote once the edited payment's fresh quote is stored. The window between Continue on Amount and the fresh quote landing is still open. In it, SendAmountView.swift:377 has already set
wallet.sendAmountSatsto the edited amount, while the old quote is still the active one. The old send then passesisSendCurrent()and pushes Pending (:1422). Pending (SendPendingScreen.swift:110-111) and SendSuccess (:47-48) both show the edited amount. #1266 now records this window, so it closes with point 5. - Swap records survive a successful wallet wipe (Bitkit.xcodeproj/project.pbxproj:1244). After a routed payment, the in-app wallet wipe leaves its swap records behind. The pinned release writes contracts, destination addresses, amounts and signed claims to
core/pubky_swaps/*.json. Itswipe_all_databases()clears only activity and Blocktank tables, whileAppResetdeletes only LDK storage. Swap history and any saved claim preimage therefore survive a successful wipe. Nothing changed. The merge did not touchAppReset, and per the delta review core rc14 only adds regtest auth on top of swaps.11, sowipe_all_databases()still leavespubky_swaps/*.json. This is deferred to bitkit-core#197, which is open. - Going back to Amount during the swap commit can still pay a second swap (Bitkit/Views/Wallets/Send/SendConfirmationView.swift:1406-1424). 4bee733 removes the way back to Amount once the hold invoice is paid. Before that point, during
commit,decodeandsend(SendConfirmationView.swift:1406-1415), the user can still go back, request a fresh quote and pay a second swap. WhileisSubmittingPaymentis true, the header back button (showBackButton: !navigationPath.isEmpty, :336) and the amount tap (navigateToAmount, gated only bycanEditAmount, :253) both stay live. On Amount, Continue startsquoteSwapRoute, which asks every contact for an offer, so it usually takes longer than the commit to one provider. What follows, in order: 1. Q1'ssendreturns while the user is on Amount.isSendCurrent()still passes because Q1 is still the active quote, so.pendingis appended on top of Amount (:1422). 2.performSwapRoutedPaymentreturns, which releasesisSubmittingPayment. 3. Q2 arrives. Its guard (SendAmountView.swift:474-476) still passes, because nothing cleared Q1. It stores Q2 and appends.confirmon top of Pending (:409). 4. That Confirm's swipe is enabled. A swipe commits and pays a second swap, so the contact is paid twice. If Q2 lands before Q1'ssendreturns, the outcome is the same and quieter.isSendCurrent()fails, so Q1 is paid with no navigation and no toast. Q2's swipe unlocks as soon as Q1's call returns. I confirmed this by tracing the code above, andSwipeButtonruns its action in an unstructuredTaskthat is not cancelled when Confirm is popped. I did not run it: there is no provider or simulator here. It is non-blocking only because the route is behind the regtest debug flag. Two possible fixes: - Hide back and amount editing on Confirm while a swap payment is submitting. - HavequoteSwapRoutedrop its result when a payment is submitting or when the path no longer ends at Amount. Both ways back are still live during the commit. The header back button is shown on!navigationPath.isEmpty(SendConfirmationView.swift:336), andnavigateToAmount(:2102-2105) is gated only bycanEditAmount(:253-260), which ignoresisSubmittingPayment. The quote guard in SendAmountView.swift:473-475 still does not check for a submitting payment. Deferred to #1266, which is open. - Fast hold-payment failures are missed before pending registration (Bitkit/Views/Wallets/Send/SendConfirmationView.swift:1415-1424). A hold payment that fails before submission returns now opens Pending without a failure resolution. The failure listener is installed in
driveSwapClaimafter the awaited send, and pending registration also happens afterward. I reproduced this with the extracted methods by deliveringpaymentFailedduring submission: HEAD retained the failed hash without a resolution, while d2621c6 opened Failure. The order is unchanged.send(bolt11:)is at SendConfirmationView.swift:1415 andaddPendingPaymentHashat :1420. ThepaymentFailedlistener is only installed indriveSwapClaimat :1463, so a failure handled before the send returns is still dropped. Deferred to #1267, which is open.
# Conflicts: # Bitkit.xcodeproj/project.pbxproj # Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
|
Merged master in (3ca711e) to clear the conflict. Master moved to bitkit-core |
# Conflicts: # Bitkit/Resources/Localization/ar.lproj/Localizable.strings # Bitkit/Resources/Localization/ca.lproj/Localizable.strings # Bitkit/Resources/Localization/cs.lproj/Localizable.strings # Bitkit/Resources/Localization/de.lproj/Localizable.strings # Bitkit/Resources/Localization/el.lproj/Localizable.strings # Bitkit/Resources/Localization/en.lproj/Localizable.strings # Bitkit/Resources/Localization/es-419.lproj/Localizable.strings # Bitkit/Resources/Localization/es.lproj/Localizable.strings # Bitkit/Resources/Localization/fr.lproj/Localizable.strings # Bitkit/Resources/Localization/it.lproj/Localizable.strings # Bitkit/Resources/Localization/nl.lproj/Localizable.strings # Bitkit/Resources/Localization/pl.lproj/Localizable.strings # Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings # Bitkit/Resources/Localization/pt.lproj/Localizable.strings # Bitkit/Resources/Localization/ru.lproj/Localizable.strings # Bitkit/ViewModels/AppViewModel.swift # Bitkit/Views/Wallets/Send/SendAmountView.swift # Bitkit/Views/Wallets/Send/SendConfirmationView.swift # Bitkit/Views/Wallets/Send/SendSheet.swift # BitkitTests/SendConfirmationViewTests.swift
|
Merged master in (04d25e5) to clear the conflict with #1128, which brought Spark contact payments and their own Paykit router. Both routes now live side by side:
The app and test target build. @coreyphillips this needs a real re-review, not just a re-approval: the merge decides how the two routes share a send. Reverse the order if you want the swap first. Asking the core router for swaps on both sides would be a follow-up. |
ben-kaufman
left a comment
There was a problem hiding this comment.
A small Spark balance can block the available Pubky Swaps fallback.
Reviewer model: gpt-6.1-sol
Reasoning effort: xhigh
ben-kaufman
left a comment
There was a problem hiding this comment.
Editing during Spark quoting can leave the swap fallback with a different amount.
Reviewer model: gpt-6.1-sol
Reasoning effort: xhigh
|
QA: J1 and J2 pass at 58139bc (bitkit-core
J1, Spending only (500,000 sats, Savings 0), paying the on-chain-only
J2: the swap was started, the provider funded its HTLC, and the app was killed before any block. Then the app was reinstalled with its keychain reset, and the wallet restored from its recovery phrase:
Not as written:
|
|
QA: J1 passes via the saved issuer contact at 58139bc (bitkit-core
J1, Spending only (500,000 sats, Savings 0), paying the saved
Setup: the issuer must be linked to the wallet and have sent it its private payment list before the tap. synonymdev/bitkit-docker#39's README has the three calls. Not as written: the journey's |
Closes #1253
Twin: synonymdev/bitkit-android#1732
Dependency: synonymdev/bitkit-core#182
Refs:
This PR routes a Paykit contact payment through a Pubky Swaps provider when the contact only accepts on-chain bitcoin and the wallet holds only a spending balance.
Description
PUBKY_SWAPS_PROVIDERSadds extra candidates for testing, asked first.paykit-cap.*and refuses a reply not signed by the provider's key or the key its records delegate to.Route: Pubky Swaps · <provider>, with the whole swap cost in the fee row.PUBKY_SWAPS_ROUTES(environment or launch argument) on regtest debug builds only.paykit-swap-route.xmljourney from Android.Builds against the bitkit-core release
0.8.0-rc15, which includes synonymdev/bitkit-core#182 (it also links Network.framework for the swap transport). Master's Spark leg (#1191) goes first: a contact payment to an on-chain address takes the swap route when Spark can't carry it, including when Spark's balance is too small for the amount and fee, the order bitkit-core's router weighs service legs and swaps in. A swap-routed send skips master's router on Amount and Confirm, so it never picks up a Spark leg next to its quote.Why this journey
What it needs to ship
swap/1in Paykit: the signedpaykit-cap.*negotiation runs over iroh; over Paykit Encrypted Links it needs the additive paykit-rs SDK API for extension kinds (Design review: Paykit Capabilities, a general capability and intent layer for Paykit peers (answers Paykit Intents question 8) pubky/paykit-rs#229, §17).Out of Scope
wipe_all_databases(). Tracked in fix: wipe swap records with the wallet bitkit-core#197 (thread).Design
N/A — no design available.
Preview
A wallet with only a spending balance pays a contact who accepts only on-chain bitcoin. The router finds a Pubky Swaps provider among the wallet's contacts, the confirm screen shows the route, the quote and contract are signed, and the contact receives exactly 20,000 sats on chain (iPhone simulator (iOS 27.0), 2x speed, regtest).
ios-signed-swaps-route-2x.mp4
After a wallet restore from the seed, the swap the wallet left in flight before its claim is found at its provider and claimed to Savings, with the "Payment returned to Savings" notice (iPhone simulator (iOS 26.5), 2x speed, regtest; the phrase entry is not recorded). Recorded on bitkit-core
0.8.0-swaps.4, before restores took the node's payments.ios-restore-j22x.mp4
QA Notes
Setup
feat/pubky-swap-provideruntil it merges): the stack, thepubky-swapsprofile with the provider atf45e204on the stack's LND, the marketplace andfixture-issuerfixtures (the on-chain-only contact), and the LNURL-pay fixture.docker compose logs pubky-swap | grep "Provider pubky"gives the provider's pubky.xcodebuild build -project Bitkit.xcodeproj -scheme Bitkit -configuration Debug -destination 'platform=iOS Simulator,name=<simulator>' 'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) E2E_BUILD' ARCHS=arm64 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 and launched withxcrun simctl launch <udid> to.bitkit -PUBKY_SWAPS_ROUTES=true. The simulator reaches the host loopback directly.curl -H 'Host: 127.0.0.1:23010' 127.0.0.1:23010/generate/channel(500,000 sats to spending, opened withxcrun simctl openurl <udid> lightning:<lnurl>); leave savings empty; create a Pubky profile; save the provider's and the fixture-issuer's pubkys as contacts (bitkit://contact?pubky=<pubky>)../pubky-marketplacerestarts the testnet container, restarthomeserver-proxy,paykit-server,fixture-issuerandrc56-peertoo: they share its network namespace, and profile creation fails with "complete Pubky auth flow" otherwise.Journeys
paykit-swap-route.xml— a spending-only wallet pays an on-chain-only contact through a Pubky Swaps provider found among its contacts, and the contact receives exactly 20,000 satspaykit-swap-restore.xml— a swap route left in flight before its claim is finished after a wipe and a restore from the recovery phrase: claimed to Savings, with the "Payment returned to Savings" noticeManual Tests
N/A
Automated Checks
PubkySwapsRouteServiceTests.swift— configured providers are asked first and each key onceContactsManagerTests.swift— swap restore waits for a running contact load, or its timeoutSendConfirmationViewTests.swift— swap warnings read the quote's debit from Spending; a late swap result reaches only the sheet and quote that started it; only a swap bitkit-core failed ends a sent hold paymentSendConfirmationViewTestsandPubkySwapsRouteServiceTestson an iPhone simulator (iOS 27.0) atd2621c6: pass;ContactsManagerTestspassed ate878153; the app builds for the simulator (arm64) atd2621c6Evidence
58139bc5d(bitkit-core0.8.0-rc15) on a regtest iPhone 17 Pro simulator (iOS 27.0), against feat: local pubky swaps provider bitkit-docker#39's stack: both pass. Recordings, claim txids and notes are in feat: pay contacts via pubky swaps #1170 (comment).58139bc5d, against feat: local pubky swaps provider bitkit-docker#39 with its rc8fixture-issuer: passes, with norecovery_required. Confirm showed the contact as TO and the Pubky Swaps route; the recording and claim txid are in feat: pay contacts via pubky swaps #1170 (comment).efc33faon a regtest iPhone simulator (iOS 27.0), bitkit-core0.8.0-swaps.9, provider coreyphillips/pubky-swapf45e204with--no-homeserver, and noPUBKY_SWAPS_PROVIDERS, so the provider came only from the wallet's contacts (the recording above):Route: Pubky Swaps · udi3...ez6yand a fee of ₿ 986 for 20,000 sats;hold invoice accepted,HTLC funded; awaiting client claimand, after one block,client claimed; hold invoice settled;66fed8c1caaea25aedd3151d679baa2944c1ee3e17beddddaab74ddee2461c0a, has one input, the provider's HTLC, and one output of exactly 0.0002 BTC to the contact's address; Spending went from ₿ 500,000 to ₿ 479,014.recovery_required. That is a fixture artefact, unrelated to the route; the missing fail-fast is tracked in bug: saved contact payment stalls instead of failing #1254.fab6cf8since finishes the send on the broadcast claim and adds the review fixes; it was built and unit-tested, not rerun on the simulator.83940d3, bitkit-core0.8.0-swaps.4, provider coreyphillips/pubky-swapmainwith--no-homeserver:-PUBKY_SWAPS_PROVIDERSnaming the provider, in case contacts had not reloaded when the restore ran.0.8.0-swaps.2and noPUBKY_SWAPS_PROVIDERS, so the provider came only from the wallet's contacts (the recording above). The build was7315045before its formatting-only change to the new service file:Route: Pubky Swaps · 5no8...pquoand a fee of ₿ 986 for 20,000 sats;HTLC funded; awaiting client claimand, after one block,client claimed; hold invoice settled;quote_expired, and nothing was paid.01198d3, on bitkit-core0.8.0-swaps.3, the unit test selection passes apart from threePaykitPaymentProofServiceTestscases whose billing periods ended on 1 Nov;masterfixed them in421e152("test: keep proof billing periods in the current month"), and this PR's CI runs on the merge withmaster.08cc333, on bitkit-core0.8.0-swaps.4, the unit test selection passes apart from the same three date-boundPaykitPaymentProofServiceTestscases; the restore after a wallet restore is not covered by a unit test or a device run yet.unit-tests.ymlpassed at7315045; SwiftFormat lint is clean on the changed files apart from findings already onmaster, andvalidate-translations.jspasses.Gaps
Models used
claude-opus-5-5(reasoning:medium)claude-opus-5-5(reasoning:medium)