Files
ObsidianDragon/src/app_network.cpp
DanS 9edab31728 Refactor app services and stabilize refresh/UI flows
- Add refresh scheduler and network refresh service boundaries for typed
  refresh results, ordered RPC collectors, applicators, and price parsing.
- Add daemon lifecycle and wallet security workflow helpers while preserving
  App-owned command RPC, decrypt, cancellation, and UI handoff behavior.
- Split balance, console, mining, amount formatting, and async task logic into
  focused modules with expanded Phase 4 test coverage.
- Fix market price loading by triggering price refresh immediately, avoiding
  queue-pressure drops, tracking loading/error state, and adding translations.
- Polish send, explorer, peers, settings, theme/schema, and related tab UI.
- Replace checked-in generated language headers with build-generated resources.
- Document the cleanup audit, UI static-state guidance, and architecture updates.
2026-04-29 12:47:57 -05:00

1459 lines
58 KiB
C++

// 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 <nlohmann/json.hpp>
#include <curl/curl.h>
#include <fstream>
#include <utility>
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<rpc::RPCClient>();
}
if (!fast_worker_) {
fast_worker_ = std::make_unique<rpc::RPCWorker>();
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<std::string>().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<daemon::XmrigManager>();
// 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<const char*>(embedded::default_banlist_data),
embedded::default_banlist_size);
std::vector<std::string> 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<void(const std::string&)> 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<std::string>();
} 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<void(const std::string&)> 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<std::string>();
} 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<void(const std::string&)> 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<std::string>());
}, [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<std::string>());
}, [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<void(const std::string&)> callback)
{
if (!state_.connected || !rpc_) {
if (callback) callback("");
return;
}
// Collect all keys into a string
auto keys_result = std::make_shared<std::string>();
auto pending = std::make_shared<int>(0);
auto total = std::make_shared<int>(0);
// First get all addresses
auto all_addresses = std::make_shared<std::vector<std::string>>();
// 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<void(bool, const std::string&)> 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<void(bool, const std::string&)> 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<void(bool success, const std::string& result)> 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<std::string>();
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