fix: large-wallet sync starvation + shutdown/console-flash UX (Windows full node) #2
33
src/app.cpp
33
src/app.cpp
@@ -878,6 +878,9 @@ void App::update()
|
|||||||
// (a prior/unwitnessed salvage likely moved the coins into a wallet.<ts>.bak).
|
// (a prior/unwitnessed salvage likely moved the coins into a wallet.<ts>.bak).
|
||||||
maybeWarnEmptyWalletWithFundedSiblings();
|
maybeWarnEmptyWalletWithFundedSiblings();
|
||||||
|
|
||||||
|
// One-time nudge if wallet.dat has bloated past the threshold (toast + clickable alert → consolidate).
|
||||||
|
maybeWarnLargeWallet();
|
||||||
|
|
||||||
// Classify the wallet's mnemonic status (once per connect) so the Migrate-to-seed button can
|
// Classify the wallet's mnemonic status (once per connect) so the Migrate-to-seed button can
|
||||||
// glow for a legacy, pre-seed-phrase wallet.
|
// glow for a legacy, pre-seed-phrase wallet.
|
||||||
probeWalletSeedStatus();
|
probeWalletSeedStatus();
|
||||||
@@ -2421,10 +2424,19 @@ void App::renderAlertHistoryPanel()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scrollable list, newest first. Height adapts to the entry count but caps so a busy session
|
// Scrollable list, newest first. Measure the TRUE content height so wrapped (multi-line) messages
|
||||||
// scrolls inside the panel instead of blowing past the popup's max height.
|
// and optional action links aren't clipped by an under-estimate; cap so a busy session scrolls
|
||||||
const float perEntry = txtF->LegacySize * 2.0f + 14.0f * dp; // message line + time line + spacing
|
// inside the panel instead of blowing past the popup's max height.
|
||||||
const float listH = std::min(300.0f * dp, static_cast<float>(hist.size()) * perEntry);
|
const float msgWrapW = std::max(40.0f * dp, innerW - 2.0f * padX - icoF->LegacySize - 6.0f * dp);
|
||||||
|
float contentH = 0.0f;
|
||||||
|
for (const auto& a : hist) {
|
||||||
|
const float msgH = txtF->CalcTextSizeA(txtF->LegacySize, FLT_MAX, msgWrapW, a.message.c_str()).y;
|
||||||
|
contentH += std::max(msgH, static_cast<float>(icoF->LegacySize)); // icon + wrapped message
|
||||||
|
contentH += txtF->LegacySize; // relative-age line
|
||||||
|
if (a.onClick && !a.actionHint.empty()) contentH += txtF->LegacySize; // action-link line
|
||||||
|
contentH += 8.0f * dp; // inter-entry spacing
|
||||||
|
}
|
||||||
|
const float listH = std::min(300.0f * dp, contentH);
|
||||||
ImGui::BeginChild("##AlertRows", ImVec2(0, listH), false);
|
ImGui::BeginChild("##AlertRows", ImVec2(0, listH), false);
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
for (auto it = hist.rbegin(); it != hist.rend(); ++it, ++idx) {
|
for (auto it = hist.rbegin(); it != hist.rend(); ++it, ++idx) {
|
||||||
@@ -2452,6 +2464,19 @@ void App::renderAlertHistoryPanel()
|
|||||||
ImGui::TextWrapped("%s", a.message.c_str());
|
ImGui::TextWrapped("%s", a.message.c_str());
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
|
// Optional clickable action (accent link), directly under the message so it stays prominent.
|
||||||
|
if (a.onClick && !a.actionHint.empty()) {
|
||||||
|
ImGui::SetCursorPosX(padX + icoF->LegacySize + 6.0f * dp);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, m::Primary());
|
||||||
|
ImGui::TextUnformatted(a.actionHint.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
const ImVec2 lmn = ImGui::GetItemRectMin(), lmx = ImGui::GetItemRectMax();
|
||||||
|
ImGui::GetWindowDrawList()->AddLine(ImVec2(lmn.x, lmx.y), ImVec2(lmx.x, lmx.y), m::Primary());
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemClicked()) { a.onClick(); ImGui::CloseCurrentPopup(); }
|
||||||
|
}
|
||||||
// Relative age, dim, indented under the message.
|
// Relative age, dim, indented under the message.
|
||||||
ImGui::SetCursorPosX(padX + icoF->LegacySize + 6.0f * dp);
|
ImGui::SetCursorPosX(padX + icoF->LegacySize + 6.0f * dp);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, m::OnSurfaceDisabled());
|
ImGui::PushStyleColor(ImGuiCol_Text, m::OnSurfaceDisabled());
|
||||||
|
|||||||
@@ -817,6 +817,7 @@ private:
|
|||||||
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
|
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
|
||||||
void maybeRemindSeedBackup();
|
void maybeRemindSeedBackup();
|
||||||
void maybeWarnEmptyWalletWithFundedSiblings(); // full-node: empty active wallet + a funded sibling → warn once
|
void maybeWarnEmptyWalletWithFundedSiblings(); // full-node: empty active wallet + a funded sibling → warn once
|
||||||
|
void maybeWarnLargeWallet(); // full-node: wallet.dat past bloat threshold → one-time toast + clickable alert
|
||||||
void scanFundedSiblingsAsync(); // off-UI-thread probe of sibling wallet files
|
void scanFundedSiblingsAsync(); // off-UI-thread probe of sibling wallet files
|
||||||
static void scrubAndRemoveExport(const std::string& path); // zero + delete a plaintext key export (H-02)
|
static void scrubAndRemoveExport(const std::string& path); // zero + delete a plaintext key export (H-02)
|
||||||
void sweepStaleDecryptExports(); // startup net: purge stale obsidiandecryptexport* files (H-02)
|
void sweepStaleDecryptExports(); // startup net: purge stale obsidiandecryptexport* files (H-02)
|
||||||
@@ -1007,6 +1008,7 @@ private:
|
|||||||
bool seed_backup_loading_ = false;
|
bool seed_backup_loading_ = false;
|
||||||
bool seed_backup_no_mnemonic_ = false;
|
bool seed_backup_no_mnemonic_ = false;
|
||||||
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
|
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
|
||||||
|
bool large_wallet_checked_ = false; // gate: stat wallet.dat for the bloat nudge once per launch
|
||||||
|
|
||||||
// Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed
|
// Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed
|
||||||
// once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a
|
// once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
#include "rpc/connection.h"
|
#include "rpc/connection.h"
|
||||||
#include "chat/chat_identity.h" // deriveChatIdentityFromSecret for HushChat identity provisioning
|
#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
|
#include "ui/windows/chat_tab.h" // ui::ResetChatTab — wipe chat UI plaintext on a wallet switch
|
||||||
|
#include "ui/windows/shield_dialog.h" // ui::ShieldDialog — Merge to Address shortcut from the bloat nudge
|
||||||
#include "ui/windows/mining_pool_panel.h" // ui::resolveMiningUserAddress
|
#include "ui/windows/mining_pool_panel.h" // ui::resolveMiningUserAddress
|
||||||
#include <sodium.h> // sodium_memzero for wiping the fetched mnemonic
|
#include <sodium.h> // sodium_memzero for wiping the fetched mnemonic
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -4232,6 +4233,36 @@ void App::maybeRemindSeedBackup()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One-time nudge (full-node) when the BDB wallet.dat has bloated past the threshold. Berkeley DB never
|
||||||
|
// shrinks in place and shielded-note witness data accumulates, so a mining/shielded wallet can grow
|
||||||
|
// unbounded. Fires ONCE (persisted flag) a warning toast + a clickable "Consolidate notes…" entry in the
|
||||||
|
// bell/alert panel that opens Merge to Address; re-arms if the file later drops back under the threshold.
|
||||||
|
void App::maybeWarnLargeWallet()
|
||||||
|
{
|
||||||
|
if (capture_mode_ || lite_wallet_) return; // no live nags during a UI sweep; lite has no wallet.dat
|
||||||
|
if (!supportsFullNodeLifecycleActions() || !settings_) return;
|
||||||
|
if (!state_.connected || !state_.encryption_state_known) return;
|
||||||
|
if (state_.warming_up || state_.daemon_initializing || !state_.sync.isSynced()) return;
|
||||||
|
if (large_wallet_checked_) return; // stat wallet.dat at most once per launch
|
||||||
|
large_wallet_checked_ = true;
|
||||||
|
|
||||||
|
static constexpr uint64_t kWalletBloatWarnBytes = 500ull * 1024 * 1024; // 500 MB (matches the Settings banner)
|
||||||
|
const std::string walletPath = util::Platform::getDragonXDataDir() + "/wallet.dat";
|
||||||
|
const uint64_t sz = util::Platform::getFileSize(walletPath);
|
||||||
|
if (sz <= kWalletBloatWarnBytes) {
|
||||||
|
// Re-arm the one-time warning if the file shrank back under the threshold (e.g. after a fresh seed wallet).
|
||||||
|
if (settings_->getLargeWalletWarned()) { settings_->setLargeWalletWarned(false); settings_->save(); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (settings_->getLargeWalletWarned()) return; // already warned once for this bloat episode
|
||||||
|
settings_->setLargeWalletWarned(true);
|
||||||
|
settings_->save();
|
||||||
|
ui::Notifications::instance().action(
|
||||||
|
TR("wallet_size_warn"), ui::NotificationType::Warning,
|
||||||
|
[]() { ui::ShieldDialog::show(ui::ShieldDialog::Mode::MergeToAddress); },
|
||||||
|
TR("wallet_size_consolidate"), 12.0f);
|
||||||
|
}
|
||||||
|
|
||||||
// Complementary on-disk safety net (full-node) for a wallet salvage we did NOT witness this launch — it
|
// 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
|
// 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
|
// detectWalletAutoRecovery() never fired. If the active wallet loads EMPTY while a sibling wallet file in
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ bool Settings::load(const std::string& path)
|
|||||||
}
|
}
|
||||||
loadScalar(j, "wizard_completed", wizard_completed_);
|
loadScalar(j, "wizard_completed", wizard_completed_);
|
||||||
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
|
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
|
||||||
|
loadScalar(j, "large_wallet_warned", large_wallet_warned_);
|
||||||
if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) {
|
if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) {
|
||||||
empty_wallet_warning_acked_.clear();
|
empty_wallet_warning_acked_.clear();
|
||||||
for (const auto& w : j["empty_wallet_warning_acked"])
|
for (const auto& w : j["empty_wallet_warning_acked"])
|
||||||
@@ -506,6 +507,7 @@ bool Settings::save(const std::string& path)
|
|||||||
}
|
}
|
||||||
j["wizard_completed"] = wizard_completed_;
|
j["wizard_completed"] = wizard_completed_;
|
||||||
j["seed_backup_reminded"] = seed_backup_reminded_;
|
j["seed_backup_reminded"] = seed_backup_reminded_;
|
||||||
|
j["large_wallet_warned"] = large_wallet_warned_;
|
||||||
j["empty_wallet_warning_acked"] = json::array();
|
j["empty_wallet_warning_acked"] = json::array();
|
||||||
for (const auto& w : empty_wallet_warning_acked_)
|
for (const auto& w : empty_wallet_warning_acked_)
|
||||||
j["empty_wallet_warning_acked"].push_back(w);
|
j["empty_wallet_warning_acked"].push_back(w);
|
||||||
|
|||||||
@@ -330,6 +330,10 @@ public:
|
|||||||
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
|
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
|
||||||
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
|
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
|
||||||
|
|
||||||
|
// One-time nudge when wallet.dat grows past the bloat threshold (re-armed if it shrinks back).
|
||||||
|
bool getLargeWalletWarned() const { return large_wallet_warned_; }
|
||||||
|
void setLargeWalletWarned(bool v) { large_wallet_warned_ = v; }
|
||||||
|
|
||||||
// Wallet filenames for which the one-time "this wallet is empty but a sibling holds funds"
|
// Wallet filenames for which the one-time "this wallet is empty but a sibling holds funds"
|
||||||
// warning has been dismissed. Keyed per active wallet file so switching to a different empty
|
// warning has been dismissed. Keyed per active wallet file so switching to a different empty
|
||||||
// wallet can warn again (see App::maybeWarnEmptyWalletWithFundedSiblings).
|
// wallet can warn again (see App::maybeWarnEmptyWalletWithFundedSiblings).
|
||||||
@@ -597,6 +601,7 @@ private:
|
|||||||
std::map<std::string, AddressMeta> address_meta_;
|
std::map<std::string, AddressMeta> address_meta_;
|
||||||
bool wizard_completed_ = false;
|
bool wizard_completed_ = false;
|
||||||
bool seed_backup_reminded_ = false;
|
bool seed_backup_reminded_ = false;
|
||||||
|
bool large_wallet_warned_ = false;
|
||||||
std::set<std::string> empty_wallet_warning_acked_; // wallet files whose empty-wallet warning was dismissed
|
std::set<std::string> empty_wallet_warning_acked_; // wallet files whose empty-wallet warning was dismissed
|
||||||
bool encryption_pending_ = false;
|
bool encryption_pending_ = false;
|
||||||
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
|
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ struct AlertRecord {
|
|||||||
std::string message;
|
std::string message;
|
||||||
NotificationType type;
|
NotificationType type;
|
||||||
std::int64_t epoch; // std::time(nullptr) at push — wall-clock, for relative-age display
|
std::int64_t epoch; // std::time(nullptr) at push — wall-clock, for relative-age display
|
||||||
|
std::function<void()> onClick; // optional: makes this bell-panel entry actionable
|
||||||
|
std::string actionHint; // optional: accent link label rendered for the action
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Notification {
|
struct Notification {
|
||||||
@@ -93,14 +95,24 @@ public:
|
|||||||
push(message, NotificationType::Error, duration);
|
push(message, NotificationType::Error, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
void push(const std::string& message, NotificationType type, float duration = 5.0f) {
|
// An actionable alert: a normal toast PLUS a clickable entry in the bell/alert-history panel.
|
||||||
|
// onClick fires when the user clicks the accent `actionHint` link in that panel.
|
||||||
|
void action(const std::string& message, NotificationType type, std::function<void()> onClick,
|
||||||
|
const std::string& actionHint, float duration = -1.0f) {
|
||||||
|
if (duration < 0.0f) duration = schemaDuration("duration-warning", 3.5f);
|
||||||
|
push(message, type, duration, std::move(onClick), actionHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
void push(const std::string& message, NotificationType type, float duration = 5.0f,
|
||||||
|
std::function<void()> onClick = nullptr, const std::string& actionHint = "") {
|
||||||
notifications_.emplace_back(message, type, duration);
|
notifications_.emplace_back(message, type, duration);
|
||||||
|
|
||||||
// Retain a copy in the persistent history (the toast above will fade in seconds; this
|
// Retain a copy in the persistent history (the toast above will fade in seconds; this
|
||||||
// survives so the user can review what happened). Thread note: every push is on the UI
|
// survives so the user can review what happened). Thread note: every push is on the UI
|
||||||
// thread (RPC results run as main-thread MainCb callbacks), so this container needs no lock,
|
// thread (RPC results run as main-thread MainCb callbacks), so this container needs no lock,
|
||||||
// consistent with the rest of this class. Do NOT push from a raw worker thread.
|
// consistent with the rest of this class. Do NOT push from a raw worker thread.
|
||||||
history_.push_back(AlertRecord{message, type, static_cast<std::int64_t>(std::time(nullptr))});
|
history_.push_back(AlertRecord{message, type, static_cast<std::int64_t>(std::time(nullptr)),
|
||||||
|
std::move(onClick), actionHint});
|
||||||
++total_pushed_;
|
++total_pushed_;
|
||||||
while (history_.size() > kMaxHistory) {
|
while (history_.size() > kMaxHistory) {
|
||||||
history_.pop_front();
|
history_.pop_front();
|
||||||
|
|||||||
Reference in New Issue
Block a user