diff --git a/src/app.cpp b/src/app.cpp index da60c88..d29b3bd 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -5015,7 +5015,10 @@ void App::renderSeedMigrationDialog() if (confirmed && legacyEmpty) { if (ui::material::TactileButton(TR("mig_adopt"), ImVec2(180 * dp, 0))) beginAdoptSeedWallet(); - } else if (confirmed && !legacyEmpty) { + } else if (confirmed && seed_migration_legacy_remaining_ >= 0.0 && !legacyEmpty) { + // Only claim a remainder once the legacy balance was actually measured; while a sweep is + // mined but the balance scan is still gated (node syncing), remaining stays at the -1.0 + // sentinel, so fall through to the waiting state rather than offering a sweep on an unknown. ImGui::PushStyleColor(ImGuiCol_Text, ui::material::ReadableError()); ImGui::TextWrapped("%s", TR("mig_remainder")); ImGui::PopStyleColor(); @@ -5026,7 +5029,7 @@ void App::renderSeedMigrationDialog() } ImGui::SameLine(); if (ui::material::TactileButton(TR("refresh"), ImVec2(110 * dp, 0))) - pollSweepStatus(); + pollSweepStatus(/*forceBalance=*/true); ImGui::SameLine(); if (ui::material::TactileButton(TR("mig_later"), ImVec2(100 * dp, 0))) close(); // pending state + txid persist; resumes here next time diff --git a/src/app.h b/src/app.h index 1acd56d..236425d 100644 --- a/src/app.h +++ b/src/app.h @@ -1155,7 +1155,7 @@ private: // opid the daemon no longer knows (stale) -> back to the dismissable Sweep gate (re-check balance). std::function makeSweepCompletionCallback(bool resumed); void beginSweepToSeedWallet(); // z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"] -> dest - void pollSweepStatus(); // Confirming step: poll sweep confirmations + legacy balance + void pollSweepStatus(bool forceBalance = false); // Confirming step: confs always; balance only when it can move the gate void locateSweepTxid(); // recover a lost sweep txid (send to dest) -> Confirming gate void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan void provisionChatIdentityFromSecret(std::string secret); diff --git a/src/app_network.cpp b/src/app_network.cpp index 295323d..48179ba 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -33,6 +33,7 @@ #include "rpc/rpc_client.h" #include "rpc/rpc_worker.h" #include "rpc/connection.h" +#include "services/seed_migration_poll.h" // migrationWantsBalanceScan: gate the Confirming balance scan #include "services/lowest_block_search.h" // detectLowestAvailableBlockHeight: pure search + probe classification #include "chat/chat_identity.h" // deriveChatIdentityFromSecret for HushChat identity provisioning #include "ui/windows/chat_tab.h" // ui::ResetChatTab — wipe chat UI plaintext on a wallet switch @@ -6338,30 +6339,36 @@ std::function App::makeSweepCompletionCallback(b // adopt step is gated on the tx being mined (confs >= 1) AND the legacy balance being ~0, so we // never swap wallet.dat while the funds could still bounce back (dropped/reorged tx) or while a // remainder is left behind (a sweep too big for one transaction). -void App::pollSweepStatus() +void App::pollSweepStatus(bool forceBalance) { if (capture_mode_) return; // no live ops during a UI sweep if (!rpc_ || !worker_ || !state_.connected) return; if (seed_migration_confirm_in_flight_ || seed_migration_sweep_txid_.empty()) return; seed_migration_confirm_in_flight_ = true; const std::string txid = seed_migration_sweep_txid_; - worker_->post([this, txid]() -> rpc::RPCWorker::MainCb { - int confs = 0; double remaining = -1.0; bool ok = false; + // Snapshot main-thread state for the worker - never read members off the worker thread. + const bool syncing = effectivelySyncing(); + const double priorRemaining = seed_migration_legacy_remaining_; + worker_->post([this, txid, forceBalance, syncing, priorRemaining]() -> rpc::RPCWorker::MainCb { + int confs = 0; double remaining = -1.0; bool ok = false; bool scanned = false; rpc::RPCClient::TraceScope trace("Migrate / confirm"); try { auto tx = rpc_->call("gettransaction", {txid}); if (tx.contains("confirmations") && tx["confirmations"].is_number()) confs = tx["confirmations"].get(); - auto bal = rpc_->call("z_gettotalbalance"); - if (bal.contains("total") && bal["total"].is_string()) - remaining = std::stod(bal["total"].get()); + if (services::migrationWantsBalanceScan(forceBalance, syncing, confs, priorRemaining)) { + auto bal = rpc_->call("z_gettotalbalance"); + if (bal.contains("total") && bal["total"].is_string()) + remaining = std::stod(bal["total"].get()); + scanned = true; + } ok = true; } catch (...) {} - return [this, confs, remaining, ok]() { + return [this, confs, remaining, ok, scanned]() { seed_migration_confirm_in_flight_ = false; if (ok) { seed_migration_sweep_confs_ = confs; - seed_migration_legacy_remaining_ = remaining; + if (scanned) seed_migration_legacy_remaining_ = remaining; } }; }); diff --git a/src/services/seed_migration_poll.h b/src/services/seed_migration_poll.h new file mode 100644 index 0000000..bfc96ee --- /dev/null +++ b/src/services/seed_migration_poll.h @@ -0,0 +1,20 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 + +#pragma once + +namespace dragonx { +namespace services { + +// The expensive z_gettotalbalance only changes the seed-migration adopt gate once the sweep is mined and +// we have no post-mine figure yet (priorRemaining < 0). A manual Refresh forces it; an active sync +// suppresses the automatic scan (the Refresh button still overrides). +inline bool migrationWantsBalanceScan(bool forceBalance, bool syncing, int confs, double priorRemaining) +{ + if (forceBalance) return true; + return !syncing && confs >= 1 && priorRemaining < 0.0; +} + +} // namespace services +} // namespace dragonx diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 9d33c78..2b596f4 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -44,6 +44,7 @@ #include "services/wallet_scan_governor.h" #include "services/scan_policy.h" #include "services/connect_scan_gate.h" +#include "services/seed_migration_poll.h" #include "services/chat_scan_policy.h" #include "services/mining_thread_commit.h" #include "services/network_refresh_service.h" @@ -18292,6 +18293,17 @@ void testLiteWalletExistsCache() dragonx::test::g_liteFakeWalletExists = true; // restore default for later tests } +void testMigrationWantsBalanceScan() +{ + using dragonx::services::migrationWantsBalanceScan; + EXPECT_FALSE(migrationWantsBalanceScan(false, false, 0, -1.0)); // not mined + EXPECT_TRUE(migrationWantsBalanceScan(false, false, 1, -1.0)); // mined, no figure yet + EXPECT_FALSE(migrationWantsBalanceScan(false, false, 3, 0.0)); // already scanned + EXPECT_FALSE(migrationWantsBalanceScan(false, false, 3, 2.5)); // already scanned (remainder) + EXPECT_TRUE(migrationWantsBalanceScan(true, true, 0, 0.0)); // forced overrides sync / unmined + EXPECT_FALSE(migrationWantsBalanceScan(false, true, 1, -1.0)); // syncing defers the automatic scan +} + int main() { testConnectionConfig(); @@ -18348,6 +18360,7 @@ int main() testRefreshProfileSelection(); testConnectionInfoSeeding(); testConnectScanGate(); + testMigrationWantsBalanceScan(); testScanPolicy(); testChatScanPolicy(); testChooseChatPayer();