From e7a65f1c5a26557224a440229586b641b27f9a18 Mon Sep 17 00:00:00 2001 From: benk10 Date: Mon, 17 Nov 2025 16:54:37 -0500 Subject: [PATCH 1/5] Mark RBFed txs as removed from mempool --- Bitkit/Services/CoreService.swift | 65 ++++++++++++++----- .../Wallets/Activity/ActivityItemView.swift | 7 +- .../Wallets/Activity/ActivityRowOnchain.swift | 4 ++ 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/Bitkit/Services/CoreService.swift b/Bitkit/Services/CoreService.swift index 4a165f2e..bec5a079 100644 --- a/Bitkit/Services/CoreService.swift +++ b/Bitkit/Services/CoreService.swift @@ -150,6 +150,7 @@ class ActivityService { let preservedTransferTxId = existingOnchain?.transferTxId let preservedFeeRate = existingOnchain?.feeRate ?? 1 let preservedAddress = existingOnchain?.address ?? "Loading..." + let preservedDoesExist = existingOnchain?.doesExist ?? true // Check if this transaction is a channel transfer (open or close) if preservedChannelId == nil || !preservedIsTransfer { @@ -211,6 +212,9 @@ class ActivityService { let finalChannelId = preservedChannelId let finalTransferTxId = preservedTransferTxId + // If confirmed, set doesExist to true; otherwise preserve existing value + let finalDoesExist = isConfirmed ? true : preservedDoesExist + let onchain = OnchainActivity( id: payment.id, txType: payment.direction == .outbound ? .sent : .received, @@ -224,7 +228,7 @@ class ActivityService { isBoosted: shouldMarkAsBoosted, // Mark as boosted if it's a replacement transaction boostTxIds: boostTxIds, isTransfer: finalIsTransfer, - doesExist: true, + doesExist: finalDoesExist, confirmTimestamp: confirmedTimestamp, channelId: finalChannelId, transferTxId: finalTransferTxId, @@ -241,6 +245,11 @@ class ActivityService { print(payment) addedCount += 1 } + + // If a removed transaction confirms, mark its replacement transactions as removed + if !preservedDoesExist && isConfirmed { + try await self.markReplacementTransactionsAsRemoved(originalTxId: txid) + } } else if case let .bolt11(hash, preimage, secret, description, bolt11) = payment.kind { // Skip pending inbound payments, just means they created an invoice guard !(payment.status == .pending && payment.direction == .inbound) else { continue } @@ -298,6 +307,35 @@ class ActivityService { } } + /// Marks replacement transactions (with originalTxId in boostTxIds) as doesExist = false when original confirms + private func markReplacementTransactionsAsRemoved(originalTxId: String) async throws { + let allActivities = try getActivities( + filter: .onchain, + txType: nil, + tags: nil, + search: nil, + minDate: nil, + maxDate: nil, + limit: nil, + sortDirection: nil + ) + + for activity in allActivities { + guard case let .onchain(onchainActivity) = activity else { continue } + + if onchainActivity.boostTxIds.contains(originalTxId) && onchainActivity.doesExist { + Logger.info( + "Marking replacement transaction \(onchainActivity.txId) as doesExist = false (original \(originalTxId) confirmed)", + context: "CoreService.markReplacementTransactionsAsRemoved" + ) + + var updatedActivity = onchainActivity + updatedActivity.doesExist = false + try updateActivity(activityId: onchainActivity.id, activity: .onchain(updatedActivity)) + } + } + } + /// Finds the channel ID associated with a transaction based on its direction private func findChannelForTransaction(txid: String, direction: PaymentDirection) async -> String? { switch direction { @@ -693,25 +731,18 @@ class ActivityService { "Added original transaction \(onchainActivity.txId) to replaced transactions list", context: "CoreService.boostOnchainTransaction" ) - // For RBF, delete the original activity since it's been replaced - // The new transaction will be synced automatically from LDK + // For RBF, mark the original activity as doesExist = false instead of deleting it + // This allows it to be displayed with the "removed" status Logger.debug( - "Attempting to delete original activity \(activityId) before RBF replacement", context: "CoreService.boostOnchainTransaction" + "Marking original activity \(activityId) as doesExist = false (replaced by RBF)", context: "CoreService.boostOnchainTransaction" ) - // Use the proper delete function that returns a Bool - let deleteResult = try deleteActivityById(activityId: activityId) - Logger.info("Delete result for original activity \(activityId): \(deleteResult)", context: "CoreService.boostOnchainTransaction") - - // Double-check that the activity was deleted - let checkActivity = try getActivityById(activityId: activityId) - if checkActivity == nil { - Logger.info("Confirmed: Original activity \(activityId) was successfully deleted", context: "CoreService.boostOnchainTransaction") - } else { - Logger.error( - "Warning: Original activity \(activityId) still exists after deletion attempt", context: "CoreService.boostOnchainTransaction" - ) - } + onchainActivity.doesExist = false + try updateActivity(activityId: activityId, activity: .onchain(onchainActivity)) + Logger.info( + "Successfully marked activity \(activityId) as doesExist = false (replaced by RBF)", + context: "CoreService.boostOnchainTransaction" + ) self.activitiesChangedSubject.send() } diff --git a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift index cf3bcafa..983e739c 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift @@ -225,7 +225,12 @@ struct ActivityItemView: View { BodySSBText(t("wallet__activity_failed"), textColor: .purpleAccent) } case let .onchain(activity): - if activity.confirmed == true { + if !activity.doesExist { + Image("x-circle") + .foregroundColor(.textSecondary) + .frame(width: 16, height: 16) + BodySSBText(t("wallet__activity_removed_title"), textColor: .textSecondary) + } else if activity.confirmed == true { Image("check-circle") .foregroundColor(.greenAccent) .frame(width: 16, height: 16) diff --git a/Bitkit/Views/Wallets/Activity/ActivityRowOnchain.swift b/Bitkit/Views/Wallets/Activity/ActivityRowOnchain.swift index 62f9dcf4..b09c8027 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityRowOnchain.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityRowOnchain.swift @@ -49,6 +49,10 @@ struct ActivityRowOnchain: View { } private var description: String { + if !item.doesExist { + return t("wallet__activity_removed") + } + if item.isTransfer { switch item.txType { case .sent: From 2e87903cd88a3d23a00425203baad2dfc267c0e0 Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 18 Nov 2025 07:36:53 -0500 Subject: [PATCH 2/5] Add status removed to accessibility ID --- Bitkit/Views/Wallets/Activity/ActivityItemView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift index 983e739c..e4edcb47 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift @@ -134,6 +134,9 @@ struct ActivityItemView: View { private var statusAccessibilityIdentifier: String? { switch viewModel.activity { case let .onchain(activity): + if !activity.doesExist { + return "StatusRemoved" + } if activity.confirmed == true { return "StatusConfirmed" } From a7e0ea72606172ed1c2ea592bc888d98cd9acc12 Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 18 Nov 2025 12:53:46 -0500 Subject: [PATCH 3/5] Fix status text and boost icon on original activity --- Bitkit/Services/CoreService.swift | 7 +++++++ Bitkit/Views/Wallets/Activity/ActivityItemView.swift | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Bitkit/Services/CoreService.swift b/Bitkit/Services/CoreService.swift index bec5a079..228df375 100644 --- a/Bitkit/Services/CoreService.swift +++ b/Bitkit/Services/CoreService.swift @@ -731,6 +731,13 @@ class ActivityService { "Added original transaction \(onchainActivity.txId) to replaced transactions list", context: "CoreService.boostOnchainTransaction" ) + // For RBF, mark the old activity as boosted before marking it as replaced + onchainActivity.isBoosted = true + Logger.debug( + "Marked original activity \(activityId) as boosted before RBF replacement", + context: "CoreService.boostOnchainTransaction" + ) + // For RBF, mark the original activity as doesExist = false instead of deleting it // This allows it to be displayed with the "removed" status Logger.debug( diff --git a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift index e4edcb47..53a9440c 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift @@ -232,7 +232,7 @@ struct ActivityItemView: View { Image("x-circle") .foregroundColor(.textSecondary) .frame(width: 16, height: 16) - BodySSBText(t("wallet__activity_removed_title"), textColor: .textSecondary) + BodySSBText(t("wallet__activity_removed"), textColor: .textSecondary) } else if activity.confirmed == true { Image("check-circle") .foregroundColor(.greenAccent) From d99335546c597decaefaf0a949d2b945bbb85cac Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 18 Nov 2025 13:00:58 -0500 Subject: [PATCH 4/5] Fix status text color and icon --- Bitkit/Views/Wallets/Activity/ActivityItemView.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift index 53a9440c..af6551c6 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityItemView.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityItemView.swift @@ -229,10 +229,12 @@ struct ActivityItemView: View { } case let .onchain(activity): if !activity.doesExist { - Image("x-circle") - .foregroundColor(.textSecondary) + Image("x-mark") + .resizable() + .scaledToFit() + .foregroundColor(.redAccent) .frame(width: 16, height: 16) - BodySSBText(t("wallet__activity_removed"), textColor: .textSecondary) + BodySSBText(t("wallet__activity_removed"), textColor: .redAccent) } else if activity.confirmed == true { Image("check-circle") .foregroundColor(.greenAccent) From 01aed7adf2756847dc82b4e6f84b79c620ba1cc1 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 19 Nov 2025 12:54:53 -0500 Subject: [PATCH 5/5] Update icon for removed tx --- Bitkit/Views/Wallets/Activity/ActivityIcon.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Bitkit/Views/Wallets/Activity/ActivityIcon.swift b/Bitkit/Views/Wallets/Activity/ActivityIcon.swift index d1c590d9..75a0620a 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityIcon.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityIcon.swift @@ -9,6 +9,7 @@ struct ActivityIcon: View { let size: CGFloat let isBoosted: Bool let isTransfer: Bool + let doesExist: Bool init(activity: Activity, size: CGFloat = 32) { self.size = size @@ -20,6 +21,7 @@ struct ActivityIcon: View { txType = ln.txType isBoosted = false isTransfer = false + doesExist = true case let .onchain(onchain): isLightning = false status = nil @@ -27,6 +29,7 @@ struct ActivityIcon: View { txType = onchain.txType isBoosted = onchain.isBoosted isTransfer = onchain.isTransfer + doesExist = onchain.doesExist } } @@ -55,6 +58,13 @@ struct ActivityIcon: View { size: size ) } + } else if !doesExist { + CircularIcon( + icon: "x-mark", + iconColor: .redAccent, + backgroundColor: .red16, + size: size + ) } else if isBoosted && !(confirmed ?? false) { CircularIcon( icon: "timer-alt",