feat(lite): async wallet open with server failover

Opening an existing lite wallet ran synchronously on the UI thread and used a
single server, so a dead/unreachable lightwalletd server froze startup for the
connect timeout and then stranded the wallet ("disconnected" spinner) — and the
DragonX lite servers are flaky (often several down at once).

Add LiteWalletController::beginOpenExisting() / pumpAsyncOpen(): the open runs on
a background thread (mirroring the sync/broadcast shared-lifetime pattern — it
captures only shared_ptrs + value copies, never `this`), trying the preferred
server first and then every other usable default until one succeeds. The main
thread finalizes the result (flips walletOpen, starts sync) or records the reason.
The rollout gate is still checked up-front on the main thread.

App: auto-open now calls beginOpenExisting() and pumps it each tick, retrying on
a 20s interval so a transient outage self-heals once a server returns; a failed
open surfaces its reason (notification + Network tab) instead of a silent spinner.

Tested: a fake bridge that fails specific servers exercises both
preferred-dead -> fallback-opens and all-dead -> fails-with-reason. Built clean
for full-node, lite, and Windows cross-compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 16:53:24 -05:00
parent 9ff5508989
commit dbeae3ac98
6 changed files with 219 additions and 20 deletions

View File

@@ -161,6 +161,19 @@ public:
LiteWalletLifecycleResult openWallet(LiteWalletOpenRequest request);
LiteWalletLifecycleResult restoreWallet(LiteWalletRestoreRequest request);
// Asynchronous open of an EXISTING wallet WITH server failover: tries the selected lite
// server first, then the other usable defaults, on a background thread — so an unreachable
// server never freezes the UI and a single dead server no longer strands the wallet.
// Non-blocking; call pumpAsyncOpen() each tick to finalize. Returns false if a wallet is
// already open, an open is already in progress, no wallet exists, or the rollout gate blocks
// lite. On all-servers-fail, status()/lastOpenError() report the reason.
bool beginOpenExisting();
// Finalize a completed async open on the calling (main) thread: flips walletOpen()/status 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(); }
const std::string& lastOpenError() const { return lastOpenError_; }
bool syncStarted() const { return syncStarted_; }
bool syncComplete() const { return syncDone_ && syncDone_->load(); }
@@ -249,6 +262,7 @@ private:
// mutex would instead serialize sync against syncstatus polling and defeat the design.
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
LiteWalletLifecycleService lifecycle_;
LiteWalletGateway gateway_;
LiteSyncService sync_;
@@ -272,6 +286,18 @@ private:
std::shared_ptr<std::optional<LiteBroadcastResult>> broadcastResult_ =
std::make_shared<std::optional<LiteBroadcastResult>>();
// Asynchronous open with server failover (mirrors the sync/broadcast shared-lifetime pattern:
// the detached thread captures only shared_ptrs + value copies, never `this`, so it can
// safely outlive the controller). pumpAsyncOpen() finalizes the result on the main thread.
std::vector<std::string> failoverServerUrls() const;
struct OpenOutcome { bool ok = false; std::string serverUrl; std::string error; };
std::thread openThread_;
std::shared_ptr<std::atomic<bool>> openRunning_ = std::make_shared<std::atomic<bool>>(false);
std::shared_ptr<std::mutex> openResultMutex_ = std::make_shared<std::mutex>();
std::shared_ptr<std::optional<OpenOutcome>> openResult_ =
std::make_shared<std::optional<OpenOutcome>>();
std::string lastOpenError_; // main-thread only
// Joinable background refresh worker (fast iterations: syncstatus, plus data once synced).
std::thread worker_;
std::atomic<bool> running_{false};