#include "network_refresh_service.h" #include "../util/logger.h" #include #include #include #include #include using json = nlohmann::json; namespace dragonx { namespace services { namespace { template std::optional readOptional(const json& source, const char* key) { try { if (source.contains(key)) return source[key].get(); } catch (...) {} return std::nullopt; } std::optional readBalanceString(const json& source, const char* key) { try { if (source.contains(key)) return std::stod(source[key].get()); } catch (...) {} return std::nullopt; } void applyBalancesFromUnspent(std::vector& addresses, const json& unspent) { if (!unspent.is_array()) return; std::map balances; for (const auto& output : unspent) { auto address = readOptional(output, "address"); auto amount = readOptional(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; } } bool hasTransactionType(const std::vector& transactions, const std::string& txid, const std::string& type) { for (const auto& transaction : transactions) { if (transaction.txid == txid && transaction.type == type) return true; } return false; } bool tryFetchRawTransaction(NetworkRefreshService::RefreshRpcGateway& rpc, const std::string& txid, json& rawTransaction) { try { rawTransaction = rpc.call("gettransaction", json::array({txid})); return !rawTransaction.is_null(); } catch (...) { return false; } } void applyRawTransactionMetadata(TransactionInfo& info, const json& rawTransaction, bool includeSendDetails) { if (rawTransaction.is_null()) return; if (auto value = readOptional(rawTransaction, "time")) info.timestamp = *value; else if (auto value = readOptional(rawTransaction, "timereceived")) info.timestamp = *value; if (auto value = readOptional(rawTransaction, "confirmations")) info.confirmations = *value; if (!includeSendDetails) return; if (auto value = readOptional(rawTransaction, "amount")) info.amount = *value; if (!rawTransaction.contains("details") || !rawTransaction["details"].is_array()) return; for (const auto& detail : rawTransaction["details"]) { auto category = readOptional(detail, "category"); if (!category || *category != "send") continue; if (auto value = readOptional(detail, "amount")) info.amount = *value; if (auto value = readOptional(detail, "address")) info.address = *value; break; } } void applyRawTransactionMetadata(NetworkRefreshService::TransactionViewCacheEntry& entry, const json& rawTransaction) { if (rawTransaction.is_null()) return; if (auto value = readOptional(rawTransaction, "time")) entry.timestamp = *value; else if (auto value = readOptional(rawTransaction, "timereceived")) entry.timestamp = *value; if (auto value = readOptional(rawTransaction, "confirmations")) entry.confirmations = *value; } void appendTrackedSendPlaceholder(std::vector& transactions, const std::string& txid, const json* rawTransaction) { if (hasTransactionType(transactions, txid, "send")) return; TransactionInfo info; info.txid = txid; info.type = "send"; if (rawTransaction) applyRawTransactionMetadata(info, *rawTransaction, true); transactions.push_back(std::move(info)); } void appendMissingPreviousTransactions(std::vector& transactions, const std::vector& previousTransactions, bool includeAll, const std::unordered_set& pinnedTxids) { for (const auto& transaction : previousTransactions) { bool pinned = pinnedTxids.find(transaction.txid) != pinnedTxids.end(); if (!includeAll && !pinned) { bool shieldedRelated = (!transaction.address.empty() && transaction.address[0] == 'z') || (!transaction.from_address.empty() && transaction.from_address[0] == 'z') || !transaction.memo.empty(); if (!shieldedRelated) continue; } if (hasTransactionType(transactions, transaction.txid, transaction.type)) continue; transactions.push_back(transaction); } } } // namespace NetworkRefreshService::ConnectionInfoResult NetworkRefreshService::parseConnectionInfoResult(const json& info) { ConnectionInfoResult result; if (!info.is_object()) return result; result.ok = true; result.daemonVersion = readOptional(info, "version"); result.protocolVersion = readOptional(info, "protocolversion"); result.p2pPort = readOptional(info, "p2pport"); result.longestChain = readOptional(info, "longestchain"); result.notarized = readOptional(info, "notarized"); result.blocks = readOptional(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(); } 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, const std::optional& prefetchedInfo) { ConnectionInitResult result; if (prefetchedInfo && prefetchedInfo->ok) { result.info = *prefetchedInfo; } else { 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(blockInfo, "blocks"); result.headers = readOptional(blockInfo, "headers"); result.bestBlockHash = readOptional(blockInfo, "bestblockhash"); result.verificationProgress = readOptional(blockInfo, "verificationprogress"); result.longestChain = readOptional(blockInfo, "longestchain"); result.notarized = readOptional(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(); } catch (...) {} } result.miningOk = miningOk && miningInfo.is_object(); if (result.miningOk) { result.generate = readOptional(miningInfo, "generate"); result.genproclimit = readOptional(miningInfo, "genproclimit"); result.blocks = readOptional(miningInfo, "blocks"); result.difficulty = readOptional(miningInfo, "difficulty"); result.networkHashrate = readOptional(miningInfo, "networkhashps"); result.chain = readOptional(miningInfo, "chain"); } return result; } NetworkRefreshService::MiningRefreshResult NetworkRefreshService::collectMiningRefreshResult( RefreshRpcGateway& rpc, double daemonMemoryMb, bool includeSlowRefresh, bool includeLocalHashrate) { json miningInfo; json localHashrate; bool miningOk = false; bool hashrateOk = false; if (includeLocalHashrate) { 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(peer, "id")) info.id = *value; if (auto value = readOptional(peer, "addr")) info.addr = *value; if (auto value = readOptional(peer, "subver")) info.subver = *value; if (auto value = readOptional(peer, "services")) info.services = *value; if (auto value = readOptional(peer, "version")) info.version = *value; if (auto value = readOptional(peer, "conntime")) info.conntime = *value; if (auto value = readOptional(peer, "banscore")) info.banscore = *value; if (auto value = readOptional(peer, "pingtime")) info.pingtime = *value; if (auto value = readOptional(peer, "bytessent")) info.bytessent = *value; if (auto value = readOptional(peer, "bytesrecv")) info.bytesrecv = *value; if (auto value = readOptional(peer, "startingheight")) info.startingheight = *value; if (auto value = readOptional(peer, "synced_headers")) info.synced_headers = *value; if (auto value = readOptional(peer, "synced_blocks")) info.synced_blocks = *value; if (auto value = readOptional(peer, "inbound")) info.inbound = *value; if (auto value = readOptional(peer, "tls_cipher")) info.tls_cipher = *value; if (auto value = readOptional(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(ban, "address")) info.address = *value; if (auto value = readOptional(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::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(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 NetworkRefreshService::parseTransparentAddressList(const json& addressList) { std::vector addresses; if (!addressList.is_array()) return addresses; for (const auto& addressJson : addressList) { try { addresses.push_back(buildTransparentAddressInfo(addressJson.get())); } catch (...) {} } return addresses; } void NetworkRefreshService::applyShieldedBalancesFromUnspent(std::vector& addresses, const json& unspent) { applyBalancesFromUnspent(addresses, unspent); } void NetworkRefreshService::applyTransparentBalancesFromUnspent(std::vector& addresses, const json& unspent) { applyBalancesFromUnspent(addresses, unspent); } NetworkRefreshService::AddressRefreshSnapshot NetworkRefreshService::buildAddressRefreshSnapshot(const WalletState& state) { AddressRefreshSnapshot snapshot; for (const auto& info : state.z_addresses) { if (!info.address.empty()) snapshot.shieldedSpendingKeys[info.address] = info.has_spending_key; } return snapshot; } NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddressRefreshResult( RefreshRpcGateway& rpc, const AddressRefreshSnapshot& snapshot) { 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(); } catch (...) { continue; } auto cached = snapshot.shieldedSpendingKeys.find(address); if (cached != snapshot.shieldedSpendingKeys.end()) { AddressInfo info; info.address = address; info.type = "shielded"; info.has_spending_key = cached->second; result.shieldedAddresses.push_back(std::move(info)); } else { 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(); } 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& 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; snapshot.previousTransactions = state.transactions; return snapshot; } void NetworkRefreshService::appendTransparentTransactions(std::vector& transactions, std::set& knownTxids, const json& result, const std::set& miningAddresses) { if (!result.is_array()) return; for (const auto& transactionJson : result) { TransactionInfo info; if (auto value = readOptional(transactionJson, "txid")) info.txid = *value; if (auto value = readOptional(transactionJson, "category")) info.type = *value; if (auto value = readOptional(transactionJson, "amount")) info.amount = *value; if (auto value = readOptional(transactionJson, "time")) { info.timestamp = *value; } else if (auto received = readOptional(transactionJson, "timereceived")) { info.timestamp = *received; } if (auto value = readOptional(transactionJson, "confirmations")) info.confirmations = *value; if (auto value = readOptional(transactionJson, "address")) info.address = *value; if (info.type == "receive" && !info.address.empty() && miningAddresses.count(info.address)) { info.type = "mined"; } if (!info.txid.empty()) knownTxids.insert(info.txid); transactions.push_back(std::move(info)); } } void NetworkRefreshService::appendShieldedReceivedTransactions(std::vector& transactions, std::set& knownTxids, const std::string& address, const json& received, const std::set& miningAddresses) { if (received.is_null() || !received.is_array()) return; for (const auto& note : received) { auto txid = readOptional(note, "txid"); if (!txid || txid->empty()) continue; auto change = readOptional(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 = miningAddresses.count(address) ? "mined" : "receive"; info.address = address; if (auto value = readOptional(note, "amount")) info.amount = *value; if (auto value = readOptional(note, "confirmations")) info.confirmations = *value; if (auto value = readOptional(note, "time")) info.timestamp = *value; if (auto value = readOptional(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(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(output, "outgoing")) outgoing = *value; if (!outgoing) continue; TransactionViewCacheEntry::Output out; if (auto value = readOptional(output, "address")) out.address = *value; if (auto value = readOptional(output, "value")) out.value = *value; if (auto value = readOptional(output, "memoStr")) out.memo = *value; entry.outgoing_outputs.push_back(std::move(out)); } } return entry; } void NetworkRefreshService::appendViewTransactionOutputs(std::vector& 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; bool foundExistingMetadata = false; for (const auto& existing : transactions) { if (existing.txid == txid) { info.confirmations = existing.confirmations; info.timestamp = existing.timestamp; foundExistingMetadata = true; break; } } if (!foundExistingMetadata) { info.confirmations = entry.confirmations; info.timestamp = entry.timestamp; } transactions.push_back(std::move(info)); } } void NetworkRefreshService::sortTransactionsNewestFirst(std::vector& 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; result.shieldedAddressCount = snapshot.shieldedAddresses.size(); result.shieldedScanHeights = snapshot.shieldedScanHeights; std::set knownTxids; bool transactionRpcError = false; try { constexpr int pageSize = 1000; int skip = 0; while (true) { json transactions = rpc.call("listtransactions", json::array({"", pageSize, skip})); appendTransparentTransactions(result.transactions, knownTxids, transactions, snapshot.miningAddresses); if (!transactions.is_array() || transactions.size() < static_cast(pageSize)) break; skip += pageSize; } } catch (const std::exception& e) { transactionRpcError = true; DEBUG_LOGF("listtransactions error: %s\n", e.what()); } auto scannedAtTip = [&](const std::string& address) { if (currentBlockHeight < 0) return false; auto it = result.shieldedScanHeights.find(address); return it != result.shieldedScanHeights.end() && it->second >= currentBlockHeight; }; std::size_t shieldedStart = snapshot.shieldedScanStartIndex; if (shieldedStart >= snapshot.shieldedAddresses.size()) shieldedStart = 0; std::size_t nextSearchIndex = shieldedStart; for (std::size_t index = shieldedStart; index < snapshot.shieldedAddresses.size(); ++index) { const auto& address = snapshot.shieldedAddresses[index]; if (scannedAtTip(address)) { nextSearchIndex = index + 1; continue; } if (snapshot.maxShieldedReceiveScans > 0 && result.shieldedAddressesScanned >= snapshot.maxShieldedReceiveScans) { nextSearchIndex = index; break; } ++result.shieldedAddressesScanned; nextSearchIndex = index + 1; try { json received = rpc.call("z_listreceivedbyaddress", json::array({address, 0})); appendShieldedReceivedTransactions(result.transactions, knownTxids, address, received, snapshot.miningAddresses); if (currentBlockHeight >= 0) result.shieldedScanHeights[address] = currentBlockHeight; } catch (const std::exception& e) { transactionRpcError = true; DEBUG_LOGF("z_listreceivedbyaddress error: %s\n", e.what()); } } auto findPendingShieldedIndex = [&]() -> std::optional { if (snapshot.shieldedAddresses.empty()) return std::nullopt; std::size_t start = nextSearchIndex < snapshot.shieldedAddresses.size() ? nextSearchIndex : 0; for (std::size_t offset = 0; offset < snapshot.shieldedAddresses.size(); ++offset) { std::size_t index = (start + offset) % snapshot.shieldedAddresses.size(); if (!scannedAtTip(snapshot.shieldedAddresses[index])) return index; } return std::nullopt; }; auto pendingShieldedIndex = findPendingShieldedIndex(); result.shieldedScanComplete = !pendingShieldedIndex.has_value(); result.nextShieldedScanStartIndex = pendingShieldedIndex.value_or(0); for (const auto& txid : snapshot.sendTxids) { knownTxids.insert(txid); } for (const auto& [txid, cachedView] : snapshot.viewTxCache) { if (!cachedView.outgoing_outputs.empty()) knownTxids.insert(txid); } int viewTxCount = 0; for (const auto& txid : knownTxids) { bool trackedSend = snapshot.sendTxids.count(txid) > 0; auto cached = snapshot.viewTxCache.find(txid); if (cached != snapshot.viewTxCache.end()) { if (!trackedSend || !cached->second.outgoing_outputs.empty()) { appendViewTransactionOutputs(result.transactions, txid, cached->second); continue; } } if (!trackedSend && snapshot.fullyEnrichedTxids.count(txid)) continue; if (viewTxCount >= maxViewTransactionsPerCycle) break; ++viewTxCount; try { json viewTransaction = rpc.call("z_viewtransaction", json::array({txid})); if (viewTransaction.is_null() || !viewTransaction.is_object()) { if (trackedSend) { json rawTransaction; bool hasRawTransaction = tryFetchRawTransaction(rpc, txid, rawTransaction); appendTrackedSendPlaceholder(result.transactions, txid, hasRawTransaction ? &rawTransaction : nullptr); } continue; } auto entry = parseViewTransactionCacheEntry(viewTransaction); appendViewTransactionOutputs(result.transactions, txid, entry); json rawTransaction; bool hasRawTransaction = false; bool needsRawTransaction = false; for (const auto& info : result.transactions) { if (info.txid == txid && info.timestamp == 0) { needsRawTransaction = true; break; } } if (needsRawTransaction) { hasRawTransaction = tryFetchRawTransaction(rpc, txid, rawTransaction); if (hasRawTransaction) { applyRawTransactionMetadata(entry, rawTransaction); for (auto& info : result.transactions) { if (info.txid == txid && info.timestamp == 0) { applyRawTransactionMetadata(info, rawTransaction, false); } } } } if (trackedSend && !hasTransactionType(result.transactions, txid, "send")) { if (!hasRawTransaction) hasRawTransaction = tryFetchRawTransaction(rpc, txid, rawTransaction); appendTrackedSendPlaceholder(result.transactions, txid, hasRawTransaction ? &rawTransaction : nullptr); } if (!trackedSend || !entry.outgoing_outputs.empty()) { result.newViewTxEntries[txid] = std::move(entry); } } catch (const std::exception& e) { (void)e; if (trackedSend) { json rawTransaction; bool hasRawTransaction = tryFetchRawTransaction(rpc, txid, rawTransaction); appendTrackedSendPlaceholder(result.transactions, txid, hasRawTransaction ? &rawTransaction : nullptr); } } } if (!snapshot.previousTransactions.empty()) { if (transactionRpcError) { appendMissingPreviousTransactions(result.transactions, snapshot.previousTransactions, true, snapshot.pendingOpids); } else if (snapshot.shieldedAddresses.empty() || !result.shieldedScanComplete || result.shieldedAddressesScanned < result.shieldedAddressCount) { appendMissingPreviousTransactions(result.transactions, snapshot.previousTransactions, false, snapshot.pendingOpids); } } sortTransactionsNewestFirst(result.transactions); return result; } NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRecentTransactionRefreshResult( RefreshRpcGateway& rpc, const TransactionRefreshSnapshot& snapshot, int currentBlockHeight, int pageSize) { TransactionRefreshResult result; result.blockHeight = currentBlockHeight; result.transactions = snapshot.previousTransactions; result.shieldedAddressCount = snapshot.shieldedAddresses.size(); result.shieldedScanHeights = snapshot.shieldedScanHeights; try { std::set recentTxids; std::vector recentTransactions; json transactions = rpc.call("listtransactions", json::array({"", pageSize, 0})); appendTransparentTransactions(recentTransactions, recentTxids, transactions, snapshot.miningAddresses); for (auto& recent : recentTransactions) { bool replaced = false; for (auto& existing : result.transactions) { if (existing.txid == recent.txid && existing.type == recent.type) { existing = recent; replaced = true; break; } } if (!replaced) result.transactions.push_back(std::move(recent)); } } catch (const std::exception& e) { DEBUG_LOGF("recent listtransactions error: %s\n", e.what()); } std::size_t shieldedStart = snapshot.shieldedScanStartIndex; if (shieldedStart >= snapshot.shieldedAddresses.size()) shieldedStart = 0; std::size_t shieldedLimit = snapshot.shieldedAddresses.size() - shieldedStart; if (snapshot.maxShieldedReceiveScans > 0) { shieldedLimit = std::min(shieldedLimit, snapshot.maxShieldedReceiveScans); } for (std::size_t index = shieldedStart; index < shieldedStart + shieldedLimit; ++index) { const auto& address = snapshot.shieldedAddresses[index]; try { std::vector scannedTransactions; std::set scannedTxids; json received = rpc.call("z_listreceivedbyaddress", json::array({address, 0})); appendShieldedReceivedTransactions(scannedTransactions, scannedTxids, address, received, snapshot.miningAddresses); for (auto& scanned : scannedTransactions) { bool replaced = false; for (auto& existing : result.transactions) { if (existing.txid == scanned.txid && existing.type == scanned.type) { existing = scanned; replaced = true; break; } } if (!replaced) result.transactions.push_back(std::move(scanned)); } if (currentBlockHeight >= 0) result.shieldedScanHeights[address] = currentBlockHeight; ++result.shieldedAddressesScanned; } catch (const std::exception& e) { DEBUG_LOGF("recent z_listreceivedbyaddress error: %s\n", e.what()); } } result.nextShieldedScanStartIndex = (shieldedStart + shieldedLimit >= snapshot.shieldedAddresses.size()) ? 0 : shieldedStart + shieldedLimit; result.shieldedScanComplete = true; sortTransactionsNewestFirst(result.transactions); return result; } NetworkRefreshService::OperationStatusPollResult NetworkRefreshService::parseOperationStatusPoll( const json& result, const std::vector& requestedOpids) { OperationStatusPollResult parsed; if (!result.is_array()) return parsed; std::set reported; for (const auto& op : result) { if (!op.is_object()) continue; std::string opid = op.value("id", std::string()); if (opid.empty()) continue; reported.insert(opid); std::string status = op.value("status", std::string()); if (status == "success") { parsed.doneOpids.push_back(opid); parsed.anySuccess = true; if (op.contains("result") && op["result"].is_object() && op["result"].contains("txid")) { try { std::string txid = op["result"]["txid"].get(); if (!txid.empty()) { parsed.successTxids.push_back(txid); parsed.successTxidsByOpid[opid] = std::move(txid); } } catch (...) {} } } else if (status == "failed" || status == "cancelled" || status == "canceled") { parsed.doneOpids.push_back(opid); std::string msg = status == "failed" ? "Transaction failed" : "Transaction cancelled"; if (op.contains("error") && op["error"].is_object() && op["error"].contains("message")) { try { msg = op["error"]["message"].get(); } catch (...) {} } parsed.failureMessages.push_back(msg); } } for (const auto& opid : requestedOpids) { if (!opid.empty() && reported.find(opid) == reported.end()) { parsed.staleOpids.push_back(opid); } } return parsed; } 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.bestBlockHash) state.sync.best_blockhash = *result.bestBlockHash; 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) { bool hasOutgoingOutputs = !entry.outgoing_outputs.empty(); cacheUpdate.viewTxCache[txid] = std::move(entry); if (hasOutgoingOutputs) 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 transactionsDirty) const { return scheduler_.shouldRefreshTransactions(lastTxBlockHeight, currentBlockHeight, 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& 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& 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(job); constexpr std::size_t count = static_cast(Job::Count); return index < count ? index : 0; } } // namespace services } // namespace dragonx