Skip to content

Commit 9218194

Browse files
authored
Merge pull request #600 from synonymdev/feat/manual-rgs-wipe
feat: add network graph reset to recovery
2 parents d452354 + bada900 commit 9218194

8 files changed

Lines changed: 99 additions & 20 deletions

File tree

Bitkit/Components/Button/Button.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ struct CustomButton: View {
166166
size: size,
167167
icon: icon,
168168
isDisabled: effectiveIsDisabled,
169-
isPressed: isPressed
169+
isPressed: isPressed,
170+
isLoading: isLoading
170171
))
171172
case .tertiary:
172173
AnyView(TertiaryButtonView(

Bitkit/Components/Button/SecondaryButtonView.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ struct SecondaryButtonView: View {
66
let icon: AnyView?
77
let isDisabled: Bool
88
let isPressed: Bool
9+
var isLoading: Bool = false
910

1011
var body: some View {
1112
HStack(spacing: 8) {
12-
if let icon {
13+
if let icon, !isLoading {
1314
icon
1415
}
1516

16-
if size == .small {
17+
if isLoading {
18+
ProgressView()
19+
.progressViewStyle(CircularProgressViewStyle(tint: textColor))
20+
.frame(width: 20, height: 20)
21+
} else if size == .small {
1722
CaptionBText(title, textColor: textColor)
1823
} else {
1924
BodySSBText(title, textColor: textColor)

Bitkit/Resources/Localization/en.lproj/Localizable.strings

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,13 @@
569569
"security__reset_dialog_title" = "Reset Bitkit?";
570570
"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?";
571571
"security__reset_confirm" = "Yes, Reset";
572+
"security__reset_graph_button" = "Reset Network Graph";
573+
"security__reset_graph_confirm" = "Yes, Reset";
574+
"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.";
575+
"security__reset_graph_dialog_title" = "Reset Network Graph?";
576+
"security__reset_graph_error" = "Could not reset the network graph. Please try again.";
577+
"security__reset_graph_success_description" = "Bitkit will restart in a few seconds to download a fresh network graph.";
578+
"security__reset_graph_success_title" = "Network Graph Reset";
572579
"security__recovery" = "Recovery";
573580
"security__recovery_text" = "You\'ve entered Bitkit\'s recovery mode. Here are some actions to perform when running into issues that prevent the app from fully functioning. Restart the app for a normal startup.";
574581
"security__display_seed" = "Show Seed Phrase";

Bitkit/Services/LightningService.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,10 @@ extension LightningService {
975975
node?.nodeId()
976976
}
977977

978+
var hasNode: Bool {
979+
node != nil
980+
}
981+
978982
/// Use cached values to avoid blocking LDK calls on main thread
979983
@MainActor var balances: BalanceDetails? {
980984
cachedBalances

Bitkit/ViewModels/WalletViewModel.swift

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -873,18 +873,37 @@ class WalletViewModel: ObservableObject {
873873
guard !legacyNetworkGraphCleanupDone else { return }
874874
Logger.info("Running legacy network graph cleanup", context: "WalletViewModel")
875875
do {
876-
_ = try await VssBackupClient.shared.deleteKey("network_graph")
876+
try await clearNetworkGraph()
877877
} catch {
878-
Logger.debug("VSS deleteKey(network_graph): \(error)", context: "WalletViewModel")
879-
}
880-
do {
881-
try await lightningService.deleteNetworkGraph()
882-
} catch {
883-
Logger.debug("Local network graph cache cleanup: \(error)", context: "WalletViewModel")
878+
Logger.debug("Legacy network graph cleanup: \(error)", context: "WalletViewModel")
884879
}
885880
legacyNetworkGraphCleanupDone = true
886881
}
887882

883+
/// Manual recovery action: stop the node and clear the cached network graph so a fresh full
884+
/// snapshot is downloaded on the next startup. Non-destructive to funds. Propagates failures
885+
/// since a reset that leaves the graph in VSS is ineffective. Caller should restart afterwards.
886+
func resetNetworkGraph() async throws {
887+
Logger.warn("Resetting network graph (manual)", context: "WalletViewModel")
888+
// Let any in-progress startup settle so a node assigned mid-setup isn't missed.
889+
let settled = await waitForNodeToRun(timeoutSeconds: 5.0)
890+
if !settled, nodeLifecycleState == .starting {
891+
throw AppError(message: "Node still starting", debugMessage: "resetNetworkGraph aborted: startup in flight")
892+
}
893+
if lightningService.hasNode {
894+
try await stopLightningNode()
895+
}
896+
try await clearNetworkGraph()
897+
}
898+
899+
/// Clears the cached Lightning network graph: the local cache file and the VSS backup copy.
900+
/// Shared by the legacy one-time startup cleanup, the manual recovery reset, and the LDK debug screen.
901+
func clearNetworkGraph() async throws {
902+
try await lightningService.deleteNetworkGraph()
903+
_ = try await VssBackupClient.shared.deleteKey("network_graph")
904+
Logger.info("Cleared network graph from VSS", context: "WalletViewModel")
905+
}
906+
888907
/// Refreshes cache and syncs all UI state including balance
889908
/// Use this for any event that may have changed balances or channel state
890909
private func refreshAndSyncState() async {

Bitkit/Views/Recovery/RecoveryScreen.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ struct RecoveryScreen: View {
1010
@State private var locked = true
1111
@State private var showPinCheck = false
1212
@State private var showWipeAlert = false
13+
@State private var showResetGraphAlert = false
14+
@State private var isResettingGraph = false
1315
@State private var pendingAction: PendingAction?
1416

17+
/// Delay before restarting so the success toast is visible.
18+
private let resetGraphRestartDelay: Duration = .seconds(3)
19+
1520
enum PendingAction {
1621
case showSeed
1722
case wipeApp
@@ -21,6 +26,14 @@ struct RecoveryScreen: View {
2126
VStack(alignment: .leading, spacing: 0) {
2227
NavigationBar(title: t("security__recovery"), showBackButton: false, showMenuButton: false)
2328
.padding(.bottom, 16)
29+
.alert(t("security__reset_graph_dialog_title"), isPresented: $showResetGraphAlert) {
30+
Button(t("common__cancel"), role: .cancel) {}
31+
Button(t("security__reset_graph_confirm"), role: .destructive) {
32+
onResetGraphConfirmed()
33+
}
34+
} message: {
35+
Text(t("security__reset_graph_dialog_desc"))
36+
}
2437

2538
ScrollView(showsIndicators: false) {
2639
VStack(alignment: .leading, spacing: 0) {
@@ -52,6 +65,15 @@ struct RecoveryScreen: View {
5265
onContactSupport()
5366
}
5467

68+
CustomButton(
69+
title: t("security__reset_graph_button"),
70+
variant: .secondary,
71+
isDisabled: locked || wallet.walletExists != true,
72+
isLoading: isResettingGraph
73+
) {
74+
showResetGraphAlert = true
75+
}
76+
5577
CustomButton(
5678
title: t("security__wipe_app"),
5779
variant: .secondary,
@@ -223,4 +245,33 @@ struct RecoveryScreen: View {
223245

224246
showWipeAlert = false
225247
}
248+
249+
private func onResetGraphConfirmed() {
250+
isResettingGraph = true
251+
252+
Task {
253+
do {
254+
try await wallet.resetNetworkGraph()
255+
256+
app.toast(
257+
type: .success,
258+
title: t("security__reset_graph_success_title"),
259+
description: t("security__reset_graph_success_description")
260+
)
261+
262+
// Keep the loading state and restart so the graph is re-downloaded on next launch.
263+
try? await Task.sleep(for: resetGraphRestartDelay)
264+
session.skipSplashOnce = true
265+
session.bump()
266+
} catch {
267+
Logger.error("Failed to reset network graph: \(error)", context: "RecoveryScreen")
268+
app.toast(
269+
type: .error,
270+
title: t("common__error"),
271+
description: t("security__reset_graph_error")
272+
)
273+
isResettingGraph = false
274+
}
275+
}
276+
}
226277
}

Bitkit/Views/Settings/LdkDebugScreen.swift

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,8 @@ struct LdkDebugScreen: View {
135135
}
136136

137137
func deleteNetworkGraph() async {
138-
// Delete network graph from VSS
139138
do {
140-
_ = try await VssBackupClient.shared.deleteKey("network_graph")
141-
} catch {
142-
Logger.debug("VSS deleteKey(network_graph): \(error)", context: "LdkDebugScreen")
143-
}
144-
145-
// Delete local network graph cache
146-
do {
147-
let lightningService = LightningService.shared
148-
try await lightningService.deleteNetworkGraph()
139+
try await wallet.clearNetworkGraph()
149140
app.toast(type: .success, title: "Network Graph Deleted", description: "Network graph deleted successfully")
150141
} catch {
151142
Logger.error("Failed to delete network graph: \(error)")

changelog.d/next/600.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)