@@ -78,6 +78,7 @@ private data class RNRemoteActivityItem(
7878 val status : String? = null ,
7979 val message : String? = null ,
8080 val preimage : String? = null ,
81+ val boostedParents : List <String >? = null ,
8182)
8283
8384@Serializable
@@ -90,7 +91,11 @@ private data class RNRemoteWalletBackup(
9091private data class RNRemoteTransfer (val txId : String? = null , val type : String? = null )
9192
9293@Serializable
93- private data class RNRemoteBoostedTx (val oldTxId : String? = null , val newTxId : String? = null )
94+ private data class RNRemoteBoostedTx (
95+ val oldTxId : String? = null ,
96+ val newTxId : String? = null ,
97+ val childTransaction : String? = null ,
98+ )
9499
95100@Serializable
96101private data class RNRemoteBlocktankBackup (
@@ -557,6 +562,99 @@ class MigrationService @Inject constructor(
557562 }
558563 }
559564
565+ private fun extractTransfers (transfers : Map <String , List <RNRemoteTransfer >>? ): Map <String , String > {
566+ val transferMap = mutableMapOf<String , String >()
567+ transfers?.values?.flatten()?.forEach { transfer ->
568+ transfer.txId?.let { txId ->
569+ transfer.type?.let { type ->
570+ transferMap[txId] = type
571+ }
572+ }
573+ }
574+ return transferMap
575+ }
576+
577+ private fun extractBoosts (boostedTxs : Map <String , Map <String , RNRemoteBoostedTx >>? ): Map <String , String > {
578+ val boostMap = mutableMapOf<String , String >()
579+ boostedTxs?.values?.forEach { networkBoosts ->
580+ networkBoosts.forEach { (parentTxId, boost) ->
581+ val childTxId = boost.childTransaction ? : boost.newTxId
582+ childTxId?.let {
583+ boostMap[parentTxId] = it
584+ }
585+ }
586+ }
587+ return boostMap
588+ }
589+
590+ private fun extractFromWalletState (walletState : RNWalletState ): Pair <Map <String , String >, Map<String, String>>? {
591+ val transferMap = mutableMapOf<String , String >()
592+ val boostMap = mutableMapOf<String , String >()
593+
594+ walletState.wallets?.values?.forEach { walletDataItem ->
595+ walletDataItem.transfers?.let { transferMap.putAll(extractTransfers(it)) }
596+ walletDataItem.boostedTransactions?.let { boostMap.putAll(extractBoosts(it)) }
597+ }
598+
599+ return if (transferMap.isNotEmpty() || boostMap.isNotEmpty()) {
600+ Pair (transferMap, boostMap)
601+ } else {
602+ null
603+ }
604+ }
605+
606+ private fun extractFromWalletBackup (
607+ walletBackup : RNRemoteWalletBackup ,
608+ ): Pair <Map <String , String >, Map<String, String>>? {
609+ val transferMap = extractTransfers(walletBackup.transfers)
610+ val boostMap = extractBoosts(walletBackup.boostedTransactions)
611+
612+ return if (transferMap.isNotEmpty() || boostMap.isNotEmpty()) {
613+ Pair (transferMap, boostMap)
614+ } else {
615+ null
616+ }
617+ }
618+
619+ @Suppress(" TooGenericExceptionCaught" )
620+ private suspend fun extractRNWalletBackup (
621+ mmkvData : Map <String , String >
622+ ): Pair <Map <String , String >, Map<String, String>>? {
623+ val rootJson = mmkvData[" persist:root" ] ? : return null
624+
625+ return try {
626+ val jsonStart = rootJson.indexOf(" {" )
627+ val jsonString = if (jsonStart >= 0 ) rootJson.substring(jsonStart) else rootJson
628+
629+ val root = json.parseToJsonElement(jsonString).jsonObject
630+ val walletJsonString = root[" wallet" ]?.jsonPrimitive?.content ? : return null
631+ val walletData = json.parseToJsonElement(walletJsonString).jsonObject
632+
633+ val walletState = runCatching {
634+ json.decodeFromJsonElement<RNWalletState >(walletData)
635+ }.getOrNull()
636+
637+ walletState?.let { extractFromWalletState(it) } ? : run {
638+ val walletBackup = runCatching {
639+ json.decodeFromJsonElement<RNRemoteWalletBackup >(walletData)
640+ }.getOrNull()
641+ walletBackup?.let { extractFromWalletBackup(it) }
642+ }
643+ } catch (e: Exception ) {
644+ Logger .error(" Failed to decode RN wallet backup: $e " , e, context = TAG )
645+ null
646+ }
647+ }
648+
649+ @Serializable
650+ private data class RNWalletState (val wallets : Map <String , RNWalletData >? = null )
651+
652+ @Serializable
653+ private data class RNWalletData (
654+ val transfers : Map <String , List <RNRemoteTransfer >>? = null ,
655+ val boostedTransactions : Map <String , Map <String , RNRemoteBoostedTx >>? = null ,
656+ )
657+
560658 @Suppress(" NestedBlockDepth" , " TooGenericExceptionCaught" )
561659 private suspend fun extractRNClosedChannels (mmkvData : Map <String , String >): List <RNChannel >? {
562660 val rootJson = mmkvData[" persist:root" ] ? : return null
@@ -1052,6 +1150,7 @@ class MigrationService @Inject constructor(
10521150 status = item.status,
10531151 message = item.message,
10541152 preimage = item.preimage,
1153+ boostedParents = item.boostedParents,
10551154 )
10561155 }
10571156
@@ -1092,13 +1191,17 @@ class MigrationService @Inject constructor(
10921191 val boostMap = mutableMapOf<String , String >()
10931192 boostedTxs.values.forEach { networkBoosts ->
10941193 networkBoosts.forEach { (oldTxId, boost) ->
1095- boost.newTxId?.let { newTxId ->
1096- boostMap[oldTxId] = newTxId
1194+ val childTxId = boost.childTransaction ? : boost.newTxId
1195+ childTxId?.let {
1196+ boostMap[oldTxId] = it
10971197 }
10981198 }
10991199 }
11001200 if (boostMap.isNotEmpty()) {
1201+ Logger .info(" Found ${boostMap.size} boosted transactions in remote backup" , context = TAG )
11011202 pendingRemoteBoosts = boostMap
1203+ } else {
1204+ Logger .debug(" No boosted transactions found in RN remote wallet backup" , context = TAG )
11021205 }
11031206 }
11041207 }.onFailure { e ->
@@ -1149,6 +1252,17 @@ class MigrationService @Inject constructor(
11491252 extractRNActivities(mmkvData)?.let { activities ->
11501253 applyOnchainMetadata(activities)
11511254 }
1255+
1256+ extractRNWalletBackup(mmkvData)?.let { (transfers, boosts) ->
1257+ if (transfers.isNotEmpty()) {
1258+ Logger .info(" Applying ${transfers.size} local transfer markers" , context = TAG )
1259+ applyRemoteTransfers(transfers)
1260+ }
1261+ if (boosts.isNotEmpty()) {
1262+ Logger .info(" Applying ${boosts.size} local boost markers" , context = TAG )
1263+ applyBoostTransactions(boosts)
1264+ }
1265+ }
11521266 }
11531267
11541268 pendingRemoteActivityData?.let { remoteActivities ->
@@ -1162,7 +1276,7 @@ class MigrationService @Inject constructor(
11621276 }
11631277
11641278 pendingRemoteBoosts?.let { boosts ->
1165- applyRemoteBoosts (boosts)
1279+ applyBoostTransactions (boosts)
11661280 pendingRemoteBoosts = null
11671281 }
11681282
@@ -1180,17 +1294,136 @@ class MigrationService @Inject constructor(
11801294 }
11811295 }
11821296
1183- private suspend fun applyRemoteBoosts (boosts : Map <String , String >) {
1297+ private suspend fun applyBoostTransactions (boosts : Map <String , String >) {
1298+ var applied = 0
1299+
11841300 boosts.forEach { (oldTxId, newTxId) ->
1185- val onchain = activityRepo.getOnchainActivityByTxId(newTxId) ? : return @forEach
1186- val updatedBoostTxIds = if (oldTxId !in onchain.boostTxIds) {
1187- onchain.boostTxIds + oldTxId
1188- } else {
1189- onchain.boostTxIds
1301+ val oldOnchain = activityRepo.getOnchainActivityByTxId(oldTxId)
1302+ val newOnchain = activityRepo.getOnchainActivityByTxId(newTxId)
1303+
1304+ if (oldOnchain != null && newOnchain != null ) {
1305+ var parentOnchain = oldOnchain
1306+ val updatedParentBoostTxIds = if (newTxId !in parentOnchain.boostTxIds) {
1307+ parentOnchain.boostTxIds + newTxId
1308+ } else {
1309+ parentOnchain.boostTxIds
1310+ }
1311+ parentOnchain = parentOnchain.copy(
1312+ isBoosted = true ,
1313+ boostTxIds = updatedParentBoostTxIds,
1314+ )
1315+
1316+ var updatedNewOnchain = newOnchain.copy(
1317+ isBoosted = false ,
1318+ boostTxIds = newOnchain.boostTxIds.filter { it != oldTxId },
1319+ )
1320+
1321+ runCatching {
1322+ activityRepo.updateActivity(parentOnchain.id, Activity .Onchain (parentOnchain))
1323+ activityRepo.updateActivity(updatedNewOnchain.id, Activity .Onchain (updatedNewOnchain))
1324+ applied++
1325+ }.onFailure { e ->
1326+ Logger .error(
1327+ " Failed to apply CPFP boost for parent $oldTxId / child $newTxId : $e " ,
1328+ e,
1329+ context = TAG
1330+ )
1331+ }
1332+ } else if (newOnchain != null ) {
1333+ val updatedBoostTxIds = if (oldTxId !in newOnchain.boostTxIds) {
1334+ newOnchain.boostTxIds + oldTxId
1335+ } else {
1336+ newOnchain.boostTxIds
1337+ }
1338+ val updated = newOnchain.copy(
1339+ isBoosted = true ,
1340+ boostTxIds = updatedBoostTxIds,
1341+ )
1342+
1343+ runCatching {
1344+ activityRepo.updateActivity(updated.id, Activity .Onchain (updated))
1345+ applied++
1346+ }.onFailure { e ->
1347+ Logger .error(" Failed to apply RBF boost for tx $newTxId : $e " , e, context = TAG )
1348+ }
11901349 }
1191- val updated = onchain.copy(isBoosted = true , boostTxIds = updatedBoostTxIds)
1192- activityRepo.updateActivity(onchain.id, Activity .Onchain (updated))
11931350 }
1351+
1352+ Logger .info(" Applied $applied /${boosts.size} boost markers" , context = TAG )
1353+ }
1354+
1355+ private suspend fun applyBoostedParents (boostedParents : List <String >, txId : String ) {
1356+ boostedParents.forEach { parentTxId ->
1357+ val parentOnchain = activityRepo.getOnchainActivityByTxId(parentTxId)
1358+ if (parentOnchain != null ) {
1359+ val updatedParentBoostTxIds = if (txId !in parentOnchain.boostTxIds) {
1360+ parentOnchain.boostTxIds + txId
1361+ } else {
1362+ parentOnchain.boostTxIds
1363+ }
1364+ val updatedParent = parentOnchain.copy(
1365+ isBoosted = true ,
1366+ boostTxIds = updatedParentBoostTxIds,
1367+ )
1368+
1369+ runCatching {
1370+ activityRepo.updateActivity(updatedParent.id, Activity .Onchain (updatedParent))
1371+ }.onFailure { e ->
1372+ Logger .error(" Failed to mark parent $parentTxId as boosted for CPFP: $e " , e, context = TAG )
1373+ }
1374+ }
1375+ }
1376+ }
1377+
1378+ private suspend fun updateOnchainActivityMetadata (
1379+ item : RNActivityItem ,
1380+ onchain : OnchainActivity ,
1381+ ): OnchainActivity ? {
1382+ var updated: OnchainActivity = onchain
1383+ var wasUpdated = false
1384+
1385+ if (item.timestamp > 0 ) {
1386+ val migratedTimestamp = (item.timestamp / MILLISECONDS_TO_SECONDS ).toULong()
1387+ if (updated.timestamp != migratedTimestamp) {
1388+ updated = updated.copy(timestamp = migratedTimestamp)
1389+ wasUpdated = true
1390+ }
1391+ }
1392+ item.confirmTimestamp?.let { confirmTimestamp ->
1393+ if (confirmTimestamp > 0 ) {
1394+ val migratedConfirmTimestamp = (confirmTimestamp / MILLISECONDS_TO_SECONDS ).toULong()
1395+ if (updated.confirmTimestamp != migratedConfirmTimestamp) {
1396+ updated = updated.copy(confirmTimestamp = migratedConfirmTimestamp)
1397+ wasUpdated = true
1398+ }
1399+ }
1400+ }
1401+ if (item.isTransfer == true ) {
1402+ if (! updated.isTransfer || updated.channelId != item.channelId ||
1403+ updated.transferTxId != item.transferTxId
1404+ ) {
1405+ updated = updated.copy(
1406+ isTransfer = true ,
1407+ channelId = item.channelId,
1408+ transferTxId = item.transferTxId,
1409+ )
1410+ wasUpdated = true
1411+ }
1412+ }
1413+
1414+ if (item.boostedParents?.isNotEmpty() == true ) {
1415+ applyBoostedParents(item.boostedParents, item.txId ? : item.id)
1416+ updated = updated.copy(
1417+ isBoosted = false ,
1418+ boostTxIds = updated.boostTxIds.filter { it !in item.boostedParents },
1419+ )
1420+ wasUpdated = true
1421+ } else if (item.isBoosted == true ) {
1422+ updated = updated.copy(isBoosted = true )
1423+ wasUpdated = true
1424+ }
1425+
1426+ return if (wasUpdated) updated else null
11941427 }
11951428
11961429 @Suppress(" CyclomaticComplexMethod" , " NestedBlockDepth" )
@@ -1205,43 +1438,8 @@ class MigrationService @Inject constructor(
12051438 Logger .warn(" Onchain activity not found for txId: $txId " , context = TAG )
12061439 return @forEach
12071440 }
1208- var updated: OnchainActivity = onchain
1209- var wasUpdated = false
1210-
1211- if (item.timestamp > 0 ) {
1212- val migratedTimestamp = (item.timestamp / MILLISECONDS_TO_SECONDS ).toULong()
1213- if (updated.timestamp != migratedTimestamp) {
1214- updated = updated.copy(
1215- timestamp = migratedTimestamp
1216- )
1217- wasUpdated = true
1218- }
1219- }
1220- item.confirmTimestamp?.let { confirmTimestamp ->
1221- if (confirmTimestamp > 0 ) {
1222- val migratedConfirmTimestamp = (confirmTimestamp / MILLISECONDS_TO_SECONDS ).toULong()
1223- if (updated.confirmTimestamp != migratedConfirmTimestamp) {
1224- updated = updated.copy(
1225- confirmTimestamp = migratedConfirmTimestamp
1226- )
1227- wasUpdated = true
1228- }
1229- }
1230- }
1231- if (item.isTransfer == true ) {
1232- if (! updated.isTransfer || updated.channelId != item.channelId ||
1233- updated.transferTxId != item.transferTxId
1234- ) {
1235- updated = updated.copy(
1236- isTransfer = true ,
1237- channelId = item.channelId,
1238- transferTxId = item.transferTxId,
1239- )
1240- wasUpdated = true
1241- }
1242- }
12431441
1244- if (wasUpdated) {
1442+ updateOnchainActivityMetadata(item, onchain)?. let { updated ->
12451443 activityRepo.updateActivity(updated.id, Activity .Onchain (updated))
12461444 .onFailure { e ->
12471445 Logger .error(
@@ -1430,6 +1628,7 @@ data class RNActivityItem(
14301628 val status : String? = null ,
14311629 val message : String? = null ,
14321630 val preimage : String? = null ,
1631+ val boostedParents : List <String >? = null ,
14331632)
14341633
14351634@Serializable
0 commit comments