Skip to content
Open
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
11 changes: 8 additions & 3 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -866,9 +866,14 @@ 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)
PaykitPaymentRequestPresentationCoordinator.handleUnavailablePaymentRoute(
request,
app: app,
manager: paykitPaymentRequestManager,
resetWalletSendState: {
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
}
)
continue
}

Expand Down
5 changes: 3 additions & 2 deletions Bitkit/Services/ContactPaymentsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,12 @@ enum ContactPaymentsService {
operations: Operations,
defaults: UserDefaults
) async throws {
defaults.set(canUsePrivatePayments, forKey: PrivatePaykitService.publishingEnabledKey)

if canUsePrivatePayments,
let error = await operations.preparePrivateEndpoints(
contactPublicKeys,
true
false
)
{
throw error
Expand All @@ -128,7 +130,6 @@ 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)
Expand Down
23 changes: 23 additions & 0 deletions Bitkit/Services/PaykitPaymentRequestService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,21 @@ protocol PaykitPaymentRequestPresentationStoring {
func save(_ ids: Set<PaykitPaymentRequest.ID>, identity: String) throws
}

enum PaykitPaymentRequestPresentationCoordinator {
@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 +913,14 @@ final class PaykitPaymentRequestManager {
return true
}

func handleUnavailablePaymentRoute(_ request: PaykitPaymentRequest, insufficientBalance: Bool) {
if insufficientBalance {
_ = markPresentedIfPending(request)
} else {
deferPresentation(request)
}
}

private func performRefresh(
generation: Int,
excludingProtectedRequestId: PaykitPaymentRequest.ID?
Expand Down
107 changes: 81 additions & 26 deletions Bitkit/Services/PrivatePaykitService+Contacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@ 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? {
let publicKeys = rememberSavedContacts(publicKeys, replacing: true)
guard await canPublishPrivateEndpoints(wallet: wallet) else {
if let reason = await privateEndpointPublicationUnavailabilityReason(wallet: wallet) {
Logger.info("Deferring private Paykit endpoint publication during prepare: \(reason)", context: "PrivatePaykitService")
await prepareRelevantPrivateLinksIfAvailable(publicKeys, reason: "prepare")
return requireImmediatePublication && !publicKeys.isEmpty ? PrivatePaykitError.privateUnavailable : nil
}
Expand Down Expand Up @@ -293,45 +304,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 +409,13 @@ 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
}
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 +430,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 +449,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 +473,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
23 changes: 13 additions & 10 deletions Bitkit/Services/PrivatePaykitService+Invoices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,20 @@ extension PrivatePaykitService {

@MainActor
func canPublishPrivateEndpoints(wallet: WalletViewModel) async -> Bool {
guard PaykitFeatureFlags.isUIEnabled,
UserDefaults.standard.bool(forKey: Self.publishingEnabledKey),
UIApplication.shared.applicationState == .active,
wallet.walletExists == true,
wallet.nodeLifecycleState == .running,
let ownPublicKey = await PubkyService.currentPublicKey()
else {
return false
}
await privateEndpointPublicationUnavailabilityReason(wallet: wallet) == nil
}

return PubkyProfileManager.hasLocalSecretKey(for: ownPublicKey)
@MainActor
func privateEndpointPublicationUnavailabilityReason(wallet: WalletViewModel) async -> String? {
guard PaykitFeatureFlags.isUIEnabled else { return "Paykit UI is disabled" }
guard UserDefaults.standard.bool(forKey: Self.publishingEnabledKey) else { return "private publication is disabled" }
guard UIApplication.shared.applicationState == .active else { return "the app is not active" }
guard wallet.walletExists == true else { return "the wallet is unavailable" }
guard wallet.nodeLifecycleState == .running else { return "the Lightning node is not running" }
guard let ownPublicKey = await PubkyService.currentPublicKey() else { return "the Pubky session is not active" }
guard PubkyProfileManager.hasLocalSecretKey(for: ownPublicKey) else { return "the local Pubky secret key is unavailable" }

return nil
}

@MainActor
Expand Down
Loading
Loading