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:
@@ -5,6 +5,7 @@
|
||||
#include "note_consolidation.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <map>
|
||||
|
||||
namespace dragonx {
|
||||
@@ -20,6 +21,7 @@ std::vector<NoteGroup> groupSpendableNotes(const nlohmann::json& zListUnspent)
|
||||
if (address.empty()) continue;
|
||||
if (n.value("confirmations", 0) < 1) continue; // z_mergetoaddress spends confirmed notes only
|
||||
if (!n.value("spendable", true)) continue; // watch-only: no key to spend it
|
||||
if (noteIsLocked(n)) continue; // held by an operation in flight
|
||||
NoteGroup& g = byAddress[address];
|
||||
g.address = address;
|
||||
++g.notes;
|
||||
@@ -36,6 +38,13 @@ std::vector<NoteGroup> groupSpendableNotes(const nlohmann::json& zListUnspent)
|
||||
return out;
|
||||
}
|
||||
|
||||
bool noteIsLocked(const nlohmann::json& zListUnspentEntry)
|
||||
{
|
||||
if (!zListUnspentEntry.is_object()) return false;
|
||||
const auto it = zListUnspentEntry.find("locked");
|
||||
return it != zListUnspentEntry.end() && it->is_boolean() && it->get<bool>();
|
||||
}
|
||||
|
||||
bool worthConsolidating(const NoteGroup& group, double fee)
|
||||
{
|
||||
// The daemon rejects a merge whose fee is more than what it nets (merged value < 2 x fee).
|
||||
@@ -67,5 +76,107 @@ AddressRun consolidateInPlace(const std::string& address, const MergeCall& call,
|
||||
return run;
|
||||
}
|
||||
|
||||
ScopeWant scopeScanWant(const ScopeScan& scan, const ScopeInputs& in)
|
||||
{
|
||||
if (!in.mergeMode || scan.phase == ScopePhase::Loaded) return ScopeWant::Nothing;
|
||||
if (scan.inFlight || in.busyWithOp) return ScopeWant::Nothing;
|
||||
if (scan.phase == ScopePhase::Failed && !scan.autoRetry) return ScopeWant::Nothing; // waits for Retry
|
||||
if (in.now < scan.retryAt) return ScopeWant::WaitBackoff;
|
||||
if (!in.nodeReady) return ScopeWant::WaitNode;
|
||||
return ScopeWant::Post;
|
||||
}
|
||||
|
||||
std::uint32_t scopeScanBegin(ScopeScan& scan, double now)
|
||||
{
|
||||
scan.phase = ScopePhase::Loading;
|
||||
scan.inFlight = true;
|
||||
scan.inFlightGen = scan.gen;
|
||||
scan.startedAt = now;
|
||||
return scan.gen;
|
||||
}
|
||||
|
||||
bool scopeScanFinish(ScopeScan& scan, std::uint32_t gen, const std::string& error, rpc::RpcFailureKind kind,
|
||||
double now)
|
||||
{
|
||||
if (scan.inFlight && gen == scan.inFlightGen) scan.inFlight = false;
|
||||
if (gen != scan.gen) return false; // a scan from an earlier session: its data is not this one's
|
||||
if (error.empty()) {
|
||||
scan.phase = ScopePhase::Loaded;
|
||||
scan.failures = 0;
|
||||
scan.autoRetry = false;
|
||||
scan.retryAt = 0.0;
|
||||
scan.error.clear();
|
||||
return true;
|
||||
}
|
||||
scan.phase = ScopePhase::Failed;
|
||||
++scan.failures;
|
||||
scan.error = error;
|
||||
scan.autoRetry = scopeFailureRetries(kind, error) && scan.failures <= kScopeMaxAutoRetries;
|
||||
scan.retryAt = scan.autoRetry ? now + scopeRetryDelaySec(scan.failures) : 0.0;
|
||||
return false;
|
||||
}
|
||||
|
||||
void scopeScanOpen(ScopeScan& scan, double now)
|
||||
{
|
||||
if (scan.inFlight && now - scan.startedAt > kScopeInFlightMaxSec) scan.inFlight = false; // lost
|
||||
if (scan.inFlight && scan.phase == ScopePhase::Loading) return; // still loading for the last session
|
||||
++scan.gen;
|
||||
scan.phase = ScopePhase::Idle;
|
||||
scan.failures = 0;
|
||||
scan.autoRetry = false;
|
||||
scan.retryAt = 0.0;
|
||||
scan.error.clear();
|
||||
}
|
||||
|
||||
void scopeScanInvalidate(ScopeScan& scan)
|
||||
{
|
||||
++scan.gen;
|
||||
scan.phase = ScopePhase::Idle;
|
||||
scan.failures = 0;
|
||||
scan.autoRetry = false;
|
||||
scan.retryAt = 0.0;
|
||||
scan.error.clear();
|
||||
}
|
||||
|
||||
void scopeScanRetry(ScopeScan& scan)
|
||||
{
|
||||
if (scan.phase != ScopePhase::Failed) return;
|
||||
scan.phase = ScopePhase::Idle;
|
||||
scan.failures = 0;
|
||||
scan.autoRetry = false;
|
||||
scan.retryAt = 0.0;
|
||||
scan.error.clear();
|
||||
}
|
||||
|
||||
double scopeRetryDelaySec(int failures)
|
||||
{
|
||||
if (failures <= 1) return 10.0;
|
||||
if (failures == 2) return 30.0;
|
||||
return 60.0;
|
||||
}
|
||||
|
||||
bool scopeFailureRetries(rpc::RpcFailureKind kind, const std::string& message)
|
||||
{
|
||||
using K = rpc::RpcFailureKind;
|
||||
switch (kind) {
|
||||
case K::NotListening:
|
||||
case K::ConnectTimeout:
|
||||
case K::Warmup:
|
||||
case K::Deferred:
|
||||
case K::ShuttingDown:
|
||||
case K::Unhandled:
|
||||
case K::Aborted:
|
||||
return true;
|
||||
case K::Other: {
|
||||
std::string lower = message;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return lower.find("not connected") != std::string::npos;
|
||||
}
|
||||
default:
|
||||
return false; // ReplyTimeout, QueueFull, AppError, Auth, parse/type errors
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
|
||||
Reference in New Issue
Block a user