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:
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "../data/wallet_state.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
@@ -229,6 +230,7 @@ LiteWalletController::LiteWalletController(WalletCapabilities capabilities,
|
||||
LiteWalletControllerOptions options)
|
||||
: bridge_(std::make_shared<LiteClientBridge>(std::move(bridge))),
|
||||
chainName_(connectionSettings.chainName),
|
||||
connectionSettings_(connectionSettings),
|
||||
lifecycle_(capabilities, connectionSettings, bridge_.get(),
|
||||
LiteWalletLifecycleOptions{options.allowBridgeCalls,
|
||||
options.rolloutBlocked, options.rolloutMessage}),
|
||||
@@ -257,6 +259,9 @@ LiteWalletController::~LiteWalletController()
|
||||
// Likewise the broadcast thread (send/shield proving): it captures shared refs (bridge +
|
||||
// running flag + result slot), never `this`, so detaching is safe.
|
||||
if (broadcastThread_.joinable()) broadcastThread_.detach();
|
||||
// The async-open failover thread captures only shared refs (bridge + running flag + result
|
||||
// slot), never `this`, so detaching is safe if it's still trying servers at shutdown.
|
||||
if (openThread_.joinable()) openThread_.detach();
|
||||
}
|
||||
|
||||
std::unique_ptr<LiteWalletController> LiteWalletController::createLinked(
|
||||
@@ -282,6 +287,102 @@ void LiteWalletController::onLifecycleResult(const LiteWalletLifecycleResult& re
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> LiteWalletController::failoverServerUrls() const
|
||||
{
|
||||
std::vector<std::string> urls;
|
||||
const auto add = [&urls](const std::string& url) {
|
||||
if (!isLiteServerUrlUsable(url)) return;
|
||||
if (std::find(urls.begin(), urls.end(), url) == urls.end()) urls.push_back(url);
|
||||
};
|
||||
// Preferred server first (honours the user's sticky/random selection), then every other
|
||||
// usable configured server as a fallback so one dead server can't strand the wallet.
|
||||
const auto selected = selectLiteServer(connectionSettings_);
|
||||
if (selected.ok) add(selected.server.url);
|
||||
for (const auto& endpoint : connectionSettings_.servers) {
|
||||
if (endpoint.enabled) add(endpoint.url);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
bool LiteWalletController::beginOpenExisting()
|
||||
{
|
||||
if (walletOpen_.load() || openRunning_->load()) return false;
|
||||
if (lifecycle_.availability() != LiteWalletLifecycleAvailability::Ready) {
|
||||
const std::string& reason = lifecycle_.status().message;
|
||||
status_ = WalletBackendStatus{WalletBackendState::Error,
|
||||
reason.empty() ? "lite wallet is not available" : reason, {}, {}, 0.0};
|
||||
lastOpenError_ = status_.message;
|
||||
return false;
|
||||
}
|
||||
auto servers = failoverServerUrls();
|
||||
if (servers.empty()) {
|
||||
status_ = WalletBackendStatus{WalletBackendState::Error,
|
||||
"no usable lite servers are configured", {}, {}, 0.0};
|
||||
lastOpenError_ = status_.message;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (openThread_.joinable()) openThread_.join(); // a prior attempt has fully finished
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(*openResultMutex_);
|
||||
openResult_->reset();
|
||||
}
|
||||
openRunning_->store(true);
|
||||
status_ = WalletBackendStatus{WalletBackendState::Connecting, "opening wallet", {}, {}, 0.0};
|
||||
|
||||
// Capture only shared refs + value copies (never `this`) so the thread can safely outlive us.
|
||||
auto bridge = bridge_;
|
||||
auto running = openRunning_;
|
||||
auto resultMutex = openResultMutex_;
|
||||
auto resultSlot = openResult_;
|
||||
openThread_ = std::thread([bridge, servers, running, resultMutex, resultSlot]() {
|
||||
OpenOutcome outcome;
|
||||
outcome.error = "could not reach any lite server";
|
||||
for (const auto& url : servers) {
|
||||
if (!bridge) break;
|
||||
// initialize_existing loads the wallet file but contacts the server to start the
|
||||
// light client; ok && non-empty value == ready (mirrors the lifecycle's success test).
|
||||
const auto call = bridge->initializeExisting(/*dangerous=*/false, url);
|
||||
if (call.ok && !call.value.empty()) {
|
||||
outcome.ok = true;
|
||||
outcome.serverUrl = url;
|
||||
break;
|
||||
}
|
||||
if (!call.error.empty()) outcome.error = call.error;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(*resultMutex);
|
||||
*resultSlot = outcome;
|
||||
}
|
||||
running->store(false);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
void LiteWalletController::pumpAsyncOpen()
|
||||
{
|
||||
OpenOutcome outcome;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(*openResultMutex_);
|
||||
if (!openResult_->has_value()) return; // nothing finished since last pump
|
||||
outcome = std::move(openResult_->value());
|
||||
openResult_->reset();
|
||||
}
|
||||
if (openThread_.joinable()) openThread_.join(); // producer set its result, then exits
|
||||
|
||||
if (outcome.ok) {
|
||||
walletOpen_ = true;
|
||||
lastOpenError_.clear();
|
||||
status_ = WalletBackendStatus{WalletBackendState::Ready, "wallet open", {}, {}, 0.0};
|
||||
if (persist_) persist_();
|
||||
startSync(); // begin background sync on the backend
|
||||
startWorker(); // begin periodic refresh -> WalletState
|
||||
} else {
|
||||
lastOpenError_ = outcome.error;
|
||||
status_ = WalletBackendStatus{WalletBackendState::Error, outcome.error, {}, {}, 0.0};
|
||||
}
|
||||
}
|
||||
|
||||
void LiteWalletController::startSync()
|
||||
{
|
||||
if (syncLaunched_.exchange(true)) return;
|
||||
|
||||
Reference in New Issue
Block a user