Skip to content

Commit 9a620b4

Browse files
authored
Merge pull request synonymdev#230 from synonymdev/feat/rbf-removed-mempool
Mark RBFed txs as removed from mempool
2 parents 44c695b + 7953eb9 commit 9a620b4

File tree

4 files changed

+80
-18
lines changed

4 files changed

+80
-18
lines changed

Bitkit/Services/CoreService.swift

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ class ActivityService {
150150
let preservedTransferTxId = existingOnchain?.transferTxId
151151
let preservedFeeRate = existingOnchain?.feeRate ?? 1
152152
let preservedAddress = existingOnchain?.address ?? "Loading..."
153+
let preservedDoesExist = existingOnchain?.doesExist ?? true
153154

154155
// Check if this transaction is a channel transfer (open or close)
155156
if preservedChannelId == nil || !preservedIsTransfer {
@@ -211,6 +212,9 @@ class ActivityService {
211212
let finalChannelId = preservedChannelId
212213
let finalTransferTxId = preservedTransferTxId
213214

215+
// If confirmed, set doesExist to true; otherwise preserve existing value
216+
let finalDoesExist = isConfirmed ? true : preservedDoesExist
217+
214218
let onchain = OnchainActivity(
215219
id: payment.id,
216220
txType: payment.direction == .outbound ? .sent : .received,
@@ -224,7 +228,7 @@ class ActivityService {
224228
isBoosted: shouldMarkAsBoosted, // Mark as boosted if it's a replacement transaction
225229
boostTxIds: boostTxIds,
226230
isTransfer: finalIsTransfer,
227-
doesExist: true,
231+
doesExist: finalDoesExist,
228232
confirmTimestamp: confirmedTimestamp,
229233
channelId: finalChannelId,
230234
transferTxId: finalTransferTxId,
@@ -241,6 +245,11 @@ class ActivityService {
241245
print(payment)
242246
addedCount += 1
243247
}
248+
249+
// If a removed transaction confirms, mark its replacement transactions as removed
250+
if !preservedDoesExist && isConfirmed {
251+
try await self.markReplacementTransactionsAsRemoved(originalTxId: txid)
252+
}
244253
} else if case let .bolt11(hash, preimage, secret, description, bolt11) = payment.kind {
245254
// Skip pending inbound payments, just means they created an invoice
246255
guard !(payment.status == .pending && payment.direction == .inbound) else { continue }
@@ -298,6 +307,35 @@ class ActivityService {
298307
}
299308
}
300309

310+
/// Marks replacement transactions (with originalTxId in boostTxIds) as doesExist = false when original confirms
311+
private func markReplacementTransactionsAsRemoved(originalTxId: String) async throws {
312+
let allActivities = try getActivities(
313+
filter: .onchain,
314+
txType: nil,
315+
tags: nil,
316+
search: nil,
317+
minDate: nil,
318+
maxDate: nil,
319+
limit: nil,
320+
sortDirection: nil
321+
)
322+
323+
for activity in allActivities {
324+
guard case let .onchain(onchainActivity) = activity else { continue }
325+
326+
if onchainActivity.boostTxIds.contains(originalTxId) && onchainActivity.doesExist {
327+
Logger.info(
328+
"Marking replacement transaction \(onchainActivity.txId) as doesExist = false (original \(originalTxId) confirmed)",
329+
context: "CoreService.markReplacementTransactionsAsRemoved"
330+
)
331+
332+
var updatedActivity = onchainActivity
333+
updatedActivity.doesExist = false
334+
try updateActivity(activityId: onchainActivity.id, activity: .onchain(updatedActivity))
335+
}
336+
}
337+
}
338+
301339
/// Finds the channel ID associated with a transaction based on its direction
302340
private func findChannelForTransaction(txid: String, direction: PaymentDirection) async -> String? {
303341
switch direction {
@@ -693,25 +731,25 @@ class ActivityService {
693731
"Added original transaction \(onchainActivity.txId) to replaced transactions list", context: "CoreService.boostOnchainTransaction"
694732
)
695733

696-
// For RBF, delete the original activity since it's been replaced
697-
// The new transaction will be synced automatically from LDK
734+
// For RBF, mark the old activity as boosted before marking it as replaced
735+
onchainActivity.isBoosted = true
698736
Logger.debug(
699-
"Attempting to delete original activity \(activityId) before RBF replacement", context: "CoreService.boostOnchainTransaction"
737+
"Marked original activity \(activityId) as boosted before RBF replacement",
738+
context: "CoreService.boostOnchainTransaction"
700739
)
701740

702-
// Use the proper delete function that returns a Bool
703-
let deleteResult = try deleteActivityById(activityId: activityId)
704-
Logger.info("Delete result for original activity \(activityId): \(deleteResult)", context: "CoreService.boostOnchainTransaction")
705-
706-
// Double-check that the activity was deleted
707-
let checkActivity = try getActivityById(activityId: activityId)
708-
if checkActivity == nil {
709-
Logger.info("Confirmed: Original activity \(activityId) was successfully deleted", context: "CoreService.boostOnchainTransaction")
710-
} else {
711-
Logger.error(
712-
"Warning: Original activity \(activityId) still exists after deletion attempt", context: "CoreService.boostOnchainTransaction"
713-
)
714-
}
741+
// For RBF, mark the original activity as doesExist = false instead of deleting it
742+
// This allows it to be displayed with the "removed" status
743+
Logger.debug(
744+
"Marking original activity \(activityId) as doesExist = false (replaced by RBF)", context: "CoreService.boostOnchainTransaction"
745+
)
746+
747+
onchainActivity.doesExist = false
748+
try updateActivity(activityId: activityId, activity: .onchain(onchainActivity))
749+
Logger.info(
750+
"Successfully marked activity \(activityId) as doesExist = false (replaced by RBF)",
751+
context: "CoreService.boostOnchainTransaction"
752+
)
715753

716754
self.activitiesChangedSubject.send()
717755
}

Bitkit/Views/Wallets/Activity/ActivityIcon.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct ActivityIcon: View {
99
let size: CGFloat
1010
let isBoosted: Bool
1111
let isTransfer: Bool
12+
let doesExist: Bool
1213

1314
init(activity: Activity, size: CGFloat = 32) {
1415
self.size = size
@@ -20,13 +21,15 @@ struct ActivityIcon: View {
2021
txType = ln.txType
2122
isBoosted = false
2223
isTransfer = false
24+
doesExist = true
2325
case let .onchain(onchain):
2426
isLightning = false
2527
status = nil
2628
confirmed = onchain.confirmed
2729
txType = onchain.txType
2830
isBoosted = onchain.isBoosted
2931
isTransfer = onchain.isTransfer
32+
doesExist = onchain.doesExist
3033
}
3134
}
3235

@@ -55,6 +58,13 @@ struct ActivityIcon: View {
5558
size: size
5659
)
5760
}
61+
} else if !doesExist {
62+
CircularIcon(
63+
icon: "x-mark",
64+
iconColor: .redAccent,
65+
backgroundColor: .red16,
66+
size: size
67+
)
5868
} else if isBoosted && !(confirmed ?? false) {
5969
CircularIcon(
6070
icon: "timer-alt",

Bitkit/Views/Wallets/Activity/ActivityItemView.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ struct ActivityItemView: View {
134134
private var statusAccessibilityIdentifier: String? {
135135
switch viewModel.activity {
136136
case let .onchain(activity):
137+
if !activity.doesExist {
138+
return "StatusRemoved"
139+
}
137140
if activity.confirmed == true {
138141
return "StatusConfirmed"
139142
}
@@ -225,7 +228,14 @@ struct ActivityItemView: View {
225228
BodySSBText(t("wallet__activity_failed"), textColor: .purpleAccent)
226229
}
227230
case let .onchain(activity):
228-
if activity.confirmed == true {
231+
if !activity.doesExist {
232+
Image("x-mark")
233+
.resizable()
234+
.scaledToFit()
235+
.foregroundColor(.redAccent)
236+
.frame(width: 16, height: 16)
237+
BodySSBText(t("wallet__activity_removed"), textColor: .redAccent)
238+
} else if activity.confirmed == true {
229239
Image("check-circle")
230240
.foregroundColor(.greenAccent)
231241
.frame(width: 16, height: 16)

Bitkit/Views/Wallets/Activity/ActivityRowOnchain.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ struct ActivityRowOnchain: View {
4949
}
5050

5151
private var description: String {
52+
if !item.doesExist {
53+
return t("wallet__activity_removed")
54+
}
55+
5256
if item.isTransfer {
5357
switch item.txType {
5458
case .sent:

0 commit comments

Comments
 (0)