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:
44
src/app.cpp
44
src/app.cpp
@@ -480,25 +480,21 @@ void App::update()
|
|||||||
|
|
||||||
// Apply any lite-wallet refresh the controller's background worker produced (main thread).
|
// Apply any lite-wallet refresh the controller's background worker produced (main thread).
|
||||||
if (lite_wallet_) {
|
if (lite_wallet_) {
|
||||||
// Auto-open an existing wallet once, after the window is up. initialize_existing needs no
|
// Finalize any completed async open on the main thread (flips walletOpen / surfaces failure).
|
||||||
// passphrase (it just loads the file); a previously-synced wallet then resumes from its
|
lite_wallet_->pumpAsyncOpen();
|
||||||
// saved height (fast) rather than re-scanning from the checkpoint.
|
|
||||||
if (!lite_autoopen_done_) {
|
// Auto-open an existing wallet asynchronously with server failover (initialize_existing
|
||||||
|
// needs no passphrase — it just loads the file and contacts the server). Running it off
|
||||||
|
// the UI thread means an unreachable server never freezes startup, and trying the other
|
||||||
|
// default servers means one dead server no longer strands the wallet. Retried on an
|
||||||
|
// interval so a transient outage self-heals once a server comes back.
|
||||||
|
const double nowSecs = ImGui::GetTime();
|
||||||
|
if (!lite_wallet_->walletOpen() && !lite_wallet_->openInProgress() &&
|
||||||
|
lite_wallet_->walletExists() &&
|
||||||
|
(!lite_autoopen_done_ || nowSecs - lite_open_last_attempt_ > 20.0)) {
|
||||||
lite_autoopen_done_ = true;
|
lite_autoopen_done_ = true;
|
||||||
if (!lite_wallet_->walletOpen() && lite_wallet_->walletExists()) {
|
lite_open_last_attempt_ = nowSecs;
|
||||||
const auto openResult = lite_wallet_->openWallet(wallet::LiteWalletOpenRequest{});
|
lite_wallet_->beginOpenExisting();
|
||||||
// Surface why an existing wallet failed to open (e.g. the lightwalletd server
|
|
||||||
// is unreachable) — otherwise the UI just shows a silent "disconnected" spinner.
|
|
||||||
if (!openResult.ok && !openResult.walletReady) {
|
|
||||||
lite_open_error_ = !openResult.error.empty() ? openResult.error
|
|
||||||
: (!openResult.status.message.empty()
|
|
||||||
? openResult.status.message
|
|
||||||
: "Could not open the wallet");
|
|
||||||
DEBUG_LOGF("[Lite] auto-open failed: %s\n", lite_open_error_.c_str());
|
|
||||||
ui::Notifications::instance().error(
|
|
||||||
std::string("Wallet open failed: ") + lite_open_error_, 8.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lite has no RPC daemon (tryConnect() is a no-op in lite builds), so derive the app's
|
// Lite has no RPC daemon (tryConnect() is a no-op in lite builds), so derive the app's
|
||||||
@@ -506,7 +502,17 @@ void App::update()
|
|||||||
// A wallet is open only after a successful backend init against the lite server, so this
|
// A wallet is open only after a successful backend init against the lite server, so this
|
||||||
// is a non-blocking proxy for "lite backend operational".
|
// is a non-blocking proxy for "lite backend operational".
|
||||||
state_.connected = lite_wallet_->walletOpen();
|
state_.connected = lite_wallet_->walletOpen();
|
||||||
if (state_.connected) lite_open_error_.clear(); // opened successfully — clear any prior error
|
if (state_.connected) {
|
||||||
|
lite_open_error_.clear(); // opened successfully — clear any prior error
|
||||||
|
} else {
|
||||||
|
// Surface a failed open (e.g. server unreachable) once per distinct reason, so the
|
||||||
|
// disconnected state isn't silent.
|
||||||
|
const std::string& err = lite_wallet_->lastOpenError();
|
||||||
|
if (!err.empty() && err != lite_open_error_) {
|
||||||
|
lite_open_error_ = err;
|
||||||
|
ui::Notifications::instance().error(std::string("Wallet open failed: ") + err, 8.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Suppress the status bar's full-node connection-detail line in lite ("" and "Connected"
|
// Suppress the status bar's full-node connection-detail line in lite ("" and "Connected"
|
||||||
// are both hidden); the connected/no-wallet indicator + sync status convey lite state.
|
// are both hidden); the connected/no-wallet indicator + sync status convey lite state.
|
||||||
connection_status_ = state_.connected ? "Connected" : "";
|
connection_status_ = state_.connected ? "Connected" : "";
|
||||||
|
|||||||
@@ -444,6 +444,7 @@ private:
|
|||||||
// One-shot guard: auto-open an existing lite wallet on the first update() tick (kept off
|
// One-shot guard: auto-open an existing lite wallet on the first update() tick (kept off
|
||||||
// init() so a slow initialize_existing network call doesn't freeze startup before the window).
|
// init() so a slow initialize_existing network call doesn't freeze startup before the window).
|
||||||
bool lite_autoopen_done_ = false;
|
bool lite_autoopen_done_ = false;
|
||||||
|
double lite_open_last_attempt_ = 0.0; // ImGui time of the last async open attempt (retry timer)
|
||||||
// Reason an existing lite wallet failed to auto-open (e.g. server unreachable). Surfaced in
|
// 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.
|
// the UI so a stuck "disconnected" state isn't silent; cleared once a wallet opens.
|
||||||
std::string lite_open_error_;
|
std::string lite_open_error_;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "../data/wallet_state.h"
|
#include "../data/wallet_state.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -229,6 +230,7 @@ LiteWalletController::LiteWalletController(WalletCapabilities capabilities,
|
|||||||
LiteWalletControllerOptions options)
|
LiteWalletControllerOptions options)
|
||||||
: bridge_(std::make_shared<LiteClientBridge>(std::move(bridge))),
|
: bridge_(std::make_shared<LiteClientBridge>(std::move(bridge))),
|
||||||
chainName_(connectionSettings.chainName),
|
chainName_(connectionSettings.chainName),
|
||||||
|
connectionSettings_(connectionSettings),
|
||||||
lifecycle_(capabilities, connectionSettings, bridge_.get(),
|
lifecycle_(capabilities, connectionSettings, bridge_.get(),
|
||||||
LiteWalletLifecycleOptions{options.allowBridgeCalls,
|
LiteWalletLifecycleOptions{options.allowBridgeCalls,
|
||||||
options.rolloutBlocked, options.rolloutMessage}),
|
options.rolloutBlocked, options.rolloutMessage}),
|
||||||
@@ -257,6 +259,9 @@ LiteWalletController::~LiteWalletController()
|
|||||||
// Likewise the broadcast thread (send/shield proving): it captures shared refs (bridge +
|
// Likewise the broadcast thread (send/shield proving): it captures shared refs (bridge +
|
||||||
// running flag + result slot), never `this`, so detaching is safe.
|
// running flag + result slot), never `this`, so detaching is safe.
|
||||||
if (broadcastThread_.joinable()) broadcastThread_.detach();
|
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(
|
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()
|
void LiteWalletController::startSync()
|
||||||
{
|
{
|
||||||
if (syncLaunched_.exchange(true)) return;
|
if (syncLaunched_.exchange(true)) return;
|
||||||
|
|||||||
@@ -161,6 +161,19 @@ public:
|
|||||||
LiteWalletLifecycleResult openWallet(LiteWalletOpenRequest request);
|
LiteWalletLifecycleResult openWallet(LiteWalletOpenRequest request);
|
||||||
LiteWalletLifecycleResult restoreWallet(LiteWalletRestoreRequest 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 syncStarted() const { return syncStarted_; }
|
||||||
bool syncComplete() const { return syncDone_ && syncDone_->load(); }
|
bool syncComplete() const { return syncDone_ && syncDone_->load(); }
|
||||||
|
|
||||||
@@ -249,6 +262,7 @@ private:
|
|||||||
// mutex would instead serialize sync against syncstatus polling and defeat the design.
|
// mutex would instead serialize sync against syncstatus polling and defeat the design.
|
||||||
std::shared_ptr<LiteClientBridge> bridge_;
|
std::shared_ptr<LiteClientBridge> bridge_;
|
||||||
std::string chainName_; // backend chain id (for walletExists); from connection settings
|
std::string chainName_; // backend chain id (for walletExists); from connection settings
|
||||||
|
LiteConnectionSettings connectionSettings_; // kept for the failover candidate-server list
|
||||||
LiteWalletLifecycleService lifecycle_;
|
LiteWalletLifecycleService lifecycle_;
|
||||||
LiteWalletGateway gateway_;
|
LiteWalletGateway gateway_;
|
||||||
LiteSyncService sync_;
|
LiteSyncService sync_;
|
||||||
@@ -272,6 +286,18 @@ private:
|
|||||||
std::shared_ptr<std::optional<LiteBroadcastResult>> broadcastResult_ =
|
std::shared_ptr<std::optional<LiteBroadcastResult>> broadcastResult_ =
|
||||||
std::make_shared<std::optional<LiteBroadcastResult>>();
|
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).
|
// Joinable background refresh worker (fast iterations: syncstatus, plus data once synced).
|
||||||
std::thread worker_;
|
std::thread worker_;
|
||||||
std::atomic<bool> running_{false};
|
std::atomic<bool> running_{false};
|
||||||
|
|||||||
@@ -38,12 +38,15 @@ inline std::atomic<bool> g_liteFakeSendFails{false}; // when true, "send"/"shie
|
|||||||
inline std::atomic<long> g_liteFakeSaveCount{0}; // counts "save" commands (persistence checks)
|
inline std::atomic<long> g_liteFakeSaveCount{0}; // counts "save" commands (persistence checks)
|
||||||
inline std::atomic<bool> g_liteFakeEncrypted{false}; // wallet-encryption state machine (encrypt/...)
|
inline std::atomic<bool> g_liteFakeEncrypted{false}; // wallet-encryption state machine (encrypt/...)
|
||||||
inline std::atomic<bool> g_liteFakeLocked{false}; // spending-keys-locked state
|
inline std::atomic<bool> g_liteFakeLocked{false}; // spending-keys-locked state
|
||||||
|
// initialize_existing fails for any server URL containing this substring (open-failover tests).
|
||||||
|
inline std::string g_liteFakeDeadServerSubstr;
|
||||||
|
|
||||||
inline void resetLiteFakeCounters()
|
inline void resetLiteFakeCounters()
|
||||||
{
|
{
|
||||||
g_liteFakeAlloc = 0;
|
g_liteFakeAlloc = 0;
|
||||||
g_liteFakeFreed = 0;
|
g_liteFakeFreed = 0;
|
||||||
g_liteFakeShutdownCalled = false;
|
g_liteFakeShutdownCalled = false;
|
||||||
|
g_liteFakeDeadServerSubstr.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
inline char* liteFakeDup(const char* s)
|
inline char* liteFakeDup(const char* s)
|
||||||
@@ -67,7 +70,16 @@ inline char* liteFakeInitFromPhrase(bool, const char*, const char*,
|
|||||||
{
|
{
|
||||||
return liteFakeDup("{\"seed\":\"fake seed phrase words\",\"birthday\":0}");
|
return liteFakeDup("{\"seed\":\"fake seed phrase words\",\"birthday\":0}");
|
||||||
}
|
}
|
||||||
inline char* liteFakeInitExisting(bool, const char*) { return liteFakeDup("OK"); }
|
// initialize_existing fails (server unreachable) for any server URL containing
|
||||||
|
// g_liteFakeDeadServerSubstr, so tests can exercise the controller's open-with-failover.
|
||||||
|
inline char* liteFakeInitExisting(bool, const char* server)
|
||||||
|
{
|
||||||
|
if (!g_liteFakeDeadServerSubstr.empty() && server &&
|
||||||
|
std::string(server).find(g_liteFakeDeadServerSubstr) != std::string::npos) {
|
||||||
|
return liteFakeDup("Error: could not connect to server"); // bridge maps to ok=false
|
||||||
|
}
|
||||||
|
return liteFakeDup("OK");
|
||||||
|
}
|
||||||
inline char* liteFakeExecute(const char* command, const char* args)
|
inline char* liteFakeExecute(const char* command, const char* args)
|
||||||
{
|
{
|
||||||
// new-address generation returns a JSON array with the new address (type from args: zs/R).
|
// new-address generation returns a JSON array with the new address (type from args: zs/R).
|
||||||
|
|||||||
@@ -3538,6 +3538,58 @@ void testLiteChainNameMigration()
|
|||||||
fs::remove_all(dir, ec);
|
fs::remove_all(dir, ec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Async open with server failover: when the preferred lite server is unreachable, the controller
|
||||||
|
// transparently opens against the next usable one; only an all-servers-down case fails (and then
|
||||||
|
// surfaces a reason). Open runs off the UI thread, finalized by pumpAsyncOpen() on the main thread.
|
||||||
|
void testLiteWalletControllerOpenFailover()
|
||||||
|
{
|
||||||
|
using namespace dragonx::wallet;
|
||||||
|
const auto liteCaps = makeWalletCapabilities(WalletBuildKind::Lite, false, true);
|
||||||
|
|
||||||
|
LiteConnectionSettings conn;
|
||||||
|
conn.chainName = "main";
|
||||||
|
conn.servers = {
|
||||||
|
LiteServerEndpoint{"https://dead.example", "Dead", true},
|
||||||
|
LiteServerEndpoint{"https://good.example", "Good", true},
|
||||||
|
};
|
||||||
|
conn.selectionMode = LiteServerSelectionMode::Sticky;
|
||||||
|
conn.stickyServerUrl = "https://dead.example"; // the PREFERRED server is the dead one
|
||||||
|
|
||||||
|
// Preferred server dead, fallback good -> the wallet opens via the fallback.
|
||||||
|
{
|
||||||
|
dragonx::test::resetLiteFakeCounters();
|
||||||
|
dragonx::test::g_liteFakeWalletExists = true;
|
||||||
|
dragonx::test::g_liteFakeDeadServerSubstr = "dead.example";
|
||||||
|
LiteWalletController controller(liteCaps, conn,
|
||||||
|
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||||
|
EXPECT_TRUE(controller.beginOpenExisting());
|
||||||
|
for (int i = 0; i < 400 && controller.openInProgress(); ++i)
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||||
|
EXPECT_FALSE(controller.openInProgress());
|
||||||
|
controller.pumpAsyncOpen();
|
||||||
|
EXPECT_TRUE(controller.walletOpen());
|
||||||
|
EXPECT_TRUE(controller.lastOpenError().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// All servers dead -> open fails, wallet stays closed, reason surfaced.
|
||||||
|
{
|
||||||
|
dragonx::test::resetLiteFakeCounters();
|
||||||
|
dragonx::test::g_liteFakeWalletExists = true;
|
||||||
|
dragonx::test::g_liteFakeDeadServerSubstr = "example"; // both servers fail
|
||||||
|
LiteWalletController controller(liteCaps, conn,
|
||||||
|
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||||
|
EXPECT_TRUE(controller.beginOpenExisting());
|
||||||
|
for (int i = 0; i < 400 && controller.openInProgress(); ++i)
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||||
|
controller.pumpAsyncOpen();
|
||||||
|
EXPECT_FALSE(controller.walletOpen());
|
||||||
|
EXPECT_TRUE(!controller.lastOpenError().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
dragonx::test::g_liteFakeWalletExists = false;
|
||||||
|
dragonx::test::g_liteFakeDeadServerSubstr.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// M2: a parsed lite refresh bundle maps through to the app's WalletState (the last hop
|
// M2: a parsed lite refresh bundle maps through to the app's WalletState (the last hop
|
||||||
// the Balance/Receive/Transactions tabs read), with zatoshi->DRGX conversion, z/t address
|
// the Balance/Receive/Transactions tabs read), with zatoshi->DRGX conversion, z/t address
|
||||||
// split, transaction typing, confirmations, and sync progress.
|
// split, transaction typing, confirmations, and sync progress.
|
||||||
@@ -4419,6 +4471,7 @@ int main()
|
|||||||
testLiteClientBridgeUsesRuntimeOwnedStringCleanup();
|
testLiteClientBridgeUsesRuntimeOwnedStringCleanup();
|
||||||
testLiteBackendInjectableFakeBridge();
|
testLiteBackendInjectableFakeBridge();
|
||||||
testLiteWalletControllerLifecycle();
|
testLiteWalletControllerLifecycle();
|
||||||
|
testLiteWalletControllerOpenFailover();
|
||||||
testLiteWalletControllerM4();
|
testLiteWalletControllerM4();
|
||||||
testLiteWalletControllerM5Persistence();
|
testLiteWalletControllerM5Persistence();
|
||||||
testLiteWalletControllerEncryption();
|
testLiteWalletControllerEncryption();
|
||||||
|
|||||||
Reference in New Issue
Block a user