diff --git a/src/app.cpp b/src/app.cpp index dc5616e..ff1e58d 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -490,11 +490,13 @@ void App::update() // 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. + // interval so a transient outage self-heals once a server comes back — and much sooner + // (a few seconds) when the failure was a server merely warming up (-28), which clears fast. const double nowSecs = ImGui::GetTime(); + const double retryInterval = lite_wallet_->lastOpenWasWarmup() ? 4.0 : 20.0; if (!lite_wallet_->walletOpen() && !lite_wallet_->openInProgress() && lite_wallet_->walletExists() && - (!lite_autoopen_done_ || nowSecs - lite_open_last_attempt_ > 20.0)) { + (!lite_autoopen_done_ || nowSecs - lite_open_last_attempt_ > retryInterval)) { lite_autoopen_done_ = true; lite_open_last_attempt_ = nowSecs; lite_wallet_->beginOpenExisting(); diff --git a/src/wallet/lite_wallet_controller.cpp b/src/wallet/lite_wallet_controller.cpp index a2f6aa5..5502168 100644 --- a/src/wallet/lite_wallet_controller.cpp +++ b/src/wallet/lite_wallet_controller.cpp @@ -22,6 +22,16 @@ namespace wallet { namespace { constexpr double kZatoshisPerCoin = 100000000.0; // DRGX has 1e8 zatoshis per coin +// A lightwalletd open error that means "server is up but still warming up" (JSON-RPC -28 / +// "Activating best chain", "Loading"/"Verifying"/"Rescanning" phases) rather than a dead server. +// Such a server will be ready shortly, so the caller should retry it soon. +bool liteOpenErrorIsWarmup(const std::string& error) +{ + const auto has = [&error](const char* s) { return error.find(s) != std::string::npos; }; + return has("-28") || has("Activating best chain") || has("warming up") || + has("Loading block") || has("Verifying blocks") || has("Rescanning"); +} + // Extract a backend {"error":..} message (string or arbitrary JSON) into a plain string. std::string extractJsonError(const nlohmann::json& value) { @@ -361,7 +371,10 @@ bool LiteWalletController::beginOpenExisting() } const std::string why = call.error.empty() ? "unreachable" : call.error; liteLog(" " + url + ": " + why); - if (!call.error.empty()) outcome.error = call.error; + if (!call.error.empty()) { + outcome.error = call.error; + if (liteOpenErrorIsWarmup(call.error)) outcome.warming = true; // healthy, just starting + } } { std::lock_guard lk(*resultMutex); @@ -386,6 +399,7 @@ void LiteWalletController::pumpAsyncOpen() if (outcome.ok) { walletOpen_ = true; lastOpenError_.clear(); + lastOpenWarming_ = false; status_ = WalletBackendStatus{WalletBackendState::Ready, "wallet open", {}, {}, 0.0}; liteLog("Wallet opened via " + outcome.serverUrl); if (persist_) persist_(); @@ -393,8 +407,10 @@ void LiteWalletController::pumpAsyncOpen() startWorker(); // begin periodic refresh -> WalletState } else { lastOpenError_ = outcome.error; + lastOpenWarming_ = outcome.warming; // a healthy server was warming up -> retry sooner status_ = WalletBackendStatus{WalletBackendState::Error, outcome.error, {}, {}, 0.0}; - liteLog("Open failed: " + outcome.error); + liteLog("Open failed: " + outcome.error + + (outcome.warming ? " (a server is warming up — will retry shortly)" : "")); } } diff --git a/src/wallet/lite_wallet_controller.h b/src/wallet/lite_wallet_controller.h index 2883239..7ed8c9a 100644 --- a/src/wallet/lite_wallet_controller.h +++ b/src/wallet/lite_wallet_controller.h @@ -173,6 +173,10 @@ public: void pumpAsyncOpen(); bool openInProgress() const { return openRunning_ && openRunning_->load(); } 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 + // should retry sooner rather than waiting out the normal interval. + bool lastOpenWasWarmup() const { return lastOpenWarming_; } bool syncStarted() const { return syncStarted_; } bool syncComplete() const { return syncDone_ && syncDone_->load(); } @@ -290,13 +294,14 @@ private: // 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 failoverServerUrls() const; - struct OpenOutcome { bool ok = false; std::string serverUrl; std::string error; }; + struct OpenOutcome { bool ok = false; bool warming = false; std::string serverUrl; std::string error; }; std::thread openThread_; std::shared_ptr> openRunning_ = std::make_shared>(false); std::shared_ptr openResultMutex_ = std::make_shared(); std::shared_ptr> openResult_ = std::make_shared>(); std::string lastOpenError_; // main-thread only + bool lastOpenWarming_ = false; // last failed open hit a warming-up (-28) server // Joinable background refresh worker (fast iterations: syncstatus, plus data once synced). std::thread worker_; diff --git a/tests/fake_lite_backend.h b/tests/fake_lite_backend.h index 4b1e497..f496ae9 100644 --- a/tests/fake_lite_backend.h +++ b/tests/fake_lite_backend.h @@ -40,6 +40,8 @@ inline std::atomic g_liteFakeEncrypted{false}; // wallet-encryption state inline std::atomic 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; +// initialize_existing returns a warming-up (-28) error for URLs containing this substring. +inline std::string g_liteFakeWarmupServerSubstr; inline void resetLiteFakeCounters() { @@ -47,6 +49,7 @@ inline void resetLiteFakeCounters() g_liteFakeFreed = 0; g_liteFakeShutdownCalled = false; g_liteFakeDeadServerSubstr.clear(); + g_liteFakeWarmupServerSubstr.clear(); } inline char* liteFakeDup(const char* s) @@ -74,9 +77,18 @@ inline char* liteFakeInitFromPhrase(bool, const char*, const char*, // 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 + if (server) { + const std::string s(server); + if (!g_liteFakeWarmupServerSubstr.empty() && + s.find(g_liteFakeWarmupServerSubstr) != std::string::npos) { + // Server is up but warming up — mirrors the real -28 / "Activating best chain". + return liteFakeDup("Error: grpc-message: \"error requesting block: -28: " + "Activating best chain...\""); + } + if (!g_liteFakeDeadServerSubstr.empty() && + s.find(g_liteFakeDeadServerSubstr) != std::string::npos) { + return liteFakeDup("Error: could not connect to server"); // bridge maps to ok=false + } } return liteFakeDup("OK"); } diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index bcd609b..b708e25 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -3599,8 +3599,26 @@ void testLiteWalletControllerOpenFailover() EXPECT_TRUE(!controller.lastOpenError().empty()); } + // A warming-up (-28) server is flagged so the caller can retry sooner. Preferred server is + // warming, the fallback is dead -> open fails but lastOpenWasWarmup() is set. + { + dragonx::test::resetLiteFakeCounters(); + dragonx::test::g_liteFakeWalletExists = true; + dragonx::test::g_liteFakeWarmupServerSubstr = "dead.example"; // preferred (sticky) is warming + dragonx::test::g_liteFakeDeadServerSubstr = "good.example"; // fallback unreachable + 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.lastOpenWasWarmup()); + } + dragonx::test::g_liteFakeWalletExists = false; dragonx::test::g_liteFakeDeadServerSubstr.clear(); + dragonx::test::g_liteFakeWarmupServerSubstr.clear(); } // M2: a parsed lite refresh bundle maps through to the app's WalletState (the last hop