From b07e81c38d52bb30b71129e2c69633548e827c8e Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 16 Feb 2026 11:30:04 -0300 Subject: [PATCH 1/3] fix: add migration peer recovery logic --- .../to/bitkit/services/MigrationService.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/app/src/main/java/to/bitkit/services/MigrationService.kt b/app/src/main/java/to/bitkit/services/MigrationService.kt index 7456c8686..42fe920c0 100644 --- a/app/src/main/java/to/bitkit/services/MigrationService.kt +++ b/app/src/main/java/to/bitkit/services/MigrationService.kt @@ -90,6 +90,7 @@ class MigrationService @Inject constructor( private const val RN_PENDING_METADATA_KEY = "rnPendingMetadata" private const val RN_PENDING_TRANSFERS_KEY = "rnPendingTransfers" private const val RN_PENDING_BOOSTS_KEY = "rnPendingBoosts" + private const val RN_DID_ATTEMPT_PEER_RECOVERY_KEY = "rnDidAttemptMigrationPeerRecovery" private const val OPENING_CURLY_BRACE = "{" private const val MMKV_ROOT = "persist:root" private const val RN_WALLET_NAME = "wallet0" @@ -1251,6 +1252,44 @@ class MigrationService @Inject constructor( Logger.info("RN migration completed, marked for post-migration sync", context = TAG) } + private suspend fun isRnMigrationCompleted(): Boolean { + val key = stringPreferencesKey(RN_MIGRATION_COMPLETED_KEY) + return rnMigrationStore.data.first()[key] == "true" + } + + private suspend fun didAttemptPeerRecovery(): Boolean { + val key = stringPreferencesKey(RN_DID_ATTEMPT_PEER_RECOVERY_KEY) + return rnMigrationStore.data.first()[key] == "true" + } + + private suspend fun setDidAttemptPeerRecovery() { + val key = stringPreferencesKey(RN_DID_ATTEMPT_PEER_RECOVERY_KEY) + rnMigrationStore.edit { it[key] = "true" } + } + + suspend fun tryFetchMigrationPeersFromBackup(): List { + if (!isRnMigrationCompleted()) return emptyList() + if (didAttemptPeerRecovery()) return emptyList() + + setDidAttemptPeerRecovery() + + return runCatching { + val data = rnBackupClient.retrieve("peers", fileGroup = "ldk") ?: return emptyList() + val peers = json.decodeFromString>(String(data)) + if (peers.isEmpty()) return emptyList() + + val trustedIds = Env.trustedLnPeers.map { it.nodeId }.toSet() + val uris = peers + .filter { it.pubKey !in trustedIds } + .map { "${it.pubKey}@${it.address}:${it.port}" } + + Logger.info("Migration peer recovery: fetched ${uris.size} peer(s) from remote backup", context = TAG) + uris + }.onFailure { + Logger.warn("Migration peer recovery failed (will not retry)", it, context = TAG) + }.getOrDefault(emptyList()) + } + suspend fun cleanupAfterMigration() { clearPersistedMigrationData() setNeedsPostMigrationSync(false) @@ -2076,6 +2115,13 @@ data class RNWidgetsWithOptions( val widgetOptions: Map, ) +@Serializable +data class BackupPeerEntry( + val pubKey: String, + val address: String, + val port: UShort, +) + private val Context.rnMigrationDataStore: DataStore by preferencesDataStore("rn_migration") private val Context.rnKeychainDataStore: DataStore by preferencesDataStore("RN_KEYCHAIN") From de5ae97f22f0d59cc972b8b5ea35ec7f7947957b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 16 Feb 2026 11:30:28 -0300 Subject: [PATCH 2/3] fix: check migrated external peers on node start --- .../to/bitkit/repositories/LightningRepo.kt | 17 +++++++++++++++++ .../to/bitkit/repositories/LightningRepoTest.kt | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 5c0d66ab5..da9b8aba7 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -51,6 +51,7 @@ import to.bitkit.di.BgDispatcher import to.bitkit.env.Env import to.bitkit.ext.getSatsPerVByteFor import to.bitkit.ext.nowTimestamp +import to.bitkit.ext.of import to.bitkit.ext.toPeerDetailsList import to.bitkit.models.CoinSelectionPreference import to.bitkit.models.NodeLifecycleState @@ -64,6 +65,7 @@ import to.bitkit.services.LnurlChannelResponse import to.bitkit.services.LnurlService import to.bitkit.services.LnurlWithdrawResponse import to.bitkit.services.LspNotificationsService +import to.bitkit.services.MigrationService import to.bitkit.services.NodeEventHandler import to.bitkit.utils.AppError import to.bitkit.utils.Logger @@ -73,6 +75,7 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject +import javax.inject.Provider import javax.inject.Singleton import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration @@ -94,6 +97,7 @@ class LightningRepo @Inject constructor( private val preActivityMetadataRepo: PreActivityMetadataRepo, private val connectivityRepo: ConnectivityRepo, private val vssBackupClientLdk: VssBackupClientLdk, + private val migrationServiceProvider: Provider, ) { private val _lightningState = MutableStateFlow(LightningState()) val lightningState = _lightningState.asStateFlow() @@ -348,6 +352,7 @@ class LightningRepo @Inject constructor( connectToTrustedPeers().onFailure { Logger.error("Failed to connect to trusted peers", it, context = TAG) } + connectMigrationPeers() sync().onFailure { e -> Logger.warn("Initial sync failed, event-driven sync will retry", e, context = TAG) @@ -666,6 +671,18 @@ class LightningRepo @Inject constructor( runCatching { lightningService.connectToTrustedPeers() } } + private suspend fun connectMigrationPeers() { + val peerUris = migrationServiceProvider.get().tryFetchMigrationPeersFromBackup() + for (uri in peerUris) { + runCatching { + val peer = PeerDetails.of(uri) + lightningService.connectPeer(peer) + }.onFailure { + Logger.error("Failed to connect migration peer: $uri", it, context = TAG) + } + } + } + suspend fun connectPeer(peer: PeerDetails): Result = executeWhenNodeRunning("connectPeer") { lightningService.connectPeer(peer).map { syncState() diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index 014922880..3b35fb600 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -46,6 +46,7 @@ import to.bitkit.services.CoreService import to.bitkit.services.LightningService import to.bitkit.services.LnurlService import to.bitkit.services.LspNotificationsService +import to.bitkit.services.MigrationService import to.bitkit.test.BaseUnitTest import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -67,6 +68,7 @@ class LightningRepoTest : BaseUnitTest() { private val lnurlService = mock() private val connectivityRepo = mock() private val vssBackupClientLdk = mock() + private val migrationService = mock() @Before fun setUp() = runBlocking { @@ -76,6 +78,7 @@ class LightningRepoTest : BaseUnitTest() { whenever(connectivityRepo.isOnline).thenReturn(MutableStateFlow(ConnectivityState.CONNECTED)) whenever(settingsStore.data).thenReturn(flowOf(SettingsData())) whenever(lightningService.aresRequiredPeersInNetworkGraph()).thenReturn(true) + whenever(migrationService.tryFetchMigrationPeersFromBackup()).thenReturn(emptyList()) sut = LightningRepo( bgDispatcher = testDispatcher, lightningService = lightningService, @@ -89,6 +92,7 @@ class LightningRepoTest : BaseUnitTest() { preActivityMetadataRepo = preActivityMetadataRepo, connectivityRepo = connectivityRepo, vssBackupClientLdk = vssBackupClientLdk, + migrationServiceProvider = { migrationService }, ) } From bbe1f79fc35a16dee679a9316960c2d111617220 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 16 Feb 2026 12:12:49 -0300 Subject: [PATCH 3/3] refactor: move peer connect logic to WalletViewModel.kt --- .../to/bitkit/repositories/LightningRepo.kt | 17 ----------------- .../to/bitkit/viewmodels/WalletViewModel.kt | 14 ++++++++++++++ .../to/bitkit/repositories/LightningRepoTest.kt | 4 ---- .../java/to/bitkit/ui/WalletViewModelTest.kt | 1 + 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index da9b8aba7..5c0d66ab5 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -51,7 +51,6 @@ import to.bitkit.di.BgDispatcher import to.bitkit.env.Env import to.bitkit.ext.getSatsPerVByteFor import to.bitkit.ext.nowTimestamp -import to.bitkit.ext.of import to.bitkit.ext.toPeerDetailsList import to.bitkit.models.CoinSelectionPreference import to.bitkit.models.NodeLifecycleState @@ -65,7 +64,6 @@ import to.bitkit.services.LnurlChannelResponse import to.bitkit.services.LnurlService import to.bitkit.services.LnurlWithdrawResponse import to.bitkit.services.LspNotificationsService -import to.bitkit.services.MigrationService import to.bitkit.services.NodeEventHandler import to.bitkit.utils.AppError import to.bitkit.utils.Logger @@ -75,7 +73,6 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject -import javax.inject.Provider import javax.inject.Singleton import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration @@ -97,7 +94,6 @@ class LightningRepo @Inject constructor( private val preActivityMetadataRepo: PreActivityMetadataRepo, private val connectivityRepo: ConnectivityRepo, private val vssBackupClientLdk: VssBackupClientLdk, - private val migrationServiceProvider: Provider, ) { private val _lightningState = MutableStateFlow(LightningState()) val lightningState = _lightningState.asStateFlow() @@ -352,7 +348,6 @@ class LightningRepo @Inject constructor( connectToTrustedPeers().onFailure { Logger.error("Failed to connect to trusted peers", it, context = TAG) } - connectMigrationPeers() sync().onFailure { e -> Logger.warn("Initial sync failed, event-driven sync will retry", e, context = TAG) @@ -671,18 +666,6 @@ class LightningRepo @Inject constructor( runCatching { lightningService.connectToTrustedPeers() } } - private suspend fun connectMigrationPeers() { - val peerUris = migrationServiceProvider.get().tryFetchMigrationPeersFromBackup() - for (uri in peerUris) { - runCatching { - val peer = PeerDetails.of(uri) - lightningService.connectPeer(peer) - }.onFailure { - Logger.error("Failed to connect migration peer: $uri", it, context = TAG) - } - } - } - suspend fun connectPeer(peer: PeerDetails): Result = executeWhenNodeRunning("connectPeer") { lightningService.connectPeer(peer).map { syncState() diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index cd2b5ee43..4e38e5ddd 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -26,6 +26,7 @@ import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R import to.bitkit.data.SettingsStore import to.bitkit.di.BgDispatcher +import to.bitkit.ext.of import to.bitkit.models.Toast import to.bitkit.repositories.BackupRepo import to.bitkit.repositories.BlocktankRepo @@ -291,6 +292,7 @@ class WalletViewModel @Inject constructor( migrationService.consumePendingChannelMigration() } walletRepo.setWalletExistsState() + connectMigrationPeers() walletRepo.syncBalances() if (_restoreState.value.isIdle()) { walletRepo.refreshBip21() @@ -304,6 +306,18 @@ class WalletViewModel @Inject constructor( } } + private suspend fun connectMigrationPeers() { + val peerUris = migrationService.tryFetchMigrationPeersFromBackup() + for (uri in peerUris) { + runCatching { + val peer = PeerDetails.of(uri) + lightningRepo.connectPeer(peer) + }.onFailure { + Logger.error("Failed to connect migration peer: $uri", it, context = TAG) + } + } + } + fun stop() { if (!walletExists) return diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index 3b35fb600..014922880 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -46,7 +46,6 @@ import to.bitkit.services.CoreService import to.bitkit.services.LightningService import to.bitkit.services.LnurlService import to.bitkit.services.LspNotificationsService -import to.bitkit.services.MigrationService import to.bitkit.test.BaseUnitTest import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -68,7 +67,6 @@ class LightningRepoTest : BaseUnitTest() { private val lnurlService = mock() private val connectivityRepo = mock() private val vssBackupClientLdk = mock() - private val migrationService = mock() @Before fun setUp() = runBlocking { @@ -78,7 +76,6 @@ class LightningRepoTest : BaseUnitTest() { whenever(connectivityRepo.isOnline).thenReturn(MutableStateFlow(ConnectivityState.CONNECTED)) whenever(settingsStore.data).thenReturn(flowOf(SettingsData())) whenever(lightningService.aresRequiredPeersInNetworkGraph()).thenReturn(true) - whenever(migrationService.tryFetchMigrationPeersFromBackup()).thenReturn(emptyList()) sut = LightningRepo( bgDispatcher = testDispatcher, lightningService = lightningService, @@ -92,7 +89,6 @@ class LightningRepoTest : BaseUnitTest() { preActivityMetadataRepo = preActivityMetadataRepo, connectivityRepo = connectivityRepo, vssBackupClientLdk = vssBackupClientLdk, - migrationServiceProvider = { migrationService }, ) } diff --git a/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt b/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt index fdfbacb49..817350c9c 100644 --- a/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt @@ -57,6 +57,7 @@ class WalletViewModelTest : BaseUnitTest() { whenever(walletRepo.walletState).thenReturn(walletState) whenever(lightningRepo.lightningState).thenReturn(lightningState) whenever(migrationService.isMigrationChecked()).thenReturn(true) + whenever(migrationService.tryFetchMigrationPeersFromBackup()).thenReturn(emptyList()) whenever(connectivityRepo.isOnline).thenReturn(isOnline) sut = WalletViewModel(