From b5da1f05b0cd272adc642d1c4335217ff9d06e45 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 6 Mar 2026 07:39:39 -0300 Subject: [PATCH 1/5] fix: don't override channel manager and move the check to pre start node --- .../to/bitkit/viewmodels/WalletViewModel.kt | 61 ++++++------------- 1 file changed, 17 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index e2e8f54f4..73e3486b9 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -62,7 +62,6 @@ class WalletViewModel @Inject constructor( companion object { private const val TAG = "WalletViewModel" private val TIMEOUT_RESTORE_WAIT = 30.seconds - private const val CHANNEL_RECOVERY_RESTART_DELAY_MS = 500L } val lightningState = lightningRepo.lightningState @@ -267,8 +266,13 @@ class WalletViewModel @Inject constructor( waitForRestoreIfNeeded() - val channelMigration = buildChannelMigrationIfAvailable() + val orphanedRecoveryResult = fetchOrphanedChannelMonitorsIfNeeded() + val isOrphanedRecovery = orphanedRecoveryResult != null + val channelMigration = buildChannelMigrationIfAvailable(isOrphanedRecovery) startNode(walletIndex, channelMigration) + if (orphanedRecoveryResult == true) { + migrationService.markChannelRecoveryChecked() + } } finally { isStarting = false } @@ -282,10 +286,11 @@ class WalletViewModel @Inject constructor( } ?: Logger.warn("waitForRestoreIfNeeded timeout, proceeding anyway", context = TAG) } - private fun buildChannelMigrationIfAvailable(): ChannelDataMigration? = + private fun buildChannelMigrationIfAvailable(isOrphanedRecovery: Boolean = false): ChannelDataMigration? = migrationService.peekPendingChannelMigration()?.let { migration -> ChannelDataMigration( - channelManager = migration.channelManager.map { it.toUByte() }, + // don't overwrite channel manager for orphaned recovery, we only need the monitors for the sweep + channelManager = if (isOrphanedRecovery) null else migration.channelManager.map { it.toUByte() }, channelMonitors = migration.channelMonitors.map { monitor -> monitor.map { it.toUByte() } }, ) } @@ -306,7 +311,6 @@ class WalletViewModel @Inject constructor( if (_restoreState.value.isIdle()) { walletRepo.refreshBip21() } - checkForOrphanedChannelMonitorRecovery() } .onFailure { Logger.error("Node startup error", it, context = TAG) @@ -328,48 +332,17 @@ class WalletViewModel @Inject constructor( } } - private suspend fun checkForOrphanedChannelMonitorRecovery() { - if (migrationService.isChannelRecoveryChecked()) return - - Logger.info("Running one-time channel monitor recovery check", context = TAG) + private suspend fun fetchOrphanedChannelMonitorsIfNeeded(): Boolean? { + if (migrationService.isChannelRecoveryChecked()) return null + if (migrationService.peekPendingChannelMigration() != null) return null - val allMonitorsRetrieved = runCatching { - val allRetrieved = migrationService.fetchRNRemoteLdkData() - val channelMigration = buildChannelMigrationIfAvailable() - - if (channelMigration == null) { - Logger.info("No channel monitors found on RN backup", context = TAG) - return@runCatching allRetrieved - } - - Logger.info( - "Found ${channelMigration.channelMonitors.size} monitors on RN backup, attempting recovery", - context = TAG, - ) - - lightningRepo.stop().onFailure { - Logger.error("Failed to stop node for channel recovery", it, context = TAG) - } - delay(CHANNEL_RECOVERY_RESTART_DELAY_MS) - lightningRepo.start(channelMigration = channelMigration, shouldRetry = false) - .onSuccess { - migrationService.consumePendingChannelMigration() - walletRepo.syncNodeAndWallet() - walletRepo.syncBalances() - Logger.info("Channel monitor recovery complete", context = TAG) - } - .onFailure { - Logger.error("Failed to restart node after channel recovery", it, context = TAG) - } + Logger.info("Running pre-startup channel monitor recovery check", context = TAG) - allRetrieved + return runCatching { + migrationService.fetchRNRemoteLdkData() + }.onFailure { + Logger.error("Pre-startup channel monitor fetch failed", it, context = TAG) }.getOrDefault(false) - - if (allMonitorsRetrieved) { - migrationService.markChannelRecoveryChecked() - } else { - Logger.warn("Some monitors failed to download, will retry on next startup", context = TAG) - } } fun stop() { From 8b9fa79a04759290cc9c2e65feb417819ed86f29 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 6 Mar 2026 09:20:58 -0300 Subject: [PATCH 2/5] chore: add log --- app/src/main/java/to/bitkit/services/MigrationService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/to/bitkit/services/MigrationService.kt b/app/src/main/java/to/bitkit/services/MigrationService.kt index 7f2e1c493..aaec5c61b 100644 --- a/app/src/main/java/to/bitkit/services/MigrationService.kt +++ b/app/src/main/java/to/bitkit/services/MigrationService.kt @@ -364,6 +364,7 @@ class MigrationService @Inject constructor( suspend fun markChannelRecoveryChecked() { val key = stringPreferencesKey(RN_CHANNEL_RECOVERY_CHECKED_KEY) rnMigrationStore.edit { it[key] = "true" } + Logger.info("markChannelRecoveryChecked", TAG) } suspend fun hasRNWalletData(): Boolean { From 544bc533bf12cd1e78d7c0aa0a2f823843e10ad0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 6 Mar 2026 09:59:19 -0300 Subject: [PATCH 3/5] chore: add log --- app/src/main/java/to/bitkit/services/MigrationService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/to/bitkit/services/MigrationService.kt b/app/src/main/java/to/bitkit/services/MigrationService.kt index aaec5c61b..4b2002b1e 100644 --- a/app/src/main/java/to/bitkit/services/MigrationService.kt +++ b/app/src/main/java/to/bitkit/services/MigrationService.kt @@ -1489,6 +1489,7 @@ class MigrationService @Inject constructor( } if (monitors.isNotEmpty()) { + Logger.info("Found ${monitors.size} channel monitors", TAG) pendingChannelMigration = PendingChannelMigration( channelManager = managerData, channelMonitors = monitors, From 50aff0d97028a09800fba7d810e8b6abd87a43dc Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 6 Mar 2026 10:21:29 -0300 Subject: [PATCH 4/5] fix: restart node to apply channel migration if node was already running --- .../main/java/to/bitkit/viewmodels/WalletViewModel.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 73e3486b9..253fbd86e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -61,6 +61,7 @@ class WalletViewModel @Inject constructor( ) : ViewModel() { companion object { private const val TAG = "WalletViewModel" + private const val NODE_RESTART_DELAY_MS = 500L private val TIMEOUT_RESTORE_WAIT = 30.seconds } @@ -269,7 +270,17 @@ class WalletViewModel @Inject constructor( val orphanedRecoveryResult = fetchOrphanedChannelMonitorsIfNeeded() val isOrphanedRecovery = orphanedRecoveryResult != null val channelMigration = buildChannelMigrationIfAvailable(isOrphanedRecovery) + + // If node is already running, stop it first so the migration data is applied on restart + val isNodeRunning = lightningRepo.lightningState.value.nodeLifecycleState.isRunningOrStarting() + if (channelMigration != null && isNodeRunning) { + Logger.info("Stopping running node to apply channel migration data", context = TAG) + lightningRepo.stop() + delay(NODE_RESTART_DELAY_MS) + } + startNode(walletIndex, channelMigration) + if (orphanedRecoveryResult == true) { migrationService.markChannelRecoveryChecked() } From e73fba5ff2c9b5798a3f07a2f89cda68e0bb93e7 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 6 Mar 2026 11:26:08 -0300 Subject: [PATCH 5/5] fix: restore master code and don't override channel manager on channel monitor recovery --- .../to/bitkit/viewmodels/WalletViewModel.kt | 75 ++++++++++++------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 253fbd86e..ffc4d407f 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -61,8 +61,8 @@ class WalletViewModel @Inject constructor( ) : ViewModel() { companion object { private const val TAG = "WalletViewModel" - private const val NODE_RESTART_DELAY_MS = 500L private val TIMEOUT_RESTORE_WAIT = 30.seconds + private const val CHANNEL_RECOVERY_RESTART_DELAY_MS = 500L } val lightningState = lightningRepo.lightningState @@ -267,23 +267,8 @@ class WalletViewModel @Inject constructor( waitForRestoreIfNeeded() - val orphanedRecoveryResult = fetchOrphanedChannelMonitorsIfNeeded() - val isOrphanedRecovery = orphanedRecoveryResult != null - val channelMigration = buildChannelMigrationIfAvailable(isOrphanedRecovery) - - // If node is already running, stop it first so the migration data is applied on restart - val isNodeRunning = lightningRepo.lightningState.value.nodeLifecycleState.isRunningOrStarting() - if (channelMigration != null && isNodeRunning) { - Logger.info("Stopping running node to apply channel migration data", context = TAG) - lightningRepo.stop() - delay(NODE_RESTART_DELAY_MS) - } - + val channelMigration = buildChannelMigrationIfAvailable() startNode(walletIndex, channelMigration) - - if (orphanedRecoveryResult == true) { - migrationService.markChannelRecoveryChecked() - } } finally { isStarting = false } @@ -297,11 +282,10 @@ class WalletViewModel @Inject constructor( } ?: Logger.warn("waitForRestoreIfNeeded timeout, proceeding anyway", context = TAG) } - private fun buildChannelMigrationIfAvailable(isOrphanedRecovery: Boolean = false): ChannelDataMigration? = + private fun buildChannelMigrationIfAvailable(): ChannelDataMigration? = migrationService.peekPendingChannelMigration()?.let { migration -> ChannelDataMigration( - // don't overwrite channel manager for orphaned recovery, we only need the monitors for the sweep - channelManager = if (isOrphanedRecovery) null else migration.channelManager.map { it.toUByte() }, + channelManager = migration.channelManager.map { it.toUByte() }, channelMonitors = migration.channelMonitors.map { monitor -> monitor.map { it.toUByte() } }, ) } @@ -322,6 +306,7 @@ class WalletViewModel @Inject constructor( if (_restoreState.value.isIdle()) { walletRepo.refreshBip21() } + checkForOrphanedChannelMonitorRecovery() } .onFailure { Logger.error("Node startup error", it, context = TAG) @@ -343,17 +328,51 @@ class WalletViewModel @Inject constructor( } } - private suspend fun fetchOrphanedChannelMonitorsIfNeeded(): Boolean? { - if (migrationService.isChannelRecoveryChecked()) return null - if (migrationService.peekPendingChannelMigration() != null) return null + private suspend fun checkForOrphanedChannelMonitorRecovery() { + if (migrationService.isChannelRecoveryChecked()) return - Logger.info("Running pre-startup channel monitor recovery check", context = TAG) + Logger.info("Running one-time channel monitor recovery check", context = TAG) - return runCatching { - migrationService.fetchRNRemoteLdkData() - }.onFailure { - Logger.error("Pre-startup channel monitor fetch failed", it, context = TAG) + val allMonitorsRetrieved = runCatching { + val allRetrieved = migrationService.fetchRNRemoteLdkData() + // don't overwrite channel manager, we only need the monitors for the sweep + val channelMigration = buildChannelMigrationIfAvailable()?.let { + ChannelDataMigration(channelManager = null, channelMonitors = it.channelMonitors) + } + + if (channelMigration == null) { + Logger.info("No channel monitors found on RN backup", context = TAG) + return@runCatching allRetrieved + } + + Logger.info( + "Found ${channelMigration.channelMonitors.size} monitors on RN backup, attempting recovery", + context = TAG, + ) + + lightningRepo.stop().onFailure { + Logger.error("Failed to stop node for channel recovery", it, context = TAG) + } + delay(CHANNEL_RECOVERY_RESTART_DELAY_MS) + lightningRepo.start(channelMigration = channelMigration, shouldRetry = false) + .onSuccess { + migrationService.consumePendingChannelMigration() + walletRepo.syncNodeAndWallet() + walletRepo.syncBalances() + Logger.info("Channel monitor recovery complete", context = TAG) + } + .onFailure { + Logger.error("Failed to restart node after channel recovery", it, context = TAG) + } + + allRetrieved }.getOrDefault(false) + + if (allMonitorsRetrieved) { + migrationService.markChannelRecoveryChecked() + } else { + Logger.warn("Some monitors failed to download, will retry on next startup", context = TAG) + } } fun stop() {