@@ -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
147176struct 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
0 commit comments