perf(lite): move wallet ops and server switches off the UI thread

New address, Import key, Encrypt and Remove encryption called the
backend on the UI thread. Each ends in the backend's save, which waits on
the lock a scan holds for its whole duration and every refresh read holds
briefly, so during a scan the window froze until the scan ended. A create
or restore with a passphrase encrypted and saved just after its first
scan had started: for a restore, a freeze for the whole initial scan. A
server switch destroyed the old controller on the UI thread and joined
its refresh worker, which can sit for minutes in a read against a dead
server (each balance, notes or list read retries for ~62 s).

- The four ops run one at a time on a background lane and their results
  are applied on the main thread; Settings shows "Working…" meanwhile.
  Key and passphrase copies are wiped after the call, or when a queued
  op is dropped.
- A create or restore encrypts and saves (an open unlocks) before the
  first scan starts, so the save takes milliseconds and the scan runs on
  the encrypted file.
- Import, Encrypt, Remove encryption and Receive's New address are
  disabled while a scan runs ("Wait for blockchain to sync"); Redownload
  blocks is disabled while an op is queued.
- A switch retires the old controller without blocking (queued ops
  dropped, pending ones reported failed, its client shut down) and
  destroys it on a background thread. It no longer saves on the way out:
  the new client re-scans the blocks since the last save, as after a
  restart.
- A switch waits for an op in flight, at most 15 s, so the op can't
  finish and save on the old client while the new one owns the wallet
  file. The Network tab already shows the new selection meanwhile.
- The refresh worker stops between backend commands, skips the remaining
  server reads once one fails, and then retries after 4, 8, then 15 s
  instead of every 2 s.
- Quit waits up to 2.5 s for an op in flight, e.g. Encrypt's save.

Encrypt and Remove encryption now report a failed save: the backend
returns it as a JSON error that the old check never saw, so the failure
was reported as success. The trimmed copy of a key typed into Import is
now wiped too, and stopping the refresh worker can no longer miss its
wakeup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 21:09:48 -05:00
co-authored by Claude Opus 5.5
parent b2a57a61a0
commit 55975901a2
11 changed files with 1023 additions and 147 deletions
+10 -3
View File
@@ -768,9 +768,9 @@ public:
void verifyDebugCredential(const std::string& secret, std::function<void(bool)> cb);
/// @brief Something finished on a background thread and waits for update() to apply it on the main thread
/// (the RPC worker lanes, the HTTP lane). main.cpp's idle loop polls this every <= 200 ms (hasImmediateWork) and
/// renders a frame when it is true, so the result shows without input, even while the window is hidden. A new
/// lane whose results are applied in update() adds its term HERE.
/// (the RPC worker lanes, the HTTP lane, lite wallet ops). main.cpp's idle loop polls this every <= 200 ms
/// (hasImmediateWork) and renders a frame when it is true, so the result shows without input, even while the
/// window is hidden. A new lane whose results are applied in update() adds its term HERE.
bool hasPendingBackgroundResults() const;
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
std::string transactionSendProgressText() const;
@@ -927,6 +927,13 @@ private:
// Reason an existing lite wallet failed to auto-open (e.g. server unreachable). Surfaced in
// the UI so a stuck "disconnected" state isn't silent; cleared once a wallet opens.
std::string lite_open_error_;
// A forced lite rebuild (Network-tab server switch) deferred while a wallet op is in flight; update() retries it
// (rebuildLiteWallet). deferred_at bounds the wait (wallet::kLiteServerSwitchMaxDeferSeconds).
bool lite_rebuild_pending_ = false;
std::chrono::steady_clock::time_point lite_rebuild_deferred_at_{};
// Retire the current lite controller without blocking the UI thread (fail its pending op callbacks, stop its
// worker, shut its client down) and destroy it on a detached thread. lite_wallet_ is null afterwards.
void retireLiteWallet();
// HushChat (experimental; gated by DRAGONX_ENABLE_CHAT — inert when OFF). App owns the chat
// service so the transaction-refresh harvest can decrypt incoming memos into threaded messages.