Refactor app services and stabilize refresh/UI flows
- Add refresh scheduler and network refresh service boundaries for typed refresh results, ordered RPC collectors, applicators, and price parsing. - Add daemon lifecycle and wallet security workflow helpers while preserving App-owned command RPC, decrypt, cancellation, and UI handoff behavior. - Split balance, console, mining, amount formatting, and async task logic into focused modules with expanded Phase 4 test coverage. - Fix market price loading by triggering price refresh immediately, avoiding queue-pressure drops, tracking loading/error state, and adding translations. - Polish send, explorer, peers, settings, theme/schema, and related tab UI. - Replace checked-in generated language headers with build-generated resources. - Document the cleanup audit, UI static-state guidance, and architecture updates.
This commit is contained in:
976
src/services/network_refresh_service.cpp
Normal file
976
src/services/network_refresh_service.cpp
Normal file
@@ -0,0 +1,976 @@
|
||||
#include "network_refresh_service.h"
|
||||
#include "../util/logger.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T>
|
||||
std::optional<T> readOptional(const json& source, const char* key)
|
||||
{
|
||||
try {
|
||||
if (source.contains(key)) return source[key].get<T>();
|
||||
} catch (...) {}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<double> readBalanceString(const json& source, const char* key)
|
||||
{
|
||||
try {
|
||||
if (source.contains(key)) return std::stod(source[key].get<std::string>());
|
||||
} catch (...) {}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void applyBalancesFromUnspent(std::vector<AddressInfo>& addresses, const json& unspent)
|
||||
{
|
||||
if (!unspent.is_array()) return;
|
||||
|
||||
std::map<std::string, double> balances;
|
||||
for (const auto& output : unspent) {
|
||||
auto address = readOptional<std::string>(output, "address");
|
||||
auto amount = readOptional<double>(output, "amount");
|
||||
if (address && amount) balances[*address] += *amount;
|
||||
}
|
||||
|
||||
for (auto& info : addresses) {
|
||||
auto balance = balances.find(info.address);
|
||||
if (balance != balances.end()) info.balance = balance->second;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NetworkRefreshService::ConnectionInfoResult NetworkRefreshService::parseConnectionInfoResult(const json& info)
|
||||
{
|
||||
ConnectionInfoResult result;
|
||||
if (!info.is_object()) return result;
|
||||
|
||||
result.ok = true;
|
||||
result.daemonVersion = readOptional<int>(info, "version");
|
||||
result.protocolVersion = readOptional<int>(info, "protocolversion");
|
||||
result.p2pPort = readOptional<int>(info, "p2pport");
|
||||
result.longestChain = readOptional<int>(info, "longestchain");
|
||||
result.notarized = readOptional<int>(info, "notarized");
|
||||
result.blocks = readOptional<int>(info, "blocks");
|
||||
return result;
|
||||
}
|
||||
|
||||
NetworkRefreshService::WalletEncryptionResult NetworkRefreshService::parseWalletEncryptionResult(const json& walletInfo)
|
||||
{
|
||||
WalletEncryptionResult result;
|
||||
if (!walletInfo.is_object()) return result;
|
||||
|
||||
result.ok = true;
|
||||
result.encrypted = walletInfo.contains("unlocked_until");
|
||||
if (result.encrypted) {
|
||||
try {
|
||||
result.unlockedUntil = walletInfo["unlocked_until"].get<std::int64_t>();
|
||||
} catch (...) {
|
||||
result.ok = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
NetworkRefreshService::WarmupPollResult NetworkRefreshService::collectWarmupPollResult(RefreshRpcGateway& rpc)
|
||||
{
|
||||
WarmupPollResult result;
|
||||
try {
|
||||
json info = rpc.call("getinfo", json::array());
|
||||
result.ready = true;
|
||||
result.info = parseConnectionInfoResult(info);
|
||||
} catch (const std::exception& e) {
|
||||
result.errorMessage = e.what();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
NetworkRefreshService::ConnectionInitResult NetworkRefreshService::collectConnectionInitResult(RefreshRpcGateway& rpc)
|
||||
{
|
||||
ConnectionInitResult result;
|
||||
|
||||
try {
|
||||
json info = rpc.call("getinfo", json::array());
|
||||
result.info = parseConnectionInfoResult(info);
|
||||
} catch (...) {}
|
||||
|
||||
try {
|
||||
json walletInfo = rpc.call("getwalletinfo", json::array());
|
||||
result.encryption = parseWalletEncryptionResult(walletInfo);
|
||||
} catch (...) {}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefreshResult(
|
||||
const json& totalBalance, bool balanceOk, const json& blockInfo, bool blockOk)
|
||||
{
|
||||
CoreRefreshResult result;
|
||||
result.balanceOk = balanceOk && totalBalance.is_object();
|
||||
if (result.balanceOk) {
|
||||
result.shieldedBalance = readBalanceString(totalBalance, "private");
|
||||
result.transparentBalance = readBalanceString(totalBalance, "transparent");
|
||||
result.totalBalance = readBalanceString(totalBalance, "total");
|
||||
}
|
||||
|
||||
result.blockchainOk = blockOk && blockInfo.is_object();
|
||||
if (result.blockchainOk) {
|
||||
result.blocks = readOptional<int>(blockInfo, "blocks");
|
||||
result.headers = readOptional<int>(blockInfo, "headers");
|
||||
result.verificationProgress = readOptional<double>(blockInfo, "verificationprogress");
|
||||
result.longestChain = readOptional<int>(blockInfo, "longestchain");
|
||||
result.notarized = readOptional<int>(blockInfo, "notarized");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefreshResult(RefreshRpcGateway& rpc)
|
||||
{
|
||||
json totalBalance;
|
||||
json blockInfo;
|
||||
bool balanceOk = false;
|
||||
bool blockOk = false;
|
||||
|
||||
try {
|
||||
totalBalance = rpc.call("z_gettotalbalance", json::array());
|
||||
balanceOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Balance error: %s\n", e.what());
|
||||
}
|
||||
|
||||
try {
|
||||
blockInfo = rpc.call("getblockchaininfo", json::array());
|
||||
blockOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
||||
}
|
||||
|
||||
return parseCoreRefreshResult(totalBalance, balanceOk, blockInfo, blockOk);
|
||||
}
|
||||
|
||||
NetworkRefreshService::MiningRefreshResult NetworkRefreshService::parseMiningRefreshResult(
|
||||
const json& miningInfo, bool miningOk, const json& localHashrate, bool hashrateOk, double daemonMemoryMb)
|
||||
{
|
||||
MiningRefreshResult result;
|
||||
result.daemonMemoryMb = daemonMemoryMb;
|
||||
|
||||
if (hashrateOk) {
|
||||
try {
|
||||
result.localHashrate = localHashrate.get<double>();
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
result.miningOk = miningOk && miningInfo.is_object();
|
||||
if (result.miningOk) {
|
||||
result.generate = readOptional<bool>(miningInfo, "generate");
|
||||
result.genproclimit = readOptional<int>(miningInfo, "genproclimit");
|
||||
result.blocks = readOptional<int>(miningInfo, "blocks");
|
||||
result.difficulty = readOptional<double>(miningInfo, "difficulty");
|
||||
result.networkHashrate = readOptional<double>(miningInfo, "networkhashps");
|
||||
result.chain = readOptional<std::string>(miningInfo, "chain");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
NetworkRefreshService::MiningRefreshResult NetworkRefreshService::collectMiningRefreshResult(
|
||||
RefreshRpcGateway& rpc,
|
||||
double daemonMemoryMb,
|
||||
bool includeSlowRefresh)
|
||||
{
|
||||
json miningInfo;
|
||||
json localHashrate;
|
||||
bool miningOk = false;
|
||||
bool hashrateOk = false;
|
||||
|
||||
try {
|
||||
localHashrate = rpc.call("getlocalsolps", json::array());
|
||||
hashrateOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("getLocalHashrate error: %s\n", e.what());
|
||||
}
|
||||
|
||||
if (includeSlowRefresh) {
|
||||
try {
|
||||
miningInfo = rpc.call("getmininginfo", json::array());
|
||||
miningOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("getMiningInfo error: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
return parseMiningRefreshResult(miningInfo, miningOk, localHashrate, hashrateOk, daemonMemoryMb);
|
||||
}
|
||||
|
||||
NetworkRefreshService::PeerRefreshResult NetworkRefreshService::parsePeerRefreshResult(
|
||||
const json& peers, const json& bannedPeers)
|
||||
{
|
||||
PeerRefreshResult result;
|
||||
|
||||
if (peers.is_array()) {
|
||||
for (const auto& peer : peers) {
|
||||
PeerInfo info;
|
||||
if (auto value = readOptional<int>(peer, "id")) info.id = *value;
|
||||
if (auto value = readOptional<std::string>(peer, "addr")) info.addr = *value;
|
||||
if (auto value = readOptional<std::string>(peer, "subver")) info.subver = *value;
|
||||
if (auto value = readOptional<std::string>(peer, "services")) info.services = *value;
|
||||
if (auto value = readOptional<int>(peer, "version")) info.version = *value;
|
||||
if (auto value = readOptional<std::int64_t>(peer, "conntime")) info.conntime = *value;
|
||||
if (auto value = readOptional<int>(peer, "banscore")) info.banscore = *value;
|
||||
if (auto value = readOptional<double>(peer, "pingtime")) info.pingtime = *value;
|
||||
if (auto value = readOptional<std::int64_t>(peer, "bytessent")) info.bytessent = *value;
|
||||
if (auto value = readOptional<std::int64_t>(peer, "bytesrecv")) info.bytesrecv = *value;
|
||||
if (auto value = readOptional<int>(peer, "startingheight")) info.startingheight = *value;
|
||||
if (auto value = readOptional<int>(peer, "synced_headers")) info.synced_headers = *value;
|
||||
if (auto value = readOptional<int>(peer, "synced_blocks")) info.synced_blocks = *value;
|
||||
if (auto value = readOptional<bool>(peer, "inbound")) info.inbound = *value;
|
||||
if (auto value = readOptional<std::string>(peer, "tls_cipher")) info.tls_cipher = *value;
|
||||
if (auto value = readOptional<bool>(peer, "tls_verified")) info.tls_verified = *value;
|
||||
result.peers.push_back(std::move(info));
|
||||
}
|
||||
}
|
||||
|
||||
if (bannedPeers.is_array()) {
|
||||
for (const auto& ban : bannedPeers) {
|
||||
BannedPeer info;
|
||||
if (auto value = readOptional<std::string>(ban, "address")) info.address = *value;
|
||||
if (auto value = readOptional<std::int64_t>(ban, "banned_until")) info.banned_until = *value;
|
||||
result.bannedPeers.push_back(std::move(info));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
NetworkRefreshService::PeerRefreshResult NetworkRefreshService::collectPeerRefreshResult(RefreshRpcGateway& rpc)
|
||||
{
|
||||
json peers = json::array();
|
||||
json bannedPeers = json::array();
|
||||
bool peersOk = false;
|
||||
bool bannedOk = false;
|
||||
|
||||
try {
|
||||
peers = rpc.call("getpeerinfo", json::array());
|
||||
peersOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("getPeerInfo error: %s\n", e.what());
|
||||
}
|
||||
|
||||
try {
|
||||
bannedPeers = rpc.call("listbanned", json::array());
|
||||
bannedOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("listBanned error: %s\n", e.what());
|
||||
}
|
||||
|
||||
return parsePeerRefreshResult(peersOk ? peers : json::array(),
|
||||
bannedOk ? bannedPeers : json::array());
|
||||
}
|
||||
|
||||
std::optional<NetworkRefreshService::PriceRefreshResult> NetworkRefreshService::parseCoinGeckoPriceResponse(
|
||||
const std::string& response, std::time_t fetchedAt)
|
||||
{
|
||||
try {
|
||||
auto parsed = json::parse(response);
|
||||
if (!parsed.contains("dragonx-2")) return std::nullopt;
|
||||
|
||||
const auto& data = parsed["dragonx-2"];
|
||||
PriceRefreshResult result;
|
||||
result.market.price_usd = data.value("usd", 0.0);
|
||||
result.market.price_btc = data.value("btc", 0.0);
|
||||
result.market.change_24h = data.value("usd_24h_change", 0.0);
|
||||
result.market.volume_24h = data.value("usd_24h_vol", 0.0);
|
||||
result.market.market_cap = data.value("usd_market_cap", 0.0);
|
||||
|
||||
char buf[64];
|
||||
std::tm* tm = std::localtime(&fetchedAt);
|
||||
if (tm && std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tm) > 0) {
|
||||
result.market.last_updated = buf;
|
||||
}
|
||||
return result;
|
||||
} catch (...) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
NetworkRefreshService::PriceHttpResult NetworkRefreshService::parsePriceHttpResponse(
|
||||
const PriceHttpResponse& response,
|
||||
std::time_t fetchedAt)
|
||||
{
|
||||
PriceHttpResult result;
|
||||
|
||||
if (!response.transportOk || response.httpStatus != 200) {
|
||||
const std::string reason = response.transportOk ? "OK" : response.transportError;
|
||||
result.errorMessage = "Price fetch failed: " + reason +
|
||||
" (HTTP " + std::to_string(response.httpStatus) + ")";
|
||||
return result;
|
||||
}
|
||||
|
||||
auto parsed = parseCoinGeckoPriceResponse(response.body, fetchedAt);
|
||||
if (!parsed) {
|
||||
result.errorMessage = "Price fetch returned an unrecognized response";
|
||||
return result;
|
||||
}
|
||||
|
||||
result.price = std::move(*parsed);
|
||||
return result;
|
||||
}
|
||||
|
||||
AddressInfo NetworkRefreshService::buildShieldedAddressInfo(const std::string& address,
|
||||
const json& validation,
|
||||
bool validationSucceeded)
|
||||
{
|
||||
AddressInfo info;
|
||||
info.address = address;
|
||||
info.type = "shielded";
|
||||
|
||||
if (!validationSucceeded) {
|
||||
info.has_spending_key = true;
|
||||
return info;
|
||||
}
|
||||
|
||||
info.has_spending_key = false;
|
||||
if (auto isMine = readOptional<bool>(validation, "ismine")) {
|
||||
info.has_spending_key = *isMine;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
AddressInfo NetworkRefreshService::buildTransparentAddressInfo(const std::string& address)
|
||||
{
|
||||
AddressInfo info;
|
||||
info.address = address;
|
||||
info.type = "transparent";
|
||||
return info;
|
||||
}
|
||||
|
||||
std::vector<AddressInfo> NetworkRefreshService::parseTransparentAddressList(const json& addressList)
|
||||
{
|
||||
std::vector<AddressInfo> addresses;
|
||||
if (!addressList.is_array()) return addresses;
|
||||
|
||||
for (const auto& addressJson : addressList) {
|
||||
try {
|
||||
addresses.push_back(buildTransparentAddressInfo(addressJson.get<std::string>()));
|
||||
} catch (...) {}
|
||||
}
|
||||
return addresses;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyShieldedBalancesFromUnspent(std::vector<AddressInfo>& addresses,
|
||||
const json& unspent)
|
||||
{
|
||||
applyBalancesFromUnspent(addresses, unspent);
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyTransparentBalancesFromUnspent(std::vector<AddressInfo>& addresses,
|
||||
const json& unspent)
|
||||
{
|
||||
applyBalancesFromUnspent(addresses, unspent);
|
||||
}
|
||||
|
||||
NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddressRefreshResult(RefreshRpcGateway& rpc)
|
||||
{
|
||||
AddressRefreshResult result;
|
||||
|
||||
try {
|
||||
json zList = rpc.call("z_listaddresses", json::array());
|
||||
if (zList.is_array()) {
|
||||
for (const auto& addressJson : zList) {
|
||||
std::string address;
|
||||
try {
|
||||
address = addressJson.get<std::string>();
|
||||
} catch (...) {
|
||||
continue;
|
||||
}
|
||||
|
||||
json validationResult;
|
||||
bool validationSucceeded = false;
|
||||
try {
|
||||
validationResult = rpc.call("z_validateaddress", json::array({address}));
|
||||
validationSucceeded = true;
|
||||
} catch (...) {
|
||||
// Older daemons can fail validation for wallet-owned addresses.
|
||||
}
|
||||
result.shieldedAddresses.push_back(
|
||||
buildShieldedAddressInfo(address, validationResult, validationSucceeded));
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("z_listaddresses error: %s\n", e.what());
|
||||
}
|
||||
|
||||
try {
|
||||
json unspent = rpc.call("z_listunspent", json::array());
|
||||
applyShieldedBalancesFromUnspent(result.shieldedAddresses, unspent);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
|
||||
for (auto& info : result.shieldedAddresses) {
|
||||
try {
|
||||
json balance = rpc.call("z_getbalance", json::array({info.address}));
|
||||
if (!balance.is_null()) info.balance = balance.get<double>();
|
||||
} catch (...) {}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
json tList = rpc.call("getaddressesbyaccount", json::array({""}));
|
||||
result.transparentAddresses = parseTransparentAddressList(tList);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("getaddressesbyaccount error: %s\n", e.what());
|
||||
}
|
||||
|
||||
try {
|
||||
json unspent = rpc.call("listunspent", json::array());
|
||||
applyTransparentBalancesFromUnspent(result.transparentAddresses, unspent);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
NetworkRefreshService::TransactionRefreshSnapshot NetworkRefreshService::buildTransactionRefreshSnapshot(
|
||||
const WalletState& state,
|
||||
const TransactionViewCache& viewTxCache,
|
||||
const std::unordered_set<std::string>& sendTxids)
|
||||
{
|
||||
TransactionRefreshSnapshot snapshot;
|
||||
|
||||
for (const auto& address : state.z_addresses) {
|
||||
if (!address.address.empty()) snapshot.shieldedAddresses.push_back(address.address);
|
||||
}
|
||||
|
||||
for (const auto& cachedView : viewTxCache) {
|
||||
snapshot.fullyEnrichedTxids.insert(cachedView.first);
|
||||
}
|
||||
for (const auto& transaction : state.transactions) {
|
||||
if (transaction.confirmations > 6 && transaction.timestamp != 0 && !transaction.txid.empty()) {
|
||||
snapshot.fullyEnrichedTxids.insert(transaction.txid);
|
||||
}
|
||||
}
|
||||
|
||||
snapshot.viewTxCache = viewTxCache;
|
||||
snapshot.sendTxids = sendTxids;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::appendTransparentTransactions(std::vector<TransactionInfo>& transactions,
|
||||
std::set<std::string>& knownTxids,
|
||||
const json& result)
|
||||
{
|
||||
if (!result.is_array()) return;
|
||||
|
||||
for (const auto& transactionJson : result) {
|
||||
TransactionInfo info;
|
||||
if (auto value = readOptional<std::string>(transactionJson, "txid")) info.txid = *value;
|
||||
if (auto value = readOptional<std::string>(transactionJson, "category")) info.type = *value;
|
||||
if (auto value = readOptional<double>(transactionJson, "amount")) info.amount = *value;
|
||||
if (auto value = readOptional<std::int64_t>(transactionJson, "time")) {
|
||||
info.timestamp = *value;
|
||||
} else if (auto received = readOptional<std::int64_t>(transactionJson, "timereceived")) {
|
||||
info.timestamp = *received;
|
||||
}
|
||||
if (auto value = readOptional<int>(transactionJson, "confirmations")) info.confirmations = *value;
|
||||
if (auto value = readOptional<std::string>(transactionJson, "address")) info.address = *value;
|
||||
if (!info.txid.empty()) knownTxids.insert(info.txid);
|
||||
transactions.push_back(std::move(info));
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkRefreshService::appendShieldedReceivedTransactions(std::vector<TransactionInfo>& transactions,
|
||||
std::set<std::string>& knownTxids,
|
||||
const std::string& address,
|
||||
const json& received)
|
||||
{
|
||||
if (received.is_null() || !received.is_array()) return;
|
||||
|
||||
for (const auto& note : received) {
|
||||
auto txid = readOptional<std::string>(note, "txid");
|
||||
if (!txid || txid->empty()) continue;
|
||||
|
||||
auto change = readOptional<bool>(note, "change");
|
||||
if (change && *change) continue;
|
||||
|
||||
bool dominated = false;
|
||||
for (const auto& existing : transactions) {
|
||||
if (existing.txid == *txid && existing.type == "receive") {
|
||||
dominated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dominated) continue;
|
||||
|
||||
TransactionInfo info;
|
||||
info.txid = *txid;
|
||||
info.type = "receive";
|
||||
info.address = address;
|
||||
if (auto value = readOptional<double>(note, "amount")) info.amount = *value;
|
||||
if (auto value = readOptional<int>(note, "confirmations")) info.confirmations = *value;
|
||||
if (auto value = readOptional<std::int64_t>(note, "time")) info.timestamp = *value;
|
||||
if (auto value = readOptional<std::string>(note, "memoStr")) info.memo = *value;
|
||||
knownTxids.insert(*txid);
|
||||
transactions.push_back(std::move(info));
|
||||
}
|
||||
}
|
||||
|
||||
NetworkRefreshService::TransactionViewCacheEntry NetworkRefreshService::parseViewTransactionCacheEntry(
|
||||
const json& viewTransaction)
|
||||
{
|
||||
TransactionViewCacheEntry entry;
|
||||
if (!viewTransaction.is_object()) return entry;
|
||||
|
||||
if (viewTransaction.contains("spends") && viewTransaction["spends"].is_array()) {
|
||||
for (const auto& spend : viewTransaction["spends"]) {
|
||||
if (auto address = readOptional<std::string>(spend, "address")) {
|
||||
entry.from_address = *address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (viewTransaction.contains("outputs") && viewTransaction["outputs"].is_array()) {
|
||||
for (const auto& output : viewTransaction["outputs"]) {
|
||||
bool outgoing = false;
|
||||
if (auto value = readOptional<bool>(output, "outgoing")) outgoing = *value;
|
||||
if (!outgoing) continue;
|
||||
|
||||
TransactionViewCacheEntry::Output out;
|
||||
if (auto value = readOptional<std::string>(output, "address")) out.address = *value;
|
||||
if (auto value = readOptional<double>(output, "value")) out.value = *value;
|
||||
if (auto value = readOptional<std::string>(output, "memoStr")) out.memo = *value;
|
||||
entry.outgoing_outputs.push_back(std::move(out));
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
||||
const std::string& txid,
|
||||
const TransactionViewCacheEntry& entry)
|
||||
{
|
||||
for (const auto& out : entry.outgoing_outputs) {
|
||||
bool alreadyTracked = false;
|
||||
for (const auto& existing : transactions) {
|
||||
if (existing.txid == txid && existing.type == "send" &&
|
||||
std::abs(existing.amount + out.value) < 0.00000001) {
|
||||
alreadyTracked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (alreadyTracked) continue;
|
||||
|
||||
TransactionInfo info;
|
||||
info.txid = txid;
|
||||
info.type = "send";
|
||||
info.address = out.address;
|
||||
info.amount = -out.value;
|
||||
info.memo = out.memo;
|
||||
info.from_address = entry.from_address;
|
||||
for (const auto& existing : transactions) {
|
||||
if (existing.txid == txid) {
|
||||
info.confirmations = existing.confirmations;
|
||||
info.timestamp = existing.timestamp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
transactions.push_back(std::move(info));
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkRefreshService::sortTransactionsNewestFirst(std::vector<TransactionInfo>& transactions)
|
||||
{
|
||||
std::sort(transactions.begin(), transactions.end(),
|
||||
[](const TransactionInfo& left, const TransactionInfo& right) {
|
||||
return left.timestamp > right.timestamp;
|
||||
});
|
||||
}
|
||||
|
||||
NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTransactionRefreshResult(
|
||||
RefreshRpcGateway& rpc,
|
||||
const TransactionRefreshSnapshot& snapshot,
|
||||
int currentBlockHeight,
|
||||
int maxViewTransactionsPerCycle)
|
||||
{
|
||||
TransactionRefreshResult result;
|
||||
result.blockHeight = currentBlockHeight;
|
||||
|
||||
std::set<std::string> knownTxids;
|
||||
|
||||
try {
|
||||
json transactions = rpc.call("listtransactions", json::array({"", 9999}));
|
||||
appendTransparentTransactions(result.transactions, knownTxids, transactions);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("listtransactions error: %s\n", e.what());
|
||||
}
|
||||
|
||||
for (const auto& address : snapshot.shieldedAddresses) {
|
||||
try {
|
||||
json received = rpc.call("z_listreceivedbyaddress", json::array({address, 0}));
|
||||
appendShieldedReceivedTransactions(result.transactions, knownTxids, address, received);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("z_listreceivedbyaddress error for %s: %s\n",
|
||||
address.substr(0, 12).c_str(), e.what());
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& txid : snapshot.sendTxids) {
|
||||
knownTxids.insert(txid);
|
||||
}
|
||||
|
||||
int viewTxCount = 0;
|
||||
for (const auto& txid : knownTxids) {
|
||||
if (snapshot.fullyEnrichedTxids.count(txid)) continue;
|
||||
|
||||
auto cached = snapshot.viewTxCache.find(txid);
|
||||
if (cached != snapshot.viewTxCache.end()) {
|
||||
appendViewTransactionOutputs(result.transactions, txid, cached->second);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (viewTxCount >= maxViewTransactionsPerCycle) break;
|
||||
++viewTxCount;
|
||||
|
||||
try {
|
||||
json viewTransaction = rpc.call("z_viewtransaction", json::array({txid}));
|
||||
if (viewTransaction.is_null() || !viewTransaction.is_object()) continue;
|
||||
|
||||
auto entry = parseViewTransactionCacheEntry(viewTransaction);
|
||||
appendViewTransactionOutputs(result.transactions, txid, entry);
|
||||
|
||||
for (auto& info : result.transactions) {
|
||||
if (info.txid != txid || info.timestamp != 0) continue;
|
||||
|
||||
try {
|
||||
json rawTransaction = rpc.call("gettransaction", json::array({txid}));
|
||||
if (!rawTransaction.is_null()) {
|
||||
if (auto value = readOptional<std::int64_t>(rawTransaction, "time")) info.timestamp = *value;
|
||||
if (auto value = readOptional<int>(rawTransaction, "confirmations")) info.confirmations = *value;
|
||||
}
|
||||
} catch (...) {}
|
||||
break;
|
||||
}
|
||||
|
||||
result.newViewTxEntries[txid] = std::move(entry);
|
||||
} catch (const std::exception& e) {
|
||||
(void)e;
|
||||
}
|
||||
}
|
||||
|
||||
sortTransactionsNewestFirst(result.transactions);
|
||||
return result;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyConnectionInfoResult(WalletState& state, const ConnectionInfoResult& result)
|
||||
{
|
||||
if (!result.ok) return;
|
||||
|
||||
if (result.daemonVersion) state.daemon_version = *result.daemonVersion;
|
||||
if (result.protocolVersion) state.protocol_version = *result.protocolVersion;
|
||||
if (result.p2pPort) state.p2p_port = *result.p2pPort;
|
||||
if (result.longestChain && *result.longestChain > 0) state.longestchain = *result.longestChain;
|
||||
if (result.notarized) state.notarized = *result.notarized;
|
||||
if (result.blocks) state.sync.blocks = *result.blocks;
|
||||
if (state.longestchain > 0 && state.sync.blocks > state.longestchain) state.longestchain = state.sync.blocks;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyWalletEncryptionResult(WalletState& state, const WalletEncryptionResult& result)
|
||||
{
|
||||
if (!result.ok) return;
|
||||
|
||||
if (result.encrypted) {
|
||||
state.encrypted = true;
|
||||
state.unlocked_until = result.unlockedUntil;
|
||||
state.locked = (result.unlockedUntil == 0);
|
||||
} else {
|
||||
state.encrypted = false;
|
||||
state.locked = false;
|
||||
state.unlocked_until = 0;
|
||||
}
|
||||
state.encryption_state_known = true;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result)
|
||||
{
|
||||
applyConnectionInfoResult(state, result.info);
|
||||
applyWalletEncryptionResult(state, result.encryption);
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyCoreRefreshResult(WalletState& state,
|
||||
const CoreRefreshResult& result,
|
||||
std::time_t updatedAt)
|
||||
{
|
||||
if (result.balanceOk) {
|
||||
if (result.shieldedBalance) state.shielded_balance = *result.shieldedBalance;
|
||||
if (result.transparentBalance) state.transparent_balance = *result.transparentBalance;
|
||||
if (result.totalBalance) state.total_balance = *result.totalBalance;
|
||||
state.last_balance_update = updatedAt;
|
||||
}
|
||||
|
||||
if (!result.blockchainOk) return;
|
||||
|
||||
if (result.blocks) state.sync.blocks = *result.blocks;
|
||||
if (result.headers) state.sync.headers = *result.headers;
|
||||
if (result.verificationProgress) state.sync.verification_progress = *result.verificationProgress;
|
||||
if (result.longestChain && *result.longestChain > 0) state.longestchain = *result.longestChain;
|
||||
if (state.longestchain > 0 && state.sync.blocks > state.longestchain) state.longestchain = state.sync.blocks;
|
||||
if (state.longestchain > 0)
|
||||
state.sync.syncing = (state.sync.blocks < state.longestchain - 2);
|
||||
else
|
||||
state.sync.syncing = (state.sync.blocks < state.sync.headers - 2);
|
||||
if (result.notarized) state.notarized = *result.notarized;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyMiningRefreshResult(WalletState& state,
|
||||
const MiningRefreshResult& result,
|
||||
std::time_t updatedAt)
|
||||
{
|
||||
if (result.localHashrate) {
|
||||
state.mining.localHashrate = *result.localHashrate;
|
||||
state.mining.hashrate_history.push_back(state.mining.localHashrate);
|
||||
if (state.mining.hashrate_history.size() > MiningInfo::MAX_HISTORY) {
|
||||
state.mining.hashrate_history.erase(state.mining.hashrate_history.begin());
|
||||
}
|
||||
}
|
||||
|
||||
if (result.miningOk) {
|
||||
if (result.generate) state.mining.generate = *result.generate;
|
||||
if (result.genproclimit) state.mining.genproclimit = *result.genproclimit;
|
||||
if (result.blocks) state.mining.blocks = *result.blocks;
|
||||
if (result.difficulty) state.mining.difficulty = *result.difficulty;
|
||||
if (result.networkHashrate) state.mining.networkHashrate = *result.networkHashrate;
|
||||
if (result.chain) state.mining.chain = *result.chain;
|
||||
state.last_mining_update = updatedAt;
|
||||
}
|
||||
|
||||
state.mining.daemon_memory_mb = result.daemonMemoryMb;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyPeerRefreshResult(WalletState& state,
|
||||
PeerRefreshResult&& result,
|
||||
std::time_t updatedAt)
|
||||
{
|
||||
state.peers = std::move(result.peers);
|
||||
state.bannedPeers = std::move(result.bannedPeers);
|
||||
state.last_peer_update = updatedAt;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::markPriceRefreshStarted(WalletState& state)
|
||||
{
|
||||
state.market.price_loading = true;
|
||||
state.market.price_error.clear();
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyPriceRefreshResult(WalletState& state,
|
||||
const PriceRefreshResult& result,
|
||||
std::chrono::steady_clock::time_point fetchedAt)
|
||||
{
|
||||
state.market.price_loading = false;
|
||||
state.market.price_error.clear();
|
||||
state.market.price_usd = result.market.price_usd;
|
||||
state.market.price_btc = result.market.price_btc;
|
||||
state.market.change_24h = result.market.change_24h;
|
||||
state.market.volume_24h = result.market.volume_24h;
|
||||
state.market.market_cap = result.market.market_cap;
|
||||
state.market.last_updated = result.market.last_updated;
|
||||
state.market.last_fetch_time = fetchedAt;
|
||||
|
||||
state.market.price_history.push_back(result.market.price_usd);
|
||||
if (state.market.price_history.size() > MarketInfo::MAX_HISTORY) {
|
||||
state.market.price_history.erase(state.market.price_history.begin());
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyPriceRefreshFailure(WalletState& state,
|
||||
const std::string& errorMessage)
|
||||
{
|
||||
state.market.price_loading = false;
|
||||
state.market.price_error = errorMessage.empty()
|
||||
? std::string("Price fetch failed")
|
||||
: errorMessage;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyAddressRefreshResult(WalletState& state,
|
||||
AddressRefreshResult&& result)
|
||||
{
|
||||
state.z_addresses = std::move(result.shieldedAddresses);
|
||||
state.t_addresses = std::move(result.transparentAddresses);
|
||||
}
|
||||
|
||||
void NetworkRefreshService::applyTransactionRefreshResult(WalletState& state,
|
||||
TransactionCacheUpdate cacheUpdate,
|
||||
TransactionRefreshResult&& result,
|
||||
std::time_t updatedAt)
|
||||
{
|
||||
state.transactions = std::move(result.transactions);
|
||||
state.last_tx_update = updatedAt;
|
||||
cacheUpdate.lastTxBlockHeight = result.blockHeight;
|
||||
|
||||
for (auto& [txid, entry] : result.newViewTxEntries) {
|
||||
cacheUpdate.viewTxCache[txid] = std::move(entry);
|
||||
cacheUpdate.sendTxids.erase(txid);
|
||||
}
|
||||
|
||||
cacheUpdate.confirmedTxCache.clear();
|
||||
cacheUpdate.confirmedTxIds.clear();
|
||||
for (const auto& tx : state.transactions) {
|
||||
if (tx.confirmations >= 10 && tx.timestamp != 0) {
|
||||
cacheUpdate.confirmedTxIds.insert(tx.txid);
|
||||
cacheUpdate.confirmedTxCache.push_back(tx);
|
||||
}
|
||||
}
|
||||
cacheUpdate.confirmedCacheBlock = result.blockHeight;
|
||||
}
|
||||
|
||||
bool NetworkRefreshService::shouldRefreshTransactions(int lastTxBlockHeight,
|
||||
int currentBlockHeight,
|
||||
bool transactionsEmpty,
|
||||
bool transactionsDirty) const
|
||||
{
|
||||
return scheduler_.shouldRefreshTransactions(lastTxBlockHeight,
|
||||
currentBlockHeight,
|
||||
transactionsEmpty,
|
||||
transactionsDirty);
|
||||
}
|
||||
|
||||
bool NetworkRefreshService::beginJob(Job job)
|
||||
{
|
||||
return beginDispatch(job).accepted;
|
||||
}
|
||||
|
||||
bool NetworkRefreshService::beginJob(Job job, std::size_t queuedWork, std::size_t maxQueuedWork)
|
||||
{
|
||||
return beginDispatch(job, queuedWork, maxQueuedWork).accepted;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::finishJob(Job job)
|
||||
{
|
||||
jobFlag(job).store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool NetworkRefreshService::jobInProgress(Job job) const
|
||||
{
|
||||
return jobFlag(job).load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void NetworkRefreshService::resetJobs()
|
||||
{
|
||||
coreInProgress_.store(false, std::memory_order_release);
|
||||
addressesInProgress_.store(false, std::memory_order_release);
|
||||
transactionsInProgress_.store(false, std::memory_order_release);
|
||||
miningInProgress_.store(false, std::memory_order_release);
|
||||
peersInProgress_.store(false, std::memory_order_release);
|
||||
priceInProgress_.store(false, std::memory_order_release);
|
||||
encryptionInProgress_.store(false, std::memory_order_release);
|
||||
connectionInitInProgress_.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
NetworkRefreshService::DispatchTicket NetworkRefreshService::beginDispatch(
|
||||
Job job, std::size_t queuedWork, std::size_t maxQueuedWork)
|
||||
{
|
||||
std::size_t index = jobIndex(job);
|
||||
lastQueueDepth_[index].store(queuedWork, std::memory_order_release);
|
||||
if (maxQueuedWork > 0 && queuedWork >= maxQueuedWork) {
|
||||
skippedQueuePressure_[index].fetch_add(1, std::memory_order_relaxed);
|
||||
return {job, generations_[index].load(std::memory_order_acquire), false};
|
||||
}
|
||||
|
||||
bool expected = false;
|
||||
if (!jobFlag(job).compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
|
||||
skippedInFlight_[index].fetch_add(1, std::memory_order_relaxed);
|
||||
return {job, generations_[index].load(std::memory_order_acquire), false};
|
||||
}
|
||||
|
||||
std::uint64_t generation = generations_[index].fetch_add(1, std::memory_order_acq_rel) + 1;
|
||||
started_[index].fetch_add(1, std::memory_order_relaxed);
|
||||
return {job, generation, true};
|
||||
}
|
||||
|
||||
bool NetworkRefreshService::completeDispatch(const DispatchTicket& ticket)
|
||||
{
|
||||
if (!ticket.accepted) return false;
|
||||
std::size_t index = jobIndex(ticket.job);
|
||||
if (generations_[index].load(std::memory_order_acquire) != ticket.generation) {
|
||||
staleCallbacks_[index].fetch_add(1, std::memory_order_relaxed);
|
||||
return false;
|
||||
}
|
||||
jobFlag(ticket.job).store(false, std::memory_order_release);
|
||||
finished_[index].fetch_add(1, std::memory_order_relaxed);
|
||||
return true;
|
||||
}
|
||||
|
||||
void NetworkRefreshService::cancelDispatch(const DispatchTicket& ticket)
|
||||
{
|
||||
if (!ticket.accepted) return;
|
||||
std::size_t index = jobIndex(ticket.job);
|
||||
if (generations_[index].load(std::memory_order_acquire) == ticket.generation) {
|
||||
jobFlag(ticket.job).store(false, std::memory_order_release);
|
||||
} else {
|
||||
staleCallbacks_[index].fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
NetworkRefreshService::JobStats NetworkRefreshService::stats(Job job) const
|
||||
{
|
||||
std::size_t index = jobIndex(job);
|
||||
return {
|
||||
started_[index].load(std::memory_order_acquire),
|
||||
finished_[index].load(std::memory_order_acquire),
|
||||
skippedInFlight_[index].load(std::memory_order_acquire),
|
||||
skippedQueuePressure_[index].load(std::memory_order_acquire),
|
||||
staleCallbacks_[index].load(std::memory_order_acquire),
|
||||
lastQueueDepth_[index].load(std::memory_order_acquire)
|
||||
};
|
||||
}
|
||||
|
||||
std::atomic<bool>& NetworkRefreshService::jobFlag(Job job)
|
||||
{
|
||||
switch (job) {
|
||||
case Job::Core: return coreInProgress_;
|
||||
case Job::Addresses: return addressesInProgress_;
|
||||
case Job::Transactions: return transactionsInProgress_;
|
||||
case Job::Mining: return miningInProgress_;
|
||||
case Job::Peers: return peersInProgress_;
|
||||
case Job::Price: return priceInProgress_;
|
||||
case Job::Encryption: return encryptionInProgress_;
|
||||
case Job::ConnectionInit: return connectionInitInProgress_;
|
||||
case Job::Count: return coreInProgress_;
|
||||
}
|
||||
return coreInProgress_;
|
||||
}
|
||||
|
||||
const std::atomic<bool>& NetworkRefreshService::jobFlag(Job job) const
|
||||
{
|
||||
switch (job) {
|
||||
case Job::Core: return coreInProgress_;
|
||||
case Job::Addresses: return addressesInProgress_;
|
||||
case Job::Transactions: return transactionsInProgress_;
|
||||
case Job::Mining: return miningInProgress_;
|
||||
case Job::Peers: return peersInProgress_;
|
||||
case Job::Price: return priceInProgress_;
|
||||
case Job::Encryption: return encryptionInProgress_;
|
||||
case Job::ConnectionInit: return connectionInitInProgress_;
|
||||
case Job::Count: return coreInProgress_;
|
||||
}
|
||||
return coreInProgress_;
|
||||
}
|
||||
|
||||
std::size_t NetworkRefreshService::jobIndex(Job job)
|
||||
{
|
||||
std::size_t index = static_cast<std::size_t>(job);
|
||||
constexpr std::size_t count = static_cast<std::size_t>(Job::Count);
|
||||
return index < count ? index : 0;
|
||||
}
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
336
src/services/network_refresh_service.h
Normal file
336
src/services/network_refresh_service.h
Normal file
@@ -0,0 +1,336 @@
|
||||
#pragma once
|
||||
|
||||
#include "data/wallet_state.h"
|
||||
#include "refresh_scheduler.h"
|
||||
#include "rpc/rpc_worker.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
class NetworkRefreshService {
|
||||
public:
|
||||
using Timer = RefreshScheduler::Timer;
|
||||
using Intervals = RefreshScheduler::Intervals;
|
||||
|
||||
class RefreshRpcGateway {
|
||||
public:
|
||||
virtual ~RefreshRpcGateway() = default;
|
||||
virtual nlohmann::json call(const std::string& method,
|
||||
const nlohmann::json& params) = 0;
|
||||
};
|
||||
|
||||
enum class Job {
|
||||
Core,
|
||||
Addresses,
|
||||
Transactions,
|
||||
Mining,
|
||||
Peers,
|
||||
Price,
|
||||
Encryption,
|
||||
ConnectionInit,
|
||||
Count
|
||||
};
|
||||
|
||||
struct DispatchTicket {
|
||||
Job job = Job::Core;
|
||||
std::uint64_t generation = 0;
|
||||
bool accepted = false;
|
||||
};
|
||||
|
||||
struct JobStats {
|
||||
std::uint64_t started = 0;
|
||||
std::uint64_t finished = 0;
|
||||
std::uint64_t skippedInFlight = 0;
|
||||
std::uint64_t skippedQueuePressure = 0;
|
||||
std::uint64_t staleCallbacks = 0;
|
||||
std::size_t lastQueueDepth = 0;
|
||||
};
|
||||
|
||||
struct EnqueueResult {
|
||||
DispatchTicket ticket;
|
||||
bool enqueued = false;
|
||||
std::size_t queueDepth = 0;
|
||||
};
|
||||
|
||||
struct ConnectionInfoResult {
|
||||
bool ok = false;
|
||||
std::optional<int> daemonVersion;
|
||||
std::optional<int> protocolVersion;
|
||||
std::optional<int> p2pPort;
|
||||
std::optional<int> longestChain;
|
||||
std::optional<int> notarized;
|
||||
std::optional<int> blocks;
|
||||
};
|
||||
|
||||
struct WalletEncryptionResult {
|
||||
bool ok = false;
|
||||
bool encrypted = false;
|
||||
std::int64_t unlockedUntil = 0;
|
||||
};
|
||||
|
||||
struct WarmupPollResult {
|
||||
bool ready = false;
|
||||
ConnectionInfoResult info;
|
||||
std::string errorMessage;
|
||||
};
|
||||
|
||||
struct ConnectionInitResult {
|
||||
ConnectionInfoResult info;
|
||||
WalletEncryptionResult encryption;
|
||||
};
|
||||
|
||||
struct CoreRefreshResult {
|
||||
bool balanceOk = false;
|
||||
std::optional<double> shieldedBalance;
|
||||
std::optional<double> transparentBalance;
|
||||
std::optional<double> totalBalance;
|
||||
bool blockchainOk = false;
|
||||
std::optional<int> blocks;
|
||||
std::optional<int> headers;
|
||||
std::optional<double> verificationProgress;
|
||||
std::optional<int> longestChain;
|
||||
std::optional<int> notarized;
|
||||
};
|
||||
|
||||
struct MiningRefreshResult {
|
||||
std::optional<double> localHashrate;
|
||||
bool miningOk = false;
|
||||
std::optional<bool> generate;
|
||||
std::optional<int> genproclimit;
|
||||
std::optional<int> blocks;
|
||||
std::optional<double> difficulty;
|
||||
std::optional<double> networkHashrate;
|
||||
std::optional<std::string> chain;
|
||||
double daemonMemoryMb = 0.0;
|
||||
};
|
||||
|
||||
struct PeerRefreshResult {
|
||||
std::vector<PeerInfo> peers;
|
||||
std::vector<BannedPeer> bannedPeers;
|
||||
};
|
||||
|
||||
struct PriceRefreshResult {
|
||||
MarketInfo market;
|
||||
};
|
||||
|
||||
struct PriceHttpResponse {
|
||||
bool transportOk = false;
|
||||
long httpStatus = 0;
|
||||
std::string body;
|
||||
std::string transportError;
|
||||
};
|
||||
|
||||
struct PriceHttpResult {
|
||||
std::optional<PriceRefreshResult> price;
|
||||
std::string errorMessage;
|
||||
};
|
||||
|
||||
struct AddressRefreshResult {
|
||||
std::vector<AddressInfo> shieldedAddresses;
|
||||
std::vector<AddressInfo> transparentAddresses;
|
||||
};
|
||||
|
||||
struct TransactionViewCacheEntry {
|
||||
std::string from_address;
|
||||
struct Output {
|
||||
std::string address;
|
||||
double value = 0.0;
|
||||
std::string memo;
|
||||
};
|
||||
std::vector<Output> outgoing_outputs;
|
||||
};
|
||||
|
||||
using TransactionViewCache = std::unordered_map<std::string, TransactionViewCacheEntry>;
|
||||
|
||||
struct TransactionRefreshSnapshot {
|
||||
std::vector<std::string> shieldedAddresses;
|
||||
std::unordered_set<std::string> fullyEnrichedTxids;
|
||||
TransactionViewCache viewTxCache;
|
||||
std::unordered_set<std::string> sendTxids;
|
||||
};
|
||||
|
||||
struct TransactionRefreshResult {
|
||||
std::vector<TransactionInfo> transactions;
|
||||
int blockHeight = -1;
|
||||
TransactionViewCache newViewTxEntries;
|
||||
};
|
||||
|
||||
struct TransactionCacheUpdate {
|
||||
TransactionViewCache& viewTxCache;
|
||||
std::unordered_set<std::string>& sendTxids;
|
||||
std::vector<TransactionInfo>& confirmedTxCache;
|
||||
std::unordered_set<std::string>& confirmedTxIds;
|
||||
int& confirmedCacheBlock;
|
||||
int& lastTxBlockHeight;
|
||||
};
|
||||
|
||||
static Intervals intervalsForPage(ui::NavPage page) { return RefreshScheduler::intervalsForPage(page); }
|
||||
|
||||
static ConnectionInfoResult parseConnectionInfoResult(const nlohmann::json& info);
|
||||
static WalletEncryptionResult parseWalletEncryptionResult(const nlohmann::json& walletInfo);
|
||||
static WarmupPollResult collectWarmupPollResult(RefreshRpcGateway& rpc);
|
||||
static ConnectionInitResult collectConnectionInitResult(RefreshRpcGateway& rpc);
|
||||
static CoreRefreshResult parseCoreRefreshResult(const nlohmann::json& totalBalance,
|
||||
bool balanceOk,
|
||||
const nlohmann::json& blockInfo,
|
||||
bool blockOk);
|
||||
static CoreRefreshResult collectCoreRefreshResult(RefreshRpcGateway& rpc);
|
||||
static MiningRefreshResult parseMiningRefreshResult(const nlohmann::json& miningInfo,
|
||||
bool miningOk,
|
||||
const nlohmann::json& localHashrate,
|
||||
bool hashrateOk,
|
||||
double daemonMemoryMb);
|
||||
static MiningRefreshResult collectMiningRefreshResult(RefreshRpcGateway& rpc,
|
||||
double daemonMemoryMb,
|
||||
bool includeSlowRefresh);
|
||||
static PeerRefreshResult parsePeerRefreshResult(const nlohmann::json& peers,
|
||||
const nlohmann::json& bannedPeers);
|
||||
static PeerRefreshResult collectPeerRefreshResult(RefreshRpcGateway& rpc);
|
||||
static std::optional<PriceRefreshResult> parseCoinGeckoPriceResponse(const std::string& response,
|
||||
std::time_t fetchedAt);
|
||||
static PriceHttpResult parsePriceHttpResponse(const PriceHttpResponse& response,
|
||||
std::time_t fetchedAt);
|
||||
static AddressInfo buildShieldedAddressInfo(const std::string& address,
|
||||
const nlohmann::json& validation,
|
||||
bool validationSucceeded);
|
||||
static AddressInfo buildTransparentAddressInfo(const std::string& address);
|
||||
static std::vector<AddressInfo> parseTransparentAddressList(const nlohmann::json& addressList);
|
||||
static void applyShieldedBalancesFromUnspent(std::vector<AddressInfo>& addresses,
|
||||
const nlohmann::json& unspent);
|
||||
static void applyTransparentBalancesFromUnspent(std::vector<AddressInfo>& addresses,
|
||||
const nlohmann::json& unspent);
|
||||
static AddressRefreshResult collectAddressRefreshResult(RefreshRpcGateway& rpc);
|
||||
static TransactionRefreshSnapshot buildTransactionRefreshSnapshot(const WalletState& state,
|
||||
const TransactionViewCache& viewTxCache,
|
||||
const std::unordered_set<std::string>& sendTxids);
|
||||
static void appendTransparentTransactions(std::vector<TransactionInfo>& transactions,
|
||||
std::set<std::string>& knownTxids,
|
||||
const nlohmann::json& result);
|
||||
static void appendShieldedReceivedTransactions(std::vector<TransactionInfo>& transactions,
|
||||
std::set<std::string>& knownTxids,
|
||||
const std::string& address,
|
||||
const nlohmann::json& received);
|
||||
static TransactionViewCacheEntry parseViewTransactionCacheEntry(const nlohmann::json& viewTransaction);
|
||||
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
||||
const std::string& txid,
|
||||
const TransactionViewCacheEntry& entry);
|
||||
static void sortTransactionsNewestFirst(std::vector<TransactionInfo>& transactions);
|
||||
static TransactionRefreshResult collectTransactionRefreshResult(RefreshRpcGateway& rpc,
|
||||
const TransactionRefreshSnapshot& snapshot,
|
||||
int currentBlockHeight,
|
||||
int maxViewTransactionsPerCycle);
|
||||
|
||||
static void applyConnectionInfoResult(WalletState& state, const ConnectionInfoResult& result);
|
||||
static void applyWalletEncryptionResult(WalletState& state, const WalletEncryptionResult& result);
|
||||
static void applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result);
|
||||
static void applyCoreRefreshResult(WalletState& state,
|
||||
const CoreRefreshResult& result,
|
||||
std::time_t updatedAt);
|
||||
static void applyMiningRefreshResult(WalletState& state,
|
||||
const MiningRefreshResult& result,
|
||||
std::time_t updatedAt);
|
||||
static void applyPeerRefreshResult(WalletState& state,
|
||||
PeerRefreshResult&& result,
|
||||
std::time_t updatedAt);
|
||||
static void markPriceRefreshStarted(WalletState& state);
|
||||
static void applyPriceRefreshResult(WalletState& state,
|
||||
const PriceRefreshResult& result,
|
||||
std::chrono::steady_clock::time_point fetchedAt);
|
||||
static void applyPriceRefreshFailure(WalletState& state,
|
||||
const std::string& errorMessage);
|
||||
static void applyAddressRefreshResult(WalletState& state,
|
||||
AddressRefreshResult&& result);
|
||||
static void applyTransactionRefreshResult(WalletState& state,
|
||||
TransactionCacheUpdate cacheUpdate,
|
||||
TransactionRefreshResult&& result,
|
||||
std::time_t updatedAt);
|
||||
|
||||
void applyPage(ui::NavPage page) { scheduler_.applyPage(page); }
|
||||
void setIntervals(Intervals intervals) { scheduler_.setIntervals(intervals); }
|
||||
const Intervals& intervals() const { return scheduler_.intervals(); }
|
||||
|
||||
void tick(float deltaSeconds) { scheduler_.tick(deltaSeconds); }
|
||||
bool isDue(Timer timer) const { return scheduler_.isDue(timer); }
|
||||
bool consumeDue(Timer timer) { return scheduler_.consumeDue(timer); }
|
||||
void reset(Timer timer) { scheduler_.reset(timer); }
|
||||
void markDue(Timer timer) { scheduler_.markDue(timer); }
|
||||
void setTimer(Timer timer, float seconds) { scheduler_.setTimer(timer, seconds); }
|
||||
float timer(Timer timer) const { return scheduler_.timer(timer); }
|
||||
float interval(Timer timer) const { return scheduler_.interval(timer); }
|
||||
void markImmediateRefresh() { scheduler_.markImmediateRefresh(); }
|
||||
void markWalletMutationRefresh() { scheduler_.markWalletMutationRefresh(); }
|
||||
void resetTxAge() { scheduler_.resetTxAge(); }
|
||||
bool shouldRefreshTransactions(int lastTxBlockHeight,
|
||||
int currentBlockHeight,
|
||||
bool transactionsEmpty,
|
||||
bool transactionsDirty) const;
|
||||
|
||||
bool beginJob(Job job);
|
||||
bool beginJob(Job job, std::size_t queuedWork, std::size_t maxQueuedWork);
|
||||
void finishJob(Job job);
|
||||
bool jobInProgress(Job job) const;
|
||||
void resetJobs();
|
||||
DispatchTicket beginDispatch(Job job, std::size_t queuedWork = 0, std::size_t maxQueuedWork = 0);
|
||||
bool completeDispatch(const DispatchTicket& ticket);
|
||||
void cancelDispatch(const DispatchTicket& ticket);
|
||||
JobStats stats(Job job) const;
|
||||
|
||||
template <typename Worker, typename WorkFn>
|
||||
EnqueueResult enqueue(Job job, Worker& worker, WorkFn&& work, std::size_t maxQueuedWork = 0)
|
||||
{
|
||||
std::size_t queueDepth = worker.pendingTaskCount();
|
||||
auto ticket = beginDispatch(job, queueDepth, maxQueuedWork);
|
||||
if (!ticket.accepted) return {ticket, false, queueDepth};
|
||||
|
||||
worker.post([this, ticket, work = std::forward<WorkFn>(work)]() mutable -> rpc::RPCWorker::MainCb {
|
||||
auto mainCallback = work();
|
||||
return [this, ticket, mainCallback = std::move(mainCallback)]() mutable {
|
||||
if (!completeDispatch(ticket)) return;
|
||||
if (mainCallback) mainCallback();
|
||||
};
|
||||
});
|
||||
|
||||
return {ticket, true, queueDepth};
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<bool>& jobFlag(Job job);
|
||||
const std::atomic<bool>& jobFlag(Job job) const;
|
||||
static std::size_t jobIndex(Job job);
|
||||
|
||||
RefreshScheduler scheduler_;
|
||||
std::atomic<bool> coreInProgress_{false};
|
||||
std::atomic<bool> addressesInProgress_{false};
|
||||
std::atomic<bool> transactionsInProgress_{false};
|
||||
std::atomic<bool> miningInProgress_{false};
|
||||
std::atomic<bool> peersInProgress_{false};
|
||||
std::atomic<bool> priceInProgress_{false};
|
||||
std::atomic<bool> encryptionInProgress_{false};
|
||||
std::atomic<bool> connectionInitInProgress_{false};
|
||||
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> generations_{};
|
||||
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> started_{};
|
||||
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> finished_{};
|
||||
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> skippedInFlight_{};
|
||||
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> skippedQueuePressure_{};
|
||||
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> staleCallbacks_{};
|
||||
std::array<std::atomic<std::size_t>, static_cast<std::size_t>(Job::Count)> lastQueueDepth_{};
|
||||
};
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
158
src/services/refresh_scheduler.cpp
Normal file
158
src/services/refresh_scheduler.cpp
Normal file
@@ -0,0 +1,158 @@
|
||||
#include "refresh_scheduler.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
RefreshScheduler::Intervals RefreshScheduler::intervalsForPage(ui::NavPage page)
|
||||
{
|
||||
using NP = ui::NavPage;
|
||||
switch (page) {
|
||||
case NP::Overview: return {2.0f, 10.0f, 15.0f, 0.0f};
|
||||
case NP::Send: return {3.0f, 10.0f, 5.0f, 0.0f};
|
||||
case NP::Receive: return {5.0f, 15.0f, 5.0f, 0.0f};
|
||||
case NP::History: return {5.0f, 3.0f, 15.0f, 0.0f};
|
||||
case NP::Mining: return {5.0f, 15.0f, 15.0f, 0.0f};
|
||||
case NP::Peers: return {5.0f, 15.0f, 15.0f, 5.0f};
|
||||
case NP::Market: return {5.0f, 15.0f, 15.0f, 0.0f};
|
||||
default: return {5.0f, 15.0f, 15.0f, 0.0f};
|
||||
}
|
||||
}
|
||||
|
||||
void RefreshScheduler::applyPage(ui::NavPage page)
|
||||
{
|
||||
setIntervals(intervalsForPage(page));
|
||||
}
|
||||
|
||||
void RefreshScheduler::setIntervals(Intervals intervals)
|
||||
{
|
||||
intervals_ = intervals;
|
||||
}
|
||||
|
||||
void RefreshScheduler::tick(float deltaSeconds)
|
||||
{
|
||||
float delta = std::max(0.0f, deltaSeconds);
|
||||
timers_.core += delta;
|
||||
timers_.transactions += delta;
|
||||
timers_.addresses += delta;
|
||||
timers_.peers += delta;
|
||||
timers_.price += delta;
|
||||
timers_.fast += delta;
|
||||
timers_.txAge += delta;
|
||||
timers_.opid += delta;
|
||||
}
|
||||
|
||||
bool RefreshScheduler::isDue(Timer timer) const
|
||||
{
|
||||
float timerInterval = interval(timer);
|
||||
return timerInterval > 0.0f && timerRef(timer) >= timerInterval;
|
||||
}
|
||||
|
||||
bool RefreshScheduler::consumeDue(Timer timer)
|
||||
{
|
||||
if (!isDue(timer)) return false;
|
||||
reset(timer);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RefreshScheduler::reset(Timer timer)
|
||||
{
|
||||
timerRef(timer) = 0.0f;
|
||||
}
|
||||
|
||||
void RefreshScheduler::markDue(Timer timer)
|
||||
{
|
||||
float timerInterval = interval(timer);
|
||||
timerRef(timer) = timerInterval > 0.0f ? timerInterval : 0.0f;
|
||||
}
|
||||
|
||||
void RefreshScheduler::setTimer(Timer timer, float seconds)
|
||||
{
|
||||
timerRef(timer) = std::max(0.0f, seconds);
|
||||
}
|
||||
|
||||
float RefreshScheduler::timer(Timer timer) const
|
||||
{
|
||||
return timerRef(timer);
|
||||
}
|
||||
|
||||
float RefreshScheduler::interval(Timer timer) const
|
||||
{
|
||||
switch (timer) {
|
||||
case Timer::Core: return intervals_.core;
|
||||
case Timer::Transactions: return intervals_.transactions;
|
||||
case Timer::Addresses: return intervals_.addresses;
|
||||
case Timer::Peers: return intervals_.peers;
|
||||
case Timer::Price: return kPrice;
|
||||
case Timer::Fast: return kFast;
|
||||
case Timer::TxAge: return kTxMaxAge;
|
||||
case Timer::Opid: return kOpidPoll;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
void RefreshScheduler::markImmediateRefresh()
|
||||
{
|
||||
markDue(Timer::Core);
|
||||
markDue(Timer::Transactions);
|
||||
markDue(Timer::Addresses);
|
||||
markDue(Timer::Peers);
|
||||
}
|
||||
|
||||
void RefreshScheduler::markWalletMutationRefresh()
|
||||
{
|
||||
markDue(Timer::Core);
|
||||
markDue(Timer::Transactions);
|
||||
markDue(Timer::Addresses);
|
||||
}
|
||||
|
||||
void RefreshScheduler::resetTxAge()
|
||||
{
|
||||
reset(Timer::TxAge);
|
||||
}
|
||||
|
||||
bool RefreshScheduler::shouldRefreshTransactions(int lastTxBlockHeight,
|
||||
int currentBlockHeight,
|
||||
bool transactionsEmpty,
|
||||
bool transactionsDirty) const
|
||||
{
|
||||
return lastTxBlockHeight < 0
|
||||
|| currentBlockHeight != lastTxBlockHeight
|
||||
|| transactionsEmpty
|
||||
|| transactionsDirty
|
||||
|| isDue(Timer::TxAge);
|
||||
}
|
||||
|
||||
float& RefreshScheduler::timerRef(Timer timer)
|
||||
{
|
||||
switch (timer) {
|
||||
case Timer::Core: return timers_.core;
|
||||
case Timer::Transactions: return timers_.transactions;
|
||||
case Timer::Addresses: return timers_.addresses;
|
||||
case Timer::Peers: return timers_.peers;
|
||||
case Timer::Price: return timers_.price;
|
||||
case Timer::Fast: return timers_.fast;
|
||||
case Timer::TxAge: return timers_.txAge;
|
||||
case Timer::Opid: return timers_.opid;
|
||||
}
|
||||
return timers_.core;
|
||||
}
|
||||
|
||||
const float& RefreshScheduler::timerRef(Timer timer) const
|
||||
{
|
||||
switch (timer) {
|
||||
case Timer::Core: return timers_.core;
|
||||
case Timer::Transactions: return timers_.transactions;
|
||||
case Timer::Addresses: return timers_.addresses;
|
||||
case Timer::Peers: return timers_.peers;
|
||||
case Timer::Price: return timers_.price;
|
||||
case Timer::Fast: return timers_.fast;
|
||||
case Timer::TxAge: return timers_.txAge;
|
||||
case Timer::Opid: return timers_.opid;
|
||||
}
|
||||
return timers_.core;
|
||||
}
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
81
src/services/refresh_scheduler.h
Normal file
81
src/services/refresh_scheduler.h
Normal file
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/sidebar.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
class RefreshScheduler {
|
||||
public:
|
||||
enum class Timer {
|
||||
Core,
|
||||
Transactions,
|
||||
Addresses,
|
||||
Peers,
|
||||
Price,
|
||||
Fast,
|
||||
TxAge,
|
||||
Opid
|
||||
};
|
||||
|
||||
struct Intervals {
|
||||
float core;
|
||||
float transactions;
|
||||
float addresses;
|
||||
float peers;
|
||||
};
|
||||
|
||||
static constexpr float kCoreDefault = 5.0f;
|
||||
static constexpr float kAddressDefault = 15.0f;
|
||||
static constexpr float kTransactionDefault = 10.0f;
|
||||
static constexpr float kPeerDefault = 10.0f;
|
||||
static constexpr float kPrice = 60.0f;
|
||||
static constexpr float kFast = 1.0f;
|
||||
static constexpr float kTxMaxAge = 15.0f;
|
||||
static constexpr float kOpidPoll = 2.0f;
|
||||
|
||||
static Intervals intervalsForPage(ui::NavPage page);
|
||||
|
||||
void applyPage(ui::NavPage page);
|
||||
void setIntervals(Intervals intervals);
|
||||
const Intervals& intervals() const { return intervals_; }
|
||||
|
||||
void tick(float deltaSeconds);
|
||||
|
||||
bool isDue(Timer timer) const;
|
||||
bool consumeDue(Timer timer);
|
||||
void reset(Timer timer);
|
||||
void markDue(Timer timer);
|
||||
void setTimer(Timer timer, float seconds);
|
||||
float timer(Timer timer) const;
|
||||
float interval(Timer timer) const;
|
||||
|
||||
void markImmediateRefresh();
|
||||
void markWalletMutationRefresh();
|
||||
void resetTxAge();
|
||||
bool shouldRefreshTransactions(int lastTxBlockHeight,
|
||||
int currentBlockHeight,
|
||||
bool transactionsEmpty,
|
||||
bool transactionsDirty) const;
|
||||
|
||||
private:
|
||||
struct Timers {
|
||||
float core = 0.0f;
|
||||
float transactions = 0.0f;
|
||||
float addresses = 0.0f;
|
||||
float peers = 0.0f;
|
||||
float price = 0.0f;
|
||||
float fast = 0.0f;
|
||||
float txAge = 0.0f;
|
||||
float opid = 0.0f;
|
||||
};
|
||||
|
||||
float& timerRef(Timer timer);
|
||||
const float& timerRef(Timer timer) const;
|
||||
|
||||
Intervals intervals_{kCoreDefault, kTransactionDefault, kAddressDefault, kPeerDefault};
|
||||
Timers timers_;
|
||||
};
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
127
src/services/wallet_security_controller.cpp
Normal file
127
src/services/wallet_security_controller.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
#include "wallet_security_controller.h"
|
||||
#include "../util/secure_vault.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
WalletSecurityController::~WalletSecurityController()
|
||||
{
|
||||
clearDeferredEncryption();
|
||||
}
|
||||
|
||||
void WalletSecurityController::beginDeferredEncryption(std::string passphrase, std::string pin)
|
||||
{
|
||||
clearDeferredEncryption();
|
||||
deferred_.passphrase = std::move(passphrase);
|
||||
deferred_.pin = std::move(pin);
|
||||
deferred_.pending = true;
|
||||
deferred_.lastConnectAttempt = -10.0;
|
||||
}
|
||||
|
||||
WalletSecurityController::DeferredEncryptionSnapshot WalletSecurityController::deferredEncryption() const
|
||||
{
|
||||
return {deferred_.passphrase, deferred_.pin};
|
||||
}
|
||||
|
||||
bool WalletSecurityController::shouldAttemptDeferredConnect(double nowSeconds, double minIntervalSeconds)
|
||||
{
|
||||
if (!deferred_.pending) return false;
|
||||
if (nowSeconds - deferred_.lastConnectAttempt < minIntervalSeconds) return false;
|
||||
deferred_.lastConnectAttempt = nowSeconds;
|
||||
return true;
|
||||
}
|
||||
|
||||
void WalletSecurityController::clearDeferredEncryption()
|
||||
{
|
||||
secureClear(deferred_.passphrase);
|
||||
secureClear(deferred_.pin);
|
||||
deferred_.pending = false;
|
||||
deferred_.lastConnectAttempt = -10.0;
|
||||
}
|
||||
|
||||
WalletSecurityController::DeferredEncryptionResult WalletSecurityController::runDeferredEncryption(
|
||||
DeferredEncryptionSnapshot request, RpcGateway& rpc, VaultGateway* vault)
|
||||
{
|
||||
DeferredEncryptionResult result;
|
||||
result.pinProvided = !request.pin.empty();
|
||||
|
||||
std::string error;
|
||||
if (!rpc.encryptWallet(request.passphrase, error)) {
|
||||
result.error = error.empty() ? "encryptwallet failed" : error;
|
||||
secureClear(request.passphrase);
|
||||
secureClear(request.pin);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.encrypted = true;
|
||||
result.restartRequired = true;
|
||||
if (result.pinProvided && vault) {
|
||||
result.pinStored = vault->storePin(request.pin, request.passphrase);
|
||||
}
|
||||
|
||||
secureClear(request.passphrase);
|
||||
secureClear(request.pin);
|
||||
return result;
|
||||
}
|
||||
|
||||
WalletSecurityController::PinValidationResult WalletSecurityController::validatePinSetup(
|
||||
const std::string& pin, const std::string& confirmation, bool allowEmpty, std::size_t minLength)
|
||||
{
|
||||
if (pin.empty() && confirmation.empty()) {
|
||||
return allowEmpty
|
||||
? PinValidationResult{true, PinValidationError::None, ""}
|
||||
: PinValidationResult{false, PinValidationError::Empty, "PIN is required"};
|
||||
}
|
||||
if (pin != confirmation) {
|
||||
return {false, PinValidationError::Mismatch, "PINs do not match"};
|
||||
}
|
||||
if (pin.size() < minLength) {
|
||||
return {false, PinValidationError::TooShort, "PIN is too short"};
|
||||
}
|
||||
for (unsigned char c : pin) {
|
||||
if (!std::isdigit(c)) {
|
||||
return {false, PinValidationError::NonDigit, "PIN must contain only digits"};
|
||||
}
|
||||
}
|
||||
return {true, PinValidationError::None, ""};
|
||||
}
|
||||
|
||||
WalletSecurityController::KeyKind WalletSecurityController::classifyAddress(const std::string& address)
|
||||
{
|
||||
return !address.empty() && address[0] == 'z' ? KeyKind::Shielded : KeyKind::Transparent;
|
||||
}
|
||||
|
||||
WalletSecurityController::KeyKind WalletSecurityController::classifyPrivateKey(const std::string& key)
|
||||
{
|
||||
return !key.empty() && key[0] == 's' ? KeyKind::Shielded : KeyKind::Transparent;
|
||||
}
|
||||
|
||||
const char* WalletSecurityController::importSuccessMessage(KeyKind kind)
|
||||
{
|
||||
return kind == KeyKind::Shielded
|
||||
? "Z-address key imported successfully. Wallet is rescanning."
|
||||
: "T-address key imported successfully. Wallet is rescanning.";
|
||||
}
|
||||
|
||||
std::string WalletSecurityController::decryptExportFileName(std::uint64_t timestampSeconds)
|
||||
{
|
||||
char buffer[64];
|
||||
snprintf(buffer, sizeof(buffer), "obsidiandecryptexport%llu",
|
||||
static_cast<unsigned long long>(timestampSeconds));
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
void WalletSecurityController::secureClear(std::string& value)
|
||||
{
|
||||
if (!value.empty()) {
|
||||
util::SecureVault::secureZero(&value[0], value.size());
|
||||
value.clear();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
93
src/services/wallet_security_controller.h
Normal file
93
src/services/wallet_security_controller.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
class WalletSecurityController {
|
||||
public:
|
||||
enum class PinValidationError {
|
||||
None,
|
||||
Empty,
|
||||
Mismatch,
|
||||
TooShort,
|
||||
NonDigit
|
||||
};
|
||||
|
||||
struct PinValidationResult {
|
||||
bool ok = false;
|
||||
PinValidationError error = PinValidationError::None;
|
||||
const char* message = "";
|
||||
};
|
||||
|
||||
struct DeferredEncryptionSnapshot {
|
||||
std::string passphrase;
|
||||
std::string pin;
|
||||
};
|
||||
|
||||
class RpcGateway {
|
||||
public:
|
||||
virtual ~RpcGateway() = default;
|
||||
virtual bool encryptWallet(const std::string& passphrase, std::string& error) = 0;
|
||||
virtual bool unlockWallet(const std::string& passphrase, int timeoutSeconds, std::string& error) = 0;
|
||||
virtual bool exportWallet(const std::string& fileName, long timeoutSeconds, std::string& error) = 0;
|
||||
virtual bool importWallet(const std::string& filePath, long timeoutSeconds, std::string& error) = 0;
|
||||
};
|
||||
|
||||
class VaultGateway {
|
||||
public:
|
||||
virtual ~VaultGateway() = default;
|
||||
virtual bool storePin(const std::string& pin, const std::string& passphrase) = 0;
|
||||
};
|
||||
|
||||
enum class KeyKind {
|
||||
Transparent,
|
||||
Shielded
|
||||
};
|
||||
|
||||
struct DeferredEncryptionResult {
|
||||
bool encrypted = false;
|
||||
bool pinProvided = false;
|
||||
bool pinStored = false;
|
||||
bool restartRequired = false;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
~WalletSecurityController();
|
||||
|
||||
void beginDeferredEncryption(std::string passphrase, std::string pin = {});
|
||||
bool hasDeferredEncryption() const { return deferred_.pending; }
|
||||
DeferredEncryptionSnapshot deferredEncryption() const;
|
||||
bool shouldAttemptDeferredConnect(double nowSeconds, double minIntervalSeconds = 3.0);
|
||||
void clearDeferredEncryption();
|
||||
|
||||
DeferredEncryptionResult runDeferredEncryption(DeferredEncryptionSnapshot request,
|
||||
RpcGateway& rpc,
|
||||
VaultGateway* vault);
|
||||
|
||||
static PinValidationResult validatePinSetup(const std::string& pin,
|
||||
const std::string& confirmation,
|
||||
bool allowEmpty = false,
|
||||
std::size_t minLength = 4);
|
||||
static KeyKind classifyAddress(const std::string& address);
|
||||
static KeyKind classifyPrivateKey(const std::string& key);
|
||||
static const char* importSuccessMessage(KeyKind kind);
|
||||
static std::string decryptExportFileName(std::uint64_t timestampSeconds);
|
||||
static void secureClear(std::string& value);
|
||||
|
||||
private:
|
||||
struct DeferredEncryptionState {
|
||||
std::string passphrase;
|
||||
std::string pin;
|
||||
bool pending = false;
|
||||
double lastConnectAttempt = -10.0;
|
||||
};
|
||||
|
||||
DeferredEncryptionState deferred_;
|
||||
};
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
114
src/services/wallet_security_workflow.cpp
Normal file
114
src/services/wallet_security_workflow.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
#include "wallet_security_workflow.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
void WalletSecurityWorkflow::reset()
|
||||
{
|
||||
state_ = {};
|
||||
}
|
||||
|
||||
void WalletSecurityWorkflow::start(std::chrono::steady_clock::time_point now)
|
||||
{
|
||||
state_.phase = DecryptPhase::Working;
|
||||
state_.step = DecryptStep::Unlock;
|
||||
state_.status = stepStatus(DecryptStep::Unlock);
|
||||
state_.inProgress = true;
|
||||
state_.stepStarted = now;
|
||||
state_.overallStarted = now;
|
||||
}
|
||||
|
||||
void WalletSecurityWorkflow::advanceTo(DecryptStep step, std::string status,
|
||||
std::chrono::steady_clock::time_point now)
|
||||
{
|
||||
state_.phase = DecryptPhase::Working;
|
||||
state_.step = step;
|
||||
state_.status = std::move(status);
|
||||
state_.inProgress = true;
|
||||
state_.stepStarted = now;
|
||||
}
|
||||
|
||||
void WalletSecurityWorkflow::failEntry(std::string status)
|
||||
{
|
||||
state_.phase = DecryptPhase::PassphraseEntry;
|
||||
state_.step = DecryptStep::Unlock;
|
||||
state_.status = std::move(status);
|
||||
state_.inProgress = false;
|
||||
}
|
||||
|
||||
void WalletSecurityWorkflow::fail(std::string status)
|
||||
{
|
||||
state_.phase = DecryptPhase::Error;
|
||||
state_.status = std::move(status);
|
||||
state_.inProgress = false;
|
||||
}
|
||||
|
||||
void WalletSecurityWorkflow::closeDialogForImport()
|
||||
{
|
||||
state_.inProgress = false;
|
||||
state_.importActive = true;
|
||||
}
|
||||
|
||||
void WalletSecurityWorkflow::finishImport()
|
||||
{
|
||||
state_.importActive = false;
|
||||
}
|
||||
|
||||
WalletSecurityWorkflow::WalletFilePlan WalletSecurityWorkflow::planWalletFiles(
|
||||
const std::string& dataDir,
|
||||
std::uint64_t timestampSeconds)
|
||||
{
|
||||
WalletFilePlan plan;
|
||||
plan.dataDir = dataDir;
|
||||
plan.exportFile = WalletSecurityController::decryptExportFileName(timestampSeconds);
|
||||
plan.exportPath = dataDir + plan.exportFile;
|
||||
plan.walletPath = dataDir + "wallet.dat";
|
||||
plan.backupPath = dataDir + "wallet.dat.encrypted.bak";
|
||||
return plan;
|
||||
}
|
||||
|
||||
const char* WalletSecurityWorkflow::stepStatus(DecryptStep step)
|
||||
{
|
||||
switch (step) {
|
||||
case DecryptStep::Unlock: return "Unlocking wallet...";
|
||||
case DecryptStep::ExportKeys: return "Exporting wallet keys...";
|
||||
case DecryptStep::StopDaemon: return "Stopping daemon...";
|
||||
case DecryptStep::BackupWallet: return "Backing up encrypted wallet...";
|
||||
case DecryptStep::RestartDaemon: return "Restarting daemon...";
|
||||
case DecryptStep::ImportKeys: return "Importing wallet keys...";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const char* WalletSecurityWorkflow::stepLabel(DecryptStep step)
|
||||
{
|
||||
switch (step) {
|
||||
case DecryptStep::Unlock: return "Unlocking wallet";
|
||||
case DecryptStep::ExportKeys: return "Exporting wallet keys";
|
||||
case DecryptStep::StopDaemon: return "Stopping daemon";
|
||||
case DecryptStep::BackupWallet: return "Backing up encrypted wallet";
|
||||
case DecryptStep::RestartDaemon: return "Restarting daemon";
|
||||
case DecryptStep::ImportKeys: return "Importing wallet keys";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
WalletSecurityWorkflow::DecryptStep WalletSecurityWorkflow::stepFromIndex(int step)
|
||||
{
|
||||
if (step <= 0) return DecryptStep::Unlock;
|
||||
if (step == 1) return DecryptStep::ExportKeys;
|
||||
if (step == 2) return DecryptStep::StopDaemon;
|
||||
if (step == 3) return DecryptStep::BackupWallet;
|
||||
if (step == 4) return DecryptStep::RestartDaemon;
|
||||
return DecryptStep::ImportKeys;
|
||||
}
|
||||
|
||||
bool WalletSecurityWorkflow::stepIsComplete(DecryptStep current, DecryptStep candidate)
|
||||
{
|
||||
return stepIndex(candidate) < stepIndex(current);
|
||||
}
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
78
src/services/wallet_security_workflow.h
Normal file
78
src/services/wallet_security_workflow.h
Normal file
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include "wallet_security_controller.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
class WalletSecurityWorkflow {
|
||||
public:
|
||||
enum class DecryptPhase {
|
||||
PassphraseEntry = 0,
|
||||
Working = 1,
|
||||
Success = 2,
|
||||
Error = 3
|
||||
};
|
||||
|
||||
enum class DecryptStep {
|
||||
Unlock = 0,
|
||||
ExportKeys = 1,
|
||||
StopDaemon = 2,
|
||||
BackupWallet = 3,
|
||||
RestartDaemon = 4,
|
||||
ImportKeys = 5
|
||||
};
|
||||
|
||||
struct DecryptSnapshot {
|
||||
DecryptPhase phase = DecryptPhase::PassphraseEntry;
|
||||
DecryptStep step = DecryptStep::Unlock;
|
||||
std::string status;
|
||||
bool inProgress = false;
|
||||
bool importActive = false;
|
||||
std::chrono::steady_clock::time_point stepStarted{};
|
||||
std::chrono::steady_clock::time_point overallStarted{};
|
||||
};
|
||||
|
||||
struct WalletFilePlan {
|
||||
std::string dataDir;
|
||||
std::string exportFile;
|
||||
std::string exportPath;
|
||||
std::string walletPath;
|
||||
std::string backupPath;
|
||||
};
|
||||
|
||||
void reset();
|
||||
void start(std::chrono::steady_clock::time_point now);
|
||||
void advanceTo(DecryptStep step, std::string status,
|
||||
std::chrono::steady_clock::time_point now);
|
||||
void failEntry(std::string status);
|
||||
void fail(std::string status);
|
||||
void closeDialogForImport();
|
||||
void finishImport();
|
||||
|
||||
DecryptSnapshot snapshot() const { return state_; }
|
||||
DecryptPhase phase() const { return state_.phase; }
|
||||
DecryptStep step() const { return state_.step; }
|
||||
const std::string& status() const { return state_.status; }
|
||||
bool inProgress() const { return state_.inProgress; }
|
||||
bool importActive() const { return state_.importActive; }
|
||||
bool canClose() const { return state_.phase != DecryptPhase::Working; }
|
||||
|
||||
static WalletFilePlan planWalletFiles(const std::string& dataDir,
|
||||
std::uint64_t timestampSeconds);
|
||||
static const char* stepStatus(DecryptStep step);
|
||||
static const char* stepLabel(DecryptStep step);
|
||||
static int stepIndex(DecryptStep step) { return static_cast<int>(step); }
|
||||
static DecryptStep stepFromIndex(int step);
|
||||
static bool stepIsComplete(DecryptStep current, DecryptStep candidate);
|
||||
|
||||
private:
|
||||
DecryptSnapshot state_;
|
||||
};
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
104
src/services/wallet_security_workflow_executor.cpp
Normal file
104
src/services/wallet_security_workflow_executor.cpp
Normal file
@@ -0,0 +1,104 @@
|
||||
#include "wallet_security_workflow_executor.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::unlockWallet(
|
||||
const std::string& passphrase, RpcGateway& rpc, int timeoutSeconds)
|
||||
{
|
||||
std::string error;
|
||||
if (!rpc.unlockWallet(passphrase, timeoutSeconds, error)) {
|
||||
return {false, error.empty() ? "Incorrect passphrase" : error, true};
|
||||
}
|
||||
return {true, {}, false};
|
||||
}
|
||||
|
||||
WalletSecurityWorkflowExecutor::ExportOutcome WalletSecurityWorkflowExecutor::exportWallet(
|
||||
RpcGateway& rpc, FileGateway& files, std::uint64_t timestampSeconds, long timeoutSeconds)
|
||||
{
|
||||
ExportOutcome outcome;
|
||||
outcome.filePlan = WalletSecurityWorkflow::planWalletFiles(files.dataDir(), timestampSeconds);
|
||||
|
||||
std::string error;
|
||||
if (!rpc.exportWallet(outcome.filePlan.exportFile, timeoutSeconds, error)) {
|
||||
outcome.ok = false;
|
||||
outcome.error = error.empty() ? "Export failed" : "Export failed: " + error;
|
||||
return outcome;
|
||||
}
|
||||
|
||||
outcome.ok = true;
|
||||
return outcome;
|
||||
}
|
||||
|
||||
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::stopDaemon(RpcGateway& rpc)
|
||||
{
|
||||
std::string error;
|
||||
(void)rpc.requestDaemonStop(error);
|
||||
return {true, {}, false};
|
||||
}
|
||||
|
||||
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::backupEncryptedWallet(
|
||||
FileGateway& files, const WalletFilePlan& filePlan)
|
||||
{
|
||||
std::string error;
|
||||
if (!files.backupEncryptedWallet(filePlan, error)) {
|
||||
return {false, error.empty() ? "Failed to rename wallet.dat" : "Failed to rename wallet.dat: " + error, false};
|
||||
}
|
||||
return {true, {}, false};
|
||||
}
|
||||
|
||||
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::restartDaemonAndWait(
|
||||
DaemonGateway& daemon, RpcGateway& rpc, int preRestartDelayMs,
|
||||
int embeddedRestartSettleMs, int maxProbeSeconds)
|
||||
{
|
||||
auto waitForMs = [&](int milliseconds) -> bool {
|
||||
int remaining = milliseconds;
|
||||
while (remaining > 0 && !daemon.cancelled() && !daemon.shuttingDown()) {
|
||||
int slice = remaining >= 100 ? 100 : remaining;
|
||||
daemon.sleepForMs(slice);
|
||||
remaining -= slice;
|
||||
}
|
||||
return !daemon.cancelled() && !daemon.shuttingDown();
|
||||
};
|
||||
|
||||
if (!waitForMs(preRestartDelayMs)) return {false, "", false};
|
||||
|
||||
if (daemon.isUsingEmbeddedDaemon()) {
|
||||
daemon.stopEmbeddedDaemon();
|
||||
if (daemon.cancelled() || daemon.shuttingDown()) return {false, "", false};
|
||||
if (!waitForMs(embeddedRestartSettleMs)) return {false, "", false};
|
||||
daemon.startEmbeddedDaemon();
|
||||
}
|
||||
|
||||
bool daemonUp = false;
|
||||
std::string lastError;
|
||||
for (int i = 0; i < maxProbeSeconds && !daemon.cancelled() && !daemon.shuttingDown(); ++i) {
|
||||
daemon.sleepForMs(1000);
|
||||
if (rpc.probeDaemon(lastError)) {
|
||||
daemonUp = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (daemon.cancelled() || daemon.shuttingDown()) return {false, "", false};
|
||||
if (!daemonUp) return {false, "Daemon failed to restart", false};
|
||||
return {true, {}, false};
|
||||
}
|
||||
|
||||
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::importWallet(
|
||||
ImportGateway& importer, const std::string& exportPath, long timeoutSeconds)
|
||||
{
|
||||
std::string error;
|
||||
if (!importer.importWallet(exportPath, timeoutSeconds, error)) {
|
||||
return {false, error.empty() ? "Key import failed" : "Key import failed: " + error, false};
|
||||
}
|
||||
return {true, {}, false};
|
||||
}
|
||||
|
||||
void WalletSecurityWorkflowExecutor::cleanupVaultAndPin(const VaultCleanupGateway& cleanup)
|
||||
{
|
||||
if (cleanup) cleanup();
|
||||
}
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
83
src/services/wallet_security_workflow_executor.h
Normal file
83
src/services/wallet_security_workflow_executor.h
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include "wallet_security_workflow.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
namespace services {
|
||||
|
||||
class WalletSecurityWorkflowExecutor {
|
||||
public:
|
||||
using WalletFilePlan = WalletSecurityWorkflow::WalletFilePlan;
|
||||
|
||||
struct Outcome {
|
||||
bool ok = false;
|
||||
std::string error;
|
||||
bool passphraseRejected = false;
|
||||
};
|
||||
|
||||
struct ExportOutcome : Outcome {
|
||||
WalletFilePlan filePlan;
|
||||
};
|
||||
|
||||
class RpcGateway {
|
||||
public:
|
||||
virtual ~RpcGateway() = default;
|
||||
virtual bool unlockWallet(const std::string& passphrase, int timeoutSeconds, std::string& error) = 0;
|
||||
virtual bool exportWallet(const std::string& fileName, long timeoutSeconds, std::string& error) = 0;
|
||||
virtual bool requestDaemonStop(std::string& error) = 0;
|
||||
virtual bool probeDaemon(std::string& error) = 0;
|
||||
};
|
||||
|
||||
class ImportGateway {
|
||||
public:
|
||||
virtual ~ImportGateway() = default;
|
||||
virtual bool importWallet(const std::string& exportPath, long timeoutSeconds, std::string& error) = 0;
|
||||
};
|
||||
|
||||
class FileGateway {
|
||||
public:
|
||||
virtual ~FileGateway() = default;
|
||||
virtual std::string dataDir() = 0;
|
||||
virtual bool backupEncryptedWallet(const WalletFilePlan& filePlan, std::string& error) = 0;
|
||||
};
|
||||
|
||||
class DaemonGateway {
|
||||
public:
|
||||
virtual ~DaemonGateway() = default;
|
||||
virtual bool isUsingEmbeddedDaemon() const = 0;
|
||||
virtual void stopEmbeddedDaemon() = 0;
|
||||
virtual bool startEmbeddedDaemon() = 0;
|
||||
virtual bool cancelled() const = 0;
|
||||
virtual bool shuttingDown() const = 0;
|
||||
virtual void sleepForMs(int milliseconds) = 0;
|
||||
};
|
||||
|
||||
using VaultCleanupGateway = std::function<void()>;
|
||||
|
||||
static Outcome unlockWallet(const std::string& passphrase,
|
||||
RpcGateway& rpc,
|
||||
int timeoutSeconds = 600);
|
||||
static ExportOutcome exportWallet(RpcGateway& rpc,
|
||||
FileGateway& files,
|
||||
std::uint64_t timestampSeconds,
|
||||
long timeoutSeconds = 300L);
|
||||
static Outcome stopDaemon(RpcGateway& rpc);
|
||||
static Outcome backupEncryptedWallet(FileGateway& files,
|
||||
const WalletFilePlan& filePlan);
|
||||
static Outcome restartDaemonAndWait(DaemonGateway& daemon,
|
||||
RpcGateway& rpc,
|
||||
int preRestartDelayMs = 2000,
|
||||
int embeddedRestartSettleMs = 1000,
|
||||
int maxProbeSeconds = 60);
|
||||
static Outcome importWallet(ImportGateway& importer,
|
||||
const std::string& exportPath,
|
||||
long timeoutSeconds = 1200L);
|
||||
static void cleanupVaultAndPin(const VaultCleanupGateway& cleanup);
|
||||
};
|
||||
|
||||
} // namespace services
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user