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:
976
src/services/network_refresh_service.cpp
Normal file
976
src/services/network_refresh_service.cpp
Normal 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
|
||||
Reference in New Issue
Block a user