Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/src/main/java/to/bitkit/ext/Context.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -89,3 +90,15 @@ 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) }
if (launchIntent != null) {
startActivity(launchIntent)
}
Runtime.getRuntime().exit(0)
Comment thread
jvsena42 marked this conversation as resolved.
}
32 changes: 23 additions & 9 deletions app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -485,6 +477,28 @@ class LightningRepo @Inject constructor(
}
}

suspend fun resetNetworkGraph(walletIndex: Int = 0): Result<Unit> = withContext(bgDispatcher) {
Logger.warn("Resetting network graph (manual)", context = TAG)
runCatching {
if (lightningService.node != null) {
lightningService.stop()
}
// Propagate VSS failures: a manual reset that leaves the graph in VSS is ineffective.
clearNetworkGraph(walletIndex).getOrThrow()
}
}

private suspend fun clearNetworkGraph(walletIndex: Int): Result<Unit> {
lightningService.resetNetworkGraph(walletIndex)
return 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)
Comment thread
jvsena42 marked this conversation as resolved.
}
}

@Suppress("TooGenericExceptionCaught")
suspend fun sync(): Result<Unit> = executeWhenNodeRunning("sync") {
// If sync is in progress, mark pending and skip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ fun RecoveryModeScreen(
recoveryViewModel.wipeWallet()
},
onWipeCancel = recoveryViewModel::hideWipeConfirmation,
onResetGraph = recoveryViewModel::showGraphResetConfirmation,
onResetGraphConfirm = recoveryViewModel::resetNetworkGraph,
onResetGraphCancel = recoveryViewModel::hideGraphResetConfirmation,
)

AnimatedVisibility(
Expand Down Expand Up @@ -107,6 +110,9 @@ private fun Content(
onWipeApp: () -> Unit,
onWipeConfirm: () -> Unit,
onWipeCancel: () -> Unit,
onResetGraph: () -> Unit,
onResetGraphConfirm: () -> Unit,
onResetGraphCancel: () -> Unit,
) {
Column(
modifier = Modifier
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -181,6 +205,9 @@ private fun Preview() {
onWipeApp = {},
onWipeConfirm = {},
onWipeCancel = {},
onResetGraph = {},
onResetGraphConfirm = {},
onResetGraphCancel = {},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ 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
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
Expand All @@ -17,13 +19,15 @@ 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
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(
Expand Down Expand Up @@ -116,6 +120,42 @@ 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),
)
// Keep the loading state and restart so the graph is re-downloaded on next launch.
delay(RESTART_DELAY)
context.relaunchApp()
},
Comment thread
jvsena42 marked this conversation as resolved.
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 ->
Expand Down Expand Up @@ -181,12 +221,16 @@ class RecoveryViewModel @Inject constructor(
private companion object {
const val TAG = "RecoveryViewModel"
private const val SUBJECT = "Bitkit Support"
private val RESTART_DELAY = 5.seconds
}
}

@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,
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,13 @@
<string name="security__reset_confirm">Yes, Reset</string>
<string name="security__reset_dialog_desc">Are you sure you want to reset your Bitkit Wallet? Do you have a backup of your recovery phrase and wallet data?</string>
<string name="security__reset_dialog_title">Reset Bitkit?</string>
<string name="security__reset_graph_button">Reset Network Graph</string>
<string name="security__reset_graph_confirm">Yes, Reset</string>
<string name="security__reset_graph_dialog_desc">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.</string>
<string name="security__reset_graph_dialog_title">Reset Network Graph?</string>
<string name="security__reset_graph_error">Could not reset the network graph. Please try again.</string>
<string name="security__reset_graph_success_description">Bitkit will restart in a few seconds to download a fresh network graph.</string>
<string name="security__reset_graph_success_title">Network Graph Reset</string>
<string name="security__reset_text">Back up your wallet first to avoid loss of your funds and wallet data. Resetting will overwrite your current Bitkit setup.</string>
<string name="security__reset_title">Reset And Restore</string>
<string name="security__success_bio">You have successfully set up a PIN code and {biometricsName} to improve wallet security.</string>
Expand Down
25 changes: 25 additions & 0 deletions app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,31 @@ 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 `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()
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/1020.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
Loading