From bdf11abd7732d29fec0270e01cdd3d6a920e9249 Mon Sep 17 00:00:00 2001 From: benk10 Date: Fri, 9 Jan 2026 15:18:46 -0500 Subject: [PATCH] fix: recover channel orders during migration --- .../to/bitkit/services/MigrationService.kt | 165 +++++++++++++++--- 1 file changed, 143 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/to/bitkit/services/MigrationService.kt b/app/src/main/java/to/bitkit/services/MigrationService.kt index 6ecaf0216..810c5f5fc 100644 --- a/app/src/main/java/to/bitkit/services/MigrationService.kt +++ b/app/src/main/java/to/bitkit/services/MigrationService.kt @@ -35,8 +35,10 @@ import org.lightningdevkit.ldknode.Network import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore import to.bitkit.data.WidgetsStore +import to.bitkit.data.dao.TransferDao import to.bitkit.data.dto.price.GraphPeriod import to.bitkit.data.dto.price.TradingPair +import to.bitkit.data.entities.TransferEntity import to.bitkit.data.keychain.Keychain import to.bitkit.data.resetPin import to.bitkit.di.json @@ -46,6 +48,7 @@ import to.bitkit.models.CoinSelectionPreference import to.bitkit.models.PrimaryDisplay import to.bitkit.models.Suggestion import to.bitkit.models.TransactionSpeed +import to.bitkit.models.TransferType import to.bitkit.models.WidgetType import to.bitkit.models.WidgetWithPosition import to.bitkit.models.widget.BlocksPreferences @@ -74,6 +77,7 @@ class MigrationService @Inject constructor( private val coreService: CoreService, private val rnBackupClient: RNBackupClient, private val bip39Service: Bip39Service, + private val transferDao: TransferDao, ) { companion object { private const val TAG = "Migration" @@ -128,6 +132,9 @@ class MigrationService @Inject constructor( @Volatile private var pendingRemoteMetadata: RNMetadata? = null + @Volatile + private var pendingRemotePaidOrders: Map? = null + private fun buildRnLdkAccountPath(): File = run { val rnNetworkString = when (Env.network) { Network.BITCOIN -> "bitcoin" @@ -534,6 +541,48 @@ class MigrationService @Inject constructor( }.getOrNull() } + private fun extractRNBlocktank(mmkvData: Map): Pair, Map>? { + val rootJson = mmkvData[MMKV_ROOT] ?: return null + + return runCatching { + val jsonStart = rootJson.indexOf(OPENING_CURLY_BRACE) + val jsonString = if (jsonStart >= 0) rootJson.substring(jsonStart) else rootJson + + val root = json.parseToJsonElement(jsonString).jsonObject + val blocktankJsonString = root["blocktank"]?.jsonPrimitive?.content ?: return@runCatching null + + val blocktankData = json.parseToJsonElement(blocktankJsonString).jsonObject + val orderIds = mutableListOf() + val paidOrdersMap = mutableMapOf() + + blocktankData["orders"]?.jsonArray?.forEach { orderElement -> + orderElement.jsonObject["id"]?.jsonPrimitive?.content?.let { id -> + orderIds.add(id) + } + } + + blocktankData["paidOrders"]?.jsonObject?.forEach { (orderId, txIdElement) -> + val txId = txIdElement.jsonPrimitive.content + paidOrdersMap[orderId] = txId + if (orderId !in orderIds) { + orderIds.add(orderId) + } + } + + if (orderIds.isEmpty() && paidOrdersMap.isEmpty()) { + return@runCatching null + } + + Logger.info( + "Extracted ${orderIds.size} order IDs and ${paidOrdersMap.size} paid orders from local blocktank", + context = TAG, + ) + Pair(orderIds, paidOrdersMap) + }.onFailure { + Logger.error("Failed to decode RN blocktank: $it", it, context = TAG) + }.getOrNull() + } + @Suppress("NestedBlockDepth") private fun extractRNClosedChannels(mmkvData: Map): List? { val rootJson = mmkvData[MMKV_ROOT] ?: return null @@ -922,6 +971,34 @@ class MigrationService @Inject constructor( extractRNTodos(mmkvData)?.let { todos -> applyRNTodos(todos) } + + extractRNBlocktank(mmkvData)?.let { (orderIds, paidOrders) -> + applyRNBlocktank(orderIds, paidOrders) + } + } + + private suspend fun applyRNBlocktank(orderIds: List, paidOrders: Map) { + if (orderIds.isEmpty()) return + + paidOrders.forEach { (orderId, txId) -> + cacheStore.addPaidOrder(orderId, txId) + } + + runCatching { + val fetchedOrders = coreService.blocktank.orders( + orderIds = orderIds, + filter = null, + refresh = true, + ) + if (fetchedOrders.isNotEmpty()) { + coreService.blocktank.upsertOrderList(fetchedOrders) + if (paidOrders.isNotEmpty()) { + createTransfersForPaidOrders(paidOrders, fetchedOrders) + } + } + }.onFailure { e -> + Logger.warn("Failed to fetch and upsert local Blocktank orders: $e", context = TAG) + } } suspend fun getRNRemoteBackupTimestamp(): ULong? = runCatching { @@ -1108,31 +1185,13 @@ class MigrationService @Inject constructor( private suspend fun applyRNRemoteBlocktank(data: ByteArray) { runCatching { val backup = decodeBackupData(data) - val orderIds = mutableListOf() - backup.orders?.let { orders -> - orderIds.addAll(orders.map { it.id }) - } - - backup.paidOrders?.let { paidOrdersMap -> - orderIds.addAll(paidOrdersMap.keys) - paidOrdersMap.forEach { (orderId, txId) -> + backup.paidOrders?.let { paidOrders -> + paidOrders.forEach { (orderId, txId) -> cacheStore.addPaidOrder(orderId, txId) } - } - - if (orderIds.isNotEmpty()) { - runCatching { - val fetchedOrders = coreService.blocktank.orders( - orderIds = orderIds, - filter = null, - refresh = true, - ) - if (fetchedOrders.isNotEmpty()) { - coreService.blocktank.upsertOrderList(fetchedOrders) - } - }.onFailure { e -> - Logger.warn("Failed to fetch and upsert Blocktank orders: $e", context = TAG) + if (paidOrders.isNotEmpty()) { + pendingRemotePaidOrders = paidOrders } } }.onFailure { e -> @@ -1140,6 +1199,43 @@ class MigrationService @Inject constructor( } } + private suspend fun createTransfersForPaidOrders( + paidOrdersMap: Map, + orders: List, + ) { + val now = System.currentTimeMillis() / 1000 + val transfers = paidOrdersMap.mapNotNull { (orderId, txId) -> + val order = orders.find { it.id == orderId } + when { + order == null -> { + Logger.warn("Paid order $orderId not found in fetched orders", context = TAG) + null + } + order.state2 == com.synonym.bitkitcore.BtOrderState2.EXECUTED -> null + else -> TransferEntity( + id = txId, + type = TransferType.TO_SPENDING, + amountSats = (order.clientBalanceSat + order.feeSat).toLong(), + channelId = null, + fundingTxId = null, + lspOrderId = orderId, + isSettled = false, + createdAt = now, + settledAt = null, + ) + } + } + + if (transfers.isNotEmpty()) { + runCatching { + transferDao.upsert(transfers) + Logger.info("Created ${transfers.size} transfers for paid Blocktank orders", context = TAG) + }.onFailure { e -> + Logger.error("Failed to create transfers for paid orders: $e", context = TAG) + } + } + } + suspend fun reapplyMetadataAfterSync() { if (hasRNMmkvData()) { val mmkvData = loadRNMmkvData() ?: return @@ -1183,6 +1279,31 @@ class MigrationService @Inject constructor( applyRNMetadata(metadata) pendingRemoteMetadata = null } + + pendingRemotePaidOrders?.let { paidOrders -> + applyRemotePaidOrders(paidOrders) + pendingRemotePaidOrders = null + } + } + + private suspend fun applyRemotePaidOrders(paidOrders: Map) { + if (paidOrders.isEmpty()) return + + val orderIds = paidOrders.keys.toList() + + runCatching { + val fetchedOrders = coreService.blocktank.orders( + orderIds = orderIds, + filter = null, + refresh = true, + ) + if (fetchedOrders.isNotEmpty()) { + coreService.blocktank.upsertOrderList(fetchedOrders) + createTransfersForPaidOrders(paidOrders, fetchedOrders) + } + }.onFailure { e -> + Logger.warn("Failed to fetch and process remote paid orders: $e", context = TAG) + } } private suspend fun applyRemoteTransfers(transfers: Map) {