feat(recovery): redesign the wallet auto-recovery flow
Turns the "Daemon Error + raw log dump" moment into one calm, honest recovery
dialog plus a recovery-aware rescan screen. Presentation + orchestration only —
the file-safety logic in rebuildWalletDatabase()/restoreOriginalWallet() (source
selection, verify-before-swap, copy/rename-never-delete, .bak) is unchanged.
- One authoritative dialog with a phase machine Offer -> Working -> Done/Failed.
The duplicate in-overlay recovery card, the untranslated red "Daemon Error"
heading, and the raw daemon-log dump are gone for the recovery case (they stay
for genuine, unrelated crashes).
- Offer is a choice-cards layout: "Repair automatically" (recommended, accent-
tinted) vs "Restore original", side by side; the rare actions ("Show me the
files", "Decide later") and a plain-language "What happens to my files?" sit in
a quiet footer. When the rebuild helper is missing, it collapses to a single
Restore card — never a dead end.
- Post-repair rescan shows a calm "Finishing your wallet repair" screen with
elapsed time + the growing wallet size, instead of "RPC timeout / taking longer
than expected / restart daemon"; the daemon-crash toast is suppressed and the
detection toast is downgraded from red to info.
- Fixes a confirmed dead-end: if a repair succeeds but the restarted daemon then
crashes for a *different* reason (block index, disk, OOM), the recovery flags
now clear (in tryConnect + onConnected) so it surfaces as a normal daemon
failure instead of freezing forever on a reassuring "don't restart" screen.
- Clickable "Wallet repair available" status-bar chip for re-entry.
The same app.cpp changes HiDPI-harden the surfaces the recovery flow lives on:
the status-bar and loading-overlay hand-drawn geometry are multiplied by dpiScale
(they rendered native-size and clipped at HiDPI / font_scale>1), the loading-
overlay status text wraps instead of running off both edges, and the node-status
banner floors its height to its DPI-baked font so the title can't clip off the top.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -243,7 +243,7 @@ void App::detectWalletAutoRecovery()
|
||||
wallet_auto_recovered_ = true;
|
||||
wallet_auto_recovered_warned_ = true;
|
||||
show_wallet_recovered_dialog_ = true;
|
||||
ui::Notifications::instance().error(TR("wallet_recovered_notify"), 30.0f);
|
||||
ui::Notifications::instance().info(TR("wallet_recovered_notify"), 30.0f); // calm, not red — coins are safe
|
||||
VERBOSE_LOGF("[recovery] Daemon auto-recovered/salvaged wallet.dat — surfacing the recovery dialog\n");
|
||||
}
|
||||
|
||||
@@ -408,9 +408,12 @@ void App::tryConnect()
|
||||
// "stuck connecting" while the node silently died-and-respawned. Surface each new crash once.
|
||||
const int crashes = daemon_controller_->crashCount();
|
||||
if (crashes > daemon_last_seen_crashes_) {
|
||||
daemon_last_seen_crashes_ = crashes;
|
||||
daemon_last_seen_crashes_ = crashes; // consume it either way, so it can't toast later
|
||||
const std::string detail = daemon_controller_->lastError();
|
||||
if (!detail.empty()) {
|
||||
// Suppress the scary "dragonxd exited unexpectedly" toast during recovery: the stop after a
|
||||
// salvage, and the intentional restart-with-rescan after a repair, are both EXPECTED here and
|
||||
// owned by the recovery UI (dialog / calm rescan overlay).
|
||||
if (!detail.empty() && !wallet_auto_recovered_ && !post_recovery_rescan_) {
|
||||
connection_status_ = TR("sb_daemon_start_failed");
|
||||
ui::Notifications::instance().error(detail, 30.0f);
|
||||
}
|
||||
@@ -527,6 +530,15 @@ void App::tryConnect()
|
||||
VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt);
|
||||
|
||||
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
|
||||
// A repair completed and we restarted with a full rescan, but the fresh daemon has
|
||||
// now EXITED — a fault distinct from the wallet file (corrupt block index, disk full,
|
||||
// OOM). Drop out of the calm "finishing repair" state so this surfaces as a normal
|
||||
// daemon failure (reindex offer / crash toast / restart) instead of silently freezing
|
||||
// the reconnect loop on a reassuring "don't restart" screen with no way forward.
|
||||
if (post_recovery_rescan_) {
|
||||
post_recovery_rescan_ = false;
|
||||
wallet_auto_recovered_ = false; // repair done; the original-salvage hold is over
|
||||
}
|
||||
// If the node aborted because its BLOCK DATABASE is unreadable (a daemon-vs-chaindata
|
||||
// format mismatch after an update, or a corrupt index), crash-restarting just repeats
|
||||
// the same abort — and each attempt reloads the whole index (wasteful). Detect it once
|
||||
@@ -597,6 +609,11 @@ void App::onConnected()
|
||||
state_.daemon_initializing = false; // RPC is answering now; clear the "initializing" overlay
|
||||
daemon_wait_attempts_ = 0; // re-arm the port-busy / start-failure notifications
|
||||
connect_stall_since_ = 0.0; // connected — clear the "taking too long" clock
|
||||
// A repair's rescan finished and connected — retire the recovery session so its crash-toast/error-card
|
||||
// suppression and status-chip hold can't persist forever. (Only when we were mid-post-repair-rescan;
|
||||
// a pre-repair salvaged-daemon connect keeps the flag so the recovery dialog/chip stay available.)
|
||||
if (post_recovery_rescan_) wallet_auto_recovered_ = false;
|
||||
post_recovery_rescan_ = false; // repair's post-restart rescan is past the RPC-less phase now
|
||||
daemon_start_error_shown_ = false;
|
||||
daemon_last_seen_crashes_ = 0; // (onConnected resets the daemon's crash count too)
|
||||
connection_status_ = TR("connected");
|
||||
@@ -4575,7 +4592,12 @@ void App::restoreOriginalWallet()
|
||||
ui::Notifications::instance().warning(TR("wallet_restore_busy"));
|
||||
return;
|
||||
}
|
||||
show_wallet_recovered_dialog_ = false;
|
||||
// Keep the recovery dialog OPEN and drive it into the Working phase — it shows progress and the
|
||||
// honest outcome in place (pumpWalletRestore flips it to Done/Failed). Presentation only; every
|
||||
// file-safety step below is unchanged.
|
||||
show_wallet_recovered_dialog_ = true;
|
||||
recovery_phase_ = RecoveryPhase::Working;
|
||||
recovery_last_action_rebuild_ = false;
|
||||
{ std::lock_guard<std::mutex> lk(wallet_restore_mutex_); wallet_restore_done_ = false; }
|
||||
daemon_restarting_ = true; // gate the reconnect loop while we swap files
|
||||
connection_status_ = TR("sb_restarting_daemon");
|
||||
@@ -4684,6 +4706,19 @@ void App::pumpWalletRestore()
|
||||
if (wallet_restore_done_) { done = true; sev = wallet_restore_severity_; msg = wallet_restore_msg_; wallet_restore_done_ = false; }
|
||||
}
|
||||
if (!done) return;
|
||||
// Primary outcome channel: if the recovery dialog is still up (Working), flip it to Done/Failed in
|
||||
// place with the real result. The toast below stays as the secondary echo for a dismissed/alt-tabbed
|
||||
// user. sev 2 = failed (op discarded, nothing changed); sev 0/1 = done (1 carries a warning message).
|
||||
if (show_wallet_recovered_dialog_ && recovery_phase_ == RecoveryPhase::Working) {
|
||||
recovery_outcome_sev_ = sev;
|
||||
recovery_outcome_msg_ = msg;
|
||||
recovery_phase_ = (sev == 2) ? RecoveryPhase::Failed : RecoveryPhase::Done;
|
||||
// Clean success (sev 0) → the daemon is now restarting with a full rescan. Flag it so the loading
|
||||
// overlay shows a calm "finishing repair" screen (not the scary generic stall) until it connects.
|
||||
// NOT for sev 1 (a warning like "node didn't restart") — nothing is rescanning then, and the Done
|
||||
// dialog already shows that message. Timestamp is stamped on the first overlay frame.
|
||||
if (sev == 0) { post_recovery_rescan_ = true; post_recovery_rescan_since_ = 0.0; }
|
||||
}
|
||||
if (sev == 2) ui::Notifications::instance().error(msg, 25.0f);
|
||||
else if (sev == 1) ui::Notifications::instance().warning(msg, 20.0f);
|
||||
else ui::Notifications::instance().success(msg.empty() ? TR("wallet_restore_ok") : msg, 12.0f);
|
||||
@@ -4705,7 +4740,10 @@ static std::string findWalletRebuildHelper()
|
||||
const std::string p = d + "/" + exe;
|
||||
if (fs::exists(p, ec)) return p;
|
||||
}
|
||||
return {};
|
||||
// Not sitting next to the app/daemon — but a self-contained exe carries it embedded. Extract it on
|
||||
// demand (first-run param extraction is gated on needsParamsExtraction(), so it may never have run
|
||||
// on a machine that already had the Sapling params). Returns "" on non-embedded builds.
|
||||
return dragonx::resources::ensureWalletRebuildHelperExtracted();
|
||||
}
|
||||
|
||||
bool App::walletRebuildAvailable() const { return !findWalletRebuildHelper().empty(); }
|
||||
@@ -4724,7 +4762,11 @@ void App::rebuildWalletDatabase()
|
||||
const std::string helper = findWalletRebuildHelper();
|
||||
if (helper.empty()) { ui::Notifications::instance().error(TR("wallet_rebuild_no_helper"), 15.0f); return; }
|
||||
|
||||
show_wallet_recovered_dialog_ = false;
|
||||
// Keep the recovery dialog OPEN through the rebuild (Working → Done/Failed in place). Presentation
|
||||
// only; the run-helper → verify-before-swap → copy/rename-never-delete steps below are unchanged.
|
||||
show_wallet_recovered_dialog_ = true;
|
||||
recovery_phase_ = RecoveryPhase::Working;
|
||||
recovery_last_action_rebuild_ = true;
|
||||
{ std::lock_guard<std::mutex> lk(wallet_restore_mutex_); wallet_restore_done_ = false; }
|
||||
daemon_restarting_ = true;
|
||||
connection_status_ = TR("sb_restarting_daemon");
|
||||
|
||||
Reference in New Issue
Block a user