diff --git a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 16899355..27588237 100644 --- a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/synonymdev/bitkit-core", "state" : { "branch" : "master", - "revision" : "b15f99707dd874225f52a618ae77fb7550f233c6" + "revision" : "1a714203e9780d0d5c53a2e8fccd1e6a5b05716c" } }, { diff --git a/Bitkit/Services/CoreService.swift b/Bitkit/Services/CoreService.swift index 5b43bea8..db90c40b 100644 --- a/Bitkit/Services/CoreService.swift +++ b/Bitkit/Services/CoreService.swift @@ -7,8 +7,8 @@ import LDKNode class ActivityService { private let coreService: CoreService - // Track replacement transactions (RBF) to mark them as boosted - private static var replacementTransactions: Set = [] + // Track replacement transactions (RBF): newTxId -> parent/original txIds + private static var replacementTransactions: [String: [String]] = [:] // Track replaced transactions that should be ignored during sync private static var replacedTransactions: Set = [] @@ -83,23 +83,36 @@ class ActivityService { confirmedTimestamp = timestamp } - // Get existing activity to preserve certain flags like isBoosted + // Get existing activity to preserve certain flags like isBoosted and boostTxIds let existingActivity = try getActivityById(activityId: payment.id) - let preservedIsBoosted = - if case let .onchain(existing) = existingActivity { - existing.isBoosted - } else { - false + let existingOnchain: OnchainActivity? = { + if let existingActivity, case let .onchain(existing) = existingActivity { + return existing } + return nil + }() + let preservedIsBoosted = existingOnchain?.isBoosted ?? false + let preservedBoostTxIds = existingOnchain?.boostTxIds ?? [] // Check if this is a replacement transaction (RBF) that should be marked as boosted - let isReplacementTransaction = ActivityService.replacementTransactions.contains(txid) + let isReplacementTransaction = ActivityService.replacementTransactions.keys.contains(txid) let shouldMarkAsBoosted = preservedIsBoosted || isReplacementTransaction + // Capture tracked parents for replacement transactions (RBF) before removing from tracking + let trackedParents: [String] = { + if isReplacementTransaction { + return ActivityService.replacementTransactions[txid] ?? [] + } + return [] + }() + + // Use tracked parents when this is a replacement; otherwise keep preserved + let boostTxIds = isReplacementTransaction ? trackedParents : preservedBoostTxIds + if isReplacementTransaction { Logger.debug("Found replacement transaction \(txid), marking as boosted", context: "CoreService.syncLdkNodePayments") - // Remove from tracking set since we've processed it - ActivityService.replacementTransactions.remove(txid) + // Remove from tracking map since we've processed it + ActivityService.replacementTransactions.removeValue(forKey: txid) // Also clean up any old replaced transactions that might be lingering // This helps prevent the replacedTransactions set from growing indefinitely @@ -125,6 +138,7 @@ class ActivityService { confirmed: isConfirmed, timestamp: timestamp, isBoosted: shouldMarkAsBoosted, // Mark as boosted if it's a replacement transaction + boostTxIds: boostTxIds, isTransfer: false, // TODO: handle when paying for order doesExist: true, confirmTimestamp: confirmedTimestamp, @@ -282,6 +296,7 @@ class ActivityService { // For CPFP, mark the original activity as boosted (parent transaction still exists) onchainActivity.isBoosted = true + onchainActivity.boostTxIds.append(txid) try updateActivity(activityId: activityId, activity: .onchain(onchainActivity)) Logger.info("Successfully marked activity \(activityId) as boosted via CPFP", context: "CoreService.boostOnchainTransaction") } else { @@ -296,9 +311,11 @@ class ActivityService { Logger.info("RBF transaction created successfully: \(txid)", context: "CoreService.boostOnchainTransaction") - // Track the replacement transaction so we can mark it as boosted when it syncs - ActivityService.replacementTransactions.insert(txid) - Logger.debug("Added replacement transaction \(txid) to tracking list", context: "CoreService.boostOnchainTransaction") + // Track the replacement transaction with its full parent chain + // Include existing boostTxIds (from previous boosts) plus the current txId being replaced + let boostedParentsTxIds = onchainActivity.boostTxIds + [onchainActivity.txId] + ActivityService.replacementTransactions[txid] = boostedParentsTxIds + Logger.debug("Added replacement transaction \(txid) to tracking list with boosted parents txids: \(boostedParentsTxIds)", context: "CoreService.boostOnchainTransaction") // Track the original transaction ID so we can ignore it during sync ActivityService.replacedTransactions.insert(onchainActivity.txId) @@ -374,6 +391,7 @@ class ActivityService { confirmed: template.confirmed ?? false, timestamp: timestamp, isBoosted: template.isBoosted ?? false, + boostTxIds: template.boostTxIds, isTransfer: template.isTransfer ?? false, doesExist: true, confirmTimestamp: template.confirmed == true ? timestamp + 3600 : nil, @@ -418,6 +436,7 @@ private struct ActivityTemplate { let tags: [String] let confirmed: Bool? let isBoosted: Bool? + let boostTxIds: [String] let isTransfer: Bool? init( @@ -429,7 +448,8 @@ private struct ActivityTemplate { tags: [String] = [], confirmed: Bool? = nil, isBoosted: Bool? = nil, - isTransfer: Bool? = nil + isTransfer: Bool? = nil, + boostTxIds: [String] = [] ) { self.type = type self.txType = txType @@ -440,6 +460,7 @@ private struct ActivityTemplate { self.confirmed = confirmed self.isBoosted = isBoosted self.isTransfer = isTransfer + self.boostTxIds = boostTxIds } } diff --git a/Bitkit/Views/Wallets/Activity/ActivityExplorerView.swift b/Bitkit/Views/Wallets/Activity/ActivityExplorerView.swift index 8f484e06..1c7fc1cf 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityExplorerView.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityExplorerView.swift @@ -143,8 +143,20 @@ struct ActivityExplorerView: View { .padding(.bottom, 16) } - Divider() - .padding(.bottom, 16) + if !onchain.boostTxIds.isEmpty { + Divider() + .padding(.bottom, 16) + ForEach(Array(onchain.boostTxIds.enumerated()), id: \.offset) { index, boostTxId in + let key = onchain.txType == .received + ? "wallet__activity_boosted_cpfp" + : "wallet__activity_boosted_rbf" + + InfoSection( + title: t(key, variables: ["num": String(index + 1)]), + content: boostTxId + ) + } + } } else if let lightning { if let preimage = lightning.preimage { InfoSection( @@ -225,6 +237,7 @@ struct ActivityExplorer_Previews: PreviewProvider { confirmed: true, timestamp: UInt64(Date().timeIntervalSince1970), isBoosted: false, + boostTxIds: [], isTransfer: false, doesExist: true, confirmTimestamp: nil, @@ -237,6 +250,8 @@ struct ActivityExplorer_Previews: PreviewProvider { ) .previewDisplayName("Onchain Payment") } + .environmentObject(AppViewModel()) + .environmentObject(SettingsViewModel()) .environmentObject(CurrencyViewModel()) .preferredColorScheme(.dark) } diff --git a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift index f691a586..fa17a1ea 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift @@ -454,6 +454,7 @@ struct ActivityItemView_Previews: PreviewProvider { confirmed: true, timestamp: UInt64(Date().timeIntervalSince1970), isBoosted: false, + boostTxIds: [], isTransfer: false, doesExist: true, confirmTimestamp: nil, diff --git a/Bitkit/Views/Wallets/Sheets/BoostSheet.swift b/Bitkit/Views/Wallets/Sheets/BoostSheet.swift index 54b9acc8..40558675 100644 --- a/Bitkit/Views/Wallets/Sheets/BoostSheet.swift +++ b/Bitkit/Views/Wallets/Sheets/BoostSheet.swift @@ -431,6 +431,7 @@ struct BoostSheet: View { confirmed: false, timestamp: UInt64(Date().timeIntervalSince1970), isBoosted: false, + boostTxIds: [], isTransfer: false, doesExist: true, confirmTimestamp: nil, diff --git a/BitkitTests/ActivityListTest.swift b/BitkitTests/ActivityListTest.swift index c6560dd4..2b30ae0a 100644 --- a/BitkitTests/ActivityListTest.swift +++ b/BitkitTests/ActivityListTest.swift @@ -84,6 +84,7 @@ final class ActivityTests: XCTestCase { confirmed: true, timestamp: timestamp, isBoosted: false, + boostTxIds: [], isTransfer: false, doesExist: true, confirmTimestamp: nil, @@ -235,6 +236,7 @@ final class ActivityTests: XCTestCase { confirmed: true, timestamp: timestamp, isBoosted: false, + boostTxIds: [], isTransfer: false, doesExist: true, confirmTimestamp: nil, @@ -401,6 +403,7 @@ final class ActivityTests: XCTestCase { confirmed: true, timestamp: timestamp, isBoosted: false, + boostTxIds: [], isTransfer: false, doesExist: true, confirmTimestamp: nil,