Skip to content

Commit ee78a43

Browse files
authored
Merge pull request #544 from synonymdev/codex/fix-public-payments-polish-part1
2 parents 0401d3a + c67f220 commit ee78a43

14 files changed

Lines changed: 310 additions & 36 deletions

Bitkit/Extensions/Activity+Contact.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ extension Activity {
66
return contacts.first(where: { PubkyPublicKeyFormat.matches($0.publicKey, contactPublicKey) })
77
}
88

9+
func isReplacedSentTransaction(txIdsInBoostTxIds: Set<String>) -> Bool {
10+
guard case let .onchain(onchain) = self else { return false }
11+
return !onchain.doesExist && onchain.txType == .sent && txIdsInBoostTxIds.contains(onchain.txId)
12+
}
13+
914
private var contactPublicKey: String? {
1015
switch self {
1116
case let .lightning(lightning):

Bitkit/Managers/ScannerManager.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ class ScannerManager: ObservableObject {
3838
}
3939

4040
func handleScan(_ uri: String, context: ScannerContext) async {
41+
let uri = uri.trimmingCharacters(in: .whitespacesAndNewlines)
42+
guard !uri.isEmpty else { return }
43+
4144
Haptics.play(.scanSuccess)
4245

4346
switch context {
@@ -90,7 +93,7 @@ class ScannerManager: ObservableObject {
9093
}
9194
}
9295

93-
private func handlePubkyRouteIfNeeded(_ input: String) -> Bool {
96+
private func handlePubkyRouteIfNeeded(_ input: String, hiding sheetId: SheetID? = .scanner, reason: String = "Scanner routed pubky key") -> Bool {
9497
guard let navigation,
9598
let route = resolvePastedPubkyRoute(
9699
input: input,
@@ -101,7 +104,9 @@ class ScannerManager: ObservableObject {
101104
return false
102105
}
103106

104-
sheets?.hideSheetIfActive(.scanner, reason: "Scanner routed pubky key")
107+
if let sheetId {
108+
sheets?.hideSheetIfActive(sheetId, reason: reason)
109+
}
105110
navigation.navigate(route)
106111
return true
107112
}
@@ -115,6 +120,11 @@ class ScannerManager: ObservableObject {
115120
Haptics.play(.scanSuccess)
116121

117122
do {
123+
if handlePubkyRouteIfNeeded(uri, hiding: .send, reason: "Send scanner routed pubky key") {
124+
completion(nil)
125+
return
126+
}
127+
118128
try await app.handleScannedData(uri)
119129

120130
let route = PaymentNavigationHelper.appropriateSendRoute(
@@ -177,7 +187,7 @@ class ScannerManager: ObservableObject {
177187
return
178188
}
179189

180-
await handleScan(uri, context: context)
190+
await handleScan(uri.trimmingCharacters(in: .whitespacesAndNewlines), context: context)
181191
}
182192

183193
func handleImageSelection(_ item: PhotosPickerItem?, context: ScannerContext, completion: @escaping (SendRoute?) -> Void = { _ in }) async {

Bitkit/Services/CoreService.swift

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ class ActivityService {
582582
{
583583
activity.boostTxIds.append(txid)
584584
activity.isBoosted = true
585+
activity.contact = activity.contact ?? replacedActivity?.contact
585586
activity.updatedAt = UInt64(Date().timeIntervalSince1970)
586587
try await self.update(id: activity.id, activity: .onchain(activity))
587588

@@ -965,17 +966,25 @@ class ActivityService {
965966

966967
func get(contact publicKey: String, sortDirection: SortDirection = .desc) async throws -> [Activity] {
967968
let normalizedKey = PubkyPublicKeyFormat.normalized(publicKey) ?? publicKey
969+
let txIdsInBoostTxIds = await getTxIdsInBoostTxIds()
968970
// TODO: push contact filtering into BitkitCore once the activity store exposes it.
969971
let activities = try await get(filter: .all, sortDirection: sortDirection)
970972

971-
return activities.filter { activity in
972-
switch activity {
973-
case let .lightning(lightning):
974-
return PubkyPublicKeyFormat.matches(lightning.contact, normalizedKey)
975-
case let .onchain(onchain):
976-
return PubkyPublicKeyFormat.matches(onchain.contact, normalizedKey)
973+
return activities
974+
.filter { !isReplacedSentTransaction($0, txIdsInBoostTxIds: txIdsInBoostTxIds) }
975+
.filter { activity in
976+
switch activity {
977+
case let .lightning(lightning):
978+
return PubkyPublicKeyFormat.matches(lightning.contact, normalizedKey)
979+
case let .onchain(onchain):
980+
return PubkyPublicKeyFormat.matches(onchain.contact, normalizedKey)
981+
}
977982
}
978-
}
983+
}
984+
985+
private func isReplacedSentTransaction(_ activity: Activity, txIdsInBoostTxIds: Set<String>) -> Bool {
986+
guard case let .onchain(onchain) = activity else { return false }
987+
return !onchain.doesExist && onchain.txType == .sent && txIdsInBoostTxIds.contains(onchain.txId)
979988
}
980989

981990
func update(id: String, activity: Activity) async throws {
@@ -1046,7 +1055,7 @@ class ActivityService {
10461055
let normalizedContact = publicKey.map { PubkyPublicKeyFormat.normalized($0) ?? $0 }
10471056

10481057
try await ServiceQueue.background(.core) {
1049-
guard let activity = try getActivityById(activityId: id) else {
1058+
guard let activity = try getActivityById(activityId: id) ?? (try? BitkitCore.getActivityByTxId(txId: id)).map(Activity.onchain) else {
10501059
throw AppError(message: "Activity not found", debugMessage: "Activity with ID \(id) not found")
10511060
}
10521061

@@ -1055,19 +1064,49 @@ class ActivityService {
10551064
guard lightning.contact != normalizedContact else { return }
10561065
lightning.contact = normalizedContact
10571066
lightning.updatedAt = UInt64(Date().timeIntervalSince1970)
1058-
try updateActivity(activityId: id, activity: .lightning(lightning))
1067+
try updateActivity(activityId: lightning.id, activity: .lightning(lightning))
10591068
self.activitiesChangedSubject.send()
10601069

10611070
case var .onchain(onchain):
1062-
guard onchain.contact != normalizedContact else { return }
1063-
onchain.contact = normalizedContact
1064-
onchain.updatedAt = UInt64(Date().timeIntervalSince1970)
1065-
try updateActivity(activityId: id, activity: .onchain(onchain))
1066-
self.activitiesChangedSubject.send()
1071+
let contactChanged = onchain.contact != normalizedContact
1072+
if contactChanged {
1073+
onchain.contact = normalizedContact
1074+
onchain.updatedAt = UInt64(Date().timeIntervalSince1970)
1075+
try updateActivity(activityId: onchain.id, activity: .onchain(onchain))
1076+
}
1077+
1078+
let replacementContactChanged = try self.updateReplacementContactIfNeeded(for: onchain, normalizedContact: normalizedContact)
1079+
if contactChanged || replacementContactChanged {
1080+
self.activitiesChangedSubject.send()
1081+
}
10671082
}
10681083
}
10691084
}
10701085

1086+
private func updateReplacementContactIfNeeded(for activity: OnchainActivity, normalizedContact: String?) throws -> Bool {
1087+
guard !activity.doesExist, activity.txType == .sent else { return false }
1088+
1089+
let activities = try getActivities(
1090+
filter: .onchain,
1091+
txType: nil,
1092+
tags: nil,
1093+
search: nil,
1094+
minDate: nil,
1095+
maxDate: nil,
1096+
limit: nil,
1097+
sortDirection: nil
1098+
)
1099+
var didUpdate = false
1100+
for case var .onchain(replacement) in activities where replacement.boostTxIds.contains(activity.txId) {
1101+
guard replacement.contact != normalizedContact else { continue }
1102+
replacement.contact = normalizedContact
1103+
replacement.updatedAt = UInt64(Date().timeIntervalSince1970)
1104+
try updateActivity(activityId: replacement.id, activity: .onchain(replacement))
1105+
didUpdate = true
1106+
}
1107+
return didUpdate
1108+
}
1109+
10711110
func delete(id: String) async throws -> Bool {
10721111
try await ServiceQueue.background(.core) {
10731112
// Rebuild cache if deleting an onchain activity with boostTxIds

Bitkit/Services/LightningService.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,10 @@ extension LightningService {
11441144
Logger.error(error, context: "node.eventHandled()")
11451145
}
11461146

1147+
if case .channelReady = event {
1148+
await refreshChannelCache()
1149+
}
1150+
11471151
onEvent?(event)
11481152

11491153
switch event {
@@ -1204,7 +1208,6 @@ extension LightningService {
12041208
Logger.info(
12051209
"👐 Channel ready: channelId: \(channelId) userChannelId: \(userChannelId) counterpartyNodeId: \(counterpartyNodeId ?? "?") fundingTxo: \(fundingTxo != nil ? "\(fundingTxo!.txid):\(fundingTxo!.vout)" : "nil")"
12061210
)
1207-
await refreshChannelCache()
12081211
case let .channelClosed(channelId, userChannelId, counterpartyNodeId, reason):
12091212
let reasonString = reason.map { String(describing: $0) } ?? ""
12101213
Logger.info(

Bitkit/ViewModels/ActivityListViewModel.swift

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,7 @@ class ActivityListViewModel: ObservableObject {
273273
}
274274

275275
func contactActivities(publicKey: String) async throws -> [Activity] {
276-
let activities = try await coreService.activity.get(contact: publicKey, sortDirection: .desc)
277-
return await filterOutReplacedSentTransactions(activities)
276+
try await coreService.activity.get(contact: publicKey, sortDirection: .desc)
278277
}
279278

280279
func setContact(_ contactPublicKey: String, forPaymentId paymentId: String, syncLdkPayments: Bool = true) async throws {
@@ -470,19 +469,7 @@ extension ActivityListViewModel {
470469
// Get cached set of txIds that appear in boostTxIds
471470
let txIdsInBoostTxIds = await coreService.activity.getTxIdsInBoostTxIds()
472471

473-
// Filter out activities that:
474-
// 1. Are onchain
475-
// 2. Have doesExist = false
476-
// 3. Are sent transactions
477-
// 4. Appear in another transaction's boostTxIds
478-
return activities.filter { activity in
479-
if case let .onchain(onchain) = activity {
480-
if !onchain.doesExist && onchain.txType == .sent && txIdsInBoostTxIds.contains(onchain.txId) {
481-
return false
482-
}
483-
}
484-
return true
485-
}
472+
return activities.filter { !$0.isReplacedSentTransaction(txIdsInBoostTxIds: txIdsInBoostTxIds) }
486473
}
487474

488475
/// Filter activities based on the selected tab

Bitkit/ViewModels/AppViewModel.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,13 @@ extension AppViewModel {
667667
return
668668
}
669669

670+
if PubkyPublicKeyFormat.normalized(normalized) != nil {
671+
guard currentSequence == manualEntryValidationSequence else { return }
672+
manualEntryValidationResult = .valid
673+
isManualEntryInputValid = true
674+
return
675+
}
676+
670677
// Try to decode the invoice
671678
guard let decodedData = try? await decode(invoice: normalized) else {
672679
guard currentSequence == manualEntryValidationSequence else { return }

Bitkit/ViewModels/NavigationViewModel.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func fallbackRouteForMissingPendingImport(hasPendingImport: Bool) -> Route? {
126126
hasPendingImport ? nil : .payContacts
127127
}
128128

129-
func resolvePastedPubkyRoute(input: String, ownPublicKey: String?, contacts: [PubkyContact]) -> Route? {
129+
func resolvePubkyRoute(input: String, ownPublicKey: String?, contacts: [PubkyContact]) -> Route? {
130130
guard let normalizedKey = PubkyPublicKeyFormat.normalized(input) else {
131131
return nil
132132
}
@@ -142,6 +142,10 @@ func resolvePastedPubkyRoute(input: String, ownPublicKey: String?, contacts: [Pu
142142
return .addContact(publicKey: normalizedKey)
143143
}
144144

145+
func resolvePastedPubkyRoute(input: String, ownPublicKey: String?, contacts: [PubkyContact]) -> Route? {
146+
resolvePubkyRoute(input: input, ownPublicKey: ownPublicKey, contacts: contacts)
147+
}
148+
145149
@MainActor
146150
class NavigationViewModel: ObservableObject {
147151
@Published var path: [Route] = []

Bitkit/ViewModels/WalletViewModel.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ class WalletViewModel: ObservableObject {
216216
case .channelReady:
217217
self.bolt11 = ""
218218
Task {
219+
await self.reconnectTrustedPeers()
219220
await self.refreshAndSyncState()
220221
try? await self.refreshBip21()
221222
}
@@ -861,12 +862,20 @@ class WalletViewModel: ObservableObject {
861862

862863
/// Sync channels and peers only
863864
private func syncChannelsAndPeers() {
865+
let hadUsableChannels = channels?.contains(where: \.isUsable) ?? false
864866
peers = lightningService.peers
865867
channels = lightningService.channels
868+
let hasUsableChannels = channels?.contains(where: \.isUsable) ?? false
866869

867870
if let channels {
868871
channelCount = channels.count
869872
}
873+
874+
if sharesPublicPaykitEndpoints, hasUsableChannels, !hadUsableChannels {
875+
Task { [weak self] in
876+
await self?.syncPublicPaykitEndpointsAfterChannelBecameUsable()
877+
}
878+
}
870879
}
871880

872881
/// Sync balance details only
@@ -961,7 +970,7 @@ class WalletViewModel: ObservableObject {
961970
func refreshPublicPaykitEndpoints(forceRefreshBolt11: Bool = false) async throws -> (onchainAddress: String, bolt11: String) {
962971
let publicOnchainAddress = try await refreshReusableOnchainAddress()
963972

964-
if hasReadyChannels {
973+
if hasUsableChannels {
965974
let hasReusableInvoice = await hasReusablePublicPaykitInvoice()
966975
let shouldRefreshBolt11 = forceRefreshBolt11 || !hasReusableInvoice
967976
if shouldRefreshBolt11 {
@@ -984,6 +993,14 @@ class WalletViewModel: ObservableObject {
984993
}
985994
}
986995

996+
private func syncPublicPaykitEndpointsAfterChannelBecameUsable() async {
997+
do {
998+
try await PublicPaykitService.syncPublishedEndpoints(wallet: self, publish: true)
999+
} catch {
1000+
Logger.warn("Failed to refresh public Paykit endpoints after channel became usable: \(error)", context: "WalletViewModel")
1001+
}
1002+
}
1003+
9871004
private func hasReusablePublicPaykitInvoice() async -> Bool {
9881005
guard !publicPaykitBolt11.isEmpty else { return false }
9891006
guard publicPaykitBolt11ExpiresAt > Date().timeIntervalSince1970 + Self.publicPaykitInvoiceRefreshBufferSeconds else { return false }

Bitkit/Views/Contacts/AddContactView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ struct AddContactView: View {
321321
return false
322322
}
323323

324+
navigation.navigateBack()
324325
app.contactPaymentContext = ContactPaymentContext(publicKey: publicKey)
325326
sheets.showSheet(.send, data: SendConfig(view: route))
326327
return true

Bitkit/Views/Wallets/Send/SendEnterManuallyView.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ struct SendEnterManuallyView: View {
55
@EnvironmentObject var wallet: WalletViewModel
66
@EnvironmentObject var currency: CurrencyViewModel
77
@EnvironmentObject var settings: SettingsViewModel
8+
@EnvironmentObject var contactsManager: ContactsManager
9+
@EnvironmentObject var navigation: NavigationViewModel
10+
@EnvironmentObject var pubkyProfile: PubkyProfileManager
11+
@EnvironmentObject var sheets: SheetViewModel
812

913
@Binding var navigationPath: [SendRoute]
1014
@FocusState private var isTextEditorFocused: Bool
@@ -77,6 +81,16 @@ struct SendEnterManuallyView: View {
7781

7882
guard !uri.isEmpty, app.isManualEntryInputValid else { return }
7983

84+
if let route = resolvePubkyRoute(
85+
input: uri,
86+
ownPublicKey: pubkyProfile.publicKey,
87+
contacts: contactsManager.contacts
88+
) {
89+
sheets.hideSheetIfActive(.send, reason: "Manual pubky entry routed to contacts")
90+
navigation.navigate(route)
91+
return
92+
}
93+
8094
do {
8195
wallet.resetSendState(speed: settings.defaultTransactionSpeed)
8296

@@ -106,5 +120,11 @@ struct SendEnterManuallyView: View {
106120
SendEnterManuallyView(navigationPath: .constant([]))
107121
.environmentObject(AppViewModel())
108122
.environmentObject(WalletViewModel())
123+
.environmentObject(CurrencyViewModel())
124+
.environmentObject(SettingsViewModel.shared)
125+
.environmentObject(ContactsManager())
126+
.environmentObject(NavigationViewModel())
127+
.environmentObject(PubkyProfileManager())
128+
.environmentObject(SheetViewModel())
109129
.preferredColorScheme(.dark)
110130
}

0 commit comments

Comments
 (0)