feat(wallet): wallet-files list + switching (P2)

Second phase of multi-wallet: list wallet files and switch the active one.

- Daemon plumbing: EmbeddedDaemon::setWalletFile -> start() passes -wallet=<name>
  (non-default only; skipped during the isolated seed-migration start), and
  DaemonController::syncSettings pushes active_wallet_file on each start.
- App::switchToWallet: persist the new active wallet, then stop + restart the
  node on -wallet=<name> (rescan only if it was never synced in this datadir).
  Reuses the seed-adopt restart coordination; WalletState clears on disconnect
  and the P1 identity-scoped caches re-key, so no previous-wallet data leaks.
  Guarded: full-node, embedded daemon, not mid-restart, no pending send.
- Wallets dialog (Settings -> Backup & Data -> "Wallets…"): a table of wallet
  files (datadir wallet*.dat + user-added folders' *.dat) with size (disk) and
  cached balance / address count / last-opened (wallet index), a current/Active
  badge, Open (switch), and Add folder. Out-of-datadir wallets show "import to
  open" (P3). Added as a sweep surface.
- Mining payout safety: non-destructive warning if the pool-mode worker looks
  like a DragonX address not in the current wallet (stale after a switch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 16:20:41 -05:00
parent 72bf59149d
commit e8a8ce68b2
11 changed files with 353 additions and 0 deletions

View File

@@ -1007,6 +1007,58 @@ void App::updateWalletIndexForActiveWallet(bool markOpened)
if (wallet_index_.upsert(e)) wallet_index_.save();
}
void App::switchToWallet(const std::string& walletFile)
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build");
return;
}
if (walletFile.empty() || !settings_) return;
if (settings_->getActiveWalletFile() == walletFile) {
ui::Notifications::instance().info("That wallet is already open.");
return;
}
if (daemon_restarting_) {
ui::Notifications::instance().warning("The node is busy restarting — try again in a moment.");
return;
}
if (!isUsingEmbeddedDaemon()) {
ui::Notifications::instance().warning("Switching wallets needs the embedded daemon — stop any external dragonxd first.");
return;
}
if (hasTransactionSendProgress()) {
ui::Notifications::instance().warning("Finish or cancel the pending send before switching wallets.");
return;
}
// Rescan only if this wallet was never synced in this datadir (freshly imported/new). One we've
// loaded here before just catches up from its recorded block on start — fast.
bool needRescan = true;
if (const auto* e = wallet_index_.find(walletFile)) needRescan = !e->syncedHere;
settings_->setActiveWalletFile(walletFile);
settings_->save();
// Same restart coordination as the seed-adopt flow: gate the main-loop reconnect, disconnect,
// then stop + restart on a background thread. syncSettings() re-reads active_wallet_file on
// start, so the daemon comes up on -wallet=<name>. WalletState clears on disconnect and the
// per-wallet caches re-key off the new identity (P1), so no previous-wallet data leaks through.
daemon_restarting_ = true;
if (rpc_ && rpc_->isConnected()) rpc_->disconnect();
onDisconnected("Switching wallet");
ui::Notifications::instance().info("Switching wallet — the node will restart…");
async_tasks_.submit("Switch wallet", [this, needRescan](const util::AsyncTaskManager::Token&) {
// Stop the node fully so it releases the datadir .lock + RPC port before we relaunch.
stopEmbeddedDaemon();
for (int i = 0; i < 60 && isEmbeddedDaemonRunning(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(500));
if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true);
if (!shutting_down_) startEmbeddedDaemon();
daemon_restarting_ = false; // re-arm reconnect once the new daemon has been launched
});
}
void App::wipePendingTransactionHistoryCachePassphrase()
{
if (!pending_transaction_history_cache_passphrase_.empty()) {