diff --git a/src/app.cpp b/src/app.cpp index 43ee453..7ab136d 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1518,6 +1518,10 @@ void App::update() // C210: the "Keep in each address" consolidation run, if one is going (paced; one RPC at a time). pumpConsolidation(); + // A Settings bootstrap that finished while its dialog wasn't drawn (clicked away, minimized): process it + // here so the connect and reconcile gates release and the post-bootstrap rescan is armed. + ui::BootstrapDownloadDialog::pump(); + // After a bootstrap the preserved wallet.dat has stale spent-state and must be reconciled // against the freshly-imported chain, or the first send tries to spend already-spent notes and // is rejected. The -rescan restart can't do it (the snapshot lacks pre-snapshot blocks), so we diff --git a/src/ui/windows/bootstrap_completion.h b/src/ui/windows/bootstrap_completion.h new file mode 100644 index 0000000..0fad045 --- /dev/null +++ b/src/ui/windows/bootstrap_completion.h @@ -0,0 +1,36 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 +// +// Pure decision for what to do when the Settings bootstrap worker finishes (C221). + +#pragma once + +#include "../../util/bootstrap.h" + +namespace dragonx { +namespace ui { + +struct BootstrapCompletion { + enum class Next { Pending, Done, Failed }; + Next next = Next::Pending; + bool markRescan = false; // completed: reconcile the preserved wallet.dat against the new chain + bool releaseGate = false; // dialog not open: clear App::bootstrap_downloading_ ourselves + bool toast = false; // dialog not open (and not a user cancel): tell the user the outcome +}; + +inline BootstrapCompletion resolveBootstrapCompletion(bool workerDone, util::Bootstrap::State state, + bool dialogOpen, bool userCancelled) +{ + BootstrapCompletion c; + if (!workerDone) return c; + const bool ok = (state == util::Bootstrap::State::Completed); + c.next = ok ? BootstrapCompletion::Next::Done : BootstrapCompletion::Next::Failed; + c.markRescan = ok; + c.releaseGate = !dialogOpen; + c.toast = !dialogOpen && !userCancelled; + return c; +} + +} // namespace ui +} // namespace dragonx diff --git a/src/ui/windows/bootstrap_download_dialog.h b/src/ui/windows/bootstrap_download_dialog.h index 57f5a77..d31f4aa 100644 --- a/src/ui/windows/bootstrap_download_dialog.h +++ b/src/ui/windows/bootstrap_download_dialog.h @@ -15,6 +15,8 @@ #include "../material/type.h" #include "../material/colors.h" #include "../theme.h" +#include "../notifications.h" +#include "bootstrap_completion.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" @@ -69,6 +71,35 @@ public: else (void)s_bootstrap.release(); } + // Process a finished worker. Idempotent and safe every tick: App::update() calls it so completion never + // depends on the dialog being drawn (clicked away, window minimized); renderProgress() calls it too (the + // UI sweep returns from update() early). With the dialog open it only advances the screen (Done/Failed own + // the gate: Close / Restart daemon clear it, so the Done screen behaves as before). With the dialog + // closed nobody can press those, so release the connect/reconcile gate here and toast the outcome. + static void pump() { + if (!s_app || !s_bootstrap || s_app->isShuttingDown()) return; // quit: abortForShutdown() owns it + if (!s_bootstrap->isDone()) return; + const auto finalProg = s_bootstrap->getProgress(); + const auto c = resolveBootstrapCompletion(true, finalProg.state, s_open, s_cancelling); + if (c.markRescan) s_app->markPostBootstrapRescanPending(); + if (c.next == BootstrapCompletion::Next::Done) { + s_state = State::Done; + } else { + s_errorMsg = finalProg.error; + if (s_errorMsg.empty()) s_errorMsg = TR("grpc_bootstrap_failed"); + s_state = State::Failed; + } + s_bootstrap.reset(); + s_cancelling = false; + if (c.releaseGate) s_app->setBootstrapDownloading(false); + if (c.toast) { + if (c.next == BootstrapCompletion::Next::Done) + Notifications::instance().success(TR("bootstrap_success")); + else + Notifications::instance().error(std::string(TR("bootstrap_failed")) + ": " + s_errorMsg); + } + } + static void render() { if (!s_app) return; if (!s_open) { @@ -240,21 +271,8 @@ private: } ImGui::EndDisabled(); - // Check completion - if (s_bootstrap->isDone()) { - auto finalProg = s_bootstrap->getProgress(); - if (finalProg.state == util::Bootstrap::State::Completed) { - // Reconcile the preserved wallet.dat against the new chain once the daemon is back up. - s_app->markPostBootstrapRescanPending(); - s_state = State::Done; - } else { - s_errorMsg = finalProg.error; - if (s_errorMsg.empty()) s_errorMsg = TR("grpc_bootstrap_failed"); - s_state = State::Failed; - } - s_bootstrap.reset(); - s_cancelling = false; - } + // Check completion (shared with App::update() so a closed dialog can't strand a finished bootstrap) + pump(); } // ---- Done screen ---- diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 1060cdf..e1819e3 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -89,6 +89,7 @@ #include "util/daemon_updater.h" #include "util/http_download.h" #include "util/bootstrap.h" +#include "ui/windows/bootstrap_completion.h" #include "util/background_sampler.h" #include "util/lite_server_probe.h" #include "util/wallet_file_probe.h" @@ -17667,6 +17668,46 @@ void testWriteFileIfDifferent() removeTree(dir); } +void testBootstrapCompletion() +{ + using dragonx::ui::resolveBootstrapCompletion; + using B = dragonx::util::Bootstrap; + using N = dragonx::ui::BootstrapCompletion::Next; + + // Worker not finished: nothing happens regardless of state/dialog. + { + auto c = resolveBootstrapCompletion(false, B::State::Extracting, false, false); + EXPECT_TRUE(c.next == N::Pending); + EXPECT_TRUE(!c.markRescan && !c.releaseGate && !c.toast); + } + // Completed, dialog open: rescan, but the dialog owns the gate/toast. + { + auto c = resolveBootstrapCompletion(true, B::State::Completed, true, false); + EXPECT_TRUE(c.next == N::Done); + EXPECT_TRUE(c.markRescan); + EXPECT_TRUE(!c.releaseGate && !c.toast); + } + // Completed, dialog closed: we release the gate and toast the success. + { + auto c = resolveBootstrapCompletion(true, B::State::Completed, false, false); + EXPECT_TRUE(c.next == N::Done); + EXPECT_TRUE(c.markRescan && c.releaseGate && c.toast); + } + // Failed, dialog closed: release the gate and toast, but no rescan. + { + auto c = resolveBootstrapCompletion(true, B::State::Failed, false, false); + EXPECT_TRUE(c.next == N::Failed); + EXPECT_TRUE(!c.markRescan); + EXPECT_TRUE(c.releaseGate && c.toast); + } + // User-cancelled, dialog closed: release the gate silently (no toast, no rescan). + { + auto c = resolveBootstrapCompletion(true, B::State::Failed, false, true); + EXPECT_TRUE(c.next == N::Failed); + EXPECT_TRUE(!c.markRescan && c.releaseGate && !c.toast); + } +} + } // namespace int main() @@ -17800,6 +17841,7 @@ int main() testBootstrapPreflightKeepsOldChain(); testBootstrapUserCancelWindow(); testBootstrapQuitWait(); + testBootstrapCompletion(); testWriteFileAtomic(); testCopyFileAtomic(); testBundleCopy();