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.
This commit is contained in:
2026-04-29 12:47:57 -05:00
parent ee8a08e569
commit 9edab31728
95 changed files with 8776 additions and 37563 deletions

View File

@@ -0,0 +1,976 @@
#include "network_refresh_service.h"
#include "../util/logger.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <map>
#include <set>
using json = nlohmann::json;
namespace dragonx {
namespace services {
namespace {
template <typename T>
std::optional<T> readOptional(const json& source, const char* key)
{
try {
if (source.contains(key)) return source[key].get<T>();
} catch (...) {}
return std::nullopt;
}
std::optional<double> readBalanceString(const json& source, const char* key)
{
try {
if (source.contains(key)) return std::stod(source[key].get<std::string>());
} catch (...) {}
return std::nullopt;
}
void applyBalancesFromUnspent(std::vector<AddressInfo>& addresses, const json& unspent)
{
if (!unspent.is_array()) return;
std::map<std::string, double> balances;
for (const auto& output : unspent) {
auto address = readOptional<std::string>(output, "address");
auto amount = readOptional<double>(output, "amount");
if (address && amount) balances[*address] += *amount;
}
for (auto& info : addresses) {
auto balance = balances.find(info.address);
if (balance != balances.end()) info.balance = balance->second;
}
}
} // namespace
NetworkRefreshService::ConnectionInfoResult NetworkRefreshService::parseConnectionInfoResult(const json& info)
{
ConnectionInfoResult result;
if (!info.is_object()) return result;
result.ok = true;
result.daemonVersion = readOptional<int>(info, "version");
result.protocolVersion = readOptional<int>(info, "protocolversion");
result.p2pPort = readOptional<int>(info, "p2pport");
result.longestChain = readOptional<int>(info, "longestchain");
result.notarized = readOptional<int>(info, "notarized");
result.blocks = readOptional<int>(info, "blocks");
return result;
}
NetworkRefreshService::WalletEncryptionResult NetworkRefreshService::parseWalletEncryptionResult(const json& walletInfo)
{
WalletEncryptionResult result;
if (!walletInfo.is_object()) return result;
result.ok = true;
result.encrypted = walletInfo.contains("unlocked_until");
if (result.encrypted) {
try {
result.unlockedUntil = walletInfo["unlocked_until"].get<std::int64_t>();
} catch (...) {
result.ok = false;
}
}
return result;
}
NetworkRefreshService::WarmupPollResult NetworkRefreshService::collectWarmupPollResult(RefreshRpcGateway& rpc)
{
WarmupPollResult result;
try {
json info = rpc.call("getinfo", json::array());
result.ready = true;
result.info = parseConnectionInfoResult(info);
} catch (const std::exception& e) {
result.errorMessage = e.what();
}
return result;
}
NetworkRefreshService::ConnectionInitResult NetworkRefreshService::collectConnectionInitResult(RefreshRpcGateway& rpc)
{
ConnectionInitResult result;
try {
json info = rpc.call("getinfo", json::array());
result.info = parseConnectionInfoResult(info);
} catch (...) {}
try {
json walletInfo = rpc.call("getwalletinfo", json::array());
result.encryption = parseWalletEncryptionResult(walletInfo);
} catch (...) {}
return result;
}
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefreshResult(
const json& totalBalance, bool balanceOk, const json& blockInfo, bool blockOk)
{
CoreRefreshResult result;
result.balanceOk = balanceOk && totalBalance.is_object();
if (result.balanceOk) {
result.shieldedBalance = readBalanceString(totalBalance, "private");
result.transparentBalance = readBalanceString(totalBalance, "transparent");
result.totalBalance = readBalanceString(totalBalance, "total");
}
result.blockchainOk = blockOk && blockInfo.is_object();
if (result.blockchainOk) {
result.blocks = readOptional<int>(blockInfo, "blocks");
result.headers = readOptional<int>(blockInfo, "headers");
result.verificationProgress = readOptional<double>(blockInfo, "verificationprogress");
result.longestChain = readOptional<int>(blockInfo, "longestchain");
result.notarized = readOptional<int>(blockInfo, "notarized");
}
return result;
}
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefreshResult(RefreshRpcGateway& rpc)
{
json totalBalance;
json blockInfo;
bool balanceOk = false;
bool blockOk = false;
try {
totalBalance = rpc.call("z_gettotalbalance", json::array());
balanceOk = true;
} catch (const std::exception& e) {
DEBUG_LOGF("Balance error: %s\n", e.what());
}
try {
blockInfo = rpc.call("getblockchaininfo", json::array());
blockOk = true;
} catch (const std::exception& e) {
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
}
return parseCoreRefreshResult(totalBalance, balanceOk, blockInfo, blockOk);
}
NetworkRefreshService::MiningRefreshResult NetworkRefreshService::parseMiningRefreshResult(
const json& miningInfo, bool miningOk, const json& localHashrate, bool hashrateOk, double daemonMemoryMb)
{
MiningRefreshResult result;
result.daemonMemoryMb = daemonMemoryMb;
if (hashrateOk) {
try {
result.localHashrate = localHashrate.get<double>();
} catch (...) {}
}
result.miningOk = miningOk && miningInfo.is_object();
if (result.miningOk) {
result.generate = readOptional<bool>(miningInfo, "generate");
result.genproclimit = readOptional<int>(miningInfo, "genproclimit");
result.blocks = readOptional<int>(miningInfo, "blocks");
result.difficulty = readOptional<double>(miningInfo, "difficulty");
result.networkHashrate = readOptional<double>(miningInfo, "networkhashps");
result.chain = readOptional<std::string>(miningInfo, "chain");
}
return result;
}
NetworkRefreshService::MiningRefreshResult NetworkRefreshService::collectMiningRefreshResult(
RefreshRpcGateway& rpc,
double daemonMemoryMb,
bool includeSlowRefresh)
{
json miningInfo;
json localHashrate;
bool miningOk = false;
bool hashrateOk = false;
try {
localHashrate = rpc.call("getlocalsolps", json::array());
hashrateOk = true;
} catch (const std::exception& e) {
DEBUG_LOGF("getLocalHashrate error: %s\n", e.what());
}
if (includeSlowRefresh) {
try {
miningInfo = rpc.call("getmininginfo", json::array());
miningOk = true;
} catch (const std::exception& e) {
DEBUG_LOGF("getMiningInfo error: %s\n", e.what());
}
}
return parseMiningRefreshResult(miningInfo, miningOk, localHashrate, hashrateOk, daemonMemoryMb);
}
NetworkRefreshService::PeerRefreshResult NetworkRefreshService::parsePeerRefreshResult(
const json& peers, const json& bannedPeers)
{
PeerRefreshResult result;
if (peers.is_array()) {
for (const auto& peer : peers) {
PeerInfo info;
if (auto value = readOptional<int>(peer, "id")) info.id = *value;
if (auto value = readOptional<std::string>(peer, "addr")) info.addr = *value;
if (auto value = readOptional<std::string>(peer, "subver")) info.subver = *value;
if (auto value = readOptional<std::string>(peer, "services")) info.services = *value;
if (auto value = readOptional<int>(peer, "version")) info.version = *value;
if (auto value = readOptional<std::int64_t>(peer, "conntime")) info.conntime = *value;
if (auto value = readOptional<int>(peer, "banscore")) info.banscore = *value;
if (auto value = readOptional<double>(peer, "pingtime")) info.pingtime = *value;
if (auto value = readOptional<std::int64_t>(peer, "bytessent")) info.bytessent = *value;
if (auto value = readOptional<std::int64_t>(peer, "bytesrecv")) info.bytesrecv = *value;
if (auto value = readOptional<int>(peer, "startingheight")) info.startingheight = *value;
if (auto value = readOptional<int>(peer, "synced_headers")) info.synced_headers = *value;
if (auto value = readOptional<int>(peer, "synced_blocks")) info.synced_blocks = *value;
if (auto value = readOptional<bool>(peer, "inbound")) info.inbound = *value;
if (auto value = readOptional<std::string>(peer, "tls_cipher")) info.tls_cipher = *value;
if (auto value = readOptional<bool>(peer, "tls_verified")) info.tls_verified = *value;
result.peers.push_back(std::move(info));
}
}
if (bannedPeers.is_array()) {
for (const auto& ban : bannedPeers) {
BannedPeer info;
if (auto value = readOptional<std::string>(ban, "address")) info.address = *value;
if (auto value = readOptional<std::int64_t>(ban, "banned_until")) info.banned_until = *value;
result.bannedPeers.push_back(std::move(info));
}
}
return result;
}
NetworkRefreshService::PeerRefreshResult NetworkRefreshService::collectPeerRefreshResult(RefreshRpcGateway& rpc)
{
json peers = json::array();
json bannedPeers = json::array();
bool peersOk = false;
bool bannedOk = false;
try {
peers = rpc.call("getpeerinfo", json::array());
peersOk = true;
} catch (const std::exception& e) {
DEBUG_LOGF("getPeerInfo error: %s\n", e.what());
}
try {
bannedPeers = rpc.call("listbanned", json::array());
bannedOk = true;
} catch (const std::exception& e) {
DEBUG_LOGF("listBanned error: %s\n", e.what());
}
return parsePeerRefreshResult(peersOk ? peers : json::array(),
bannedOk ? bannedPeers : json::array());
}
std::optional<NetworkRefreshService::PriceRefreshResult> NetworkRefreshService::parseCoinGeckoPriceResponse(
const std::string& response, std::time_t fetchedAt)
{
try {
auto parsed = json::parse(response);
if (!parsed.contains("dragonx-2")) return std::nullopt;
const auto& data = parsed["dragonx-2"];
PriceRefreshResult result;
result.market.price_usd = data.value("usd", 0.0);
result.market.price_btc = data.value("btc", 0.0);
result.market.change_24h = data.value("usd_24h_change", 0.0);
result.market.volume_24h = data.value("usd_24h_vol", 0.0);
result.market.market_cap = data.value("usd_market_cap", 0.0);
char buf[64];
std::tm* tm = std::localtime(&fetchedAt);
if (tm && std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm) > 0) {
result.market.last_updated = buf;
}
return result;
} catch (...) {
return std::nullopt;
}
}
NetworkRefreshService::PriceHttpResult NetworkRefreshService::parsePriceHttpResponse(
const PriceHttpResponse& response,
std::time_t fetchedAt)
{
PriceHttpResult result;
if (!response.transportOk || response.httpStatus != 200) {
const std::string reason = response.transportOk ? "OK" : response.transportError;
result.errorMessage = "Price fetch failed: " + reason +
" (HTTP " + std::to_string(response.httpStatus) + ")";
return result;
}
auto parsed = parseCoinGeckoPriceResponse(response.body, fetchedAt);
if (!parsed) {
result.errorMessage = "Price fetch returned an unrecognized response";
return result;
}
result.price = std::move(*parsed);
return result;
}
AddressInfo NetworkRefreshService::buildShieldedAddressInfo(const std::string& address,
const json& validation,
bool validationSucceeded)
{
AddressInfo info;
info.address = address;
info.type = "shielded";
if (!validationSucceeded) {
info.has_spending_key = true;
return info;
}
info.has_spending_key = false;
if (auto isMine = readOptional<bool>(validation, "ismine")) {
info.has_spending_key = *isMine;
}
return info;
}
AddressInfo NetworkRefreshService::buildTransparentAddressInfo(const std::string& address)
{
AddressInfo info;
info.address = address;
info.type = "transparent";
return info;
}
std::vector<AddressInfo> NetworkRefreshService::parseTransparentAddressList(const json& addressList)
{
std::vector<AddressInfo> addresses;
if (!addressList.is_array()) return addresses;
for (const auto& addressJson : addressList) {
try {
addresses.push_back(buildTransparentAddressInfo(addressJson.get<std::string>()));
} catch (...) {}
}
return addresses;
}
void NetworkRefreshService::applyShieldedBalancesFromUnspent(std::vector<AddressInfo>& addresses,
const json& unspent)
{
applyBalancesFromUnspent(addresses, unspent);
}
void NetworkRefreshService::applyTransparentBalancesFromUnspent(std::vector<AddressInfo>& addresses,
const json& unspent)
{
applyBalancesFromUnspent(addresses, unspent);
}
NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddressRefreshResult(RefreshRpcGateway& rpc)
{
AddressRefreshResult result;
try {
json zList = rpc.call("z_listaddresses", json::array());
if (zList.is_array()) {
for (const auto& addressJson : zList) {
std::string address;
try {
address = addressJson.get<std::string>();
} catch (...) {
continue;
}
json validationResult;
bool validationSucceeded = false;
try {
validationResult = rpc.call("z_validateaddress", json::array({address}));
validationSucceeded = true;
} catch (...) {
// Older daemons can fail validation for wallet-owned addresses.
}
result.shieldedAddresses.push_back(
buildShieldedAddressInfo(address, validationResult, validationSucceeded));
}
}
} catch (const std::exception& e) {
DEBUG_LOGF("z_listaddresses error: %s\n", e.what());
}
try {
json unspent = rpc.call("z_listunspent", json::array());
applyShieldedBalancesFromUnspent(result.shieldedAddresses, unspent);
} catch (const std::exception& e) {
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
for (auto& info : result.shieldedAddresses) {
try {
json balance = rpc.call("z_getbalance", json::array({info.address}));
if (!balance.is_null()) info.balance = balance.get<double>();
} catch (...) {}
}
}
try {
json tList = rpc.call("getaddressesbyaccount", json::array({""}));
result.transparentAddresses = parseTransparentAddressList(tList);
} catch (const std::exception& e) {
DEBUG_LOGF("getaddressesbyaccount error: %s\n", e.what());
}
try {
json unspent = rpc.call("listunspent", json::array());
applyTransparentBalancesFromUnspent(result.transparentAddresses, unspent);
} catch (const std::exception& e) {
DEBUG_LOGF("listunspent error: %s\n", e.what());
}
return result;
}
NetworkRefreshService::TransactionRefreshSnapshot NetworkRefreshService::buildTransactionRefreshSnapshot(
const WalletState& state,
const TransactionViewCache& viewTxCache,
const std::unordered_set<std::string>& sendTxids)
{
TransactionRefreshSnapshot snapshot;
for (const auto& address : state.z_addresses) {
if (!address.address.empty()) snapshot.shieldedAddresses.push_back(address.address);
}
for (const auto& cachedView : viewTxCache) {
snapshot.fullyEnrichedTxids.insert(cachedView.first);
}
for (const auto& transaction : state.transactions) {
if (transaction.confirmations > 6 && transaction.timestamp != 0 && !transaction.txid.empty()) {
snapshot.fullyEnrichedTxids.insert(transaction.txid);
}
}
snapshot.viewTxCache = viewTxCache;
snapshot.sendTxids = sendTxids;
return snapshot;
}
void NetworkRefreshService::appendTransparentTransactions(std::vector<TransactionInfo>& transactions,
std::set<std::string>& knownTxids,
const json& result)
{
if (!result.is_array()) return;
for (const auto& transactionJson : result) {
TransactionInfo info;
if (auto value = readOptional<std::string>(transactionJson, "txid")) info.txid = *value;
if (auto value = readOptional<std::string>(transactionJson, "category")) info.type = *value;
if (auto value = readOptional<double>(transactionJson, "amount")) info.amount = *value;
if (auto value = readOptional<std::int64_t>(transactionJson, "time")) {
info.timestamp = *value;
} else if (auto received = readOptional<std::int64_t>(transactionJson, "timereceived")) {
info.timestamp = *received;
}
if (auto value = readOptional<int>(transactionJson, "confirmations")) info.confirmations = *value;
if (auto value = readOptional<std::string>(transactionJson, "address")) info.address = *value;
if (!info.txid.empty()) knownTxids.insert(info.txid);
transactions.push_back(std::move(info));
}
}
void NetworkRefreshService::appendShieldedReceivedTransactions(std::vector<TransactionInfo>& transactions,
std::set<std::string>& knownTxids,
const std::string& address,
const json& received)
{
if (received.is_null() || !received.is_array()) return;
for (const auto& note : received) {
auto txid = readOptional<std::string>(note, "txid");
if (!txid || txid->empty()) continue;
auto change = readOptional<bool>(note, "change");
if (change && *change) continue;
bool dominated = false;
for (const auto& existing : transactions) {
if (existing.txid == *txid && existing.type == "receive") {
dominated = true;
break;
}
}
if (dominated) continue;
TransactionInfo info;
info.txid = *txid;
info.type = "receive";
info.address = address;
if (auto value = readOptional<double>(note, "amount")) info.amount = *value;
if (auto value = readOptional<int>(note, "confirmations")) info.confirmations = *value;
if (auto value = readOptional<std::int64_t>(note, "time")) info.timestamp = *value;
if (auto value = readOptional<std::string>(note, "memoStr")) info.memo = *value;
knownTxids.insert(*txid);
transactions.push_back(std::move(info));
}
}
NetworkRefreshService::TransactionViewCacheEntry NetworkRefreshService::parseViewTransactionCacheEntry(
const json& viewTransaction)
{
TransactionViewCacheEntry entry;
if (!viewTransaction.is_object()) return entry;
if (viewTransaction.contains("spends") && viewTransaction["spends"].is_array()) {
for (const auto& spend : viewTransaction["spends"]) {
if (auto address = readOptional<std::string>(spend, "address")) {
entry.from_address = *address;
break;
}
}
}
if (viewTransaction.contains("outputs") && viewTransaction["outputs"].is_array()) {
for (const auto& output : viewTransaction["outputs"]) {
bool outgoing = false;
if (auto value = readOptional<bool>(output, "outgoing")) outgoing = *value;
if (!outgoing) continue;
TransactionViewCacheEntry::Output out;
if (auto value = readOptional<std::string>(output, "address")) out.address = *value;
if (auto value = readOptional<double>(output, "value")) out.value = *value;
if (auto value = readOptional<std::string>(output, "memoStr")) out.memo = *value;
entry.outgoing_outputs.push_back(std::move(out));
}
}
return entry;
}
void NetworkRefreshService::appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
const std::string& txid,
const TransactionViewCacheEntry& entry)
{
for (const auto& out : entry.outgoing_outputs) {
bool alreadyTracked = false;
for (const auto& existing : transactions) {
if (existing.txid == txid && existing.type == "send" &&
std::abs(existing.amount + out.value) < 0.00000001) {
alreadyTracked = true;
break;
}
}
if (alreadyTracked) continue;
TransactionInfo info;
info.txid = txid;
info.type = "send";
info.address = out.address;
info.amount = -out.value;
info.memo = out.memo;
info.from_address = entry.from_address;
for (const auto& existing : transactions) {
if (existing.txid == txid) {
info.confirmations = existing.confirmations;
info.timestamp = existing.timestamp;
break;
}
}
transactions.push_back(std::move(info));
}
}
void NetworkRefreshService::sortTransactionsNewestFirst(std::vector<TransactionInfo>& transactions)
{
std::sort(transactions.begin(), transactions.end(),
[](const TransactionInfo& left, const TransactionInfo& right) {
return left.timestamp > right.timestamp;
});
}
NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTransactionRefreshResult(
RefreshRpcGateway& rpc,
const TransactionRefreshSnapshot& snapshot,
int currentBlockHeight,
int maxViewTransactionsPerCycle)
{
TransactionRefreshResult result;
result.blockHeight = currentBlockHeight;
std::set<std::string> knownTxids;
try {
json transactions = rpc.call("listtransactions", json::array({"", 9999}));
appendTransparentTransactions(result.transactions, knownTxids, transactions);
} catch (const std::exception& e) {
DEBUG_LOGF("listtransactions error: %s\n", e.what());
}
for (const auto& address : snapshot.shieldedAddresses) {
try {
json received = rpc.call("z_listreceivedbyaddress", json::array({address, 0}));
appendShieldedReceivedTransactions(result.transactions, knownTxids, address, received);
} catch (const std::exception& e) {
DEBUG_LOGF("z_listreceivedbyaddress error for %s: %s\n",
address.substr(0, 12).c_str(), e.what());
}
}
for (const auto& txid : snapshot.sendTxids) {
knownTxids.insert(txid);
}
int viewTxCount = 0;
for (const auto& txid : knownTxids) {
if (snapshot.fullyEnrichedTxids.count(txid)) continue;
auto cached = snapshot.viewTxCache.find(txid);
if (cached != snapshot.viewTxCache.end()) {
appendViewTransactionOutputs(result.transactions, txid, cached->second);
continue;
}
if (viewTxCount >= maxViewTransactionsPerCycle) break;
++viewTxCount;
try {
json viewTransaction = rpc.call("z_viewtransaction", json::array({txid}));
if (viewTransaction.is_null() || !viewTransaction.is_object()) continue;
auto entry = parseViewTransactionCacheEntry(viewTransaction);
appendViewTransactionOutputs(result.transactions, txid, entry);
for (auto& info : result.transactions) {
if (info.txid != txid || info.timestamp != 0) continue;
try {
json rawTransaction = rpc.call("gettransaction", json::array({txid}));
if (!rawTransaction.is_null()) {
if (auto value = readOptional<std::int64_t>(rawTransaction, "time")) info.timestamp = *value;
if (auto value = readOptional<int>(rawTransaction, "confirmations")) info.confirmations = *value;
}
} catch (...) {}
break;
}
result.newViewTxEntries[txid] = std::move(entry);
} catch (const std::exception& e) {
(void)e;
}
}
sortTransactionsNewestFirst(result.transactions);
return result;
}
void NetworkRefreshService::applyConnectionInfoResult(WalletState& state, const ConnectionInfoResult& result)
{
if (!result.ok) return;
if (result.daemonVersion) state.daemon_version = *result.daemonVersion;
if (result.protocolVersion) state.protocol_version = *result.protocolVersion;
if (result.p2pPort) state.p2p_port = *result.p2pPort;
if (result.longestChain && *result.longestChain > 0) state.longestchain = *result.longestChain;
if (result.notarized) state.notarized = *result.notarized;
if (result.blocks) state.sync.blocks = *result.blocks;
if (state.longestchain > 0 && state.sync.blocks > state.longestchain) state.longestchain = state.sync.blocks;
}
void NetworkRefreshService::applyWalletEncryptionResult(WalletState& state, const WalletEncryptionResult& result)
{
if (!result.ok) return;
if (result.encrypted) {
state.encrypted = true;
state.unlocked_until = result.unlockedUntil;
state.locked = (result.unlockedUntil == 0);
} else {
state.encrypted = false;
state.locked = false;
state.unlocked_until = 0;
}
state.encryption_state_known = true;
}
void NetworkRefreshService::applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result)
{
applyConnectionInfoResult(state, result.info);
applyWalletEncryptionResult(state, result.encryption);
}
void NetworkRefreshService::applyCoreRefreshResult(WalletState& state,
const CoreRefreshResult& result,
std::time_t updatedAt)
{
if (result.balanceOk) {
if (result.shieldedBalance) state.shielded_balance = *result.shieldedBalance;
if (result.transparentBalance) state.transparent_balance = *result.transparentBalance;
if (result.totalBalance) state.total_balance = *result.totalBalance;
state.last_balance_update = updatedAt;
}
if (!result.blockchainOk) return;
if (result.blocks) state.sync.blocks = *result.blocks;
if (result.headers) state.sync.headers = *result.headers;
if (result.verificationProgress) state.sync.verification_progress = *result.verificationProgress;
if (result.longestChain && *result.longestChain > 0) state.longestchain = *result.longestChain;
if (state.longestchain > 0 && state.sync.blocks > state.longestchain) state.longestchain = state.sync.blocks;
if (state.longestchain > 0)
state.sync.syncing = (state.sync.blocks < state.longestchain - 2);
else
state.sync.syncing = (state.sync.blocks < state.sync.headers - 2);
if (result.notarized) state.notarized = *result.notarized;
}
void NetworkRefreshService::applyMiningRefreshResult(WalletState& state,
const MiningRefreshResult& result,
std::time_t updatedAt)
{
if (result.localHashrate) {
state.mining.localHashrate = *result.localHashrate;
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 (result.miningOk) {
if (result.generate) state.mining.generate = *result.generate;
if (result.genproclimit) state.mining.genproclimit = *result.genproclimit;
if (result.blocks) state.mining.blocks = *result.blocks;
if (result.difficulty) state.mining.difficulty = *result.difficulty;
if (result.networkHashrate) state.mining.networkHashrate = *result.networkHashrate;
if (result.chain) state.mining.chain = *result.chain;
state.last_mining_update = updatedAt;
}
state.mining.daemon_memory_mb = result.daemonMemoryMb;
}
void NetworkRefreshService::applyPeerRefreshResult(WalletState& state,
PeerRefreshResult&& result,
std::time_t updatedAt)
{
state.peers = std::move(result.peers);
state.bannedPeers = std::move(result.bannedPeers);
state.last_peer_update = updatedAt;
}
void NetworkRefreshService::markPriceRefreshStarted(WalletState& state)
{
state.market.price_loading = true;
state.market.price_error.clear();
}
void NetworkRefreshService::applyPriceRefreshResult(WalletState& state,
const PriceRefreshResult& result,
std::chrono::steady_clock::time_point fetchedAt)
{
state.market.price_loading = false;
state.market.price_error.clear();
state.market.price_usd = result.market.price_usd;
state.market.price_btc = result.market.price_btc;
state.market.change_24h = result.market.change_24h;
state.market.volume_24h = result.market.volume_24h;
state.market.market_cap = result.market.market_cap;
state.market.last_updated = result.market.last_updated;
state.market.last_fetch_time = fetchedAt;
state.market.price_history.push_back(result.market.price_usd);
if (state.market.price_history.size() > MarketInfo::MAX_HISTORY) {
state.market.price_history.erase(state.market.price_history.begin());
}
}
void NetworkRefreshService::applyPriceRefreshFailure(WalletState& state,
const std::string& errorMessage)
{
state.market.price_loading = false;
state.market.price_error = errorMessage.empty()
? std::string("Price fetch failed")
: errorMessage;
}
void NetworkRefreshService::applyAddressRefreshResult(WalletState& state,
AddressRefreshResult&& result)
{
state.z_addresses = std::move(result.shieldedAddresses);
state.t_addresses = std::move(result.transparentAddresses);
}
void NetworkRefreshService::applyTransactionRefreshResult(WalletState& state,
TransactionCacheUpdate cacheUpdate,
TransactionRefreshResult&& result,
std::time_t updatedAt)
{
state.transactions = std::move(result.transactions);
state.last_tx_update = updatedAt;
cacheUpdate.lastTxBlockHeight = result.blockHeight;
for (auto& [txid, entry] : result.newViewTxEntries) {
cacheUpdate.viewTxCache[txid] = std::move(entry);
cacheUpdate.sendTxids.erase(txid);
}
cacheUpdate.confirmedTxCache.clear();
cacheUpdate.confirmedTxIds.clear();
for (const auto& tx : state.transactions) {
if (tx.confirmations >= 10 && tx.timestamp != 0) {
cacheUpdate.confirmedTxIds.insert(tx.txid);
cacheUpdate.confirmedTxCache.push_back(tx);
}
}
cacheUpdate.confirmedCacheBlock = result.blockHeight;
}
bool NetworkRefreshService::shouldRefreshTransactions(int lastTxBlockHeight,
int currentBlockHeight,
bool transactionsEmpty,
bool transactionsDirty) const
{
return scheduler_.shouldRefreshTransactions(lastTxBlockHeight,
currentBlockHeight,
transactionsEmpty,
transactionsDirty);
}
bool NetworkRefreshService::beginJob(Job job)
{
return beginDispatch(job).accepted;
}
bool NetworkRefreshService::beginJob(Job job, std::size_t queuedWork, std::size_t maxQueuedWork)
{
return beginDispatch(job, queuedWork, maxQueuedWork).accepted;
}
void NetworkRefreshService::finishJob(Job job)
{
jobFlag(job).store(false, std::memory_order_release);
}
bool NetworkRefreshService::jobInProgress(Job job) const
{
return jobFlag(job).load(std::memory_order_acquire);
}
void NetworkRefreshService::resetJobs()
{
coreInProgress_.store(false, std::memory_order_release);
addressesInProgress_.store(false, std::memory_order_release);
transactionsInProgress_.store(false, std::memory_order_release);
miningInProgress_.store(false, std::memory_order_release);
peersInProgress_.store(false, std::memory_order_release);
priceInProgress_.store(false, std::memory_order_release);
encryptionInProgress_.store(false, std::memory_order_release);
connectionInitInProgress_.store(false, std::memory_order_release);
}
NetworkRefreshService::DispatchTicket NetworkRefreshService::beginDispatch(
Job job, std::size_t queuedWork, std::size_t maxQueuedWork)
{
std::size_t index = jobIndex(job);
lastQueueDepth_[index].store(queuedWork, std::memory_order_release);
if (maxQueuedWork > 0 && queuedWork >= maxQueuedWork) {
skippedQueuePressure_[index].fetch_add(1, std::memory_order_relaxed);
return {job, generations_[index].load(std::memory_order_acquire), false};
}
bool expected = false;
if (!jobFlag(job).compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
skippedInFlight_[index].fetch_add(1, std::memory_order_relaxed);
return {job, generations_[index].load(std::memory_order_acquire), false};
}
std::uint64_t generation = generations_[index].fetch_add(1, std::memory_order_acq_rel) + 1;
started_[index].fetch_add(1, std::memory_order_relaxed);
return {job, generation, true};
}
bool NetworkRefreshService::completeDispatch(const DispatchTicket& ticket)
{
if (!ticket.accepted) return false;
std::size_t index = jobIndex(ticket.job);
if (generations_[index].load(std::memory_order_acquire) != ticket.generation) {
staleCallbacks_[index].fetch_add(1, std::memory_order_relaxed);
return false;
}
jobFlag(ticket.job).store(false, std::memory_order_release);
finished_[index].fetch_add(1, std::memory_order_relaxed);
return true;
}
void NetworkRefreshService::cancelDispatch(const DispatchTicket& ticket)
{
if (!ticket.accepted) return;
std::size_t index = jobIndex(ticket.job);
if (generations_[index].load(std::memory_order_acquire) == ticket.generation) {
jobFlag(ticket.job).store(false, std::memory_order_release);
} else {
staleCallbacks_[index].fetch_add(1, std::memory_order_relaxed);
}
}
NetworkRefreshService::JobStats NetworkRefreshService::stats(Job job) const
{
std::size_t index = jobIndex(job);
return {
started_[index].load(std::memory_order_acquire),
finished_[index].load(std::memory_order_acquire),
skippedInFlight_[index].load(std::memory_order_acquire),
skippedQueuePressure_[index].load(std::memory_order_acquire),
staleCallbacks_[index].load(std::memory_order_acquire),
lastQueueDepth_[index].load(std::memory_order_acquire)
};
}
std::atomic<bool>& NetworkRefreshService::jobFlag(Job job)
{
switch (job) {
case Job::Core: return coreInProgress_;
case Job::Addresses: return addressesInProgress_;
case Job::Transactions: return transactionsInProgress_;
case Job::Mining: return miningInProgress_;
case Job::Peers: return peersInProgress_;
case Job::Price: return priceInProgress_;
case Job::Encryption: return encryptionInProgress_;
case Job::ConnectionInit: return connectionInitInProgress_;
case Job::Count: return coreInProgress_;
}
return coreInProgress_;
}
const std::atomic<bool>& NetworkRefreshService::jobFlag(Job job) const
{
switch (job) {
case Job::Core: return coreInProgress_;
case Job::Addresses: return addressesInProgress_;
case Job::Transactions: return transactionsInProgress_;
case Job::Mining: return miningInProgress_;
case Job::Peers: return peersInProgress_;
case Job::Price: return priceInProgress_;
case Job::Encryption: return encryptionInProgress_;
case Job::ConnectionInit: return connectionInitInProgress_;
case Job::Count: return coreInProgress_;
}
return coreInProgress_;
}
std::size_t NetworkRefreshService::jobIndex(Job job)
{
std::size_t index = static_cast<std::size_t>(job);
constexpr std::size_t count = static_cast<std::size_t>(Job::Count);
return index < count ? index : 0;
}
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,336 @@
#pragma once
#include "data/wallet_state.h"
#include "refresh_scheduler.h"
#include "rpc/rpc_worker.h"
#include <nlohmann/json.hpp>
#include <array>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <ctime>
#include <optional>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
namespace dragonx {
namespace services {
class NetworkRefreshService {
public:
using Timer = RefreshScheduler::Timer;
using Intervals = RefreshScheduler::Intervals;
class RefreshRpcGateway {
public:
virtual ~RefreshRpcGateway() = default;
virtual nlohmann::json call(const std::string& method,
const nlohmann::json& params) = 0;
};
enum class Job {
Core,
Addresses,
Transactions,
Mining,
Peers,
Price,
Encryption,
ConnectionInit,
Count
};
struct DispatchTicket {
Job job = Job::Core;
std::uint64_t generation = 0;
bool accepted = false;
};
struct JobStats {
std::uint64_t started = 0;
std::uint64_t finished = 0;
std::uint64_t skippedInFlight = 0;
std::uint64_t skippedQueuePressure = 0;
std::uint64_t staleCallbacks = 0;
std::size_t lastQueueDepth = 0;
};
struct EnqueueResult {
DispatchTicket ticket;
bool enqueued = false;
std::size_t queueDepth = 0;
};
struct ConnectionInfoResult {
bool ok = false;
std::optional<int> daemonVersion;
std::optional<int> protocolVersion;
std::optional<int> p2pPort;
std::optional<int> longestChain;
std::optional<int> notarized;
std::optional<int> blocks;
};
struct WalletEncryptionResult {
bool ok = false;
bool encrypted = false;
std::int64_t unlockedUntil = 0;
};
struct WarmupPollResult {
bool ready = false;
ConnectionInfoResult info;
std::string errorMessage;
};
struct ConnectionInitResult {
ConnectionInfoResult info;
WalletEncryptionResult encryption;
};
struct CoreRefreshResult {
bool balanceOk = false;
std::optional<double> shieldedBalance;
std::optional<double> transparentBalance;
std::optional<double> totalBalance;
bool blockchainOk = false;
std::optional<int> blocks;
std::optional<int> headers;
std::optional<double> verificationProgress;
std::optional<int> longestChain;
std::optional<int> notarized;
};
struct MiningRefreshResult {
std::optional<double> localHashrate;
bool miningOk = false;
std::optional<bool> generate;
std::optional<int> genproclimit;
std::optional<int> blocks;
std::optional<double> difficulty;
std::optional<double> networkHashrate;
std::optional<std::string> chain;
double daemonMemoryMb = 0.0;
};
struct PeerRefreshResult {
std::vector<PeerInfo> peers;
std::vector<BannedPeer> bannedPeers;
};
struct PriceRefreshResult {
MarketInfo market;
};
struct PriceHttpResponse {
bool transportOk = false;
long httpStatus = 0;
std::string body;
std::string transportError;
};
struct PriceHttpResult {
std::optional<PriceRefreshResult> price;
std::string errorMessage;
};
struct AddressRefreshResult {
std::vector<AddressInfo> shieldedAddresses;
std::vector<AddressInfo> transparentAddresses;
};
struct TransactionViewCacheEntry {
std::string from_address;
struct Output {
std::string address;
double value = 0.0;
std::string memo;
};
std::vector<Output> outgoing_outputs;
};
using TransactionViewCache = std::unordered_map<std::string, TransactionViewCacheEntry>;
struct TransactionRefreshSnapshot {
std::vector<std::string> shieldedAddresses;
std::unordered_set<std::string> fullyEnrichedTxids;
TransactionViewCache viewTxCache;
std::unordered_set<std::string> sendTxids;
};
struct TransactionRefreshResult {
std::vector<TransactionInfo> transactions;
int blockHeight = -1;
TransactionViewCache newViewTxEntries;
};
struct TransactionCacheUpdate {
TransactionViewCache& viewTxCache;
std::unordered_set<std::string>& sendTxids;
std::vector<TransactionInfo>& confirmedTxCache;
std::unordered_set<std::string>& confirmedTxIds;
int& confirmedCacheBlock;
int& lastTxBlockHeight;
};
static Intervals intervalsForPage(ui::NavPage page) { return RefreshScheduler::intervalsForPage(page); }
static ConnectionInfoResult parseConnectionInfoResult(const nlohmann::json& info);
static WalletEncryptionResult parseWalletEncryptionResult(const nlohmann::json& walletInfo);
static WarmupPollResult collectWarmupPollResult(RefreshRpcGateway& rpc);
static ConnectionInitResult collectConnectionInitResult(RefreshRpcGateway& rpc);
static CoreRefreshResult parseCoreRefreshResult(const nlohmann::json& totalBalance,
bool balanceOk,
const nlohmann::json& blockInfo,
bool blockOk);
static CoreRefreshResult collectCoreRefreshResult(RefreshRpcGateway& rpc);
static MiningRefreshResult parseMiningRefreshResult(const nlohmann::json& miningInfo,
bool miningOk,
const nlohmann::json& localHashrate,
bool hashrateOk,
double daemonMemoryMb);
static MiningRefreshResult collectMiningRefreshResult(RefreshRpcGateway& rpc,
double daemonMemoryMb,
bool includeSlowRefresh);
static PeerRefreshResult parsePeerRefreshResult(const nlohmann::json& peers,
const nlohmann::json& bannedPeers);
static PeerRefreshResult collectPeerRefreshResult(RefreshRpcGateway& rpc);
static std::optional<PriceRefreshResult> parseCoinGeckoPriceResponse(const std::string& response,
std::time_t fetchedAt);
static PriceHttpResult parsePriceHttpResponse(const PriceHttpResponse& response,
std::time_t fetchedAt);
static AddressInfo buildShieldedAddressInfo(const std::string& address,
const nlohmann::json& validation,
bool validationSucceeded);
static AddressInfo buildTransparentAddressInfo(const std::string& address);
static std::vector<AddressInfo> parseTransparentAddressList(const nlohmann::json& addressList);
static void applyShieldedBalancesFromUnspent(std::vector<AddressInfo>& addresses,
const nlohmann::json& unspent);
static void applyTransparentBalancesFromUnspent(std::vector<AddressInfo>& addresses,
const nlohmann::json& unspent);
static AddressRefreshResult collectAddressRefreshResult(RefreshRpcGateway& rpc);
static TransactionRefreshSnapshot buildTransactionRefreshSnapshot(const WalletState& state,
const TransactionViewCache& viewTxCache,
const std::unordered_set<std::string>& sendTxids);
static void appendTransparentTransactions(std::vector<TransactionInfo>& transactions,
std::set<std::string>& knownTxids,
const nlohmann::json& result);
static void appendShieldedReceivedTransactions(std::vector<TransactionInfo>& transactions,
std::set<std::string>& knownTxids,
const std::string& address,
const nlohmann::json& received);
static TransactionViewCacheEntry parseViewTransactionCacheEntry(const nlohmann::json& viewTransaction);
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
const std::string& txid,
const TransactionViewCacheEntry& entry);
static void sortTransactionsNewestFirst(std::vector<TransactionInfo>& transactions);
static TransactionRefreshResult collectTransactionRefreshResult(RefreshRpcGateway& rpc,
const TransactionRefreshSnapshot& snapshot,
int currentBlockHeight,
int maxViewTransactionsPerCycle);
static void applyConnectionInfoResult(WalletState& state, const ConnectionInfoResult& result);
static void applyWalletEncryptionResult(WalletState& state, const WalletEncryptionResult& result);
static void applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result);
static void applyCoreRefreshResult(WalletState& state,
const CoreRefreshResult& result,
std::time_t updatedAt);
static void applyMiningRefreshResult(WalletState& state,
const MiningRefreshResult& result,
std::time_t updatedAt);
static void applyPeerRefreshResult(WalletState& state,
PeerRefreshResult&& result,
std::time_t updatedAt);
static void markPriceRefreshStarted(WalletState& state);
static void applyPriceRefreshResult(WalletState& state,
const PriceRefreshResult& result,
std::chrono::steady_clock::time_point fetchedAt);
static void applyPriceRefreshFailure(WalletState& state,
const std::string& errorMessage);
static void applyAddressRefreshResult(WalletState& state,
AddressRefreshResult&& result);
static void applyTransactionRefreshResult(WalletState& state,
TransactionCacheUpdate cacheUpdate,
TransactionRefreshResult&& result,
std::time_t updatedAt);
void applyPage(ui::NavPage page) { scheduler_.applyPage(page); }
void setIntervals(Intervals intervals) { scheduler_.setIntervals(intervals); }
const Intervals& intervals() const { return scheduler_.intervals(); }
void tick(float deltaSeconds) { scheduler_.tick(deltaSeconds); }
bool isDue(Timer timer) const { return scheduler_.isDue(timer); }
bool consumeDue(Timer timer) { return scheduler_.consumeDue(timer); }
void reset(Timer timer) { scheduler_.reset(timer); }
void markDue(Timer timer) { scheduler_.markDue(timer); }
void setTimer(Timer timer, float seconds) { scheduler_.setTimer(timer, seconds); }
float timer(Timer timer) const { return scheduler_.timer(timer); }
float interval(Timer timer) const { return scheduler_.interval(timer); }
void markImmediateRefresh() { scheduler_.markImmediateRefresh(); }
void markWalletMutationRefresh() { scheduler_.markWalletMutationRefresh(); }
void resetTxAge() { scheduler_.resetTxAge(); }
bool shouldRefreshTransactions(int lastTxBlockHeight,
int currentBlockHeight,
bool transactionsEmpty,
bool transactionsDirty) const;
bool beginJob(Job job);
bool beginJob(Job job, std::size_t queuedWork, std::size_t maxQueuedWork);
void finishJob(Job job);
bool jobInProgress(Job job) const;
void resetJobs();
DispatchTicket beginDispatch(Job job, std::size_t queuedWork = 0, std::size_t maxQueuedWork = 0);
bool completeDispatch(const DispatchTicket& ticket);
void cancelDispatch(const DispatchTicket& ticket);
JobStats stats(Job job) const;
template <typename Worker, typename WorkFn>
EnqueueResult enqueue(Job job, Worker& worker, WorkFn&& work, std::size_t maxQueuedWork = 0)
{
std::size_t queueDepth = worker.pendingTaskCount();
auto ticket = beginDispatch(job, queueDepth, maxQueuedWork);
if (!ticket.accepted) return {ticket, false, queueDepth};
worker.post([this, ticket, work = std::forward<WorkFn>(work)]() mutable -> rpc::RPCWorker::MainCb {
auto mainCallback = work();
return [this, ticket, mainCallback = std::move(mainCallback)]() mutable {
if (!completeDispatch(ticket)) return;
if (mainCallback) mainCallback();
};
});
return {ticket, true, queueDepth};
}
private:
std::atomic<bool>& jobFlag(Job job);
const std::atomic<bool>& jobFlag(Job job) const;
static std::size_t jobIndex(Job job);
RefreshScheduler scheduler_;
std::atomic<bool> coreInProgress_{false};
std::atomic<bool> addressesInProgress_{false};
std::atomic<bool> transactionsInProgress_{false};
std::atomic<bool> miningInProgress_{false};
std::atomic<bool> peersInProgress_{false};
std::atomic<bool> priceInProgress_{false};
std::atomic<bool> encryptionInProgress_{false};
std::atomic<bool> connectionInitInProgress_{false};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> generations_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> started_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> finished_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> skippedInFlight_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> skippedQueuePressure_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> staleCallbacks_{};
std::array<std::atomic<std::size_t>, static_cast<std::size_t>(Job::Count)> lastQueueDepth_{};
};
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,158 @@
#include "refresh_scheduler.h"
#include <algorithm>
namespace dragonx {
namespace services {
RefreshScheduler::Intervals RefreshScheduler::intervalsForPage(ui::NavPage page)
{
using NP = ui::NavPage;
switch (page) {
case NP::Overview: return {2.0f, 10.0f, 15.0f, 0.0f};
case NP::Send: return {3.0f, 10.0f, 5.0f, 0.0f};
case NP::Receive: return {5.0f, 15.0f, 5.0f, 0.0f};
case NP::History: return {5.0f, 3.0f, 15.0f, 0.0f};
case NP::Mining: return {5.0f, 15.0f, 15.0f, 0.0f};
case NP::Peers: return {5.0f, 15.0f, 15.0f, 5.0f};
case NP::Market: return {5.0f, 15.0f, 15.0f, 0.0f};
default: return {5.0f, 15.0f, 15.0f, 0.0f};
}
}
void RefreshScheduler::applyPage(ui::NavPage page)
{
setIntervals(intervalsForPage(page));
}
void RefreshScheduler::setIntervals(Intervals intervals)
{
intervals_ = intervals;
}
void RefreshScheduler::tick(float deltaSeconds)
{
float delta = std::max(0.0f, deltaSeconds);
timers_.core += delta;
timers_.transactions += delta;
timers_.addresses += delta;
timers_.peers += delta;
timers_.price += delta;
timers_.fast += delta;
timers_.txAge += delta;
timers_.opid += delta;
}
bool RefreshScheduler::isDue(Timer timer) const
{
float timerInterval = interval(timer);
return timerInterval > 0.0f && timerRef(timer) >= timerInterval;
}
bool RefreshScheduler::consumeDue(Timer timer)
{
if (!isDue(timer)) return false;
reset(timer);
return true;
}
void RefreshScheduler::reset(Timer timer)
{
timerRef(timer) = 0.0f;
}
void RefreshScheduler::markDue(Timer timer)
{
float timerInterval = interval(timer);
timerRef(timer) = timerInterval > 0.0f ? timerInterval : 0.0f;
}
void RefreshScheduler::setTimer(Timer timer, float seconds)
{
timerRef(timer) = std::max(0.0f, seconds);
}
float RefreshScheduler::timer(Timer timer) const
{
return timerRef(timer);
}
float RefreshScheduler::interval(Timer timer) const
{
switch (timer) {
case Timer::Core: return intervals_.core;
case Timer::Transactions: return intervals_.transactions;
case Timer::Addresses: return intervals_.addresses;
case Timer::Peers: return intervals_.peers;
case Timer::Price: return kPrice;
case Timer::Fast: return kFast;
case Timer::TxAge: return kTxMaxAge;
case Timer::Opid: return kOpidPoll;
}
return 0.0f;
}
void RefreshScheduler::markImmediateRefresh()
{
markDue(Timer::Core);
markDue(Timer::Transactions);
markDue(Timer::Addresses);
markDue(Timer::Peers);
}
void RefreshScheduler::markWalletMutationRefresh()
{
markDue(Timer::Core);
markDue(Timer::Transactions);
markDue(Timer::Addresses);
}
void RefreshScheduler::resetTxAge()
{
reset(Timer::TxAge);
}
bool RefreshScheduler::shouldRefreshTransactions(int lastTxBlockHeight,
int currentBlockHeight,
bool transactionsEmpty,
bool transactionsDirty) const
{
return lastTxBlockHeight < 0
|| currentBlockHeight != lastTxBlockHeight
|| transactionsEmpty
|| transactionsDirty
|| isDue(Timer::TxAge);
}
float& RefreshScheduler::timerRef(Timer timer)
{
switch (timer) {
case Timer::Core: return timers_.core;
case Timer::Transactions: return timers_.transactions;
case Timer::Addresses: return timers_.addresses;
case Timer::Peers: return timers_.peers;
case Timer::Price: return timers_.price;
case Timer::Fast: return timers_.fast;
case Timer::TxAge: return timers_.txAge;
case Timer::Opid: return timers_.opid;
}
return timers_.core;
}
const float& RefreshScheduler::timerRef(Timer timer) const
{
switch (timer) {
case Timer::Core: return timers_.core;
case Timer::Transactions: return timers_.transactions;
case Timer::Addresses: return timers_.addresses;
case Timer::Peers: return timers_.peers;
case Timer::Price: return timers_.price;
case Timer::Fast: return timers_.fast;
case Timer::TxAge: return timers_.txAge;
case Timer::Opid: return timers_.opid;
}
return timers_.core;
}
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,81 @@
#pragma once
#include "ui/sidebar.h"
namespace dragonx {
namespace services {
class RefreshScheduler {
public:
enum class Timer {
Core,
Transactions,
Addresses,
Peers,
Price,
Fast,
TxAge,
Opid
};
struct Intervals {
float core;
float transactions;
float addresses;
float peers;
};
static constexpr float kCoreDefault = 5.0f;
static constexpr float kAddressDefault = 15.0f;
static constexpr float kTransactionDefault = 10.0f;
static constexpr float kPeerDefault = 10.0f;
static constexpr float kPrice = 60.0f;
static constexpr float kFast = 1.0f;
static constexpr float kTxMaxAge = 15.0f;
static constexpr float kOpidPoll = 2.0f;
static Intervals intervalsForPage(ui::NavPage page);
void applyPage(ui::NavPage page);
void setIntervals(Intervals intervals);
const Intervals& intervals() const { return intervals_; }
void tick(float deltaSeconds);
bool isDue(Timer timer) const;
bool consumeDue(Timer timer);
void reset(Timer timer);
void markDue(Timer timer);
void setTimer(Timer timer, float seconds);
float timer(Timer timer) const;
float interval(Timer timer) const;
void markImmediateRefresh();
void markWalletMutationRefresh();
void resetTxAge();
bool shouldRefreshTransactions(int lastTxBlockHeight,
int currentBlockHeight,
bool transactionsEmpty,
bool transactionsDirty) const;
private:
struct Timers {
float core = 0.0f;
float transactions = 0.0f;
float addresses = 0.0f;
float peers = 0.0f;
float price = 0.0f;
float fast = 0.0f;
float txAge = 0.0f;
float opid = 0.0f;
};
float& timerRef(Timer timer);
const float& timerRef(Timer timer) const;
Intervals intervals_{kCoreDefault, kTransactionDefault, kAddressDefault, kPeerDefault};
Timers timers_;
};
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,127 @@
#include "wallet_security_controller.h"
#include "../util/secure_vault.h"
#include <cctype>
#include <cstdio>
#include <utility>
namespace dragonx {
namespace services {
WalletSecurityController::~WalletSecurityController()
{
clearDeferredEncryption();
}
void WalletSecurityController::beginDeferredEncryption(std::string passphrase, std::string pin)
{
clearDeferredEncryption();
deferred_.passphrase = std::move(passphrase);
deferred_.pin = std::move(pin);
deferred_.pending = true;
deferred_.lastConnectAttempt = -10.0;
}
WalletSecurityController::DeferredEncryptionSnapshot WalletSecurityController::deferredEncryption() const
{
return {deferred_.passphrase, deferred_.pin};
}
bool WalletSecurityController::shouldAttemptDeferredConnect(double nowSeconds, double minIntervalSeconds)
{
if (!deferred_.pending) return false;
if (nowSeconds - deferred_.lastConnectAttempt < minIntervalSeconds) return false;
deferred_.lastConnectAttempt = nowSeconds;
return true;
}
void WalletSecurityController::clearDeferredEncryption()
{
secureClear(deferred_.passphrase);
secureClear(deferred_.pin);
deferred_.pending = false;
deferred_.lastConnectAttempt = -10.0;
}
WalletSecurityController::DeferredEncryptionResult WalletSecurityController::runDeferredEncryption(
DeferredEncryptionSnapshot request, RpcGateway& rpc, VaultGateway* vault)
{
DeferredEncryptionResult result;
result.pinProvided = !request.pin.empty();
std::string error;
if (!rpc.encryptWallet(request.passphrase, error)) {
result.error = error.empty() ? "encryptwallet failed" : error;
secureClear(request.passphrase);
secureClear(request.pin);
return result;
}
result.encrypted = true;
result.restartRequired = true;
if (result.pinProvided && vault) {
result.pinStored = vault->storePin(request.pin, request.passphrase);
}
secureClear(request.passphrase);
secureClear(request.pin);
return result;
}
WalletSecurityController::PinValidationResult WalletSecurityController::validatePinSetup(
const std::string& pin, const std::string& confirmation, bool allowEmpty, std::size_t minLength)
{
if (pin.empty() && confirmation.empty()) {
return allowEmpty
? PinValidationResult{true, PinValidationError::None, ""}
: PinValidationResult{false, PinValidationError::Empty, "PIN is required"};
}
if (pin != confirmation) {
return {false, PinValidationError::Mismatch, "PINs do not match"};
}
if (pin.size() < minLength) {
return {false, PinValidationError::TooShort, "PIN is too short"};
}
for (unsigned char c : pin) {
if (!std::isdigit(c)) {
return {false, PinValidationError::NonDigit, "PIN must contain only digits"};
}
}
return {true, PinValidationError::None, ""};
}
WalletSecurityController::KeyKind WalletSecurityController::classifyAddress(const std::string& address)
{
return !address.empty() && address[0] == 'z' ? KeyKind::Shielded : KeyKind::Transparent;
}
WalletSecurityController::KeyKind WalletSecurityController::classifyPrivateKey(const std::string& key)
{
return !key.empty() && key[0] == 's' ? KeyKind::Shielded : KeyKind::Transparent;
}
const char* WalletSecurityController::importSuccessMessage(KeyKind kind)
{
return kind == KeyKind::Shielded
? "Z-address key imported successfully. Wallet is rescanning."
: "T-address key imported successfully. Wallet is rescanning.";
}
std::string WalletSecurityController::decryptExportFileName(std::uint64_t timestampSeconds)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "obsidiandecryptexport%llu",
static_cast<unsigned long long>(timestampSeconds));
return std::string(buffer);
}
void WalletSecurityController::secureClear(std::string& value)
{
if (!value.empty()) {
util::SecureVault::secureZero(&value[0], value.size());
value.clear();
}
}
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,93 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
namespace dragonx {
namespace services {
class WalletSecurityController {
public:
enum class PinValidationError {
None,
Empty,
Mismatch,
TooShort,
NonDigit
};
struct PinValidationResult {
bool ok = false;
PinValidationError error = PinValidationError::None;
const char* message = "";
};
struct DeferredEncryptionSnapshot {
std::string passphrase;
std::string pin;
};
class RpcGateway {
public:
virtual ~RpcGateway() = default;
virtual bool encryptWallet(const std::string& passphrase, std::string& error) = 0;
virtual bool unlockWallet(const std::string& passphrase, int timeoutSeconds, std::string& error) = 0;
virtual bool exportWallet(const std::string& fileName, long timeoutSeconds, std::string& error) = 0;
virtual bool importWallet(const std::string& filePath, long timeoutSeconds, std::string& error) = 0;
};
class VaultGateway {
public:
virtual ~VaultGateway() = default;
virtual bool storePin(const std::string& pin, const std::string& passphrase) = 0;
};
enum class KeyKind {
Transparent,
Shielded
};
struct DeferredEncryptionResult {
bool encrypted = false;
bool pinProvided = false;
bool pinStored = false;
bool restartRequired = false;
std::string error;
};
~WalletSecurityController();
void beginDeferredEncryption(std::string passphrase, std::string pin = {});
bool hasDeferredEncryption() const { return deferred_.pending; }
DeferredEncryptionSnapshot deferredEncryption() const;
bool shouldAttemptDeferredConnect(double nowSeconds, double minIntervalSeconds = 3.0);
void clearDeferredEncryption();
DeferredEncryptionResult runDeferredEncryption(DeferredEncryptionSnapshot request,
RpcGateway& rpc,
VaultGateway* vault);
static PinValidationResult validatePinSetup(const std::string& pin,
const std::string& confirmation,
bool allowEmpty = false,
std::size_t minLength = 4);
static KeyKind classifyAddress(const std::string& address);
static KeyKind classifyPrivateKey(const std::string& key);
static const char* importSuccessMessage(KeyKind kind);
static std::string decryptExportFileName(std::uint64_t timestampSeconds);
static void secureClear(std::string& value);
private:
struct DeferredEncryptionState {
std::string passphrase;
std::string pin;
bool pending = false;
double lastConnectAttempt = -10.0;
};
DeferredEncryptionState deferred_;
};
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,114 @@
#include "wallet_security_workflow.h"
#include <utility>
namespace dragonx {
namespace services {
void WalletSecurityWorkflow::reset()
{
state_ = {};
}
void WalletSecurityWorkflow::start(std::chrono::steady_clock::time_point now)
{
state_.phase = DecryptPhase::Working;
state_.step = DecryptStep::Unlock;
state_.status = stepStatus(DecryptStep::Unlock);
state_.inProgress = true;
state_.stepStarted = now;
state_.overallStarted = now;
}
void WalletSecurityWorkflow::advanceTo(DecryptStep step, std::string status,
std::chrono::steady_clock::time_point now)
{
state_.phase = DecryptPhase::Working;
state_.step = step;
state_.status = std::move(status);
state_.inProgress = true;
state_.stepStarted = now;
}
void WalletSecurityWorkflow::failEntry(std::string status)
{
state_.phase = DecryptPhase::PassphraseEntry;
state_.step = DecryptStep::Unlock;
state_.status = std::move(status);
state_.inProgress = false;
}
void WalletSecurityWorkflow::fail(std::string status)
{
state_.phase = DecryptPhase::Error;
state_.status = std::move(status);
state_.inProgress = false;
}
void WalletSecurityWorkflow::closeDialogForImport()
{
state_.inProgress = false;
state_.importActive = true;
}
void WalletSecurityWorkflow::finishImport()
{
state_.importActive = false;
}
WalletSecurityWorkflow::WalletFilePlan WalletSecurityWorkflow::planWalletFiles(
const std::string& dataDir,
std::uint64_t timestampSeconds)
{
WalletFilePlan plan;
plan.dataDir = dataDir;
plan.exportFile = WalletSecurityController::decryptExportFileName(timestampSeconds);
plan.exportPath = dataDir + plan.exportFile;
plan.walletPath = dataDir + "wallet.dat";
plan.backupPath = dataDir + "wallet.dat.encrypted.bak";
return plan;
}
const char* WalletSecurityWorkflow::stepStatus(DecryptStep step)
{
switch (step) {
case DecryptStep::Unlock: return "Unlocking wallet...";
case DecryptStep::ExportKeys: return "Exporting wallet keys...";
case DecryptStep::StopDaemon: return "Stopping daemon...";
case DecryptStep::BackupWallet: return "Backing up encrypted wallet...";
case DecryptStep::RestartDaemon: return "Restarting daemon...";
case DecryptStep::ImportKeys: return "Importing wallet keys...";
}
return "";
}
const char* WalletSecurityWorkflow::stepLabel(DecryptStep step)
{
switch (step) {
case DecryptStep::Unlock: return "Unlocking wallet";
case DecryptStep::ExportKeys: return "Exporting wallet keys";
case DecryptStep::StopDaemon: return "Stopping daemon";
case DecryptStep::BackupWallet: return "Backing up encrypted wallet";
case DecryptStep::RestartDaemon: return "Restarting daemon";
case DecryptStep::ImportKeys: return "Importing wallet keys";
}
return "";
}
WalletSecurityWorkflow::DecryptStep WalletSecurityWorkflow::stepFromIndex(int step)
{
if (step <= 0) return DecryptStep::Unlock;
if (step == 1) return DecryptStep::ExportKeys;
if (step == 2) return DecryptStep::StopDaemon;
if (step == 3) return DecryptStep::BackupWallet;
if (step == 4) return DecryptStep::RestartDaemon;
return DecryptStep::ImportKeys;
}
bool WalletSecurityWorkflow::stepIsComplete(DecryptStep current, DecryptStep candidate)
{
return stepIndex(candidate) < stepIndex(current);
}
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,78 @@
#pragma once
#include "wallet_security_controller.h"
#include <chrono>
#include <cstdint>
#include <string>
namespace dragonx {
namespace services {
class WalletSecurityWorkflow {
public:
enum class DecryptPhase {
PassphraseEntry = 0,
Working = 1,
Success = 2,
Error = 3
};
enum class DecryptStep {
Unlock = 0,
ExportKeys = 1,
StopDaemon = 2,
BackupWallet = 3,
RestartDaemon = 4,
ImportKeys = 5
};
struct DecryptSnapshot {
DecryptPhase phase = DecryptPhase::PassphraseEntry;
DecryptStep step = DecryptStep::Unlock;
std::string status;
bool inProgress = false;
bool importActive = false;
std::chrono::steady_clock::time_point stepStarted{};
std::chrono::steady_clock::time_point overallStarted{};
};
struct WalletFilePlan {
std::string dataDir;
std::string exportFile;
std::string exportPath;
std::string walletPath;
std::string backupPath;
};
void reset();
void start(std::chrono::steady_clock::time_point now);
void advanceTo(DecryptStep step, std::string status,
std::chrono::steady_clock::time_point now);
void failEntry(std::string status);
void fail(std::string status);
void closeDialogForImport();
void finishImport();
DecryptSnapshot snapshot() const { return state_; }
DecryptPhase phase() const { return state_.phase; }
DecryptStep step() const { return state_.step; }
const std::string& status() const { return state_.status; }
bool inProgress() const { return state_.inProgress; }
bool importActive() const { return state_.importActive; }
bool canClose() const { return state_.phase != DecryptPhase::Working; }
static WalletFilePlan planWalletFiles(const std::string& dataDir,
std::uint64_t timestampSeconds);
static const char* stepStatus(DecryptStep step);
static const char* stepLabel(DecryptStep step);
static int stepIndex(DecryptStep step) { return static_cast<int>(step); }
static DecryptStep stepFromIndex(int step);
static bool stepIsComplete(DecryptStep current, DecryptStep candidate);
private:
DecryptSnapshot state_;
};
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,104 @@
#include "wallet_security_workflow_executor.h"
namespace dragonx {
namespace services {
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::unlockWallet(
const std::string& passphrase, RpcGateway& rpc, int timeoutSeconds)
{
std::string error;
if (!rpc.unlockWallet(passphrase, timeoutSeconds, error)) {
return {false, error.empty() ? "Incorrect passphrase" : error, true};
}
return {true, {}, false};
}
WalletSecurityWorkflowExecutor::ExportOutcome WalletSecurityWorkflowExecutor::exportWallet(
RpcGateway& rpc, FileGateway& files, std::uint64_t timestampSeconds, long timeoutSeconds)
{
ExportOutcome outcome;
outcome.filePlan = WalletSecurityWorkflow::planWalletFiles(files.dataDir(), timestampSeconds);
std::string error;
if (!rpc.exportWallet(outcome.filePlan.exportFile, timeoutSeconds, error)) {
outcome.ok = false;
outcome.error = error.empty() ? "Export failed" : "Export failed: " + error;
return outcome;
}
outcome.ok = true;
return outcome;
}
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::stopDaemon(RpcGateway& rpc)
{
std::string error;
(void)rpc.requestDaemonStop(error);
return {true, {}, false};
}
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::backupEncryptedWallet(
FileGateway& files, const WalletFilePlan& filePlan)
{
std::string error;
if (!files.backupEncryptedWallet(filePlan, error)) {
return {false, error.empty() ? "Failed to rename wallet.dat" : "Failed to rename wallet.dat: " + error, false};
}
return {true, {}, false};
}
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::restartDaemonAndWait(
DaemonGateway& daemon, RpcGateway& rpc, int preRestartDelayMs,
int embeddedRestartSettleMs, int maxProbeSeconds)
{
auto waitForMs = [&](int milliseconds) -> bool {
int remaining = milliseconds;
while (remaining > 0 && !daemon.cancelled() && !daemon.shuttingDown()) {
int slice = remaining >= 100 ? 100 : remaining;
daemon.sleepForMs(slice);
remaining -= slice;
}
return !daemon.cancelled() && !daemon.shuttingDown();
};
if (!waitForMs(preRestartDelayMs)) return {false, "", false};
if (daemon.isUsingEmbeddedDaemon()) {
daemon.stopEmbeddedDaemon();
if (daemon.cancelled() || daemon.shuttingDown()) return {false, "", false};
if (!waitForMs(embeddedRestartSettleMs)) return {false, "", false};
daemon.startEmbeddedDaemon();
}
bool daemonUp = false;
std::string lastError;
for (int i = 0; i < maxProbeSeconds && !daemon.cancelled() && !daemon.shuttingDown(); ++i) {
daemon.sleepForMs(1000);
if (rpc.probeDaemon(lastError)) {
daemonUp = true;
break;
}
}
if (daemon.cancelled() || daemon.shuttingDown()) return {false, "", false};
if (!daemonUp) return {false, "Daemon failed to restart", false};
return {true, {}, false};
}
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::importWallet(
ImportGateway& importer, const std::string& exportPath, long timeoutSeconds)
{
std::string error;
if (!importer.importWallet(exportPath, timeoutSeconds, error)) {
return {false, error.empty() ? "Key import failed" : "Key import failed: " + error, false};
}
return {true, {}, false};
}
void WalletSecurityWorkflowExecutor::cleanupVaultAndPin(const VaultCleanupGateway& cleanup)
{
if (cleanup) cleanup();
}
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,83 @@
#pragma once
#include "wallet_security_workflow.h"
#include <cstdint>
#include <functional>
#include <string>
namespace dragonx {
namespace services {
class WalletSecurityWorkflowExecutor {
public:
using WalletFilePlan = WalletSecurityWorkflow::WalletFilePlan;
struct Outcome {
bool ok = false;
std::string error;
bool passphraseRejected = false;
};
struct ExportOutcome : Outcome {
WalletFilePlan filePlan;
};
class RpcGateway {
public:
virtual ~RpcGateway() = default;
virtual bool unlockWallet(const std::string& passphrase, int timeoutSeconds, std::string& error) = 0;
virtual bool exportWallet(const std::string& fileName, long timeoutSeconds, std::string& error) = 0;
virtual bool requestDaemonStop(std::string& error) = 0;
virtual bool probeDaemon(std::string& error) = 0;
};
class ImportGateway {
public:
virtual ~ImportGateway() = default;
virtual bool importWallet(const std::string& exportPath, long timeoutSeconds, std::string& error) = 0;
};
class FileGateway {
public:
virtual ~FileGateway() = default;
virtual std::string dataDir() = 0;
virtual bool backupEncryptedWallet(const WalletFilePlan& filePlan, std::string& error) = 0;
};
class DaemonGateway {
public:
virtual ~DaemonGateway() = default;
virtual bool isUsingEmbeddedDaemon() const = 0;
virtual void stopEmbeddedDaemon() = 0;
virtual bool startEmbeddedDaemon() = 0;
virtual bool cancelled() const = 0;
virtual bool shuttingDown() const = 0;
virtual void sleepForMs(int milliseconds) = 0;
};
using VaultCleanupGateway = std::function<void()>;
static Outcome unlockWallet(const std::string& passphrase,
RpcGateway& rpc,
int timeoutSeconds = 600);
static ExportOutcome exportWallet(RpcGateway& rpc,
FileGateway& files,
std::uint64_t timestampSeconds,
long timeoutSeconds = 300L);
static Outcome stopDaemon(RpcGateway& rpc);
static Outcome backupEncryptedWallet(FileGateway& files,
const WalletFilePlan& filePlan);
static Outcome restartDaemonAndWait(DaemonGateway& daemon,
RpcGateway& rpc,
int preRestartDelayMs = 2000,
int embeddedRestartSettleMs = 1000,
int maxProbeSeconds = 60);
static Outcome importWallet(ImportGateway& importer,
const std::string& exportPath,
long timeoutSeconds = 1200L);
static void cleanupVaultAndPin(const VaultCleanupGateway& cleanup);
};
} // namespace services
} // namespace dragonx