// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 lk(m_); v_ = std::move(v); return *this; } std::string get() const { std::lock_guard 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 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(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 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& getZAddresses() const { return state_.z_addresses; } const std::vector& getTAddresses() const { return state_.t_addresses; } // Transactions const std::vector& 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& getPeers() const { return state_.peers; } const std::vector& 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 callback = nullptr); void createNewTAddress(std::function 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& 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 callback); // callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export. void exportAllKeys(std::function 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 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 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 callback); // Transaction operations void sendTransaction(const std::string& from, const std::string& to, double amount, double fee, const std::string& memo, std::function 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& 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: /screenshots-full//.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; // /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(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=. 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 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=, 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 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 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 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 callback, bool background = false); void markPendingSendTransactionSucceeded(const std::string& opid, const std::string& txid); void removePendingSendTransactions(const std::vector& 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_; std::unique_ptr 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 fast_rpc_; std::unique_ptr 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 http_cancel_{false}; // set at shutdown: queued GETs return at once, an in-flight one aborts std::unique_ptr 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 settings_; std::unique_ptr 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 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 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 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 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& 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 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_controller_; std::unique_ptr 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> mining_ctl_queue_; bool mining_ctl_stop_ = false; void postMiningControl(std::function 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_; // ":/" 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> closeIntraday, closeDaily; std::vector ohlcIntraday, ohlcDaily; std::chrono::steady_clock::time_point fetchedAt{}; }; std::unordered_map 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 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 shutting_down_{false}; std::atomic 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 stop_session_active_{false}; std::atomic stop_user_choice_{0}; // daemon::StopUserChoice, set by the shutdown screen std::atomic stop_kill_offered_{false}; // the node looks stuck: the screen may offer "Kill" std::atomic 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 daemon_restarting_{false}; std::atomic 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 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 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 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..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..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 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 wallet_switch_phase_{0}; std::atomic 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 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 pending_delete_result_{-1}; std::atomic 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 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 daemon_output_reset_pending_{false}; // lifecycle thread -> UI thread std::atomic 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 node_rebuilding_{false}; std::atomic 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 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_busy_gate_ = std::make_shared(); rpc::ConnectRetryBackoff connect_backoff_; // S8: per-failure-kind delay before the next connect attempt std::thread canary_thread_; std::atomic 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 /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 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 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 setup; // reveal the surface (null = plain tab) std::function teardown; // clear it (null = nothing to undo) int settle = 4;// blur overlays override to 8 }; std::vector 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 addresses, z_addresses, t_addresses; std::vector 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 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 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 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 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 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 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 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 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 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 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 history_hint_floors_; std::unordered_set 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 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 confirmed_tx_cache_; std::unordered_set 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 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 , // of ", logged once per wallet tx as its initial witness is set. The 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 witness_seen_txids_; int witness_total_txs_ = 0; bool opid_poll_in_progress_ = false; std::atomic opid_filter_unsupported_{false}; // see opidFilterUnsupported(); set on the worker std::atomic maint_calls_{0}; // maintenance calls in flight (MaintenanceCallScope) std::atomic 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 pending_opids_; // opids to poll for completion // Non-User ops only (absent = User): what each op's completion re-reads. std::unordered_map 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 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 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> 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 send_txids_; // Tip height when each tracked txid was added (TTL backstop, see pruneTrackedSendTxids()). std::unordered_map send_txid_heights_; static constexpr int kTrackedSendTtlBlocks = 20; void pruneTrackedSendTxids(); // First-run wizard state WizardPhase wizard_phase_ = WizardPhase::None; std::unique_ptr 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 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 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..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 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& 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