- Locking the wallet (manual / auto / lite) now calls wipeDisplayedSecrets(), which scrubs + closes any revealed seed phrase, migration seed, or exported key — previously a locked wallet could leave the phrase/key on screen and resident. Runs synchronously, before the async lock round-trip. - The console secret-arg redaction list only had full-node RPC names, so lite verbs (import / timport / encrypt / decrypt / unlock) echoed keys and passphrases into the scrollback and up-arrow history; added them. - The offline wallet-rebuild helper was invoked via a shell string (popen "sh -c") built from paths including the wallet filename, so a crafted name could inject a command; it now refuses any path with a shell metacharacter (rc stays -1 and the existing verify-before-swap fails safe). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2146 lines
140 KiB
C++
2146 lines
140 KiB
C++
// DragonX Wallet - ImGui Edition
|
||
// Copyright 2024-2026 The Hush Developers
|
||
// Released under the GPLv3
|
||
|
||
#pragma once
|
||
|
||
#include <memory>
|
||
#include <string>
|
||
#include <functional>
|
||
#include <thread>
|
||
#include <atomic>
|
||
#include <mutex>
|
||
#include <chrono>
|
||
#include <unordered_map>
|
||
#include <unordered_set>
|
||
#include <deque>
|
||
#include <condition_variable>
|
||
#include <cstdint>
|
||
#include <optional>
|
||
#include <nlohmann/json_fwd.hpp>
|
||
#include "data/transaction_history_cache.h"
|
||
#include "data/transaction_history_store_policy.h"
|
||
#include "data/address_book.h"
|
||
#include "data/wallet_index.h"
|
||
#include "data/wallet_state.h"
|
||
#include "rpc/connection.h"
|
||
#include "daemon/stop_delivery.h"
|
||
#include "daemon/daemon_busy_detect.h"
|
||
#include "rpc/rpc_busy_gate.h"
|
||
#include "rpc/long_call_lane.h"
|
||
#include "services/history_scan_policy.h"
|
||
#include "services/connect_scan_gate.h"
|
||
#include "services/scan_policy.h"
|
||
#include "services/wallet_scan_governor.h"
|
||
#include "services/chat_scan_policy.h"
|
||
#include "services/note_consolidation.h"
|
||
#include "services/mining_thread_commit.h"
|
||
#include "services/network_refresh_service.h"
|
||
#include "services/wallet_security_controller.h"
|
||
#include "services/wallet_security_workflow.h"
|
||
#include "util/async_task_manager.h"
|
||
#include "resources/provision_state.h"
|
||
#include "util/background_sampler.h"
|
||
#include "util/coalescing_writer.h"
|
||
#include "util/sync_eta.h"
|
||
#include "util/fetch_backoff.h"
|
||
#include "util/idle_tracker.h"
|
||
#include "util/pool_stats_service.h"
|
||
#include "wallet/wallet_capabilities.h"
|
||
#include "chat/chat_service.h"
|
||
#include "chat/chat_database.h"
|
||
#include "ui/sidebar.h"
|
||
#include "ui/windows/console_tab.h"
|
||
#include "imgui.h"
|
||
|
||
// Forward declarations
|
||
namespace dragonx {
|
||
namespace rpc {
|
||
class RPCClient;
|
||
class RPCWorker;
|
||
}
|
||
namespace config { class Settings; }
|
||
namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
|
||
namespace util { class Bootstrap; class SecureVault; }
|
||
namespace wallet { class LiteWalletController; struct LiteWalletAppRefreshModel; }
|
||
}
|
||
|
||
namespace dragonx {
|
||
|
||
/**
|
||
* @brief First-run wizard states
|
||
*/
|
||
enum class WizardPhase {
|
||
None, // No wizard — normal operation
|
||
BootstrapOffer, // Step 1: offer bootstrap download
|
||
BootstrapInProgress,// downloading / extracting
|
||
BootstrapFailed, // error with retry/skip option
|
||
Appearance, // Step 2: visual effects / performance options
|
||
EncryptOffer, // Step 3: offer wallet encryption
|
||
EncryptInProgress, // encrypting + daemon restarting
|
||
PinSetup, // Step 4: optional PIN setup (after encryption)
|
||
Done // wizard complete, launch normally
|
||
};
|
||
|
||
/**
|
||
* @brief Encrypt-wallet dialog phases (settings page flow)
|
||
*/
|
||
enum class EncryptDialogPhase {
|
||
PassphraseEntry, // Enter & confirm passphrase
|
||
Encrypting, // In-progress animation
|
||
PinSetup, // Offer PIN after successful encryption
|
||
Done // Finished — close dialog
|
||
};
|
||
|
||
// A status string written by a background/worker thread and read every frame by the UI thread. Its
|
||
// operator= locks, so all the plain `x = "..."` assignment sites stay unchanged; readers call get()
|
||
// for a consistent per-frame snapshot instead of racing a non-atomic std::string. (M-05, L-06)
|
||
class GuardedStatus {
|
||
public:
|
||
GuardedStatus() = default;
|
||
GuardedStatus& operator=(std::string v) { std::lock_guard<std::mutex> lk(m_); v_ = std::move(v); return *this; }
|
||
std::string get() const { std::lock_guard<std::mutex> lk(m_); return v_; }
|
||
private:
|
||
mutable std::mutex m_;
|
||
std::string v_;
|
||
};
|
||
|
||
/**
|
||
* @brief Main application class
|
||
*
|
||
* Manages application state, RPC connection, and coordinates UI rendering.
|
||
*/
|
||
class App {
|
||
public:
|
||
App();
|
||
void wipeSecrets(); // scrub all resident secret buffers; called from ~App() AND the forced-exit path (L-05)
|
||
void wipeDisplayedSecrets(); // scrub + close any on-screen secret (seed/key displays) on wallet lock
|
||
~App();
|
||
|
||
// Non-copyable
|
||
App(const App&) = delete;
|
||
App& operator=(const App&) = delete;
|
||
|
||
/**
|
||
* @brief Initialize the application
|
||
* @return true if initialization succeeded
|
||
*/
|
||
bool init();
|
||
|
||
/**
|
||
* @brief Update application state (called every frame)
|
||
*/
|
||
void update();
|
||
|
||
/**
|
||
* @brief Wall-clock seconds since the previous update(), capped at kMaxLogicDeltaSeconds.
|
||
*
|
||
* Logic timers (refresh scheduler, poll intervals, countdowns) advance by this. Visual
|
||
* interpolation keeps io.DeltaTime, which update() clamps to 0.25 s so animations don't jump. The
|
||
* main loop idles between frames (one frame per 2 s wake when nothing is happening), so a timer
|
||
* fed the clamped delta would run up to 8x slow.
|
||
*/
|
||
float logicDeltaTime() const { return logic_dt_; }
|
||
static constexpr float kMaxLogicDeltaSeconds = 6.0f;
|
||
|
||
/**
|
||
* @brief Push the saved appearance prefs (low-spec, theme effects, blur, UI and noise opacity,
|
||
* console scanline) to the live effects state. Low-spec overrides the rest without touching the
|
||
* saved values. Called at startup, by the low-spec hotkey, the first-run wizard and the Settings
|
||
* page, so every path applies the same thing.
|
||
*/
|
||
void applyVisualSettings();
|
||
|
||
/**
|
||
* @brief Pre-frame tasks that must run BEFORE ImGui::NewFrame().
|
||
*
|
||
* Font atlas rebuilds (hot-reload, user font scale changes) must
|
||
* happen before NewFrame() because NewFrame() caches font pointers.
|
||
* Rebuilding mid-frame causes dangling pointer crashes.
|
||
*/
|
||
void preFrame();
|
||
|
||
/**
|
||
* @brief Render the application UI (called every frame)
|
||
*/
|
||
void render();
|
||
|
||
/**
|
||
* @brief Clean shutdown
|
||
*/
|
||
void shutdown();
|
||
|
||
/**
|
||
* @brief Wait (bounded, 3 s) for queued settings.json / wallets.json writes to land on disk.
|
||
* main() calls it around shutdown(): _Exit() skips ~App(), so nothing else would.
|
||
*/
|
||
void flushPendingWrites() { file_writer_.flush(std::chrono::milliseconds(3000)); }
|
||
|
||
/**
|
||
* @brief Check if app should exit (render loop can stop)
|
||
* Enforces minimum 1-second display of shutdown screen so user sees feedback.
|
||
*/
|
||
bool shouldQuit() const {
|
||
if (!quit_requested_ || !shutdown_complete_) return false;
|
||
auto elapsed = std::chrono::steady_clock::now() - shutdown_start_time_;
|
||
return elapsed >= std::chrono::seconds(1) && !bootstrapQuitPending();
|
||
}
|
||
|
||
/**
|
||
* @brief A bootstrap cancelled by beginShutdown() is still replacing the chain data (its abort removes the
|
||
* partial chain). The shutdown screen stays up until it is done (capped), so main()'s exit watchdog never
|
||
* cuts it off.
|
||
*/
|
||
bool bootstrapQuitPending() const;
|
||
|
||
/**
|
||
* @brief Request application exit — begins async shutdown
|
||
*/
|
||
void requestQuit();
|
||
|
||
/**
|
||
* @brief Begin graceful shutdown (called on window close)
|
||
* Starts daemon stop on a background thread while UI keeps rendering.
|
||
*/
|
||
void beginShutdown();
|
||
|
||
/**
|
||
* @brief Whether we are in the shutdown phase
|
||
*/
|
||
bool isShuttingDown() const { return shutting_down_; }
|
||
// True while the wallet-switch progress modal is open — keeps the frame loop redrawing so its live
|
||
// phase/spinner update in real time even when the app is otherwise idle.
|
||
bool isWalletSwitchInProgress() const { return wallet_switch_dialog_open_.load(); }
|
||
wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
|
||
bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
|
||
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
|
||
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
|
||
|
||
// Daemon (v1.3.0+) coinbase auto-shield status, from z_autoshieldstatus. "Not probed" / all-false on
|
||
// pre-1.3.0 daemons (no such RPC) — callers treat that as "the wallet handles auto-shield itself".
|
||
bool daemonAutoShieldProbed() const { return daemon_autoshield_probed_; }
|
||
bool daemonAutoShieldActive() const { return daemon_autoshield_active_; }
|
||
const std::string& daemonAutoShieldAddress() const { return daemon_autoshield_address_; }
|
||
const std::string& daemonAutoShieldDisabledReason() const { return daemon_autoshield_disabled_reason_; }
|
||
bool daemonAutoShieldSeedRecoverable() const { return daemon_autoshield_seed_recoverable_; }
|
||
|
||
// W7 QoL: a plaintext support-diagnostics snapshot (version, variant, daemon/RPC/wallet/log state)
|
||
// for the "Copy diagnostics" action. Contains no secrets.
|
||
std::string buildDiagnosticsReport();
|
||
bool supportsSoloMining() const { return wallet::supportsSoloMining(walletCapabilities()); }
|
||
bool supportsPoolMining() const { return wallet::supportsPoolMining(walletCapabilities()); }
|
||
bool supportsLiteBackend() const { return wallet::supportsLiteBackend(walletCapabilities()); }
|
||
|
||
/**
|
||
* @brief Render the shutdown overlay (called instead of normal UI during shutdown)
|
||
*/
|
||
void renderShutdownScreen();
|
||
|
||
/**
|
||
* @brief Tail the last N lines of the daemon's debug.log (best-effort, reads only the file tail).
|
||
* Fallback for the shutdown screen when we have no captured stdout — e.g. an external daemon we
|
||
* attached to rather than spawned — so the user can still see the node flushing/exiting.
|
||
*/
|
||
std::vector<std::string> tailDaemonDebugLog(int maxLines) const;
|
||
|
||
// Whether beginShutdown() should pause and confirm before stopping the daemon: this quit would stop it,
|
||
// and it is rebuilding witnesses (its own log says so) or has been alive but too busy to answer RPC for
|
||
// a while. Stopping mid-rebuild throws the rebuild away, and a node killed then replays and redoes it.
|
||
bool shouldConfirmDaemonStop() const;
|
||
// The node is alive and its RPC has only timed out / been refused as busy for >= 30s (a rebuild that
|
||
// logged no marker, e.g. an external node whose output we can't read).
|
||
bool rpcBusyWhileAlive() const;
|
||
// The node is rebuilding witnesses (our own node's log says so) or has been too busy to answer for a
|
||
// while: stopping it now either waits up to an hour or makes it start over. UI thread only.
|
||
bool nodeBusyForStop() const;
|
||
// May quitting (or a default-policy stop) stop a node this session didn't launch? With stop_external_daemon
|
||
// on, or when it is the node an earlier session of ours launched and left running (its ownership record
|
||
// matches a live process). That one is stopped gracefully and waited for, never killed.
|
||
bool mayStopExternalNode() const;
|
||
// For an operation that would stop the node (restart, rescan, switch, reinstall, bootstrap, ...): when
|
||
// nodeBusyForStop(), tell the user to try again later and return true; the caller then does nothing.
|
||
bool refuseIfNodeBusy();
|
||
// Long wallet RPCs (rescan, key imports; C61) run one at a time on long_lane_. Main thread. Decide a refusal
|
||
// BEFORE copying a key into a closure: true when one can't start now, with the reason in English in *why
|
||
// (the console's text) and as an i18n key in *trKey (the GUI's toast / status line).
|
||
bool longWalletOpRefused(std::string* why, const char** trKey = nullptr) const;
|
||
// Main thread. Runs body on the lane's own ungated client (no client timeout, aborted at quit) on an async
|
||
// task; body must catch everything and returns the continuation, which runs on the main thread once the lane
|
||
// is free again. false (body never runs) when the lane is already taken.
|
||
using LongWalletBody = std::function<std::function<void()>(rpc::RPCClient&)>;
|
||
bool runLongWalletTask(LongWalletBody body);
|
||
// Record how an RPC to the node just went, for rpcBusyWhileAlive(): busy kinds (ReplyTimeout /
|
||
// QueueFull) start the clock, an answer stops it, anything else leaves it alone.
|
||
void noteDaemonRpcResult(bool answered, rpc::RpcFailureKind kind);
|
||
// Called by main.cpp: brings the window back (restore + raise) when a quit is deferred for the confirm
|
||
// modal while minimized or behind other windows — the modal only renders in a visible frame.
|
||
void setRaiseWindowCallback(std::function<void()> cb) { raise_window_ = std::move(cb); }
|
||
// The "node is rebuilding — stop anyway / keep running / cancel" modal, rendered from render().
|
||
void renderDaemonStopConfirm();
|
||
|
||
/**
|
||
* @brief Render loading overlay in content area while daemon is starting/syncing
|
||
* @param contentH Height of the content area child window
|
||
*/
|
||
void renderLoadingOverlay(float contentH);
|
||
|
||
// Accessors for subsystems
|
||
rpc::RPCClient* rpc() { return rpc_.get(); }
|
||
rpc::RPCWorker* worker() { return worker_.get(); }
|
||
// Console backend accessors (fast-lane-preferring, defined in app.cpp where the
|
||
// subsystem types are complete) used by the shared console executor.
|
||
rpc::RPCClient* consoleRpc();
|
||
rpc::RPCWorker* consoleWorker();
|
||
daemon::EmbeddedDaemon* consoleDaemon();
|
||
daemon::XmrigManager* consoleXmrig();
|
||
config::Settings* settings() { return settings_.get(); }
|
||
// Request a font-atlas rebuild before the next frame (e.g. after toggling color emoji). Handled in
|
||
// preFrame() via Typography::reload — safe to call from UI code mid-frame.
|
||
void requestFontRebuild() { font_rebuild_requested_ = true; }
|
||
/// Load the base ui.toml (disk, synced to the embedded copy when sizes differ; embedded fallback). Idempotent.
|
||
/// main() calls it before the first Typography::load so the atlas is built once at the final sizes.
|
||
static bool loadBaseUiSchema();
|
||
// Lite wallet controller (non-null only in lite builds with a linked backend).
|
||
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
||
// HushChat service (identity + in-memory message store); the Chat tab reads its store.
|
||
chat::ChatService& chatService() { return chat_service_; }
|
||
// Public forwarder for the UI: the wallet z-address others use to reach this user in chat. Call on
|
||
// click (not per-frame) — the underlying chatReplyZaddr() may persist a freshly chosen address.
|
||
std::string chatMyAddress() { return chatReplyZaddr(); }
|
||
// Why the Chat tab has no identity yet, so it can show the right prompt instead of always
|
||
// "unlock your wallet". Locked = encrypted & locked (seed unreadable); Preparing = unlocked and
|
||
// deriving the identity (fetch in flight, or waiting on the node / address list); Unavailable =
|
||
// no usable secret for this wallet (view-only, or a legacy wallet on a daemon without the needed
|
||
// export RPC) — correctly latched, so the prompt points at Back up / Migrate instead.
|
||
enum class ChatIdentityStatus { Ready, Locked, Preparing, Unavailable };
|
||
ChatIdentityStatus chatIdentityStatus() const;
|
||
// HushChat composing: construct, broadcast (broadcastChatMemos), and locally echo an outgoing
|
||
// message (to a conversation whose peer key we know) / a new-conversation contact request.
|
||
void sendChatMessage(const std::string& conversationId, const std::string& text);
|
||
void startChatConversation(const std::string& peerZaddr, const std::string& text);
|
||
// Send a contact request into an existing conversation (used to retry a failed request in place).
|
||
void sendContactRequestForCid(const std::string& cid, const std::string& peerZaddr,
|
||
const std::string& text);
|
||
// Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
|
||
// (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
|
||
// the chat feature is off. Returns true when it had to fabricate the (random) demo identity
|
||
// because none was provisioned, so the sweep can drop it again at the end.
|
||
bool seedChatDemoData();
|
||
// Reason the lite wallet failed to auto-open this session (empty if none / opened OK).
|
||
const std::string& liteOpenError() const { return lite_open_error_; }
|
||
// Show the lite send-time unlock modal (called when a spend is attempted on a locked wallet).
|
||
void requestLiteUnlock() { lite_unlock_prompt_ = true; }
|
||
// Lock the lite wallet AND immediately tear down the chat session (the lite backend `lock`
|
||
// doesn't update state_.locked until the next poll, so chat secrets would otherwise linger).
|
||
bool lockLiteWallet();
|
||
// (Re)build the lite controller from current settings so a changed lite-server selection
|
||
// takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp).
|
||
void rebuildLiteWallet(bool force = false);
|
||
WalletState& state() { return state_; }
|
||
const WalletState& state() const { return state_; }
|
||
const WalletState& getWalletState() const { return state_; }
|
||
// Shared contact store (Contacts tab / Send picker / future Chat roster). App-owned so
|
||
// every surface reads one source of truth instead of a per-dialog singleton.
|
||
data::AddressBook& addressBook() { return address_book_; }
|
||
const data::AddressBook& addressBook() const { return address_book_; }
|
||
|
||
// Hash of the active wallet's identity (derived from its address list). This is the tx-history
|
||
// cache key — it changes when the address set changes, so DON'T scope persistent user data on it.
|
||
std::string activeWalletIdentityHash() const;
|
||
|
||
// Stable per-wallet id ("w:"+hex) for scoping persistent per-wallet data (address-book contacts).
|
||
// Generated once and persisted in the wallet index (keyed by wallet file), never recomputed from
|
||
// the mutable address set — so creating addresses / locking / disconnecting never changes it.
|
||
// Empty only when there is no active wallet file. Establishes + persists the id on first call,
|
||
// except during a full UI sweep (capture_mode_), where it returns the existing id or "".
|
||
std::string activeWalletScopeId();
|
||
|
||
// Stable scope for the Market-tab portfolio groups: activeWalletScopeId() once this wallet's addresses are
|
||
// loaded, else "" (so a group never shows a 0.0000 balance from an empty list, and Add stays disabled
|
||
// until the wallet is known - the same window the old identity-hash gate covered). Never the address hash.
|
||
// In a full UI sweep (capture_mode_) a fixed demo id so the sweep can still exercise Add.
|
||
std::string portfolioScopeId();
|
||
// One-time (per wallet per session) migration of legacy hash-scoped portfolio groups to the stable id.
|
||
// Called from update(); a no-op until the wallet's addresses are loaded.
|
||
void reattachLegacyPortfolioScopes();
|
||
|
||
data::WalletIndex& walletIndex() { return wallet_index_; }
|
||
const data::WalletIndex& walletIndex() const { return wallet_index_; }
|
||
|
||
// Connection state (convenience wrappers)
|
||
bool isConnected() const { return state_.connected; }
|
||
int getBlockHeight() const { return state_.sync.blocks; }
|
||
const std::string& getConnectionStatus() const { return connection_status_; }
|
||
std::string getDaemonStatus() const; // thread-safe copy (daemon_status_ is written off-thread)
|
||
|
||
// Balance info (convenience wrappers)
|
||
double getShieldedBalance() const { return state_.shielded_balance; }
|
||
double getTransparentBalance() const { return state_.transparent_balance; }
|
||
double getTotalBalance() const { return state_.total_balance; }
|
||
double getUnconfirmedBalance() const { return state_.unconfirmed_balance; }
|
||
|
||
// Addresses
|
||
const std::vector<AddressInfo>& getZAddresses() const { return state_.z_addresses; }
|
||
const std::vector<AddressInfo>& getTAddresses() const { return state_.t_addresses; }
|
||
|
||
// Transactions
|
||
const std::vector<TransactionInfo>& getTransactions() const { return state_.transactions; }
|
||
|
||
// Mining
|
||
const MiningInfo& getMiningInfo() const { return state_.mining; }
|
||
void startMining(int threads);
|
||
void stopMining();
|
||
bool isMiningToggleInProgress() const { return mining_toggle_in_progress_.load(std::memory_order_relaxed); }
|
||
// Persistent mining error (last start/stop/pool failure) so a failed miner isn't just a fading toast.
|
||
const std::string& miningLastError() const { return mining_last_error_; }
|
||
bool miningErrorIsAntivirus() const { return mining_error_av_; }
|
||
void clearMiningError() { mining_last_error_.clear(); mining_error_av_ = false; }
|
||
void showAntivirusHelp() { pending_antivirus_dialog_ = true; } // Windows-only dialog; inert elsewhere
|
||
|
||
// Pool mining (xmrig)
|
||
void startPoolMining(int threads);
|
||
void stopPoolMining();
|
||
// Change the thread count of the pool miner that is running (or queued to start). Never starts one.
|
||
void retargetPoolMining(int threads);
|
||
int getXmrigRequestedThreads() const {
|
||
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
|
||
}
|
||
// Thread count of the newest pool start/retarget posted and not stopped since (0 = none). Leads
|
||
// getXmrigRequestedThreads(), which only changes once the mining-control thread runs that job.
|
||
int poolThreadsTarget() const { return pool_threads_target_.load(std::memory_order_relaxed); }
|
||
// A thread-count pick from the Mining tab (grid tile, -/+, Enter). The settings value changes at once;
|
||
// the save and the miner change happen ONCE, when the input settles (drag released, or ~0.5 s after
|
||
// the last click). Latest wins.
|
||
void selectMiningThreads(int threads);
|
||
void setMiningThreadDragActive(bool active) { mining_thread_drag_ = active; }
|
||
// A pick has not reached the miner yet (the grid must not snap back to the old count meanwhile).
|
||
bool miningThreadCommitPending() const {
|
||
return mining_thread_commit_.pending() || mining_solo_threads_pending_ > 0;
|
||
}
|
||
// True while the pool miner process is live — used to refuse replacing the binary under it.
|
||
bool isPoolMinerRunning() const {
|
||
return xmrig_manager_ && xmrig_manager_->isRunning();
|
||
}
|
||
|
||
// Auto-balance: latest per-pool hashrate snapshot (for the mining tab pool list),
|
||
// and a request to refresh it now (Refresh button / switching into Auto mode).
|
||
util::PoolStatsService::Snapshot poolStatsSnapshot() const {
|
||
return pool_stats_service_.snapshot();
|
||
}
|
||
void requestPoolBalanceRefresh() { balance_refresh_pending_ = true; }
|
||
|
||
// Installed miner version (detected from `xmrig --version`, cached; kicks the one-shot
|
||
// detection on first call) so the mining tab can show it before mining starts.
|
||
std::string poolMiningInstalledVersion();
|
||
|
||
// Mine-when-idle state query
|
||
bool isIdleMiningActive() const { return idle_mining_active_; }
|
||
|
||
// Peers
|
||
const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
|
||
const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
|
||
bool isPeerRefreshInProgress() const {
|
||
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Peers);
|
||
}
|
||
void banPeer(const std::string& ip, int duration_seconds = 86400);
|
||
void unbanPeer(const std::string& ip);
|
||
void clearBans();
|
||
|
||
// Address operations
|
||
void createNewZAddress(std::function<void(const std::string&)> callback = nullptr);
|
||
void createNewTAddress(std::function<void(const std::string&)> callback = nullptr);
|
||
|
||
// Hide/unhide addresses from the address list (persisted in settings)
|
||
void hideAddress(const std::string& addr);
|
||
void unhideAddress(const std::string& addr);
|
||
bool isAddressHidden(const std::string& addr) const;
|
||
int getHiddenAddressCount() const;
|
||
|
||
void favoriteAddress(const std::string& addr);
|
||
void unfavoriteAddress(const std::string& addr);
|
||
bool isAddressFavorite(const std::string& addr) const;
|
||
|
||
// Address metadata (labels, icons, custom ordering)
|
||
void setAddressLabel(const std::string& addr, const std::string& label);
|
||
void setAddressIcon(const std::string& addr, const std::string& icon);
|
||
std::string getAddressLabel(const std::string& addr) const;
|
||
std::string getAddressIcon(const std::string& addr) const;
|
||
int getAddressSortOrder(const std::string& addr) const;
|
||
void setAddressSortOrder(const std::string& addr, int order);
|
||
int getNextSortOrder() const;
|
||
void swapAddressOrder(const std::string& a, const std::string& b);
|
||
// Assign dense sort orders (0..N-1) to the given addresses in the given order and
|
||
// persist once. Used by drag-reorder so a drop always takes effect (even from the
|
||
// default un-ordered state, where a pairwise swap would be a no-op).
|
||
void reorderAddresses(const std::vector<std::string>& orderedAddrs);
|
||
bool isMiningAddress(const std::string& addr) const;
|
||
void setMiningAddress(const std::string& addr, bool mining);
|
||
void invalidateAddressValidationCache();
|
||
|
||
// Key export/import
|
||
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
|
||
// callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export.
|
||
void exportAllKeys(std::function<void(const std::string&, int, int)> callback);
|
||
// callback(success, errorOrEmpty, importedAddress). address is "" on failure or when the RPC
|
||
// returns none; the import routes to z_importviewingkey / z_importkey / importprivkey by key type.
|
||
// startHeight > 0 rescans from that block (shielded RPCs only; ignored for transparent WIF).
|
||
void importPrivateKey(const std::string& key, int startHeight,
|
||
std::function<void(bool, const std::string&, const std::string&)> callback);
|
||
|
||
// Sweep a spending key: import it (rescan) then z_sendmany ALL its funds (balance − fee) to a
|
||
// destination you own — a freshly generated shielded address when destMode == 0, else destExisting.
|
||
// Drives the sweep_step_ / sweep_status_ / sweep_txid_ state; reuses the async-operation tracker.
|
||
void sweepPrivateKey(const std::string& key, int startHeight, int destMode,
|
||
const std::string& destExisting);
|
||
|
||
// Wallet backup
|
||
void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback);
|
||
// Export the wallet's BIP39 seed phrase (z_exportmnemonic). The callback receives
|
||
// (ok, noMnemonic, phrase, error): ok+phrase on success; noMnemonic=true when the
|
||
// wallet's seed is not mnemonic-derived (legacy wallet). Full-node only; the phrase
|
||
// is a secret and is wiped after the callback returns.
|
||
void exportSeedPhrase(std::function<void(bool ok, bool noMnemonic,
|
||
const std::string& phrase,
|
||
const std::string& error)> callback);
|
||
|
||
// Transaction operations
|
||
void sendTransaction(const std::string& from, const std::string& to,
|
||
double amount, double fee, const std::string& memo,
|
||
std::function<void(bool success, const std::string& result)> callback);
|
||
|
||
// Register a daemon async operation id (z_shieldcoinbase / z_mergetoaddress /
|
||
// auto-shield) with the shared opid poller so its eventual success/failure is
|
||
// surfaced and balances/transactions refresh on completion. z_sendmany uses the
|
||
// richer pending-send path internally; this is for operations with no optimistic
|
||
// transaction row of their own. The origin decides what its completion re-reads: the client
|
||
// auto-shield is Auto (re-read, balance unforced); everything the user starts is User.
|
||
void trackOperation(const std::string& opid,
|
||
services::NetworkRefreshService::OperationOrigin origin = services::NetworkRefreshService::OperationOrigin::User);
|
||
// The connected daemon rejected the opid-filtered z_getoperationstatus, so polls use the unfiltered
|
||
// form (every retained op) for the rest of this connection.
|
||
bool opidFilterUnsupported() const { return opid_filter_unsupported_.load(std::memory_order_relaxed); }
|
||
|
||
// Something changed the wallet that the scans' change signals (tip, mempool) don't show: a send or other
|
||
// operation finished, an import, a Console command. The change-gated scans run again.
|
||
void noteWalletMutation() { ++wallet_mutation_epoch_; }
|
||
// The window is in the background: minimized, hidden, or covered while the app has no focus (debounced
|
||
// by the main loop; any input ends it). The background refresh profile then holds the wallet scans and
|
||
// the peripheral polls (mining, price), and no page counts as visible to the scan budget.
|
||
void setWindowBackground(bool background);
|
||
bool isWindowBackground() const { return window_background_; }
|
||
// A balance scan is in flight (the staleness badge waits for it).
|
||
bool isBalanceRefreshInFlight() const { return scan_governor_.inFlight(services::ScanStream::Balance); }
|
||
// The balance is waiting for the scan budget and still within what it promises (its floor plus one scan):
|
||
// an old value then is the budget at work, not a failing node.
|
||
bool isBalanceHeldByScanBudget() const;
|
||
|
||
// RAII around one long ("maintenance") rpc_ call made on a worker thread with a timeout above the
|
||
// default 30 s, e.g. a merge or a whole-wallet scan given 120 s. rpc_ holds its curl lock for the
|
||
// whole transfer and disconnect() takes the same lock, so these are counted for
|
||
// abortMaintenanceCalls().
|
||
class MaintenanceCallScope {
|
||
public:
|
||
explicit MaintenanceCallScope(App& app) : app_(app)
|
||
{
|
||
// Count first, then read the flag; abortMaintenanceCalls() sets the flag, then reads the count.
|
||
// Either this sees the abort and never sends, or the abort sees this call and cuts it off. (A
|
||
// call queued behind the one disconnect() waits on could otherwise take the curl lock next
|
||
// and hold disconnect() for its full 120 s.)
|
||
app_.maint_calls_.fetch_add(1);
|
||
if (app_.maint_abort_.load()) {
|
||
app_.maint_calls_.fetch_sub(1);
|
||
throw rpc::RpcTransportError(rpc::RpcFailureKind::Aborted, "RPC aborted: disconnecting");
|
||
}
|
||
}
|
||
~MaintenanceCallScope() { app_.maint_calls_.fetch_sub(1); }
|
||
MaintenanceCallScope(const MaintenanceCallScope&) = delete;
|
||
MaintenanceCallScope& operator=(const MaintenanceCallScope&) = delete;
|
||
private:
|
||
App& app_;
|
||
};
|
||
// Main thread, right before rpc_->disconnect(): abort rpc_'s in-flight call if it is a maintenance
|
||
// call, so the disconnect doesn't freeze the UI for up to its 120 s, and refuse maintenance calls that
|
||
// start before the next connect (onConnected() lifts that). Ordinary calls are left to finish (at most
|
||
// 30 s, as before), so a user's own send is never cut off mid-call.
|
||
void abortMaintenanceCalls();
|
||
// Whether a whole-wallet scan the user didn't ask for may be sent now: a connected, unlocked, synced
|
||
// full node that is past warmup, not rescanning and not busy.
|
||
bool walletScanReady() const;
|
||
|
||
// C210: the "Keep in each address" consolidation run. Owned here so it keeps going when the Merge dialog
|
||
// closes; pumped from update(). Full node only (refused in Lite). Main thread.
|
||
const services::ConsolidationRun& consolidation() const { return consolidation_; }
|
||
bool startConsolidation(const std::vector<services::NoteGroup>& groups, double fee, int batchLimit);
|
||
void stopConsolidation() { consolidation_.requestStop(); }
|
||
void dismissConsolidation() { consolidation_.dismiss(); } // no-op while running
|
||
// A one-off wallet-wide scan the user asked for (the Merge dialog's scope read, a merge): never held back,
|
||
// but charged to the scan budget (ScanStream::Adhoc) so the routine scans make room. Main thread.
|
||
services::ScanTicket beginAdhocWalletScan();
|
||
void endAdhocWalletScan(const services::ScanTicket& ticket, double costMs, services::ScanOutcome outcome);
|
||
// A GUI wallet scan is in the daemon or queued for it: a one-off scan waits rather than stack on cs_main.
|
||
bool walletScanInFlight() const { return scan_governor_.anyInFlight(); }
|
||
|
||
// Force refresh
|
||
void refreshNow();
|
||
// Called on window restore: drop any daemon-output backlog (update() runs while minimized, but its
|
||
// output parser skips while the wallet is locked), so a background witness rebuild that started and
|
||
// finished meanwhile isn't parsed in one batch and mistaken for a completed rescan (spurious toast).
|
||
void skipDaemonOutputBacklog();
|
||
void applyPendingDaemonOutputReset();
|
||
// A real user input event (key, text, mouse, touch, pen, gamepad button) was ingested. Main thread only.
|
||
// Called from main.cpp's noteEvent; do NOT call from an SDL event watch/filter (resizeWatcher).
|
||
void noteUserInput() { idle_.noteActivity(); }
|
||
void refreshMiningInfo();
|
||
void refreshPeerInfo();
|
||
void refreshMarketData();
|
||
// The Market tab's refresh button: lift any failure backoff (at most once per 5 s, against click
|
||
// spam), then refreshMarketData().
|
||
void retryMarketDataNow();
|
||
// Fetch the live exchange/pair list from CoinGecko once per session (venues are
|
||
// near-static); populates state.market.exchanges. Safe to call every frame.
|
||
void refreshExchanges();
|
||
// Fetch historical USD price series (CoinGecko market_chart) that back the portfolio
|
||
// sparkline intervals; self-throttled to ~30 min. Safe to call every frame.
|
||
void refreshMarketChart();
|
||
// Fetch the SELECTED pair's candles from that exchange's own API (data/exchange_candles.h) so the
|
||
// Market chart shows the real per-exchange price. Re-fetches on pair change; falls back to the
|
||
// CoinGecko aggregate for unmapped venues / failed fetches. Safe to call every frame.
|
||
void refreshExchangeChart();
|
||
// True while a market price-history fetch is in flight (CoinGecko aggregate OR per-exchange). The
|
||
// Market chart shows a loading indicator instead of the empty state during pair switches.
|
||
bool isMarketChartLoading() const { return chart_fetch_in_flight_ || exchange_chart_fetch_in_flight_; }
|
||
|
||
/// @brief Per-category refresh intervals, adjusted by active tab
|
||
using RefreshIntervals = services::NetworkRefreshService::Intervals;
|
||
|
||
/// @brief Get recommended refresh intervals for a given page
|
||
static RefreshIntervals getIntervalsForPage(ui::NavPage page);
|
||
|
||
// UI navigation
|
||
void setCurrentPage(ui::NavPage page);
|
||
ui::NavPage getCurrentPage() const { return current_page_; }
|
||
|
||
// Debug: screenshot sweep — cycles every skin x every enabled tab, one PNG each, into a
|
||
// timestamped folder under the config dir. Driven from App::render(); main.cpp polls
|
||
// wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
|
||
// onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
|
||
void startScreenshotSweep();
|
||
// Full UI sweep: like the tab sweep, but also drives every modal / dialog / multi-step flow /
|
||
// state overlay into view (with injected demo data, offline, firing no live ops) and captures
|
||
// each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index.
|
||
void startFullUiSweep();
|
||
std::string screenshotFullDir() const;
|
||
// Debug option: restrict either sweep to just the currently-active theme instead of cycling all.
|
||
bool sweepCurrentThemeOnly() const { return sweep_current_theme_only_; }
|
||
void setSweepCurrentThemeOnly(bool v) { sweep_current_theme_only_ = v; }
|
||
bool isScreenshotSweeping() const { return screenshot_sweep_active_; }
|
||
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
|
||
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
|
||
void onScreenshotCaptured();
|
||
std::string screenshotDir() const; // <config>/screenshots (fixed; sweeps overwrite in place)
|
||
|
||
// Dialog triggers (used by settings page to open modal dialogs)
|
||
void showImportKeyDialog() { import_view_mode_ = false; show_import_key_ = true; } // spending
|
||
void showImportViewingKeyDialog() { import_view_mode_ = true; show_import_key_ = true; } // watch-only
|
||
void showExportKeyDialog() { show_export_key_ = true; }
|
||
void showBackupDialog() { show_backup_ = true; }
|
||
void showSeedBackupDialog() { show_seed_backup_ = true; }
|
||
void showSeedMigrationDialog(); // opens the migration modal (resumes a pending one at Sweep)
|
||
// True when the current full-node wallet is a legacy, pre-seed-phrase wallet (no BIP39 mnemonic)
|
||
// that a capable daemon could migrate — the Migrate-to-seed button glows to nudge the user.
|
||
bool isPreSeedWallet() const { return wallet_seed_status_ == WalletSeedStatus::NoMnemonic; }
|
||
// Authoritative BIP39-mnemonic status of the ACTIVE wallet, decided at runtime by z_exportmnemonic
|
||
// (not the offline file probe, which can't tell an HD-but-no-mnemonic wallet from a seed-phrase one).
|
||
// 0 = unknown/undecidable, 1 = has a seed phrase, 2 = legacy (no mnemonic).
|
||
int activeWalletSeedBadge() const {
|
||
if (wallet_seed_status_ == WalletSeedStatus::HasMnemonic) return 1;
|
||
if (wallet_seed_status_ == WalletSeedStatus::NoMnemonic) return 2;
|
||
return 0;
|
||
}
|
||
void showAboutDialog() { show_about_ = true; }
|
||
void showFaqDialog() { show_faq_ = true; }
|
||
|
||
// Legacy tab compat — maps int to NavPage
|
||
void setCurrentTab(int tab);
|
||
int getCurrentTab() const { return static_cast<int>(current_page_); }
|
||
|
||
// Payment URI handling
|
||
void handlePaymentURI(const std::string& uri);
|
||
bool hasPendingPayment() const { return pending_payment_valid_; }
|
||
void clearPendingPayment() { pending_payment_valid_ = false; }
|
||
std::string getPendingToAddress() const { return pending_to_address_; }
|
||
double getPendingAmount() const { return pending_amount_; }
|
||
std::string getPendingMemo() const { return pending_memo_; }
|
||
std::string getPendingLabel() const { return pending_label_; }
|
||
|
||
// Embedded daemon control
|
||
bool startEmbeddedDaemon();
|
||
// How long stopEmbeddedDaemon() may wait, and who decides. It never kills the node on its own.
|
||
enum class StopMode {
|
||
AppShutdown, // quitting: no time limit; the shutdown screen offers "Leave running" (and "Kill" when stuck)
|
||
Background, // restart/rescan/switch/...: up to 120s, and let go as soon as the app starts quitting
|
||
Fallback, // the synchronous shutdown fallback on the UI thread: up to 20s
|
||
};
|
||
// Which node it may stop. Policy: our own, or an external one only when the "stop external daemon"
|
||
// setting says so. Connected: also the node we're connected to (adopted or external), with the
|
||
// credentials we connected with — a wallet switch, a reinstall and decrypt must free the port, the binary
|
||
// and wallet.dat. Only a node that accepts those credentials obeys.
|
||
enum class StopScope { Policy, Connected };
|
||
daemon::StopOutcome stopEmbeddedDaemon(StopMode mode = StopMode::Background, StopScope scope = StopScope::Policy);
|
||
// Stop the node for a wallet switch / adopt / restore / rebuild, which swap wallet.dat or relaunch on
|
||
// -wallet=<name>. Unlike stopEmbeddedDaemon()'s default policy (which leaves an adopted/external node
|
||
// running) it stops the node we're connected to too (StopScope::Connected). True only once the process
|
||
// has exited and the RPC port is free; false if it is still running, or the app started quitting.
|
||
bool stopDaemonForWalletSwitch();
|
||
bool isEmbeddedDaemonRunning() const;
|
||
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
|
||
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
|
||
void rescanBlockchain(); // restart daemon with -rescan flag (full-history nodes)
|
||
// Runtime rescanblockchain RPC starting at a snapshot-available height. Unlike the
|
||
// -rescan restart, this works on bootstrapped/pruned nodes (which lack pre-snapshot
|
||
// block data), reconciling the wallet's stale spent-state without a daemon restart.
|
||
// Runs on the long-call lane; false when it did not start (refused, not connected, one already running).
|
||
bool runtimeRescan(int startHeight);
|
||
// Probe for the lowest block height the node still has on disk, on worker_ (never on the UI thread).
|
||
// cb(ok, lowestHeight, fullHistory) runs on the UI thread: from worker_->drainResults(), or synchronously
|
||
// when there is nothing to probe. fullHistory==true when block 1 is readable (a normal, non-bootstrapped
|
||
// node). ok==false means "could not tell" (busy node, timeout, reconnect): never "blocks are missing".
|
||
void detectLowestAvailableBlockHeight(std::function<void(bool ok, int lowestHeight, bool fullHistory)> cb);
|
||
// Flag that a bootstrap just finished so the wallet auto-reconciles spent-state once the
|
||
// daemon is back up (consumed in update()).
|
||
void markPostBootstrapRescanPending() {
|
||
post_bootstrap_rescan_pending_ = true;
|
||
post_bootstrap_probe_attempts_ = 0;
|
||
post_bootstrap_probe_retry_at_ = {};
|
||
}
|
||
bool runtimeRescanActive() const { return runtime_rescan_active_; }
|
||
void repairWallet(); // restart daemon with -zapwallettxes=2 (wipe & rebuild wallet tx records)
|
||
void reinstallBundledDaemon(); // stop daemon, overwrite installed binary with the bundled one, restart
|
||
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
|
||
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
|
||
bool isBootstrapDownloading() const { return bootstrap_downloading_; }
|
||
void setBootstrapDownloading(bool v) { bootstrap_downloading_ = v; }
|
||
|
||
// Get daemon memory usage in MB (uses embedded daemon handle if available,
|
||
// falls back to platform-level process scan for external daemons)
|
||
double getDaemonMemoryUsageMB() const;
|
||
|
||
// Diagnostic string describing daemon memory detection path (for debugging)
|
||
const std::string& getDaemonMemDiag() const { return daemon_mem_diag_; }
|
||
|
||
// Background gradient overlay texture (a replaced texture is retired, freed at the next preFrame())
|
||
void setGradientTexture(ImTextureID tex) { if (tex != gradient_tex_) retireTexture(gradient_tex_); gradient_tex_ = tex; }
|
||
ImTextureID getGradientTexture() const { return gradient_tex_; }
|
||
|
||
// Logo texture accessor (wallet branding icon)
|
||
ImTextureID getLogoTexture() const { return logo_tex_; }
|
||
int getLogoWidth() const { return logo_w_; }
|
||
int getLogoHeight() const { return logo_h_; }
|
||
|
||
// Coin logo texture accessor (DragonX currency icon for balance tab)
|
||
ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }
|
||
|
||
// DragonX custom chat emoji (the ":drgx:" shortcode) — the mark recolored to the theme accent (like
|
||
// the logo), re-rasterized on theme change. Used by the emoji picker tile + inline in chat bubbles.
|
||
ImTextureID getDrgxEmojiTexture() const { return drgx_emoji_tex_; }
|
||
|
||
/**
|
||
* @brief Reload theme images (background gradient + logo) from new paths
|
||
* @param bgPath Path to background image override (empty = use default)
|
||
* @param logoPath Path to logo image override (empty = use default)
|
||
*/
|
||
void reloadThemeImages(const std::string& bgPath, const std::string& logoPath);
|
||
|
||
// Load / recolor-per-theme the DragonX header logo (SVG rasterized to the theme accent). Called at
|
||
// the top of render() so the wizard, lock screen, and main header all show it.
|
||
void ensureLogoTexture();
|
||
|
||
// Wizard / first-run
|
||
WizardPhase getWizardPhase() const { return wizard_phase_; }
|
||
bool isFirstRun() const;
|
||
|
||
/**
|
||
* @brief Stop daemon and re-run the setup wizard
|
||
* Called from Settings. Daemon restarts automatically when wizard completes.
|
||
*/
|
||
void restartWizard();
|
||
|
||
/**
|
||
* @brief Restart the embedded daemon (e.g. after changing debug categories)
|
||
* Shows "Restarting daemon..." in the loading overlay while the daemon cycles.
|
||
*/
|
||
void restartDaemon();
|
||
// 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(); }
|
||
// Switch the active wallet: persist the new -wallet=<name>, stop the node, restart on it
|
||
// (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
|
||
// via the identity-scoped caches (P1). No-op if that wallet is already active.
|
||
// stopDaemonConfirmed: skip the "stop the running node?" confirmation (set true when re-entered from
|
||
// that dialog). salvage: start the target node with -salvagewallet (repair a corrupt wallet).
|
||
void switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed = false, bool salvage = false);
|
||
// Main-thread continuation: if a wallet switch's daemon failed to start, revert active_wallet_file.
|
||
void processWalletSwitchRevert();
|
||
|
||
// Wallet encryption helpers
|
||
void encryptWalletWithPassphrase(const std::string& passphrase);
|
||
// Post-encrypt daemon restart: the daemon shuts itself down after
|
||
// encryptwallet, so restart it off the main thread. Shared by the
|
||
// immediate and deferred encryption continuations. When
|
||
// announceRestartStatus is true, connection_status_ is updated first so
|
||
// the loading overlay explains the restart.
|
||
void restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus);
|
||
void unlockWallet(const std::string& passphrase, int timeout);
|
||
void lockWallet();
|
||
void changePassphrase(const std::string& oldPass, const std::string& newPass);
|
||
|
||
// Dialog triggers for encryption (from settings page)
|
||
void showEncryptDialog() {
|
||
show_encrypt_dialog_ = true;
|
||
encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
|
||
encrypt_status_.clear();
|
||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
|
||
memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_));
|
||
memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
|
||
enc_dlg_saved_passphrase_.clear();
|
||
enc_dlg_pin_status_.clear();
|
||
}
|
||
void showChangePassphraseDialog() { show_change_passphrase_ = true; }
|
||
void showDecryptDialog() {
|
||
show_decrypt_dialog_ = true;
|
||
wallet_security_workflow_.reset();
|
||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||
}
|
||
|
||
// Dialog triggers for PIN (from settings page)
|
||
void showPinSetupDialog() { show_pin_setup_ = true; pin_status_.clear(); }
|
||
void showPinChangeDialog() { show_pin_change_ = true; pin_status_.clear(); }
|
||
void showPinRemoveDialog() { show_pin_remove_ = true; pin_status_.clear(); }
|
||
bool hasPinVault() const;
|
||
// The PIN vault seals a passphrase the wallet no longer accepts (C87): changed in this session, or a
|
||
// PIN unlock got RPC -14. Session-only; the vault file is kept. Settings shows "PIN out of date".
|
||
bool isPinVaultStale() const { return pin_vault_stale_; }
|
||
// A wallet encrypt / passphrase change is in flight (PIN controls stay disabled meanwhile).
|
||
bool isEncryptInProgress() const { return encrypt_in_progress_; }
|
||
|
||
// Debug-options gate: does revealing the debug dropdown need re-authentication (a PIN vault or
|
||
// an encrypted wallet)? And verify the entered PIN/passphrase — cb(ok) is invoked on the main
|
||
// thread (PIN via the vault, else the wallet passphrase via RPC).
|
||
bool debugGateRequiresAuth() const;
|
||
void verifyDebugCredential(const std::string& secret, std::function<void(bool)> cb);
|
||
|
||
/// @brief Something finished on a background thread and waits for update() to apply it on the main thread
|
||
/// (the RPC worker lanes, the HTTP lane, lite wallet ops, the startup node-file job). main.cpp's idle loop
|
||
/// polls this every <= 200 ms (hasImmediateWork) and renders a frame when it is true, so the result shows
|
||
/// without input, even while the window is hidden. A new lane whose results are applied in update() adds
|
||
/// its term HERE.
|
||
bool hasPendingBackgroundResults() const;
|
||
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
|
||
std::string transactionSendProgressText() const;
|
||
std::string transactionRefreshProgressText() const;
|
||
|
||
// Copy a SECRET (seed phrase, private key) to the clipboard and arm an auto-clear: after a
|
||
// short delay the clipboard is wiped IF it still holds this secret (so we don't clobber
|
||
// something the user copied afterwards). Only a hash of the secret is retained, never the
|
||
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
|
||
void copySecretToClipboard(const std::string& secret);
|
||
void pumpSecretClipboardClear();
|
||
// Immediately clear the clipboard if it still holds the armed secret (ignores the 45s timer).
|
||
// Called on app shutdown so a copied key/seed does not outlive the process in the OS clipboard.
|
||
void clearSecretClipboardIfArmed();
|
||
bool isTransactionRefreshInProgress() const {
|
||
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
|
||
}
|
||
|
||
private:
|
||
friend class AppDaemonLifecycleRuntime;
|
||
friend class AppStopRuntime;
|
||
friend class AppStopTaskContext;
|
||
friend class AppDaemonLifecycleTaskContext;
|
||
|
||
// Global keyboard-shortcut handling, dispatched once per frame from update().
|
||
void handleGlobalShortcuts();
|
||
|
||
bool sendStopCommandSafely(rpc::RPCClient& client, const char* context);
|
||
void maybeFinishTransactionSendProgress();
|
||
// Shared body of createNewZAddress/createNewTAddress (which are thin public forwarders).
|
||
// `shielded` selects the z_getnewaddress/getnewaddress RPC, the "shielded"/"transparent" type
|
||
// string, and the z_addresses/t_addresses target (the new AddressInfo is pushed into both that
|
||
// list and state_.addresses). Lite builds derive locally via the controller and early-return.
|
||
void createNewAddress(bool shielded, std::function<void(const std::string&)> callback);
|
||
void upsertPendingSendTransaction(const std::string& opid,
|
||
const std::string& from,
|
||
const std::string& to,
|
||
double amount,
|
||
const std::string& memo,
|
||
double fee = 0.0);
|
||
// Work around a dragonxd note-selection bug: its z_sendmany picks notes to cover the recipient
|
||
// total but not the miner fee, so a shielded send whose largest notes sum exactly to the amount
|
||
// fails with "Insufficient shielded funds, have H, need H+fee" despite ample balance. When a
|
||
// failed opid matches that (H >= the requested amount), re-issue the send once with a tiny
|
||
// self-output that lifts the daemon's selection target past the boundary so it grabs another
|
||
// note; the recipient still receives the exact amount. Returns true if a retry was issued.
|
||
bool maybeRetrySendForFeeGap(const std::string& opid, const std::string& rawMsg);
|
||
void resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
|
||
double amount, double fee, const std::string& memo,
|
||
std::function<void(bool, const std::string&)> callback);
|
||
// Shared z_sendmany submit path for sendTransaction (single recipient, markFeeGapRetry=false)
|
||
// and resendWithFeeGapWorkaround (recipient + self-output, markFeeGapRetry=true). Owns the
|
||
// in-flight increment, the worker post + call, and the result closure (dirty flags, opid
|
||
// tracking, pending-send bookkeeping, callback delivery). Callers build `recipients` (and pass
|
||
// the recipient `to`/`amount`/`memo`/`fee` used to record the optimistic pending-send row).
|
||
// When markFeeGapRetry is set, the returned opid is recorded in send_feegap_retried_opids_ so a
|
||
// retry of a retry is reported as a real error.
|
||
// background=true (autonomous chat sends / note-buffer splits): keep the single-flight + opid
|
||
// accounting but DON'T raise the global "transaction in progress" UI (status is on the chat message).
|
||
void submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
|
||
const std::string& memo, const nlohmann::json& recipients,
|
||
const char* traceLabel, bool markFeeGapRetry,
|
||
std::function<void(bool, const std::string&)> callback,
|
||
bool background = false);
|
||
void markPendingSendTransactionSucceeded(const std::string& opid,
|
||
const std::string& txid);
|
||
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
||
bool restoreBalances);
|
||
// Apply a signed per-address balance delta for a pending send: walk z_addresses then
|
||
// t_addresses for `fromAddress` and clamp its balance at >=0. A positive `signedAmount`
|
||
// restores a debit; a negative one applies it. When `includeAggregates` is set, adjust the
|
||
// private/transparent bucket (chosen by the address's leading 'z') and totalBalance with the
|
||
// same clamp. Shared by the three pending-send delta sites so they can't drift. `includeAddress`
|
||
// off leaves the per-address balances alone (the Core poll refreshes only the aggregates).
|
||
void applyPendingSendDelta(const std::string& fromAddress, double signedAmount,
|
||
bool includeAggregates, bool includeAddress = true);
|
||
// Deliver a deferred z_sendmany result to its waiting UI callback once the opid
|
||
// reaches a terminal status. Returns true if a callback was registered (and fired).
|
||
bool invokeSendResultCallback(const std::string& opid, bool ok,
|
||
const std::string& result);
|
||
// Re-apply every pending send's debit on top of freshly fetched balances: the aggregates after a
|
||
// Core poll that carried a balance, the per-address balances after an address scan. Only onto
|
||
// fresh values, or a debit applied again on every tick would drain them.
|
||
void applyPendingSendBalanceDeltas(bool includeAggregateBalances, bool includeAddressBalances = true);
|
||
std::string transactionHistoryCacheWalletIdentity() const;
|
||
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
|
||
// Record the active wallet's cached metadata (balance, address count, identity, size) into the
|
||
// wallet index (wallets.json). Cheap + throttled: only writes when a value changed. markOpened
|
||
// stamps last-opened + syncedHere (call once per connect).
|
||
void updateWalletIndexForActiveWallet(bool markOpened);
|
||
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
|
||
// Shared main-thread continuations for a wallet unlock attempt, so the passphrase
|
||
// and PIN paths cannot drift. applyUnlockFailure applies the escalating lockout
|
||
// curve on every failed path (a PIN RPC failure used to skip it).
|
||
void applyUnlockSuccess(const std::string& passphrase, int timeout);
|
||
void applyUnlockFailure(const std::string& errorMessage);
|
||
// Restore the persisted unlock-failure count + lockout (once per lock-screen session, per wallet).
|
||
void restoreLockoutState();
|
||
// Non-credential failure (busy node, unknown error): message only, no attempt counted.
|
||
void applyUnlockNotCounted(const std::string& errorMessage);
|
||
// Clear all rescan + witness-rebuild progress/accumulator state. Shared by the four
|
||
// rescan-completion sites so a new witness field can't be forgotten in one copy.
|
||
void resetWitnessRescanProgress();
|
||
void loadTransactionHistoryCacheIfAvailable();
|
||
// Persist the tx-history snapshot. Coalesce: only a change that matters, rate-limited (content <= every
|
||
// 20 s, scan heights/confirmations/tip <= every 10 min). Now: anything not yet on disk, immediately (user
|
||
// action, disconnect, lock, exit). The write itself runs on history_writer_; this only copies the rows.
|
||
void storeTransactionHistoryCacheIfAvailable(
|
||
data::HistoryStoreMode mode = data::HistoryStoreMode::Coalesce);
|
||
void pumpTransactionHistoryStore(); // update(): release a deferred store, retry a failed write
|
||
void wipePendingTransactionHistoryCachePassphrase();
|
||
void resetTransactionHistoryCacheSession();
|
||
// The wallet locked: drop the cache key, keep the history and its scan progress (an unlock used to
|
||
// force a full re-scan of every address, every auto-lock cycle).
|
||
void lockTransactionHistoryCacheSession();
|
||
void pruneShieldedHistoryScanProgress();
|
||
// Forget the receive-scan heights so every address is scanned again. keepSentScan leaves the sent
|
||
// sweep's watermark (and the enrichment backlog), so a plain refresh doesn't cost a whole-wallet sweep.
|
||
void invalidateShieldedHistoryScanProgress(bool persistCache, bool keepSentScan = false);
|
||
|
||
// Auto-balance pool selection: drive the periodic hashrate refresh and apply a
|
||
// freshly-completed snapshot (weighted-random pick + optional miner restart).
|
||
void updatePoolAutoBalance();
|
||
void applyPoolAutoBalance(const util::PoolStatsService::Snapshot& snap);
|
||
|
||
// Subsystems
|
||
std::unique_ptr<rpc::RPCClient> rpc_;
|
||
std::unique_ptr<rpc::RPCWorker> worker_;
|
||
|
||
// Fast-lane: dedicated RPC connection + worker for 1-second mining polls.
|
||
// Runs on its own thread with its own curl handle so it never blocks behind
|
||
// the main refresh batch.
|
||
std::unique_ptr<rpc::RPCClient> fast_rpc_;
|
||
std::unique_ptr<rpc::RPCWorker> fast_worker_;
|
||
|
||
// HTTP lane: price / tickers / market chart / exchange candle GETs on their own thread, so a slow or
|
||
// blocked HTTP endpoint never queues in front of wallet RPCs on worker_ (and wallet scans never delay the
|
||
// price). Not tied to the daemon connection: started in init(), stopped only at shutdown. Work functions
|
||
// touch only http_cancel_ and their captures; results apply in MainCbs (main thread). http_cancel_ is
|
||
// declared BEFORE http_worker_ on purpose: members die in reverse order, and the lane's thread reads the
|
||
// flag until ~RPCWorker joins it.
|
||
std::atomic<bool> http_cancel_{false}; // set at shutdown: queued GETs return at once, an in-flight one aborts
|
||
std::unique_ptr<rpc::RPCWorker> http_worker_;
|
||
|
||
// Saved connection credentials (needed to open the fast-lane connection)
|
||
rpc::ConnectionConfig saved_config_;
|
||
|
||
util::CoalescingWriter file_writer_; // background atomic writer for settings.json / wallets.json
|
||
std::unique_ptr<config::Settings> settings_;
|
||
std::unique_ptr<wallet::LiteWalletController> lite_wallet_; // lite builds w/ linked backend
|
||
// Pending send_tab callback for an in-flight lite send (delivered in update() once the
|
||
// controller's async broadcast result arrives). Only one lite send runs at a time.
|
||
std::function<void(bool, const std::string&)> lite_send_callback_;
|
||
// One-shot guard: auto-open an existing lite wallet on the first update() tick (kept off
|
||
// init() so a slow initialize_existing network call doesn't freeze startup before the window).
|
||
bool lite_autoopen_done_ = false;
|
||
double lite_open_last_attempt_ = 0.0; // ImGui time of the last async open attempt (retry timer)
|
||
// Reason an existing lite wallet failed to auto-open (e.g. server unreachable). Surfaced in
|
||
// the UI so a stuck "disconnected" state isn't silent; cleared once a wallet opens.
|
||
std::string lite_open_error_;
|
||
// A forced lite rebuild (Network-tab server switch) deferred while a wallet op is in flight; update() retries it
|
||
// (rebuildLiteWallet). deferred_at bounds the wait (wallet::kLiteServerSwitchMaxDeferSeconds).
|
||
bool lite_rebuild_pending_ = false;
|
||
std::chrono::steady_clock::time_point lite_rebuild_deferred_at_{};
|
||
// Retire the current lite controller without blocking the UI thread (fail its pending op callbacks, stop its
|
||
// worker, shut its client down) and destroy it on a detached thread. lite_wallet_ is null afterwards.
|
||
void retireLiteWallet();
|
||
|
||
// HushChat (experimental; gated by DRAGONX_ENABLE_CHAT — inert when OFF). App owns the chat
|
||
// service so the transaction-refresh harvest can decrypt incoming memos into threaded messages.
|
||
// The identity is derived from the wallet's OWN SDXLite-compatible seed phrase (full-node
|
||
// z_exportmnemonic / lite exportSeed → the same KDF), so it is portable across both variants.
|
||
chat::ChatService chat_service_;
|
||
chat::ChatDatabase chat_db_; // persistent backing (seed-derived encryption at rest)
|
||
bool chat_identity_provisioned_ = false; // identity set on the service this session
|
||
bool chat_identity_fetch_in_flight_ = false; // a z_exportmnemonic worker job is pending
|
||
std::chrono::steady_clock::time_point chat_identity_retry_at_{}; // after a transient failure
|
||
// Bumped by resetChatSession() on every wallet change; an in-flight identity fetch captures the
|
||
// value at post time and its completion callback discards its (previous-wallet) secret if the id
|
||
// no longer matches — so wallet A's seed can't be provisioned under wallet B via a stale job.
|
||
int chat_session_generation_ = 0;
|
||
bool chat_identity_unavailable_ = false; // provisioning failed definitively (e.g. non-mnemonic wallet)
|
||
// Per-conversation "last seen" watermark (message timestamp) for unread tracking (Q1). Updated when a
|
||
// thread is viewed (markChatConversationSeen); wiped in resetChatSession so unread doesn't leak across
|
||
// wallets. In-memory only (resets on app restart).
|
||
std::map<std::string, std::int64_t> chat_seen_watermark_;
|
||
|
||
// ── Per-frame render caches (avoid O(N) recompute every frame; see the respective call sites) ──
|
||
// Chat nav-badge unread count — recomputed only when the store revision changes or after a short
|
||
// interval (mute/hide/seen changes don't bump the store). See App::chatUnreadCount().
|
||
mutable std::uint64_t chat_unread_rev_ = ~0ull; // ~0 forces the first compute
|
||
mutable int chat_unread_cached_ = 0;
|
||
mutable double chat_unread_computed_at_ = 0.0;
|
||
// Sidebar unconfirmed-tx badge — recomputed only when the tx list changes (keyed on tx_revision),
|
||
// not every frame. See App::render().
|
||
std::uint64_t sb_unconf_key_rev_ = 0; // sentinel 0 never matches: tx_revision starts at 1
|
||
int sb_unconf_count_ = 0;
|
||
// Daemon-memory probe is expensive (/proc scan on Linux, popen on macOS): it runs on this sampler's own
|
||
// thread, at most once per 3 s and only while a reader asks (Mining tab memory bar, Peers tab Memory cell).
|
||
// See App::getDaemonMemoryUsageMB(). Mutable: that accessor is const. Probes capture values only, so any
|
||
// member order is safe; shutdown() stops it.
|
||
mutable util::BackgroundSampler daemon_mem_sampler_;
|
||
|
||
// ── Chat note buffer (BOTH variants) ────────────────────────────────────────────────────────
|
||
// Each chat message is a shielded tx that spends a note; its change needs a few confirmations before
|
||
// it's spendable again (lite: backend ANCHOR_OFFSET+1 = 5; full node: z_sendmany minconf = 1), so
|
||
// rapid sends run out of verified funds. We keep a buffer of ~kChatBufferTarget small self-notes so a
|
||
// burst of messages each spends a separate verified note, refilled from change + background self-
|
||
// splits. Chat sends, self-splits and user sends share the wallet's single send channel; inflight_op_
|
||
// + (lite) lite_send_callback_ / (full node) send_submissions_in_flight_+pending_opids_ serialize them
|
||
// so exactly one send is ever outstanding. Implemented in app_network.cpp; pumped from update().
|
||
enum class LiteOpKind { None, ChatSend, ContactRequest, Split, UserSend };
|
||
struct LiteInflightOp {
|
||
LiteOpKind kind = LiteOpKind::None;
|
||
std::string echoLocalId; // ChatSend/ContactRequest: the echo to resolve when it completes
|
||
int sessionGen = 0; // chat_session_generation_ snapshot at submit (stale-guard)
|
||
double submittedAt = 0.0;
|
||
};
|
||
struct QueuedChatOp {
|
||
LiteOpKind kind = LiteOpKind::ChatSend;
|
||
chat::OutgoingChatMemos memos; // kept so a transient-funds retry re-broadcasts, never recomposes
|
||
std::string echoLocalId;
|
||
int sessionGen = 0;
|
||
int retries = 0;
|
||
};
|
||
LiteInflightOp inflight_op_; // the single chat/split op currently on the send channel
|
||
std::deque<QueuedChatOp> chat_send_queue_; // chat/contact sends awaiting a free channel + verified note
|
||
// Note-availability estimate between refreshes/scans: reset from a fresh count, decremented on each chat
|
||
// submit (the count lags a spend by a cycle, but the wallet still picks a fresh note per send). Zeroed on
|
||
// a transient-funds failure so we stop draining until the next refresh/scan restores the truth.
|
||
int chat_verified_note_budget_ = 0;
|
||
// Stuck-send notice: when a chat message sits queued ("Sending") because the wallet is offline or
|
||
// has no shielded funds for the fee, tell the user WHY once (throttled) — the send stays queued and
|
||
// goes out when the condition clears. cause: 0=none, 1=offline, 2=unfunded. See maybeNoticeStuckChatSend.
|
||
int chat_stuck_cause_ = 0;
|
||
double chat_stuck_since_ = 0.0; // ImGui::GetTime() when this cause began (dwell gate)
|
||
double chat_stuck_notice_at_ = 0.0; // next time a repeat notice is allowed (throttle)
|
||
int chat_pipeline_note_count_ = 0; // verified + maturing self-notes (drives shouldSplit)
|
||
std::uint64_t chat_verified_shielded_zat_ = 0; // verified shielded balance (split affordability)
|
||
bool chat_note_model_seen_ = false; // saw a refresh/scan carrying per-note visibility
|
||
// Single-split-in-flight guard: a self-split's OUTPUT notes are invisible until mined (~1 block), far
|
||
// longer than any wall-clock cooldown — so we permit only ONE outstanding split and clear the flag when
|
||
// the pipeline recovers (outputs mined) or a watchdog expires (a split that never mines mustn't wedge
|
||
// refill forever). Prevents runaway splitting that would drain balance into fees.
|
||
bool chat_split_outstanding_ = false;
|
||
double chat_split_submitted_at_ = 0.0; // scanClockNow() when the outstanding split was submitted (watchdog)
|
||
// Full-node only: a coordinator-owned z_listunspent worker scan feeds the per-note counts (the shared
|
||
// balance poll discards per-note data). Set with its scan ticket; cleared only by its reply.
|
||
bool chat_note_scan_in_flight_ = false;
|
||
double chat_note_scan_last_ = 0.0; // scanClockNow() of the last note-scan dispatch or address-scan feed
|
||
std::int64_t chat_note_scan_tip_ = -1; // the settled tip the note view was read at (-1: look again)
|
||
bool chat_note_rescan_requested_ = false; // a chat op finished: its notes changed
|
||
bool chat_note_first_look_sent_ = false; // the session's one explicit look (Chat page, no model)
|
||
services::ChatPayerMap chat_verified_funds_by_addr_; // verified notes by address (the chat payer view)
|
||
std::int64_t chat_split_hold_tip_ = -1; // a split failed at this tip: none before the next block
|
||
// Chat spends the payer view's producers (a note scan, an address pass) posted earlier can't see: worker_ is
|
||
// FIFO, so a reply reflects every spend submitted before it was posted, none after. Re-applied on its reply.
|
||
struct ChatSpendLogEntry {
|
||
std::uint64_t seq = 0;
|
||
std::string payer;
|
||
services::ChatPayerSpend spend = services::ChatPayerSpend::OneNote;
|
||
};
|
||
std::uint64_t chat_spend_seq_ = 0;
|
||
std::deque<ChatSpendLogEntry> chat_spend_log_;
|
||
std::int64_t chat_note_scan_spend_seq_ = -1; // the spend seq an outstanding note scan was posted at
|
||
std::int64_t addr_pass_spend_seq_ = -1; // ...and an outstanding address pass
|
||
void debitChatPayerLogged(const std::string& payer, services::ChatPayerSpend spend);
|
||
void reapplyChatSpendsAfter(std::uint64_t seq);
|
||
// The 0-conf inbox: a txcount probe per chat tick, the inbox scan when the wallet gained a tx.
|
||
std::int64_t chat_inbox_basis_txcount_ = -1; // txcount the last successful inbox scan is current for
|
||
double chat_inbox_last_scan_at_ = 0.0; // scanClockNow() at that scan's dispatch (safety net)
|
||
bool chat_inbox_scan_wanted_ = false; // the probe found a need the budget hasn't admitted yet
|
||
std::int64_t chat_inbox_wanted_txcount_ = -1; // the count that need was found at: the next basis
|
||
double chat_inbox_probe_at_ = 0.0; // scanClockNow() of the last probe
|
||
double chat_inbox_probe_ms_ = 0.0; // its cost (self-throttle)
|
||
bool chatInboxScanAllowed() const; // the inbox's gates (identity, the hard gates, the Chat page)
|
||
void admitChatInboxScan(); // ask the scan budget; post the inbox scan when admitted
|
||
// Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs
|
||
// may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads
|
||
// this cache rather than requiring the current model to have both — else the budget flickers to 0.
|
||
std::int64_t chat_last_chain_height_ = 0;
|
||
int chat_fast_scan_last_seen_ = -1; // dedup: last memo-note count logged by the 0-conf scan
|
||
|
||
// Coordinator helpers (both variants unless noted; see app_network.cpp).
|
||
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
|
||
// Full node: the z_listunspent note-buffer scan, on need (services::shouldScanChatNotes), under the scan
|
||
// budget (ChatNotes); also off the Chat page while a queued send or a split waits on it.
|
||
void refreshChatNoteBudgetNode();
|
||
// Feeds the SAME chat send-budget from an already-collected z_listunspent (the address refresh),
|
||
// so the dedicated scan above is skipped while the address refresh is active (dedup — see #3).
|
||
// feedTip: the tip that pass may vouch for (-1 when the tip moved too recently for the wallet notifier).
|
||
// spendSeq: the chat spend seq the pass was posted at (its reply re-applies the spends after it).
|
||
void updateChatNoteBudgetFromUnspent(const std::vector<services::NetworkRefreshService::UnspentNoteLite>& unspentNotes,
|
||
std::int64_t feedTip, std::uint64_t spendSeq);
|
||
void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
|
||
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
|
||
bool shouldSplitChatBuffer();
|
||
// What one split can spend: on the full node the richest verified payer (z_sendmany pays from ONE address).
|
||
std::uint64_t chatSplitAffordableZat() const;
|
||
bool broadcastSelfSplitLite(int noteCount); // lite: self-send minting noteCount reply-address notes
|
||
bool broadcastSelfSplitNode(int noteCount); // full node: z_sendmany self-send minting noteCount notes
|
||
void enqueueChatSend(LiteOpKind kind, const chat::OutgoingChatMemos& memos, const std::string& echoLocalId);
|
||
void onChatBroadcastResult(const LiteInflightOp& op, bool ok, const std::string& error);
|
||
static bool isTransientVerifiedFundsError(const std::string& error);
|
||
int chatConfsRequired() const; // verified-note confs threshold: 5 (lite) / 1 (full node)
|
||
public:
|
||
// Status-bar summary of the chat note buffer (empty when not applicable). Shown while the Chat tab is
|
||
// active so the buffer's state (ready / building / sending) is visible.
|
||
std::string chatBufferStatusText();
|
||
private:
|
||
public:
|
||
// Total unread incoming chat messages across all conversations (for the sidebar badge). 0 when the
|
||
// feature is off / no identity.
|
||
int chatUnreadCount() const;
|
||
// Mark a conversation read up to latestTs (called by the Chat tab while a thread is displayed).
|
||
void markChatConversationSeen(const std::string& cid, std::int64_t latestTs);
|
||
// Drop the seen-watermark for a conversation (used on revive-delete so a re-imported message — even one
|
||
// whose stamped time predates the deleted thread's last message — still badges as unread).
|
||
void forgetChatConversationSeen(const std::string& cid);
|
||
private:
|
||
// Provision the chat identity once the wallet seed is reachable+unlocked (per-tick, both
|
||
// variants); derives via deriveChatIdentityFromSecret and wipes the secret. No-op when the
|
||
// feature is off, already provisioned, in flight, or unavailable.
|
||
void maybeProvisionChatIdentity();
|
||
// Surface (throttled) why an outgoing chat message is stuck "Sending" — offline or unfunded.
|
||
void maybeNoticeStuckChatSend();
|
||
// Drop the in-memory chat identity + decrypted message store, lock the chat DB, and re-arm
|
||
// provisioning. MUST be called whenever the loaded wallet changes (switch / seed-migration adopt)
|
||
// so wallet A's private chat can't surface — or be signed with A's keys — under wallet B.
|
||
void resetChatSession();
|
||
// One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per
|
||
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
|
||
void maybeRemindSeedBackup();
|
||
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
|
||
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)
|
||
|
||
// Seed-wallet migration (Phase 1: create a new mnemonic wallet in isolation, no funds moved).
|
||
void beginCreateSeedWallet(); // starts the isolated create on a background thread
|
||
void pumpSeedMigration(); // main thread: pick up background progress/result each frame
|
||
// Phase 2: sweep all legacy funds into the new wallet, then adopt it as the primary wallet.
|
||
void refreshSeedMigrationBalance(); // query the legacy total (shown on the Sweep step)
|
||
// W3-3: the terminal callback for the sweep opid, shared by the initial submit and a resume
|
||
// re-track. `resumed` selects the failure behaviour: a fresh sweep that fails -> Error; a resumed
|
||
// opid the daemon no longer knows (stale) -> back to the dismissable Sweep gate (re-check balance).
|
||
std::function<void(bool, const std::string&)> makeSweepCompletionCallback(bool resumed);
|
||
void beginSweepToSeedWallet(); // z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"] -> dest
|
||
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);
|
||
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr — chat IDENTITY (reply-to)
|
||
// A spendable z-address that can actually PAY `need` (a fee, or a split's notes plus its fee), preferring
|
||
// the identity reply address; from the chat note view's verified notes when there is one. The reply-to in
|
||
// the memo stays the identity address, so paying from a different funded note is transport-transparent.
|
||
// Empty if no z-address can cover it.
|
||
std::string chatPayFromZaddr(double need) const;
|
||
std::string generateChatLocalId(const char* prefix, int numBytes) const; // unique echo id / cid
|
||
// Broadcast the memos and, when the async op resolves, flip the echo (echoLocalId) to Sent/Failed.
|
||
bool broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::string& echoLocalId); // true if submitted
|
||
bool broadcastChatMemosLite(const chat::OutgoingChatMemos& memos); // lite two-recipient send
|
||
void ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model); // lite chat receive harvest
|
||
// Full-node 0-conf fast path on the Chat page: a txcount probe each chat tick, and the inbox scan (the chat
|
||
// reply address at minconf=0) when the wallet gained a tx, as the scan budget admits it (ChatFast), so
|
||
// incoming messages surface at mempool speed. Hidden conversations are skipped (they still un-hide via the
|
||
// normal confirmed harvest). Self-gated; no-op without a chat identity.
|
||
void fastScanChatMemos();
|
||
bool chat_fast_scan_in_flight_ = false; // the inbox probe or scan is out; cleared only by its reply
|
||
float chat_fast_scan_accum_ = 0.0f; // seconds since the last chat tick
|
||
bool font_rebuild_requested_ = false; // set by requestFontRebuild(); consumed in preFrame()
|
||
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
||
bool lite_firstrun_dismissed_ = false;
|
||
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
|
||
bool lite_unlock_prompt_ = false;
|
||
// One-shot: prompt to unlock on startup once we learn the auto-opened wallet is encrypted+locked.
|
||
bool lite_startup_lock_checked_ = false;
|
||
std::unique_ptr<daemon::DaemonController> daemon_controller_;
|
||
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
||
// Serialized async mining-control queue: xmrig start/stop (SIGTERM->SIGKILL->join, up to ~3s) run on
|
||
// this dedicated FIFO thread instead of the render thread, so the UI never blocks and stop/start
|
||
// ordering is preserved across the ~13 call sites. (M-03/L-06/L-08/L-09/L-13)
|
||
std::thread mining_ctl_thread_;
|
||
std::mutex mining_ctl_mutex_;
|
||
std::condition_variable mining_ctl_cv_;
|
||
std::deque<std::function<void()>> mining_ctl_queue_;
|
||
bool mining_ctl_stop_ = false;
|
||
void postMiningControl(std::function<void()> job); // enqueue a blocking xmrig op onto the FIFO thread
|
||
void signalMiningControlStop(); // signal only (no join): safe on the UI thread
|
||
void stopMiningControlThread(); // signal + join the control thread (shutdown)
|
||
void finishMinerShutdown(); // join control thread -> xmrig stop(3000); background only
|
||
// Builds the pool config on the main thread and posts stop(if running)+start to the control thread.
|
||
// retargetOnly: a thread-count change, decided on the control thread (never starts a miner).
|
||
void postPoolMinerJob(int threads, bool retargetOnly);
|
||
// Coalesced thread-count picks (main thread only; see services/mining_thread_commit.h).
|
||
services::MiningThreadCommit mining_thread_commit_;
|
||
bool mining_thread_drag_ = false; // the thread grid is mid-drag (set by the Mining tab)
|
||
bool mining_threads_save_pending_ = false; // settings hold a picked count that is not on disk yet
|
||
int mining_solo_threads_pending_ = 0; // settled pick waiting for an in-flight setgenerate
|
||
void tickMiningThreadCommit(); // per frame, from update()
|
||
// Auto-balance runtime state (pool mining, full-node only). The service fetches
|
||
// pool hashrates off-thread; the RNG drives the weighted-random pick.
|
||
util::PoolStatsService pool_stats_service_;
|
||
std::mt19937 balance_rng_;
|
||
long long last_balance_eval_ms_ = 0; // steady-clock ms of the last refresh kick
|
||
bool balance_refresh_pending_ = false; // UI asked for an immediate refresh
|
||
bool balance_snapshot_seen_ = false; // the current in-flight snapshot was applied
|
||
bool exchanges_fetch_started_ = false; // CoinGecko tickers fetch in flight or done (once per session)
|
||
bool chart_fetch_in_flight_ = false; // a market_chart history fetch is on the worker
|
||
// Failed market fetches back off instead of retrying: the Market tab calls these every frame, so a
|
||
// failure (offline, rate-limited) used to retry on the very next frame, ~20 requests a second.
|
||
util::FetchBackoff exchanges_backoff_;
|
||
util::FetchBackoff market_chart_backoff_;
|
||
util::FetchBackoff exchange_chart_backoff_; // for exchange_chart_fail_key_ only: other pairs still load
|
||
std::string exchange_chart_fail_key_;
|
||
std::chrono::steady_clock::time_point market_manual_retry_at_{};
|
||
bool price_fetch_failed_ = false; // last price fetch failed (connectivity probe for the backoffs)
|
||
bool price_fetch_in_flight_ = false; // a price GET is on http_worker_ (main thread only)
|
||
// Per-exchange candle chart (refreshExchangeChart): which pair the loaded series is for, an
|
||
// in-flight guard, and a slow refresh timer (candles move slowly, like the aggregate chart).
|
||
std::string exchange_chart_key_; // "<identifier>:<BASE>/<QUOTE>" of the loaded series ("" = none)
|
||
bool exchange_chart_fetch_in_flight_ = false;
|
||
std::chrono::steady_clock::time_point exchange_chart_last_fetch_{};
|
||
// Per-pair candle cache: switching back to a recently-viewed venue loads instantly (no re-fetch)
|
||
// instead of overwriting the single active buffer. Keyed like exchange_chart_key_.
|
||
struct ExchangeChartCache {
|
||
std::vector<std::pair<std::time_t, double>> closeIntraday, closeDaily;
|
||
std::vector<data::Candle> ohlcIntraday, ohlcDaily;
|
||
std::chrono::steady_clock::time_point fetchedAt{};
|
||
};
|
||
std::unordered_map<std::string, ExchangeChartCache> exchange_chart_cache_;
|
||
// Startup node files (Windows: extract the bundled params/node/miner; Linux/macOS: copy them from the exe
|
||
// dir; full node: then verify the Sapling params) on the "extract-node-files" async task. Declared BEFORE
|
||
// async_tasks_ so it outlives the task threads if the App is destroyed without shutdown().
|
||
std::shared_ptr<resources::StartupProvision> startup_files_;
|
||
std::thread::id ui_thread_id_{}; // set first thing in init(); startEmbeddedDaemon() never blocks it
|
||
bool daemon_start_after_files_ = false; // UI thread only: a daemon start was deferred while the job ran
|
||
void startStartupFilesProvision();
|
||
void pollStartupFiles(); // UI thread, every update()
|
||
void renderStartupFilesChip(); // UI thread, top of render()
|
||
// The long wallet RPC lane (C61). Its task runs on async_tasks_, so it is declared BEFORE async_tasks_ and
|
||
// outlives the task threads. Aborted at quit (beginShutdown / shutdown); never to make way for another flow:
|
||
// those are refused while it is active (nodeBusyForStop), because an aborted client does not stop the scan.
|
||
rpc::LongCallLane long_lane_;
|
||
util::AsyncTaskManager async_tasks_;
|
||
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
|
||
|
||
// Wallet state
|
||
WalletState state_;
|
||
|
||
// Shutdown state
|
||
std::atomic<bool> shutting_down_{false};
|
||
std::atomic<bool> shutdown_complete_{false};
|
||
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
||
GuardedStatus shutdown_status_; // thread-safe: written by the shutdown thread, read by the UI (M-05)
|
||
std::thread shutdown_thread_;
|
||
std::thread miner_stop_thread_; // disconnect-only quit paths: miner teardown off the UI thread
|
||
// Confirm-before-stopping-daemon-mid-witness-rebuild guard (see beginShutdown / renderDaemonStopConfirm)
|
||
bool pending_shutdown_confirm_ = false; // a quit is deferred, waiting to open the confirm modal
|
||
bool daemon_stop_confirm_open_ = false; // the confirm modal is currently showing
|
||
bool shutdown_confirmed_ = false; // user chose to proceed — bypass the guard on re-entry
|
||
bool shutdown_keep_daemon_override_ = false; // user chose "keep node running" for this shutdown only
|
||
float shutdown_timer_ = 0.0f;
|
||
std::chrono::steady_clock::time_point shutdown_start_time_;
|
||
|
||
// One stop session at a time (stopEmbeddedDaemon), and the shutdown screen's view of it.
|
||
std::mutex stop_mutex_;
|
||
std::atomic<bool> stop_session_active_{false};
|
||
std::atomic<int> stop_user_choice_{0}; // daemon::StopUserChoice, set by the shutdown screen
|
||
std::atomic<bool> stop_kill_offered_{false}; // the node looks stuck: the screen may offer "Kill"
|
||
std::atomic<int> stop_idle_minutes_{0};
|
||
std::mutex stop_client_mutex_;
|
||
rpc::RPCClient* active_stop_client_ = nullptr; // aborted on "Leave running" so a pending attempt ends at once
|
||
bool stop_kill_confirm_ = false;
|
||
void chooseStopAction(daemon::StopUserChoice choice);
|
||
|
||
// Daemon restart (e.g. after changing debug log categories)
|
||
std::atomic<bool> daemon_restarting_{false};
|
||
std::atomic<bool> daemon_restart_pending_{false}; // updated binary installed, awaiting a restart
|
||
|
||
// Wallet-switch failure recovery: the switch worker sets wallet_switch_failed_ when the new
|
||
// wallet's daemon won't start/stay up; the main loop then reverts active_wallet_file to
|
||
// wallet_switch_prev_file_ (settings writes stay on the main thread) so a broken wallet isn't
|
||
// persisted across restarts. daemon_restarting_ stays set until the revert re-arms the reconnect.
|
||
std::atomic<bool> wallet_switch_failed_{false};
|
||
// Distinguishes the failure reason for the revert message: true when the switch failed because the
|
||
// running node wouldn't release the RPC port in time (not because the target wallet is bad).
|
||
std::atomic<bool> switch_stop_failed_{false};
|
||
// True from a switch until the new wallet's daemon actually connects (onConnected). If the daemon
|
||
// instead crash-wedges (a wallet that fails LATE in init, past the fast start grace), the connect
|
||
// loop flags a revert so a broken wallet still can't stick.
|
||
std::atomic<bool> wallet_switch_pending_confirm_{false};
|
||
std::string wallet_switch_prev_file_;
|
||
// Confirm-before-stop for a switch that would stop an ADOPTED (externally-running) node. switchToWallet
|
||
// sets these and defers to renderSwitchStopDaemonDialog; confirming re-calls switchToWallet(w, true).
|
||
bool show_switch_stop_daemon_confirm_ = false;
|
||
std::string pending_switch_wallet_file_;
|
||
// Block-database recovery: set when the embedded node aborts because its block DB is unreadable
|
||
// (a daemon-vs-chaindata format mismatch after an update, or a corrupt index). While set, the
|
||
// connect loop STOPS crash-restarting into the same abort and offers a one-click reindex instead.
|
||
bool block_db_reindex_available_ = false; // node needs its block DB rebuilt (gates restart loop)
|
||
bool show_block_db_reindex_confirm_ = false; // auto-shown offer dialog
|
||
// Wallet auto-recovery: the daemon moved wallet.dat to wallet.<ts>.bak and loaded a salvaged copy
|
||
// (BDB-verify failure — often a false positive from stale/cross-platform env state). We warn loudly
|
||
// so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session.
|
||
bool wallet_auto_recovered_ = false; // a salvage happened this session
|
||
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session
|
||
bool wallet_degraded_ = false; // v1.3.0+ opened the wallet in DEGRADED mode (no new HD keys)
|
||
bool wallet_degraded_warned_ = false; // guard: surface the degraded-mode notice once per session
|
||
bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog
|
||
// Complementary on-disk safety net for a salvage we DIDN'T witness this launch (happened on a prior
|
||
// run, or under an external daemon whose startup output we never captured): if the active wallet loads
|
||
// empty while a sibling wallet file in the datadir still holds keys, warn once so the user's funds
|
||
// (likely in a wallet.<ts>.bak) aren't mistaken for loss. See maybeWarnEmptyWalletWithFundedSiblings().
|
||
struct FundedSibling { std::string fileName; int transparentKeys = 0; int shieldedKeys = 0; };
|
||
bool show_empty_wallet_warning_ = false; // auto-shown warning modal
|
||
bool empty_wallet_warn_checked_ = false; // evaluated this wallet-open already (reset in onConnected)
|
||
bool empty_wallet_scan_in_flight_ = false; // a sibling scan is running (main-thread only)
|
||
bool empty_wallet_has_salvage_bak_ = false; // modal variant: a funded salvage .bak → offer Restore
|
||
std::vector<FundedSibling> empty_wallet_funded_siblings_; // scan result (main-thread only)
|
||
// The recovery dialog is the ONE authoritative surface: it stays open through the async rebuild/
|
||
// restore, driven Offer → Working → Done/Failed (pumpWalletRestore sets the outcome). Presentation
|
||
// only — the fund-safety file ops in rebuildWalletDatabase()/restoreOriginalWallet() are unchanged.
|
||
enum class RecoveryPhase { Offer, Working, Done, Failed };
|
||
RecoveryPhase recovery_phase_ = RecoveryPhase::Offer;
|
||
int recovery_outcome_sev_ = 0; // 0 ok / 1 warn / 2 error, set at Done/Failed
|
||
std::string recovery_outcome_msg_; // honest result string for the Done/Failed body
|
||
bool recovery_last_action_rebuild_ = false; // which handler ran (for "try the other option")
|
||
// After a successful repair the daemon restarts with a full rescan — minutes long, and it won't
|
||
// answer RPC yet. This makes the loading overlay show a calm "finishing your wallet repair" screen
|
||
// (instead of the generic "daemon stuck / RPC timeout / restart daemon" text) and suppresses the
|
||
// daemon-crash toast. Set on repair success; cleared on connect (onConnected).
|
||
bool post_recovery_rescan_ = false;
|
||
double post_recovery_rescan_since_ = 0.0; // stamped on first overlay frame (ImGui::GetTime)
|
||
// "Restore original wallet" background op: worker sets these under the mutex, pumpWalletRestore()
|
||
// (main thread) shows the result. 0 = success, 1 = warning, 2 = error.
|
||
std::mutex wallet_restore_mutex_;
|
||
bool wallet_restore_done_ = false;
|
||
int wallet_restore_severity_ = 0;
|
||
std::string wallet_restore_msg_;
|
||
// Live progress for the switch modal: it stays open from confirm through stop → wait-for-exit → start →
|
||
// reconnect and auto-closes when the new node connects (onConnected). Phase is worker-updated;
|
||
// dialog_open_ gates rendering — both atomic since onConnected/the worker may run off the main thread.
|
||
// error_ is written only on the main thread (processWalletSwitchRevert) and read by the (main-thread) UI.
|
||
enum class WalletSwitchPhase : int { None = 0, Stopping, Starting, Reconnecting, Failed };
|
||
std::atomic<int> wallet_switch_phase_{0};
|
||
std::atomic<bool> wallet_switch_dialog_open_{false};
|
||
std::string wallet_switch_error_;
|
||
double wallet_switch_started_time_ = 0.0; // ImGui::GetTime() captured on first progress frame (elapsed)
|
||
// Set when a failed switch's node output indicates the target wallet is CORRUPT — the Failed modal then
|
||
// offers a one-click "-salvagewallet" repair, retrying the switch to wallet_switch_target_file_.
|
||
std::atomic<bool> switch_wallet_corrupt_{false};
|
||
std::string wallet_switch_target_file_; // the wallet we were switching TO (for a salvage retry)
|
||
|
||
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
|
||
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
||
std::atomic<int> pending_delete_result_{-1};
|
||
std::atomic<bool> pending_reinstall_node_busy_{false}; // reinstall gave up: the old node never exited
|
||
// A worker stopped the node for an operation and it didn't exit: the operation was cancelled. Told (and,
|
||
// for a rescan/repair, its armed state undone) on the UI thread. Bits: kStopFailed*.
|
||
static constexpr int kStopFailedNotify = 1, kStopFailedUndoRescan = 2;
|
||
std::atomic<int> pending_stop_failed_{0};
|
||
|
||
// Encryption state check timeout
|
||
float encryption_check_timer_ = 0.0f;
|
||
|
||
// UI State
|
||
bool quit_requested_ = false;
|
||
bool show_demo_window_ = false;
|
||
bool show_about_ = false;
|
||
bool show_faq_ = false;
|
||
bool show_import_key_ = false;
|
||
bool show_export_key_ = false;
|
||
bool show_backup_ = false;
|
||
bool show_seed_backup_ = false;
|
||
|
||
// Seed-phrase backup dialog state. seed_backup_phrase_ holds a SECRET (the revealed
|
||
// mnemonic) and is wiped with sodium_memzero when the dialog closes.
|
||
std::string seed_backup_phrase_;
|
||
std::string seed_backup_status_;
|
||
bool seed_backup_status_ok_ = true; // colour the status line by outcome (green save vs red fail)
|
||
bool seed_backup_fetch_started_ = false;
|
||
bool seed_backup_loading_ = false;
|
||
bool seed_backup_no_mnemonic_ = false;
|
||
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
|
||
bool seed_backup_nudged_this_session_ = false; // nudged (or definitively classified) once this session;
|
||
// unlike the persisted "reminded" flag it re-nudges next run
|
||
std::chrono::steady_clock::time_point seed_backup_reminder_retry_at_{}; // after an inconclusive 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
|
||
// once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a
|
||
// capable daemon can migrate; Incapable = the daemon lacks z_exportmnemonic (can't tell / can't
|
||
// migrate). Reset to Unknown on wallet switch so it re-probes the new wallet.
|
||
enum class WalletSeedStatus { Unknown, HasMnemonic, NoMnemonic, Incapable };
|
||
WalletSeedStatus wallet_seed_status_ = WalletSeedStatus::Unknown;
|
||
bool wallet_seed_status_in_flight_ = false;
|
||
int wallet_seed_status_attempts_ = 0; // give up (Incapable) after a few transient probe failures
|
||
void probeWalletSeedStatus(); // one-shot per connect; classifies the wallet's mnemonic status
|
||
|
||
// --- Seed-wallet migration (Phase 1: create; Phase 2: sweep + adopt) ---
|
||
enum class SeedMigrationStep { Intro, Working, ShowSeed, Sweep, Sweeping, Confirming, Adopting, Done, Error };
|
||
bool show_seed_migration_ = false;
|
||
SeedMigrationStep seed_migration_step_ = SeedMigrationStep::Intro;
|
||
|
||
// Intro pre-flight: probe the current wallet before offering to create a seed wallet, so we can
|
||
// skip a pointless migration (AlreadyMnemonic → offer backup) or explain why it can't run
|
||
// (DaemonTooOld). Set from beginSeedMigrationPrecheck()'s async callback (main thread).
|
||
enum class SeedMigrationPrecheck { Pending, Legacy, AlreadyMnemonic, DaemonTooOld, CheckFailed };
|
||
SeedMigrationPrecheck seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
|
||
bool seed_migration_precheck_started_ = false;
|
||
bool seed_migration_balance_loaded_ = false; // Sweep step: distinguishes "0 funds" from "not loaded yet"
|
||
bool seed_migration_nofunds_confirmed_ = false; // "replace my wallet" gate for the no-funds adopt
|
||
|
||
// "A newer node is bundled — update?" startup prompt (see maybeOfferDaemonUpdate).
|
||
bool show_daemon_update_prompt_ = false;
|
||
unsigned long long daemon_update_bundled_size_ = 0; // bundled daemon size the prompt offers
|
||
bool seed_migration_in_flight_ = false; // main-thread guard while the bg create task runs
|
||
std::string seed_migration_seed_; // SECRET — the revealed phrase, wiped on close
|
||
std::string seed_migration_dest_; // new shielded z-address (Phase 2 sweep target)
|
||
std::string seed_migration_temp_dir_; // temp datadir root holding the new wallet (kept)
|
||
bool seed_migration_backed_up_ = false; // "I've written it down" confirmation
|
||
bool seed_migration_understood_ = false; // 2nd ack: funds recoverable only via the phrase
|
||
bool seed_migration_confirm_discard_ = false; // inline "are you sure" gate for Discard (never persisted)
|
||
bool seed_migration_confirm_sweep_ = false; // inline "are you sure" gate for Sweep (never persisted)
|
||
std::string seed_migration_status_; // main-thread display (progress / error text)
|
||
double seed_migration_balance_ = 0.0; // legacy total to sweep (shown on the Sweep step)
|
||
std::string seed_migration_sweep_txid_; // the z_mergetoaddress sweep transaction id
|
||
int seed_migration_sweep_confs_ = 0; // confirmations of the sweep tx (adopt gate: >= 1)
|
||
double seed_migration_legacy_remaining_ = -1.0; // legacy balance after sweep (-1 = unknown)
|
||
float seed_migration_poll_timer_ = 0.0f; // throttles the Confirming-step poll
|
||
bool seed_migration_confirm_in_flight_ = false; // guards the confirm poll
|
||
// Cross-thread handoff from the background create task (guarded by seed_migration_mutex_).
|
||
std::mutex seed_migration_mutex_;
|
||
std::string seed_migration_progress_; // latest progress line
|
||
bool seed_migration_done_ = false; // create result ready to consume
|
||
bool seed_migration_ok_ = false;
|
||
std::string seed_migration_r_seed_, seed_migration_r_dest_, seed_migration_r_tmp_, seed_migration_r_err_;
|
||
// Cross-thread handoff from the background adopt task (guarded by seed_migration_mutex_).
|
||
bool seed_migration_adopt_done_ = false;
|
||
bool seed_migration_adopt_ok_ = false;
|
||
std::string seed_migration_adopt_err_;
|
||
|
||
// Embedded daemon state
|
||
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
|
||
// Written by the DaemonController state callback (which fires on EmbeddedDaemon's monitor thread)
|
||
// AND read on the UI thread — so all access goes through set/getDaemonStatus() under this mutex.
|
||
// getDaemonStatus() returns a COPY: a returned reference could dangle if the monitor thread
|
||
// reassigned the string mid-read (data race / use-after-free).
|
||
mutable std::mutex daemon_status_mutex_;
|
||
std::string daemon_status_; // guarded by daemon_status_mutex_ — use set/getDaemonStatus()
|
||
void setDaemonStatus(std::string s);
|
||
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
|
||
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
|
||
std::atomic<bool> daemon_output_reset_pending_{false}; // lifecycle thread -> UI thread
|
||
std::atomic<size_t> daemon_output_reset_to_{0};
|
||
// What the node's own output says it is doing (witness rebuild / startup replay). Fed on the UI thread
|
||
// from the captured output; node_rebuilding_ is its snapshot for the shutdown thread's stop loop.
|
||
daemon::DaemonBusyTracker daemon_busy_;
|
||
std::atomic<bool> node_rebuilding_{false};
|
||
std::atomic<bool> daemon_busy_reset_pending_{false}; // set from any thread; applied by feedDaemonBusy()
|
||
std::chrono::steady_clock::time_point rpc_busy_since_{}; // epoch = not busy
|
||
std::function<void()> raise_window_;
|
||
// S6: once the node is too busy to answer (a reply timeout or a full queue), the polling clients
|
||
// (rpc_, fast_rpc_) hold back everything that needs cs_main, and one canary request on its own
|
||
// client waits for the node to answer again. rpc_busy_gate_ is shared with those clients.
|
||
std::shared_ptr<rpc::RpcBusyGate> rpc_busy_gate_ = std::make_shared<rpc::RpcBusyGate>();
|
||
rpc::ConnectRetryBackoff connect_backoff_; // S8: per-failure-kind delay before the next connect attempt
|
||
std::thread canary_thread_;
|
||
std::atomic<int> canary_state_{0}; // CanaryState
|
||
std::mutex canary_mutex_;
|
||
rpc::RPCClient* canary_client_ = nullptr; // aborted on shutdown
|
||
rpc::ConnectRetryBackoff canary_backoff_; // a canary refused as queue-full waits before the next
|
||
std::chrono::steady_clock::time_point canary_retry_at_{};
|
||
void pumpRpcBusyCanary();
|
||
// A node this session didn't launch (left running by an earlier session, or found running): feed the busy
|
||
// tracker from its <datadir>/debug.log, while that log is tied to the live process (see
|
||
// daemon::externalLogFeedOffset). Our own running node's output feeds the tracker directly. UI thread.
|
||
void pumpExternalDaemonLog();
|
||
long externalNodePid() const; // that node: our ownership record, or (POSIX) its hushd.pid; 0 if none
|
||
bool busyTrackerIsLive() const; // the tracker describes a node that is running now
|
||
bool ext_log_tied_ = false;
|
||
long ext_log_pid_ = 0;
|
||
std::uintmax_t ext_log_offset_ = 0;
|
||
double ext_log_next_at_ = 0.0;
|
||
long ext_log_miss_pid_ = 0; // pid whose banner we failed to find
|
||
std::uintmax_t ext_log_miss_size_ = 0; // file size at that miss
|
||
double ext_log_miss_since_ = 0.0; // when this pid first missed (fresh-node grace)
|
||
double ext_log_retry_at_ = 0.0; // next allowed re-scan (ImGui::GetTime)
|
||
double ext_log_backoff_ = 0.0; // current backoff seconds (0 until grace elapses)
|
||
void abortRpcBusyCanary();
|
||
void feedDaemonBusy(const std::string& chunk);
|
||
void resetDaemonBusy();
|
||
std::chrono::steady_clock::time_point busy_first_seen_{}; // when the tracker last turned busy (UI thread)
|
||
// What to tell the user while the node is busy (overlay, banner, quit prompt). busy=false: it isn't.
|
||
struct NodeBusyText {
|
||
bool busy = false;
|
||
bool rebuilding = false;
|
||
std::string title; // e.g. "Node busy: rebuilding note witnesses — do not close"
|
||
std::string detail; // running time, what it's doing, progress when the node reports any
|
||
std::string elapsed; // "12:03"
|
||
std::string compact; // one line for the banner: "Running for 12:03 · Reading blocks: 45438 / 62583"
|
||
};
|
||
NodeBusyText nodeBusyText() const;
|
||
|
||
// Export/Import state
|
||
char export_result_[256] = {0}; // SECRET exported key — fixed buffer so it can be sodium_memzero'd
|
||
bool export_in_progress_ = false; // async key fetch running (spinner + disable Export)
|
||
bool export_error_ = false; // last export returned no key (locked wallet / failure)
|
||
char import_key_input_[512] = {0};
|
||
char import_clip_preview_[64] = {0}; // trimmed clipboard preview (<=48 chars + NUL); SECRET, wiped
|
||
uint8_t import_clip_class_ = 0; // 0 none / 1 ok / 2 wrong-type / 3 unrecognized
|
||
double import_clip_next_at_ = 0.0; // ImGui::GetTime() of next allowed clipboard read
|
||
bool import_clip_hover_prev_ = false; // hover edge
|
||
std::string export_address_;
|
||
std::string import_status_;
|
||
bool import_success_ = false;
|
||
bool import_key_reveal_ = false; // show the key in plaintext (default masked)
|
||
bool import_in_progress_ = false; // an import + rescan is running (disable/spinner)
|
||
std::string import_result_address_; // address imported on success (shown as a copy field)
|
||
bool import_view_mode_ = false; // dialog mode: false = spending key, true = viewing key
|
||
char import_key_scan_height_[16] = {0}; // optional rescan start height (shielded spend / viewing-key imports)
|
||
// --- Sweep: import a spending key then move all its funds to one of your own addresses, instead
|
||
// of keeping the key in the wallet (spending-key / non-view mode only). ---
|
||
bool import_sweep_mode_ = false; // sweep instead of a plain import
|
||
int sweep_dest_mode_ = 0; // destination: 0 = fresh shielded address, 1 = an existing one
|
||
char sweep_dest_pick_[128] = {0}; // chosen existing destination (sweep_dest_mode_ == 1)
|
||
enum class SweepStep { Idle, Running, Done, Error };
|
||
SweepStep sweep_step_ = SweepStep::Idle;
|
||
std::string sweep_status_; // progress / error text
|
||
std::string sweep_txid_; // sweep transaction id (on success)
|
||
std::string sweep_dest_shown_; // the destination address the funds were swept to
|
||
std::string backup_status_;
|
||
bool backup_success_ = false;
|
||
|
||
// Connection
|
||
std::string connection_status_ = "Disconnected";
|
||
bool rpc_auth_failed_ = false; // last connect hit an auth failure (bad rpcuser/rpcpassword); drives the banner
|
||
bool connection_in_progress_ = false;
|
||
bool remote_rpc_plaintext_warning_shown_ = false;
|
||
// Startup daemon-launch diagnostics: bound the "RPC port busy, no config" wait before warning,
|
||
// and show the embedded-daemon start failure (binary/params/spawn) only once. Reset on connect.
|
||
int daemon_wait_attempts_ = 0;
|
||
bool daemon_start_error_shown_ = false;
|
||
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
|
||
bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active
|
||
bool refresh_policy_background_ = false; // whether the background refresh profile is active
|
||
// Sync-settle hysteresis + adaptive balance-poll throttle. Balance polling (z_gettotalbalance) is
|
||
// O(mapWallet) and holds the daemon's cs_main, which starves block connection on a large shielded
|
||
// wallet — so we keep the low-impact profile briefly after catching up, and back the balance poll
|
||
// off in proportion to its own measured cost. See effectivelySyncing() / balanceRefreshDue().
|
||
bool was_core_syncing_ = false; // previous Core-refresh sync state, to detect the caught-up edge
|
||
std::time_t sync_settle_until_ = 0; // hold the sync-throttle until this wall-clock time (0 = not settling)
|
||
bool force_balance_refresh_ = false; // a wallet mutation forces the next balance poll through the throttle
|
||
// Bumped with each forced-refresh request, so the poll that carried the forced balance clears the
|
||
// flag only if no newer request came in meanwhile (see requestForcedBalanceRefresh()).
|
||
std::uint32_t force_balance_seq_ = 0;
|
||
// The exact minconf=0 total of the last balance poll: the minconf=1 call is skipped while it holds
|
||
// still and nothing is pending (NetworkRefreshService::CoreRefreshPlan).
|
||
std::optional<std::int64_t> last_total_balance_zat_;
|
||
// Force the next balance poll through the throttle (the user's own send). Cleared by the Core poll
|
||
// that actually sent the forced balance call, not at dispatch, so a dispatch the job queue refuses
|
||
// (a Core job already in flight) or one that skips the balance (node behind or busy) keeps it.
|
||
void requestForcedBalanceRefresh() { force_balance_refresh_ = true; ++force_balance_seq_; }
|
||
// A wallet mutation committed (a User/Auto op succeeded, or a send returned no opid): re-read what it changed.
|
||
void markWalletMutated(bool forceBalance);
|
||
bool hasPendingUserOperations() const {
|
||
return services::NetworkRefreshService::anyUserOperationPending(pending_opids_, op_origin_);
|
||
}
|
||
// One budget for every wallet-wide scan the GUI sends on its own (balance, addresses, history, the
|
||
// between-blocks probe): one in flight at a time across both lanes, ~15% of wall-clock in total. They
|
||
// used to be throttled one by one at 10% each, which added up to 30-40% with all of them due.
|
||
// Explicit needs (dirty sets, the user's own send, a never-loaded list) bypass it but are charged.
|
||
services::WalletScanGovernor scan_governor_;
|
||
// The change signals the routine scans are gated on, besides the tip hash (state_.sync.best_blockhash).
|
||
std::uint64_t wallet_mutation_epoch_ = 0; // bumped by noteWalletMutation()
|
||
bool mempool_known_ = false; // the last Core poll's getmempoolinfo
|
||
double tip_changed_at_ = 0.0; // scanClockNow() when a Core poll saw a new tip hash (chat settle)
|
||
std::int64_t mempool_txs_ = 0;
|
||
std::int64_t mempool_bytes_ = 0;
|
||
bool window_background_ = false; // setWindowBackground()
|
||
static constexpr float kScanRetrySec = 2.0f; // a routine scan the governor held back asks again this soon
|
||
services::ScanSignals currentScanSignals() const;
|
||
services::ScanVisibility currentScanVisibility() const;
|
||
static double scanClockNow(); // the governor's clock: steady-clock seconds
|
||
// C44: holds the wallet-wide scans for a moment after the node starts answering (connect, warmup end),
|
||
// until the lock state is known and the node knows its tip or the wallet is small (20 s at most).
|
||
services::ConnectScanGate scan_gate_;
|
||
// After an address enumeration failed: no address refresh before this (the previous lists stay).
|
||
std::chrono::steady_clock::time_point address_retry_not_before_{};
|
||
// A full address pass (the per-address balances) has run this connection. Until then the first one runs
|
||
// even while the node catches up.
|
||
bool address_balances_loaded_ = false;
|
||
// The session's first balance read was sent while the node was catching up (once per connection).
|
||
bool first_balance_read_tried_ = false;
|
||
// Paces the address passes the held profiles' timers can't bring (a stale list, the first balances).
|
||
std::chrono::steady_clock::time_point address_owed_next_try_{};
|
||
bool scanGateOpen() const;
|
||
// The address list was never loaded this session, or the address set may have changed (import, sweep,
|
||
// decrypt-import): it loads even while the wallet scans are held (list-only).
|
||
bool addressListStale() const;
|
||
// What the scan entry points check before sending a wallet-wide scan (services::scan_policy.h).
|
||
services::ScanContext scanContext() const;
|
||
// The full address pass could be sent now (the hard gates, and no retry hold).
|
||
bool addressScanCanRunNow() const;
|
||
void armScanGate(const services::NetworkRefreshService::ConnectionInfoResult& info);
|
||
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
|
||
std::uint64_t clipboard_secret_hash_ = 0;
|
||
std::chrono::steady_clock::time_point clipboard_clear_deadline_{};
|
||
float loading_timer_ = 0.0f; // spinner animation for loading overlay
|
||
double connect_stall_since_ = 0.0; // ImGui::GetTime() when the daemon first went "reachable but not ready"; 0 = not stalling (see util/connect_stall.h)
|
||
bool encryption_incomplete_warned_ = false; // W2-2: once-per-session guard for the "encryption didn't complete" warning
|
||
bool lock_failure_warned_ = false; // W2-4: guard so a repeatedly-failing auto-lock warns once, not every retry
|
||
std::uint64_t alerts_seen_total_ = 0; // Notifications::totalPushed() at last alert-panel open; drives the bell's unread dot
|
||
|
||
// Current page (sidebar navigation)
|
||
ui::NavPage current_page_ = ui::NavPage::Overview;
|
||
ui::NavPage prev_page_ = ui::NavPage::Overview;
|
||
float page_alpha_ = 1.0f; // 0→1 fade on page switch
|
||
bool sidebar_collapsed_ = false; // true = icon-only mode
|
||
|
||
// Debug screenshot sweep state.
|
||
bool screenshot_sweep_active_ = false;
|
||
bool sweep_current_theme_only_ = false; // Debug Options: sweep only the active theme
|
||
bool sweep_capture_this_frame_ = false;
|
||
int sweep_skin_idx_ = 0;
|
||
int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture
|
||
std::vector<std::string> sweep_skins_; // skin ids to cycle
|
||
std::string sweep_dir_; // output folder for this sweep
|
||
std::string sweep_current_path_; // PNG path for the frame about to be captured
|
||
std::string sweep_saved_skin_; // restore on completion
|
||
ui::NavPage sweep_saved_page_ = ui::NavPage::Overview;
|
||
void updateScreenshotSweep(); // called at the top of render() while active
|
||
void applySweepTarget(); // apply current (skin,page/surface), path, arm settle
|
||
|
||
// --- Full UI sweep: the capture unit is a "surface" (a tab, optionally with a modal / step /
|
||
// state forced on top). A tab is a surface with a null setup. ---
|
||
struct SweepTarget {
|
||
std::string name; // fs-safe id, e.g. "modal-seed-backup" / "overview"
|
||
ui::NavPage page = ui::NavPage::Overview; // base tab under the surface
|
||
std::function<void(App&)> setup; // reveal the surface (null = plain tab)
|
||
std::function<void(App&)> teardown; // clear it (null = nothing to undo)
|
||
int settle = 4;// blur overlays override to 8
|
||
};
|
||
std::vector<SweepTarget> sweep_targets_;
|
||
int sweep_target_idx_ = 0;
|
||
bool sweep_full_ = false; // full-UI sweep (drives surfaces) vs the legacy tab-only sweep
|
||
// capture_mode_: set only during a full sweep. CONTRACT: while true, NO live op may fire — no
|
||
// RPC, no auto-lock, no async pump. New async paths that could run mid-sweep must guard on it.
|
||
bool capture_mode_ = false;
|
||
void startSweepImpl(bool full);
|
||
// Why a full sweep must not start now (a real wallet workflow is in flight), or nullptr.
|
||
const char* fullSweepBlockedReason() const;
|
||
void buildSweepCatalog();
|
||
void installDemoWalletData();
|
||
void clearDemoWalletData();
|
||
// The full sweep's private temp dir (demo wallet files and folders); "" when none.
|
||
std::string sweep_sandbox_;
|
||
bool sweep_chat_identity_fabricated_ = false; // seedChatDemoData() made the demo chat identity
|
||
bool sweep_show_node_banner_ = false; // let the node-status banner render on its sweep target
|
||
void applyHealthyDemoState(); // reset the connection/encryption flags to the healthy demo values
|
||
void sweepEnsureDemoChatIdentity(); // re-fabricate a demo chat identity if none (sweep teardown)
|
||
void writeSweepManifest() const;
|
||
// Snapshot of the state_ fields the demo installer mutates, restored at sweep end. Kept as a
|
||
// plain struct because WalletState has reference-alias members and isn't copy-assignable.
|
||
struct SweepStateSnapshot {
|
||
bool valid = false;
|
||
bool connected=false, warming_up=false, daemon_initializing=false;
|
||
bool encrypted=false, locked=false, encryption_state_known=false;
|
||
std::string warmup_status, warmup_description;
|
||
SyncInfo sync;
|
||
double privateBalance=0, transparentBalance=0, totalBalance=0, unconfirmedBalance=0;
|
||
std::vector<AddressInfo> addresses, z_addresses, t_addresses;
|
||
std::vector<TransactionInfo> transactions;
|
||
double market_price_usd=0;
|
||
double market_change_24h=0;
|
||
WizardPhase wizard_phase = WizardPhase::None; // the demo state dismisses the wizard
|
||
bool lite_firstrun_dismissed = false; // the demo dismisses the lite welcome prompt
|
||
} sweep_state_snapshot_;
|
||
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
|
||
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
|
||
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
|
||
|
||
// Background gradient overlay
|
||
ImTextureID gradient_tex_ = 0;
|
||
|
||
// Textures retired mid-frame (draw lists may still reference them); freed at the next preFrame().
|
||
std::vector<ImTextureID> pending_texture_destroy_;
|
||
void retireTexture(ImTextureID& t) { if (t) { pending_texture_destroy_.push_back(t); t = 0; } }
|
||
|
||
// Logo texture (reload on skin change / dark↔light switch)
|
||
ImTextureID logo_tex_ = 0;
|
||
int logo_w_ = 0;
|
||
int logo_h_ = 0;
|
||
bool logo_loaded_ = false;
|
||
bool logo_is_dark_variant_ = true; // tracks which variant is currently loaded
|
||
ImU32 logo_accent_ = 0; // theme accent the SVG logo was last rasterized with (re-render on change)
|
||
ImTextureID drgx_emoji_tex_ = 0; // ":drgx:" custom chat emoji (themed to the accent, like the logo)
|
||
int drgx_emoji_w_ = 0, drgx_emoji_h_ = 0;
|
||
|
||
// Coin logo texture (DragonX currency icon, separate from wallet branding)
|
||
ImTextureID coin_logo_tex_ = 0;
|
||
int coin_logo_w_ = 0;
|
||
int coin_logo_h_ = 0;
|
||
bool coin_logo_loaded_ = false;
|
||
|
||
// Console tab + its backend executor (full-node RPC or lite backend), created lazily.
|
||
ui::ConsoleTab console_tab_;
|
||
std::unique_ptr<ui::ConsoleCommandExecutor> console_exec_;
|
||
|
||
// Pending payment from URI
|
||
bool pending_payment_valid_ = false;
|
||
std::string pending_to_address_;
|
||
double pending_amount_ = 0.0;
|
||
std::string pending_memo_;
|
||
std::string pending_label_;
|
||
|
||
// Per-category refresh timers, policy, and worker queue guards.
|
||
services::NetworkRefreshService network_refresh_;
|
||
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
|
||
|
||
// Mining toggle guard (prevents concurrent setgenerate calls)
|
||
std::atomic<bool> mining_toggle_in_progress_{false};
|
||
// Mining error state (main-thread only — every writer is in a MainCb or render).
|
||
std::string mining_last_error_;
|
||
// Sync-ETA estimator (UI thread only). onConnected/onDisconnected may run off-thread, so they set
|
||
// the atomic flag and the status bar consumes it before update().
|
||
util::SyncRateEstimator sync_eta_;
|
||
std::atomic<bool> sync_eta_reset_pending_{false};
|
||
bool mining_error_av_ = false;
|
||
void setMiningError(std::string msg, bool av = false) { mining_last_error_ = std::move(msg); mining_error_av_ = av; }
|
||
|
||
// True from a successful startPoolMining() until the miner is confirmed connected/hashing in the
|
||
// poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay).
|
||
std::atomic<bool> pool_starting_{false};
|
||
|
||
// Thread count of the newest pool start/retarget job posted (0 = none / stopped). A hint for the main
|
||
// thread: set when a job is posted, cleared by stopPoolMining(), by a start that fails and by a retarget
|
||
// that finds no miner. The control thread re-checks the real state, so a stale value can only skip a
|
||
// job the newest posted one already covers, or post a no-op one.
|
||
std::atomic<int> pool_threads_target_{0};
|
||
void clearPoolThreadsTarget(int mine) { // only if it still holds this job's value
|
||
int expected = mine;
|
||
pool_threads_target_.compare_exchange_strong(expected, 0, std::memory_order_relaxed);
|
||
}
|
||
|
||
// Auto-shield guard (prevents concurrent auto-shield operations)
|
||
std::atomic<bool> auto_shield_pending_{false};
|
||
// v1.3.0+ daemons auto-shield coinbase themselves; probe z_autoshieldstatus once per connection and
|
||
// defer the wallet's own client-side auto-shield when the daemon is doing it (otherwise both race for
|
||
// the same coinbase UTXOs and split funds across different z-addresses). Fail-closed: a pre-1.3.0
|
||
// daemon lacks the RPC → active stays false → the wallet keeps shielding client-side (no regression).
|
||
bool daemon_autoshield_probed_ = false;
|
||
bool daemon_autoshield_active_ = false;
|
||
std::atomic<bool> daemon_autoshield_probe_inflight_{false};
|
||
std::string daemon_autoshield_address_; // z_autoshieldstatus fields (O1); empty on old daemons
|
||
std::string daemon_autoshield_disabled_reason_; // daemon's reason auto-shield is off (e.g. seed not recoverable)
|
||
bool daemon_autoshield_seed_recoverable_ = false;
|
||
|
||
// P4: Incremental transaction cache
|
||
int last_tx_block_height_ = -1; // block height at last full tx fetch
|
||
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
|
||
std::size_t shielded_history_scan_cursor_ = 0;
|
||
// False until the first full shielded-history scan finishes. Drives the History tab's
|
||
// "Loading older history…" progress so the user knows transactions are still streaming in
|
||
// after the first batch appears; goes quiet for the routine per-block re-scans afterward.
|
||
bool initial_history_scan_complete_ = false;
|
||
std::unordered_map<std::string, int> shielded_history_scan_heights_;
|
||
// The receive-scan pass over every z-address (see services::ShieldedHistoryPass).
|
||
services::ShieldedHistoryPass history_pass_;
|
||
std::uint64_t history_scan_epoch_ = 0; // bumped when the scan heights are invalidated
|
||
std::uint64_t history_sent_epoch_ = 0; // bumped when the sent sweep's watermark is reset
|
||
int history_sent_scan_height_ = -1; // W: tip of the last successful z_listsentbyaddress; -1 = none
|
||
int history_sent_anchor_height_ = -1; // A: bottom edge the last sweep's window covered (W - margin)
|
||
std::string history_sent_anchor_hash_; // H_A: getblockhash(A) at that sweep; re-verified after a load
|
||
bool history_sent_verify_pending_ = false; // a loaded watermark owes an anchor verify before its next sweep
|
||
int history_full_sweep_failures_ = 0; // whole-wallet sweeps failed in a row (backoff)
|
||
std::time_t history_full_sweep_retry_at_ = 0;
|
||
std::time_t history_sent_last_full_time_ = 0; // last successful FULL sweep: drives the weekly safety-net
|
||
int history_sent_last_full_height_ = -1; // re-sweep (sentSweepSafetyNetDue)
|
||
std::unordered_set<std::string> history_enrich_backlog_; // txids found, not enriched yet
|
||
double history_addr_scan_ms_ = 0.0; // per-address z_listreceivedbyaddress cost (estimate)
|
||
std::size_t history_transparent_rows_ = 0; // rows the last listtransactions chain returned
|
||
int history_transparent_tip_ = -1; // ...and the tip it ran at (same tip: rows can be reused)
|
||
std::uint64_t tx_dirty_generation_ = 0; // bumped by markTransactionsDirty()
|
||
std::uint64_t addr_dirty_generation_ = 0; // bumped by markAddressesDirty()
|
||
// Addresses holding notes the history doesn't show yet (from the address scan's z_listunspent): scanned
|
||
// first in the next wallet-wide cycle, so a receive or payout shows within one cycle on a large wallet
|
||
// instead of when the routine pass reaches its address. Each txid hints once per session after it is
|
||
// mined (a mempool hint doesn't use that up, so the row also catches its confirmation).
|
||
std::unordered_map<std::string, int> history_hint_floors_;
|
||
std::unordered_set<std::string> history_hinted_txids_;
|
||
// The between-blocks probe's progress through the z-addresses since what it watches last changed.
|
||
services::ProbeRotation recent_probe_rotation_;
|
||
// The z-addresses of the last successful address enumeration this session: a never-scanned address new
|
||
// to it (an imported key) resets the sent sweep's watermark, even if the list in between came back blank.
|
||
std::unordered_set<std::string> history_listed_z_;
|
||
|
||
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for
|
||
// txids we've already enriched. Keyed by txid.
|
||
using ViewTxCacheEntry = services::NetworkRefreshService::TransactionViewCacheEntry;
|
||
services::NetworkRefreshService::TransactionViewCache viewtx_cache_;
|
||
|
||
// P4c: Confirmed transaction cache — deeply-confirmed txns (>= 10 confs)
|
||
// are accumulated here and reused across refresh cycles. Only
|
||
// recent/unconfirmed txns are re-fetched from the daemon each time.
|
||
std::vector<TransactionInfo> confirmed_tx_cache_;
|
||
std::unordered_set<std::string> confirmed_tx_ids_; // fast lookup
|
||
int confirmed_cache_block_ = -1; // block height when cache was last built
|
||
|
||
// Dirty flags for demand-driven refresh
|
||
bool addresses_dirty_ = true; // true → refreshAddresses() will run
|
||
bool address_validation_cache_dirty_ = true;
|
||
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
|
||
int mining_tx_poll_block_ = -1; // C119: tip of the last dispatched Mining-tab poll
|
||
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
|
||
bool rescan_status_poll_in_progress_ = false;
|
||
// True once we've actually observed the rescan running (daemon restarted into -rescan warmup).
|
||
// Gates the "rescan complete" detection so a getrescaninfo poll that hits the still-running
|
||
// pre-restart daemon (which reports rescanning=false) can't fire a false "complete" instantly.
|
||
bool rescan_confirmed_active_ = false;
|
||
// A runtime rescanblockchain RPC is in flight (vs the -rescan daemon restart). While set,
|
||
// the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for
|
||
// the whole scan and would block them); completion is signalled by the rescan RPC callback.
|
||
bool runtime_rescan_active_ = false;
|
||
// True only for a rescan the WALLET/USER initiated (the Rescan button, a -rescan/salvage/zap/reindex
|
||
// restart, key import, seed migration) — not an autonomous background witness rebuild the daemon does
|
||
// on its own. Gates the "Blockchain rescan complete" toast so background rebuilds don't fire it;
|
||
// cleared when the toast is shown.
|
||
std::atomic<bool> user_initiated_rescan_{false}; // atomic: some rescan triggers run on worker threads
|
||
// Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
|
||
// that reconciles the preserved wallet.dat against the freshly-imported chain.
|
||
bool post_bootstrap_rescan_pending_ = false;
|
||
std::string portfolio_scope_migrated_for_; // scope id the legacy-portfolio-scope migration last ran for
|
||
bool post_bootstrap_probe_in_flight_ = false; // its detectLowestAvailableBlockHeight() is on worker_
|
||
int post_bootstrap_probe_attempts_ = 0; // probes that could not tell; gives up after 6
|
||
std::chrono::steady_clock::time_point post_bootstrap_probe_retry_at_{};
|
||
// Largest "blocks remaining" seen during the current witness-rebuild phase. The daemon's
|
||
// "Building Witnesses for block" fraction resets every call (it's re-invoked per connected
|
||
// block, each walking from its own start height to the tip), so we derive a stable, monotonic
|
||
// overall percentage from how far "remaining" has fallen below this peak. Reset per phase.
|
||
int witness_rebuild_total_blocks_ = 0;
|
||
// The daemon's primary witness signal is "Setting Initial Sapling Witness for tx <hash>, <i>
|
||
// of <N>", logged once per wallet tx as its initial witness is set. The <i> is the tx's slot in
|
||
// an UNORDERED map, so it bounces wildly (was the cause of the resetting progress). The honest
|
||
// monotonic metric is how many DISTINCT txs have been witnessed (the set only grows; it also
|
||
// dedups the daemon's occasional double-prints) over the reported total N.
|
||
std::unordered_set<std::string> witness_seen_txids_;
|
||
int witness_total_txs_ = 0;
|
||
bool opid_poll_in_progress_ = false;
|
||
std::atomic<bool> opid_filter_unsupported_{false}; // see opidFilterUnsupported(); set on the worker
|
||
std::atomic<int> maint_calls_{0}; // maintenance calls in flight (MaintenanceCallScope)
|
||
std::atomic<bool> maint_abort_{false}; // set by abortMaintenanceCalls(), cleared on connect
|
||
services::ConsolidationRun consolidation_; // C210 (see consolidation())
|
||
std::uint64_t rpc_session_ = 0; // ++ in onConnected/onDisconnected: an orphan merge is tracked only within one
|
||
double run_lock_resync_at_ = 0.0; // steadySeconds() before which the run asks for no lock-state re-read
|
||
void pumpConsolidation();
|
||
void postConsolidationCall(int addrIdx);
|
||
void postConsolidationPoll();
|
||
void finishConsolidation();
|
||
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
|
||
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
|
||
int consecutive_core_failures_ = 0;
|
||
|
||
// Pending z_sendmany operation tracking
|
||
bool send_progress_active_ = false;
|
||
int send_submissions_in_flight_ = 0;
|
||
std::vector<std::string> pending_opids_; // opids to poll for completion
|
||
// Non-User ops only (absent = User): what each op's completion re-reads.
|
||
std::unordered_map<std::string, services::NetworkRefreshService::OperationOrigin> op_origin_;
|
||
struct PendingSendInfo {
|
||
std::string from;
|
||
std::string to;
|
||
std::string memo;
|
||
double amount = 0.0;
|
||
double fee = 0.0;
|
||
std::int64_t timestamp = 0;
|
||
};
|
||
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
|
||
// Opids issued as a fee-gap auto-retry (see maybeRetrySendForFeeGap). Tracked so a retry that
|
||
// fails again is reported to the user instead of looping.
|
||
std::unordered_set<std::string> send_feegap_retried_opids_;
|
||
// z_sendmany UI callbacks held until the opid reaches a terminal status, so the
|
||
// user isn't told "sent successfully" before the tx is actually built/broadcast.
|
||
std::unordered_map<std::string, std::function<void(bool, const std::string&)>>
|
||
pending_send_callbacks_;
|
||
// Txids from completed z_sendmany operations.
|
||
// Ensures shielded sends are discoverable by z_viewtransaction
|
||
// even when they don't appear in listtransactions or
|
||
// z_listreceivedbyaddress.
|
||
std::unordered_set<std::string> send_txids_;
|
||
// Tip height when each tracked txid was added (TTL backstop, see pruneTrackedSendTxids()).
|
||
std::unordered_map<std::string, int> send_txid_heights_;
|
||
static constexpr int kTrackedSendTtlBlocks = 20;
|
||
void pruneTrackedSendTxids();
|
||
|
||
// First-run wizard state
|
||
WizardPhase wizard_phase_ = WizardPhase::None;
|
||
std::unique_ptr<util::Bootstrap> bootstrap_;
|
||
std::string wizard_bootstrap_url_; // source the wizard last started ("" = default); Retry keeps it
|
||
bool bootstrap_downloading_ = false; // true while settings bootstrap dialog is active
|
||
std::string wizard_pending_passphrase_; // held until daemon connects
|
||
std::string wizard_saved_passphrase_; // held until PinSetup completes/skipped
|
||
|
||
// Wallet security flow state shared by wizard/settings encryption paths.
|
||
services::WalletSecurityController wallet_security_;
|
||
services::WalletSecurityWorkflow wallet_security_workflow_;
|
||
|
||
// Wizard: stopping an external daemon before bootstrap
|
||
std::atomic<bool> wizard_stopping_external_{false}; // written by the stop worker, read by the UI (L-06)
|
||
GuardedStatus wizard_stop_status_; // thread-safe: written by the stop worker, read by the UI (L-06)
|
||
|
||
// PIN vault
|
||
std::unique_ptr<util::SecureVault> vault_;
|
||
data::TransactionHistoryCache transaction_history_cache_;
|
||
data::AddressBook address_book_; // shared contact store; loaded once in init(), self-saves on mutation
|
||
data::WalletIndex wallet_index_; // per-wallet metadata cache (wallets.json); populated after each load
|
||
std::string pending_transaction_history_cache_passphrase_;
|
||
bool transaction_history_cache_loaded_ = false;
|
||
// Background writer for the history snapshot. Declared AFTER transaction_history_cache_ (destroyed first,
|
||
// so a destructor-only teardown joins it before the cache goes); its jobs reference only that cache.
|
||
// App::shutdown() drains it (bounded) and joins it once idle; that is the real join (main.cpp _Exit()s past
|
||
// ~App()).
|
||
util::CoalescingWriter history_writer_;
|
||
struct HistoryStoreState {
|
||
bool have = false; // a snapshot of this session is on disk (submitted or loaded)
|
||
data::HistorySnapshotSig sig; // its signature
|
||
std::chrono::steady_clock::time_point lastSubmit{};
|
||
} history_store_; // UI thread only
|
||
bool history_store_pending_ = false; // a content change waits out kHistoryMinStoreIntervalSec
|
||
|
||
// Lock screen state
|
||
bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus
|
||
bool lock_use_pin_ = true; // true = PIN input, false = passphrase input
|
||
// The active wallet's PIN vault holds an outdated passphrase (see isPinVaultStale()). Cleared when a
|
||
// PIN is set (which also re-offers PIN entry) or removed and on a wallet switch; a Change PIN keeps it
|
||
// (same sealed passphrase).
|
||
bool pin_vault_stale_ = false;
|
||
char lock_pin_buf_[16] = {};
|
||
char lock_passphrase_buf_[256] = {};
|
||
std::string lock_error_msg_;
|
||
float lock_error_timer_ = 0.0f;
|
||
int lock_attempts_ = 0;
|
||
float lock_lockout_timer_ = 0.0f;
|
||
std::chrono::steady_clock::time_point lock_timer_armed_at_{}; // when lock_error/lockout timers were last armed
|
||
bool lock_state_restored_ = false; // one-shot: persisted lockout restored for this lock-screen session
|
||
bool lock_unlock_in_progress_ = false;
|
||
|
||
// Encrypt wallet dialog state
|
||
bool show_encrypt_dialog_ = false;
|
||
bool show_change_passphrase_ = false;
|
||
EncryptDialogPhase encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
|
||
char encrypt_pass_buf_[256] = {};
|
||
char encrypt_confirm_buf_[256] = {};
|
||
char change_old_pass_buf_[256] = {};
|
||
char change_new_pass_buf_[256] = {};
|
||
char change_confirm_buf_[256] = {};
|
||
std::string encrypt_status_;
|
||
bool encrypt_in_progress_ = false;
|
||
std::string enc_dlg_saved_passphrase_; // held for PIN setup after encrypt
|
||
char enc_dlg_pin_buf_[16] = {};
|
||
char enc_dlg_pin_confirm_buf_[16] = {};
|
||
std::string enc_dlg_pin_status_;
|
||
|
||
// PIN setup dialog state (settings page)
|
||
bool show_pin_setup_ = false;
|
||
bool show_pin_change_ = false;
|
||
bool show_pin_remove_ = false;
|
||
char pin_buf_[16] = {};
|
||
char pin_confirm_buf_[16] = {};
|
||
char pin_old_buf_[16] = {};
|
||
char pin_passphrase_buf_[256] = {};
|
||
std::string pin_status_;
|
||
bool pin_in_progress_ = false;
|
||
|
||
// Decrypt wallet dialog state
|
||
bool show_decrypt_dialog_ = false;
|
||
char decrypt_pass_buf_[256] = {};
|
||
|
||
// Wizard PIN setup state
|
||
char wizard_pin_buf_[16] = {};
|
||
char wizard_pin_confirm_buf_[16] = {};
|
||
std::string wizard_pin_status_;
|
||
|
||
// Auto-lock on idle. Fed by real input events (noteUserInput, from main.cpp's event ingest), NOT by
|
||
// widget state: a focused-but-untouched text field must not keep the wallet unlocked (C21).
|
||
util::IdleTracker idle_;
|
||
// C90: back-off clock for the daemon-unlock-expiry reconcile (steady clock, wall-clock independent).
|
||
std::chrono::steady_clock::time_point unlock_reconcile_retry_at_{};
|
||
|
||
// Wall-clock delta for logic timers (see logicDeltaTime()).
|
||
std::chrono::steady_clock::time_point last_update_time_{};
|
||
float logic_dt_ = 0.0f;
|
||
|
||
// Mine-when-idle: auto-start/stop mining based on system idle state
|
||
bool idle_mining_active_ = false; // true when mining was auto-started by idle detection
|
||
std::chrono::steady_clock::time_point idle_mining_next_check_{}; // checkIdleMining runs at 1 Hz
|
||
std::chrono::steady_clock::time_point idle_gpu_next_sample_{}; // GPU probe at most every 5 s
|
||
bool idle_gpu_busy_ = false; // last GPU-aware verdict (with hysteresis)
|
||
bool idle_scaled_to_idle_ = false; // true when threads have been scaled up for idle
|
||
|
||
// Private methods - rendering
|
||
void renderStatusBar();
|
||
// Persistent node/RPC error strip at the top of the content column when the wallet can't
|
||
// reach its node (or the embedded daemon gave up crashing). Decision logic is the pure
|
||
// evaluateNodeStatusBanner() in ui/node_status_banner.h; this draws it and wires the action.
|
||
void renderNodeStatusBanner();
|
||
// Body of the status-bar alert-history popup: recent alerts (incl. ones whose toast faded),
|
||
// newest first, with severity icon + relative age + a Clear action. See src/ui/notifications.h.
|
||
void renderAlertHistoryPanel();
|
||
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
||
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
|
||
void renderImportKeyDialog();
|
||
void renderExportKeyDialog();
|
||
void renderBackupDialog();
|
||
void renderSeedBackupDialog(); // full-node "Back up seed phrase" modal (z_exportmnemonic)
|
||
void renderSeedMigrationDialog(); // "Migrate to a seed wallet" guided modal (Phase 1: create)
|
||
void beginSeedMigrationPrecheck(); // Intro: probe whether the wallet is legacy / already-seeded
|
||
void maybeOfferDaemonUpdate(); // at startup, flag the prompt if a newer daemon is bundled
|
||
void renderDaemonUpdatePrompt(); // "a newer node is bundled — update the installed daemon?"
|
||
void renderFirstRunWizard();
|
||
void renderLockScreen();
|
||
void renderEncryptWalletDialog();
|
||
void renderDecryptWalletDialog();
|
||
void renderPinDialogs();
|
||
void renderAntivirusHelpDialog();
|
||
void renderSwitchStopDaemonDialog(); // confirm before stopping an adopted node to switch wallets
|
||
void renderBlockDbReindexDialog(); // offer to rebuild an unreadable block database (-reindex)
|
||
void reindexBlockDatabase(); // restart the daemon with -reindex to rebuild the block DB
|
||
void renderWalletRecoveredDialog(); // warn that the node auto-recovered/salvaged wallet.dat
|
||
void renderEmptyWalletWarningDialog();// warn that the active wallet is empty while a sibling holds funds
|
||
void detectWalletAutoRecovery(); // scan daemon output for a salvage; fire the warning once/session
|
||
void detectWalletDegraded(); // scan daemon output for a DEGRADED-mode open; warn once/session
|
||
void restoreOriginalWallet(); // swap the wallet.<ts>.bak back over the salvaged copy + restart
|
||
void pumpWalletRestore(); // main-thread: surface the restore/rebuild op's result
|
||
void rebuildWalletDatabase(); // rebuild a BDB-inconsistent wallet into a loadable one (helper)
|
||
bool walletRebuildAvailable() const; // the dragonx-wallet-rebuild helper is present
|
||
void processDeferredEncryption();
|
||
|
||
// Private methods - connection
|
||
void tryConnect();
|
||
void onConnected();
|
||
void onDisconnected(const std::string& reason);
|
||
// Set the "node is initializing" UI state (status line + overlay description) from the
|
||
// embedded/external daemon's launch state and its own console output (current phase + block
|
||
// height), so a connect probe that times out while the daemon loads shows WHAT it's doing.
|
||
// `reachableButBusy` is true when the probe connected but got no RPC reply (a timeout),
|
||
// false when the daemon is merely launching (not bound yet). Returns the status title.
|
||
std::string applyDaemonInitStatus(bool reachableButBusy);
|
||
// The overlay text applyDaemonInitStatus last produced. It runs only on a connect failure, which a busy
|
||
// node backs off to every 30s, so update() re-applies it once a second (the elapsed time and the
|
||
// node's own progress tick) for as long as nothing else has replaced the status.
|
||
std::string init_status_title_;
|
||
bool init_status_busy_ = false;
|
||
double status_refresh_at_ = 0.0;
|
||
// The warm-up overlay text, from the node's last -28 message — unless its own log says it is rebuilding
|
||
// witnesses: a startup rebuild runs under "Activating best chain...", which hides the step users must
|
||
// not interrupt. update() re-applies it once a second too (see init_status_title_).
|
||
void applyWarmupText(const std::string& rpcMessage);
|
||
std::string warmup_rpc_message_;
|
||
// Tear down a connection that died mid-session (daemon crash / restart / dropped
|
||
// socket) so update()'s reconnect branch re-enters tryConnect(). Unlike onDisconnected
|
||
// alone, this also rpc_->disconnect()s so rpc_->isConnected() actually flips to false.
|
||
void handleLostConnection(const std::string& reason);
|
||
void applyDefaultBanlist();
|
||
|
||
// Private methods - data refresh
|
||
void refreshData(); // Orchestrator: dispatches per-category refreshes
|
||
void refreshCoreData(bool allowBalance = true); // Balance + blockchain info (can use fast_worker_)
|
||
// Address lists + balances. With no ticket it is an explicit refresh (charged to the scan budget).
|
||
void refreshAddressData(services::ScanTicket ticket = {});
|
||
// Transaction history: decides the cycle (services::decideTxCycle) and starts it. Safe to call any
|
||
// time: it refuses when the node can't be scanned (not connected, locked, warming up, rescanning, busy).
|
||
// Returns true when a routine scan was wanted but the scan budget held it back (ask again soon).
|
||
bool refreshTransactionData();
|
||
void startTransactionCycle(bool full, bool routineAllowed, services::ScanTicket ticket); // Full or Light
|
||
void startRecentTransactionPoll(bool probe, services::ScanTicket ticket); // between blocks
|
||
void markTransactionsDirty() { transactions_dirty_ = true; ++tx_dirty_generation_; }
|
||
// A pass sent before this can't clear it (refreshAddressData compares the generation).
|
||
void markAddressesDirty() { addresses_dirty_ = true; ++addr_dirty_generation_; }
|
||
std::vector<std::string> shieldedAddressList() const;
|
||
services::ShieldedHistoryPass::Work historyScanWork() const;
|
||
// From the address scan's z_listunspent: addresses holding received notes the history doesn't show
|
||
// yet (no row, or rows still unconfirmed while the note is mined) become priority scans.
|
||
void noteHistoryHintsFromUnspent(const std::vector<services::NetworkRefreshService::UnspentNoteLite>& notes);
|
||
bool refreshEncryptionState(); // Wallet encryption/lock state
|
||
void refreshBalance(); // Legacy: balance-only refresh (used by specific callers)
|
||
void refreshAddresses(); // Legacy: standalone address refresh
|
||
void refreshPrice();
|
||
void refreshWalletEncryptionState();
|
||
// C88: reconcile the persisted "encryption requested" flag against one parsed getwalletinfo result
|
||
// (connect-time ConnectionInit and every Encryption job). Main thread only.
|
||
void reconcileEncryptionPending(const services::NetworkRefreshService::WalletEncryptionResult& enc);
|
||
void applyRefreshPolicy(ui::NavPage page);
|
||
bool effectivelySyncing() const; // syncing, or within the post-sync settle window (hysteresis)
|
||
bool currentPageNeedsWalletDataRefresh() const;
|
||
bool shouldRunWalletTransactionRefresh() const;
|
||
bool shouldRefreshRecentTransactions() const;
|
||
void checkAutoLock();
|
||
// C90: the daemon's absolute unlock window ran out while the GUI still says "unlocked": re-read getwalletinfo
|
||
// (once per expiry, backed off) so the lock screen / tx-cache lock follow the daemon.
|
||
void reconcileUnlockExpiry();
|
||
void checkIdleMining();
|
||
};
|
||
|
||
} // namespace dragonx
|