feat: non-blocking warmup — connect during daemon initialization

Instead of blocking the entire UI with "Activating best chain..." until
the daemon finishes warmup, treat warmup responses as a successful
connection. The wallet now:

- Sets connected=true + warming_up=true when daemon returns RPC -28
- Shows warmup status with block progress in the loading overlay
- Polls getinfo every few seconds to detect warmup completion
- Allows Console, Peers, Settings tabs during warmup
- Shows orange status indicator with warmup message in status bar
- Skips balance/tx/address refresh until warmup completes
- Triggers full data refresh once daemon is ready

Also: fix curl handle/header leak on reconnect, fill in empty
externalDetected error branch, bump version to v1.2.0 in build scripts.
This commit is contained in:
dan_s
2026-04-12 14:32:57 -05:00
parent 28b9e0dffb
commit 915c1b4d23
9 changed files with 181 additions and 29 deletions

View File

@@ -73,6 +73,16 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
impl_->url = "http://" + host + ":" + port + "/";
VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());
// Clean up previous curl handle/headers to avoid leaks on retries
if (impl_->headers) {
curl_slist_free_all(impl_->headers);
impl_->headers = nullptr;
}
if (impl_->curl) {
curl_easy_cleanup(impl_->curl);
impl_->curl = nullptr;
}
// Initialize curl handle
impl_->curl = curl_easy_init();
if (!impl_->curl) {
@@ -81,7 +91,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
}
// Set up headers - daemon expects text/plain, not application/json
impl_->headers = curl_slist_append(impl_->headers, "Content-Type: text/plain");
impl_->headers = curl_slist_append(nullptr, "Content-Type: text/plain");
std::string auth_header = "Authorization: Basic " + auth_;
impl_->headers = curl_slist_append(impl_->headers, auth_header.c_str());
@@ -97,6 +107,8 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
json result = call("getinfo");
if (result.contains("version")) {
connected_ = true;
warming_up_ = false;
warmup_status_.clear();
last_connect_error_.clear();
DEBUG_LOGF("Connected to dragonxd v%d\n", result["version"].get<int>());
return true;
@@ -104,7 +116,9 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
} catch (const std::exception& e) {
last_connect_error_ = e.what();
// Daemon warmup messages (Loading block index, Verifying blocks, etc.)
// are normal startup progress — don't label them "Connection failed".
// are normal startup progress — the daemon is reachable and auth works,
// it just hasn't finished initializing yet. Mark as connected+warmup
// so the wallet can show the UI instead of a blocking overlay.
std::string msg = e.what();
bool isWarmup = (msg.find("Loading") != std::string::npos ||
msg.find("Verifying") != std::string::npos ||
@@ -113,13 +127,19 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
msg.find("Rescanning") != std::string::npos ||
msg.find("Pruning") != std::string::npos);
if (isWarmup) {
DEBUG_LOGF("Daemon starting: %s\n", msg.c_str());
connected_ = true;
warming_up_ = true;
warmup_status_ = msg;
DEBUG_LOGF("Daemon warming up: %s\n", msg.c_str());
return true;
} else {
DEBUG_LOGF("Connection failed: %s\n", msg.c_str());
}
}
connected_ = false;
warming_up_ = false;
warmup_status_.clear();
return false;
}
@@ -127,6 +147,8 @@ void RPCClient::disconnect()
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
connected_ = false;
warming_up_ = false;
warmup_status_.clear();
if (impl_->curl) {
curl_easy_cleanup(impl_->curl);
impl_->curl = nullptr;

View File

@@ -53,6 +53,19 @@ public:
*/
bool isConnected() const { return connected_; }
/**
* @brief True if the last connect() succeeded but daemon returned a warmup error.
* The curl handle is valid and auth succeeded — RPC calls will throw warmup errors
* until the daemon finishes initializing.
*/
bool isWarmingUp() const { return warming_up_; }
/**
* @brief The warmup status message (e.g. "Activating best chain...").
* Empty when not in warmup.
*/
const std::string& getWarmupStatus() const { return warmup_status_; }
/**
* @brief Get the error message from the last failed connect() attempt.
*/
@@ -182,6 +195,8 @@ private:
std::string port_;
std::string auth_; // Base64 encoded "user:password"
bool connected_ = false;
bool warming_up_ = false;
std::string warmup_status_;
std::string last_connect_error_;
mutable std::recursive_mutex curl_mutex_; // serializes all curl handle access