// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once // The hard gates in front of the wallet-wide scans: whether the node can take one at all right now. The // WalletScanGovernor decides on top of them how often. The scan entry points (refreshAddressData, // refreshTransactionData) check these themselves, so no caller (tab entry, refreshData, an import, the // history's re-route through the address scan) can send a scan into a node that is syncing, warming up, // rescanning, busy or locked, or while the window is in the background. The cheap address listing (no // wallet scan) still runs for a stale list, and the session's first balances are read once even while the // node catches up (otherwise every balance would read 0 for the whole catch-up). // // Pure, header-only. #include "ui/sidebar.h" #include namespace dragonx { namespace services { enum class ScanTier { None, // nothing ListOnly, // the address lists only (z_listaddresses, getaddressesbyaccount): cheap, no wallet scan Full, // the lists and the per-address balances (z_listunspent, listunspent) }; struct ScanContext { bool connected = false; bool locked = false; bool warmingUp = false; bool runtimeRescan = false; bool rpcBusy = false; bool syncing = false; // effectivelySyncing(): behind, or settling after catching up bool gateOpen = true; // the ConnectScanGate bool background = false; // the window is minimized, hidden, or covered without focus }; // The node answers RPCs that take cs_main without queueing behind something long. inline bool nodeCanAnswerScans(const ScanContext& c) { return c.connected && !c.warmingUp && !c.runtimeRescan && !c.rpcBusy; } // A wallet-wide scan (O(wallet), holds cs_main) may be sent. inline bool wholeWalletScanAllowed(const ScanContext& c) { return nodeCanAnswerScans(c) && !c.background && !c.locked && !c.syncing && c.gateOpen; } // The session's first read of a wallet value (the balance, the per-address balances): like // wholeWalletScanAllowed, but also while the node catches up. Once, so the catch-up shows the wallet's last // known values instead of 0. inline bool firstReadAllowed(const ScanContext& c) { return nodeCanAnswerScans(c) && !c.background && !c.locked && c.gateOpen; } // listStale: never loaded this connection, or the address set may have changed (import, sweep, // decrypt-import). A stale list still loads while the scans are held, without the balances. // balancesOwed: no full pass has read the per-address balances yet this session. inline ScanTier planAddressScan(const ScanContext& c, bool listStale, bool balancesOwed = false) { if (wholeWalletScanAllowed(c)) return ScanTier::Full; if (balancesOwed && firstReadAllowed(c)) return ScanTier::Full; return (nodeCanAnswerScans(c) && listStale) ? ScanTier::ListOnly : ScanTier::None; } // What entering a page does. Wallet data isn't fetched on entry any more (the periodic refresh and the // scan governor decide), except what the page can't show without: a stale address list, a never-loaded // history. struct PageEntryPlan { bool core = false; bool peers = false; bool mining = false; ScanTier addressesNow = ScanTier::None; bool transactionsNow = false; bool markAddresses = false; bool markTransactions = false; }; inline PageEntryPlan planPageEntry(ui::NavPage page, const ScanContext& c, bool addressListStale, bool transactionsNeverLoaded) { using NP = ui::NavPage; PageEntryPlan p; if (!c.connected) return p; switch (page) { case NP::Overview: p.core = !c.runtimeRescan && !c.rpcBusy; break; case NP::Peers: p.peers = !c.rpcBusy; break; case NP::Mining: p.mining = !c.warmingUp && !c.runtimeRescan && !c.rpcBusy; break; case NP::Send: case NP::Receive: case NP::History: { const ScanTier tier = planAddressScan(c, addressListStale); const bool loadNow = addressListStale && tier != ScanTier::None; if (loadNow) p.addressesNow = tier; else if (page != NP::History) p.markAddresses = true; // History needs the addresses only when stale if (page == NP::History) { if (!loadNow && transactionsNeverLoaded && wholeWalletScanAllowed(c)) p.transactionsNow = true; else p.markTransactions = true; } break; } default: break; } return p; } // The window counts as in the background when it is minimized or hidden, or covered while none of the // app's windows has focus (SDL reports occlusion on X11, Wayland and macOS, never on Windows). inline bool windowLooksBackground(bool minimized, bool hidden, bool occluded, bool appHasFocus) { return minimized || hidden || (occluded && !appHasFocus); } constexpr double kBackgroundDebounceSeconds = 3.0; constexpr double kBackgroundInputGraceSeconds = 2.0; // Background once it has looked so for a few seconds with no input meanwhile (a restore is immediate). inline bool isBackgroundWindow(bool flagged, double flaggedForSeconds, double secondsSinceInput) { return flagged && flaggedForSeconds >= kBackgroundDebounceSeconds && secondsSinceInput >= kBackgroundInputGraceSeconds; } // After a failed address enumeration, hold address refreshes for max(10 s, 10x what that pass cost). A pass // that failed because the node was busy (a reply timeout, a full queue, held back by the busy gate) gets the // floor: its wall time was the wait for cs_main, not its cost, and the busy gate already paces a busy node. inline double addressRetryDelaySeconds(double failedScanMs, bool nodeBusy = false) { if (nodeBusy) return 10.0; return std::max(10.0, (std::max(0.0, failedScanMs) / 1000.0) / 0.10); } } // namespace services } // namespace dragonx