From a3fa899a7f3a2c025e22d14dc775e9f09f5d6985 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Jun 2026 08:20:24 -0300 Subject: [PATCH 1/9] feat: create shared method for clear network graph --- .../to/bitkit/repositories/LightningRepo.kt | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index e329e2b715..f9d27b23d1 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -361,15 +361,7 @@ class LightningRepo @Inject constructor( Logger.warn("Network graph is stale, resetting and restarting...", context = TAG) lightningService.stop() - lightningService.resetNetworkGraph(walletIndex) - - runCatching { - vssBackupClientLdk.setup(walletIndex).getOrThrow() - vssBackupClientLdk.deleteObject("network_graph").getOrThrow() - Logger.info("Cleared stale network graph from VSS (first delete)", context = TAG) - }.onFailure { - Logger.warn("Failed to clear graph from VSS (first delete)", it, context = TAG) - } + clearNetworkGraph(walletIndex) _lightningState.update { it.copy(nodeLifecycleState = NodeLifecycleState.Stopped) } shouldRestartForGraphReset = true @@ -485,6 +477,27 @@ class LightningRepo @Inject constructor( } } + suspend fun resetNetworkGraph(walletIndex: Int = 0): Result = withContext(bgDispatcher) { + Logger.warn("Resetting network graph (manual)", context = TAG) + runCatching { + if (lightningService.node != null) { + lightningService.stop() + } + clearNetworkGraph(walletIndex) + } + } + + private suspend fun clearNetworkGraph(walletIndex: Int) { + lightningService.resetNetworkGraph(walletIndex) + runCatching { + vssBackupClientLdk.setup(walletIndex).getOrThrow() + vssBackupClientLdk.deleteObject("network_graph").getOrThrow() + Logger.info("Cleared network graph from VSS", context = TAG) + }.onFailure { + Logger.warn("Failed to clear network graph from VSS", it, context = TAG) + } + } + @Suppress("TooGenericExceptionCaught") suspend fun sync(): Result = executeWhenNodeRunning("sync") { // If sync is in progress, mark pending and skip From dc5af95153f506c55d90335bbd43fb5fe8b94afc Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Jun 2026 08:20:44 -0300 Subject: [PATCH 2/9] test: graph clear test --- .../to/bitkit/repositories/LightningRepoTest.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index c04e983a71..ba484909a5 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -175,6 +175,19 @@ class LightningRepoTest : BaseUnitTest() { } } + @Test + fun `resetNetworkGraph clears local cache and VSS copy`() = test { + whenever(vssBackupClientLdk.setup(any())).thenReturn(Result.success(Unit)) + whenever(vssBackupClientLdk.deleteObject(any(), any())).thenReturn(Result.success(true)) + + val result = sut.resetNetworkGraph() + + assertTrue(result.isSuccess) + verify(lightningService).resetNetworkGraph(0) + verify(vssBackupClientLdk).setup(0) + verify(vssBackupClientLdk).deleteObject(eq("network_graph"), any()) + } + @Test fun `newAddress should fail when node is not running`() = test { val result = sut.newAddress() From c8c3672eb0b3f34d057ede56d54c06cb612c0e62 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Jun 2026 08:22:05 -0300 Subject: [PATCH 3/9] feat: implement graph reset button --- .../ui/screens/recovery/RecoveryModeScreen.kt | 27 +++++++++++++ .../ui/screens/recovery/RecoveryViewModel.kt | 38 +++++++++++++++++++ app/src/main/res/values/strings.xml | 7 ++++ 3 files changed, 72 insertions(+) diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryModeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryModeScreen.kt index 4051a6fdc1..16005bc1ac 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryModeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryModeScreen.kt @@ -73,6 +73,9 @@ fun RecoveryModeScreen( recoveryViewModel.wipeWallet() }, onWipeCancel = recoveryViewModel::hideWipeConfirmation, + onResetGraph = recoveryViewModel::showGraphResetConfirmation, + onResetGraphConfirm = recoveryViewModel::resetNetworkGraph, + onResetGraphCancel = recoveryViewModel::hideGraphResetConfirmation, ) AnimatedVisibility( @@ -107,6 +110,9 @@ private fun Content( onWipeApp: () -> Unit, onWipeConfirm: () -> Unit, onWipeCancel: () -> Unit, + onResetGraph: () -> Unit, + onResetGraphConfirm: () -> Unit, + onResetGraphCancel: () -> Unit, ) { Column( modifier = Modifier @@ -147,6 +153,13 @@ private fun Content( onClick = onContactSupport, ) + SecondaryButton( + text = stringResource(R.string.security__reset_graph_button), + isLoading = uiState.isResettingGraph, + onClick = onResetGraph, + enabled = walletExists, + ) + SecondaryButton( text = stringResource(R.string.security__wipe_app), enabled = walletExists, @@ -166,6 +179,17 @@ private fun Content( onDismiss = onWipeCancel, ) } + + if (uiState.showGraphResetConfirmation) { + AppAlertDialog( + onDismissRequest = onResetGraphCancel, + title = stringResource(R.string.security__reset_graph_dialog_title), + text = stringResource(R.string.security__reset_graph_dialog_desc), + confirmText = stringResource(R.string.security__reset_graph_confirm), + onConfirm = onResetGraphConfirm, + onDismiss = onResetGraphCancel, + ) + } } @Preview(showSystemUi = true) @@ -181,6 +205,9 @@ private fun Preview() { onWipeApp = {}, onWipeConfirm = {}, onWipeCancel = {}, + onResetGraph = {}, + onResetGraphConfirm = {}, + onResetGraphCancel = {}, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt index c0f19f3782..d1952fbb8b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt @@ -3,6 +3,7 @@ package to.bitkit.ui.screens.recovery import android.content.Context import android.content.Intent import android.net.Uri +import androidx.compose.runtime.Immutable import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -116,6 +117,40 @@ class RecoveryViewModel @Inject constructor( _uiState.update { it.copy(showWipeConfirmation = false) } } + fun showGraphResetConfirmation() { + _uiState.update { it.copy(showGraphResetConfirmation = true) } + } + + fun hideGraphResetConfirmation() { + _uiState.update { it.copy(showGraphResetConfirmation = false) } + } + + fun resetNetworkGraph() { + viewModelScope.launch { + _uiState.update { it.copy(isResettingGraph = true, showGraphResetConfirmation = false) } + + lightningRepo.resetNetworkGraph().fold( + onSuccess = { + ToastEventBus.send( + type = Toast.ToastType.SUCCESS, + title = context.getString(R.string.security__reset_graph_success_title), + description = context.getString(R.string.security__reset_graph_success_description), + ) + }, + onFailure = { error -> + Logger.error("Failed to reset network graph", error, context = TAG) + ToastEventBus.send( + type = Toast.ToastType.ERROR, + title = context.getString(R.string.common__error), + description = context.getString(R.string.security__reset_graph_error), + ) + }, + ) + + _uiState.update { it.copy(isResettingGraph = false) } + } + } + fun wipeWallet() { viewModelScope.launch { walletRepo.wipeWallet().onFailure { error -> @@ -184,9 +219,12 @@ class RecoveryViewModel @Inject constructor( } } +@Immutable data class RecoveryUiState( val isExportingLogs: Boolean = false, val showWipeConfirmation: Boolean = false, + val showGraphResetConfirmation: Boolean = false, + val isResettingGraph: Boolean = false, val errorMessage: String? = null, val authAction: PendingAuthAction = PendingAuthAction.None, val isPinEnabled: Boolean = false, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b5d018958e..aca7c3b270 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -678,6 +678,13 @@ Yes, Reset Are you sure you want to reset your Bitkit Wallet? Do you have a backup of your recovery phrase and wallet data? Reset Bitkit? + Reset Network Graph + Yes, Reset + This clears the cached Lightning network graph so it is downloaded again from scratch. It can fix \"route not found\" errors. Restart the app afterwards. + Reset Network Graph? + Could not reset the network graph. Please try again. + Restart the app to download a fresh network graph. + Network Graph Reset Back up your wallet first to avoid loss of your funds and wallet data. Resetting will overwrite your current Bitkit setup. Reset And Restore You have successfully set up a PIN code and {biometricsName} to improve wallet security. From c135fc996f873ddc04c4a8f1a493395fc5421cd2 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Jun 2026 08:22:14 -0300 Subject: [PATCH 4/9] doc: changelog --- changelog.d/next/1017.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/next/1017.fixed.md diff --git a/changelog.d/next/1017.fixed.md b/changelog.d/next/1017.fixed.md new file mode 100644 index 0000000000..a7fd9a71cf --- /dev/null +++ b/changelog.d/next/1017.fixed.md @@ -0,0 +1 @@ +Recovery mode now has a Reset Network Graph option that re-downloads the Lightning network graph to fix "route not found" errors. From 5516c1744b7b2db308d0397c740b713bcaeb2721 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Jun 2026 08:24:16 -0300 Subject: [PATCH 5/9] chore: rename changelog fragment --- changelog.d/next/{1017.fixed.md => 1020.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{1017.fixed.md => 1020.fixed.md} (100%) diff --git a/changelog.d/next/1017.fixed.md b/changelog.d/next/1020.fixed.md similarity index 100% rename from changelog.d/next/1017.fixed.md rename to changelog.d/next/1020.fixed.md From 26bddf9f5db722de808984bcd4a885b4a3448efe Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Jun 2026 09:14:07 -0300 Subject: [PATCH 6/9] feat: restart automatically after success --- app/src/main/java/to/bitkit/ext/Context.kt | 11 +++++++++++ .../bitkit/ui/screens/recovery/RecoveryViewModel.kt | 11 +++++++++-- app/src/main/res/values/strings.xml | 4 ++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/ext/Context.kt b/app/src/main/java/to/bitkit/ext/Context.kt index 371894157a..c93d3e8209 100644 --- a/app/src/main/java/to/bitkit/ext/Context.kt +++ b/app/src/main/java/to/bitkit/ext/Context.kt @@ -20,6 +20,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.core.net.toUri import to.bitkit.R +import to.bitkit.androidServices.LightningNodeService import java.io.InputStream // System Services @@ -89,3 +90,13 @@ fun Context.startActivityAppSettings() { startActivity(Intent(Settings.ACTION_SETTINGS)) } } + +fun Context.relaunchApp() { + // Stop the foreground node service (its onDestroy stops the LDK node) before relaunching. + runCatching { stopService(Intent(this, LightningNodeService::class.java)) } + + val launchIntent = packageManager.getLaunchIntentForPackage(packageName) + ?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) } + startActivity(launchIntent) + Runtime.getRuntime().exit(0) +} diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt index d1952fbb8b..7f6b689ea3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -18,6 +19,7 @@ import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.data.SettingsStore import to.bitkit.env.Env +import to.bitkit.ext.relaunchApp import to.bitkit.models.Toast import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LogsRepo @@ -25,6 +27,7 @@ import to.bitkit.repositories.WalletRepo import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.utils.Logger import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds @HiltViewModel class RecoveryViewModel @Inject constructor( @@ -136,6 +139,10 @@ class RecoveryViewModel @Inject constructor( title = context.getString(R.string.security__reset_graph_success_title), description = context.getString(R.string.security__reset_graph_success_description), ) + // Keep the loading state and restart so the graph is re-downloaded on next launch. + delay(RESTART_DELAY) + lightningRepo.stop() + context.relaunchApp() }, onFailure = { error -> Logger.error("Failed to reset network graph", error, context = TAG) @@ -144,10 +151,9 @@ class RecoveryViewModel @Inject constructor( title = context.getString(R.string.common__error), description = context.getString(R.string.security__reset_graph_error), ) + _uiState.update { it.copy(isResettingGraph = false) } }, ) - - _uiState.update { it.copy(isResettingGraph = false) } } } @@ -216,6 +222,7 @@ class RecoveryViewModel @Inject constructor( private companion object { const val TAG = "RecoveryViewModel" private const val SUBJECT = "Bitkit Support" + private val RESTART_DELAY = 5.seconds } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aca7c3b270..2479a03f0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -680,10 +680,10 @@ Reset Bitkit? Reset Network Graph Yes, Reset - This clears the cached Lightning network graph so it is downloaded again from scratch. It can fix \"route not found\" errors. Restart the app afterwards. + This clears the cached Lightning network graph so it is downloaded again from scratch. It can fix \"route not found\" errors. Bitkit will restart automatically. Reset Network Graph? Could not reset the network graph. Please try again. - Restart the app to download a fresh network graph. + Bitkit will restart in a few seconds to download a fresh network graph. Network Graph Reset Back up your wallet first to avoid loss of your funds and wallet data. Resetting will overwrite your current Bitkit setup. Reset And Restore From f3dfc72e55edccbbab1986d440813a7554198618 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Jun 2026 10:12:38 -0300 Subject: [PATCH 7/9] fix: launchIntent guard --- app/src/main/java/to/bitkit/ext/Context.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ext/Context.kt b/app/src/main/java/to/bitkit/ext/Context.kt index c93d3e8209..d605abad4b 100644 --- a/app/src/main/java/to/bitkit/ext/Context.kt +++ b/app/src/main/java/to/bitkit/ext/Context.kt @@ -97,6 +97,8 @@ fun Context.relaunchApp() { val launchIntent = packageManager.getLaunchIntentForPackage(packageName) ?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) } - startActivity(launchIntent) + if (launchIntent != null) { + startActivity(launchIntent) + } Runtime.getRuntime().exit(0) } From 5206cda017d4d5da1760df7e6273c231b1d934cd Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Jun 2026 10:17:15 -0300 Subject: [PATCH 8/9] fix: remove duplicated stop --- .../main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt index 7f6b689ea3..29c2124253 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt @@ -141,7 +141,6 @@ class RecoveryViewModel @Inject constructor( ) // Keep the loading state and restart so the graph is re-downloaded on next launch. delay(RESTART_DELAY) - lightningRepo.stop() context.relaunchApp() }, onFailure = { error -> From 9cda2c393fe117705ccf84876c79919a1c05a897 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 17 Jun 2026 10:18:07 -0300 Subject: [PATCH 9/9] fix: propagate vss failure --- .../java/to/bitkit/repositories/LightningRepo.kt | 7 ++++--- .../java/to/bitkit/repositories/LightningRepoTest.kt | 12 ++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index f9d27b23d1..dc4f99a7e4 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -483,13 +483,14 @@ class LightningRepo @Inject constructor( if (lightningService.node != null) { lightningService.stop() } - clearNetworkGraph(walletIndex) + // Propagate VSS failures: a manual reset that leaves the graph in VSS is ineffective. + clearNetworkGraph(walletIndex).getOrThrow() } } - private suspend fun clearNetworkGraph(walletIndex: Int) { + private suspend fun clearNetworkGraph(walletIndex: Int): Result { lightningService.resetNetworkGraph(walletIndex) - runCatching { + return runCatching { vssBackupClientLdk.setup(walletIndex).getOrThrow() vssBackupClientLdk.deleteObject("network_graph").getOrThrow() Logger.info("Cleared network graph from VSS", context = TAG) diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index ba484909a5..8b40195825 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -188,6 +188,18 @@ class LightningRepoTest : BaseUnitTest() { verify(vssBackupClientLdk).deleteObject(eq("network_graph"), any()) } + @Test + fun `resetNetworkGraph fails when VSS delete fails`() = test { + whenever(vssBackupClientLdk.setup(any())).thenReturn(Result.success(Unit)) + whenever(vssBackupClientLdk.deleteObject(any(), any())) + .thenReturn(Result.failure(RuntimeException("vss unavailable"))) + + val result = sut.resetNetworkGraph() + + assertTrue(result.isFailure) + verify(lightningService).resetNetworkGraph(0) + } + @Test fun `newAddress should fail when node is not running`() = test { val result = sut.newAddress()