feat(ux): robust sync ETA, persist Explorer settings, gate export-all out of lite

- Sync ETA: a new pure util::SyncRateEstimator (App-owned) replaces the
  function-local statics that went stale after a rescan/restart (producing the
  "412h" ETA). It self-resets on a height drop and on connect/disconnect/rescan/
  restart (via an atomic flag, since onConnected can run off-thread), formats
  with days, and uses the Warning() token.
- Settings: the Explorer-tab URL fields + custom-fees/fetch-prices checkboxes
  now persist on edit — they were the only controls still relying on the
  About-only Save button that a user on this tab never sees.
- Export-all-keys: hidden in the lite build (which has its own key export),
  instead of dead-ending on "Not connected to the daemon".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 13:02:46 -05:00
co-authored by Claude Opus 4.8
parent 8fe20f6385
commit f3f0b32a48
5 changed files with 99 additions and 42 deletions
+64
View File
@@ -0,0 +1,64 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// A tiny, pure block-sync rate estimator for the status-bar / peers-card ETA. Pure (no ImGui, no App):
// App owns one instance, feeds it the current height + a monotonic clock once per frame while syncing,
// and resets it on connect / disconnect / rescan / restart so a height drop never produces a stale or
// absurd ("412h") ETA.
#pragma once
#include <cstddef>
#include <cstdio>
namespace dragonx {
namespace util {
class SyncRateEstimator {
public:
void reset() { prevBlocks_ = -1; prevTime_ = 0.0; rate_ = 0.0; }
// Call once per frame while syncing. `now` is monotonic seconds (e.g. ImGui::GetTime()).
void update(int blocks, double now) {
// First sample, a rescan/restart height drop, or a clock reset → re-baseline, don't compute.
if (prevBlocks_ < 0 || blocks < prevBlocks_ || now < prevTime_) {
prevBlocks_ = blocks; prevTime_ = now; rate_ = 0.0; return;
}
const double dt = now - prevTime_;
if (blocks > prevBlocks_) {
if (dt > 0.5) {
const double raw = (blocks - prevBlocks_) / dt;
rate_ = rate_ > 0.0 ? rate_ * 0.7 + raw * 0.3 : raw; // smooth (same alpha as the old code)
prevBlocks_ = blocks; prevTime_ = now;
}
} else if (dt > 10.0) { // stalled: decay and re-baseline the clock
rate_ *= 0.5; prevTime_ = now;
if (rate_ < 0.1) rate_ = 0.0;
}
}
double rate() const { return rate_; } // blocks/sec, 0 when unknown
// <= 0 when unknown. Capped at ~1 year so a tiny rate can't overflow.
long long etaSeconds(int blocksLeft) const {
if (rate_ <= 0.1 || blocksLeft <= 0) return -1;
const double s = blocksLeft / rate_;
return s > 3.15e7 ? 31500000LL : static_cast<long long>(s);
}
private:
int prevBlocks_ = -1; // -1 = unset (distinct from a legitimate height 0)
double prevTime_ = 0.0;
double rate_ = 0.0;
};
// Human ETA with days, e.g. "3d 4h", "5h 12m", "7m 9s", "42s". English unit letters (as the old code).
inline void formatEta(long long s, char* out, std::size_t n) {
if (s >= 86400) std::snprintf(out, n, "%lldd %lldh", s / 86400, (s % 86400) / 3600);
else if (s >= 3600) std::snprintf(out, n, "%lldh %lldm", s / 3600, (s % 3600) / 60);
else if (s >= 60) std::snprintf(out, n, "%lldm %llds", s / 60, s % 60);
else std::snprintf(out, n, "%llds", s);
}
} // namespace util
} // namespace dragonx