From 97033fefeba136dc9ea5bad9c0b9a36f4142d105 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 26 Aug 2026 09:05:51 -0500 Subject: [PATCH 1/2] fix: harden Paykit contact payments --- .../PrivatePaykitService+Contacts.swift | 1 - Bitkit/ViewModels/AppViewModel.swift | 20 +++++++++++++------ changelog.d/next/paykit-request-qa.fixed.md | 1 + 3 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 changelog.d/next/paykit-request-qa.fixed.md diff --git a/Bitkit/Services/PrivatePaykitService+Contacts.swift b/Bitkit/Services/PrivatePaykitService+Contacts.swift index 0e0c2d151..fc1dd8fc6 100644 --- a/Bitkit/Services/PrivatePaykitService+Contacts.swift +++ b/Bitkit/Services/PrivatePaykitService+Contacts.swift @@ -352,7 +352,6 @@ extension PrivatePaykitService { let linkableReceiverPaths = receiverPathSelection.linkableReceiverPaths let publicationReceiverPaths = receiverPathSelection.publishableReceiverPaths if let error = receiverPathSelection.error { - firstError = firstError ?? error Logger.warn( "Failed to inspect private Paykit receiver markers for \(PubkyPublicKeyFormat.redacted(publicKey)) during \(reason): \(error)", context: "PrivatePaykit" diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index 41c385f5e..a42d258d7 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -468,6 +468,7 @@ extension AppViewModel { data = try await decode(invoice: uri) try ensureScannedDataHandlingOwnership(handlingId, claimedContactPaymentContext: claimedContactPaymentContext) } + let requestedAmount = contactPaymentContext?.incomingPaymentRequest?.amountSats switch data { // BIP21 (Unified) invoice handling @@ -500,7 +501,7 @@ extension AppViewModel { if nodeIsRunning { // Node is running → we have fresh balances; validate immediately. // Prefer lightning; if insufficient or no channels/capacity, fall back to onchain. - let canSendLightning = lightningService.canSend(amountSats: lightningInvoice.amountSatoshis) + let canSendLightning = lightningService.canSend(amountSats: requestedAmount ?? lightningInvoice.amountSatoshis) if canSendLightning { handleScannedLightningInvoice(lightningInvoice, bolt11: lnInvoice, onchainInvoice: invoice) @@ -520,7 +521,10 @@ extension AppViewModel { // usable channels without capacity). // Fall back to onchain and validate onchain balance immediately. let onchainBalance = lightningService.balances?.spendableOnchainBalanceSats ?? 0 - guard validateOnchainBalance(invoiceAmount: invoice.amountSatoshis, onchainBalance: onchainBalance) else { + guard validateOnchainBalance( + invoiceAmount: requestedAmount ?? invoice.amountSatoshis, + onchainBalance: onchainBalance + ) else { return } @@ -544,7 +548,10 @@ extension AppViewModel { // If node is running, validate balance immediately if lightningService.status?.isRunning == true { let onchainBalance = lightningService.balances?.spendableOnchainBalanceSats ?? 0 - guard validateOnchainBalance(invoiceAmount: invoice.amountSatoshis, onchainBalance: onchainBalance) else { + guard validateOnchainBalance( + invoiceAmount: requestedAmount ?? invoice.amountSatoshis, + onchainBalance: onchainBalance + ) else { return } } @@ -575,20 +582,21 @@ extension AppViewModel { // If node is running, we can check for channels and validate immediately if lightningService.status?.isRunning == true { + let paymentAmount = requestedAmount ?? invoice.amountSatoshis // If user has no channels at all, they can never pay a pure lightning invoice. // Show insufficient spending toast and do not navigate to the send flow. let hasAnyChannels = (lightningService.channels?.isEmpty == false) if !hasAnyChannels { let spendingBalance = lightningService.balances?.totalLightningBalanceSats ?? 0 - showInsufficientSpendingToast(invoiceAmount: invoice.amountSatoshis, spendingBalance: spendingBalance) + showInsufficientSpendingToast(invoiceAmount: paymentAmount, spendingBalance: spendingBalance) return } // If channels are usable, validate capacity immediately if let channels = lightningService.channels, channels.contains(where: \.isUsable) { - guard lightningService.canSend(amountSats: invoice.amountSatoshis) else { + guard lightningService.canSend(amountSats: paymentAmount) else { let spendingBalance = lightningService.balances?.totalLightningBalanceSats ?? 0 - showInsufficientSpendingToast(invoiceAmount: invoice.amountSatoshis, spendingBalance: spendingBalance) + showInsufficientSpendingToast(invoiceAmount: paymentAmount, spendingBalance: spendingBalance) return } } diff --git a/changelog.d/next/paykit-request-qa.fixed.md b/changelog.d/next/paykit-request-qa.fixed.md new file mode 100644 index 000000000..568aeec57 --- /dev/null +++ b/changelog.d/next/paykit-request-qa.fixed.md @@ -0,0 +1 @@ +Contact payments now ignore malformed receiver markers from other contacts and show the correct insufficient-balance error for unaffordable payment requests. From 95e19fd8dd29abc60a6a08af73349a78c50c0198 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 26 Aug 2026 09:07:23 -0500 Subject: [PATCH 2/2] chore: rename changelog fragment --- changelog.d/next/{paykit-request-qa.fixed.md => 684.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{paykit-request-qa.fixed.md => 684.fixed.md} (100%) diff --git a/changelog.d/next/paykit-request-qa.fixed.md b/changelog.d/next/684.fixed.md similarity index 100% rename from changelog.d/next/paykit-request-qa.fixed.md rename to changelog.d/next/684.fixed.md