diff --git a/build.sh b/build.sh index 498aa27..cd4cc0a 100755 --- a/build.sh +++ b/build.sh @@ -20,7 +20,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -VERSION="1.1.1" +VERSION="1.2.0" # ── Colours ────────────────────────────────────────────────────────────────── RED='\033[0;31m' diff --git a/scripts/create-appimage.sh b/scripts/create-appimage.sh index ed3a798..794a482 100755 --- a/scripts/create-appimage.sh +++ b/scripts/create-appimage.sh @@ -8,7 +8,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BUILD_DIR="${SCRIPT_DIR}/build/linux" APPDIR="${BUILD_DIR}/AppDir" -VERSION="1.1.1" +VERSION="1.2.0" # Colors GREEN='\033[0;32m' diff --git a/scripts/legacy/build-release.sh b/scripts/legacy/build-release.sh index c488c26..e6e4570 100755 --- a/scripts/legacy/build-release.sh +++ b/scripts/legacy/build-release.sh @@ -7,7 +7,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BUILD_DIR="${SCRIPT_DIR}/build/linux" -VERSION="1.1.1" +VERSION="1.2.0" # Colors for output RED='\033[0;31m' diff --git a/src/app.cpp b/src/app.cpp index 4b1ce09..31dd944 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -610,13 +610,16 @@ void App::update() core_timer_ = 0.0f; refreshCoreData(); } - if (transaction_timer_ >= active_tx_interval_) { - transaction_timer_ = 0.0f; - refreshTransactionData(); - } - if (address_timer_ >= active_addr_interval_) { - address_timer_ = 0.0f; - refreshAddressData(); + // Skip balance/tx/address refresh during warmup — RPC calls fail with -28 + if (!state_.warming_up) { + if (transaction_timer_ >= active_tx_interval_) { + transaction_timer_ = 0.0f; + refreshTransactionData(); + } + if (address_timer_ >= active_addr_interval_) { + address_timer_ = 0.0f; + refreshAddressData(); + } } if (peer_timer_ >= active_peer_interval_) { peer_timer_ = 0.0f; @@ -1087,7 +1090,7 @@ void App::render() bool pageNeedsDaemon = (current_page_ != ui::NavPage::Console && current_page_ != ui::NavPage::Peers && current_page_ != ui::NavPage::Settings); - bool daemonReady = state_.connected; // don't gate on sync state + bool daemonReady = state_.connected && !state_.warming_up; // Don't show lock screen while pool mining — xmrig runs independently // of the wallet and locking would block the mining UI needlessly. @@ -1095,6 +1098,11 @@ void App::render() if (state_.isLocked() && !poolMiningActive) { // Lock screen — covers tab content just like the loading overlay renderLockScreen(); + } else if (state_.warming_up) { + // Daemon is reachable but still initializing — show warmup overlay + // without blocking on encryption state (getwalletinfo fails during warmup) + lock_screen_was_visible_ = false; + renderLoadingOverlay(contentH); } else if (pageNeedsDaemon && (!daemonReady || (state_.connected && !state_.encryption_state_known))) { // Track how long we've been waiting for encryption state if (state_.connected && !state_.encryption_state_known) { @@ -1421,7 +1429,16 @@ void App::renderStatusBar() // Connection status float dotOpacity = S.drawElement("components.status-bar", "connection-dot").opacity; if (dotOpacity < 0.0f) dotOpacity = 1.0f; - if (state_.connected) { + if (state_.warming_up) { + ImGui::PushFont(ui::material::Type().iconSmall()); + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, dotOpacity), ICON_MD_CIRCLE); + ImGui::PopFont(); + ImGui::SameLine(0, sbIconTextGap); + // Show truncated warmup status (e.g. "Activating best chain... (Block 12345)") + const char* warmupText = state_.warmup_status.empty() + ? "Warming up..." : state_.warmup_status.c_str(); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", warmupText); + } else if (state_.connected) { ImGui::PushFont(ui::material::Type().iconSmall()); ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, dotOpacity), ICON_MD_CIRCLE); ImGui::PopFont(); diff --git a/src/app_network.cpp b/src/app_network.cpp index a52c6b1..6227c0e 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -150,12 +150,29 @@ void App::tryConnect() worker_->post([this, config, daemonStarting, externalDetected, attempt]() -> rpc::RPCWorker::MainCb { bool connected = rpc_->connect(config.host, config.port, config.rpcuser, config.rpcpassword); std::string connectErr = rpc_->getLastConnectError(); + bool warmingUp = rpc_->isWarmingUp(); + std::string warmupStatus = rpc_->getWarmupStatus(); - return [this, config, connected, daemonStarting, externalDetected, attempt, connectErr]() { + return [this, config, connected, warmingUp, warmupStatus, daemonStarting, externalDetected, attempt, connectErr]() { if (connected) { - VERBOSE_LOGF("[connect #%d] Connected successfully\n", attempt); + VERBOSE_LOGF("[connect #%d] Connected successfully%s\n", attempt, + warmingUp ? " (daemon warming up)" : ""); saved_config_ = config; // save for fast-lane connection onConnected(); + if (warmingUp) { + // Daemon is reachable and auth works, but RPC calls will + // fail until warmup completes. Set the warmup state so + // the UI shows status instead of a blocking overlay. + state_.warming_up = true; + state_.warmup_status = warmupStatus; + // Append current block height from daemon output + if (embedded_daemon_) { + int h = embedded_daemon_->getLastBlockHeight(); + if (h > 0) + state_.warmup_status += " (Block " + std::to_string(h) + ")"; + } + connection_status_ = state_.warmup_status; + } } else { // HTTP 401 = authentication failure. The daemon is running // but our rpcuser/rpcpassword don't match. Don't retry @@ -203,18 +220,6 @@ void App::tryConnect() "RPC authentication failed (HTTP 401). " "The rpcuser/rpcpassword in DRAGONX.conf don't match the running daemon. " "Restart the daemon or correct the credentials."); - } else if (connectErr.find("Loading") != std::string::npos || - connectErr.find("Verifying") != std::string::npos || - connectErr.find("Activating") != std::string::npos || - connectErr.find("Rewinding") != std::string::npos || - connectErr.find("Rescanning") != std::string::npos || - connectErr.find("Pruning") != std::string::npos) { - // Daemon is reachable but still in warmup (Loading block index, etc.) - // Check this BEFORE daemonStarting so the actual warmup status is shown. - state_.connected = false; - connection_status_ = connectErr; - VERBOSE_LOGF("[connect #%d] Daemon warmup: %s\n", attempt, connectErr.c_str()); - core_timer_ = CORE_INTERVAL_DEFAULT - 1.0f; } else if (daemonStarting) { state_.connected = false; // Show the actual RPC error alongside the waiting message so @@ -228,6 +233,15 @@ void App::tryConnect() attempt, connectErr.c_str()); core_timer_ = CORE_INTERVAL_DEFAULT - 1.0f; } else if (externalDetected) { + state_.connected = false; + if (!connectErr.empty()) { + connection_status_ = "Connecting to daemon — " + connectErr; + } else { + connection_status_ = "Connecting to external daemon..."; + } + VERBOSE_LOGF("[connect #%d] External daemon detected but RPC failed (%s), will retry...\n", + attempt, connectErr.c_str()); + core_timer_ = CORE_INTERVAL_DEFAULT - 1.0f; } else { onDisconnected("Connection failed"); VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt); @@ -376,6 +390,8 @@ void App::onConnected() void App::onDisconnected(const std::string& reason) { state_.connected = false; + state_.warming_up = false; + state_.warmup_status.clear(); state_.clear(); connection_status_ = reason; @@ -490,6 +506,13 @@ void App::refreshData() { if (!state_.connected || !rpc_ || !worker_) return; + // During warmup, only poll for warmup completion via refreshCoreData. + // Other RPC calls (balance, addresses, transactions) will fail with -28. + if (state_.warming_up) { + refreshCoreData(); + return; + } + // Dispatch each category independently — results trickle into the UI // as each completes, rather than waiting for the slowest phase. refreshCoreData(); @@ -518,6 +541,57 @@ void App::refreshCoreData() { if (!state_.connected) return; + // During warmup, poll getinfo to detect when warmup ends. + // Most RPC calls (balance, blockchain info) will fail with -28 during warmup. + if (state_.warming_up) { + if (core_refresh_in_progress_.exchange(true)) return; + worker_->post([this]() -> rpc::RPCWorker::MainCb { + json info; + bool ok = false; + std::string errMsg; + try { + info = rpc_->call("getinfo"); + ok = true; + } catch (const std::exception& e) { + errMsg = e.what(); + } + return [this, info, ok, errMsg]() { + if (ok) { + // Warmup finished — daemon is fully ready + state_.warming_up = false; + state_.warmup_status.clear(); + connection_status_ = "Connected"; + VERBOSE_LOGF("[warmup] Daemon ready, warmup complete\n"); + // Parse initial info + try { + if (info.contains("version")) + state_.daemon_version = info["version"].get(); + if (info.contains("blocks")) + state_.sync.blocks = info["blocks"].get(); + if (info.contains("longestchain")) { + int lc = info["longestchain"].get(); + if (lc > 0) state_.longestchain = lc; + } + } catch (...) {} + // Trigger full data refresh now that daemon is ready + refreshData(); + } else { + // Still warming up — update status + state_.warmup_status = errMsg; + if (embedded_daemon_) { + int h = embedded_daemon_->getLastBlockHeight(); + if (h > 0) + state_.warmup_status += " (Block " + std::to_string(h) + ")"; + } + connection_status_ = state_.warmup_status; + VERBOSE_LOGF("[warmup] Still warming up: %s\n", errMsg.c_str()); + } + core_refresh_in_progress_.store(false, std::memory_order_release); + }; + }); + return; + } + // Use fast-lane on Overview for snappier balance updates bool useFast = (current_page_ == ui::NavPage::Overview); auto* w = useFast && fast_worker_ && fast_worker_->isRunning() diff --git a/src/daemon/embedded_daemon.h b/src/daemon/embedded_daemon.h index ce9d157..f0cc2e2 100644 --- a/src/daemon/embedded_daemon.h +++ b/src/daemon/embedded_daemon.h @@ -116,6 +116,26 @@ public: * @brief Get last N lines of daemon output (thread-safe snapshot) */ std::vector getRecentLines(int maxLines = 8) const; + + /** + * @brief Extract the latest block height from daemon output (thread-safe). + * Parses the last "height=N" from UpdateTip lines without copying + * the entire output buffer. Returns -1 if no UpdateTip found. + */ + int getLastBlockHeight() const { + std::lock_guard lk(output_mutex_); + // Search backwards from the end for "height=" + size_t pos = process_output_.rfind("height="); + if (pos == std::string::npos) return -1; + pos += 7; // skip "height=" + int h = 0; + for (size_t i = pos; i < process_output_.size(); ++i) { + char c = process_output_[i]; + if (c >= '0' && c <= '9') h = h * 10 + (c - '0'); + else break; + } + return h > 0 ? h : -1; + } /** * @brief Whether start() detected an existing daemon on the RPC port. diff --git a/src/data/wallet_state.h b/src/data/wallet_state.h index 716bf9a..1695c2e 100644 --- a/src/data/wallet_state.h +++ b/src/data/wallet_state.h @@ -180,6 +180,8 @@ struct PoolMiningState { struct WalletState { // Connection bool connected = false; + bool warming_up = false; // daemon reachable but in RPC warmup (error -28) + std::string warmup_status; // e.g. "Activating best chain..." int daemon_version = 0; std::string daemon_subversion; int protocol_version = 0; @@ -250,6 +252,8 @@ struct WalletState { void clear() { connected = false; + warming_up = false; + warmup_status.clear(); daemon_version = 0; daemon_subversion.clear(); protocol_version = 0; diff --git a/src/rpc/rpc_client.cpp b/src/rpc/rpc_client.cpp index e2e0020..0299e69 100644 --- a/src/rpc/rpc_client.cpp +++ b/src/rpc/rpc_client.cpp @@ -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()); 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 lk(curl_mutex_); connected_ = false; + warming_up_ = false; + warmup_status_.clear(); if (impl_->curl) { curl_easy_cleanup(impl_->curl); impl_->curl = nullptr; diff --git a/src/rpc/rpc_client.h b/src/rpc/rpc_client.h index a831110..db030c3 100644 --- a/src/rpc/rpc_client.h +++ b/src/rpc/rpc_client.h @@ -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