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
3 changes: 2 additions & 1 deletion Bitkit/Components/Button/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ struct CustomButton: View {
size: size,
icon: icon,
isDisabled: effectiveIsDisabled,
isPressed: isPressed
isPressed: isPressed,
isLoading: isLoading
))
case .tertiary:
AnyView(TertiaryButtonView(
Expand Down
9 changes: 7 additions & 2 deletions Bitkit/Components/Button/SecondaryButtonView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ struct SecondaryButtonView: View {
let icon: AnyView?
let isDisabled: Bool
let isPressed: Bool
var isLoading: Bool = false

var body: some View {
HStack(spacing: 8) {
if let icon {
if let icon, !isLoading {
icon
}

if size == .small {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: textColor))
.frame(width: 20, height: 20)
} else if size == .small {
CaptionBText(title, textColor: textColor)
} else {
BodySSBText(title, textColor: textColor)
Expand Down
7 changes: 7 additions & 0 deletions Bitkit/Resources/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,13 @@
"security__reset_dialog_title" = "Reset Bitkit?";
"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?";
"security__reset_confirm" = "Yes, Reset";
"security__reset_graph_button" = "Reset Network Graph";
"security__reset_graph_confirm" = "Yes, Reset";
"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.";
"security__reset_graph_dialog_title" = "Reset Network Graph?";
"security__reset_graph_error" = "Could not reset the network graph. Please try again.";
"security__reset_graph_success_description" = "Bitkit will restart in a few seconds to download a fresh network graph.";
"security__reset_graph_success_title" = "Network Graph Reset";
"security__recovery" = "Recovery";
"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.";
"security__display_seed" = "Show Seed Phrase";
Expand Down
4 changes: 4 additions & 0 deletions Bitkit/Services/LightningService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,10 @@ extension LightningService {
node?.nodeId()
}

var hasNode: Bool {
node != nil
}

/// Use cached values to avoid blocking LDK calls on main thread
@MainActor var balances: BalanceDetails? {
cachedBalances
Expand Down
33 changes: 26 additions & 7 deletions Bitkit/ViewModels/WalletViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -873,18 +873,37 @@ class WalletViewModel: ObservableObject {
guard !legacyNetworkGraphCleanupDone else { return }
Logger.info("Running legacy network graph cleanup", context: "WalletViewModel")
do {
_ = try await VssBackupClient.shared.deleteKey("network_graph")
try await clearNetworkGraph()
} catch {
Logger.debug("VSS deleteKey(network_graph): \(error)", context: "WalletViewModel")
}
do {
try await lightningService.deleteNetworkGraph()
} catch {
Logger.debug("Local network graph cache cleanup: \(error)", context: "WalletViewModel")
Logger.debug("Legacy network graph cleanup: \(error)", context: "WalletViewModel")
}
legacyNetworkGraphCleanupDone = true
}

/// Manual recovery action: stop the node and clear the cached network graph so a fresh full
/// snapshot is downloaded on the next startup. Non-destructive to funds. Propagates failures
/// since a reset that leaves the graph in VSS is ineffective. Caller should restart afterwards.
func resetNetworkGraph() async throws {
Logger.warn("Resetting network graph (manual)", context: "WalletViewModel")
// Let any in-progress startup settle so a node assigned mid-setup isn't missed.
let settled = await waitForNodeToRun(timeoutSeconds: 5.0)
if !settled, nodeLifecycleState == .starting {
throw AppError(message: "Node still starting", debugMessage: "resetNetworkGraph aborted: startup in flight")
}
if lightningService.hasNode {
try await stopLightningNode()
}
try await clearNetworkGraph()
}
Comment thread
jvsena42 marked this conversation as resolved.

/// Clears the cached Lightning network graph: the local cache file and the VSS backup copy.
/// Shared by the legacy one-time startup cleanup, the manual recovery reset, and the LDK debug screen.
func clearNetworkGraph() async throws {
try await lightningService.deleteNetworkGraph()
_ = try await VssBackupClient.shared.deleteKey("network_graph")
Comment thread
jvsena42 marked this conversation as resolved.
Logger.info("Cleared network graph from VSS", context: "WalletViewModel")
}

/// Refreshes cache and syncs all UI state including balance
/// Use this for any event that may have changed balances or channel state
private func refreshAndSyncState() async {
Expand Down
51 changes: 51 additions & 0 deletions Bitkit/Views/Recovery/RecoveryScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ struct RecoveryScreen: View {
@State private var locked = true
@State private var showPinCheck = false
@State private var showWipeAlert = false
@State private var showResetGraphAlert = false
@State private var isResettingGraph = false
@State private var pendingAction: PendingAction?

/// Delay before restarting so the success toast is visible.
private let resetGraphRestartDelay: Duration = .seconds(3)

enum PendingAction {
case showSeed
case wipeApp
Expand All @@ -21,6 +26,14 @@ struct RecoveryScreen: View {
VStack(alignment: .leading, spacing: 0) {
NavigationBar(title: t("security__recovery"), showBackButton: false, showMenuButton: false)
.padding(.bottom, 16)
.alert(t("security__reset_graph_dialog_title"), isPresented: $showResetGraphAlert) {
Button(t("common__cancel"), role: .cancel) {}
Button(t("security__reset_graph_confirm"), role: .destructive) {
onResetGraphConfirmed()
}
} message: {
Text(t("security__reset_graph_dialog_desc"))
}

ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 0) {
Expand Down Expand Up @@ -52,6 +65,15 @@ struct RecoveryScreen: View {
onContactSupport()
}

CustomButton(
title: t("security__reset_graph_button"),
variant: .secondary,
isDisabled: locked || wallet.walletExists != true,
isLoading: isResettingGraph
) {
showResetGraphAlert = true
}
Comment thread
jvsena42 marked this conversation as resolved.

CustomButton(
title: t("security__wipe_app"),
variant: .secondary,
Expand Down Expand Up @@ -223,4 +245,33 @@ struct RecoveryScreen: View {

showWipeAlert = false
}

private func onResetGraphConfirmed() {
isResettingGraph = true

Task {
do {
try await wallet.resetNetworkGraph()

app.toast(
type: .success,
title: t("security__reset_graph_success_title"),
description: t("security__reset_graph_success_description")
)

// Keep the loading state and restart so the graph is re-downloaded on next launch.
try? await Task.sleep(for: resetGraphRestartDelay)
session.skipSplashOnce = true
session.bump()
} catch {
Logger.error("Failed to reset network graph: \(error)", context: "RecoveryScreen")
app.toast(
type: .error,
title: t("common__error"),
description: t("security__reset_graph_error")
)
isResettingGraph = false
}
}
}
}
11 changes: 1 addition & 10 deletions Bitkit/Views/Settings/LdkDebugScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,8 @@ struct LdkDebugScreen: View {
}

func deleteNetworkGraph() async {
// Delete network graph from VSS
do {
_ = try await VssBackupClient.shared.deleteKey("network_graph")
} catch {
Logger.debug("VSS deleteKey(network_graph): \(error)", context: "LdkDebugScreen")
}

// Delete local network graph cache
do {
let lightningService = LightningService.shared
try await lightningService.deleteNetworkGraph()
try await wallet.clearNetworkGraph()
app.toast(type: .success, title: "Network Graph Deleted", description: "Network graph deleted successfully")
} catch {
Logger.error("Failed to delete network graph: \(error)")
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/600.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