Skip to content

Commit 60ad136

Browse files
authored
Merge pull request #1020 from synonymdev/feat/manual-rgs-wipe
feat: add network graph reset to recovery
2 parents 29a99f1 + 9cda2c3 commit 60ad136

7 files changed

Lines changed: 140 additions & 9 deletions

File tree

app/src/main/java/to/bitkit/ext/Context.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import androidx.core.app.NotificationManagerCompat
2020
import androidx.core.content.ContextCompat
2121
import androidx.core.net.toUri
2222
import to.bitkit.R
23+
import to.bitkit.androidServices.LightningNodeService
2324
import java.io.InputStream
2425

2526
// System Services
@@ -89,3 +90,15 @@ fun Context.startActivityAppSettings() {
8990
startActivity(Intent(Settings.ACTION_SETTINGS))
9091
}
9192
}
93+
94+
fun Context.relaunchApp() {
95+
// Stop the foreground node service (its onDestroy stops the LDK node) before relaunching.
96+
runCatching { stopService(Intent(this, LightningNodeService::class.java)) }
97+
98+
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
99+
?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) }
100+
if (launchIntent != null) {
101+
startActivity(launchIntent)
102+
}
103+
Runtime.getRuntime().exit(0)
104+
}

app/src/main/java/to/bitkit/repositories/LightningRepo.kt

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -361,15 +361,7 @@ class LightningRepo @Inject constructor(
361361
Logger.warn("Network graph is stale, resetting and restarting...", context = TAG)
362362

363363
lightningService.stop()
364-
lightningService.resetNetworkGraph(walletIndex)
365-
366-
runCatching {
367-
vssBackupClientLdk.setup(walletIndex).getOrThrow()
368-
vssBackupClientLdk.deleteObject("network_graph").getOrThrow()
369-
Logger.info("Cleared stale network graph from VSS (first delete)", context = TAG)
370-
}.onFailure {
371-
Logger.warn("Failed to clear graph from VSS (first delete)", it, context = TAG)
372-
}
364+
clearNetworkGraph(walletIndex)
373365

374366
_lightningState.update { it.copy(nodeLifecycleState = NodeLifecycleState.Stopped) }
375367
shouldRestartForGraphReset = true
@@ -485,6 +477,28 @@ class LightningRepo @Inject constructor(
485477
}
486478
}
487479

480+
suspend fun resetNetworkGraph(walletIndex: Int = 0): Result<Unit> = withContext(bgDispatcher) {
481+
Logger.warn("Resetting network graph (manual)", context = TAG)
482+
runCatching {
483+
if (lightningService.node != null) {
484+
lightningService.stop()
485+
}
486+
// Propagate VSS failures: a manual reset that leaves the graph in VSS is ineffective.
487+
clearNetworkGraph(walletIndex).getOrThrow()
488+
}
489+
}
490+
491+
private suspend fun clearNetworkGraph(walletIndex: Int): Result<Unit> {
492+
lightningService.resetNetworkGraph(walletIndex)
493+
return runCatching {
494+
vssBackupClientLdk.setup(walletIndex).getOrThrow()
495+
vssBackupClientLdk.deleteObject("network_graph").getOrThrow()
496+
Logger.info("Cleared network graph from VSS", context = TAG)
497+
}.onFailure {
498+
Logger.warn("Failed to clear network graph from VSS", it, context = TAG)
499+
}
500+
}
501+
488502
@Suppress("TooGenericExceptionCaught")
489503
suspend fun sync(): Result<Unit> = executeWhenNodeRunning("sync") {
490504
// If sync is in progress, mark pending and skip

app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryModeScreen.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ fun RecoveryModeScreen(
7373
recoveryViewModel.wipeWallet()
7474
},
7575
onWipeCancel = recoveryViewModel::hideWipeConfirmation,
76+
onResetGraph = recoveryViewModel::showGraphResetConfirmation,
77+
onResetGraphConfirm = recoveryViewModel::resetNetworkGraph,
78+
onResetGraphCancel = recoveryViewModel::hideGraphResetConfirmation,
7679
)
7780

7881
AnimatedVisibility(
@@ -107,6 +110,9 @@ private fun Content(
107110
onWipeApp: () -> Unit,
108111
onWipeConfirm: () -> Unit,
109112
onWipeCancel: () -> Unit,
113+
onResetGraph: () -> Unit,
114+
onResetGraphConfirm: () -> Unit,
115+
onResetGraphCancel: () -> Unit,
110116
) {
111117
Column(
112118
modifier = Modifier
@@ -147,6 +153,13 @@ private fun Content(
147153
onClick = onContactSupport,
148154
)
149155

156+
SecondaryButton(
157+
text = stringResource(R.string.security__reset_graph_button),
158+
isLoading = uiState.isResettingGraph,
159+
onClick = onResetGraph,
160+
enabled = walletExists,
161+
)
162+
150163
SecondaryButton(
151164
text = stringResource(R.string.security__wipe_app),
152165
enabled = walletExists,
@@ -166,6 +179,17 @@ private fun Content(
166179
onDismiss = onWipeCancel,
167180
)
168181
}
182+
183+
if (uiState.showGraphResetConfirmation) {
184+
AppAlertDialog(
185+
onDismissRequest = onResetGraphCancel,
186+
title = stringResource(R.string.security__reset_graph_dialog_title),
187+
text = stringResource(R.string.security__reset_graph_dialog_desc),
188+
confirmText = stringResource(R.string.security__reset_graph_confirm),
189+
onConfirm = onResetGraphConfirm,
190+
onDismiss = onResetGraphCancel,
191+
)
192+
}
169193
}
170194

171195
@Preview(showSystemUi = true)
@@ -181,6 +205,9 @@ private fun Preview() {
181205
onWipeApp = {},
182206
onWipeConfirm = {},
183207
onWipeCancel = {},
208+
onResetGraph = {},
209+
onResetGraphConfirm = {},
210+
onResetGraphCancel = {},
184211
)
185212
}
186213
}

app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package to.bitkit.ui.screens.recovery
33
import android.content.Context
44
import android.content.Intent
55
import android.net.Uri
6+
import androidx.compose.runtime.Immutable
67
import androidx.core.net.toUri
78
import androidx.lifecycle.ViewModel
89
import androidx.lifecycle.viewModelScope
910
import dagger.hilt.android.lifecycle.HiltViewModel
1011
import dagger.hilt.android.qualifiers.ApplicationContext
12+
import kotlinx.coroutines.delay
1113
import kotlinx.coroutines.flow.MutableStateFlow
1214
import kotlinx.coroutines.flow.StateFlow
1315
import kotlinx.coroutines.flow.asStateFlow
@@ -17,13 +19,15 @@ import kotlinx.coroutines.launch
1719
import to.bitkit.R
1820
import to.bitkit.data.SettingsStore
1921
import to.bitkit.env.Env
22+
import to.bitkit.ext.relaunchApp
2023
import to.bitkit.models.Toast
2124
import to.bitkit.repositories.LightningRepo
2225
import to.bitkit.repositories.LogsRepo
2326
import to.bitkit.repositories.WalletRepo
2427
import to.bitkit.ui.shared.toast.ToastEventBus
2528
import to.bitkit.utils.Logger
2629
import javax.inject.Inject
30+
import kotlin.time.Duration.Companion.seconds
2731

2832
@HiltViewModel
2933
class RecoveryViewModel @Inject constructor(
@@ -116,6 +120,42 @@ class RecoveryViewModel @Inject constructor(
116120
_uiState.update { it.copy(showWipeConfirmation = false) }
117121
}
118122

123+
fun showGraphResetConfirmation() {
124+
_uiState.update { it.copy(showGraphResetConfirmation = true) }
125+
}
126+
127+
fun hideGraphResetConfirmation() {
128+
_uiState.update { it.copy(showGraphResetConfirmation = false) }
129+
}
130+
131+
fun resetNetworkGraph() {
132+
viewModelScope.launch {
133+
_uiState.update { it.copy(isResettingGraph = true, showGraphResetConfirmation = false) }
134+
135+
lightningRepo.resetNetworkGraph().fold(
136+
onSuccess = {
137+
ToastEventBus.send(
138+
type = Toast.ToastType.SUCCESS,
139+
title = context.getString(R.string.security__reset_graph_success_title),
140+
description = context.getString(R.string.security__reset_graph_success_description),
141+
)
142+
// Keep the loading state and restart so the graph is re-downloaded on next launch.
143+
delay(RESTART_DELAY)
144+
context.relaunchApp()
145+
},
146+
onFailure = { error ->
147+
Logger.error("Failed to reset network graph", error, context = TAG)
148+
ToastEventBus.send(
149+
type = Toast.ToastType.ERROR,
150+
title = context.getString(R.string.common__error),
151+
description = context.getString(R.string.security__reset_graph_error),
152+
)
153+
_uiState.update { it.copy(isResettingGraph = false) }
154+
},
155+
)
156+
}
157+
}
158+
119159
fun wipeWallet() {
120160
viewModelScope.launch {
121161
walletRepo.wipeWallet().onFailure { error ->
@@ -181,12 +221,16 @@ class RecoveryViewModel @Inject constructor(
181221
private companion object {
182222
const val TAG = "RecoveryViewModel"
183223
private const val SUBJECT = "Bitkit Support"
224+
private val RESTART_DELAY = 5.seconds
184225
}
185226
}
186227

228+
@Immutable
187229
data class RecoveryUiState(
188230
val isExportingLogs: Boolean = false,
189231
val showWipeConfirmation: Boolean = false,
232+
val showGraphResetConfirmation: Boolean = false,
233+
val isResettingGraph: Boolean = false,
190234
val errorMessage: String? = null,
191235
val authAction: PendingAuthAction = PendingAuthAction.None,
192236
val isPinEnabled: Boolean = false,

app/src/main/res/values/strings.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,13 @@
678678
<string name="security__reset_confirm">Yes, Reset</string>
679679
<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>
680680
<string name="security__reset_dialog_title">Reset Bitkit?</string>
681+
<string name="security__reset_graph_button">Reset Network Graph</string>
682+
<string name="security__reset_graph_confirm">Yes, Reset</string>
683+
<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>
684+
<string name="security__reset_graph_dialog_title">Reset Network Graph?</string>
685+
<string name="security__reset_graph_error">Could not reset the network graph. Please try again.</string>
686+
<string name="security__reset_graph_success_description">Bitkit will restart in a few seconds to download a fresh network graph.</string>
687+
<string name="security__reset_graph_success_title">Network Graph Reset</string>
681688
<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>
682689
<string name="security__reset_title">Reset And Restore</string>
683690
<string name="security__success_bio">You have successfully set up a PIN code and {biometricsName} to improve wallet security.</string>

app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,31 @@ class LightningRepoTest : BaseUnitTest() {
175175
}
176176
}
177177

178+
@Test
179+
fun `resetNetworkGraph clears local cache and VSS copy`() = test {
180+
whenever(vssBackupClientLdk.setup(any())).thenReturn(Result.success(Unit))
181+
whenever(vssBackupClientLdk.deleteObject(any(), any())).thenReturn(Result.success(true))
182+
183+
val result = sut.resetNetworkGraph()
184+
185+
assertTrue(result.isSuccess)
186+
verify(lightningService).resetNetworkGraph(0)
187+
verify(vssBackupClientLdk).setup(0)
188+
verify(vssBackupClientLdk).deleteObject(eq("network_graph"), any())
189+
}
190+
191+
@Test
192+
fun `resetNetworkGraph fails when VSS delete fails`() = test {
193+
whenever(vssBackupClientLdk.setup(any())).thenReturn(Result.success(Unit))
194+
whenever(vssBackupClientLdk.deleteObject(any(), any()))
195+
.thenReturn(Result.failure(RuntimeException("vss unavailable")))
196+
197+
val result = sut.resetNetworkGraph()
198+
199+
assertTrue(result.isFailure)
200+
verify(lightningService).resetNetworkGraph(0)
201+
}
202+
178203
@Test
179204
fun `newAddress should fail when node is not running`() = test {
180205
val result = sut.newAddress()

changelog.d/next/1020.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Recovery mode now has a Reset Network Graph option that re-downloads the Lightning network graph to fix "route not found" errors.

0 commit comments

Comments
 (0)