feat(lite): async + failover for Settings-page create/open/restore

The Settings page drove the controller's synchronous createWallet/openWallet/
restoreWallet, which blocks the UI thread on the (often flaky) lightwalletd and
gives up after the first server. Add a generic async lifecycle path that mirrors
the async-open failover but carries the full request (passphrase, restore seed/
birthday/account/overwrite):

  - beginCreateWalletAsync / beginOpenWalletAsync / beginRestoreWalletAsync run
    on a detached thread that builds its OWN local LiteWalletLifecycleService
    from captured value copies + the shared bridge (never `this`, so it can
    safely outlive the controller). Each request type's serverUrl override field
    feeds the failover: try the preferred server, then every other usable
    default; stop on the first ready wallet or a structural block; keep the
    preferred server's error on total failure. The request's secrets are wiped
    once the attempt finishes.
  - pumpLifecycleResult() finalizes on the main thread (flip walletOpen, persist,
    start sync) and caches the result for the UI; wired into App::update next to
    pumpAsyncOpen(). beginAsyncLifecycle() now also yields to an in-flight
    lifecycle request so the auto-open loop can't race it on the same bridge.
  - settings_page kicks off the async op, disables the button while in flight,
    and polls the cached result each frame for the status/summary.

Tests: testLiteWalletControllerAsyncLifecycleFailover covers async create (with
passphrase) and restore failing over preferred->fallback, plus all-servers-down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 11:42:47 -05:00
parent 320c659689
commit 6f9123f651
5 changed files with 315 additions and 15 deletions

View File

@@ -178,6 +178,25 @@ public:
// and starts sync, or records the failure. Cheap no-op when nothing is pending. Call each tick.
void pumpAsyncOpen();
bool openInProgress() const { return openRunning_ && openRunning_->load(); }
// Asynchronous FULL lifecycle (create / open / restore WITH passphrase + restore params) with
// the same server failover as beginOpenExisting(). Unlike beginCreateWallet() (plain new seed,
// no passphrase), these carry the complete request so the Settings page can create with
// encryption, open an encrypted wallet, or restore from a seed without freezing the UI on a
// flaky server. Non-blocking; finalized by pumpLifecycleResult() on the main thread. The
// request is taken by value and its secret fields are securely wiped once the attempt finishes.
// Returns false if a wallet is open, a lifecycle/open is already in progress, or no usable
// server exists (the request's secrets are still wiped on that path).
bool beginCreateWalletAsync(LiteWalletCreateRequest request);
bool beginOpenWalletAsync(LiteWalletOpenRequest request);
bool beginRestoreWalletAsync(LiteWalletRestoreRequest request);
bool lifecycleRequestInProgress() const { return lifecycleRunning_ && lifecycleRunning_->load(); }
// Finalize a completed async lifecycle request on the main thread (flip walletOpen()/status,
// persist, start sync) and cache the result for the UI. Cheap no-op when nothing is pending.
// Call each tick alongside pumpAsyncOpen().
void pumpLifecycleResult();
// The most recent finalized async-lifecycle result (create/open/restore), for UI display.
const LiteWalletLifecycleResult& lastLifecycleResult() const { return lastLifecycleResult_; }
const std::string& lastOpenError() const { return lastOpenError_; }
// True if the last failed open hit a server that was merely warming up (JSON-RPC -28 /
// "Activating best chain"): the server is healthy and will be ready shortly, so the caller
@@ -273,6 +292,8 @@ private:
std::shared_ptr<LiteClientBridge> bridge_;
std::string chainName_; // backend chain id (for walletExists); from connection settings
LiteConnectionSettings connectionSettings_; // kept for the failover candidate-server list
WalletCapabilities capabilities_; // for thread-local lifecycle services (async path)
LiteWalletLifecycleOptions lifecycleOptions_; // ditto (allowBridgeCalls / rollout gate)
LiteWalletLifecycleService lifecycle_;
LiteWalletGateway gateway_;
LiteSyncService sync_;
@@ -311,6 +332,21 @@ private:
std::string lastOpenError_; // main-thread only
bool lastOpenWarming_ = false; // last failed open hit a warming-up (-28) server
// Asynchronous FULL lifecycle (create/open/restore with passphrase + restore params). Same
// shared-lifetime discipline as the open thread: the detached thread builds its OWN local
// LiteWalletLifecycleService from captured value copies + the shared bridge (never `this`), so
// it can safely outlive the controller. pumpLifecycleResult() finalizes on the main thread.
bool beginLifecycleRequestAsync(
const char* verb,
std::function<LiteWalletLifecycleResult(LiteWalletLifecycleService&, const std::string&)> exec,
std::function<void()> wipeSecrets);
std::thread lifecycleThread_;
std::shared_ptr<std::atomic<bool>> lifecycleRunning_ = std::make_shared<std::atomic<bool>>(false);
std::shared_ptr<std::mutex> lifecycleResultMutex_ = std::make_shared<std::mutex>();
std::shared_ptr<std::optional<LiteWalletLifecycleResult>> lifecycleResult_ =
std::make_shared<std::optional<LiteWalletLifecycleResult>>();
LiteWalletLifecycleResult lastLifecycleResult_; // main-thread only; last finalized request
// Joinable background refresh worker (fast iterations: syncstatus, plus data once synced).
std::thread worker_;
std::atomic<bool> running_{false};