Security audit remediation (15 confirmed findings from the codebase audit): - H-02: scrub+delete the decrypt-flow plaintext key export on ALL exit paths (RAII guard) and purge stale obsidiandecryptexport* files at startup. - M-01/L-03/L-04/L-05/L-07: sodium_memzero the Set-PIN and encrypt-PIN worker passphrase/PIN copies, the RPC Basic-auth string (auth_), the exported/imported key buffers (App::wipeSecrets, called from ~App and before main's _Exit), and the first-run wizard "Skip" buffers. - M-03/M-04/M-05/L-06: return locked COPIES from XmrigManager/EmbeddedDaemon getters (dedicated error_mutex_; DaemonController::lastError now by value), route xmrig last_error_ writes through a locked setter, and wrap shutdown_status_/wizard_stop_status_ in a locking GuardedStatus (wizard_stopping_external_ -> std::atomic). - M-02: persist after a console send/shield/import in the lite backend. - L-01: require the confirm click for z_shieldcoinbase/z_mergetoaddress. - L-02: quote/escape each Windows daemon argv per the MSDN CommandLineToArgvW rules. - L-08: pin json/tomlplusplus/libwebp FetchContent to immutable commit SHAs. - I-01: extract updater archives from the already-verified in-memory buffer (no disk re-read TOCTOU). Feature: warn once (full-node) when the active wallet loads empty while a sibling wallet file in the datadir holds keys. A funded salvage wallet.<ts>.bak routes to the recovery/Restore flow; a funded sibling .dat routes to the wallet manager. Per-wallet-file dismissal; gated on synced + address-list-loaded to avoid false positives on warm reconnect / spent-down wallets. UX fixes: - send: show the TOTAL balance (with a spendable "available" note) in the source dropdown and keep pending-change addresses visible. - chat: insert emoji at the cursor position; restrict new-chat recipients to shielded (z) addresses. - console: optional auto-focus of the command input on tab open (off by default). - shutdown: when "stop external daemon" is on, keep the shutdown screen up until the external node actually exits, showing live status. Adversarially reviewed; verified across full-node, lite, and Windows builds; tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1688 lines
71 KiB
C++
1688 lines
71 KiB
C++
#include "network_refresh_service.h"
|
|
#include "../util/logger.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <map>
|
|
#include <set>
|
|
#include <unordered_map>
|
|
|
|
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;
|
|
|
|
// Partition each note/utxo by its per-entry "confirmations": `total` (minconf=0 — DISPLAY, includes
|
|
// the user's own pending 0-conf change) vs `spendable` (confirmations>=1 — what z_sendmany, run at
|
|
// minconf=1, can actually spend). This lets a single z_listunspent(0)/listunspent(0) feed both.
|
|
std::map<std::string, double> total;
|
|
std::map<std::string, double> spendable;
|
|
for (const auto& output : unspent) {
|
|
auto address = readOptional<std::string>(output, "address");
|
|
auto amount = readOptional<double>(output, "amount");
|
|
if (!address || !amount) continue;
|
|
total[*address] += *amount;
|
|
auto conf = readOptional<int>(output, "confirmations");
|
|
if (conf && *conf >= 1) spendable[*address] += *amount;
|
|
}
|
|
|
|
// The address lists are rebuilt fresh (default 0) each refresh, so hard-set both — an address with no
|
|
// notes in this set is 0, and spendableBalance is always a subset sum of balance.
|
|
for (auto& info : addresses) {
|
|
auto t = total.find(info.address);
|
|
auto s = spendable.find(info.address);
|
|
info.balance = (t != total.end()) ? t->second : 0.0;
|
|
info.spendableBalance = (s != spendable.end()) ? s->second : 0.0;
|
|
}
|
|
}
|
|
|
|
bool hasTransactionType(const std::vector<TransactionInfo>& 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<std::int64_t>(rawTransaction, "time")) info.timestamp = *value;
|
|
else if (auto value = readOptional<std::int64_t>(rawTransaction, "timereceived")) info.timestamp = *value;
|
|
if (auto value = readOptional<int>(rawTransaction, "confirmations")) info.confirmations = *value;
|
|
|
|
if (!includeSendDetails) return;
|
|
|
|
if (auto value = readOptional<double>(rawTransaction, "amount")) info.amount = *value;
|
|
|
|
if (!rawTransaction.contains("details") || !rawTransaction["details"].is_array()) return;
|
|
for (const auto& detail : rawTransaction["details"]) {
|
|
auto category = readOptional<std::string>(detail, "category");
|
|
if (!category || *category != "send") continue;
|
|
|
|
if (auto value = readOptional<double>(detail, "amount")) info.amount = *value;
|
|
if (auto value = readOptional<std::string>(detail, "address")) info.address = *value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void applyRawTransactionMetadata(NetworkRefreshService::TransactionViewCacheEntry& entry,
|
|
const json& rawTransaction)
|
|
{
|
|
if (rawTransaction.is_null()) return;
|
|
|
|
if (auto value = readOptional<std::int64_t>(rawTransaction, "time")) entry.timestamp = *value;
|
|
else if (auto value = readOptional<std::int64_t>(rawTransaction, "timereceived")) entry.timestamp = *value;
|
|
if (auto value = readOptional<int>(rawTransaction, "confirmations")) entry.confirmations = *value;
|
|
}
|
|
|
|
void appendTrackedSendPlaceholder(std::vector<TransactionInfo>& 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<TransactionInfo>& transactions,
|
|
const std::vector<TransactionInfo>& previousTransactions,
|
|
bool includeAll,
|
|
const std::unordered_set<std::string>& 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);
|
|
}
|
|
}
|
|
|
|
using HushChatMemoOutputMap = std::unordered_map<std::string, std::vector<chat::HushChatMemoOutput>>;
|
|
|
|
std::size_t readMemoPosition(const json& source, std::size_t fallback)
|
|
{
|
|
for (const char* key : {"position", "outputIndex", "outindex"}) {
|
|
auto value = readOptional<int>(source, key);
|
|
if (value && *value >= 0) return static_cast<std::size_t>(*value);
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
void recordHushChatMemoOutput(HushChatMemoOutputMap* outputs,
|
|
const std::string& txid,
|
|
const std::string& memo,
|
|
std::size_t position)
|
|
{
|
|
if (!outputs || txid.empty() || memo.empty()) return;
|
|
(*outputs)[txid].push_back(chat::HushChatMemoOutput{position, memo});
|
|
}
|
|
|
|
void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetadata>& destination,
|
|
const std::string& txid,
|
|
const std::vector<chat::HushChatMemoOutput>& outputs)
|
|
{
|
|
auto extracted = chat::extractHushChatTransactionMetadata(chat::HushChatTransactionInput{txid, outputs});
|
|
for (auto& metadata : extracted.metadata) {
|
|
destination.push_back(std::move(metadata));
|
|
}
|
|
}
|
|
|
|
void appendExtractedHushChatMetadata(std::vector<chat::HushChatTransactionMetadata>& destination,
|
|
const HushChatMemoOutputMap& outputsByTxid)
|
|
{
|
|
if (!chat::hushChatFeatureEnabledAtBuild()) return;
|
|
|
|
for (const auto& [txid, outputs] : outputsByTxid) {
|
|
appendExtractedHushChatMetadata(destination, txid, outputs);
|
|
}
|
|
}
|
|
|
|
} // 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,
|
|
const std::optional<ConnectionInfoResult>& 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, const json& spendableBalance, 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");
|
|
}
|
|
if (spendableBalance.is_object()) { // confirmed totals (minconf=1); left unset on old daemons
|
|
result.spendableShieldedBalance = readBalanceString(spendableBalance, "private");
|
|
result.spendableTransparentBalance = readBalanceString(spendableBalance, "transparent");
|
|
result.spendableTotalBalance = readBalanceString(spendableBalance, "total");
|
|
}
|
|
|
|
result.blockchainOk = blockOk && blockInfo.is_object();
|
|
if (result.blockchainOk) {
|
|
result.blocks = readOptional<int>(blockInfo, "blocks");
|
|
result.headers = readOptional<int>(blockInfo, "headers");
|
|
result.bestBlockHash = readOptional<std::string>(blockInfo, "bestblockhash");
|
|
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, bool includeBalance)
|
|
{
|
|
json totalBalance;
|
|
json spendableBalance;
|
|
json blockInfo;
|
|
bool balanceOk = false;
|
|
bool blockOk = false;
|
|
|
|
if (includeBalance) {
|
|
try { // DISPLAY total: minconf=0 — includes the user's own pending change so it doesn't crater
|
|
totalBalance = rpc.call("z_gettotalbalance", json::array({0}));
|
|
balanceOk = true;
|
|
} catch (const std::exception& e) {
|
|
DEBUG_LOGF("Balance error: %s\n", e.what());
|
|
}
|
|
try { // SPENDABLE total: minconf=1 (confirmed). If absent, spendable degrades to display in apply.
|
|
spendableBalance = rpc.call("z_gettotalbalance", json::array({1}));
|
|
} catch (...) {}
|
|
}
|
|
|
|
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, spendableBalance, 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,
|
|
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<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"];
|
|
// CoinGecko emits JSON null (not an omitted key) for fields it can't currently compute —
|
|
// commonly usd_24h_change on illiquid/newly-listed tokens — while still returning a valid
|
|
// spot price in the same object. .value(key, default) throws type_error on a PRESENT null,
|
|
// which the outer catch turns into "no price update at all", so read null-tolerantly and
|
|
// keep the valid usd/btc rather than discarding the whole refresh.
|
|
auto num = [&data](const char* key, double def) {
|
|
auto it = data.find(key);
|
|
return (it != data.end() && it->is_number()) ? it->get<double>() : def;
|
|
};
|
|
PriceRefreshResult result;
|
|
result.market.price_usd = num("usd", 0.0);
|
|
result.market.price_btc = num("btc", 0.0);
|
|
result.market.change_24h = num("usd_24h_change", 0.0);
|
|
result.market.volume_24h = num("usd_24h_vol", 0.0);
|
|
result.market.market_cap = num("usd_market_cap", 0.0);
|
|
|
|
char buf[64];
|
|
// Runs on the RPC worker thread — std::localtime shares a process-wide static tm, so use the
|
|
// reentrant variant into a local tm (matches the rest of the codebase).
|
|
std::tm tmv{};
|
|
#ifdef _WIN32
|
|
localtime_s(&tmv, &fetchedAt);
|
|
#else
|
|
localtime_r(&fetchedAt, &tmv);
|
|
#endif
|
|
if (std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tmv) > 0) {
|
|
result.market.last_updated = buf;
|
|
}
|
|
return result;
|
|
} catch (...) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
std::vector<std::pair<std::time_t, double>> NetworkRefreshService::parseCoinGeckoMarketChart(
|
|
const std::string& response)
|
|
{
|
|
std::vector<std::pair<std::time_t, double>> series;
|
|
try {
|
|
auto parsed = json::parse(response);
|
|
if (!parsed.contains("prices") || !parsed["prices"].is_array()) return series;
|
|
const auto& prices = parsed["prices"];
|
|
series.reserve(prices.size());
|
|
for (const auto& row : prices) {
|
|
// Each row is [milliseconds, price]; skip anything malformed.
|
|
if (!row.is_array() || row.size() < 2) continue;
|
|
if (!row[0].is_number() || !row[1].is_number()) continue;
|
|
const double ms = row[0].get<double>();
|
|
const double price = row[1].get<double>();
|
|
series.emplace_back(static_cast<std::time_t>(ms / 1000.0), price);
|
|
}
|
|
} catch (...) {
|
|
series.clear();
|
|
}
|
|
return series;
|
|
}
|
|
|
|
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::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<std::string>();
|
|
} 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());
|
|
result.addressListOk = false; // enumeration failed → the shielded list may be falsely short
|
|
}
|
|
|
|
try {
|
|
json unspent = rpc.call("z_listunspent", json::array({0, 9999999, false})); // minconf=0 → include 0-conf change
|
|
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 { // display total (minconf=0, includes pending change)
|
|
json total = rpc.call("z_getbalance", json::array({info.address, 0}));
|
|
if (!total.is_null()) info.balance = total.get<double>();
|
|
} catch (...) {}
|
|
try { // spendable (minconf=1); degrade to the display value on old daemons
|
|
json conf = rpc.call("z_getbalance", json::array({info.address, 1}));
|
|
info.spendableBalance = (!conf.is_null()) ? conf.get<double>() : info.balance;
|
|
} catch (...) { info.spendableBalance = info.balance; }
|
|
}
|
|
}
|
|
|
|
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());
|
|
result.addressListOk = false; // enumeration failed → the transparent list may be falsely short
|
|
}
|
|
|
|
try {
|
|
json unspent = rpc.call("listunspent", json::array({0})); // minconf=0 → include 0-conf change
|
|
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;
|
|
snapshot.previousTransactions = state.transactions;
|
|
return snapshot;
|
|
}
|
|
|
|
void NetworkRefreshService::appendTransparentTransactions(std::vector<TransactionInfo>& transactions,
|
|
std::set<std::string>& knownTxids,
|
|
const json& result,
|
|
const std::set<std::string>& miningAddresses)
|
|
{
|
|
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.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<TransactionInfo>& transactions,
|
|
std::set<std::string>& knownTxids,
|
|
const std::string& address,
|
|
const json& received,
|
|
const std::set<std::string>& miningAddresses,
|
|
HushChatMemoOutputMap* chatMemoOutputs)
|
|
{
|
|
if (received.is_null() || !received.is_array()) return;
|
|
|
|
std::size_t fallbackPosition = 0;
|
|
for (const auto& note : received) {
|
|
const std::size_t notePosition = readMemoPosition(note, fallbackPosition++);
|
|
auto txid = readOptional<std::string>(note, "txid");
|
|
if (!txid || txid->empty()) continue;
|
|
|
|
auto change = readOptional<bool>(note, "change");
|
|
if (change && *change) continue;
|
|
|
|
auto memoValue = readOptional<std::string>(note, "memoStr");
|
|
if (memoValue) recordHushChatMemoOutput(chatMemoOutputs, *txid, *memoValue, notePosition);
|
|
|
|
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<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 (memoValue) info.memo = *memoValue;
|
|
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()) {
|
|
std::size_t fallbackPosition = 0;
|
|
for (const auto& output : viewTransaction["outputs"]) {
|
|
const std::size_t outputPosition = readMemoPosition(output, fallbackPosition++);
|
|
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;
|
|
out.position = outputPosition;
|
|
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;
|
|
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<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;
|
|
result.shieldedAddressCount = snapshot.shieldedAddresses.size();
|
|
result.shieldedScanHeights = snapshot.shieldedScanHeights;
|
|
|
|
std::set<std::string> knownTxids;
|
|
HushChatMemoOutputMap hushChatReceivedOutputs;
|
|
HushChatMemoOutputMap* hushChatReceivedOutputsPtr = chat::hushChatFeatureEnabledAtBuild()
|
|
? &hushChatReceivedOutputs
|
|
: nullptr;
|
|
|
|
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<std::size_t>(pageSize)) break;
|
|
skip += pageSize;
|
|
}
|
|
} catch (const std::exception& e) {
|
|
transactionRpcError = true;
|
|
DEBUG_LOGF("listtransactions error: %s\n", e.what());
|
|
}
|
|
|
|
// An address counts as "scanned at tip" if its last scan is within shieldedScanTipTolerance
|
|
// blocks of the current tip. Tolerance 0 means strict (exact tip); a small tolerance lets a
|
|
// multi-cycle pass over many addresses complete even as the tip advances during it.
|
|
const int scanStaleThreshold = currentBlockHeight - std::max(0, snapshot.shieldedScanTipTolerance);
|
|
auto scannedAtTip = [&](const std::string& address) {
|
|
if (currentBlockHeight < 0) return false;
|
|
auto it = result.shieldedScanHeights.find(address);
|
|
return it != result.shieldedScanHeights.end() && it->second >= scanStaleThreshold;
|
|
};
|
|
|
|
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,
|
|
hushChatReceivedOutputsPtr);
|
|
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<std::size_t> {
|
|
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);
|
|
// NB: do NOT extract chat metadata from our OWN outgoing memos here — ingest marks
|
|
// everything Incoming, so that duplicates every sent message as a phantom "from peer"
|
|
// entry. Genuine incoming comes from the z_listreceivedbyaddress path; our sends are
|
|
// recorded by the local echo. (The recent-refresh variant already omits this.)
|
|
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);
|
|
// (Chat metadata is harvested only from RECEIVED notes, not our own outgoing memos — see
|
|
// the cached-view branch above.)
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
appendExtractedHushChatMetadata(result.hushChatMetadata, hushChatReceivedOutputs);
|
|
|
|
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;
|
|
HushChatMemoOutputMap hushChatReceivedOutputs;
|
|
HushChatMemoOutputMap* hushChatReceivedOutputsPtr = chat::hushChatFeatureEnabledAtBuild()
|
|
? &hushChatReceivedOutputs
|
|
: nullptr;
|
|
|
|
try {
|
|
std::set<std::string> recentTxids;
|
|
std::vector<TransactionInfo> 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<TransactionInfo> scannedTransactions;
|
|
std::set<std::string> scannedTxids;
|
|
json received = rpc.call("z_listreceivedbyaddress", json::array({address, 0}));
|
|
appendShieldedReceivedTransactions(scannedTransactions,
|
|
scannedTxids,
|
|
address,
|
|
received,
|
|
snapshot.miningAddresses,
|
|
hushChatReceivedOutputsPtr);
|
|
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;
|
|
|
|
appendExtractedHushChatMetadata(result.hushChatMetadata, hushChatReceivedOutputs);
|
|
|
|
sortTransactionsNewestFirst(result.transactions);
|
|
return result;
|
|
}
|
|
|
|
NetworkRefreshService::OperationStatusPollResult NetworkRefreshService::parseOperationStatusPoll(
|
|
const json& result,
|
|
const std::vector<std::string>& requestedOpids)
|
|
{
|
|
OperationStatusPollResult parsed;
|
|
if (!result.is_array()) return parsed;
|
|
|
|
// We poll z_getoperationstatus with no filter (this daemon rejects the per-opid filtered form),
|
|
// so the result lists ALL operations. Only act on the ones we're tracking — otherwise an
|
|
// unrelated failed/old operation would fire a spurious error toast or pollute send state.
|
|
const std::set<std::string> requested(requestedOpids.begin(), requestedOpids.end());
|
|
|
|
std::set<std::string> reported;
|
|
for (const auto& op : result) {
|
|
if (!op.is_object()) continue;
|
|
// Type-checked reads: .value(key, default) throws if the key is PRESENT with a non-string
|
|
// type, which would abort the whole poll (and wedge it for the session — see the call site).
|
|
if (!op.contains("id") || !op["id"].is_string()) continue;
|
|
std::string opid = op["id"].get<std::string>();
|
|
if (opid.empty()) continue;
|
|
if (requested.find(opid) == requested.end()) continue; // not one of ours — ignore
|
|
reported.insert(opid);
|
|
|
|
std::string status = (op.contains("status") && op["status"].is_string())
|
|
? op["status"].get<std::string>() : 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<std::string>();
|
|
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<std::string>();
|
|
} catch (...) {}
|
|
}
|
|
parsed.failureMessages.push_back(msg);
|
|
parsed.failureByOpid[opid] = 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;
|
|
// Confirmed/spendable totals; if the minconf=1 call was unavailable (old daemon) degrade to the
|
|
// display value so nothing is *over*-reported as spendable (z_sendmany stays the final gate).
|
|
state.spendablePrivateBalance = result.spendableShieldedBalance.value_or(state.privateBalance);
|
|
state.spendableTransparentBalance = result.spendableTransparentBalance.value_or(state.transparentBalance);
|
|
state.spendableTotalBalance = result.spendableTotalBalance.value_or(state.totalBalance);
|
|
state.unconfirmedBalance = std::max(0.0, state.totalBalance - state.spendableTotalBalance);
|
|
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);
|
|
// Keep the combined `addresses` view in sync with the authoritative z/t lists.
|
|
// Consumers that read state.addresses (Market-tab portfolio, auto-shield target
|
|
// selection, pool-mining transparent fallback) depend on this being rebuilt — the
|
|
// full-node refresh path is the only place that bulk-updates the address lists.
|
|
state.rebuildAddressList();
|
|
}
|
|
|
|
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);
|
|
|
|
// Bump every job's generation so any ticket minted before this reset (a refresh still
|
|
// in flight on the worker when a disconnect cleared state_) is treated as stale by
|
|
// completeDispatch — preventing last-connection data from landing on the new session.
|
|
for (auto& generation : generations_) {
|
|
generation.fetch_add(1, std::memory_order_acq_rel);
|
|
}
|
|
}
|
|
|
|
NetworkRefreshService::DaemonRescanScan NetworkRefreshService::parseDaemonRescanOutput(const std::string& output)
|
|
{
|
|
DaemonRescanScan scan;
|
|
bool& foundRescan = scan.foundRescan;
|
|
bool& finished = scan.finished;
|
|
float& rescanPct = scan.rescanPct;
|
|
std::string& lastStatus = scan.lastStatus;
|
|
|
|
// Sapling witness rebuild progress. Two daemon signals:
|
|
// "Setting Initial Sapling Witness for tx <hash>, <i> of <N>" (the common one —
|
|
// one per wallet tx; <hash> is the stable per-tx key, <N> the total tx count)
|
|
// "Building Witnesses for block <h> <frac> complete, <n> remaining" (rarer)
|
|
bool& foundWitness = scan.foundWitness;
|
|
float& witnessPct = scan.witnessPct; // -1 = no fraction parsed this batch
|
|
int& witnessRemaining = scan.witnessRemaining;
|
|
int& witnessReadTotal = scan.witnessReadTotal; // new daemon: the <total> in "<done> / <total>"
|
|
bool& witnessRebuilt = scan.witnessRebuilt; // new daemon's completion line seen this batch
|
|
std::vector<std::string>& witnessTxids = scan.witnessTxids; // distinct-tx keys seen this batch
|
|
int& witnessTotalTxs = scan.witnessTotalTxs; // max N seen this batch
|
|
|
|
size_t pos = 0;
|
|
while (pos < output.size()) {
|
|
size_t eol = output.find('\n', pos);
|
|
if (eol == std::string::npos) eol = output.size();
|
|
std::string line = output.substr(pos, eol - pos);
|
|
pos = eol + 1;
|
|
|
|
if (line.find("Rescanning from height") != std::string::npos ||
|
|
line.find("Rescanning last") != std::string::npos) {
|
|
foundRescan = true;
|
|
lastStatus = line;
|
|
}
|
|
|
|
auto stillIdx = line.find("Still rescanning");
|
|
if (stillIdx != std::string::npos) {
|
|
foundRescan = true;
|
|
auto progIdx = line.find("Progress=");
|
|
if (progIdx != std::string::npos) {
|
|
size_t numStart = progIdx + 9;
|
|
size_t numEnd = numStart;
|
|
while (numEnd < line.size() && (std::isdigit(line[numEnd]) || line[numEnd] == '.')) {
|
|
numEnd++;
|
|
}
|
|
if (numEnd > numStart) {
|
|
try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {}
|
|
}
|
|
}
|
|
lastStatus = line;
|
|
}
|
|
|
|
auto rescIdx = line.find("Rescanning...");
|
|
if (rescIdx != std::string::npos) {
|
|
foundRescan = true;
|
|
auto pctIdx = line.find('%');
|
|
if (pctIdx != std::string::npos && pctIdx > 0) {
|
|
size_t numEnd = pctIdx;
|
|
size_t numStart = numEnd;
|
|
while (numStart > 0 && (std::isdigit(line[numStart - 1]) || line[numStart - 1] == '.')) {
|
|
numStart--;
|
|
}
|
|
if (numStart < numEnd) {
|
|
try { rescanPct = std::stof(line.substr(numStart, numEnd - numStart)); } catch (...) {}
|
|
}
|
|
}
|
|
lastStatus = line;
|
|
}
|
|
|
|
if (line.find("Done rescanning") != std::string::npos ||
|
|
line.find("Rescan complete") != std::string::npos) {
|
|
finished = true;
|
|
}
|
|
|
|
// This daemon prints no "Done rescanning" line; instead it logs the rescan
|
|
// benchmark timing exactly when the scan finishes, e.g.:
|
|
// "... rescan 16760577ms"
|
|
// Match the lowercase " rescan " bench category ending in "<digits>ms" (the
|
|
// progress lines are "Still rescanning"/"Rescanning...", which never end in ms).
|
|
if (line.find(" rescan ") != std::string::npos) {
|
|
std::string t = line;
|
|
while (!t.empty() && (t.back() == '\r' || t.back() == '\n' || t.back() == ' '))
|
|
t.pop_back();
|
|
if (t.size() >= 3 && t.compare(t.size() - 2, 2, "ms") == 0 &&
|
|
std::isdigit(static_cast<unsigned char>(t[t.size() - 3]))) {
|
|
finished = true;
|
|
}
|
|
}
|
|
|
|
// Sapling note witness rebuild: "Building Witnesses for block <h> <frac>
|
|
// complete, <n> remaining". The float before "complete" is a 0..1 fraction.
|
|
auto witIdx = line.find("Building Witnesses for block");
|
|
if (witIdx != std::string::npos) {
|
|
foundWitness = true;
|
|
auto compIdx = line.find(" complete", witIdx);
|
|
if (compIdx != std::string::npos) {
|
|
size_t numEnd = compIdx, numStart = numEnd;
|
|
while (numStart > 0 && (std::isdigit((unsigned char)line[numStart - 1]) ||
|
|
line[numStart - 1] == '.')) numStart--;
|
|
if (numStart < numEnd) {
|
|
try { witnessPct = std::stof(line.substr(numStart, numEnd - numStart)) * 100.0f; } catch (...) {}
|
|
}
|
|
}
|
|
auto remIdx = line.find(" remaining", witIdx);
|
|
if (remIdx != std::string::npos) {
|
|
size_t numEnd = remIdx, numStart = numEnd;
|
|
while (numStart > 0 && std::isdigit((unsigned char)line[numStart - 1])) numStart--;
|
|
if (numStart < numEnd) {
|
|
try { witnessRemaining = std::stoi(line.substr(numStart, numEnd - numStart)); } catch (...) {}
|
|
}
|
|
}
|
|
lastStatus = line;
|
|
}
|
|
|
|
// Newer multi-threaded daemon: "Reading blocks for witness rebuild: <done> / <total>".
|
|
// This replaced the per-block "Building Witnesses" line and is an exact done/total
|
|
// count, so capture BOTH (total drives the denominator directly — no peak anchoring).
|
|
auto rdIdx = line.find("Reading blocks for witness rebuild:");
|
|
if (rdIdx != std::string::npos) {
|
|
foundWitness = true;
|
|
auto slash = line.find('/', rdIdx);
|
|
if (slash != std::string::npos) {
|
|
size_t dEnd = slash;
|
|
while (dEnd > rdIdx && !std::isdigit((unsigned char)line[dEnd - 1])) dEnd--;
|
|
size_t dStart = dEnd;
|
|
while (dStart > rdIdx && std::isdigit((unsigned char)line[dStart - 1])) dStart--;
|
|
size_t tStart = slash + 1;
|
|
while (tStart < line.size() && !std::isdigit((unsigned char)line[tStart])) tStart++;
|
|
size_t tEnd = tStart;
|
|
while (tEnd < line.size() && std::isdigit((unsigned char)line[tEnd])) tEnd++;
|
|
if (dStart < dEnd && tStart < tEnd) {
|
|
try {
|
|
long done = std::stol(line.substr(dStart, dEnd - dStart));
|
|
long total = std::stol(line.substr(tStart, tEnd - tStart));
|
|
if (total > 0 && done >= 0 && done <= total) {
|
|
witnessRemaining = static_cast<int>(total - done);
|
|
witnessReadTotal = static_cast<int>(total);
|
|
}
|
|
} catch (...) {}
|
|
}
|
|
}
|
|
lastStatus = line;
|
|
}
|
|
|
|
// New daemon's witness-rebuild completion line:
|
|
// "... rebuilt <n> note witness cache(s) to height <h> in <ms>ms using <t> thread(s)".
|
|
// The parallel rebuild logs no progress, so on completion snap the bar to 100%.
|
|
if (line.find("note witness cache") != std::string::npos &&
|
|
line.find("thread(s)") != std::string::npos) {
|
|
foundWitness = true;
|
|
witnessRebuilt = true;
|
|
lastStatus = line;
|
|
}
|
|
|
|
// Primary signal: "Setting Initial Sapling Witness for tx <hash>, <i> of <N>".
|
|
// <i> is the tx's slot in an unordered map (bounces — useless as progress), so
|
|
// we key on <hash> (one per tx) and count distinct txs against <N>. Extract the
|
|
// hash between "for tx " and the following comma, and N after " of ".
|
|
{
|
|
auto setIdx = line.find("Setting Initial Sapling Witness for tx ");
|
|
if (setIdx != std::string::npos) {
|
|
foundWitness = true;
|
|
size_t hStart = setIdx + std::string("Setting Initial Sapling Witness for tx ").size();
|
|
size_t comma = line.find(',', hStart);
|
|
if (comma != std::string::npos && comma > hStart) {
|
|
witnessTxids.push_back(line.substr(hStart, comma - hStart));
|
|
auto ofIdx = line.find(" of ", comma);
|
|
if (ofIdx != std::string::npos) {
|
|
size_t nStart = ofIdx + 4, nEnd = nStart;
|
|
while (nEnd < line.size() && std::isdigit((unsigned char)line[nEnd])) nEnd++;
|
|
if (nEnd > nStart) {
|
|
try {
|
|
int n = std::stoi(line.substr(nStart, nEnd - nStart));
|
|
if (n > witnessTotalTxs) witnessTotalTxs = n;
|
|
} catch (...) {}
|
|
}
|
|
}
|
|
}
|
|
lastStatus = line;
|
|
}
|
|
}
|
|
}
|
|
|
|
return scan;
|
|
}
|
|
|
|
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
|