1666 lines
71 KiB
C++
1666 lines
71 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.
|
|
|
|
#include "app.h"
|
|
#include "rpc/rpc_client.h"
|
|
#include "rpc/rpc_worker.h"
|
|
#include "rpc/connection.h"
|
|
#include "config/settings.h"
|
|
#include "daemon/embedded_daemon.h"
|
|
#include "daemon/xmrig_manager.h"
|
|
#include "ui/notifications.h"
|
|
#include "util/platform.h"
|
|
#include "util/perf_log.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
#include <curl/curl.h>
|
|
#include <fstream>
|
|
|
|
namespace dragonx {
|
|
|
|
using json = nlohmann::json;
|
|
|
|
// ============================================================================
|
|
// Connection Management
|
|
// ============================================================================
|
|
|
|
void App::tryConnect()
|
|
{
|
|
if (connection_in_progress_) return;
|
|
|
|
connection_in_progress_ = true;
|
|
connection_status_ = "Loading configuration...";
|
|
|
|
// 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;
|
|
DEBUG_LOGF("Could not find DRAGONX.conf or missing rpcuser/rpcpassword\n");
|
|
|
|
// 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 (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) {
|
|
connection_status_ = "Waiting for daemon config...";
|
|
return;
|
|
}
|
|
|
|
connection_status_ = "No DRAGONX.conf found";
|
|
|
|
// Try to start embedded daemon if enabled
|
|
if (use_embedded_daemon_ && !isEmbeddedDaemonRunning()) {
|
|
connection_status_ = "Starting dragonxd...";
|
|
if (startEmbeddedDaemon()) {
|
|
// Will retry connection after daemon starts
|
|
DEBUG_LOGF("Embedded daemon starting, will retry connection...\n");
|
|
} else if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) {
|
|
connection_status_ = "Waiting for daemon config...";
|
|
DEBUG_LOGF("External daemon detected but no config yet, will retry...\n");
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
connection_status_ = "Connecting to dragonxd...";
|
|
DEBUG_LOGF("Connecting to %s:%s\n", config.host.c_str(), config.port.c_str());
|
|
|
|
// 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;
|
|
return;
|
|
}
|
|
|
|
// Capture daemon state before posting to worker
|
|
bool daemonStarting = embedded_daemon_ &&
|
|
(embedded_daemon_->getState() == daemon::EmbeddedDaemon::State::Starting ||
|
|
embedded_daemon_->getState() == daemon::EmbeddedDaemon::State::Running);
|
|
bool externalDetected = embedded_daemon_ && embedded_daemon_->externalDaemonDetected();
|
|
|
|
worker_->post([this, config, daemonStarting, externalDetected]() -> rpc::RPCWorker::MainCb {
|
|
bool connected = rpc_->connect(config.host, config.port, config.rpcuser, config.rpcpassword);
|
|
|
|
return [this, config, connected, daemonStarting, externalDetected]() {
|
|
if (connected) {
|
|
saved_config_ = config; // save for fast-lane connection
|
|
onConnected();
|
|
} else {
|
|
if (daemonStarting) {
|
|
state_.connected = false;
|
|
connection_status_ = "Waiting for dragonxd to start...";
|
|
DEBUG_LOGF("Connection attempt failed — daemon still starting, will retry...\n");
|
|
} else if (externalDetected) {
|
|
state_.connected = false;
|
|
connection_status_ = "Connecting to daemon...";
|
|
DEBUG_LOGF("External daemon on port but RPC not ready yet, will retry...\n");
|
|
} else {
|
|
onDisconnected("Connection failed");
|
|
|
|
if (use_embedded_daemon_ && !isEmbeddedDaemonRunning()) {
|
|
// Prevent infinite crash-restart loop
|
|
if (embedded_daemon_ && embedded_daemon_->getCrashCount() >= 3) {
|
|
connection_status_ = "Daemon crashed " + std::to_string(embedded_daemon_->getCrashCount()) + " times";
|
|
DEBUG_LOGF("Daemon crashed %d times — not restarting (use Settings > Restart Daemon to retry)\n",
|
|
embedded_daemon_->getCrashCount());
|
|
} else {
|
|
connection_status_ = "Starting dragonxd...";
|
|
if (startEmbeddedDaemon()) {
|
|
DEBUG_LOGF("Embedded daemon starting, will retry connection...\n");
|
|
} else if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) {
|
|
connection_status_ = "Connecting to daemon...";
|
|
DEBUG_LOGF("External daemon detected, will connect via RPC...\n");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
connection_in_progress_ = false;
|
|
};
|
|
});
|
|
}
|
|
|
|
void App::onConnected()
|
|
{
|
|
state_.connected = true;
|
|
connection_status_ = "Connected";
|
|
|
|
// Reset crash counter on successful connection
|
|
if (embedded_daemon_) {
|
|
embedded_daemon_->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.
|
|
if (worker_ && rpc_) {
|
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
|
json info, walletInfo;
|
|
bool infoOk = false, walletOk = false;
|
|
try {
|
|
info = rpc_->call("getinfo");
|
|
infoOk = true;
|
|
} catch (...) {}
|
|
try {
|
|
walletInfo = rpc_->call("getwalletinfo");
|
|
walletOk = true;
|
|
} catch (...) {}
|
|
return [this, info, walletInfo, infoOk, walletOk]() {
|
|
if (infoOk) {
|
|
try {
|
|
if (info.contains("version"))
|
|
state_.daemon_version = info["version"].get<int>();
|
|
if (info.contains("protocolversion"))
|
|
state_.protocol_version = info["protocolversion"].get<int>();
|
|
if (info.contains("p2pport"))
|
|
state_.p2p_port = info["p2pport"].get<int>();
|
|
if (info.contains("longestchain"))
|
|
state_.longestchain = info["longestchain"].get<int>();
|
|
if (info.contains("notarized"))
|
|
state_.notarized = info["notarized"].get<int>();
|
|
if (info.contains("blocks"))
|
|
state_.sync.blocks = info["blocks"].get<int>();
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("[onConnected] getinfo callback error: %s\n", e.what());
|
|
}
|
|
}
|
|
// Apply encryption/lock state immediately so the lock
|
|
// screen shows on the very first frame after connect.
|
|
if (walletOk) {
|
|
try {
|
|
if (walletInfo.contains("unlocked_until")) {
|
|
state_.encrypted = true;
|
|
int64_t until = walletInfo["unlocked_until"].get<int64_t>();
|
|
state_.unlocked_until = until;
|
|
state_.locked = (until == 0);
|
|
} else {
|
|
state_.encrypted = false;
|
|
state_.locked = false;
|
|
state_.unlocked_until = 0;
|
|
}
|
|
state_.encryption_state_known = true;
|
|
} catch (...) {}
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
// onConnected already fetched getwalletinfo — tell refreshData to skip
|
|
// the duplicate call on the very first cycle.
|
|
encryption_state_prefetched_ = true;
|
|
|
|
// 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);
|
|
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();
|
|
}
|
|
|
|
void App::onDisconnected(const std::string& reason)
|
|
{
|
|
state_.connected = false;
|
|
state_.clear();
|
|
connection_status_ = reason;
|
|
|
|
// Tear down the fast-lane connection
|
|
if (fast_worker_) {
|
|
fast_worker_->stop();
|
|
}
|
|
if (fast_rpc_) {
|
|
fast_rpc_->disconnect();
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Data Refresh
|
|
// ============================================================================
|
|
|
|
void App::refreshData()
|
|
{
|
|
if (!state_.connected || !rpc_ || !worker_) return;
|
|
|
|
// Prevent overlapping refreshes — skip if one is still running
|
|
if (refresh_in_progress_.exchange(true)) return;
|
|
|
|
// Capture decision flags on the main thread before posting to worker.
|
|
const bool doAddresses = addresses_dirty_;
|
|
const bool doPeers = (current_page_ == ui::NavPage::Peers);
|
|
const bool doEncrypt = !encryption_state_prefetched_;
|
|
if (encryption_state_prefetched_) encryption_state_prefetched_ = false;
|
|
|
|
// P4a: Skip transactions if no new blocks since last full fetch
|
|
const int currentBlocks = state_.sync.blocks;
|
|
const bool doTransactions = (last_tx_block_height_ < 0
|
|
|| currentBlocks != last_tx_block_height_
|
|
|| state_.transactions.empty());
|
|
|
|
// Snapshot z-addresses for transaction fetch (needed on worker thread)
|
|
std::vector<std::string> txZAddrs;
|
|
if (doTransactions) {
|
|
for (const auto& za : state_.z_addresses) {
|
|
if (!za.address.empty()) txZAddrs.push_back(za.address);
|
|
}
|
|
}
|
|
|
|
// P4b: Collect txids that are fully enriched (skip re-enrichment)
|
|
std::set<std::string> fullyEnriched;
|
|
if (doTransactions) {
|
|
for (const auto& tx : state_.transactions) {
|
|
if (tx.confirmations > 6 && tx.timestamp != 0) {
|
|
fullyEnriched.insert(tx.txid);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Single consolidated worker task — all RPC calls happen back-to-back
|
|
// on a single thread with no inter-task queue overhead.
|
|
worker_->post([this, doAddresses, doPeers, doEncrypt, doTransactions,
|
|
currentBlocks, txZAddrs = std::move(txZAddrs),
|
|
fullyEnriched = std::move(fullyEnriched)]() -> rpc::RPCWorker::MainCb {
|
|
// ================================================================
|
|
// Phase 1: Balance + blockchain info
|
|
// ================================================================
|
|
json totalBal, blockInfo;
|
|
bool balOk = false, blockOk = false;
|
|
|
|
try {
|
|
totalBal = rpc_->call("z_gettotalbalance");
|
|
balOk = true;
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("Balance error: %s\n", e.what());
|
|
}
|
|
try {
|
|
blockInfo = rpc_->call("getblockchaininfo");
|
|
blockOk = true;
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
|
}
|
|
|
|
// ================================================================
|
|
// Phase 2: Addresses (only when dirtied)
|
|
// ================================================================
|
|
std::vector<AddressInfo> zAddrs, tAddrs;
|
|
bool addrOk = false;
|
|
if (doAddresses) {
|
|
addrOk = true;
|
|
// z-addresses
|
|
try {
|
|
json zList = rpc_->call("z_listaddresses");
|
|
for (const auto& addr : zList) {
|
|
AddressInfo info;
|
|
info.address = addr.get<std::string>();
|
|
info.type = "shielded";
|
|
zAddrs.push_back(info);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("z_listaddresses error: %s\n", e.what());
|
|
}
|
|
// z-balances via z_listunspent (single call)
|
|
try {
|
|
json unspent = rpc_->call("z_listunspent");
|
|
std::map<std::string, double> zBalances;
|
|
for (const auto& utxo : unspent) {
|
|
if (utxo.contains("address") && utxo.contains("amount")) {
|
|
zBalances[utxo["address"].get<std::string>()] += utxo["amount"].get<double>();
|
|
}
|
|
}
|
|
for (auto& info : zAddrs) {
|
|
auto it = zBalances.find(info.address);
|
|
if (it != zBalances.end()) info.balance = it->second;
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
|
|
for (auto& info : zAddrs) {
|
|
try {
|
|
json bal = rpc_->call("z_getbalance", json::array({info.address}));
|
|
if (!bal.is_null()) info.balance = bal.get<double>();
|
|
} catch (...) {}
|
|
}
|
|
}
|
|
// t-addresses
|
|
try {
|
|
json tList = rpc_->call("getaddressesbyaccount", json::array({""}));
|
|
for (const auto& addr : tList) {
|
|
AddressInfo info;
|
|
info.address = addr.get<std::string>();
|
|
info.type = "transparent";
|
|
tAddrs.push_back(info);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("getaddressesbyaccount error: %s\n", e.what());
|
|
}
|
|
// t-balances via listunspent
|
|
try {
|
|
json utxos = rpc_->call("listunspent");
|
|
std::map<std::string, double> tBalances;
|
|
for (const auto& utxo : utxos) {
|
|
tBalances[utxo["address"].get<std::string>()] += utxo["amount"].get<double>();
|
|
}
|
|
for (auto& info : tAddrs) {
|
|
auto it = tBalances.find(info.address);
|
|
if (it != tBalances.end()) info.balance = it->second;
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// Phase 3: Transactions (only when new blocks)
|
|
// ================================================================
|
|
std::vector<TransactionInfo> txns;
|
|
bool txOk = false;
|
|
if (doTransactions) {
|
|
txOk = true;
|
|
std::set<std::string> knownTxids;
|
|
|
|
// Phase 3a: transparent transactions
|
|
try {
|
|
json result = rpc_->call("listtransactions", json::array({"", 9999}));
|
|
for (const auto& tx : result) {
|
|
TransactionInfo info;
|
|
if (tx.contains("txid")) info.txid = tx["txid"].get<std::string>();
|
|
if (tx.contains("category")) info.type = tx["category"].get<std::string>();
|
|
if (tx.contains("amount")) info.amount = tx["amount"].get<double>();
|
|
if (tx.contains("time")) info.timestamp = tx["time"].get<int64_t>();
|
|
if (tx.contains("confirmations")) info.confirmations = tx["confirmations"].get<int>();
|
|
if (tx.contains("address")) info.address = tx["address"].get<std::string>();
|
|
knownTxids.insert(info.txid);
|
|
txns.push_back(info);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("listtransactions error: %s\n", e.what());
|
|
}
|
|
|
|
// Phase 3b: shielded receives
|
|
for (const auto& addr : txZAddrs) {
|
|
try {
|
|
json zresult = rpc_->call("z_listreceivedbyaddress", json::array({addr, 0}));
|
|
if (zresult.is_null() || !zresult.is_array()) continue;
|
|
for (const auto& note : zresult) {
|
|
std::string txid;
|
|
if (note.contains("txid")) txid = note["txid"].get<std::string>();
|
|
if (txid.empty()) continue;
|
|
if (note.contains("change") && note["change"].get<bool>()) continue;
|
|
bool dominated = false;
|
|
for (const auto& existing : txns) {
|
|
if (existing.txid == txid && existing.type == "receive") {
|
|
dominated = true; break;
|
|
}
|
|
}
|
|
if (dominated) continue;
|
|
TransactionInfo info;
|
|
info.txid = txid;
|
|
info.type = "receive";
|
|
info.address = addr;
|
|
if (note.contains("amount")) info.amount = note["amount"].get<double>();
|
|
if (note.contains("confirmations")) info.confirmations = note["confirmations"].get<int>();
|
|
if (note.contains("time")) info.timestamp = note["time"].get<int64_t>();
|
|
if (note.contains("memoStr")) info.memo = note["memoStr"].get<std::string>();
|
|
knownTxids.insert(txid);
|
|
txns.push_back(info);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("z_listreceivedbyaddress error for %s: %s\n",
|
|
addr.substr(0, 12).c_str(), e.what());
|
|
}
|
|
}
|
|
|
|
// Phase 3c: detect shielded sends via z_viewtransaction
|
|
for (const std::string& txid : knownTxids) {
|
|
if (fullyEnriched.count(txid)) continue;
|
|
try {
|
|
json vtx = rpc_->call("z_viewtransaction", json::array({txid}));
|
|
if (vtx.is_null() || !vtx.is_object()) continue;
|
|
if (vtx.contains("outputs") && vtx["outputs"].is_array()) {
|
|
for (const auto& output : vtx["outputs"]) {
|
|
bool outgoing = false;
|
|
if (output.contains("outgoing"))
|
|
outgoing = output["outgoing"].get<bool>();
|
|
if (!outgoing) continue;
|
|
std::string destAddr;
|
|
if (output.contains("address"))
|
|
destAddr = output["address"].get<std::string>();
|
|
double value = 0.0;
|
|
if (output.contains("value"))
|
|
value = output["value"].get<double>();
|
|
bool alreadyTracked = false;
|
|
for (const auto& existing : txns) {
|
|
if (existing.txid == txid && existing.type == "send"
|
|
&& std::abs(existing.amount + value) < 0.00000001) {
|
|
alreadyTracked = true; break;
|
|
}
|
|
}
|
|
if (alreadyTracked) continue;
|
|
TransactionInfo info;
|
|
info.txid = txid;
|
|
info.type = "send";
|
|
info.address = destAddr;
|
|
info.amount = -value;
|
|
if (output.contains("memoStr"))
|
|
info.memo = output["memoStr"].get<std::string>();
|
|
for (const auto& existing : txns) {
|
|
if (existing.txid == txid) {
|
|
info.confirmations = existing.confirmations;
|
|
info.timestamp = existing.timestamp;
|
|
break;
|
|
}
|
|
}
|
|
if (info.timestamp == 0) {
|
|
try {
|
|
json rawtx = rpc_->call("gettransaction", json::array({txid}));
|
|
if (!rawtx.is_null() && rawtx.contains("time"))
|
|
info.timestamp = rawtx["time"].get<int64_t>();
|
|
if (!rawtx.is_null() && rawtx.contains("confirmations"))
|
|
info.confirmations = rawtx["confirmations"].get<int>();
|
|
} catch (...) {}
|
|
}
|
|
if (vtx.contains("spends") && vtx["spends"].is_array()) {
|
|
for (const auto& spend : vtx["spends"]) {
|
|
if (spend.contains("address")) {
|
|
info.from_address = spend["address"].get<std::string>();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
txns.push_back(info);
|
|
}
|
|
}
|
|
} catch (const std::exception& e) {
|
|
(void)e; // z_viewtransaction may not be available for all txids
|
|
}
|
|
}
|
|
|
|
std::sort(txns.begin(), txns.end(),
|
|
[](const TransactionInfo& a, const TransactionInfo& b) {
|
|
return a.timestamp > b.timestamp;
|
|
});
|
|
}
|
|
|
|
// ================================================================
|
|
// Phase 4: Peers (only when tab is active)
|
|
// ================================================================
|
|
std::vector<PeerInfo> peers;
|
|
std::vector<BannedPeer> bannedPeers;
|
|
bool peerOk = false;
|
|
if (doPeers) {
|
|
peerOk = true;
|
|
try {
|
|
json result = rpc_->call("getpeerinfo");
|
|
for (const auto& peer : result) {
|
|
PeerInfo info;
|
|
if (peer.contains("id")) info.id = peer["id"].get<int>();
|
|
if (peer.contains("addr")) info.addr = peer["addr"].get<std::string>();
|
|
if (peer.contains("subver")) info.subver = peer["subver"].get<std::string>();
|
|
if (peer.contains("services")) info.services = peer["services"].get<std::string>();
|
|
if (peer.contains("version")) info.version = peer["version"].get<int>();
|
|
if (peer.contains("conntime")) info.conntime = peer["conntime"].get<int64_t>();
|
|
if (peer.contains("banscore")) info.banscore = peer["banscore"].get<int>();
|
|
if (peer.contains("pingtime")) info.pingtime = peer["pingtime"].get<double>();
|
|
if (peer.contains("bytessent")) info.bytessent = peer["bytessent"].get<int64_t>();
|
|
if (peer.contains("bytesrecv")) info.bytesrecv = peer["bytesrecv"].get<int64_t>();
|
|
if (peer.contains("startingheight")) info.startingheight = peer["startingheight"].get<int>();
|
|
if (peer.contains("synced_headers")) info.synced_headers = peer["synced_headers"].get<int>();
|
|
if (peer.contains("synced_blocks")) info.synced_blocks = peer["synced_blocks"].get<int>();
|
|
if (peer.contains("inbound")) info.inbound = peer["inbound"].get<bool>();
|
|
if (peer.contains("tls_cipher")) info.tls_cipher = peer["tls_cipher"].get<std::string>();
|
|
if (peer.contains("tls_verified")) info.tls_verified = peer["tls_verified"].get<bool>();
|
|
peers.push_back(info);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("getPeerInfo error: %s\n", e.what());
|
|
}
|
|
try {
|
|
json result = rpc_->call("listbanned");
|
|
for (const auto& ban : result) {
|
|
BannedPeer info;
|
|
if (ban.contains("address")) info.address = ban["address"].get<std::string>();
|
|
if (ban.contains("banned_until")) info.banned_until = ban["banned_until"].get<int64_t>();
|
|
bannedPeers.push_back(info);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("listBanned error: %s\n", e.what());
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// Phase 5: Wallet encryption state
|
|
// ================================================================
|
|
json walletInfo;
|
|
bool encryptOk = false;
|
|
if (doEncrypt) {
|
|
try {
|
|
walletInfo = rpc_->call("getwalletinfo");
|
|
encryptOk = true;
|
|
} catch (...) {}
|
|
}
|
|
|
|
// ================================================================
|
|
// Single main-thread callback — apply ALL results at once
|
|
// ================================================================
|
|
return [this, totalBal, blockInfo, balOk, blockOk,
|
|
zAddrs = std::move(zAddrs), tAddrs = std::move(tAddrs), addrOk,
|
|
txns = std::move(txns), txOk, currentBlocks,
|
|
peers = std::move(peers), bannedPeers = std::move(bannedPeers), peerOk,
|
|
walletInfo, encryptOk]() {
|
|
// --- Balance ---
|
|
try {
|
|
if (balOk) {
|
|
if (totalBal.contains("private"))
|
|
state_.shielded_balance = std::stod(totalBal["private"].get<std::string>());
|
|
if (totalBal.contains("transparent"))
|
|
state_.transparent_balance = std::stod(totalBal["transparent"].get<std::string>());
|
|
if (totalBal.contains("total"))
|
|
state_.total_balance = std::stod(totalBal["total"].get<std::string>());
|
|
state_.last_balance_update = std::time(nullptr);
|
|
}
|
|
if (blockOk) {
|
|
if (blockInfo.contains("blocks"))
|
|
state_.sync.blocks = blockInfo["blocks"].get<int>();
|
|
if (blockInfo.contains("headers"))
|
|
state_.sync.headers = blockInfo["headers"].get<int>();
|
|
if (blockInfo.contains("verificationprogress"))
|
|
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
|
|
state_.sync.syncing = (state_.sync.blocks < state_.sync.headers - 2);
|
|
if (blockInfo.contains("longestchain"))
|
|
state_.longestchain = blockInfo["longestchain"].get<int>();
|
|
if (blockInfo.contains("notarized"))
|
|
state_.notarized = blockInfo["notarized"].get<int>();
|
|
}
|
|
// Auto-shield transparent funds if enabled
|
|
if (balOk && 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("[refreshData] balance callback error: %s\n", e.what());
|
|
}
|
|
|
|
// --- Addresses ---
|
|
if (addrOk) {
|
|
state_.z_addresses = std::move(zAddrs);
|
|
state_.t_addresses = std::move(tAddrs);
|
|
address_list_dirty_ = true;
|
|
addresses_dirty_ = false;
|
|
}
|
|
|
|
// --- Transactions ---
|
|
if (txOk) {
|
|
state_.transactions = std::move(txns);
|
|
state_.last_tx_update = std::time(nullptr);
|
|
last_tx_block_height_ = currentBlocks;
|
|
}
|
|
|
|
// --- Peers ---
|
|
if (peerOk) {
|
|
state_.peers = std::move(peers);
|
|
state_.bannedPeers = std::move(bannedPeers);
|
|
state_.last_peer_update = std::time(nullptr);
|
|
}
|
|
|
|
// --- Encryption state ---
|
|
if (encryptOk) {
|
|
try {
|
|
if (walletInfo.contains("unlocked_until")) {
|
|
state_.encrypted = true;
|
|
int64_t until = walletInfo["unlocked_until"].get<int64_t>();
|
|
state_.unlocked_until = until;
|
|
state_.locked = (until == 0);
|
|
} else {
|
|
state_.encrypted = false;
|
|
state_.locked = false;
|
|
state_.unlocked_until = 0;
|
|
}
|
|
state_.encryption_state_known = true;
|
|
} catch (...) {}
|
|
}
|
|
|
|
refresh_in_progress_.store(false, std::memory_order_release);
|
|
};
|
|
});
|
|
}
|
|
|
|
void App::refreshBalance()
|
|
{
|
|
if (!worker_ || !rpc_) return;
|
|
|
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
|
// --- Worker thread: do blocking RPC ---
|
|
json totalBal, blockInfo;
|
|
bool balOk = false, blockOk = false;
|
|
|
|
try {
|
|
totalBal = rpc_->call("z_gettotalbalance");
|
|
balOk = true;
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("Balance error: %s\n", e.what());
|
|
}
|
|
|
|
try {
|
|
blockInfo = rpc_->call("getblockchaininfo");
|
|
blockOk = true;
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
|
}
|
|
|
|
// --- Main thread: apply results to state_ ---
|
|
return [this, totalBal, blockInfo, balOk, blockOk]() {
|
|
try {
|
|
if (balOk) {
|
|
if (totalBal.contains("private"))
|
|
state_.shielded_balance = std::stod(totalBal["private"].get<std::string>());
|
|
if (totalBal.contains("transparent"))
|
|
state_.transparent_balance = std::stod(totalBal["transparent"].get<std::string>());
|
|
if (totalBal.contains("total"))
|
|
state_.total_balance = std::stod(totalBal["total"].get<std::string>());
|
|
state_.last_balance_update = std::time(nullptr);
|
|
}
|
|
if (blockOk) {
|
|
if (blockInfo.contains("blocks"))
|
|
state_.sync.blocks = blockInfo["blocks"].get<int>();
|
|
if (blockInfo.contains("headers"))
|
|
state_.sync.headers = blockInfo["headers"].get<int>();
|
|
if (blockInfo.contains("verificationprogress"))
|
|
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
|
|
state_.sync.syncing = (state_.sync.blocks < state_.sync.headers - 2);
|
|
|
|
// Consolidate chain-tip fields that were previously fetched
|
|
// via a separate getinfo call in refreshMiningInfo.
|
|
if (blockInfo.contains("longestchain"))
|
|
state_.longestchain = blockInfo["longestchain"].get<int>();
|
|
if (blockInfo.contains("notarized"))
|
|
state_.notarized = blockInfo["notarized"].get<int>();
|
|
}
|
|
|
|
// Auto-shield transparent funds if enabled
|
|
if (balOk && settings_ && settings_->getAutoShield() &&
|
|
state_.transparent_balance > 0.0001 && !state_.sync.syncing &&
|
|
!auto_shield_pending_.exchange(true)) {
|
|
// Find first shielded address as target
|
|
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("[refreshBalance] callback error: %s\n", e.what());
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
void App::refreshAddresses()
|
|
{
|
|
if (!worker_ || !rpc_) return;
|
|
|
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
|
// --- Worker thread: fetch all address data with minimal RPC calls ---
|
|
std::vector<AddressInfo> zAddrs, tAddrs;
|
|
|
|
// 1. Get z-addresses
|
|
try {
|
|
json zList = rpc_->call("z_listaddresses");
|
|
for (const auto& addr : zList) {
|
|
AddressInfo info;
|
|
info.address = addr.get<std::string>();
|
|
info.type = "shielded";
|
|
zAddrs.push_back(info);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("z_listaddresses error: %s\n", e.what());
|
|
}
|
|
|
|
// 2. P3: Use z_listUnspent (single call) instead of per-addr z_getbalance
|
|
try {
|
|
json unspent = rpc_->call("z_listunspent");
|
|
std::map<std::string, double> zBalances;
|
|
for (const auto& utxo : unspent) {
|
|
if (utxo.contains("address") && utxo.contains("amount")) {
|
|
zBalances[utxo["address"].get<std::string>()] += utxo["amount"].get<double>();
|
|
}
|
|
}
|
|
for (auto& info : zAddrs) {
|
|
auto it = zBalances.find(info.address);
|
|
if (it != zBalances.end()) {
|
|
info.balance = it->second;
|
|
}
|
|
}
|
|
} catch (const std::exception& e) {
|
|
// Fallback: z_listUnspent might not be available — use batched z_getbalance
|
|
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
|
|
for (auto& info : zAddrs) {
|
|
try {
|
|
json bal = rpc_->call("z_getbalance", json::array({info.address}));
|
|
if (!bal.is_null()) info.balance = bal.get<double>();
|
|
} catch (...) {}
|
|
}
|
|
}
|
|
|
|
// 3. Get t-addresses
|
|
try {
|
|
json tList = rpc_->call("getaddressesbyaccount", json::array({""}));
|
|
for (const auto& addr : tList) {
|
|
AddressInfo info;
|
|
info.address = addr.get<std::string>();
|
|
info.type = "transparent";
|
|
tAddrs.push_back(info);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("getaddressesbyaccount error: %s\n", e.what());
|
|
}
|
|
|
|
// 4. Get unspent for t-address balances
|
|
try {
|
|
json utxos = rpc_->call("listunspent");
|
|
std::map<std::string, double> tBalances;
|
|
for (const auto& utxo : utxos) {
|
|
tBalances[utxo["address"].get<std::string>()] += utxo["amount"].get<double>();
|
|
}
|
|
for (auto& info : tAddrs) {
|
|
auto it = tBalances.find(info.address);
|
|
if (it != tBalances.end()) info.balance = it->second;
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
|
}
|
|
|
|
// --- Main thread: apply results and rebuild address list once ---
|
|
return [this, zAddrs = std::move(zAddrs), tAddrs = std::move(tAddrs)]() {
|
|
state_.z_addresses = std::move(zAddrs);
|
|
state_.t_addresses = std::move(tAddrs);
|
|
// P8: single rebuild via dirty flag (drains in update())
|
|
address_list_dirty_ = true;
|
|
// Addresses fetched successfully — clear the demand flag
|
|
addresses_dirty_ = false;
|
|
};
|
|
});
|
|
}
|
|
|
|
void App::refreshTransactions()
|
|
{
|
|
if (!worker_ || !rpc_) return;
|
|
|
|
// P4a: Skip if no new blocks since last full fetch
|
|
int currentBlocks = state_.sync.blocks;
|
|
bool fullRefresh = (last_tx_block_height_ < 0 || currentBlocks != last_tx_block_height_
|
|
|| state_.transactions.empty());
|
|
if (!fullRefresh) return;
|
|
|
|
// Capture the z-addresses list for the worker thread
|
|
std::vector<std::string> zAddrs;
|
|
for (const auto& za : state_.z_addresses) {
|
|
if (!za.address.empty()) zAddrs.push_back(za.address);
|
|
}
|
|
|
|
worker_->post([this, zAddrs = std::move(zAddrs), currentBlocks]() -> rpc::RPCWorker::MainCb {
|
|
// --- Worker thread: all blocking RPC calls happen here ---
|
|
std::vector<TransactionInfo> txns;
|
|
std::set<std::string> knownTxids;
|
|
|
|
// P4b: Collect txids that are fully enriched (skip re-enrichment)
|
|
std::set<std::string> fullyEnriched;
|
|
for (const auto& tx : state_.transactions) {
|
|
if (tx.confirmations > 6 && tx.timestamp != 0) {
|
|
fullyEnriched.insert(tx.txid);
|
|
}
|
|
}
|
|
|
|
// ---- Phase 1: transparent transactions from listtransactions ----
|
|
try {
|
|
json result = rpc_->call("listtransactions", json::array({"", 9999}));
|
|
for (const auto& tx : result) {
|
|
TransactionInfo info;
|
|
if (tx.contains("txid")) info.txid = tx["txid"].get<std::string>();
|
|
if (tx.contains("category")) info.type = tx["category"].get<std::string>();
|
|
if (tx.contains("amount")) info.amount = tx["amount"].get<double>();
|
|
if (tx.contains("time")) info.timestamp = tx["time"].get<int64_t>();
|
|
if (tx.contains("confirmations")) info.confirmations = tx["confirmations"].get<int>();
|
|
if (tx.contains("address")) info.address = tx["address"].get<std::string>();
|
|
knownTxids.insert(info.txid);
|
|
txns.push_back(info);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("listtransactions error: %s\n", e.what());
|
|
}
|
|
|
|
// ---- Phase 2: shielded receives via z_listreceivedbyaddress ----
|
|
for (const auto& addr : zAddrs) {
|
|
try {
|
|
json zresult = rpc_->call("z_listreceivedbyaddress", json::array({addr, 0}));
|
|
if (zresult.is_null() || !zresult.is_array()) continue;
|
|
|
|
for (const auto& note : zresult) {
|
|
std::string txid;
|
|
if (note.contains("txid")) txid = note["txid"].get<std::string>();
|
|
if (txid.empty()) continue;
|
|
if (note.contains("change") && note["change"].get<bool>()) continue;
|
|
|
|
bool dominated = false;
|
|
for (const auto& existing : txns) {
|
|
if (existing.txid == txid && existing.type == "receive") {
|
|
dominated = true; break;
|
|
}
|
|
}
|
|
if (dominated) continue;
|
|
|
|
TransactionInfo info;
|
|
info.txid = txid;
|
|
info.type = "receive";
|
|
info.address = addr;
|
|
if (note.contains("amount")) info.amount = note["amount"].get<double>();
|
|
if (note.contains("confirmations")) info.confirmations = note["confirmations"].get<int>();
|
|
if (note.contains("time")) info.timestamp = note["time"].get<int64_t>();
|
|
if (note.contains("memoStr")) info.memo = note["memoStr"].get<std::string>();
|
|
knownTxids.insert(txid);
|
|
txns.push_back(info);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("z_listreceivedbyaddress error for %s: %s\n",
|
|
addr.substr(0, 12).c_str(), e.what());
|
|
}
|
|
}
|
|
|
|
// ---- Phase 3: detect shielded sends via z_viewtransaction ----
|
|
// P4d: Only check new/unconfirmed txids
|
|
for (const std::string& txid : knownTxids) {
|
|
if (fullyEnriched.count(txid)) continue; // P4b: skip already-enriched
|
|
|
|
try {
|
|
json vtx = rpc_->call("z_viewtransaction", json::array({txid}));
|
|
if (vtx.is_null() || !vtx.is_object()) continue;
|
|
|
|
if (vtx.contains("outputs") && vtx["outputs"].is_array()) {
|
|
for (const auto& output : vtx["outputs"]) {
|
|
bool outgoing = false;
|
|
if (output.contains("outgoing"))
|
|
outgoing = output["outgoing"].get<bool>();
|
|
if (!outgoing) continue;
|
|
|
|
std::string destAddr;
|
|
if (output.contains("address"))
|
|
destAddr = output["address"].get<std::string>();
|
|
double value = 0.0;
|
|
if (output.contains("value"))
|
|
value = output["value"].get<double>();
|
|
|
|
bool alreadyTracked = false;
|
|
for (const auto& existing : txns) {
|
|
if (existing.txid == txid && existing.type == "send"
|
|
&& std::abs(existing.amount + value) < 0.00000001) {
|
|
alreadyTracked = true; break;
|
|
}
|
|
}
|
|
if (alreadyTracked) continue;
|
|
|
|
TransactionInfo info;
|
|
info.txid = txid;
|
|
info.type = "send";
|
|
info.address = destAddr;
|
|
info.amount = -value;
|
|
if (output.contains("memoStr"))
|
|
info.memo = output["memoStr"].get<std::string>();
|
|
|
|
// Get confirmations/time from existing entry
|
|
for (const auto& existing : txns) {
|
|
if (existing.txid == txid) {
|
|
info.confirmations = existing.confirmations;
|
|
info.timestamp = existing.timestamp;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (info.timestamp == 0) {
|
|
try {
|
|
json rawtx = rpc_->call("gettransaction", json::array({txid}));
|
|
if (!rawtx.is_null() && rawtx.contains("time"))
|
|
info.timestamp = rawtx["time"].get<int64_t>();
|
|
if (!rawtx.is_null() && rawtx.contains("confirmations"))
|
|
info.confirmations = rawtx["confirmations"].get<int>();
|
|
} catch (...) {}
|
|
}
|
|
|
|
if (vtx.contains("spends") && vtx["spends"].is_array()) {
|
|
for (const auto& spend : vtx["spends"]) {
|
|
if (spend.contains("address")) {
|
|
info.from_address = spend["address"].get<std::string>();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
txns.push_back(info);
|
|
}
|
|
}
|
|
} catch (const std::exception& e) {
|
|
// z_viewtransaction may not be available for all txids
|
|
}
|
|
}
|
|
|
|
// Sort by timestamp descending
|
|
std::sort(txns.begin(), txns.end(),
|
|
[](const TransactionInfo& a, const TransactionInfo& b) {
|
|
return a.timestamp > b.timestamp;
|
|
});
|
|
|
|
// --- Main thread: apply results ---
|
|
return [this, txns = std::move(txns), currentBlocks]() {
|
|
state_.transactions = std::move(txns);
|
|
state_.last_tx_update = std::time(nullptr);
|
|
last_tx_block_height_ = currentBlocks; // P4a: track last-fetched height
|
|
};
|
|
});
|
|
}
|
|
|
|
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;
|
|
|
|
// Prevent worker queue pileup — skip if previous refresh hasn't finished
|
|
if (mining_refresh_in_progress_.exchange(true)) return;
|
|
|
|
// Capture daemon memory outside (may be accessed on main thread)
|
|
double daemonMemMb = 0.0;
|
|
if (embedded_daemon_) {
|
|
daemonMemMb = embedded_daemon_->getMemoryUsageMB();
|
|
}
|
|
|
|
// 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);
|
|
|
|
w->post([this, rpc, daemonMemMb, doSlowRefresh]() -> rpc::RPCWorker::MainCb {
|
|
json miningInfo, localHashrateJson;
|
|
bool miningOk = false, hashrateOk = false;
|
|
|
|
// Fast path: only getlocalsolps (single RPC call, ~1ms) — returns H/s (RandomX)
|
|
try {
|
|
localHashrateJson = rpc->call("getlocalsolps");
|
|
hashrateOk = true;
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("getLocalHashrate error: %s\n", e.what());
|
|
}
|
|
|
|
// Slow path: getmininginfo every ~5s
|
|
if (doSlowRefresh) {
|
|
try {
|
|
miningInfo = rpc->call("getmininginfo");
|
|
miningOk = true;
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("getMiningInfo error: %s\n", e.what());
|
|
}
|
|
}
|
|
|
|
return [this, miningInfo, localHashrateJson, miningOk, hashrateOk, daemonMemMb]() {
|
|
try {
|
|
if (hashrateOk) {
|
|
state_.mining.localHashrate = localHashrateJson.get<double>();
|
|
state_.mining.hashrate_history.push_back(state_.mining.localHashrate);
|
|
if (state_.mining.hashrate_history.size() > MiningInfo::MAX_HISTORY) {
|
|
state_.mining.hashrate_history.erase(state_.mining.hashrate_history.begin());
|
|
}
|
|
}
|
|
if (miningOk) {
|
|
if (miningInfo.contains("generate"))
|
|
state_.mining.generate = miningInfo["generate"].get<bool>();
|
|
if (miningInfo.contains("genproclimit"))
|
|
state_.mining.genproclimit = miningInfo["genproclimit"].get<int>();
|
|
if (miningInfo.contains("blocks"))
|
|
state_.mining.blocks = miningInfo["blocks"].get<int>();
|
|
if (miningInfo.contains("difficulty"))
|
|
state_.mining.difficulty = miningInfo["difficulty"].get<double>();
|
|
if (miningInfo.contains("networkhashps"))
|
|
state_.mining.networkHashrate = miningInfo["networkhashps"].get<double>();
|
|
if (miningInfo.contains("chain"))
|
|
state_.mining.chain = miningInfo["chain"].get<std::string>();
|
|
state_.last_mining_update = std::time(nullptr);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("[refreshMiningInfo] callback error: %s\n", e.what());
|
|
}
|
|
state_.mining.daemon_memory_mb = daemonMemMb;
|
|
mining_refresh_in_progress_.store(false);
|
|
};
|
|
});
|
|
}
|
|
|
|
void App::refreshPeerInfo()
|
|
{
|
|
if (!worker_ || !rpc_) return;
|
|
|
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
|
std::vector<PeerInfo> peers;
|
|
std::vector<BannedPeer> bannedPeers;
|
|
|
|
try {
|
|
json result = rpc_->call("getpeerinfo");
|
|
for (const auto& peer : result) {
|
|
PeerInfo info;
|
|
if (peer.contains("id")) info.id = peer["id"].get<int>();
|
|
if (peer.contains("addr")) info.addr = peer["addr"].get<std::string>();
|
|
if (peer.contains("subver")) info.subver = peer["subver"].get<std::string>();
|
|
if (peer.contains("services")) info.services = peer["services"].get<std::string>();
|
|
if (peer.contains("version")) info.version = peer["version"].get<int>();
|
|
if (peer.contains("conntime")) info.conntime = peer["conntime"].get<int64_t>();
|
|
if (peer.contains("banscore")) info.banscore = peer["banscore"].get<int>();
|
|
if (peer.contains("pingtime")) info.pingtime = peer["pingtime"].get<double>();
|
|
if (peer.contains("bytessent")) info.bytessent = peer["bytessent"].get<int64_t>();
|
|
if (peer.contains("bytesrecv")) info.bytesrecv = peer["bytesrecv"].get<int64_t>();
|
|
if (peer.contains("startingheight")) info.startingheight = peer["startingheight"].get<int>();
|
|
if (peer.contains("synced_headers")) info.synced_headers = peer["synced_headers"].get<int>();
|
|
if (peer.contains("synced_blocks")) info.synced_blocks = peer["synced_blocks"].get<int>();
|
|
if (peer.contains("inbound")) info.inbound = peer["inbound"].get<bool>();
|
|
if (peer.contains("tls_cipher")) info.tls_cipher = peer["tls_cipher"].get<std::string>();
|
|
if (peer.contains("tls_verified")) info.tls_verified = peer["tls_verified"].get<bool>();
|
|
peers.push_back(info);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("getPeerInfo error: %s\n", e.what());
|
|
}
|
|
|
|
try {
|
|
json result = rpc_->call("listbanned");
|
|
for (const auto& ban : result) {
|
|
BannedPeer info;
|
|
if (ban.contains("address")) info.address = ban["address"].get<std::string>();
|
|
if (ban.contains("banned_until")) info.banned_until = ban["banned_until"].get<int64_t>();
|
|
bannedPeers.push_back(info);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("listBanned error: %s\n", e.what());
|
|
}
|
|
|
|
return [this, peers = std::move(peers), bannedPeers = std::move(bannedPeers)]() {
|
|
state_.peers = std::move(peers);
|
|
state_.bannedPeers = std::move(bannedPeers);
|
|
state_.last_peer_update = std::time(nullptr);
|
|
};
|
|
});
|
|
}
|
|
|
|
void App::refreshPrice()
|
|
{
|
|
// Skip if price fetching is disabled
|
|
if (!settings_->getFetchPrices()) return;
|
|
if (!worker_) return;
|
|
|
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
|
// --- Worker thread: blocking HTTP GET to CoinGecko ---
|
|
MarketInfo market;
|
|
bool ok = false;
|
|
|
|
try {
|
|
CURL* curl = curl_easy_init();
|
|
if (!curl) {
|
|
DEBUG_LOGF("Failed to initialize curl for price fetch\n");
|
|
return nullptr;
|
|
}
|
|
|
|
std::string response_data;
|
|
const char* url = "https://api.coingecko.com/api/v3/simple/price?ids=hush&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);
|
|
|
|
if (res == CURLE_OK && http_code == 200) {
|
|
auto j = json::parse(response_data);
|
|
if (j.contains("hush")) {
|
|
const auto& data = j["hush"];
|
|
market.price_usd = data.value("usd", 0.0);
|
|
market.price_btc = data.value("btc", 0.0);
|
|
market.change_24h = data.value("usd_24h_change", 0.0);
|
|
market.volume_24h = data.value("usd_24h_vol", 0.0);
|
|
market.market_cap = data.value("usd_market_cap", 0.0);
|
|
|
|
auto now = std::chrono::system_clock::now();
|
|
auto time_t = std::chrono::system_clock::to_time_t(now);
|
|
char buf[64];
|
|
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", localtime(&time_t));
|
|
market.last_updated = buf;
|
|
ok = true;
|
|
DEBUG_LOGF("Price updated: $%.6f USD\n", market.price_usd);
|
|
}
|
|
} else {
|
|
DEBUG_LOGF("Price fetch failed: %s (HTTP %ld)\n",
|
|
res != CURLE_OK ? curl_easy_strerror(res) : "OK", http_code);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("Price fetch error: %s\n", e.what());
|
|
}
|
|
|
|
if (!ok) return nullptr;
|
|
|
|
return [this, market]() {
|
|
state_.market.price_usd = market.price_usd;
|
|
state_.market.price_btc = market.price_btc;
|
|
state_.market.change_24h = market.change_24h;
|
|
state_.market.volume_24h = market.volume_24h;
|
|
state_.market.market_cap = market.market_cap;
|
|
state_.market.last_updated = market.last_updated;
|
|
|
|
state_.market.price_history.push_back(market.price_usd);
|
|
if (state_.market.price_history.size() > MarketInfo::MAX_HISTORY) {
|
|
state_.market.price_history.erase(state_.market.price_history.begin());
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
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 transparent address as the mining wallet address
|
|
for (const auto& addr : state_.addresses) {
|
|
if (addr.type == "transparent" && !addr.address.empty()) {
|
|
cfg.wallet_address = addr.address;
|
|
break;
|
|
}
|
|
}
|
|
if (cfg.wallet_address.empty() && !state_.z_addresses.empty()) {
|
|
cfg.wallet_address = state_.z_addresses[0].address;
|
|
}
|
|
|
|
if (cfg.wallet_address.empty()) {
|
|
DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n");
|
|
ui::Notifications::instance().error("No wallet address available for pool mining");
|
|
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();
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Address Operations
|
|
// ============================================================================
|
|
|
|
void App::createNewZAddress(std::function<void(const std::string&)> callback)
|
|
{
|
|
if (!state_.connected || !rpc_) return;
|
|
|
|
rpc_->z_getNewAddress([this, callback](const json& result) {
|
|
std::string addr = result.get<std::string>();
|
|
addresses_dirty_ = true;
|
|
refreshAddresses();
|
|
if (callback) callback(addr);
|
|
});
|
|
}
|
|
|
|
void App::createNewTAddress(std::function<void(const std::string&)> callback)
|
|
{
|
|
if (!state_.connected || !rpc_) return;
|
|
|
|
rpc_->getNewAddress([this, callback](const json& result) {
|
|
std::string addr = result.get<std::string>();
|
|
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);
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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;
|
|
}
|
|
|
|
// Check if it's a z-address or t-address
|
|
if (address.length() > 0 && address[0] == 'z') {
|
|
// 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;
|
|
}
|
|
|
|
// Detect key type based on prefix
|
|
bool is_zkey = (key.length() > 0 && key[0] == 's'); // z-address keys start with 'secret-extended-key'
|
|
|
|
if (is_zkey) {
|
|
rpc_->z_importKey(key, true, [this, callback](const json& result) {
|
|
refreshAddresses();
|
|
if (callback) callback(true, "Z-address key imported successfully. Wallet is rescanning.");
|
|
}, [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, "T-address key imported successfully. Wallet is rescanning.");
|
|
}, [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;
|
|
}
|
|
|
|
// Build recipients array
|
|
nlohmann::json recipients = nlohmann::json::array();
|
|
nlohmann::json recipient;
|
|
recipient["address"] = to;
|
|
// Format amount to exactly 8 decimal places (satoshi precision).
|
|
// Sending a raw double can produce 15+ decimal digits which the
|
|
// daemon's ParseFixedPoint rejects with "Invalid amount".
|
|
char amt_buf[32];
|
|
snprintf(amt_buf, sizeof(amt_buf), "%.8f", amount);
|
|
recipient["amount"] = std::string(amt_buf);
|
|
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;
|
|
}
|
|
if (callback) callback(ok, result_str);
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
} // namespace dragonx
|