// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // app_network.cpp — RPC connection, data refresh, and network operations. // Split from app.cpp for maintainability. // // Connection state machine: // // [Disconnected] // │ // ▼ tryConnect() every 5s // Auto-detect DRAGONX.conf (host, port, rpcuser, rpcpassword) // │ // ├─ no config found ──► start embedded daemon ──► retry // │ // ▼ post async rpc_->connect() to worker_ // [Connecting] // │ // ├─ success ──► onConnected() ──► [Connected] // │ │ // │ ▼ refreshData() every 5s // │ [Running] // │ │ // │ ├─ RPC error ──► onDisconnected() // │ │ │ // ├─ auth 401 ──► .cookie auth ──► retry│ ▼ // │ │ [Disconnected] // └─ failure ──► onDisconnected(reason) ┘ // may restart daemon #include "app.h" #include "rpc/rpc_client.h" #include "rpc/rpc_worker.h" #include "rpc/connection.h" #include "config/settings.h" #include "daemon/daemon_controller.h" #include "daemon/embedded_daemon.h" #include "daemon/xmrig_manager.h" #include "ui/notifications.h" #include "default_banlist_embedded.h" #include "util/amount_format.h" #include "util/platform.h" #include "util/perf_log.h" #include "util/i18n.h" #include #include #include #include namespace dragonx { using json = nlohmann::json; using NetworkRefreshService = services::NetworkRefreshService; namespace { class AppRefreshRpcGateway final : public NetworkRefreshService::RefreshRpcGateway { public: explicit AppRefreshRpcGateway(rpc::RPCClient& rpc) : rpc_(rpc) {} json call(const std::string& method, const json& params) override { return rpc_.call(method, params); } private: rpc::RPCClient& rpc_; }; } // namespace // ============================================================================ // Warmup Message Translation // Maps raw daemon RPC warmup messages to user-friendly text. // ============================================================================ struct WarmupText { const char* title; const char* description; }; static WarmupText translateWarmup(const std::string& raw) { if (raw.find("Loading") != std::string::npos) return {"Loading blockchain data...", "Reading the block database from disk. This may take a few minutes after updates."}; if (raw.find("Verifying") != std::string::npos) return {"Verifying blockchain...", "Checking recent blocks to make sure your chain data is valid."}; if (raw.find("Activating") != std::string::npos) return {"Processing blocks...", "Applying blocks to build the current chain state."}; if (raw.find("Rewinding") != std::string::npos) return {"Reorganizing chain...", "A chain reorganization was detected. Reverting to the correct chain."}; if (raw.find("Rescanning") != std::string::npos) return {"Scanning for transactions...", "Searching the blockchain for transactions belonging to your wallet. This can take a while."}; if (raw.find("Pruning") != std::string::npos) return {"Optimizing storage...", "Removing old block data to free up disk space."}; // Fallback: use the raw message return {raw.c_str(), ""}; } // ============================================================================ // Connection Management // ============================================================================ void App::tryConnect() { if (connection_in_progress_) return; static int connect_attempt = 0; ++connect_attempt; connection_in_progress_ = true; connection_status_ = TR("sb_loading_config"); // Auto-detect configuration (file I/O — fast, safe on main thread) auto config = rpc::Connection::autoDetectConfig(); if (config.rpcuser.empty() || config.rpcpassword.empty()) { connection_in_progress_ = false; std::string confPath = rpc::Connection::getDefaultConfPath(); VERBOSE_LOGF("[connect #%d] No valid config — DRAGONX.conf missing or no rpcuser/rpcpassword (looked at: %s)\n", connect_attempt, confPath.c_str()); // If we already know an external daemon is on the port, just wait // for the config file to appear (the daemon creates it on first run). if (daemon_controller_ && daemon_controller_->externalDaemonDetected()) { connection_status_ = TR("sb_waiting_config"); VERBOSE_LOGF("[connect #%d] External daemon detected on port, waiting for config file to appear\n", connect_attempt); network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core, services::RefreshScheduler::kCoreDefault - 1.0f); return; } connection_status_ = TR("sb_no_conf"); // Try to start embedded daemon if enabled if (use_embedded_daemon_ && !isEmbeddedDaemonRunning()) { connection_status_ = TR("sb_starting_daemon"); if (startEmbeddedDaemon()) { // Will retry connection after daemon starts VERBOSE_LOGF("[connect #%d] Embedded daemon starting, will retry connection...\n", connect_attempt); network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core, services::RefreshScheduler::kCoreDefault - 1.0f); } else if (daemon_controller_ && daemon_controller_->externalDaemonDetected()) { connection_status_ = TR("sb_waiting_config"); VERBOSE_LOGF("[connect #%d] External daemon detected but no config yet, will retry...\n", connect_attempt); network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core, services::RefreshScheduler::kCoreDefault - 1.0f); } else { VERBOSE_LOGF("[connect #%d] startEmbeddedDaemon() failed — lastError: %s, binary: %s\n", connect_attempt, daemon_controller_ ? daemon_controller_->lastError().c_str() : "(no daemon object)", daemon::EmbeddedDaemon::findDaemonBinary().c_str()); } } else if (!use_embedded_daemon_) { VERBOSE_LOGF("[connect #%d] Embedded daemon disabled (using external). No config found at %s\n", connect_attempt, confPath.c_str()); } return; } connection_status_ = TR("sb_connecting_daemon"); VERBOSE_LOGF("[connect #%d] Connecting to %s:%s (user=%s)\n", connect_attempt, config.host.c_str(), config.port.c_str(), config.rpcuser.c_str()); if (rpc::Connection::usesPlaintextRemote(config) && !remote_rpc_plaintext_warning_shown_) { remote_rpc_plaintext_warning_shown_ = true; ui::Notifications::instance().warning( "Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS.", 10.0f); } // Run the blocking rpc_->connect() on the worker thread so the UI // stays responsive (curl connect timeout can be up to 10 seconds). if (!worker_) { connection_in_progress_ = false; VERBOSE_LOGF("[connect #%d] No worker thread available!\n", connect_attempt); return; } // Capture daemon state before posting to worker bool daemonStarting = daemon_controller_ && (daemon_controller_->state() == daemon::EmbeddedDaemon::State::Starting || daemon_controller_->state() == daemon::EmbeddedDaemon::State::Running); bool externalDetected = daemon_controller_ && daemon_controller_->externalDaemonDetected(); int attempt = connect_attempt; // Log detailed daemon state for diagnostics if (daemon_controller_) { const char* stateStr = "unknown"; switch (daemon_controller_->state()) { case daemon::EmbeddedDaemon::State::Stopped: stateStr = "Stopped"; break; case daemon::EmbeddedDaemon::State::Starting: stateStr = "Starting"; break; case daemon::EmbeddedDaemon::State::Running: stateStr = "Running"; break; case daemon::EmbeddedDaemon::State::Stopping: stateStr = "Stopping"; break; case daemon::EmbeddedDaemon::State::Error: stateStr = "Error"; break; } VERBOSE_LOGF("[connect #%d] Daemon state: %s, running: %s, external: %s, crashes: %d, lastErr: %s\n", attempt, stateStr, daemon_controller_->isRunning() ? "yes" : "no", externalDetected ? "yes" : "no", daemon_controller_->crashCount(), daemon_controller_->lastError().empty() ? "(none)" : daemon_controller_->lastError().c_str()); } else { VERBOSE_LOGF("[connect #%d] No embedded daemon object (use_embedded=%s)\n", attempt, use_embedded_daemon_ ? "yes" : "no"); } worker_->post([this, config, daemonStarting, externalDetected, attempt]() -> rpc::RPCWorker::MainCb { bool connected = rpc_->connect(config.host, config.port, config.rpcuser, config.rpcpassword, config.use_tls); std::string connectErr = rpc_->getLastConnectError(); bool warmingUp = rpc_->isWarmingUp(); std::string warmupStatus = rpc_->getWarmupStatus(); return [this, config, connected, warmingUp, warmupStatus, daemonStarting, externalDetected, attempt, connectErr]() { if (connected) { 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; auto wt = translateWarmup(warmupStatus); state_.warmup_status = wt.title; state_.warmup_description = wt.description; // Append current block height from daemon output if (daemon_controller_) { int h = daemon_controller_->lastBlockHeight(); 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 // endlessly — tell the user what's wrong. bool authFailure = (connectErr.find("401") != std::string::npos); if (authFailure) { rpc::ConnectionConfig cookieConfig; if (rpc::Connection::buildCookieAuthConfig(config, cookieConfig)) { VERBOSE_LOGF("[connect #%d] HTTP 401 — retrying with .cookie auth from %s\n", attempt, cookieConfig.hush_dir.c_str()); worker_->post([this, cookieConfig, attempt]() -> rpc::RPCWorker::MainCb { bool ok = rpc_->connect(cookieConfig.host, cookieConfig.port, cookieConfig.rpcuser, cookieConfig.rpcpassword, cookieConfig.use_tls); return [this, cookieConfig, ok, attempt]() { connection_in_progress_ = false; if (ok) { VERBOSE_LOGF("[connect #%d] Connected via .cookie auth\n", attempt); saved_config_ = cookieConfig; onConnected(); } else { state_.connected = false; connection_status_ = TR("sb_auth_failed"); VERBOSE_LOGF("[connect #%d] .cookie auth also failed\n", attempt); ui::Notifications::instance().error( "RPC authentication failed (HTTP 401). " "The rpcuser/rpcpassword in DRAGONX.conf don't match the running daemon. " "Restart the daemon or correct the credentials."); } }; }); return; // async retry in progress } state_.connected = false; std::string confPath = rpc::Connection::getDefaultConfPath(); connection_status_ = TR("sb_auth_failed"); VERBOSE_LOGF("[connect #%d] HTTP 401 — rpcuser/rpcpassword in %s don't match the daemon. " "Edit the file or restart the daemon to regenerate credentials.\n", attempt, confPath.c_str()); ui::Notifications::instance().error( "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 (daemonStarting) { state_.connected = false; // Show the actual RPC error alongside the waiting message so // auth mismatches and timeouts aren't silently hidden. if (!connectErr.empty()) { char buf[256]; snprintf(buf, sizeof(buf), TR("sb_waiting_daemon_err"), connectErr.c_str()); connection_status_ = buf; } else { connection_status_ = TR("sb_waiting_daemon"); } VERBOSE_LOGF("[connect #%d] RPC connection failed (%s) — daemon still starting, will retry...\n", attempt, connectErr.c_str()); network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core, services::RefreshScheduler::kCoreDefault - 1.0f); } else if (externalDetected) { state_.connected = false; if (!connectErr.empty()) { char buf[256]; snprintf(buf, sizeof(buf), TR("sb_connecting_err"), connectErr.c_str()); connection_status_ = buf; } else { connection_status_ = TR("sb_connecting_external"); } VERBOSE_LOGF("[connect #%d] External daemon detected but RPC failed (%s), will retry...\n", attempt, connectErr.c_str()); network_refresh_.setTimer(services::NetworkRefreshService::Timer::Core, services::RefreshScheduler::kCoreDefault - 1.0f); } else { onDisconnected("Connection failed"); VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt); if (use_embedded_daemon_ && !isEmbeddedDaemonRunning()) { // Prevent infinite crash-restart loop if (daemon_controller_ && daemon_controller_->crashCount() >= 3) { { char buf[128]; snprintf(buf, sizeof(buf), TR("sb_daemon_crashed"), daemon_controller_->crashCount()); connection_status_ = buf; } VERBOSE_LOGF("[connect #%d] Daemon crashed %d times — not restarting (use Settings > Restart Daemon to retry)\n", attempt, daemon_controller_->crashCount()); } else { connection_status_ = TR("sb_starting_daemon"); if (startEmbeddedDaemon()) { VERBOSE_LOGF("[connect #%d] Embedded daemon starting, will retry connection...\n", attempt); } else if (daemon_controller_ && daemon_controller_->externalDaemonDetected()) { connection_status_ = TR("sb_connecting_generic"); VERBOSE_LOGF("[connect #%d] External daemon detected, will connect via RPC...\n", attempt); } else { VERBOSE_LOGF("[connect #%d] Failed to start embedded daemon — lastError: %s\n", attempt, daemon_controller_ ? daemon_controller_->lastError().c_str() : "(no daemon object)"); } } } else if (!use_embedded_daemon_) { VERBOSE_LOGF("[connect #%d] Embedded daemon disabled — external daemon at %s:%s not responding\n", attempt, config.host.c_str(), config.port.c_str()); } else { VERBOSE_LOGF("[connect #%d] Embedded daemon is running but RPC failed — daemon may be initializing\n", attempt); } } } connection_in_progress_ = false; }; }); } void App::onConnected() { state_.connected = true; connection_status_ = TR("connected"); // Reset crash counter on successful connection if (daemon_controller_) { daemon_controller_->resetCrashCount(); } // Get daemon info + wallet encryption state on the worker thread. // Fetching getwalletinfo here (before refreshData) ensures the lock // screen appears immediately instead of after 6+ queued RPC calls. bool initialPrefetchQueued = false; if (worker_ && rpc_) { auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::ConnectionInit, *worker_, [this]() -> rpc::RPCWorker::MainCb { AppRefreshRpcGateway refreshRpc(*rpc_); auto result = NetworkRefreshService::collectConnectionInitResult(refreshRpc); return [this, result]() { NetworkRefreshService::applyConnectionInitResult(state_, result); }; }, 3); initialPrefetchQueued = enqueued.enqueued; } // onConnected already fetched getwalletinfo — tell refreshData to skip // the duplicate call on the very first cycle. encryption_state_prefetched_ = initialPrefetchQueued; // Addresses are unknown on fresh connect — force a fetch addresses_dirty_ = true; // Start the fast-lane RPC connection (dedicated to 1-second mining polls). // Uses its own curl handle + worker thread so getlocalsolps never blocks // behind the main refresh batch. if (!fast_rpc_) { fast_rpc_ = std::make_unique(); } if (!fast_worker_) { fast_worker_ = std::make_unique(); fast_worker_->start(); } // Connect on the fast worker's own thread (non-blocking to main) fast_worker_->post([this]() -> rpc::RPCWorker::MainCb { bool ok = fast_rpc_->connect(saved_config_.host, saved_config_.port, saved_config_.rpcuser, saved_config_.rpcpassword, saved_config_.use_tls); return [ok]() { if (!ok) { DEBUG_LOGF("[FastLane] Failed to connect secondary RPC client\\n"); } else { DEBUG_LOGF("[FastLane] Secondary RPC client connected\\n"); } }; }); // Initial data refresh refreshData(); refreshMarketData(); // Apply compiled-in default ban list applyDefaultBanlist(); } void App::onDisconnected(const std::string& reason) { state_.connected = false; state_.warming_up = false; state_.warmup_status.clear(); state_.clear(); connection_status_ = reason; // Clear RPC result caches viewtx_cache_.clear(); confirmed_tx_cache_.clear(); confirmed_tx_ids_.clear(); confirmed_cache_block_ = -1; last_tx_block_height_ = -1; // Tear down the fast-lane connection if (fast_worker_) { fast_worker_->stop(); } if (fast_rpc_) { fast_rpc_->disconnect(); } } // ============================================================================ // Data Refresh — Tab-Aware Prioritized System // // Data is split into independent categories, each with its own refresh // function, timer, and in-progress guard. The orchestrator (refreshData) // dispatches all categories, but each can also be called independently // (e.g. on tab switch for immediate refresh). // // Categories: // Core — z_gettotalbalance + getblockchaininfo (balance, sync) // Addresses — z_listaddresses + listunspent (address list, per-addr balances) // Transactions — listtransactions + z_listreceivedbyaddress + z_viewtransaction // Peers — getpeerinfo + listbanned (already standalone) // Encryption — getwalletinfo (one-shot on connect) // // Intervals are adjusted by applyRefreshPolicy() based on the active tab, // so the user sees faster updates for the data they're interacting with. // ============================================================================ App::RefreshIntervals App::getIntervalsForPage(ui::NavPage page) { return services::NetworkRefreshService::intervalsForPage(page); } void App::applyRefreshPolicy(ui::NavPage page) { network_refresh_.setIntervals(getIntervalsForPage(page)); } void App::setCurrentPage(ui::NavPage page) { if (page == current_page_) return; current_page_ = page; applyRefreshPolicy(page); using RefreshTimer = services::NetworkRefreshService::Timer; // Immediate refresh for the incoming tab's priority data if (state_.connected && !state_.isLocked()) { using NP = ui::NavPage; switch (page) { case NP::Overview: refreshCoreData(); network_refresh_.reset(RefreshTimer::Core); break; case NP::History: transactions_dirty_ = true; refreshTransactionData(); network_refresh_.reset(RefreshTimer::Transactions); break; case NP::Send: case NP::Receive: addresses_dirty_ = true; refreshAddressData(); network_refresh_.reset(RefreshTimer::Addresses); break; case NP::Peers: refreshPeerInfo(); network_refresh_.reset(RefreshTimer::Peers); break; case NP::Mining: refreshMiningInfo(); break; default: break; } } } bool App::shouldRefreshTransactions() const { const int currentBlocks = state_.sync.blocks; return network_refresh_.shouldRefreshTransactions(last_tx_block_height_, currentBlocks, state_.transactions.empty(), transactions_dirty_); } 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(); if (addresses_dirty_) refreshAddressData(); if (shouldRefreshTransactions()) refreshTransactionData(); if (current_page_ == ui::NavPage::Peers) refreshPeerInfo(); if (!encryption_state_prefetched_) { encryption_state_prefetched_ = false; refreshEncryptionState(); } } // ============================================================================ // Core Data: balance + blockchain info (~50-100ms, 2 RPC calls) // Uses fast_worker_ when on Overview tab for lower latency. // ============================================================================ 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 (!worker_) return; auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Core, *worker_, [this]() -> rpc::RPCWorker::MainCb { AppRefreshRpcGateway refreshRpc(*rpc_); auto result = NetworkRefreshService::collectWarmupPollResult(refreshRpc); return [this, result = std::move(result)]() { if (result.ready) { // Warmup finished — daemon is fully ready state_.warming_up = false; state_.warmup_status.clear(); state_.warmup_description.clear(); connection_status_ = TR("connected"); VERBOSE_LOGF("[warmup] Daemon ready, warmup complete\n"); NetworkRefreshService::applyConnectionInfoResult(state_, result.info); // Trigger full data refresh now that daemon is ready refreshData(); } else { // Still warming up — update status auto wt = translateWarmup(result.errorMessage); state_.warmup_status = wt.title; state_.warmup_description = wt.description; if (daemon_controller_) { int h = daemon_controller_->lastBlockHeight(); if (h > 0) state_.warmup_status += " (Block " + std::to_string(h) + ")"; } connection_status_ = state_.warmup_status; VERBOSE_LOGF("[warmup] Still warming up: %s\n", result.errorMessage.c_str()); } }; }, 3); if (!enqueued.enqueued) return; 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() ? fast_worker_.get() : worker_.get(); auto* rpc = useFast && fast_rpc_ && fast_rpc_->isConnected() ? fast_rpc_.get() : rpc_.get(); if (!w || !rpc) return; auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Core, *w, [this, rpc]() -> rpc::RPCWorker::MainCb { AppRefreshRpcGateway refreshRpc(*rpc); auto result = NetworkRefreshService::collectCoreRefreshResult(refreshRpc); return [this, result]() { try { NetworkRefreshService::applyCoreRefreshResult(state_, result, std::time(nullptr)); // Auto-shield transparent funds if enabled if (result.balanceOk && settings_ && settings_->getAutoShield() && state_.transparent_balance > 0.0001 && !state_.sync.syncing && !auto_shield_pending_.exchange(true)) { std::string targetZAddr; for (const auto& addr : state_.addresses) { if (addr.isShielded()) { targetZAddr = addr.address; break; } } if (!targetZAddr.empty() && rpc_) { DEBUG_LOGF("[AutoShield] Shielding %.8f DRGX to %s\n", state_.transparent_balance, targetZAddr.c_str()); rpc_->z_shieldCoinbase("*", targetZAddr, 0.0001, 50, [this](const json& result) { if (result.contains("opid")) { DEBUG_LOGF("[AutoShield] Started: %s\n", result["opid"].get().c_str()); } auto_shield_pending_ = false; }, [this](const std::string& err) { DEBUG_LOGF("[AutoShield] Error: %s\n", err.c_str()); auto_shield_pending_ = false; }); } else { auto_shield_pending_ = false; } } } catch (const std::exception& e) { DEBUG_LOGF("[refreshCoreData] callback error: %s\n", e.what()); } }; }, 3); if (!enqueued.enqueued) return; } // ============================================================================ // Address Data: z/t address lists + per-address balances // ============================================================================ void App::refreshAddressData() { if (!worker_ || !rpc_ || !state_.connected) return; auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Addresses, *worker_, [this]() -> rpc::RPCWorker::MainCb { AppRefreshRpcGateway refreshRpc(*rpc_); auto result = NetworkRefreshService::collectAddressRefreshResult(refreshRpc); return [this, result = std::move(result)]() mutable { NetworkRefreshService::applyAddressRefreshResult(state_, std::move(result)); address_list_dirty_ = true; addresses_dirty_ = false; }; }, 3); if (!enqueued.enqueued) return; } // ============================================================================ // Transaction Data: transparent + shielded receives + z_viewtransaction enrichment // ============================================================================ void App::refreshTransactionData() { if (!worker_ || !rpc_ || !state_.connected) return; const int currentBlocks = state_.sync.blocks; auto transactionSnapshot = NetworkRefreshService::buildTransactionRefreshSnapshot( state_, viewtx_cache_, send_txids_); auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Transactions, *worker_, [this, currentBlocks, transactionSnapshot = std::move(transactionSnapshot)]() -> rpc::RPCWorker::MainCb { AppRefreshRpcGateway refreshRpc(*rpc_); auto result = NetworkRefreshService::collectTransactionRefreshResult( refreshRpc, transactionSnapshot, currentBlocks, MAX_VIEWTX_PER_CYCLE); return [this, result = std::move(result)]() mutable { NetworkRefreshService::TransactionCacheUpdate cacheUpdate{ viewtx_cache_, send_txids_, confirmed_tx_cache_, confirmed_tx_ids_, confirmed_cache_block_, last_tx_block_height_ }; NetworkRefreshService::applyTransactionRefreshResult( state_, cacheUpdate, std::move(result), std::time(nullptr)); }; }, 3); if (!enqueued.enqueued) return; transactions_dirty_ = false; network_refresh_.resetTxAge(); } // ============================================================================ // Encryption State: wallet info (one-shot on connect, lightweight) // ============================================================================ void App::refreshEncryptionState() { if (!worker_ || !rpc_ || !state_.connected) return; auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Encryption, *worker_, [this]() -> rpc::RPCWorker::MainCb { json walletInfo; bool ok = false; try { walletInfo = rpc_->call("getwalletinfo"); ok = true; } catch (...) {} if (!ok) return nullptr; auto result = NetworkRefreshService::parseWalletEncryptionResult(walletInfo); return [this, result]() { NetworkRefreshService::applyWalletEncryptionResult(state_, result); }; }, 3); if (!enqueued.enqueued) return; } void App::refreshBalance() { refreshCoreData(); } void App::refreshAddresses() { addresses_dirty_ = true; refreshAddressData(); } void App::refreshMiningInfo() { // Use the dedicated fast-lane worker + connection so mining polls // never block behind the main refresh batch. Falls back to the main // worker if the fast lane isn't ready yet (e.g. during initial connect). auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get(); auto* rpc = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); if (!w || !rpc) return; // Capture daemon memory outside (may be accessed on main thread) double daemonMemMb = 0.0; if (daemon_controller_) { daemonMemMb = daemon_controller_->memoryUsageMB(); } // Slow-tick counter: run full getmininginfo every ~5 seconds // to reduce RPC overhead. getlocalsolps (returns H/s for RandomX) runs every tick (1s). // NOTE: getinfo is NOT called here — longestchain/notarized are updated by // refreshBalance (via getblockchaininfo), and daemon_version/protocol_version/ // p2p_port are static for the lifetime of a connection (set in onConnected). bool doSlowRefresh = (mining_slow_counter_++ % 5 == 0); auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Mining, *w, [this, rpc, daemonMemMb, doSlowRefresh]() -> rpc::RPCWorker::MainCb { AppRefreshRpcGateway refreshRpc(*rpc); auto result = NetworkRefreshService::collectMiningRefreshResult( refreshRpc, daemonMemMb, doSlowRefresh); return [this, result]() { try { NetworkRefreshService::applyMiningRefreshResult(state_, result, std::time(nullptr)); } catch (const std::exception& e) { DEBUG_LOGF("[refreshMiningInfo] callback error: %s\n", e.what()); } }; }, 2); if (!enqueued.enqueued) return; } void App::refreshPeerInfo() { if (!rpc_) return; // Use fast-lane worker to bypass head-of-line blocking behind refreshData. auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get(); auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(); if (!w) return; auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Peers, *w, [this, r]() -> rpc::RPCWorker::MainCb { AppRefreshRpcGateway refreshRpc(*r); auto result = NetworkRefreshService::collectPeerRefreshResult(refreshRpc); return [this, result = std::move(result)]() mutable { NetworkRefreshService::applyPeerRefreshResult(state_, std::move(result), std::time(nullptr)); }; }, 2); if (!enqueued.enqueued) return; } void App::refreshPrice() { // Skip if price fetching is disabled if (!settings_->getFetchPrices()) return; if (!worker_) return; auto enqueued = network_refresh_.enqueue(services::NetworkRefreshService::Job::Price, *worker_, [this]() -> rpc::RPCWorker::MainCb { // --- Worker thread: blocking HTTP GET to CoinGecko --- NetworkRefreshService::PriceHttpResult result; try { CURL* curl = curl_easy_init(); if (!curl) { DEBUG_LOGF("Failed to initialize curl for price fetch\n"); result.errorMessage = "Price fetch failed: failed to initialize curl"; } else { std::string response_data; const char* url = "https://api.coingecko.com/api/v3/simple/price?ids=dragonx-2&vs_currencies=usd,btc&include_24hr_change=true&include_24hr_vol=true&include_market_cap=true"; auto write_callback = [](void* contents, size_t size, size_t nmemb, std::string* userp) -> size_t { size_t totalSize = size * nmemb; userp->append((char*)contents, totalSize); return totalSize; }; curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_data); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "DragonX-Wallet/1.0"); CURLcode res = curl_easy_perform(curl); long http_code = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); curl_easy_cleanup(curl); NetworkRefreshService::PriceHttpResponse response{ res == CURLE_OK, http_code, std::move(response_data), res == CURLE_OK ? std::string() : std::string(curl_easy_strerror(res)) }; result = NetworkRefreshService::parsePriceHttpResponse(response, std::time(nullptr)); } } catch (const std::exception& e) { DEBUG_LOGF("Price fetch error: %s\n", e.what()); result.errorMessage = std::string("Price fetch error: ") + e.what(); } catch (...) { DEBUG_LOGF("Price fetch error: unknown exception\n"); result.errorMessage = "Price fetch error: unknown exception"; } if (result.price) { DEBUG_LOGF("Price updated: $%.6f USD\n", result.price->market.price_usd); } else { DEBUG_LOGF("%s\n", result.errorMessage.c_str()); } return [this, result = std::move(result)]() mutable { if (result.price) { NetworkRefreshService::applyPriceRefreshResult(state_, *result.price, std::chrono::steady_clock::now()); } else { NetworkRefreshService::applyPriceRefreshFailure(state_, result.errorMessage); } }; }, 0); if (!enqueued.enqueued) return; NetworkRefreshService::markPriceRefreshStarted(state_); } void App::refreshMarketData() { refreshPrice(); } // ============================================================================ // Mining Operations // ============================================================================ void App::startMining(int threads) { if (!state_.connected || !rpc_ || !worker_) return; if (mining_toggle_in_progress_.exchange(true)) return; // already in progress worker_->post([this, threads]() -> rpc::RPCWorker::MainCb { bool ok = false; std::string errMsg; try { rpc_->call("setgenerate", {true, threads}); ok = true; } catch (const std::exception& e) { errMsg = e.what(); } return [this, threads, ok, errMsg]() { mining_toggle_in_progress_.store(false); if (ok) { state_.mining.generate = true; state_.mining.genproclimit = threads; DEBUG_LOGF("Mining started with %d threads\n", threads); } else { DEBUG_LOGF("Failed to start mining: %s\n", errMsg.c_str()); ui::Notifications::instance().error("Mining failed: " + errMsg); } }; }); } void App::stopMining() { if (!state_.connected || !rpc_ || !worker_) return; if (mining_toggle_in_progress_.exchange(true)) return; // already in progress worker_->post([this]() -> rpc::RPCWorker::MainCb { bool ok = false; try { rpc_->call("setgenerate", {false, 0}); ok = true; } catch (const std::exception& e) { DEBUG_LOGF("Failed to stop mining: %s\n", e.what()); } return [this, ok]() { mining_toggle_in_progress_.store(false); if (ok) { state_.mining.generate = false; state_.mining.localHashrate = 0.0; DEBUG_LOGF("Mining stopped\n"); } }; }); } void App::startPoolMining(int threads) { if (!xmrig_manager_) xmrig_manager_ = std::make_unique(); // If already running, stop first (e.g. thread count change) if (xmrig_manager_->isRunning()) { xmrig_manager_->stop(); } // Stop solo mining first if active if (state_.mining.generate) stopMining(); daemon::XmrigManager::Config cfg; cfg.pool_url = settings_->getPoolUrl(); cfg.worker_name = settings_->getPoolWorker(); cfg.algo = settings_->getPoolAlgo(); cfg.threads = threads; // Use the same thread selection as solo mining cfg.tls = settings_->getPoolTls(); cfg.hugepages = settings_->getPoolHugepages(); // Use first shielded address as the mining wallet address, fall back to transparent for (const auto& addr : state_.z_addresses) { if (!addr.address.empty()) { cfg.wallet_address = addr.address; break; } } if (cfg.wallet_address.empty()) { for (const auto& addr : state_.addresses) { if (addr.type == "transparent" && !addr.address.empty()) { cfg.wallet_address = addr.address; break; } } } // Fallback: use pool worker address from settings (available even before // the daemon is connected or the blockchain is synced). if (cfg.wallet_address.empty() && !cfg.worker_name.empty()) { cfg.wallet_address = cfg.worker_name; } if (cfg.wallet_address.empty()) { DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n"); ui::Notifications::instance().error("No wallet address available — generate a Z address in the Receive tab"); return; } if (!xmrig_manager_->start(cfg)) { std::string err = xmrig_manager_->getLastError(); DEBUG_LOGF("[ERROR] Pool mining: %s\n", err.c_str()); // Check for Windows Defender blocking (error 225 = ERROR_VIRUS_INFECTED) if (err.find("error 225") != std::string::npos || err.find("virus") != std::string::npos) { ui::Notifications::instance().error( "Windows Defender blocked xmrig. Add exclusion for %APPDATA%\\ObsidianDragon"); #ifdef _WIN32 // Offer to open Windows Security settings pending_antivirus_dialog_ = true; #endif } else { ui::Notifications::instance().error("Failed to start pool miner: " + err); } } } void App::stopPoolMining() { if (xmrig_manager_ && xmrig_manager_->isRunning()) { xmrig_manager_->stop(3000); } } // ============================================================================ // Peer Operations // ============================================================================ void App::banPeer(const std::string& ip, int duration_seconds) { if (!state_.connected || !rpc_) return; rpc_->setBan(ip, "add", [this](const json&) { refreshPeerInfo(); }, nullptr, duration_seconds); } void App::unbanPeer(const std::string& ip) { if (!state_.connected || !rpc_) return; rpc_->setBan(ip, "remove", [this](const json&) { refreshPeerInfo(); }); } void App::clearBans() { if (!state_.connected || !rpc_) return; rpc_->clearBanned([this](const json&) { state_.banned_peers.clear(); }); } void App::applyDefaultBanlist() { if (!state_.connected || !rpc_ || !worker_) return; // Parse the embedded default_banlist.txt (compiled from res/default_banlist.txt) std::string data(reinterpret_cast(embedded::default_banlist_data), embedded::default_banlist_size); std::vector ips; size_t pos = 0; while (pos < data.size()) { size_t eol = data.find('\n', pos); if (eol == std::string::npos) eol = data.size(); std::string line = data.substr(pos, eol - pos); pos = eol + 1; // Strip carriage return (Windows line endings) if (!line.empty() && line.back() == '\r') line.pop_back(); // Strip leading/trailing whitespace size_t start = line.find_first_not_of(" \t"); if (start == std::string::npos) continue; line = line.substr(start, line.find_last_not_of(" \t") - start + 1); // Skip empty lines and comments if (line.empty() || line[0] == '#') continue; ips.push_back(line); } if (ips.empty()) return; // Apply bans on the worker thread to avoid blocking the UI worker_->post([this, ips]() -> rpc::RPCWorker::MainCb { int applied = 0; for (const auto& ip : ips) { try { // 0 = permanent ban (until node restart or manual unban) // Using a very long duration (10 years) for effectively permanent bans rpc_->call("setban", {ip, "add", 315360000}); applied++; } catch (...) { // Already banned or invalid — skip silently } } return [applied]() { if (applied > 0) { DEBUG_LOGF("[Banlist] Applied %d default bans\n", applied); } }; }); } // ============================================================================ // Address Operations // ============================================================================ void App::createNewZAddress(std::function callback) { if (!state_.connected || !rpc_ || !worker_) return; worker_->post([this, callback]() -> rpc::RPCWorker::MainCb { std::string addr; try { json result = rpc_->call("z_getnewaddress"); addr = result.get(); } catch (const std::exception& e) { DEBUG_LOGF("z_getnewaddress error: %s\n", e.what()); } return [this, callback, addr]() { if (!addr.empty()) { // Inject immediately so UI can select the address next frame AddressInfo info; info.address = addr; info.type = "shielded"; info.balance = 0.0; state_.z_addresses.push_back(info); address_list_dirty_ = true; // Also trigger full refresh to get proper balances addresses_dirty_ = true; refreshAddresses(); } if (callback) callback(addr); }; }); } void App::createNewTAddress(std::function callback) { if (!state_.connected || !rpc_ || !worker_) return; worker_->post([this, callback]() -> rpc::RPCWorker::MainCb { std::string addr; try { json result = rpc_->call("getnewaddress"); addr = result.get(); } catch (const std::exception& e) { DEBUG_LOGF("getnewaddress error: %s\n", e.what()); } return [this, callback, addr]() { if (!addr.empty()) { // Inject immediately so UI can select the address next frame AddressInfo info; info.address = addr; info.type = "transparent"; info.balance = 0.0; state_.t_addresses.push_back(info); address_list_dirty_ = true; // Also trigger full refresh to get proper balances addresses_dirty_ = true; refreshAddresses(); } if (callback) callback(addr); }; }); } void App::hideAddress(const std::string& addr) { if (settings_) { settings_->hideAddress(addr); settings_->save(); } } void App::unhideAddress(const std::string& addr) { if (settings_) { settings_->unhideAddress(addr); settings_->save(); } } bool App::isAddressHidden(const std::string& addr) const { return settings_ && settings_->isAddressHidden(addr); } int App::getHiddenAddressCount() const { return settings_ ? settings_->getHiddenAddressCount() : 0; } void App::favoriteAddress(const std::string& addr) { if (settings_) { settings_->favoriteAddress(addr); settings_->save(); } } void App::unfavoriteAddress(const std::string& addr) { if (settings_) { settings_->unfavoriteAddress(addr); settings_->save(); } } bool App::isAddressFavorite(const std::string& addr) const { return settings_ && settings_->isAddressFavorite(addr); } void App::setAddressLabel(const std::string& addr, const std::string& label) { if (settings_) { settings_->setAddressLabel(addr, label); settings_->save(); } } void App::setAddressIcon(const std::string& addr, const std::string& icon) { if (settings_) { settings_->setAddressIcon(addr, icon); settings_->save(); } } std::string App::getAddressLabel(const std::string& addr) const { if (!settings_) return ""; return settings_->getAddressMeta(addr).label; } std::string App::getAddressIcon(const std::string& addr) const { if (!settings_) return ""; return settings_->getAddressMeta(addr).icon; } int App::getAddressSortOrder(const std::string& addr) const { if (!settings_) return -1; return settings_->getAddressMeta(addr).sortOrder; } void App::setAddressSortOrder(const std::string& addr, int order) { if (settings_) { settings_->setAddressSortOrder(addr, order); settings_->save(); } } int App::getNextSortOrder() const { return settings_ ? settings_->getNextSortOrder() : 0; } void App::swapAddressOrder(const std::string& a, const std::string& b) { if (settings_) { settings_->swapAddressOrder(a, b); settings_->save(); } } // ============================================================================ // Key Export/Import Operations // ============================================================================ void App::exportPrivateKey(const std::string& address, std::function callback) { if (!state_.connected || !rpc_) { if (callback) callback(""); return; } auto keyKind = services::WalletSecurityController::classifyAddress(address); if (keyKind == services::WalletSecurityController::KeyKind::Shielded) { // Z-address: use z_exportkey rpc_->z_exportKey(address, [callback](const json& result) { if (callback) callback(result.get()); }, [callback](const std::string& error) { DEBUG_LOGF("Export z-key error: %s\n", error.c_str()); ui::Notifications::instance().error("Key export failed: " + error); if (callback) callback(""); }); } else { // T-address: use dumpprivkey rpc_->dumpPrivKey(address, [callback](const json& result) { if (callback) callback(result.get()); }, [callback](const std::string& error) { DEBUG_LOGF("Export t-key error: %s\n", error.c_str()); ui::Notifications::instance().error("Key export failed: " + error); if (callback) callback(""); }); } } void App::exportAllKeys(std::function callback) { if (!state_.connected || !rpc_) { if (callback) callback(""); return; } // Collect all keys into a string auto keys_result = std::make_shared(); auto pending = std::make_shared(0); auto total = std::make_shared(0); // First get all addresses auto all_addresses = std::make_shared>(); // Add t-addresses for (const auto& addr : state_.t_addresses) { all_addresses->push_back(addr.address); } // Add z-addresses for (const auto& addr : state_.z_addresses) { all_addresses->push_back(addr.address); } *total = all_addresses->size(); *pending = *total; if (*total == 0) { if (callback) callback("# No addresses to export\n"); return; } *keys_result = "# DragonX Wallet Private Keys Export\n"; *keys_result += "# WARNING: Keep this file secure! Anyone with these keys can spend your coins!\n\n"; for (const auto& addr : *all_addresses) { exportPrivateKey(addr, [keys_result, pending, total, callback, addr](const std::string& key) { if (!key.empty()) { *keys_result += "# " + addr + "\n"; *keys_result += key + "\n\n"; } (*pending)--; if (*pending == 0 && callback) { callback(*keys_result); } }); } } void App::importPrivateKey(const std::string& key, std::function callback) { if (!state_.connected || !rpc_) { if (callback) callback(false, "Not connected"); return; } auto keyKind = services::WalletSecurityController::classifyPrivateKey(key); if (keyKind == services::WalletSecurityController::KeyKind::Shielded) { rpc_->z_importKey(key, true, [this, callback](const json& result) { refreshAddresses(); if (callback) callback(true, services::WalletSecurityController::importSuccessMessage( services::WalletSecurityController::KeyKind::Shielded)); }, [callback](const std::string& error) { if (callback) callback(false, error); }); } else { rpc_->importPrivKey(key, true, [this, callback](const json& result) { refreshAddresses(); if (callback) callback(true, services::WalletSecurityController::importSuccessMessage( services::WalletSecurityController::KeyKind::Transparent)); }, [callback](const std::string& error) { if (callback) callback(false, error); }); } } void App::backupWallet(const std::string& destination, std::function callback) { if (!state_.connected || !rpc_) { if (callback) callback(false, "Not connected"); return; } // Use z_exportwallet or similar to export all keys // For now, we'll use exportAllKeys and save to file exportAllKeys([destination, callback](const std::string& keys) { if (keys.empty()) { if (callback) callback(false, "Failed to export keys"); return; } // Write to file std::ofstream file(destination); if (!file.is_open()) { if (callback) callback(false, "Could not open file: " + destination); return; } file << keys; file.close(); if (callback) callback(true, "Wallet backup saved to: " + destination); }); } // ============================================================================ // Transaction Operations // ============================================================================ void App::sendTransaction(const std::string& from, const std::string& to, double amount, double fee, const std::string& memo, std::function callback) { if (!state_.connected || !rpc_) { if (callback) callback(false, "Not connected"); return; } // Check that we have the spending key for the from address if (!from.empty() && from[0] == 'z') { bool spendable = false; for (const auto& addr : state_.z_addresses) { if (addr.address == from) { spendable = addr.has_spending_key; break; } } if (!spendable) { if (callback) callback(false, "This is a view-only address (no spending key). Import the spending key to send from this address."); return; } } // Build recipients array nlohmann::json recipients = nlohmann::json::array(); nlohmann::json recipient; recipient["address"] = to; recipient["amount"] = util::formatAmountFixed(amount); if (!memo.empty()) { recipient["memo"] = memo; } recipients.push_back(recipient); // Run z_sendmany on worker thread to avoid blocking UI if (worker_) { worker_->post([this, from, recipients, callback]() -> rpc::RPCWorker::MainCb { bool ok = false; std::string result_str; try { auto result = rpc_->call("z_sendmany", {from, recipients}); result_str = result.get(); ok = true; } catch (const std::exception& e) { result_str = e.what(); } return [this, callback, ok, result_str]() { if (ok) { // A send changes address balances — refresh on next cycle addresses_dirty_ = true; // Force transaction list refresh so the sent tx appears immediately transactions_dirty_ = true; last_tx_block_height_ = -1; network_refresh_.markWalletMutationRefresh(); // Track the opid so we can poll for completion if (!result_str.empty()) { pending_opids_.push_back(result_str); } } if (callback) callback(ok, result_str); }; }); } } } // namespace dragonx