Files
ObsidianDragon/src/services/network_refresh_service.h
T
DanSandClaude Opus 4.8 3cda8c9e31 perf(history): persist the sent-sweep watermark (windowed relaunch)
z_listsentbyaddress re-walks the whole wallet when there's no usable
watermark, which the daemon does under cs_main for every send -- hundreds
of seconds, 400+ s and ~19 GB RSS on a large wallet. The watermark lived
only in memory, so every relaunch, reconnect or daemon restart paid for a
full sweep. This persists it so those cases run a bounded windowed sweep
([z, 0, 2, tip - W + margin]) instead (SENT-HISTORY-SWEEP Part 1).

- The watermark (W = last swept tip), its anchor (A = W - margin) and anchor
  hash (H_A = getblockhash(A)), plus the enrichment backlog and the full-
  sweep backoff, ride in the encrypted history snapshot as an optional
  `sent_sweep` object (schema stays v1; a missing/inconsistent one reads as
  "no watermark" = today's behaviour, so older builds and payloads are safe).
- Before the first windowed sweep after a load, the anchor is verified
  (getblockhash(A) == H_A). A match means any fork is inside the window; a
  mismatch (reorg deeper than the margin, different chain/datadir) or an
  inability to check drops the watermark and sweeps the whole wallet.
- The first sweep after a load uses a bigger margin (absorbs notifier lag),
  and a watermark older than 2400 blocks uses the long timeout (callLong).
- The backlog is persisted WITH W: a pure z->z send's txid lives only in a
  sweep result and would be lost if the window moved past an un-enriched one.
- Watermark changes reach disk (folded into the store signature), and every
  hard case still forces a full sweep: reorg (anchor verify), GUI rescan /
  zap / import / address change, restoreOriginalWallet, and disconnect.

Deferred (follow-ups, each only turns a correct-but-slow case faster, never a
correctness risk): the banded full sweep for seeding on a pre-note-cache
daemon, the external-import tripwire, and the weekly safety-net re-sweep.
Part 2 (the daemon's decrypt cost) is already on the daemon's
perf/sapling-note-cache branch.

Pure helpers + cache round-trip + anchor-verify/large-window covered by
tests; the getblockhash-after-sweep updates the pinned RPC sequences.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-16 14:47:48 -05:00

584 lines
31 KiB
C++

#pragma once
#include "chat/chat_protocol.h"
#include "data/wallet_state.h"
#include "refresh_scheduler.h"
#include "rpc/rpc_worker.h"
#include "rpc/rpc_failure.h"
#include <nlohmann/json.hpp>
#include <array>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <ctime>
#include <optional>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
namespace dragonx {
namespace services {
class NetworkRefreshService {
public:
using Timer = RefreshScheduler::Timer;
using Intervals = RefreshScheduler::Intervals;
class RefreshRpcGateway {
public:
virtual ~RefreshRpcGateway() = default;
virtual nlohmann::json call(const std::string& method,
const nlohmann::json& params) = 0;
// A call allowed to outlast the default timeout (a whole-wallet sweep on a large wallet).
// Gateways without a separate timeout just call().
virtual nlohmann::json callLong(const std::string& method, const nlohmann::json& params, long timeoutSec)
{
(void)timeoutSec;
return call(method, params);
}
};
enum class Job {
Core,
Addresses,
Transactions,
Mining,
Peers,
Price,
Encryption,
ConnectionInit,
Count
};
struct DispatchTicket {
Job job = Job::Core;
std::uint64_t generation = 0;
bool accepted = false;
};
struct JobStats {
std::uint64_t started = 0;
std::uint64_t finished = 0;
std::uint64_t skippedInFlight = 0;
std::uint64_t skippedQueuePressure = 0;
std::uint64_t staleCallbacks = 0;
std::size_t lastQueueDepth = 0;
};
struct EnqueueResult {
DispatchTicket ticket;
bool enqueued = false;
std::size_t queueDepth = 0;
};
struct ConnectionInfoResult {
bool ok = false;
std::optional<int> daemonVersion;
std::optional<int> protocolVersion;
std::optional<int> p2pPort;
std::optional<int> longestChain;
std::optional<int> notarized;
std::optional<int> blocks;
};
struct WalletEncryptionResult {
bool ok = false;
bool encrypted = false;
std::int64_t unlockedUntil = 0;
std::optional<int> txCount; // getwalletinfo's txcount (the wallet's transactions)
};
struct WarmupPollResult {
bool ready = false;
ConnectionInfoResult info;
std::string errorMessage;
// Why the poll failed: a -28 warmup reply means the node is there; a refused/reset connection
// means it's gone (the caller counts those toward its lost-connection check).
rpc::RpcFailureKind errorKind = rpc::RpcFailureKind::None;
};
struct ConnectionInitResult {
ConnectionInfoResult info;
WalletEncryptionResult encryption;
};
struct CoreRefreshResult {
bool balanceOk = false;
std::optional<double> shieldedBalance; // display (minconf=0, incl. pending change)
std::optional<double> transparentBalance;
std::optional<double> totalBalance;
std::optional<double> spendableShieldedBalance; // confirmed (minconf=1)
std::optional<double> spendableTransparentBalance;
std::optional<double> spendableTotalBalance;
bool blockchainOk = false;
std::optional<int> blocks;
std::optional<int> headers;
std::optional<std::string> bestBlockHash;
std::optional<double> verificationProgress;
std::optional<int> longestChain;
std::optional<int> notarized;
double balanceScanMs = 0.0; // wall-clock spent in z_gettotalbalance this refresh (0 if balance skipped)
// How those calls failed (None when they succeeded or weren't made), so a caller can tell a busy
// node (ReplyTimeout / QueueFull) from a gone one (NotListening) without reading what().
rpc::RpcFailureKind blockchainErrKind = rpc::RpcFailureKind::None;
rpc::RpcFailureKind balanceErrKind = rpc::RpcFailureKind::None;
// The minconf=0 call reached the daemon (it may still have failed). False when the balance was
// skipped (behind, busy chain read) or held back by the busy gate (Deferred) before it was sent.
bool balanceAttempted = false;
bool spendableQueried = false; // the minconf=1 call was made
std::optional<std::int64_t> totalBalanceZat; // the minconf=0 "total" in zatoshis, exact
// getmempoolinfo (CoreRefreshPlan::fetchMempool): a change signal that takes no cs_main.
bool mempoolFetched = false;
bool mempoolOk = false;
std::int64_t mempoolTxs = 0;
std::int64_t mempoolBytes = 0;
};
// What one Core refresh fetches. The minconf=1 (spendable) z_gettotalbalance is a second whole-wallet
// scan under cs_main; it only tells something the minconf=0 call doesn't while funds are unconfirmed
// or pending, so it runs only when needSpendable or the exact total moved since prevTotalZat.
struct CoreRefreshPlan {
bool includeBalance = true;
bool needSpendable = true;
std::optional<std::int64_t> prevTotalZat;
bool fetchMempool = false; // add getmempoolinfo after a successful getblockchaininfo
bool evenIfBehind = false; // the session's first balance read: also while the node is behind
};
struct MiningRefreshResult {
std::optional<double> localHashrate;
bool miningOk = false;
std::optional<bool> generate;
std::optional<int> genproclimit;
std::optional<int> blocks;
std::optional<double> difficulty;
std::optional<double> networkHashrate;
std::optional<std::string> chain;
double daemonMemoryMb = 0.0;
};
struct PeerRefreshResult {
std::vector<PeerInfo> peers;
std::vector<BannedPeer> bannedPeers;
};
struct PriceRefreshResult {
MarketInfo market;
};
struct PriceHttpResponse {
bool transportOk = false;
long httpStatus = 0;
std::string body;
std::string transportError;
};
struct PriceHttpResult {
std::optional<PriceRefreshResult> price;
std::string errorMessage;
};
// Minimal per-note view of a z_listunspent entry — just what a downstream consumer needs to derive
// spendable-note budgets without re-scanning. Kept UI/feature-agnostic (no chat specifics here).
struct UnspentNoteLite {
double amount = 0.0; // note value, DRGX
bool locked = false; // tied up by an in-flight send
int confirmations = 0; // TRUE depth (rawconfirmations when present, else confirmations)
std::string address; // the z-address holding it
std::string txid; // the transaction that created it
bool change = false; // change from the wallet's own spend (not a receive)
};
struct AddressRefreshResult {
std::vector<AddressInfo> shieldedAddresses;
std::vector<AddressInfo> transparentAddresses;
// False if either address-enumeration RPC (z_listaddresses / getaddressesbyaccount) threw, so the
// lists may be falsely short. Consumers that treat an empty list as authoritative (e.g. the
// empty-wallet warning) must not trust a 0 count unless this is true.
bool addressListOk = true;
// Wall-clock spent in the address scan (dominated by z_listunspent — O(mapWallet), holds the
// daemon's cs_main). Lets the caller throttle how often it polls (addressRefreshDue()).
double scanMs = 0.0;
// The wallet's unspent notes from this same z_listunspent scan, so a consumer (e.g. the chat
// note-budget) can be fed for free instead of running its own duplicate z_listunspent.
std::vector<UnspentNoteLite> unspentNotes;
bool balancesOk = true; // false: a balance call failed, the per-address balances may be stale
bool replyTimedOut = false; // a call hit the reply timeout (the daemon kept running it)
// The lists only: no z_listunspent / listunspent, or a balance call failed (the pass then degrades to
// list-only). Applying it carries the previous balances over.
bool listOnly = false;
bool nodeBusy = false; // a call failed because the node was busy (timeout, full queue, held back)
};
struct AddressRefreshSnapshot {
std::unordered_map<std::string, bool> shieldedSpendingKeys;
};
struct TransactionViewCacheEntry {
std::string from_address;
std::int64_t timestamp = 0;
int confirmations = 0;
struct Output {
std::string address;
double value = 0.0;
std::string memo;
std::size_t position = 0;
};
std::vector<Output> outgoing_outputs;
};
using TransactionViewCache = std::unordered_map<std::string, TransactionViewCacheEntry>;
struct TransactionRefreshSnapshot {
std::vector<std::string> shieldedAddresses;
std::unordered_set<std::string> fullyEnrichedTxids;
TransactionViewCache viewTxCache;
std::unordered_set<std::string> sendTxids;
std::unordered_set<std::string> pendingOpids;
std::vector<TransactionInfo> previousTransactions;
std::set<std::string> miningAddresses;
std::unordered_map<std::string, int> shieldedScanHeights;
std::size_t shieldedScanStartIndex = 0;
std::size_t maxShieldedReceiveScans = 0; // receive scans this cycle; 0 = no limit
// The pass's floor (services::ShieldedHistoryPass): an address scanned below it is scanned again.
// -1: only never-scanned addresses (and priority ones) are.
int shieldedScanFloorHeight = -1;
// Scanned first, whatever the cursor, when never scanned or scanned below their floor: new
// addresses, the chat reply address, addresses holding notes the history doesn't show yet.
std::unordered_map<std::string, int> priorityScanFloors;
bool skipShieldedScans = false; // a Light cycle (or a probe-less recent poll): no receive scans
bool skipSentSweep = false;
int sentScanFromHeight = -1; // the sweep's watermark (W); -1 = sweep the whole wallet
// A loaded watermark owes an anchor verify before its first windowed sweep: getblockhash(A) must
// still equal H_A (no reorg deeper than the margin). A mismatch forces a full sweep. While pending,
// the window also uses the bigger first-after-load margin.
bool sentVerifyPending = false;
int sentVerifyAnchorHeight = -1; // A
std::string sentVerifyAnchorHash; // H_A
std::size_t transparentRowHint = 0; // listtransactions rows last time: sizes the first page
// Same tip and nothing changed since the rows on screen were fetched: reuse their transparent rows
// instead of paging listtransactions again.
bool reuseTransparentRows = false;
std::unordered_set<std::string> enrichBacklogTxids; // discovered earlier, not enriched yet
// Run a windowed sent sweep even though the receive pass isn't complete (the caller rate-limits
// this), so untracked sends (from the Console, or another wallet on the same seed) don't wait for a
// whole pass. The whole-wallet sweep stays tied to a completed pass.
bool allowMidPassSentSweep = false;
};
struct TransactionRefreshResult {
std::vector<TransactionInfo> transactions;
std::vector<chat::HushChatTransactionMetadata> hushChatMetadata;
int blockHeight = -1;
TransactionViewCache newViewTxEntries;
// Tracked sends whose view result is final although it shows no outgoing output: the op only
// paid the wallet itself (shield, merge/consolidate, auto-shield, chat note split, a transfer
// between own addresses) and is now mined or conflicted. Applying the result stops tracking them.
std::unordered_set<std::string> resolvedSendTxids;
std::size_t nextShieldedScanStartIndex = 0;
std::size_t shieldedAddressesScanned = 0;
std::size_t shieldedAddressCount = 0;
std::unordered_map<std::string, int> shieldedScanHeights;
bool shieldedScanComplete = true;
// Wall-clock spent in the whole cycle: listtransactions, the receive scans and the sent sweep each
// hold cs_main. Lets the caller throttle wallet-wide scans by their measured cost (txRefreshDue()).
double scanMs = 0.0;
std::size_t shieldedScansSucceeded = 0; // shieldedAddressesScanned counts attempts
double receiveScanOkMs = 0.0; // time in the receive scans that succeeded
std::size_t transparentRowCount = 0; // rows the listtransactions chain returned
bool transparentChainOk = false;
bool transparentRowsReused = false;
bool sentSweepRan = false;
bool sentSweepOk = false;
bool sentSweepFull = false;
int sentSweepHeight = -1; // W: the tip a successful sweep covered
int sentSweepAnchorHeight = -1; // A = W - margin, the bottom edge the window covered
std::string sentSweepAnchorHash; // H_A = getblockhash(A), fetched after a successful sweep
double sentSweepMs = 0.0; // time in the sweep (part of scanMs)
rpc::RpcFailureKind sentSweepErrKind = rpc::RpcFailureKind::None;
// Txids found but not enriched (over the per-cycle cap, or the view failed transiently): the caller
// keeps them and feeds them back as enrichBacklogTxids.
std::unordered_set<std::string> unenrichedTxids;
bool rpcError = false; // a wallet-wide call failed
bool replyTimedOut = false; // ...with a reply timeout (the daemon kept running it)
};
struct OperationStatusPollResult {
std::vector<std::string> doneOpids;
std::vector<std::string> staleOpids;
std::vector<std::string> successTxids;
std::unordered_map<std::string, std::string> successTxidsByOpid;
std::vector<std::string> failureMessages;
std::unordered_map<std::string, std::string> failureByOpid; // opid -> error message
bool anySuccess = false;
};
// Who started a tracked async operation, i.e. what its completion re-reads (App::markWalletMutated).
// Absent from the App's origin map = User.
enum class OperationOrigin : std::uint8_t { User, Auto, Chat };
struct OperationBatchPlan {
bool userSucceeded = false; // >= 1 User op succeeded: re-read the wallet, force the balance
bool autoSucceeded = false; // >= 1 Auto op (client auto-shield) succeeded: re-read, unforced
bool walletTerminal = false; // >= 1 non-chat op finished (any outcome) or went stale
bool chatTerminal = false; // >= 1 chat op finished or went stale: refresh the chat note view
};
struct TransactionCacheUpdate {
TransactionViewCache& viewTxCache;
std::unordered_set<std::string>& sendTxids;
std::vector<TransactionInfo>& confirmedTxCache;
std::unordered_set<std::string>& confirmedTxIds;
int& confirmedCacheBlock;
int& lastTxBlockHeight;
};
static Intervals intervalsForPage(ui::NavPage page) { return RefreshScheduler::intervalsForPage(page); }
static ConnectionInfoResult parseConnectionInfoResult(const nlohmann::json& info);
// The node knows the chain tip: longestchain stays 0 after a start until its first UpdateTip.
static bool connectionInfoTipKnown(const ConnectionInfoResult& info);
// The tip is known and the node is more than 2 blocks behind it (the sync tolerance).
static bool connectionInfoShowsBehind(const ConnectionInfoResult& info);
static WalletEncryptionResult parseWalletEncryptionResult(const nlohmann::json& walletInfo);
static WarmupPollResult collectWarmupPollResult(RefreshRpcGateway& rpc);
static ConnectionInitResult collectConnectionInitResult(
RefreshRpcGateway& rpc,
const std::optional<ConnectionInfoResult>& prefetchedInfo = std::nullopt);
static CoreRefreshResult parseCoreRefreshResult(const nlohmann::json& totalBalance,
const nlohmann::json& spendableBalance,
bool balanceOk,
const nlohmann::json& blockInfo,
bool blockOk);
// includeBalance=false skips z_gettotalbalance (which takes the wallet lock + cs_main) and only
// fetches getblockchaininfo — used while syncing, where the balance is incomplete anyway and the
// wallet should minimise lock contention with block connection.
static CoreRefreshResult collectCoreRefreshResult(RefreshRpcGateway& rpc, const CoreRefreshPlan& plan);
static CoreRefreshResult collectCoreRefreshResult(RefreshRpcGateway& rpc, bool includeBalance = true)
{
CoreRefreshPlan plan;
plan.includeBalance = includeBalance;
return collectCoreRefreshResult(rpc, plan);
}
// An amount as the daemon formats it ("[-]digits[.up to 8 digits]") in zatoshis; nullopt for anything
// else or anything that would overflow. Exact, so "did the total move" never trips on rounding.
static std::optional<std::int64_t> parseMoneyZat(const std::string& text);
// Whether the next balance poll needs the minconf=1 (spendable) call whatever the total does: never
// fetched, unconfirmed or pending funds, or a forced refresh after the user's own send.
static bool spendableBalanceNeeded(const WalletState& state, bool pendingSendWork, bool forced);
// The total the next poll compares against (CoreRefreshPlan::prevTotalZat). None after a poll whose
// minconf=1 call was made but brought back no total (timeout, held back, error): that left spendable
// degraded to display, so the next poll must count the total as moved and measure it again.
static std::optional<std::int64_t> nextTotalBaseline(const CoreRefreshResult& result,
std::optional<std::int64_t> previous);
static MiningRefreshResult parseMiningRefreshResult(const nlohmann::json& miningInfo,
bool miningOk,
const nlohmann::json& localHashrate,
bool hashrateOk,
double daemonMemoryMb);
static MiningRefreshResult collectMiningRefreshResult(RefreshRpcGateway& rpc,
double daemonMemoryMb,
bool includeSlowRefresh,
bool includeLocalHashrate = true);
static PeerRefreshResult parsePeerRefreshResult(const nlohmann::json& peers,
const nlohmann::json& bannedPeers);
static PeerRefreshResult collectPeerRefreshResult(RefreshRpcGateway& rpc);
static std::optional<PriceRefreshResult> parseCoinGeckoPriceResponse(const std::string& response,
std::time_t fetchedAt);
static PriceHttpResult parsePriceHttpResponse(const PriceHttpResponse& response,
std::time_t fetchedAt);
// Parse a CoinGecko /market_chart response ({"prices":[[ms,price],...]}) into a timestamped
// (unix seconds, USD price) series, oldest->newest. Malformed rows are skipped; on any error
// (or a missing "prices" array) an empty vector is returned. Pure/no-I/O.
static std::vector<std::pair<std::time_t, double>> parseCoinGeckoMarketChart(const std::string& response);
static AddressInfo buildShieldedAddressInfo(const std::string& address,
const nlohmann::json& validation,
bool validationSucceeded);
static AddressInfo buildTransparentAddressInfo(const std::string& address);
static std::vector<AddressInfo> parseTransparentAddressList(const nlohmann::json& addressList);
static void applyShieldedBalancesFromUnspent(std::vector<AddressInfo>& addresses,
const nlohmann::json& unspent);
static void applyTransparentBalancesFromUnspent(std::vector<AddressInfo>& addresses,
const nlohmann::json& unspent);
static AddressRefreshSnapshot buildAddressRefreshSnapshot(const WalletState& state);
// listOnly: enumerate the addresses only (cheap), without the wallet-wide balance scans; for while the
// scans are held (the node catching up, the connect gate). Both lists are enumerated first, and the
// balances are scanned only when both enumerations succeeded (a failed pass is discarded whole).
static AddressRefreshResult collectAddressRefreshResult(
RefreshRpcGateway& rpc,
const AddressRefreshSnapshot& snapshot = {},
bool listOnly = false);
static TransactionRefreshSnapshot buildTransactionRefreshSnapshot(const WalletState& state,
const TransactionViewCache& viewTxCache,
const std::unordered_set<std::string>& sendTxids);
static void appendTransparentTransactions(std::vector<TransactionInfo>& transactions,
std::set<std::string>& knownTxids,
const nlohmann::json& result,
const std::set<std::string>& miningAddresses = {});
static void appendShieldedReceivedTransactions(std::vector<TransactionInfo>& transactions,
std::set<std::string>& knownTxids,
const std::string& address,
const nlohmann::json& received,
const std::set<std::string>& miningAddresses = {},
std::unordered_map<std::string, std::vector<chat::HushChatMemoOutput>>* chatMemoOutputs = nullptr);
static TransactionViewCacheEntry parseViewTransactionCacheEntry(const nlohmann::json& viewTransaction);
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
const std::string& txid,
const TransactionViewCacheEntry& entry);
// Build "send" rows from a z_listsentbyaddress entry's sends.transparentSends[] — the transparent
// (z->t deshield) outgoing outputs that z_viewtransaction does not surface. Deduped by
// txid+address+amount against rows already present (e.g. listtransactions-derived t->t sends).
static void appendSentTransparentOutputs(std::vector<TransactionInfo>& transactions,
const std::string& txid,
const nlohmann::json& sentEntry);
static void sortTransactionsNewestFirst(std::vector<TransactionInfo>& transactions);
static TransactionRefreshResult collectTransactionRefreshResult(RefreshRpcGateway& rpc,
const TransactionRefreshSnapshot& snapshot,
int currentBlockHeight,
int maxViewTransactionsPerCycle);
static TransactionRefreshResult collectRecentTransactionRefreshResult(
RefreshRpcGateway& rpc,
const TransactionRefreshSnapshot& snapshot,
int currentBlockHeight,
int pageSize = 100);
static OperationStatusPollResult parseOperationStatusPoll(const nlohmann::json& result,
const std::vector<std::string>& requestedOpids);
// Params for z_getoperationstatus/result filtered to these opids: ONE argument, an array of ids, so
// [[id, ...]]. nullopt when no non-empty id remains: an empty filter means every op the node has
// retained. Built explicitly, because a one-element braced list of a json ({ids}) collapses to the
// inner array, which the daemon rejects ("JSON value is not an array as expected").
static std::optional<nlohmann::json> buildOperationFilterParams(const std::vector<std::string>& opids);
// Classify a poll's terminal ops by origin (absent from the map = User).
static OperationBatchPlan planOperationBatch(const OperationStatusPollResult& parsed,
const std::unordered_map<std::string, OperationOrigin>& origins);
// Any pending id is a User op (absent from the map = User).
static bool anyUserOperationPending(const std::vector<std::string>& pendingOpids,
const std::unordered_map<std::string, OperationOrigin>& origins);
struct DaemonRescanScan {
bool foundRescan = false;
bool finished = false;
float rescanPct = 0.0f;
std::string lastStatus;
bool foundWitness = false;
float witnessPct = -1.0f;
int witnessRemaining = -1;
int witnessReadTotal = -1;
bool witnessRebuilt = false;
std::vector<std::string> witnessTxids;
int witnessTotalTxs = -1;
};
// Pure line-by-line scan of newly-appended dragonxd stdout for rescan + Sapling
// witness-rebuild progress. No I/O, no state — unit-tested against fixture log snippets.
static DaemonRescanScan parseDaemonRescanOutput(const std::string& output);
static void applyConnectionInfoResult(WalletState& state, const ConnectionInfoResult& result);
static void applyWalletEncryptionResult(WalletState& state, const WalletEncryptionResult& result);
static void applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result);
static void applyCoreRefreshResult(WalletState& state,
const CoreRefreshResult& result,
std::time_t updatedAt);
static void applyMiningRefreshResult(WalletState& state,
const MiningRefreshResult& result,
std::time_t updatedAt);
static void applyPeerRefreshResult(WalletState& state,
PeerRefreshResult&& result,
std::time_t updatedAt);
static void markPriceRefreshStarted(WalletState& state);
static void applyPriceRefreshResult(WalletState& state,
const PriceRefreshResult& result,
std::chrono::steady_clock::time_point fetchedAt);
static void applyPriceRefreshFailure(WalletState& state,
const std::string& errorMessage);
static void applyAddressRefreshResult(WalletState& state,
AddressRefreshResult&& result);
static void applyTransactionRefreshResult(WalletState& state,
TransactionCacheUpdate cacheUpdate,
TransactionRefreshResult&& result,
std::time_t updatedAt);
void applyPage(ui::NavPage page) { scheduler_.applyPage(page); }
void setIntervals(Intervals intervals) { scheduler_.setIntervals(intervals); }
const Intervals& intervals() const { return scheduler_.intervals(); }
void tick(float deltaSeconds) { scheduler_.tick(deltaSeconds); }
bool isDue(Timer timer) const { return scheduler_.isDue(timer); }
bool consumeDue(Timer timer) { return scheduler_.consumeDue(timer); }
void reset(Timer timer) { scheduler_.reset(timer); }
void markDue(Timer timer) { scheduler_.markDue(timer); }
void setTimer(Timer timer, float seconds) { scheduler_.setTimer(timer, seconds); }
void deferFor(Timer timer, float seconds) { scheduler_.deferFor(timer, seconds); }
void retrySoon(Timer timer, float seconds) { scheduler_.retrySoon(timer, seconds); }
float timer(Timer timer) const { return scheduler_.timer(timer); }
float interval(Timer timer) const { return scheduler_.interval(timer); }
void markImmediateRefresh() { scheduler_.markImmediateRefresh(); }
void markWalletMutationRefresh() { scheduler_.markWalletMutationRefresh(); }
void resetTxAge() { scheduler_.resetTxAge(); }
bool shouldRefreshTransactions(int lastTxBlockHeight,
int currentBlockHeight,
bool transactionsDirty) const;
bool beginJob(Job job);
bool beginJob(Job job, std::size_t queuedWork, std::size_t maxQueuedWork);
void finishJob(Job job);
bool jobInProgress(Job job) const;
void resetJobs();
DispatchTicket beginDispatch(Job job, std::size_t queuedWork = 0, std::size_t maxQueuedWork = 0);
bool completeDispatch(const DispatchTicket& ticket);
void cancelDispatch(const DispatchTicket& ticket);
JobStats stats(Job job) const;
template <typename Worker, typename WorkFn>
EnqueueResult enqueue(Job job, Worker& worker, WorkFn&& work, std::size_t maxQueuedWork = 0)
{
std::size_t queueDepth = worker.pendingTaskCount();
auto ticket = beginDispatch(job, queueDepth, maxQueuedWork);
if (!ticket.accepted) return {ticket, false, queueDepth};
worker.post([this, ticket, work = std::forward<WorkFn>(work)]() mutable -> rpc::RPCWorker::MainCb {
rpc::RPCWorker::MainCb mainCallback;
try {
mainCallback = work();
} catch (...) {
mainCallback = nullptr;
}
return [this, ticket, mainCallback = std::move(mainCallback)]() mutable {
if (!completeDispatch(ticket)) return;
if (mainCallback) mainCallback();
};
});
return {ticket, true, queueDepth};
}
private:
std::atomic<bool>& jobFlag(Job job);
const std::atomic<bool>& jobFlag(Job job) const;
static std::size_t jobIndex(Job job);
RefreshScheduler scheduler_;
std::atomic<bool> coreInProgress_{false};
std::atomic<bool> addressesInProgress_{false};
std::atomic<bool> transactionsInProgress_{false};
std::atomic<bool> miningInProgress_{false};
std::atomic<bool> peersInProgress_{false};
std::atomic<bool> priceInProgress_{false};
std::atomic<bool> encryptionInProgress_{false};
std::atomic<bool> connectionInitInProgress_{false};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> generations_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> started_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> finished_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> skippedInFlight_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> skippedQueuePressure_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> staleCallbacks_{};
std::array<std::atomic<std::size_t>, static_cast<std::size_t>(Job::Count)> lastQueueDepth_{};
};
} // namespace services
} // namespace dragonx