You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Abandoning a hardware-wallet send after its broadcast fails leaves the incoming Paykit payment request accepted and its prepared on-chain proof orphaned. The payer sees nothing more; the requester waits for a payment that will never arrive.
The proof is prepared before the device is asked to sign, in beforeBroadcast (HwSendSignView.swift:114 → SendSheet.prepareHardwareContactPayment, SendSheet.swift:687). When the broadcast then fails on connectivity, the signed transaction is deliberately retained so Retry can reuse it without a second signature, so cancelContactPaymentIfBroadcastIsRetryable() (HwSendSignView.swift:144) correctly declines to cancel the proof — a retry still needs it.
Nothing cancels it on the way out, though. Leaving the screen runs hwSend.cancel() (SendSheet.swift:248), which discards the signed transaction, and app.contactPaymentContext = nil (SendSheet.swift:253), which drops the request handle. The prepared proof row stays behind.
PaykitPaymentProofService.reconcile() never recovers it. It only submits proofs that already carry proofData, and otherwise handles lightning alone:
(PaykitPaymentProofService.swift:286) — an on-chain row with no paymentIdentifier and no proofData is skipped every time, so it persists indefinitely.
Expected behavior
Abandoning a hardware-wallet send whose transaction was never broadcast should return the payment request to an actionable state on both sides, rather than leaving it accepted with an orphaned proof.
Steps to Reproduce
Enable Paykit on two devices and pair them as contacts.
Pair a Trezor on the payer and fund its native-segwit account.
On the payee: request a payment from the payer.
On the payer: open the request, set the funding source to the Trezor, and swipe to pay.
Approve every prompt on the device, cutting connectivity so the broadcast fails (stop the local electrs container, or drop Wi-Fi).
Instead of retrying, leave via the back arrow or dismiss the sheet.
The request stays accepted and the requester never sees a rejection; the pending proof row remains forever.
The naive fix is unsafe and was rejected during review of fix: unlock send sheet after failed broadcast #729. Cancelling the proof when the signed transaction is discarded deletes it for a transaction that may still confirm: .broadcastUncertain is a broadcast timeout, and BroadcastError+Connectivity.swift classifies timeout / network / dns as connectivity alongside "failed to connect to electrum:", so "definitely unsent" cannot currently be told apart from "may already be on the network". completeOnchainPayment then finds no pending row (PaykitPaymentProofService.swift:229) and the proof is lost with no way back.
Android has the safe shape. The sibling issue is synonymdev/bitkit-android#1227, whose fix (commit ae762113f on PR #1186) adds PaykitPaymentProofRepo.failOnchainPayment and routes the cancellation paths through a shared helper. Marking the payment failed rather than deleting the prepared proof rolls the request back without destroying evidence for a transaction that might still confirm. iOS has no failOnchainPayment equivalent; adding one is the suggested direction here.
Scope: only reachable with Paykit payment requests enabled and a hardware funding source. Regular sends unwind correctly via shouldCancelPaymentProof (SendConfirmationView.swift:616), which is unambiguous there because that path has no signed-but-unbroadcast middle state.
What happened?
Abandoning a hardware-wallet send after its broadcast fails leaves the incoming Paykit payment request accepted and its prepared on-chain proof orphaned. The payer sees nothing more; the requester waits for a payment that will never arrive.
The proof is prepared before the device is asked to sign, in
beforeBroadcast(HwSendSignView.swift:114→SendSheet.prepareHardwareContactPayment,SendSheet.swift:687). When the broadcast then fails on connectivity, the signed transaction is deliberately retained so Retry can reuse it without a second signature, socancelContactPaymentIfBroadcastIsRetryable()(HwSendSignView.swift:144) correctly declines to cancel the proof — a retry still needs it.Nothing cancels it on the way out, though. Leaving the screen runs
hwSend.cancel()(SendSheet.swift:248), which discards the signed transaction, andapp.contactPaymentContext = nil(SendSheet.swift:253), which drops the request handle. The prepared proof row stays behind.PaykitPaymentProofService.reconcile()never recovers it. It only submits proofs that already carryproofData, and otherwise handles lightning alone:(
PaykitPaymentProofService.swift:286) — an on-chain row with nopaymentIdentifierand noproofDatais skipped every time, so it persists indefinitely.Expected behavior
Abandoning a hardware-wallet send whose transaction was never broadcast should return the payment request to an actionable state on both sides, rather than leaving it accepted with an orphaned proof.
Steps to Reproduce
electrscontainer, or drop Wi-Fi).Additional context
.broadcastUncertainis a broadcast timeout, andBroadcastError+Connectivity.swiftclassifiestimeout/network/dnsas connectivity alongside"failed to connect to electrum:", so "definitely unsent" cannot currently be told apart from "may already be on the network".completeOnchainPaymentthen finds no pending row (PaykitPaymentProofService.swift:229) and the proof is lost with no way back.ae762113fon PR #1186) addsPaykitPaymentProofRepo.failOnchainPaymentand routes the cancellation paths through a shared helper. Marking the payment failed rather than deleting the prepared proof rolls the request back without destroying evidence for a transaction that might still confirm. iOS has nofailOnchainPaymentequivalent; adding one is the suggested direction here.shouldCancelPaymentProof(SendConfirmationView.swift:616), which is unambiguous there because that path has no signed-but-unbroadcast middle state.