Skip to content

Commit 2696805

Browse files
committed
Fix boosted tx restore and migration from RN
1 parent 52b41d2 commit 2696805

File tree

4 files changed

+186
-56
lines changed

4 files changed

+186
-56
lines changed

Bitkit/Services/CoreService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ class ActivityService {
268268
else {
269269
return false
270270
}
271-
return activity.doesExist
271+
return activity.doesExist && !activity.isBoosted
272272
}
273273

274274
init(coreService: CoreService) {

Bitkit/Services/MigrationsService.swift

Lines changed: 179 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,35 @@ struct RNActivityItem: Codable {
142142
var status: String?
143143
var message: String?
144144
var preimage: String?
145+
var boostedParents: [String]?
146+
}
147+
148+
struct RNTransfer: Codable {
149+
var txId: String?
150+
var type: String?
151+
}
152+
153+
struct RNBoostedTransaction: Codable {
154+
var oldTxId: String?
155+
var newTxId: String?
156+
var childTransaction: String?
157+
var parentTransactions: [String]?
158+
var type: String?
159+
var fee: Int64?
160+
}
161+
162+
struct RNWalletBackup: Codable {
163+
var transfers: [String: [RNTransfer]]?
164+
var boostedTransactions: [String: [String: RNBoostedTransaction]]?
165+
}
166+
167+
struct RNWalletState: Codable {
168+
var wallets: [String: RNWalletData]?
169+
}
170+
171+
struct RNWalletData: Codable {
172+
var boostedTransactions: [String: [String: RNBoostedTransaction]]?
173+
var transfers: [String: [RNTransfer]]?
145174
}
146175

147176
struct RNLightningState: Codable {
@@ -709,6 +738,80 @@ extension MigrationsService {
709738
}
710739
}
711740

741+
func extractRNWalletBackup(from mmkvData: [String: String]) -> (transfers: [String: String], boosts: [String: String])? {
742+
guard let rootJson = mmkvData["persist:root"],
743+
let jsonStart = rootJson.firstIndex(of: "{")
744+
else {
745+
return nil
746+
}
747+
748+
let jsonString = String(rootJson[jsonStart...])
749+
guard let data = jsonString.data(using: .utf8),
750+
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
751+
let walletJson = root["wallet"] as? String,
752+
let walletData = walletJson.data(using: .utf8)
753+
else {
754+
return nil
755+
}
756+
757+
func extractTransfers(_ transfers: [String: [RNTransfer]]?) -> [String: String] {
758+
var transferMap: [String: String] = [:]
759+
guard let transfers else { return transferMap }
760+
for (_, networkTransfers) in transfers {
761+
for transfer in networkTransfers {
762+
if let txId = transfer.txId, let type = transfer.type {
763+
transferMap[txId] = type
764+
}
765+
}
766+
}
767+
return transferMap
768+
}
769+
770+
func extractBoosts(_ boostedTxs: [String: [String: RNBoostedTransaction]]?) -> [String: String] {
771+
var boostMap: [String: String] = [:]
772+
guard let boostedTxs else { return boostMap }
773+
for (_, networkBoosts) in boostedTxs {
774+
for (parentTxId, boost) in networkBoosts {
775+
if let childTxId = boost.childTransaction ?? boost.newTxId {
776+
boostMap[parentTxId] = childTxId
777+
}
778+
}
779+
}
780+
return boostMap
781+
}
782+
783+
do {
784+
if let walletState = try? JSONDecoder().decode(RNWalletState.self, from: walletData),
785+
let wallets = walletState.wallets
786+
{
787+
var transferMap: [String: String] = [:]
788+
var boostMap: [String: String] = [:]
789+
790+
for (_, walletData) in wallets {
791+
transferMap.merge(extractTransfers(walletData.transfers)) { _, new in new }
792+
boostMap.merge(extractBoosts(walletData.boostedTransactions)) { _, new in new }
793+
}
794+
795+
if !transferMap.isEmpty || !boostMap.isEmpty {
796+
return (transfers: transferMap, boosts: boostMap)
797+
}
798+
}
799+
800+
let walletBackup = try JSONDecoder().decode(RNWalletBackup.self, from: walletData)
801+
let transferMap = extractTransfers(walletBackup.transfers)
802+
let boostMap = extractBoosts(walletBackup.boostedTransactions)
803+
804+
if !transferMap.isEmpty || !boostMap.isEmpty {
805+
return (transfers: transferMap, boosts: boostMap)
806+
}
807+
808+
return nil
809+
} catch {
810+
Logger.error("Failed to decode RN wallet backup: \(error)", context: "Migration")
811+
return nil
812+
}
813+
}
814+
712815
func applyRNSettings(_ settings: RNSettings) {
713816
let defaults = UserDefaults.standard
714817

@@ -973,6 +1076,18 @@ extension MigrationsService {
9731076
if let activities = extractRNActivities(from: mmkvData) {
9741077
await applyOnchainMetadata(activities)
9751078
}
1079+
1080+
// Extract and apply wallet backup data (transfers and boosts)
1081+
if let walletBackup = extractRNWalletBackup(from: mmkvData) {
1082+
if !walletBackup.transfers.isEmpty {
1083+
Logger.info("Applying \(walletBackup.transfers.count) local transfer markers", context: "Migration")
1084+
await applyRemoteTransfers(walletBackup.transfers)
1085+
}
1086+
if !walletBackup.boosts.isEmpty {
1087+
Logger.info("Applying \(walletBackup.boosts.count) local boost markers", context: "Migration")
1088+
await applyBoostTransactions(walletBackup.boosts)
1089+
}
1090+
}
9761091
}
9771092

9781093
// Handle remote backup data (for on-chain timestamps from RN backup)
@@ -992,7 +1107,7 @@ extension MigrationsService {
9921107
// Handle remote backup boosts (apply boostTxIds to activities)
9931108
if let boosts = pendingRemoteBoosts {
9941109
Logger.info("Applying \(boosts.count) remote boost markers", context: "Migration")
995-
await applyRemoteBoosts(boosts)
1110+
await applyBoostTransactions(boosts)
9961111
pendingRemoteBoosts = nil
9971112
}
9981113

@@ -1026,24 +1141,42 @@ extension MigrationsService {
10261141
Logger.info("Applied \(applied)/\(transfers.count) transfer markers", context: "Migration")
10271142
}
10281143

1029-
private func applyRemoteBoosts(_ boosts: [String: String]) async {
1144+
private func applyBoostTransactions(_ boosts: [String: String]) async {
10301145
var applied = 0
10311146

10321147
for (oldTxId, newTxId) in boosts {
1033-
guard var onchain = try? await CoreService.shared.activity.getOnchainActivityByTxId(txid: newTxId) else {
1034-
continue
1035-
}
1148+
let oldOnchain = try? await CoreService.shared.activity.getOnchainActivityByTxId(txid: oldTxId)
1149+
let newOnchain = try? await CoreService.shared.activity.getOnchainActivityByTxId(txid: newTxId)
10361150

1037-
if !onchain.boostTxIds.contains(oldTxId) {
1038-
onchain.boostTxIds.append(oldTxId)
1039-
}
1040-
onchain.isBoosted = true
1151+
if let oldOnchain, var newOnchain {
1152+
var parentOnchain = oldOnchain
1153+
if !parentOnchain.boostTxIds.contains(newTxId) {
1154+
parentOnchain.boostTxIds.append(newTxId)
1155+
}
1156+
parentOnchain.isBoosted = true
10411157

1042-
do {
1043-
try await CoreService.shared.activity.update(id: onchain.id, activity: .onchain(onchain))
1044-
applied += 1
1045-
} catch {
1046-
Logger.error("Failed to apply boost for tx \(newTxId): \(error)", context: "Migration")
1158+
newOnchain.isBoosted = false
1159+
newOnchain.boostTxIds.removeAll { $0 == oldTxId }
1160+
1161+
do {
1162+
try await CoreService.shared.activity.update(id: parentOnchain.id, activity: .onchain(parentOnchain))
1163+
try await CoreService.shared.activity.update(id: newOnchain.id, activity: .onchain(newOnchain))
1164+
applied += 1
1165+
} catch {
1166+
Logger.error("Failed to apply CPFP boost for parent \(oldTxId) / child \(newTxId): \(error)", context: "Migration")
1167+
}
1168+
} else if var newOnchain {
1169+
if !newOnchain.boostTxIds.contains(oldTxId) {
1170+
newOnchain.boostTxIds.append(oldTxId)
1171+
}
1172+
newOnchain.isBoosted = true
1173+
1174+
do {
1175+
try await CoreService.shared.activity.update(id: newOnchain.id, activity: .onchain(newOnchain))
1176+
applied += 1
1177+
} catch {
1178+
Logger.error("Failed to apply RBF boost for tx \(newTxId): \(error)", context: "Migration")
1179+
}
10471180
}
10481181
}
10491182

@@ -1064,23 +1197,18 @@ extension MigrationsService {
10641197
var applied = 0
10651198
for (activityId, tagList) in tags {
10661199
do {
1067-
// Try on-chain first
10681200
if let onchain = try? await CoreService.shared.activity.getOnchainActivityByTxId(txid: activityId) {
10691201
try await CoreService.shared.activity.upsertTags([
10701202
ActivityTags(activityId: onchain.id, tags: tagList),
10711203
])
10721204
applied += 1
1073-
}
1074-
// Then try lightning
1075-
else if let activity = try? await CoreService.shared.activity.getActivity(id: activityId),
1076-
case .lightning = activity
1205+
} else if let activity = try? await CoreService.shared.activity.getActivity(id: activityId),
1206+
case .lightning = activity
10771207
{
10781208
try await CoreService.shared.activity.upsertTags([
10791209
ActivityTags(activityId: activityId, tags: tagList),
10801210
])
10811211
applied += 1
1082-
} else {
1083-
Logger.warn("Activity \(activityId) still not found after sync", context: "Migration")
10841212
}
10851213
} catch {
10861214
Logger.error("Failed to apply pending tag for \(activityId): \(error)", context: "Migration")
@@ -1110,6 +1238,27 @@ extension MigrationsService {
11101238
onchain.transferTxId = item.transferTxId
11111239
}
11121240

1241+
if let boostedParents = item.boostedParents, !boostedParents.isEmpty {
1242+
for parentTxId in boostedParents {
1243+
if var parentOnchain = try? await CoreService.shared.activity.getOnchainActivityByTxId(txid: parentTxId) {
1244+
if !parentOnchain.boostTxIds.contains(txId) {
1245+
parentOnchain.boostTxIds.append(txId)
1246+
}
1247+
parentOnchain.isBoosted = true
1248+
1249+
do {
1250+
try await CoreService.shared.activity.update(id: parentOnchain.id, activity: .onchain(parentOnchain))
1251+
} catch {
1252+
Logger.error("Failed to mark parent \(parentTxId) as boosted for CPFP: \(error)", context: "Migration")
1253+
}
1254+
}
1255+
}
1256+
onchain.isBoosted = false
1257+
onchain.boostTxIds.removeAll { boostedParents.contains($0) }
1258+
} else if item.isBoosted == true {
1259+
onchain.isBoosted = true
1260+
}
1261+
11131262
do {
11141263
try await CoreService.shared.activity.update(id: onchain.id, activity: .onchain(onchain))
11151264
} catch {
@@ -1411,6 +1560,7 @@ extension MigrationsService {
14111560
var status: String?
14121561
var message: String?
14131562
var preimage: String?
1563+
var boostedParents: [String]?
14141564
}
14151565

14161566
struct BackupEnvelope: Codable {
@@ -1442,7 +1592,8 @@ extension MigrationsService {
14421592
transferTxId: item.transferTxId,
14431593
status: item.status,
14441594
message: item.message,
1445-
preimage: item.preimage
1595+
preimage: item.preimage,
1596+
boostedParents: item.boostedParents
14461597
)
14471598
}
14481599

@@ -1453,23 +1604,8 @@ extension MigrationsService {
14531604
}
14541605

14551606
private func applyRNRemoteWallet(_ data: Data) async throws {
1456-
struct Transfer: Codable {
1457-
var txId: String?
1458-
var type: String?
1459-
}
1460-
1461-
struct BoostedTransaction: Codable {
1462-
var oldTxId: String?
1463-
var newTxId: String?
1464-
}
1465-
1466-
struct WalletBackup: Codable {
1467-
var transfers: [String: [Transfer]]?
1468-
var boostedTransactions: [String: [String: BoostedTransaction]]?
1469-
}
1470-
14711607
struct BackupEnvelope: Codable {
1472-
let data: WalletBackup
1608+
let data: RNWalletBackup
14731609
}
14741610

14751611
guard let json = try? JSONDecoder().decode(BackupEnvelope.self, from: data) else {
@@ -1502,14 +1638,17 @@ extension MigrationsService {
15021638
var boostMap: [String: String] = [:]
15031639
for (_, networkBoosts) in boostedTxs {
15041640
for (oldTxId, boost) in networkBoosts {
1505-
if let newTxId = boost.newTxId {
1506-
boostMap[oldTxId] = newTxId
1641+
if let childTxId = boost.childTransaction ?? boost.newTxId {
1642+
boostMap[oldTxId] = childTxId
15071643
}
15081644
}
15091645
}
1646+
Logger.info("Found \(boostMap.count) boosted transactions in remote backup", context: "Migration")
15101647
if !boostMap.isEmpty {
15111648
pendingRemoteBoosts = boostMap
15121649
}
1650+
} else {
1651+
Logger.debug("No boosted transactions found in RN remote wallet backup", context: "Migration")
15131652
}
15141653
}
15151654

Bitkit/Views/Wallets/Activity/ActivityExplorerView.swift

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -190,15 +190,9 @@ struct ActivityExplorerView: View {
190190
Divider()
191191
.padding(.bottom, 16)
192192
ForEach(Array(onchain.boostTxIds.enumerated()), id: \.offset) { index, boostTxId in
193-
// Determine if this is RBF (doesExist = false, replaced) or CPFP (doesExist = true, child transaction)
194-
let boostTxDoesExistValue = boostTxDoesExist[boostTxId] ?? true
195-
let isRBF = !boostTxDoesExistValue
196-
let key = isRBF
197-
? "wallet__activity_boosted_rbf"
198-
: "wallet__activity_boosted_cpfp"
199-
193+
let isRBF = onchain.txType == .sent || !(boostTxDoesExist[boostTxId] ?? true)
200194
InfoSection(
201-
title: t(key, variables: ["num": String(index + 1)]),
195+
title: t(isRBF ? "wallet__activity_boosted_rbf" : "wallet__activity_boosted_cpfp", variables: ["num": String(index + 1)]),
202196
content: boostTxId,
203197
testId: isRBF ? "RBFBoosted" : "CPFPBoosted"
204198
)

Bitkit/Views/Wallets/Activity/ActivityItemView.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,11 @@ struct ActivityItemView: View {
129129
return true
130130
}
131131
if activity.isBoosted && !activity.boostTxIds.isEmpty {
132-
let hasCPFP = activity.boostTxIds.contains { boostTxDoesExist[$0] == true }
133-
if hasCPFP {
134-
return true
135-
}
136-
137132
if activity.txType == .sent {
138-
let hasRBF = activity.boostTxIds.contains { boostTxDoesExist[$0] == false }
139-
return hasRBF
133+
return true
134+
} else {
135+
let hasCPFP = activity.boostTxIds.contains { boostTxDoesExist[$0] == true }
136+
return hasCPFP
140137
}
141138
}
142139

0 commit comments

Comments
 (0)