Skip to content

Commit 085c69f

Browse files
committed
Fix boosted tx restore and migration from RN
1 parent cf7f89e commit 085c69f

File tree

4 files changed

+254
-66
lines changed

4 files changed

+254
-66
lines changed

app/src/main/java/to/bitkit/services/CoreService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1151,7 +1151,7 @@ class ActivityService(
11511151
for (boostTxId in boostTxIds) {
11521152
val boostActivity = getOnchainActivityByTxId(boostTxId)
11531153
if (boostActivity != null) {
1154-
doesExistMap[boostTxId] = boostActivity.doesExist
1154+
doesExistMap[boostTxId] = boostActivity.doesExist && !boostActivity.isBoosted
11551155
}
11561156
}
11571157
return@background doesExistMap

app/src/main/java/to/bitkit/services/MigrationService.kt

Lines changed: 247 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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(
9091
private 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
96101
private 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

Comments
 (0)