fix: FAQ CJK glyphs, outgoing-tx history, lite encryption, migrate & sync safety
Bug-report fixes plus adversarial-audit follow-ups. Bumps full 2.0.1->2.0.2 and lite 1.1.0->1.1.1. i18n / fonts: - Rebuild the NotoSansCJK subset with the glyphs the FAQ back-fill introduced (什/么/门/做); Chinese FAQ titles rendered as "??" on zh (and ja/ko). Transaction history (enumeration gaps): - Surface outgoing shielded z->z sends via z_listsentbyaddress (listtransactions and z_listreceivedbyaddress never report them). - Surface z->t deshields by parsing z_listsentbyaddress transparentSends (which z_viewtransaction does not expose); dedupe t->t against listtransactions. - Key send-row dedup on address so equal-value multi-output sends aren't dropped. Lite wallet (key safety): - The async create/restore/open path the UI uses now applies the passphrase: encrypt a new/restored wallet, unlock an existing one. It previously discarded the passphrase, storing the seed/keys in PLAINTEXT (create/restore) or leaving an encrypted wallet locked (open). Migrate-to-seed (fund safety): - Persist a sweep-submitted marker at broadcast so a resumed migration can't treat a ~0 balance (an unconfirmed in-flight sweep) as an empty wallet and adopt (swap wallet.dat) before the sweep confirms; best-effort locate routes to the confirm gate. Sync / threading: - Gate isSynced() on a peer-derived tip (tip_known) so a peerless node isn't reported synced (Send against a stale balance); add a "no peers" status banner. - Guard daemon_status_ with a mutex (monitor-thread write vs UI-thread read). Tests: extends tests/test_phase4.cpp across all of the above; suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwycPDQSfEKtrTS2uC4JeT
This commit is contained in:
+60
-16
@@ -11,6 +11,7 @@
|
||||
#include "rpc/rpc_worker.h"
|
||||
#include "rpc/connection.h"
|
||||
#include "config/settings.h"
|
||||
#include "data/seed_migration_resume.h"
|
||||
#include "wallet/lite_wallet_controller.h"
|
||||
#include "wallet/lite_wallet_server_selection_adapter.h"
|
||||
#include "wallet/lite_rollout_policy.h"
|
||||
@@ -2299,6 +2300,8 @@ void App::renderNodeStatusBanner()
|
||||
in.connection_status = connection_status_;
|
||||
in.daemon_last_error = daemon_controller_ ? daemon_controller_->lastError() : std::string();
|
||||
in.lite_open_error = lite_open_error_;
|
||||
in.tip_known = state_.sync.tip_known;
|
||||
in.blocks = state_.sync.blocks;
|
||||
|
||||
const ui::NodeBannerState banner = ui::evaluateNodeStatusBanner(in);
|
||||
if (!banner.show) return;
|
||||
@@ -2331,6 +2334,8 @@ void App::renderNodeStatusBanner()
|
||||
title = TR("node_banner_crashed_title"); icon = ICON_MD_ERROR; break;
|
||||
case ui::NodeBannerReason::LiteOpenFailed:
|
||||
title = TR("node_banner_lite_open_failed"); icon = ICON_MD_ERROR; break;
|
||||
case ui::NodeBannerReason::NoPeers:
|
||||
title = TR("node_banner_no_peers_title"); icon = ICON_MD_CLOUD_OFF; break;
|
||||
case ui::NodeBannerReason::FullNodeOffline:
|
||||
default:
|
||||
title = TR("node_banner_offline_title"); icon = ICON_MD_CLOUD_OFF; break;
|
||||
@@ -2817,7 +2822,8 @@ void App::renderStatusBar()
|
||||
ImGui::TextDisabled("%s", statusShown.c_str());
|
||||
}
|
||||
occupiedX = statusX;
|
||||
} else if (!daemon_status_.empty() && daemon_status_.find("Error") != std::string::npos) {
|
||||
} else if (const std::string ds = getDaemonStatus();
|
||||
!ds.empty() && ds.find("Error") != std::string::npos) {
|
||||
const char* errText = TR("sb_daemon_not_found");
|
||||
float statusW = ImGui::CalcTextSize(errText).x;
|
||||
float statusX = versionX - statusW - gap;
|
||||
@@ -4314,8 +4320,33 @@ void App::renderSeedMigrationDialog()
|
||||
break;
|
||||
}
|
||||
if (seed_migration_balance_ <= DRAGONX_DEFAULT_FEE) {
|
||||
// Nothing to sweep — adopt the fresh seed wallet directly. Its wallet.dat replaces the
|
||||
// current (empty) one; adopt still moves the legacy wallet aside to a timestamped backup.
|
||||
const bool sweepSubmitted = settings_ && settings_->getSeedMigrationSweepSubmitted();
|
||||
if (!dragonx::sweepGateMayAdoptEmptyWallet(sweepSubmitted)) {
|
||||
// A sweep was already broadcast but its txid was lost (its opid didn't survive a
|
||||
// daemon/app restart). A ~0 balance is AMBIGUOUS — the sweep may still be unconfirmed
|
||||
// in the mempool and could revert on a drop/reorg — so we must NOT adopt (swap
|
||||
// wallet.dat) here. Try to locate the sweep tx to resume the confirmation gate (which
|
||||
// requires >=1 conf); until then, wait/refresh. If the sweep was dropped the balance
|
||||
// simply returns and the normal Sweep UI reappears — the user is never trapped.
|
||||
ImGui::TextWrapped("%s", TR("mig_sweep_inflight"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextColored(kMedium, "%s%s", TR("mig_checking_sweep"), ui::material::LoadingDots());
|
||||
ImGui::Spacing();
|
||||
if (ui::material::TactileButton(TR("mig_check_sweep"), ImVec2(190 * dp, 0)))
|
||||
locateSweepTxid();
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton(TR("refresh"), ImVec2(110 * dp, 0))) {
|
||||
refreshSeedMigrationBalance();
|
||||
locateSweepTxid();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ui::material::TactileButton(TR("mig_later"), ImVec2(100 * dp, 0)))
|
||||
close();
|
||||
break;
|
||||
}
|
||||
// Nothing to sweep and no sweep was ever submitted (a genuinely empty wallet) — adopt the
|
||||
// fresh seed wallet directly. Its wallet.dat replaces the current (empty) one; adopt still
|
||||
// moves the legacy wallet aside to a timestamped backup.
|
||||
ImGui::TextWrapped("%s", TR("mig_no_funds"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s%s", TR("mig_to"), seed_migration_dest_.c_str());
|
||||
@@ -5166,6 +5197,18 @@ void App::setCurrentTab(int tab) {
|
||||
setCurrentPage(kTabMap[tab]);
|
||||
}
|
||||
|
||||
std::string App::getDaemonStatus() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(daemon_status_mutex_);
|
||||
return daemon_status_; // a copy — safe to use after the lock is released
|
||||
}
|
||||
|
||||
void App::setDaemonStatus(std::string s)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(daemon_status_mutex_);
|
||||
daemon_status_ = std::move(s);
|
||||
}
|
||||
|
||||
bool App::startEmbeddedDaemon()
|
||||
{
|
||||
if (!supportsEmbeddedDaemon()) {
|
||||
@@ -5185,16 +5228,16 @@ bool App::startEmbeddedDaemon()
|
||||
// Try to extract embedded resources if available
|
||||
if (resources::hasEmbeddedResources()) {
|
||||
DEBUG_LOGF("Extracting embedded Sapling params...\n");
|
||||
daemon_status_ = TR("sb_extracting_sapling");
|
||||
setDaemonStatus(TR("sb_extracting_sapling"));
|
||||
if (!resources::extractEmbeddedResources()) {
|
||||
daemon_status_ = TR("sb_daemon_extract_failed");
|
||||
setDaemonStatus(TR("sb_daemon_extract_failed"));
|
||||
DEBUG_LOGF("[ERROR] extractEmbeddedResources() failed — disk full or permission denied?\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check again after extraction
|
||||
if (!rpc::Connection::verifySaplingParams()) {
|
||||
daemon_status_ = TR("sb_sapling_failed");
|
||||
setDaemonStatus(TR("sb_sapling_failed"));
|
||||
DEBUG_LOGF("Sapling params still not found after extraction!\n");
|
||||
DEBUG_LOGF("Expected location: %s\n", rpc::Connection::getSaplingParamsDir().c_str());
|
||||
return false;
|
||||
@@ -5211,7 +5254,7 @@ bool App::startEmbeddedDaemon()
|
||||
if (!exe_dir.empty()) {
|
||||
std::string dirErr;
|
||||
if (!util::Platform::ensureDirectory(daemon_dir, &dirErr)) {
|
||||
daemon_status_ = dirErr;
|
||||
setDaemonStatus(dirErr);
|
||||
DEBUG_LOGF("[ERROR] %s\n", dirErr.c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -5245,7 +5288,7 @@ bool App::startEmbeddedDaemon()
|
||||
if (copied && rpc::Connection::verifySaplingParams()) {
|
||||
DEBUG_LOGF("Sapling params copied from exe directory successfully\n");
|
||||
} else {
|
||||
daemon_status_ = TR("sb_sapling_not_found");
|
||||
setDaemonStatus(TR("sb_sapling_not_found"));
|
||||
DEBUG_LOGF("Sapling params not found and no embedded resources available!\n");
|
||||
DEBUG_LOGF("Expected location: %s\n", rpc::Connection::getSaplingParamsDir().c_str());
|
||||
return false;
|
||||
@@ -5262,7 +5305,7 @@ bool App::startEmbeddedDaemon()
|
||||
if (!exe_dir.empty()) {
|
||||
std::string dirErr;
|
||||
if (!util::Platform::ensureDirectory(daemon_dir, &dirErr)) {
|
||||
daemon_status_ = dirErr;
|
||||
setDaemonStatus(dirErr);
|
||||
DEBUG_LOGF("[ERROR] %s\n", dirErr.c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -5298,7 +5341,7 @@ bool App::startEmbeddedDaemon()
|
||||
if (copyFailed) {
|
||||
char buf[512];
|
||||
snprintf(buf, sizeof(buf), TR("sb_daemon_files_failed"), daemon_dir.c_str());
|
||||
daemon_status_ = buf;
|
||||
setDaemonStatus(buf);
|
||||
DEBUG_LOGF("[ERROR] One or more daemon files failed to copy to %s\n", daemon_dir.c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -5311,21 +5354,22 @@ bool App::startEmbeddedDaemon()
|
||||
|
||||
// Set up state callback
|
||||
daemon_controller_->setStateCallback([this](daemon::EmbeddedDaemon::State state, const std::string& msg) {
|
||||
// NB: this callback runs on EmbeddedDaemon's monitor thread — write via the locked setter.
|
||||
switch (state) {
|
||||
case daemon::EmbeddedDaemon::State::Starting:
|
||||
daemon_status_ = TR("sb_starting_daemon");
|
||||
setDaemonStatus(TR("sb_starting_daemon"));
|
||||
break;
|
||||
case daemon::EmbeddedDaemon::State::Running:
|
||||
daemon_status_ = TR("sb_dragonxd_running");
|
||||
setDaemonStatus(TR("sb_dragonxd_running"));
|
||||
break;
|
||||
case daemon::EmbeddedDaemon::State::Stopping:
|
||||
daemon_status_ = TR("sb_dragonxd_stopping");
|
||||
setDaemonStatus(TR("sb_dragonxd_stopping"));
|
||||
break;
|
||||
case daemon::EmbeddedDaemon::State::Stopped:
|
||||
daemon_status_ = TR("sb_dragonxd_stopped");
|
||||
setDaemonStatus(TR("sb_dragonxd_stopped"));
|
||||
break;
|
||||
case daemon::EmbeddedDaemon::State::Error:
|
||||
daemon_status_ = "Error: " + msg;
|
||||
setDaemonStatus("Error: " + msg);
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -6958,7 +7002,7 @@ std::string App::buildDiagnosticsReport()
|
||||
<< (state_.warming_up ? " (warming up)" : "") << "\n";
|
||||
|
||||
#if !DRAGONX_LITE_BUILD
|
||||
os << "daemon status: " << daemon_status_ << "\n";
|
||||
os << "daemon status: " << getDaemonStatus() << "\n";
|
||||
if (daemon_controller_) {
|
||||
os << "daemon running: " << (daemon_controller_->isRunning() ? "yes" : "no")
|
||||
<< ", crashes: " << daemon_controller_->crashCount() << "\n";
|
||||
|
||||
Reference in New Issue
Block a user