Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,7 @@ struct AppScene: View {
app.contactPaymentContext == nil
else { return }

var shouldPresentNextRequest = true
let attemptedPresentation = await paykitPaymentRequestManager.presentRequests { requests in
guard sheets.activeSheetConfiguration == nil, !sheets.isReplacingSheet, app.contactPaymentContext == nil else { return }
for request in requests {
Expand Down Expand Up @@ -867,10 +868,16 @@ struct AppScene: View {
return
}
guard PaymentNavigationHelper.appropriateSendRoute(app: app, currency: currency, settings: settings) != nil else {
app.resetSendState()
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
paykitPaymentRequestManager.deferPresentation(request)
continue
PaykitPaymentRequestPresentationCoordinator.handleUnavailablePaymentRoute(
Comment thread
ben-kaufman marked this conversation as resolved.
request,
app: app,
manager: paykitPaymentRequestManager,
resetWalletSendState: {
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
}
)
shouldPresentNextRequest = false
return
}

guard paykitPaymentRequestManager.isCurrentPresentation(request) else {
Expand All @@ -894,6 +901,15 @@ struct AppScene: View {
Logger.warn("Failed to present incoming Paykit payment request: \(error)", context: "AppScene")
app.resetSendState()
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
if PaykitPaymentRequestPresentationCoordinator.handleAmountMismatch(
error,
request: request,
manager: paykitPaymentRequestManager,
showError: { app.toast($0) }
) {
shouldPresentNextRequest = false
return
}
paykitPaymentRequestManager.deferPresentation(request)
continue
}
Expand Down Expand Up @@ -921,6 +937,7 @@ struct AppScene: View {
}

guard attemptedPresentation,
shouldPresentNextRequest,
paykitPaymentRequestManager.requestedPresentationId != nil ||
!paykitPaymentRequestManager.requestsForPresentation().isEmpty,
sheets.activeSheetConfiguration == nil,
Expand Down
20 changes: 14 additions & 6 deletions Bitkit/Services/ContactPaymentsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ enum ContactPaymentsService {
operations: Operations,
defaults: UserDefaults
) async throws {
if !canUsePrivatePayments, defaults.bool(forKey: PrivatePaykitService.publishingEnabledKey) {
operations.setPrivateCleanupPending(true)
}
defaults.set(true, forKey: PublicPaykitService.publishingEnabledKey)
defaults.set(canUsePrivatePayments, forKey: PrivatePaykitService.publishingEnabledKey)
Comment thread
ben-kaufman marked this conversation as resolved.
defaults.set(true, forKey: confirmedPreferenceKey)

if canUsePrivatePayments,
let error = await operations.preparePrivateEndpoints(
contactPublicKeys,
Expand All @@ -127,12 +134,10 @@ enum ContactPaymentsService {

try await operations.syncPublicEndpoints(true)

defaults.set(true, forKey: PublicPaykitService.publishingEnabledKey)
defaults.set(canUsePrivatePayments, forKey: PrivatePaykitService.publishingEnabledKey)
defaults.set(true, forKey: confirmedPreferenceKey)

operations.setPublicCleanupPending(false)
operations.setPrivateCleanupPending(false)
if canUsePrivatePayments {
Comment thread
ben-kaufman marked this conversation as resolved.
operations.setPrivateCleanupPending(false)
}
}

@MainActor
Expand All @@ -142,7 +147,10 @@ enum ContactPaymentsService {
operations.setPrivateCleanupPending(false)
Comment thread
ben-kaufman marked this conversation as resolved.
} catch {
operations.setPrivateCleanupPending(true)
throw error
Logger.warn(
"Deferred private Paykit endpoint cleanup after disable failed: \(error)",
context: "ContactPaymentsService"
)
}

do {
Expand Down
40 changes: 40 additions & 0 deletions Bitkit/Services/PaykitPaymentRequestService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ struct PaykitPaymentRequest: Identifiable, Hashable {
return !overflow && milliSatoshis == requestedMilliSatoshis
}

func acceptsLightningInvoiceAmount(satoshis: UInt64) -> Bool {
satoshis == 0 || satoshis == amountSats
}

func acceptsPaymentAmount(_ amountSats: UInt64) -> Bool {
amountSats == self.amountSats
}
Expand Down Expand Up @@ -542,6 +546,34 @@ protocol PaykitPaymentRequestPresentationStoring {
func save(_ ids: Set<PaykitPaymentRequest.ID>, identity: String) throws
}

enum PaykitPaymentRequestPresentationCoordinator {
@MainActor
static func handleAmountMismatch(
_ error: Error,
request: PaykitPaymentRequest,
manager: PaykitPaymentRequestManager,
showError: (Error) -> Void
) -> Bool {
guard error as? PaykitPaymentRequestError == .amountMismatch else { return false }
showError(error)
_ = manager.markPresentedIfPending(request)
return true
}

@MainActor
static func handleUnavailablePaymentRoute(
_ request: PaykitPaymentRequest,
app: AppViewModel,
manager: PaykitPaymentRequestManager,
resetWalletSendState: () -> Void
) {
let insufficientBalance = app.didRejectScannedPaymentForInsufficientBalance
app.resetSendState()
resetWalletSendState()
manager.handleUnavailablePaymentRoute(request, insufficientBalance: insufficientBalance)
}
}

struct PaykitPaymentRequestPresentationStore: PaykitPaymentRequestPresentationStoring {
private struct State: Codable {
var idsByIdentity: [String: [PaykitPaymentRequest.ID]]
Expand Down Expand Up @@ -898,6 +930,14 @@ final class PaykitPaymentRequestManager {
return true
}

func handleUnavailablePaymentRoute(_ request: PaykitPaymentRequest, insufficientBalance: Bool) {
Comment thread
ben-kaufman marked this conversation as resolved.
if insufficientBalance {
_ = markPresentedIfPending(request)
} else {
deferPresentation(request)
}
}

private func performRefresh(
generation: Int,
excludingProtectedRequestId: PaykitPaymentRequest.ID?
Expand Down
150 changes: 112 additions & 38 deletions Bitkit/Services/PrivatePaykitService+Contacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,51 @@ import Paykit
// MARK: - Saved Contacts

extension PrivatePaykitService {
struct EndpointPublicationOperations {
let currentPublicKey: () async -> String?
let linkedReceiverPaths: (_ reason: String) async -> (paths: [String: Set<String>], error: Error?)
let receiverPaths: (_ publicKey: String) async throws -> [String]
let receiverPathSelection: (_ publicKey: String, _ receiverPaths: [String]) async throws -> PrivateReceiverPathSelection
let ensureLink: (_ publicKey: String, _ receiverPath: String) async throws -> Void
let buildEndpoints: (_ publicKey: String, _ receiverPath: String) async throws -> [PublicPaykitService.Endpoint]
let syncPaymentLists: (_ updates: [PrivatePaymentListReservationUpdateInput]) async throws -> PrivatePaymentListDeliveryReport
}

@discardableResult
func prepareSavedContacts(
_ publicKeys: [String],
wallet: WalletViewModel,
requireImmediatePublication: Bool = false
) async -> Error? {
await prepareSavedContacts(
publicKeys,
publicationUnavailableReason: privateEndpointPublicationUnavailabilityReason(wallet: wallet),
prepareLinks: { await self.prepareRelevantPrivateLinksIfAvailable($0, reason: "prepare") },
publishEndpoints: { publicKeys in
await PrivatePaykitAddressReservationStore.shared.reconcileReservedIndexesWithLdk()
return await self.syncLocalEndpointPublication(
for: publicKeys,
wallet: wallet,
reason: "prepare",
requireImmediatePublication: requireImmediatePublication
)
}
)
}

func prepareSavedContacts(
_ publicKeys: [String],
publicationUnavailableReason: String?,
prepareLinks: ([String]) async -> Void,
publishEndpoints: ([String]) async -> Error?
) async -> Error? {
let publicKeys = rememberSavedContacts(publicKeys, replacing: true)
guard await canPublishPrivateEndpoints(wallet: wallet) else {
await prepareRelevantPrivateLinksIfAvailable(publicKeys, reason: "prepare")
return requireImmediatePublication && !publicKeys.isEmpty ? PrivatePaykitError.privateUnavailable : nil
if let reason = publicationUnavailableReason {
Logger.info("Deferring private Paykit endpoint publication during prepare: \(reason)", context: "PrivatePaykitService")
await prepareLinks(publicKeys)
return nil
}

await PrivatePaykitAddressReservationStore.shared.reconcileReservedIndexesWithLdk()

return await syncLocalEndpointPublication(
for: publicKeys,
wallet: wallet,
reason: "prepare",
requireImmediatePublication: requireImmediatePublication
)
return await publishEndpoints(publicKeys)
}

func refreshSavedContactEndpoints(
Expand Down Expand Up @@ -293,45 +318,101 @@ extension PrivatePaykitService {
reason: String,
forceRefreshLightning: Bool = false,
requireImmediatePublication: Bool
) async -> Error? {
let operations = endpointPublicationOperations(
wallet: wallet,
forceRefreshLightning: forceRefreshLightning
)
return await syncLocalEndpointPublication(
for: publicKeys,
reason: reason,
requireImmediatePublication: requireImmediatePublication,
operations: operations
)
}

func syncLocalEndpointPublication(
for publicKeys: [String],
reason: String,
requireImmediatePublication: Bool,
operations: EndpointPublicationOperations
) async -> Error? {
do {
return try await withPublicationLock {
await syncLocalEndpointPublicationLocked(
for: publicKeys,
wallet: wallet,
reason: reason,
forceRefreshLightning: forceRefreshLightning,
requireImmediatePublication: requireImmediatePublication
requireImmediatePublication: requireImmediatePublication,
operations: operations
)
}
} catch {
return requireImmediatePublication ? error : nil
}
}

private func endpointPublicationOperations(
wallet: WalletViewModel,
forceRefreshLightning: Bool
) -> EndpointPublicationOperations {
EndpointPublicationOperations(
currentPublicKey: {
await PubkyService.currentPublicKey()
},
linkedReceiverPaths: { reason in
await self.linkedReceiverPathsSnapshot(reason: reason)
},
receiverPaths: { publicKey in
try await self.receiverPathsForSavedContact(publicKey: publicKey)
},
receiverPathSelection: { publicKey, receiverPaths in
try await PaykitSdkService.shared.privateReceiverPathSelection(
publicKey: publicKey,
savedReceiverPaths: receiverPaths
)
},
ensureLink: { publicKey, receiverPath in
_ = try await PaykitSdkService.shared.ensureLinkWithPeer(publicKey, receiverPath: receiverPath)
},
buildEndpoints: { publicKey, receiverPath in
try await self.buildLocalEndpoints(
for: publicKey,
receiverPath: receiverPath,
wallet: wallet,
forceRefreshLightning: forceRefreshLightning
)
},
syncPaymentLists: { updates in
try await PaykitSdkService.shared.syncPrivatePaymentListsWithReservations(
updates,
clearUnlistedLinkedPeers: false
)
}
)
}

private func syncLocalEndpointPublicationLocked(
for publicKeys: [String],
wallet: WalletViewModel,
reason: String,
forceRefreshLightning: Bool = false,
requireImmediatePublication: Bool
requireImmediatePublication: Bool,
operations: EndpointPublicationOperations
) async -> Error? {
let publicKeys = normalizedSavedContactKeys(publicKeys)
guard !publicKeys.isEmpty else { return nil }

guard await PubkyService.currentPublicKey() != nil else {
guard await operations.currentPublicKey() != nil else {
return requireImmediatePublication ? PubkyServiceError.sessionNotActive : nil
}

let linkedReceiverPathsSnapshot = await linkedReceiverPathsSnapshot(reason: reason)
let linkedReceiverPathsSnapshot = await operations.linkedReceiverPaths(reason)
var firstError = linkedReceiverPathsSnapshot.error
var updates = [PrivatePaymentListReservationUpdateInput]()
var linkRetryKeys = [PrivateMessageDrainRetryKey]()

for publicKey in publicKeys {
let receiverPaths: [String]
do {
receiverPaths = try await receiverPathsForSavedContact(publicKey: publicKey)
receiverPaths = try await operations.receiverPaths(publicKey)
} catch {
firstError = firstError ?? error
Logger.warn(
Expand All @@ -342,17 +423,18 @@ extension PrivatePaykitService {
}
let receiverPathSelection: PrivateReceiverPathSelection
do {
receiverPathSelection = try await PaykitSdkService.shared.privateReceiverPathSelection(
publicKey: publicKey,
savedReceiverPaths: receiverPaths
)
receiverPathSelection = try await operations.receiverPathSelection(publicKey, receiverPaths)
} catch {
return error
firstError = firstError ?? error
Logger.warn(
"Failed to select private Paykit receiver paths for \(PubkyPublicKeyFormat.redacted(publicKey)) during \(reason): \(error)",
context: "PrivatePaykit"
)
continue
}
let linkableReceiverPaths = receiverPathSelection.linkableReceiverPaths
let publicationReceiverPaths = receiverPathSelection.publishableReceiverPaths
if let error = receiverPathSelection.error {
firstError = firstError ?? error
Comment thread
ben-kaufman marked this conversation as resolved.
Logger.warn(
"Failed to inspect private Paykit receiver markers for \(PubkyPublicKeyFormat.redacted(publicKey)) during \(reason): \(error)",
context: "PrivatePaykit"
Expand All @@ -367,7 +449,7 @@ extension PrivatePaykitService {
for receiverPath in Set(linkableReceiverPaths).union(cleanupReceiverPaths) {
linkRetryKeys.append(PrivateMessageDrainRetryKey(publicKey: publicKey, receiverPath: receiverPath))
do {
_ = try await PaykitSdkService.shared.ensureLinkWithPeer(publicKey, receiverPath: receiverPath)
try await operations.ensureLink(publicKey, receiverPath)
} catch {
Logger.warn(
"Failed to prepare private Paykit link for \(PubkyPublicKeyFormat.redacted(publicKey)) during \(reason): \(error)",
Expand All @@ -386,12 +468,7 @@ extension PrivatePaykitService {

for receiverPath in publicationReceiverPaths {
do {
let endpoints = try await buildLocalEndpoints(
for: publicKey,
receiverPath: receiverPath,
wallet: wallet,
forceRefreshLightning: forceRefreshLightning
)
let endpoints = try await operations.buildEndpoints(publicKey, receiverPath)
let reservations = reservations(from: endpoints, publicKey: publicKey, receiverPath: receiverPath)
let update = PrivatePaymentListReservationUpdateInput(
counterparty: publicKey,
Expand All @@ -415,10 +492,7 @@ extension PrivatePaykitService {
}

do {
let report = try await PaykitSdkService.shared.syncPrivatePaymentListsWithReservations(
updates,
clearUnlistedLinkedPeers: false
)
let report = try await operations.syncPaymentLists(updates)
let deliveryError = applyPrivatePaymentListDeliveryReport(report, reason: reason)
firstError = firstError ?? deliveryError
let retryKeys = linkRetryKeys + privatePaymentListDeliveryRetryKeys(from: report)
Expand Down
Loading
Loading