fix(wallet-switch): late-fail revert, shutdown-freeze, per-wallet PIN gate

The two deferred audit MEDs + the per-wallet PIN follow-up:

- Late-init failure revert (MED): the 1.5s start grace only catches a wallet that
  fails IMMEDIATELY. A wallet that fails LATE in dragonxd init (past the grace)
  used to persist as a broken active_wallet_file. Now a switch stays
  "pending confirm" until the daemon actually connects (onConnected clears it);
  if the connect loop instead hits the crash-wedge (crashCount >= 3) while a
  switch is pending, it flags the main-thread revert (which also resets the crash
  count so the restored wallet can start).
- Shutdown freeze (MED): the switch worker's 30s daemon-stop wait now breaks
  promptly when shutdown starts, so beginShutdown's join of the switch task can't
  freeze the UI for the full 30s (shutdown stops the daemon itself). The seed-
  adopt task is intentionally left to finish (fund-safety), as before.
- Per-wallet PIN (follow-up): App::hasPinVault() (and the lock-screen path) now
  gate on the per-wallet vault presence alone, not the GLOBAL getPinEnabled flag —
  so disabling PIN on one wallet no longer suppresses another wallet's PIN
  quick-unlock after a switch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-15 15:41:03 -05:00
parent 63dcda0ad1
commit 928335dd6b
4 changed files with 26 additions and 4 deletions

View File

@@ -5126,7 +5126,10 @@ bool App::isFirstRun() const {
}
bool App::hasPinVault() const {
return vault_ && vault_->hasVault() && settings_ && settings_->getPinEnabled();
// Gate purely on the (now per-wallet) vault presence — a vault exists iff a PIN was set for THIS
// wallet. The old extra `getPinEnabled()` gate was a GLOBAL flag, so disabling PIN on one wallet
// wrongly suppressed another wallet's PIN quick-unlock after a switch.
return vault_ && vault_->hasVault();
}
bool App::debugGateRequiresAuth() const {

View File

@@ -740,6 +740,10 @@ private:
// wallet_switch_prev_file_ (settings writes stay on the main thread) so a broken wallet isn't
// persisted across restarts. daemon_restarting_ stays set until the revert re-arms the reconnect.
std::atomic<bool> wallet_switch_failed_{false};
// True from a switch until the new wallet's daemon actually connects (onConnected). If the daemon
// instead crash-wedges (a wallet that fails LATE in init, past the fast start grace), the connect
// loop flags a revert so a broken wallet still can't stick.
std::atomic<bool> wallet_switch_pending_confirm_{false};
std::string wallet_switch_prev_file_;
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast

View File

@@ -463,6 +463,14 @@ void App::tryConnect()
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
// Prevent infinite crash-restart loop
if (daemon_controller_ && daemon_controller_->crashCount() >= 3) {
if (wallet_switch_pending_confirm_.load()) {
// The just-switched-to wallet's daemon keeps crashing (e.g. a wallet that
// fails LATE in init, past the fast start grace) — revert to the previous
// wallet rather than wedging on a broken one. The main thread does the
// settings revert (processWalletSwitchRevert) + resets the crash count.
wallet_switch_pending_confirm_.store(false);
wallet_switch_failed_.store(true);
}
{ char buf[128]; snprintf(buf, sizeof(buf), TR("sb_daemon_crashed"), daemon_controller_->crashCount());
connection_status_ = buf; }
VERBOSE_LOGF("[connect #%d] Daemon crashed %d times — not restarting (use Settings > Restart Daemon to retry)\n",
@@ -496,6 +504,7 @@ void App::tryConnect()
void App::onConnected()
{
state_.connected = true;
wallet_switch_pending_confirm_.store(false); // a successful connect confirms the switch
state_.daemon_initializing = false; // RPC is answering now; clear the "initializing" overlay
daemon_wait_attempts_ = 0; // re-arm the port-busy / start-failure notifications
daemon_start_error_shown_ = false;
@@ -1056,6 +1065,7 @@ void App::switchToWallet(const std::string& walletFile)
const std::string prevWallet = settings_->getActiveWalletFile(); // for revert if the new one fails
wallet_switch_prev_file_ = prevWallet;
wallet_switch_failed_.store(false);
wallet_switch_pending_confirm_.store(true); // cleared on a successful connect (onConnected)
settings_->setActiveWalletFile(walletFile);
settings_->save();
// A prior crash-wedge shouldn't block reconnecting to the freshly-launched wallet daemon.
@@ -1091,7 +1101,9 @@ void App::switchToWallet(const std::string& walletFile)
try {
// Stop the node fully so it releases the datadir .lock + RPC port before we relaunch.
stopEmbeddedDaemon();
for (int i = 0; i < 60 && isEmbeddedDaemonRunning(); ++i)
// Break the wait promptly if shutdown starts, so beginShutdown's join() doesn't freeze the
// UI for the full 30s (shutdown then stops the daemon itself; we won't start a new one).
for (int i = 0; i < 60 && isEmbeddedDaemonRunning() && !shutting_down_; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(500));
if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true);
ok = shutting_down_ ? true : startEmbeddedDaemon();
@@ -1121,12 +1133,15 @@ void App::switchToWallet(const std::string& walletFile)
void App::processWalletSwitchRevert()
{
if (!wallet_switch_failed_.exchange(false)) return;
wallet_switch_pending_confirm_.store(false);
if (settings_ && !wallet_switch_prev_file_.empty() &&
settings_->getActiveWalletFile() != wallet_switch_prev_file_) {
settings_->setActiveWalletFile(wallet_switch_prev_file_);
settings_->save();
if (vault_) vault_->setWalletScope(wallet_switch_prev_file_);
}
// Clear the crash-wedge so the (good) previous wallet's daemon is allowed to start again.
if (daemon_controller_) daemon_controller_->resetCrashCount();
ui::Notifications::instance().error("Couldn't open that wallet — reverted to the previous one.");
daemon_restarting_ = false; // the connect loop now restarts the previous wallet's daemon
}

View File

@@ -899,8 +899,8 @@ void App::renderLockScreen() {
cy += captionFont->LegacySize + 12.0f * dp;
}
// Check if PIN vault is available
bool hasPinVault = vault_ && vault_->hasVault() && settings_ && settings_->getPinEnabled();
// Check if PIN vault is available (per-wallet vault presence; not the global getPinEnabled flag).
bool hasPinVault = vault_ && vault_->hasVault();
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists
if (hasPinVault) {