- Connect gate: after the node starts answering, wallet scans wait until the lock state is known and the node knows its tip (or the wallet has fewer than 500 transactions), at most 20 s. A node already behind latches the sync profile from the connect's getinfo. Meanwhile a list-only pass keeps the address list and the cached history up, and the session's first balance and per-address balances are read once even while catching up, so the wallet shows its last-synced values instead of 0. - Background: minimized, hidden, or covered without focus (3 s debounce, any input cancels) polls only the chain state every 15 s. Wallet scans, the mining poll, price, nudges and auto-lock interaction tracking pause; auto-lock now fires while minimized. Leaving a held profile marks every timer due again. - Tab entry marks the timers due instead of forcing scans; a stale list or a never-loaded history still loads on entry. - The scan entry points own the hard gates, failed enumerations keep the previous lists behind a retry hold, and failed balance calls never overwrite known balances with 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
147 lines
5.8 KiB
C++
147 lines
5.8 KiB
C++
// 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 <algorithm>
|
|
|
|
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
|