From 16d60a72f2230cca0610037a7a14b828c462a446 Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 27 Oct 2026 11:41:49 -0500 Subject: [PATCH] feat(wallet): one-click "switch to the built-in node and compact" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When connected to an external/standalone dragonxd (the wallet adopted a running node, so daemon_controller_ is null), Compact couldn't run. Add a path that does it in one click: compactViaEmbeddedSwitch() gates the connect loop, drops the RPC connection, and on the worker gracefully stops the external node (RPC "stop" with our credentials, via stopDaemonForWalletSwitch) and waits for the port; back on the main thread it switches the wallet to its built-in node and starts it with a one-shot compact_on_embedded_start_ flag that arms -salvagewallet, so the fresh node rewrites wallet.dat (and rescans). On stop failure nothing is changed and the external node stays up. The Compact button is now enabled whenever the build can run the embedded node; its confirmation branches — direct compact when the wallet already runs the built-in node (canCompactWallet), else a clear message (confirm_compact_switch_ msg) that it will stop the external node, switch to the built-in one, and that this changes which node binary runs. New strings confirm_compact_switch_msg / compact_switching / compact_switch_stop_failed. Mirrors restartDaemon's disconnect + background stop pattern; controller creation stays on the main thread. Can't be runtime-verified here (no live daemon). Build + tests + hygiene green. Co-Authored-By: Claude Opus 4.8 --- src/app.cpp | 8 ++++++ src/app.h | 5 ++++ src/app_network.cpp | 46 ++++++++++++++++++++++++++++++++++ src/ui/pages/settings_page.cpp | 16 ++++++++---- src/util/i18n.cpp | 3 +++ 5 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 89e19c0..cb1c13c 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -6222,6 +6222,14 @@ bool App::startEmbeddedDaemon() }); } + // Compact-via-switch: the user chose to compact after switching from an external node to this built-in + // one. Arm -salvagewallet on this start so the fresh node rewrites wallet.dat into a compact file + // (-salvagewallet implies a rescan). One-shot: it fires only for the start that follows the switch. + if (compact_on_embedded_start_.exchange(false)) { + daemon_controller_->setSalvageOnNextStart(true); + user_initiated_rescan_ = true; // the implied rescan is wallet-triggered → its completion should toast + } + // Only a process we actually launched starts a new story: a start that finds a node already running (and // launches nothing) must not wipe what the tracker knows about that node from its debug.log. const bool launched = daemon_controller_->start( diff --git a/src/app.h b/src/app.h index 7c6982b..760517e 100644 --- a/src/app.h +++ b/src/app.h @@ -810,6 +810,11 @@ public: bool canCompactWallet() const { return supportsFullNodeLifecycleActions() && isUsingEmbeddedDaemon() && daemon_controller_ != nullptr; } + // Compact when connected to an EXTERNAL node: stop it (graceful RPC "stop"), switch the wallet to its + // built-in node, and start that with -salvagewallet so it compacts. Needs a full-node build with + // embedded-daemon support. A one-shot flag arms the salvage on the resulting embedded start. + void compactViaEmbeddedSwitch(); + std::atomic compact_on_embedded_start_{false}; // A newer daemon binary is installed but the running node is still the old one until a restart. void markDaemonRestartPending() { daemon_restart_pending_.store(true); } bool daemonRestartPending() const { return daemon_restart_pending_.load(); } diff --git a/src/app_network.cpp b/src/app_network.cpp index 664efcf..5b88a5b 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -6525,6 +6525,52 @@ void App::compactWallet() daemon_controller_->setSalvageOnNextStart(false); // the restart was refused — disarm the stray salvage } +void App::compactViaEmbeddedSwitch() +{ + auto& n = ui::Notifications::instance(); + if (!supportsFullNodeLifecycleActions() || !supportsEmbeddedDaemon()) { + n.warning(TR("appx_fullnode_lifecycle_unavailable_lite")); + return; + } + if (canCompactWallet()) { compactWallet(); return; } // already on the built-in node — just compact + if (daemon_restarting_.load() || isWalletSwitchInProgress() || show_seed_migration_ || + (settings_ && settings_->getSeedMigrationPending())) { + n.warning(TR("merge_run_busy")); + return; + } + if (!worker_) return; + + // Gate the connect loop / auto-start while we swap nodes, then drop the RPC connection to the external + // node (we're about to tell it to stop). onDisconnected shows the loading overlay. + daemon_restarting_ = true; + connection_status_ = TR("compact_switching"); + if (rpc_ && rpc_->isConnected()) { abortMaintenanceCalls(); rpc_->disconnect(); } + onDisconnected("Compact: switch to the built-in node"); + n.info(TR("compact_switching"), 12.0f); + + worker_->post([this]() -> rpc::RPCWorker::MainCb { + // Graceful RPC "stop" to the external node + wait (up to ~20 s) for its RPC port to free. Blocking, + // so it runs on the worker; no RPC is needed meanwhile (we just disconnected). + const bool stopped = stopDaemonForWalletSwitch(); + return [this, stopped]() { // back on the main thread + daemon_restarting_ = false; + if (shutting_down_.load()) return; + if (!stopped) { + ui::Notifications::instance().error(TR("compact_switch_stop_failed")); + return; // the external node is still up; nothing was changed + } + // Switch the wallet to its built-in node, arm the compacting start, and bring it up. The fresh + // node starts with -salvagewallet (rewrite + rescan); the connect loop reconnects to it. (Embedded + // mode is a runtime flag that defaults on for a full-node build, so this just ensures it's set — + // after the switch our own process is the running node.) + setUseEmbeddedDaemon(true); + compact_on_embedded_start_.store(true); + ui::Notifications::instance().info(TR("compact_started")); + startEmbeddedDaemon(); + }; + }); +} + // Complementary on-disk safety net (full-node) for a wallet salvage we did NOT witness this launch — it // happened on a prior run, or under an external daemon whose startup output we never captured, so // detectWalletAutoRecovery() never fired. If the active wallet loads EMPTY while a sibling wallet file in diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 251e877..b79198b 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -2388,14 +2388,16 @@ void RenderSettingsPage(App* app) { // Compact reclaims the file space consolidation frees (BDB won't shrink in place). // It rewrites wallet.dat by restarting the node with -salvagewallet, so it only works // with the embedded node — disable it for an external dragonxd and explain on hover. - const bool canCompact = app->canCompactWallet(); - ImGui::BeginDisabled(!canCompact); + // Enabled whenever this build can run the built-in node. If connected to an external + // node, the confirmation offers to switch to the built-in one first (compactViaEmbeddedSwitch). + const bool compactPossible = app->supportsEmbeddedDaemon(); + ImGui::BeginDisabled(!compactPossible); if (material::ActionButton("##walletcompact", TR("wallet_size_compact"), ICON_MD_COMPRESS, material::ActionTier::Secondary)) s_settingsState.confirm_compact_wallet = true; ImGui::EndDisabled(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) - material::Tooltip("%s", TR(canCompact ? "tt_wallet_size_compact" : "compact_needs_embedded")); + material::Tooltip("%s", TR(compactPossible ? "tt_wallet_size_compact" : "compact_needs_embedded")); } // Row 3: folder buttons (their own row so the path gets the full width). @@ -3482,11 +3484,14 @@ void RenderSettingsPage(App* app) { // Confirm: compact wallet.dat to reclaim the space consolidation freed (rewrite via -salvagewallet, // which restarts the node and re-scans the chain). A backup of the current wallet is kept. if (s_settingsState.confirm_compact_wallet) { + // Direct compact when the wallet already runs the built-in node; otherwise the dialog explains it + // will stop the external node and switch to the built-in one first. + const bool embeddedNow = app->canCompactWallet(); if (BeginOverlayDialog(TR("confirm_compact_wallet_title"), &s_settingsState.confirm_compact_wallet, 500.0f, 0.94f)) { material::DialogWarningHeader(TR("warning"), material::WarningVec4()); ImGui::Spacing(); - ImGui::TextWrapped("%s", TR("confirm_compact_wallet_msg")); + ImGui::TextWrapped("%s", TR(embeddedNow ? "confirm_compact_wallet_msg" : "confirm_compact_switch_msg")); ImGui::Spacing(); ImGui::TextColored(material::SuccessVec4(), "%s", TR("confirm_compact_wallet_safe")); ImGui::Spacing(); @@ -3501,7 +3506,8 @@ void RenderSettingsPage(App* app) { s_settingsState.confirm_compact_wallet = false; } if (doConfirm) { - app->compactWallet(); + if (embeddedNow) app->compactWallet(); + else app->compactViaEmbeddedSwitch(); s_settingsState.confirm_compact_wallet = false; } EndOverlayDialog(); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index ee66886..50dd96a 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -886,6 +886,9 @@ void I18n::loadBuiltinEnglish() strings_["confirm_compact_wallet_safe"] = "Your funds and balance are safe \xE2\x80\x94 a timestamped backup of the current wallet is kept."; strings_["compact_wallet_confirm"] = "Compact"; strings_["compact_needs_embedded"] = "Compacting rewrites wallet.dat, which the wallet can only do with its built-in node. You're connected to a dragonxd you started yourself \xE2\x80\x94 turn on \"Use embedded dragonxd\" in Settings, or restart your dragonxd with -salvagewallet to reclaim the space."; + strings_["confirm_compact_switch_msg"] = "You're connected to a dragonxd you started yourself. To compact, the wallet will stop that node, switch to its own built-in node, and rewrite wallet.dat on it \xE2\x80\x94 restarting and re-scanning the blockchain, which can take a while. Note: the built-in node is the dragonxd in the wallet's own folder, so this changes which node you run."; + strings_["compact_switching"] = "Stopping your node and switching to the built-in one\xE2\x80\xA6"; + strings_["compact_switch_stop_failed"] = "Couldn't stop your external node \xE2\x80\x94 it's still running, so nothing was changed."; strings_["settings_debug_changed"] = "Debug categories changed \xe2\x80\x94 restart daemon to apply"; strings_["settings_auto_detected"] = "Auto-detected from DRAGONX.conf"; strings_["settings_visual_effects"] = "Visual Effects";