@@ -61,6 +61,99 @@ class ActivityService {
6161 }
6262 }
6363
64+ private func mapToCoreTransactionDetails( txid: String , _ details: LDKNode . TransactionDetails ) -> BitkitCore . TransactionDetails {
65+ let inputs = details. inputs. map { input in
66+ BitkitCore . TxInput (
67+ txid: input. txid,
68+ vout: input. vout,
69+ scriptsig: input. scriptsig,
70+ witness: input. witness,
71+ sequence: input. sequence
72+ )
73+ }
74+
75+ let outputs = details. outputs. map { output in
76+ BitkitCore . TxOutput (
77+ scriptpubkey: output. scriptpubkey,
78+ scriptpubkeyType: output. scriptpubkeyType,
79+ scriptpubkeyAddress: output. scriptpubkeyAddress,
80+ value: output. value,
81+ n: output. n
82+ )
83+ }
84+
85+ return BitkitCore . TransactionDetails (
86+ txId: txid,
87+ amountSats: details. amountSats,
88+ inputs: inputs,
89+ outputs: outputs
90+ )
91+ }
92+
93+ private func fetchTransactionDetails( txid: String ) async -> BitkitCore . TransactionDetails ? {
94+ do {
95+ return try await getTransactionDetails ( txid: txid)
96+ } catch {
97+ Logger . warn ( " Failed to fetch stored transaction details for \( txid) : \( error) " , context: " ActivityService " )
98+ return nil
99+ }
100+ }
101+
102+ func getTransactionDetails( txid: String ) async throws -> BitkitCore . TransactionDetails ? {
103+ try await ServiceQueue . background ( . core) {
104+ try BitkitCore . getTransactionDetails ( txId: txid)
105+ }
106+ }
107+
108+ // MARK: - Seen Tracking
109+
110+ func isActivitySeen( id: String ) async -> Bool {
111+ do {
112+ if let activity = try await getActivityById ( activityId: id) {
113+ switch activity {
114+ case let . onchain( onchain) :
115+ return onchain. seenAt != nil
116+ case let . lightning( lightning) :
117+ return lightning. seenAt != nil
118+ }
119+ }
120+ } catch {
121+ Logger . error ( " Failed to check seen status for activity \( id) : \( error) " , context: " ActivityService " )
122+ }
123+ return false
124+ }
125+
126+ func isOnchainActivitySeen( txid: String ) async -> Bool {
127+ if let activity = try ? await getOnchainActivityByTxId ( txid: txid) {
128+ return activity. seenAt != nil
129+ }
130+ return false
131+ }
132+
133+ func markActivityAsSeen( id: String, seenAt: UInt64? = nil ) async {
134+ let timestamp = seenAt ?? UInt64 ( Date ( ) . timeIntervalSince1970)
135+
136+ do {
137+ try await ServiceQueue . background ( . core) {
138+ try BitkitCore . markActivityAsSeen ( activityId: id, seenAt: timestamp)
139+ self . activitiesChangedSubject. send ( )
140+ }
141+ } catch {
142+ Logger . error ( " Failed to mark activity \( id) as seen: \( error) " , context: " ActivityService " )
143+ }
144+ }
145+
146+ func markOnchainActivityAsSeen( txid: String , seenAt: UInt64 ? = nil ) async {
147+ do {
148+ guard let activity = try await getOnchainActivityByTxId ( txid: txid) else {
149+ return
150+ }
151+ await markActivityAsSeen ( id: activity. id, seenAt: seenAt)
152+ } catch {
153+ Logger . error ( " Failed to mark onchain activity for \( txid) as seen: \( error) " , context: " ActivityService " )
154+ }
155+ }
156+
64157 // MARK: - Transaction Status Checks
65158
66159 func wasTransactionReplaced( txid: String ) async -> Bool {
@@ -84,17 +177,15 @@ class ActivityService {
84177 return false
85178 }
86179
87- do {
88- // Check if this transaction's activity has boostTxIds (meaning it replaced other transactions)
89- // If any of the replaced transactions have the same value, don't show the sheet
90- guard let onchain = try ? await getOnchainActivityByTxId ( txid: txid) ,
91- !onchain. boostTxIds. isEmpty
92- else {
93- return true
94- }
180+ let onchainActivity = try ? await getOnchainActivityByTxId ( txid: txid)
181+
182+ if let onchainActivity, onchainActivity. seenAt != nil {
183+ return false
184+ }
95185
96- // This transaction replaced others - check if any have the same value
97- for replacedTxid in onchain. boostTxIds {
186+ // If this is a replacement transaction with same value as original, skip the sheet
187+ if let boostTxIds = onchainActivity? . boostTxIds, !boostTxIds. isEmpty {
188+ for replacedTxid in boostTxIds {
98189 if let replaced = try ? await getOnchainActivityByTxId ( txid: replacedTxid) ,
99190 replaced. value == value
100191 {
@@ -105,8 +196,6 @@ class ActivityService {
105196 return false
106197 }
107198 }
108- } catch {
109- Logger . error ( " Failed to check existing activities for replacement: \( error) " , context: " CoreService.shouldShowReceivedSheet " )
110199 }
111200
112201 return true
@@ -213,7 +302,7 @@ class ActivityService {
213302
214303 private func processOnchainPayment(
215304 _ payment: PaymentDetails ,
216- transactionDetails: TransactionDetails ? = nil
305+ transactionDetails: BitkitCore . TransactionDetails ? = nil
217306 ) async throws {
218307 guard case let . onchain( txid, _) = payment. kind else { return }
219308
@@ -258,6 +347,7 @@ class ActivityService {
258347 let feeRate = existingOnchain? . feeRate ?? 1
259348 let preservedAddress = existingOnchain? . address ?? " Loading... "
260349 let doesExist = existingOnchain? . doesExist ?? true
350+ let seenAt = existingOnchain? . seenAt
261351
262352 // Check if this transaction is a channel transfer
263353 if channelId == nil || !isTransfer {
@@ -309,7 +399,8 @@ class ActivityService {
309399 channelId: channelId,
310400 transferTxId: transferTxId,
311401 createdAt: UInt64 ( payment. creationTime. timeIntervalSince1970) ,
312- updatedAt: paymentTimestamp
402+ updatedAt: paymentTimestamp,
403+ seenAt: seenAt
313404 )
314405
315406 if existingActivity != nil {
@@ -321,7 +412,7 @@ class ActivityService {
321412
322413 // MARK: - Onchain Event Handlers
323414
324- private func processOnchainTransaction( txid: String , details: TransactionDetails , context: String ) async throws {
415+ private func processOnchainTransaction( txid: String , details: BitkitCore . TransactionDetails , context: String ) async throws {
325416 guard let payments = LightningService . shared. payments else {
326417 Logger . warn ( " No payments available for transaction \( txid) " , context: context)
327418 return
@@ -340,15 +431,21 @@ class ActivityService {
340431 try await processOnchainPayment ( payment, transactionDetails: details)
341432 }
342433
343- func handleOnchainTransactionReceived( txid: String , details: TransactionDetails ) async throws {
434+ func handleOnchainTransactionReceived( txid: String , details: LDKNode . TransactionDetails ) async throws {
435+ let coreDetails = mapToCoreTransactionDetails ( txid: txid, details)
436+
344437 try await ServiceQueue . background ( . core) {
345- try await self . processOnchainTransaction ( txid: txid, details: details, context: " CoreService.handleOnchainTransactionReceived " )
438+ try BitkitCore . upsertTransactionDetails ( detailsList: [ coreDetails] )
439+ try await self . processOnchainTransaction ( txid: txid, details: coreDetails, context: " CoreService.handleOnchainTransactionReceived " )
346440 }
347441 }
348442
349- func handleOnchainTransactionConfirmed( txid: String , details: TransactionDetails ) async throws {
443+ func handleOnchainTransactionConfirmed( txid: String , details: LDKNode . TransactionDetails ) async throws {
444+ let coreDetails = mapToCoreTransactionDetails ( txid: txid, details)
445+
350446 try await ServiceQueue . background ( . core) {
351- try await self . processOnchainTransaction ( txid: txid, details: details, context: " CoreService.handleOnchainTransactionConfirmed " )
447+ try BitkitCore . upsertTransactionDetails ( detailsList: [ coreDetails] )
448+ try await self . processOnchainTransaction ( txid: txid, details: coreDetails, context: " CoreService.handleOnchainTransactionConfirmed " )
352449 }
353450 }
354451
@@ -497,13 +594,11 @@ class ActivityService {
497594
498595 let paymentTimestamp = UInt64 ( payment. latestUpdateTimestamp)
499596 let existingActivity = try getActivityById ( activityId: payment. id)
597+ let existingLightning : LightningActivity ? = if let existingActivity, case let . lightning( ln) = existingActivity { ln } else { nil }
500598
501599 // Skip if existing activity has newer timestamp to avoid overwriting local data
502- if let existingActivity, case let . lightning( existing) = existingActivity {
503- let existingUpdatedAt = existing. updatedAt ?? 0
504- if existingUpdatedAt > paymentTimestamp {
505- return
506- }
600+ if let existingUpdatedAt = existingLightning? . updatedAt, existingUpdatedAt > paymentTimestamp {
601+ return
507602 }
508603
509604 let state : BitkitCore . PaymentState = switch payment. status {
@@ -523,7 +618,8 @@ class ActivityService {
523618 timestamp: paymentTimestamp,
524619 preimage: preimage,
525620 createdAt: paymentTimestamp,
526- updatedAt: paymentTimestamp
621+ updatedAt: paymentTimestamp,
622+ seenAt: existingLightning? . seenAt
527623 )
528624
529625 if existingActivity != nil {
@@ -598,7 +694,8 @@ class ActivityService {
598694
599695 /// Marks replacement transactions (with originalTxId in boostTxIds) as doesExist = false when original confirms
600696 /// Finds the channel ID associated with a transaction based on its direction
601- private func findChannelForTransaction( txid: String , direction: PaymentDirection , transactionDetails: TransactionDetails ? = nil ) async -> String ?
697+ private func findChannelForTransaction( txid: String , direction: PaymentDirection ,
698+ transactionDetails: BitkitCore . TransactionDetails ? = nil ) async -> String ?
602699 {
603700 switch direction {
604701 case . inbound:
@@ -611,13 +708,13 @@ class ActivityService {
611708 }
612709
613710 /// Check if a transaction spends a closed channel's funding UTXO
614- private func findClosedChannelForTransaction( txid: String , transactionDetails: TransactionDetails ? = nil ) async -> String ? {
711+ private func findClosedChannelForTransaction( txid: String , transactionDetails: BitkitCore . TransactionDetails ? = nil ) async -> String ? {
615712 do {
616713 let closedChannels = try await getAllClosedChannels ( sortDirection: . desc)
617714 guard !closedChannels. isEmpty else { return nil }
618715
619- // Use provided transaction details if available, otherwise try node
620- guard let details = transactionDetails ?? LightningService . shared . getTransactionDetails ( txid : txid ) else {
716+ let details = if let provided = transactionDetails { provided } else { await fetchTransactionDetails ( txid : txid ) }
717+ guard let details else {
621718 Logger . warn ( " Transaction details not available for \( txid) " , context: " CoreService.findClosedChannelForTransaction " )
622719 return nil
623720 }
@@ -686,7 +783,7 @@ class ActivityService {
686783 }
687784
688785 /// Check pre-activity metadata for addresses in the transaction
689- private func findAddressInPreActivityMetadata( details: TransactionDetails , value: UInt64 ) async -> String ? {
786+ private func findAddressInPreActivityMetadata( details: BitkitCore . TransactionDetails , value: UInt64 ) async -> String ? {
690787 for output in details. outputs {
691788 guard let address = output. scriptpubkeyAddress else { continue }
692789 if let metadata = try ? await getPreActivityMetadata ( searchKey: address, searchByAddress: true ) ,
@@ -700,9 +797,11 @@ class ActivityService {
700797 }
701798
702799 /// Find the receiving address for an onchain transaction
703- private func findReceivingAddress( for txid: String , value: UInt64 , transactionDetails: TransactionDetails ? = nil ) async throws -> String ? {
704- // Use provided transaction details if available, otherwise try node
705- guard let details = transactionDetails ?? LightningService . shared. getTransactionDetails ( txid: txid) else {
800+ private func findReceivingAddress( for txid: String , value: UInt64 ,
801+ transactionDetails: BitkitCore . TransactionDetails ? = nil ) async throws -> String ?
802+ {
803+ let details = if let provided = transactionDetails { provided } else { await fetchTransactionDetails ( txid: txid) }
804+ guard let details else {
706805 Logger . warn ( " Transaction details not available for \( txid) " , context: " CoreService.findReceivingAddress " )
707806 return nil
708807 }
@@ -1048,7 +1147,8 @@ class ActivityService {
10481147 timestamp: timestamp,
10491148 preimage: template. status == . succeeded ? " preimage \( activityId) " : nil ,
10501149 createdAt: timestamp,
1051- updatedAt: timestamp
1150+ updatedAt: timestamp,
1151+ seenAt: nil
10521152 )
10531153 )
10541154 case . onchain:
@@ -1071,7 +1171,8 @@ class ActivityService {
10711171 channelId: nil ,
10721172 transferTxId: nil ,
10731173 createdAt: timestamp,
1074- updatedAt: timestamp
1174+ updatedAt: timestamp,
1175+ seenAt: nil
10751176 )
10761177 )
10771178 }
0 commit comments