Skip to content

Commit 574a8f9

Browse files
committed
Filter RBF txs
1 parent 2ce3f03 commit 574a8f9

File tree

2 files changed

+94
-41
lines changed

2 files changed

+94
-41
lines changed

Bitkit/Services/CoreService.swift

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,42 @@ class ActivityService {
2525
/// Maximum address index to search when current address exists
2626
private static let maxAddressSearchIndex: UInt32 = 100_000
2727

28+
// MARK: - BoostTxIds Cache
29+
30+
// Cached set of transaction IDs that appear in boostTxIds (for filtering replaced transactions)
31+
private var cachedTxIdsInBoostTxIds: Set<String> = []
32+
33+
/// Get the set of transaction IDs that appear in boostTxIds (cached for performance)
34+
func getTxIdsInBoostTxIds() async -> Set<String> {
35+
if cachedTxIdsInBoostTxIds.isEmpty {
36+
await refreshBoostTxIdsCache()
37+
}
38+
return cachedTxIdsInBoostTxIds
39+
}
40+
41+
private func updateBoostTxIdsCache(for activity: Activity) {
42+
if case let .onchain(onchain) = activity {
43+
cachedTxIdsInBoostTxIds.formUnion(onchain.boostTxIds)
44+
}
45+
}
46+
47+
private func refreshBoostTxIdsCache() async {
48+
do {
49+
let allOnchainActivities = try await get(filter: .onchain)
50+
var txIds: Set<String> = []
51+
for activity in allOnchainActivities {
52+
if case let .onchain(onchain) = activity {
53+
txIds.formUnion(onchain.boostTxIds)
54+
}
55+
}
56+
await MainActor.run {
57+
self.cachedTxIdsInBoostTxIds = txIds
58+
}
59+
} catch {
60+
Logger.error("Failed to refresh boostTxIds cache: \(error)", context: "ActivityService")
61+
}
62+
}
63+
2864
// MARK: - Transaction Status Checks
2965

3066
func wasTransactionReplaced(txid: String) async -> Bool {
@@ -125,20 +161,24 @@ class ActivityService {
125161
_ = try deleteActivityById(activityId: id)
126162
}
127163

164+
// Clear cache since all activities are deleted
165+
self.cachedTxIdsInBoostTxIds.removeAll()
128166
self.activitiesChangedSubject.send()
129167
}
130168
}
131169

132170
func insert(_ activity: Activity) async throws {
133171
try await ServiceQueue.background(.core) {
134172
try insertActivity(activity: activity)
173+
self.updateBoostTxIdsCache(for: activity)
135174
self.activitiesChangedSubject.send()
136175
}
137176
}
138177

139178
func upsertList(_ activities: [Activity]) async throws {
140179
try await ServiceQueue.background(.core) {
141180
try upsertActivities(activities: activities)
181+
await self.refreshBoostTxIdsCache()
142182
}
143183
}
144184

@@ -308,10 +348,7 @@ class ActivityService {
308348
// Find the activity for the replaced transaction
309349
let replacedActivity = try await self.getOnchainActivityByTxId(txid: txid)
310350

311-
let replacedTags: [String]
312351
if var existing = replacedActivity {
313-
replacedTags = await (try? self.tags(forActivity: existing.id)) ?? []
314-
315352
Logger.info(
316353
"Transaction \(txid) replaced by \(conflicts.count) conflict(s): \(conflicts.joined(separator: ", "))",
317354
context: "CoreService.handleOnchainTransactionReplaced"
@@ -323,9 +360,8 @@ class ActivityService {
323360
try await self.update(id: existing.id, activity: .onchain(existing))
324361
Logger.info("Marked transaction \(txid) as replaced", context: "CoreService.handleOnchainTransactionReplaced")
325362
} else {
326-
replacedTags = []
327363
Logger.info(
328-
"Activity not found for replaced transaction \(txid) - was deleted by initiated RBF, tags in pre-activity metadata",
364+
"Activity not found for replaced transaction \(txid) - will be created when transaction is processed",
329365
context: "CoreService.handleOnchainTransactionReplaced"
330366
)
331367
}
@@ -369,13 +405,16 @@ class ActivityService {
369405
activity.updatedAt = UInt64(Date().timeIntervalSince1970)
370406
try await self.update(id: activity.id, activity: .onchain(activity))
371407

372-
// Apply tags from the replaced transaction
373-
if !replacedTags.isEmpty {
408+
// Move tags from the replaced transaction
409+
if let replacedActivity {
374410
do {
375-
try await self.appendTags(toActivity: activity.id, replacedTags)
411+
let replacedTags = try await self.tags(forActivity: replacedActivity.id)
412+
if !replacedTags.isEmpty {
413+
try await self.appendTags(toActivity: activity.id, replacedTags)
414+
}
376415
} catch {
377416
Logger.error(
378-
"Failed to apply tags from replaced transaction \(txid) to replacement transaction \(conflictTxid): \(error)",
417+
"Failed to copy tags from replaced transaction \(txid) to replacement transaction \(conflictTxid): \(error)",
379418
context: "CoreService.handleOnchainTransactionReplaced"
380419
)
381420
}
@@ -792,19 +831,27 @@ class ActivityService {
792831
func update(id: String, activity: Activity) async throws {
793832
try await ServiceQueue.background(.core) {
794833
try updateActivity(activityId: id, activity: activity)
834+
self.updateBoostTxIdsCache(for: activity)
795835
self.activitiesChangedSubject.send()
796836
}
797837
}
798838

799839
func upsert(_ activity: Activity) async throws {
800840
try await ServiceQueue.background(.core) {
801841
try upsertActivity(activity: activity)
842+
self.updateBoostTxIdsCache(for: activity)
802843
self.activitiesChangedSubject.send()
803844
}
804845
}
805846

806847
func delete(id: String) async throws -> Bool {
807848
try await ServiceQueue.background(.core) {
849+
// Rebuild cache if deleting an onchain activity with boostTxIds
850+
let activity = try? getActivityById(activityId: id)
851+
if let activity, case let .onchain(onchain) = activity, !onchain.boostTxIds.isEmpty {
852+
await self.refreshBoostTxIdsCache()
853+
}
854+
808855
let result = try deleteActivityById(activityId: id)
809856
self.activitiesChangedSubject.send()
810857
return result
@@ -951,35 +998,12 @@ class ActivityService {
951998

952999
Logger.info("RBF transaction created successfully: \(txid)", context: "CoreService.boostOnchainTransaction")
9531000

954-
// Get tags from the old activity before deleting it
955-
let oldTags = await (try? self.tags(forActivity: activityId)) ?? []
956-
957-
// Create pre-activity metadata for the replacement transaction with tags from the old activity
958-
if !oldTags.isEmpty {
959-
let currentTime = UInt64(Date().timeIntervalSince1970)
960-
let preActivityMetadata = BitkitCore.PreActivityMetadata(
961-
paymentId: txid,
962-
tags: oldTags,
963-
paymentHash: nil,
964-
txId: txid,
965-
address: onchainActivity.address,
966-
isReceive: false,
967-
feeRate: UInt64(feeRate),
968-
isTransfer: onchainActivity.isTransfer,
969-
channelId: onchainActivity.channelId,
970-
createdAt: currentTime
971-
)
972-
try? await self.addPreActivityMetadata(preActivityMetadata)
973-
Logger.info(
974-
"Created pre-activity metadata with \(oldTags.count) tag(s) for RBF replacement transaction \(txid)",
975-
context: "CoreService.boostOnchainTransaction"
976-
)
977-
}
978-
979-
// For RBF we initiated, delete the old activity
980-
_ = try await self.delete(id: activityId)
1001+
// For RBF, mark the original activity as doesExist = false instead of deleting it
1002+
// This allows it to be displayed with the "removed" status
1003+
onchainActivity.doesExist = false
1004+
try await self.update(id: activityId, activity: .onchain(onchainActivity))
9811005
Logger.info(
982-
"Successfully deleted activity \(activityId) (replaced by RBF transaction \(txid))",
1006+
"Successfully marked activity \(activityId) as doesExist = false (replaced by RBF)",
9831007
context: "CoreService.boostOnchainTransaction"
9841008
)
9851009
}

Bitkit/ViewModels/ActivityListViewModel.swift

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,18 @@ class ActivityListViewModel: ObservableObject {
144144
do {
145145
// Get latest activities first as that's displayed on the home view
146146
let limitLatest: UInt32 = 3
147-
latestActivities = try await coreService.activity.get(filter: .all, limit: limitLatest)
147+
// Fetch extra to account for potential filtering of replaced transactions
148+
let latest = try await coreService.activity.get(filter: .all, limit: limitLatest * 3)
149+
let filtered = await filterOutReplacedSentTransactions(latest)
150+
latestActivities = Array(filtered.prefix(Int(limitLatest)))
148151

149152
// Fetch all activities
150153
await updateFilteredActivities()
151-
lightningActivities = try await coreService.activity.get(filter: .lightning)
152-
onchainActivities = try await coreService.activity.get(filter: .onchain)
154+
155+
let lightningActivities = try await coreService.activity.get(filter: .lightning)
156+
157+
let onchain = try await coreService.activity.get(filter: .onchain)
158+
onchainActivities = await filterOutReplacedSentTransactions(onchain)
153159

154160
// Update available tags and fee estimates
155161
await updateAvailableTags()
@@ -198,8 +204,11 @@ class ActivityListViewModel: ObservableObject {
198204
maxDate: maxDate
199205
)
200206

207+
// Filter out replaced sent transactions that appear in another transaction's boostTxIds
208+
let filteredOutReplaced = await filterOutReplacedSentTransactions(baseFilteredActivities)
209+
201210
// Apply tab filtering
202-
filteredActivities = filterActivitiesByTab(baseFilteredActivities, selectedTab: selectedTab)
211+
filteredActivities = filterActivitiesByTab(filteredOutReplaced, selectedTab: selectedTab)
203212

204213
// Update grouped activities
205214
updateGroupedActivities()
@@ -449,6 +458,26 @@ extension ActivityListViewModel {
449458
}
450459
}
451460

461+
/// Filter out replaced sent transactions that appear in another transaction's boostTxIds
462+
private func filterOutReplacedSentTransactions(_ activities: [Activity]) async -> [Activity] {
463+
// Get cached set of txIds that appear in boostTxIds
464+
let txIdsInBoostTxIds = await coreService.activity.getTxIdsInBoostTxIds()
465+
466+
// Filter out activities that:
467+
// 1. Are onchain
468+
// 2. Have doesExist = false
469+
// 3. Are sent transactions
470+
// 4. Appear in another transaction's boostTxIds
471+
return activities.filter { activity in
472+
if case let .onchain(onchain) = activity {
473+
if !onchain.doesExist && onchain.txType == .sent && txIdsInBoostTxIds.contains(onchain.txId) {
474+
return false
475+
}
476+
}
477+
return true
478+
}
479+
}
480+
452481
/// Filter activities based on the selected tab
453482
private func filterActivitiesByTab(_ activities: [Activity], selectedTab: ActivityTab) -> [Activity] {
454483
switch selectedTab {

0 commit comments

Comments
 (0)