perf(net): one balance scan per poll, filtered opstatus, a guarded merge scope
The Core poll ran z_gettotalbalance twice (minconf 0 and 1), each a whole- wallet scan under dragonxd's cs_main. The spendable (minconf=1) call now runs only after the display call succeeded, and only when something is unconfirmed or pending or the exact total moved: about 95% of polls make one scan instead of two. A forced post-send balance can no longer be lost to a refused enqueue, and pending-send debits aren't re-applied each tick. z_getoperationstatus is filtered to our opids with its real argument form ([[id,...]]; the old ["id"] was rejected), falling back to the unfiltered call if a daemon refuses the filter. The Shield/Merge dialog's own polls are filtered and session-guarded too, and finally reach Done; the dead RPCClient wrappers are deleted. The Merge/Consolidate scope scan becomes a tested state machine: one scan at a time, failures latch behind a Retry button, only "node not ready" retries by itself (10/30/60 s, at most 3), Lite never scans. Its 120 s calls are maintenance calls: every main-thread disconnect aborts one in flight first, so a lifecycle action no longer freezes the UI behind it. An aborted merge that was already sent says it may still go through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,12 @@
|
||||
// Shielded only: a transparent address can't receive a non-coinbase output on DragonX (private chain).
|
||||
// Pure apart from the merge call it is handed, so the test target can drive it.
|
||||
|
||||
#include "rpc/rpc_failure.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -26,10 +30,14 @@ struct NoteGroup {
|
||||
double amount = 0.0;
|
||||
};
|
||||
|
||||
// Spendable shielded notes grouped by address, most notes first. Transparent UTXOs and unconfirmed or
|
||||
// watch-only notes are left out (z_mergetoaddress only spends confirmed notes it has keys for).
|
||||
// Spendable shielded notes grouped by address, most notes first. Transparent UTXOs and unconfirmed,
|
||||
// watch-only or locked notes are left out (z_mergetoaddress only spends confirmed, unlocked notes it has
|
||||
// keys for).
|
||||
std::vector<NoteGroup> groupSpendableNotes(const nlohmann::json& zListUnspent);
|
||||
|
||||
// A z_listunspent entry the wallet has locked, e.g. for an operation in flight ("locked": true).
|
||||
bool noteIsLocked(const nlohmann::json& zListUnspentEntry);
|
||||
|
||||
// Worth consolidating in place: at least two notes, and more than one batch's fee to work with.
|
||||
bool worthConsolidating(const NoteGroup& group, double fee);
|
||||
|
||||
@@ -59,5 +67,59 @@ struct AddressRun {
|
||||
// locks a batch's notes before the call returns, so each call takes the next batch. At most maxBatches.
|
||||
AddressRun consolidateInPlace(const std::string& address, const MergeCall& call, int maxBatches = 20);
|
||||
|
||||
// ── Scope scan (the Merge/Consolidate dialog's listunspent + z_listunspent) ─────────────────────────
|
||||
// Both calls are whole-wallet scans under the daemon's cs_main. The dialog used to post the pair again on
|
||||
// the frame after any failure, every frame for as long as it stayed open (and forever on Lite, which has
|
||||
// no node). This decides when a scan may be posted; the dialog owns one ScopeScan.
|
||||
|
||||
inline double steadySeconds() // ImGui::GetTime() stands still while the window is minimized
|
||||
{
|
||||
return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
inline constexpr long kScopeCallTimeoutSec = 120; // each call; a large wallet can take minutes
|
||||
inline constexpr double kScopeInFlightMaxSec = 1800; // a scan outstanding this long is presumed lost
|
||||
inline constexpr int kScopeMaxAutoRetries = 3;
|
||||
|
||||
enum class ScopePhase { Idle, Loading, Loaded, Failed };
|
||||
|
||||
struct ScopeScan {
|
||||
ScopePhase phase = ScopePhase::Idle;
|
||||
std::uint32_t gen = 0; // bumped per dialog session; a result from another session is dropped
|
||||
bool inFlight = false; // a scan is posted and its result not yet in (survives close/reopen)
|
||||
std::uint32_t inFlightGen = 0; // the session that posted it: only its result clears inFlight
|
||||
double startedAt = 0.0;
|
||||
int failures = 0;
|
||||
bool autoRetry = false; // the last failure was "node not ready yet": retry by itself
|
||||
double retryAt = 0.0;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
struct ScopeInputs {
|
||||
bool mergeMode = false;
|
||||
bool busyWithOp = false; // a merge was submitted or is in flight
|
||||
bool nodeReady = false; // App::walletScanReady()
|
||||
double now = 0.0;
|
||||
};
|
||||
|
||||
enum class ScopeWant { Nothing, Post, WaitNode, WaitBackoff };
|
||||
|
||||
ScopeWant scopeScanWant(const ScopeScan& scan, const ScopeInputs& in);
|
||||
// Mark a scan posted: Loading, in flight for this session. Returns the generation to finish it with.
|
||||
std::uint32_t scopeScanBegin(ScopeScan& scan, double now);
|
||||
// Record a scan's result. True when it belongs to the current session and succeeded: apply its data.
|
||||
bool scopeScanFinish(ScopeScan& scan, std::uint32_t gen, const std::string& error, rpc::RpcFailureKind kind,
|
||||
double now);
|
||||
// The dialog opened: start a new session, unless a scan of the last one is still loading (adopt it).
|
||||
void scopeScanOpen(ScopeScan& scan, double now);
|
||||
// The data went stale (reconnect): the next want is a fresh scan. An in-flight scan stays guarded.
|
||||
void scopeScanInvalidate(ScopeScan& scan);
|
||||
// The user pressed Retry after a failure.
|
||||
void scopeScanRetry(ScopeScan& scan);
|
||||
double scopeRetryDelaySec(int failures); // 10, 30, then 60 s
|
||||
// Retry by itself only when the node wasn't ready (refused, warming up, held back as busy, aborted):
|
||||
// not after a reply timeout or a real error, each of which would cost another pair of full scans.
|
||||
bool scopeFailureRetries(rpc::RpcFailureKind kind, const std::string& message);
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
|
||||
Reference in New Issue
Block a user