refactor: tab-aware prioritized refresh system
Split monolithic refreshData() into independent sub-functions (refreshCoreData, refreshAddressData, refreshTransactionData, refreshEncryptionState) each with its own timer and atomic guard. Per-category timers replace the single 5s refresh_timer_: - core_timer_: balance + blockchain info (5s default) - transaction_timer_: tx list + enrichment (10s default) - address_timer_: z/t address lists (15s default) - peer_timer_: encryption state (10s default) Tab-switching via setCurrentPage() adjusts active intervals so the current tab's data refreshes faster (e.g. 3s core on Overview, 5s transactions on History) while background categories slow down. Use fast_worker_ for core data on Overview tab to avoid blocking behind the main refresh batch. Bump version to 1.1.2.
This commit is contained in:
@@ -15,7 +15,7 @@ if(APPLE)
|
||||
endif()
|
||||
|
||||
project(ObsidianDragon
|
||||
VERSION 1.1.1
|
||||
VERSION 1.1.2
|
||||
LANGUAGES C CXX
|
||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||
)
|
||||
|
||||
46
src/app.cpp
46
src/app.cpp
@@ -343,7 +343,10 @@ void App::update()
|
||||
}
|
||||
|
||||
// Update timers
|
||||
refresh_timer_ += io.DeltaTime;
|
||||
core_timer_ += io.DeltaTime;
|
||||
address_timer_ += io.DeltaTime;
|
||||
transaction_timer_ += io.DeltaTime;
|
||||
peer_timer_ += io.DeltaTime;
|
||||
price_timer_ += io.DeltaTime;
|
||||
fast_refresh_timer_ += io.DeltaTime;
|
||||
tx_age_timer_ += io.DeltaTime;
|
||||
@@ -591,19 +594,36 @@ void App::update()
|
||||
transactions_dirty_ = true;
|
||||
addresses_dirty_ = true;
|
||||
last_tx_block_height_ = -1;
|
||||
refresh_timer_ = REFRESH_INTERVAL;
|
||||
core_timer_ = active_core_interval_;
|
||||
transaction_timer_ = active_tx_interval_;
|
||||
address_timer_ = active_addr_interval_;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Regular refresh every 5 seconds
|
||||
// Per-category refresh with tab-aware intervals
|
||||
// Skip when wallet is locked — same reason as above.
|
||||
if (refresh_timer_ >= REFRESH_INTERVAL) {
|
||||
refresh_timer_ = 0.0f;
|
||||
if (state_.connected && !state_.isLocked()) {
|
||||
refreshData();
|
||||
} else if (!connection_in_progress_ &&
|
||||
if (core_timer_ >= active_core_interval_) {
|
||||
core_timer_ = 0.0f;
|
||||
refreshCoreData();
|
||||
}
|
||||
if (transaction_timer_ >= active_tx_interval_) {
|
||||
transaction_timer_ = 0.0f;
|
||||
refreshTransactionData();
|
||||
}
|
||||
if (address_timer_ >= active_addr_interval_) {
|
||||
address_timer_ = 0.0f;
|
||||
refreshAddressData();
|
||||
}
|
||||
if (peer_timer_ >= active_peer_interval_) {
|
||||
peer_timer_ = 0.0f;
|
||||
refreshEncryptionState();
|
||||
}
|
||||
} else if (core_timer_ >= active_core_interval_) {
|
||||
core_timer_ = 0.0f;
|
||||
if (!connection_in_progress_ &&
|
||||
wizard_phase_ == WizardPhase::None) {
|
||||
tryConnect();
|
||||
}
|
||||
@@ -619,7 +639,7 @@ void App::update()
|
||||
|
||||
// Keyboard shortcut: Ctrl+, to open Settings page
|
||||
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Comma)) {
|
||||
current_page_ = ui::NavPage::Settings;
|
||||
setCurrentPage(ui::NavPage::Settings);
|
||||
}
|
||||
|
||||
// Keyboard shortcut: Ctrl+Left/Right to cycle themes
|
||||
@@ -1886,7 +1906,11 @@ void App::renderAntivirusHelpDialog()
|
||||
|
||||
void App::refreshNow()
|
||||
{
|
||||
refresh_timer_ = REFRESH_INTERVAL; // Trigger immediate refresh
|
||||
// Trigger immediate refresh on all categories
|
||||
core_timer_ = active_core_interval_;
|
||||
transaction_timer_ = active_tx_interval_;
|
||||
address_timer_ = active_addr_interval_;
|
||||
peer_timer_ = active_peer_interval_;
|
||||
transactions_dirty_ = true; // Force transaction list update
|
||||
addresses_dirty_ = true; // Force address/balance update
|
||||
last_tx_block_height_ = -1; // Reset tx cache
|
||||
@@ -1909,7 +1933,7 @@ void App::handlePaymentURI(const std::string& uri)
|
||||
pending_label_ = payment.label;
|
||||
|
||||
// Switch to Send page
|
||||
current_page_ = ui::NavPage::Send;
|
||||
setCurrentPage(ui::NavPage::Send);
|
||||
|
||||
// Notify user
|
||||
std::string msg = "Payment request loaded";
|
||||
@@ -1936,7 +1960,7 @@ void App::setCurrentTab(int tab) {
|
||||
ui::NavPage::Settings, // 9 = Settings
|
||||
};
|
||||
if (tab >= 0 && tab < static_cast<int>(sizeof(kTabMap)/sizeof(kTabMap[0])))
|
||||
current_page_ = kTabMap[tab];
|
||||
setCurrentPage(kTabMap[tab]);
|
||||
}
|
||||
|
||||
bool App::startEmbeddedDaemon()
|
||||
|
||||
56
src/app.h
56
src/app.h
@@ -221,13 +221,19 @@ public:
|
||||
void refreshPeerInfo();
|
||||
void refreshMarketData();
|
||||
|
||||
/// @brief Per-category refresh intervals, adjusted by active tab
|
||||
struct RefreshIntervals {
|
||||
float core; // balance + sync status
|
||||
float transactions; // tx list + enrichment
|
||||
float addresses; // address lists + balances
|
||||
float peers; // peer info (0 = disabled)
|
||||
};
|
||||
|
||||
/// @brief Get recommended refresh intervals for a given page
|
||||
static RefreshIntervals getIntervalsForPage(ui::NavPage page);
|
||||
|
||||
// UI navigation
|
||||
void setCurrentPage(ui::NavPage page) {
|
||||
if (page != current_page_) {
|
||||
current_page_ = page;
|
||||
if (page == ui::NavPage::Peers) refreshPeerInfo();
|
||||
}
|
||||
}
|
||||
void setCurrentPage(ui::NavPage page);
|
||||
ui::NavPage getCurrentPage() const { return current_page_; }
|
||||
|
||||
// Dialog triggers (used by settings page to open modal dialogs)
|
||||
@@ -362,7 +368,6 @@ private:
|
||||
// Shutdown state
|
||||
std::atomic<bool> shutting_down_{false};
|
||||
std::atomic<bool> shutdown_complete_{false};
|
||||
std::atomic<bool> refresh_in_progress_{false};
|
||||
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
||||
std::string shutdown_status_;
|
||||
std::thread shutdown_thread_;
|
||||
@@ -438,16 +443,33 @@ private:
|
||||
std::string pending_memo_;
|
||||
std::string pending_label_;
|
||||
|
||||
// Timers (in seconds since last update)
|
||||
float refresh_timer_ = 0.0f;
|
||||
// Per-category timers (in seconds since last refresh)
|
||||
float core_timer_ = 0.0f; // balance + sync status
|
||||
float address_timer_ = 0.0f; // address lists
|
||||
float transaction_timer_ = 0.0f; // transaction list
|
||||
float peer_timer_ = 0.0f; // peer info
|
||||
float price_timer_ = 0.0f;
|
||||
float fast_refresh_timer_ = 0.0f; // For mining stats
|
||||
|
||||
// Refresh intervals (seconds)
|
||||
static constexpr float REFRESH_INTERVAL = 5.0f;
|
||||
// Default refresh intervals (seconds)
|
||||
static constexpr float CORE_INTERVAL_DEFAULT = 5.0f;
|
||||
static constexpr float ADDRESS_INTERVAL_DEFAULT = 15.0f;
|
||||
static constexpr float TX_INTERVAL_DEFAULT = 10.0f;
|
||||
static constexpr float PEER_INTERVAL_DEFAULT = 10.0f;
|
||||
static constexpr float PRICE_INTERVAL = 60.0f;
|
||||
static constexpr float FAST_REFRESH_INTERVAL = 1.0f;
|
||||
|
||||
// Active intervals — adjusted by tab priority via applyRefreshPolicy()
|
||||
float active_core_interval_ = CORE_INTERVAL_DEFAULT;
|
||||
float active_tx_interval_ = TX_INTERVAL_DEFAULT;
|
||||
float active_addr_interval_ = ADDRESS_INTERVAL_DEFAULT;
|
||||
float active_peer_interval_ = PEER_INTERVAL_DEFAULT;
|
||||
|
||||
// Per-category refresh guards (prevent worker queue pileup)
|
||||
std::atomic<bool> core_refresh_in_progress_{false};
|
||||
std::atomic<bool> address_refresh_in_progress_{false};
|
||||
std::atomic<bool> tx_refresh_in_progress_{false};
|
||||
|
||||
// Mining refresh guard (prevents worker queue pileup)
|
||||
std::atomic<bool> mining_refresh_in_progress_{false};
|
||||
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
|
||||
@@ -604,11 +626,17 @@ private:
|
||||
void applyDefaultBanlist();
|
||||
|
||||
// Private methods - data refresh
|
||||
void refreshData();
|
||||
void refreshBalance();
|
||||
void refreshAddresses();
|
||||
void refreshData(); // Orchestrator: dispatches per-category refreshes
|
||||
void refreshCoreData(); // Balance + blockchain info (can use fast_worker_)
|
||||
void refreshAddressData(); // Address lists + balances
|
||||
void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment
|
||||
void refreshEncryptionState(); // Wallet encryption/lock state
|
||||
void refreshBalance(); // Legacy: balance-only refresh (used by specific callers)
|
||||
void refreshAddresses(); // Legacy: standalone address refresh
|
||||
void refreshPrice();
|
||||
void refreshWalletEncryptionState();
|
||||
void applyRefreshPolicy(ui::NavPage page);
|
||||
bool shouldRefreshTransactions() const;
|
||||
void checkAutoLock();
|
||||
void checkIdleMining();
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ void App::tryConnect()
|
||||
if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) {
|
||||
connection_status_ = "Waiting for daemon config...";
|
||||
VERBOSE_LOGF("[connect #%d] External daemon detected on port, waiting for config file to appear\n", connect_attempt);
|
||||
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
|
||||
core_timer_ = CORE_INTERVAL_DEFAULT - 1.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,11 +89,11 @@ void App::tryConnect()
|
||||
if (startEmbeddedDaemon()) {
|
||||
// Will retry connection after daemon starts
|
||||
VERBOSE_LOGF("[connect #%d] Embedded daemon starting, will retry connection...\n", connect_attempt);
|
||||
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
|
||||
core_timer_ = CORE_INTERVAL_DEFAULT - 1.0f;
|
||||
} else if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) {
|
||||
connection_status_ = "Waiting for daemon config...";
|
||||
VERBOSE_LOGF("[connect #%d] External daemon detected but no config yet, will retry...\n", connect_attempt);
|
||||
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
|
||||
core_timer_ = CORE_INTERVAL_DEFAULT - 1.0f;
|
||||
} else {
|
||||
VERBOSE_LOGF("[connect #%d] startEmbeddedDaemon() failed — lastError: %s, binary: %s\n",
|
||||
connect_attempt,
|
||||
@@ -208,13 +208,13 @@ void App::tryConnect()
|
||||
connection_status_ = "Waiting for dragonxd to start...";
|
||||
VERBOSE_LOGF("[connect #%d] RPC connection failed — daemon still starting, will retry...\n", attempt);
|
||||
// Fast retry: force the refresh timer to fire on the next cycle
|
||||
// instead of waiting the full 5-second REFRESH_INTERVAL.
|
||||
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
|
||||
// instead of waiting the full 5-second interval.
|
||||
core_timer_ = CORE_INTERVAL_DEFAULT - 1.0f;
|
||||
} else if (externalDetected) {
|
||||
state_.connected = false;
|
||||
connection_status_ = "Connecting to daemon...";
|
||||
VERBOSE_LOGF("[connect #%d] RPC connection failed — external daemon on port but RPC not ready yet, will retry...\n", attempt);
|
||||
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
|
||||
core_timer_ = CORE_INTERVAL_DEFAULT - 1.0f;
|
||||
} else if (connectErr.find("Loading") != std::string::npos ||
|
||||
connectErr.find("Verifying") != std::string::npos ||
|
||||
connectErr.find("Activating") != std::string::npos ||
|
||||
@@ -225,7 +225,7 @@ void App::tryConnect()
|
||||
state_.connected = false;
|
||||
connection_status_ = connectErr;
|
||||
VERBOSE_LOGF("[connect #%d] Daemon warmup: %s\n", attempt, connectErr.c_str());
|
||||
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
|
||||
core_timer_ = CORE_INTERVAL_DEFAULT - 1.0f;
|
||||
} else {
|
||||
onDisconnected("Connection failed");
|
||||
VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt);
|
||||
@@ -394,111 +394,236 @@ void App::onDisconnected(const std::string& reason)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Data Refresh
|
||||
// Data Refresh — Tab-Aware Prioritized System
|
||||
//
|
||||
// Data is split into independent categories, each with its own refresh
|
||||
// function, timer, and in-progress guard. The orchestrator (refreshData)
|
||||
// dispatches all categories, but each can also be called independently
|
||||
// (e.g. on tab switch for immediate refresh).
|
||||
//
|
||||
// Categories:
|
||||
// Core — z_gettotalbalance + getblockchaininfo (balance, sync)
|
||||
// Addresses — z_listaddresses + listunspent (address list, per-addr balances)
|
||||
// Transactions — listtransactions + z_listreceivedbyaddress + z_viewtransaction
|
||||
// Peers — getpeerinfo + listbanned (already standalone)
|
||||
// Encryption — getwalletinfo (one-shot on connect)
|
||||
//
|
||||
// Intervals are adjusted by applyRefreshPolicy() based on the active tab,
|
||||
// so the user sees faster updates for the data they're interacting with.
|
||||
// ============================================================================
|
||||
|
||||
App::RefreshIntervals App::getIntervalsForPage(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 App::applyRefreshPolicy(ui::NavPage page)
|
||||
{
|
||||
auto intervals = getIntervalsForPage(page);
|
||||
active_core_interval_ = intervals.core;
|
||||
active_tx_interval_ = intervals.transactions;
|
||||
active_addr_interval_ = intervals.addresses;
|
||||
active_peer_interval_ = intervals.peers;
|
||||
}
|
||||
|
||||
void App::setCurrentPage(ui::NavPage page)
|
||||
{
|
||||
if (page == current_page_) return;
|
||||
current_page_ = page;
|
||||
applyRefreshPolicy(page);
|
||||
|
||||
// Immediate refresh for the incoming tab's priority data
|
||||
if (state_.connected && !state_.isLocked()) {
|
||||
using NP = ui::NavPage;
|
||||
switch (page) {
|
||||
case NP::Overview:
|
||||
refreshCoreData();
|
||||
core_timer_ = 0.0f;
|
||||
break;
|
||||
case NP::History:
|
||||
transactions_dirty_ = true;
|
||||
refreshTransactionData();
|
||||
transaction_timer_ = 0.0f;
|
||||
break;
|
||||
case NP::Send:
|
||||
case NP::Receive:
|
||||
addresses_dirty_ = true;
|
||||
refreshAddressData();
|
||||
address_timer_ = 0.0f;
|
||||
break;
|
||||
case NP::Peers:
|
||||
refreshPeerInfo();
|
||||
peer_timer_ = 0.0f;
|
||||
break;
|
||||
case NP::Mining:
|
||||
refreshMiningInfo();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool App::shouldRefreshTransactions() const
|
||||
{
|
||||
const int currentBlocks = state_.sync.blocks;
|
||||
return last_tx_block_height_ < 0
|
||||
|| currentBlocks != last_tx_block_height_
|
||||
|| state_.transactions.empty()
|
||||
|| transactions_dirty_
|
||||
|| tx_age_timer_ >= TX_MAX_AGE;
|
||||
}
|
||||
|
||||
void App::refreshData()
|
||||
{
|
||||
if (!state_.connected || !rpc_ || !worker_) return;
|
||||
|
||||
// Prevent overlapping refreshes — skip if one is still running
|
||||
if (refresh_in_progress_.exchange(true)) return;
|
||||
// Dispatch each category independently — results trickle into the UI
|
||||
// as each completes, rather than waiting for the slowest phase.
|
||||
refreshCoreData();
|
||||
|
||||
// Capture decision flags on the main thread before posting to worker.
|
||||
const bool doAddresses = addresses_dirty_;
|
||||
const bool doPeers = (current_page_ == ui::NavPage::Peers);
|
||||
const bool doEncrypt = !encryption_state_prefetched_;
|
||||
if (encryption_state_prefetched_) encryption_state_prefetched_ = false;
|
||||
if (addresses_dirty_)
|
||||
refreshAddressData();
|
||||
|
||||
// P4a: Refresh transactions when new blocks arrive, when explicitly
|
||||
// dirtied (e.g. after a send), or when the max-age timer expires
|
||||
// (catches mempool changes for incoming unconfirmed transactions).
|
||||
const int currentBlocks = state_.sync.blocks;
|
||||
const bool doTransactions = (last_tx_block_height_ < 0
|
||||
|| currentBlocks != last_tx_block_height_
|
||||
|| state_.transactions.empty()
|
||||
|| transactions_dirty_
|
||||
|| tx_age_timer_ >= TX_MAX_AGE);
|
||||
if (doTransactions) {
|
||||
transactions_dirty_ = false;
|
||||
tx_age_timer_ = 0.0f;
|
||||
}
|
||||
if (shouldRefreshTransactions())
|
||||
refreshTransactionData();
|
||||
|
||||
// Snapshot z-addresses for transaction fetch (needed on worker thread)
|
||||
std::vector<std::string> txZAddrs;
|
||||
if (doTransactions) {
|
||||
for (const auto& za : state_.z_addresses) {
|
||||
if (!za.address.empty()) txZAddrs.push_back(za.address);
|
||||
if (current_page_ == ui::NavPage::Peers)
|
||||
refreshPeerInfo();
|
||||
|
||||
if (!encryption_state_prefetched_) {
|
||||
encryption_state_prefetched_ = false;
|
||||
refreshEncryptionState();
|
||||
}
|
||||
}
|
||||
|
||||
// P4b: Collect txids that are fully enriched (skip re-enrichment)
|
||||
// Include all txids already in the viewtx cache — these never need
|
||||
// another z_viewtransaction RPC call.
|
||||
std::unordered_set<std::string> fullyEnriched;
|
||||
if (doTransactions) {
|
||||
// Txids we already have z_viewtransaction results for
|
||||
for (const auto& [txid, _] : viewtx_cache_) {
|
||||
fullyEnriched.insert(txid);
|
||||
}
|
||||
// Txids that are deeply confirmed and timestamped
|
||||
for (const auto& tx : state_.transactions) {
|
||||
if (tx.confirmations > 6 && tx.timestamp != 0) {
|
||||
fullyEnriched.insert(tx.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ============================================================================
|
||||
// Core Data: balance + blockchain info (~50-100ms, 2 RPC calls)
|
||||
// Uses fast_worker_ when on Overview tab for lower latency.
|
||||
// ============================================================================
|
||||
|
||||
// P4c: Snapshot the confirmed transaction cache for the worker thread.
|
||||
// These are deeply-confirmed txns we can reuse without re-fetching.
|
||||
std::vector<TransactionInfo> cachedConfirmedTxns;
|
||||
std::unordered_set<std::string> cachedConfirmedIds;
|
||||
if (doTransactions && !confirmed_tx_cache_.empty()) {
|
||||
cachedConfirmedTxns = confirmed_tx_cache_;
|
||||
cachedConfirmedIds = confirmed_tx_ids_;
|
||||
}
|
||||
void App::refreshCoreData()
|
||||
{
|
||||
if (!state_.connected) return;
|
||||
|
||||
// Snapshot viewtx cache for the worker thread
|
||||
auto viewtxCacheSnap = viewtx_cache_;
|
||||
// Use fast-lane on Overview for snappier balance updates
|
||||
bool useFast = (current_page_ == ui::NavPage::Overview);
|
||||
auto* w = useFast && fast_worker_ && fast_worker_->isRunning()
|
||||
? fast_worker_.get() : worker_.get();
|
||||
auto* rpc = useFast && fast_rpc_ && fast_rpc_->isConnected()
|
||||
? fast_rpc_.get() : rpc_.get();
|
||||
if (!w || !rpc) return;
|
||||
|
||||
// Snapshot send txids so the worker can include them in enrichment
|
||||
auto sendTxidsSnap = send_txids_;
|
||||
if (core_refresh_in_progress_.exchange(true)) return;
|
||||
|
||||
// Single consolidated worker task — all RPC calls happen back-to-back
|
||||
// on a single thread with no inter-task queue overhead.
|
||||
worker_->post([this, doAddresses, doPeers, doEncrypt, doTransactions,
|
||||
currentBlocks, txZAddrs = std::move(txZAddrs),
|
||||
fullyEnriched = std::move(fullyEnriched),
|
||||
cachedConfirmedTxns = std::move(cachedConfirmedTxns),
|
||||
cachedConfirmedIds = std::move(cachedConfirmedIds),
|
||||
viewtxCacheSnap = std::move(viewtxCacheSnap),
|
||||
sendTxidsSnap = std::move(sendTxidsSnap)]() -> rpc::RPCWorker::MainCb {
|
||||
// ================================================================
|
||||
// Phase 1: Balance + blockchain info
|
||||
// ================================================================
|
||||
w->post([this, rpc]() -> rpc::RPCWorker::MainCb {
|
||||
json totalBal, blockInfo;
|
||||
bool balOk = false, blockOk = false;
|
||||
|
||||
try {
|
||||
totalBal = rpc_->call("z_gettotalbalance");
|
||||
totalBal = rpc->call("z_gettotalbalance");
|
||||
balOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Balance error: %s\n", e.what());
|
||||
}
|
||||
try {
|
||||
blockInfo = rpc_->call("getblockchaininfo");
|
||||
blockInfo = rpc->call("getblockchaininfo");
|
||||
blockOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Phase 2: Addresses (only when dirtied)
|
||||
// ================================================================
|
||||
return [this, totalBal, blockInfo, balOk, blockOk]() {
|
||||
try {
|
||||
if (balOk) {
|
||||
if (totalBal.contains("private"))
|
||||
state_.shielded_balance = std::stod(totalBal["private"].get<std::string>());
|
||||
if (totalBal.contains("transparent"))
|
||||
state_.transparent_balance = std::stod(totalBal["transparent"].get<std::string>());
|
||||
if (totalBal.contains("total"))
|
||||
state_.total_balance = std::stod(totalBal["total"].get<std::string>());
|
||||
state_.last_balance_update = std::time(nullptr);
|
||||
}
|
||||
if (blockOk) {
|
||||
if (blockInfo.contains("blocks"))
|
||||
state_.sync.blocks = blockInfo["blocks"].get<int>();
|
||||
if (blockInfo.contains("headers"))
|
||||
state_.sync.headers = blockInfo["headers"].get<int>();
|
||||
if (blockInfo.contains("verificationprogress"))
|
||||
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
|
||||
if (blockInfo.contains("longestchain")) {
|
||||
int lc = blockInfo["longestchain"].get<int>();
|
||||
if (lc > 0) state_.longestchain = lc;
|
||||
}
|
||||
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 (blockInfo.contains("notarized"))
|
||||
state_.notarized = blockInfo["notarized"].get<int>();
|
||||
}
|
||||
// Auto-shield transparent funds if enabled
|
||||
if (balOk && settings_ && settings_->getAutoShield() &&
|
||||
state_.transparent_balance > 0.0001 && !state_.sync.syncing &&
|
||||
!auto_shield_pending_.exchange(true)) {
|
||||
std::string targetZAddr;
|
||||
for (const auto& addr : state_.addresses) {
|
||||
if (addr.isShielded()) {
|
||||
targetZAddr = addr.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!targetZAddr.empty() && rpc_) {
|
||||
DEBUG_LOGF("[AutoShield] Shielding %.8f DRGX to %s\n",
|
||||
state_.transparent_balance, targetZAddr.c_str());
|
||||
rpc_->z_shieldCoinbase("*", targetZAddr, 0.0001, 50,
|
||||
[this](const json& result) {
|
||||
if (result.contains("opid")) {
|
||||
DEBUG_LOGF("[AutoShield] Started: %s\n",
|
||||
result["opid"].get<std::string>().c_str());
|
||||
}
|
||||
auto_shield_pending_ = false;
|
||||
},
|
||||
[this](const std::string& err) {
|
||||
DEBUG_LOGF("[AutoShield] Error: %s\n", err.c_str());
|
||||
auto_shield_pending_ = false;
|
||||
});
|
||||
} else {
|
||||
auto_shield_pending_ = false;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("[refreshCoreData] callback error: %s\n", e.what());
|
||||
}
|
||||
core_refresh_in_progress_.store(false, std::memory_order_release);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Address Data: z/t address lists + per-address balances
|
||||
// ============================================================================
|
||||
|
||||
void App::refreshAddressData()
|
||||
{
|
||||
if (!worker_ || !rpc_ || !state_.connected) return;
|
||||
if (address_refresh_in_progress_.exchange(true)) return;
|
||||
|
||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||
std::vector<AddressInfo> zAddrs, tAddrs;
|
||||
bool addrOk = false;
|
||||
if (doAddresses) {
|
||||
addrOk = true;
|
||||
|
||||
// z-addresses
|
||||
try {
|
||||
json zList = rpc_->call("z_listaddresses");
|
||||
@@ -559,16 +684,59 @@ void App::refreshData()
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
||||
}
|
||||
|
||||
return [this, zAddrs = std::move(zAddrs), tAddrs = std::move(tAddrs)]() {
|
||||
state_.z_addresses = std::move(zAddrs);
|
||||
state_.t_addresses = std::move(tAddrs);
|
||||
address_list_dirty_ = true;
|
||||
addresses_dirty_ = false;
|
||||
address_refresh_in_progress_.store(false, std::memory_order_release);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Phase 3: Transactions (only when new blocks)
|
||||
// ================================================================
|
||||
// ============================================================================
|
||||
// Transaction Data: transparent + shielded receives + z_viewtransaction enrichment
|
||||
// ============================================================================
|
||||
|
||||
void App::refreshTransactionData()
|
||||
{
|
||||
if (!worker_ || !rpc_ || !state_.connected) return;
|
||||
if (tx_refresh_in_progress_.exchange(true)) return;
|
||||
|
||||
// Capture decision state on main thread
|
||||
const int currentBlocks = state_.sync.blocks;
|
||||
transactions_dirty_ = false;
|
||||
tx_age_timer_ = 0.0f;
|
||||
|
||||
// Snapshot z-addresses for shielded receive lookups
|
||||
std::vector<std::string> txZAddrs;
|
||||
for (const auto& za : state_.z_addresses) {
|
||||
if (!za.address.empty()) txZAddrs.push_back(za.address);
|
||||
}
|
||||
|
||||
// Collect txids that are fully enriched (skip re-enrichment)
|
||||
std::unordered_set<std::string> fullyEnriched;
|
||||
for (const auto& [txid, _] : viewtx_cache_) {
|
||||
fullyEnriched.insert(txid);
|
||||
}
|
||||
for (const auto& tx : state_.transactions) {
|
||||
if (tx.confirmations > 6 && tx.timestamp != 0) {
|
||||
fullyEnriched.insert(tx.txid);
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot caches for the worker thread
|
||||
auto viewtxCacheSnap = viewtx_cache_;
|
||||
auto sendTxidsSnap = send_txids_;
|
||||
|
||||
worker_->post([this, currentBlocks,
|
||||
txZAddrs = std::move(txZAddrs),
|
||||
fullyEnriched = std::move(fullyEnriched),
|
||||
viewtxCacheSnap = std::move(viewtxCacheSnap),
|
||||
sendTxidsSnap = std::move(sendTxidsSnap)]() -> rpc::RPCWorker::MainCb {
|
||||
std::vector<TransactionInfo> txns;
|
||||
std::unordered_map<std::string, ViewTxCacheEntry> newViewTxEntries;
|
||||
bool txOk = false;
|
||||
if (doTransactions) {
|
||||
txOk = true;
|
||||
std::set<std::string> knownTxids;
|
||||
|
||||
// Phase 3a: transparent transactions
|
||||
@@ -624,16 +792,12 @@ void App::refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
// Include txids from completed z_sendmany operations so that
|
||||
// pure shielded sends (which don't appear in listtransactions
|
||||
// or z_listreceivedbyaddress) are discoverable.
|
||||
// Include txids from completed z_sendmany operations
|
||||
for (const auto& txid : sendTxidsSnap) {
|
||||
knownTxids.insert(txid);
|
||||
}
|
||||
|
||||
// Phase 3c: detect shielded sends via z_viewtransaction
|
||||
// Check the in-memory viewtx cache first; only make RPC calls
|
||||
// for txids we haven't seen before.
|
||||
int viewTxCount = 0;
|
||||
|
||||
auto applyViewTxEntry = [&](const std::string& txid,
|
||||
@@ -654,7 +818,6 @@ void App::refreshData()
|
||||
info.amount = -out.value;
|
||||
info.memo = out.memo;
|
||||
info.from_address = entry.from_address;
|
||||
// Copy confirmations/timestamp from an existing entry for this txid
|
||||
for (const auto& existing : txns) {
|
||||
if (existing.txid == txid) {
|
||||
info.confirmations = existing.confirmations;
|
||||
@@ -669,7 +832,6 @@ void App::refreshData()
|
||||
for (const std::string& txid : knownTxids) {
|
||||
if (fullyEnriched.count(txid)) continue;
|
||||
|
||||
// Check viewtx cache first — avoid RPC call
|
||||
auto cit = viewtxCacheSnap.find(txid);
|
||||
if (cit != viewtxCacheSnap.end()) {
|
||||
applyViewTxEntry(txid, cit->second);
|
||||
@@ -683,7 +845,6 @@ void App::refreshData()
|
||||
json vtx = rpc_->call("z_viewtransaction", json::array({txid}));
|
||||
if (vtx.is_null() || !vtx.is_object()) continue;
|
||||
|
||||
// Build cache entry from RPC result
|
||||
ViewTxCacheEntry entry;
|
||||
if (vtx.contains("spends") && vtx["spends"].is_array()) {
|
||||
for (const auto& spend : vtx["spends"]) {
|
||||
@@ -710,10 +871,8 @@ void App::refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to current txns list
|
||||
applyViewTxEntry(txid, entry);
|
||||
|
||||
// Fetch timestamp if missing for any new txns we just added
|
||||
for (auto& info : txns) {
|
||||
if (info.txid == txid && info.timestamp == 0) {
|
||||
try {
|
||||
@@ -723,14 +882,13 @@ void App::refreshData()
|
||||
if (!rawtx.is_null() && rawtx.contains("confirmations"))
|
||||
info.confirmations = rawtx["confirmations"].get<int>();
|
||||
} catch (...) {}
|
||||
break; // only need to fetch once per txid
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Store for cache update on main thread
|
||||
newViewTxEntries[txid] = std::move(entry);
|
||||
} catch (const std::exception& e) {
|
||||
(void)e; // z_viewtransaction may not be available for all txids
|
||||
(void)e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,168 +896,18 @@ void App::refreshData()
|
||||
[](const TransactionInfo& a, const TransactionInfo& b) {
|
||||
return a.timestamp > b.timestamp;
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Phase 4: Peers (only when tab is active)
|
||||
// ================================================================
|
||||
std::vector<PeerInfo> peers;
|
||||
std::vector<BannedPeer> bannedPeers;
|
||||
bool peerOk = false;
|
||||
if (doPeers) {
|
||||
peerOk = true;
|
||||
try {
|
||||
json result = rpc_->call("getpeerinfo");
|
||||
for (const auto& peer : result) {
|
||||
PeerInfo info;
|
||||
if (peer.contains("id")) info.id = peer["id"].get<int>();
|
||||
if (peer.contains("addr")) info.addr = peer["addr"].get<std::string>();
|
||||
if (peer.contains("subver")) info.subver = peer["subver"].get<std::string>();
|
||||
if (peer.contains("services")) info.services = peer["services"].get<std::string>();
|
||||
if (peer.contains("version")) info.version = peer["version"].get<int>();
|
||||
if (peer.contains("conntime")) info.conntime = peer["conntime"].get<int64_t>();
|
||||
if (peer.contains("banscore")) info.banscore = peer["banscore"].get<int>();
|
||||
if (peer.contains("pingtime")) info.pingtime = peer["pingtime"].get<double>();
|
||||
if (peer.contains("bytessent")) info.bytessent = peer["bytessent"].get<int64_t>();
|
||||
if (peer.contains("bytesrecv")) info.bytesrecv = peer["bytesrecv"].get<int64_t>();
|
||||
if (peer.contains("startingheight")) info.startingheight = peer["startingheight"].get<int>();
|
||||
if (peer.contains("synced_headers")) info.synced_headers = peer["synced_headers"].get<int>();
|
||||
if (peer.contains("synced_blocks")) info.synced_blocks = peer["synced_blocks"].get<int>();
|
||||
if (peer.contains("inbound")) info.inbound = peer["inbound"].get<bool>();
|
||||
if (peer.contains("tls_cipher")) info.tls_cipher = peer["tls_cipher"].get<std::string>();
|
||||
if (peer.contains("tls_verified")) info.tls_verified = peer["tls_verified"].get<bool>();
|
||||
peers.push_back(info);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("getPeerInfo error: %s\n", e.what());
|
||||
}
|
||||
try {
|
||||
json result = rpc_->call("listbanned");
|
||||
for (const auto& ban : result) {
|
||||
BannedPeer info;
|
||||
if (ban.contains("address")) info.address = ban["address"].get<std::string>();
|
||||
if (ban.contains("banned_until")) info.banned_until = ban["banned_until"].get<int64_t>();
|
||||
bannedPeers.push_back(info);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("listBanned error: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Phase 5: Wallet encryption state
|
||||
// ================================================================
|
||||
json walletInfo;
|
||||
bool encryptOk = false;
|
||||
if (doEncrypt) {
|
||||
try {
|
||||
walletInfo = rpc_->call("getwalletinfo");
|
||||
encryptOk = true;
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Single main-thread callback — apply ALL results at once
|
||||
// ================================================================
|
||||
return [this, totalBal, blockInfo, balOk, blockOk,
|
||||
zAddrs = std::move(zAddrs), tAddrs = std::move(tAddrs), addrOk,
|
||||
txns = std::move(txns), txOk, currentBlocks,
|
||||
newViewTxEntries = std::move(newViewTxEntries),
|
||||
peers = std::move(peers), bannedPeers = std::move(bannedPeers), peerOk,
|
||||
walletInfo, encryptOk]() {
|
||||
// --- Balance ---
|
||||
try {
|
||||
if (balOk) {
|
||||
if (totalBal.contains("private"))
|
||||
state_.shielded_balance = std::stod(totalBal["private"].get<std::string>());
|
||||
if (totalBal.contains("transparent"))
|
||||
state_.transparent_balance = std::stod(totalBal["transparent"].get<std::string>());
|
||||
if (totalBal.contains("total"))
|
||||
state_.total_balance = std::stod(totalBal["total"].get<std::string>());
|
||||
state_.last_balance_update = std::time(nullptr);
|
||||
}
|
||||
if (blockOk) {
|
||||
if (blockInfo.contains("blocks"))
|
||||
state_.sync.blocks = blockInfo["blocks"].get<int>();
|
||||
if (blockInfo.contains("headers"))
|
||||
state_.sync.headers = blockInfo["headers"].get<int>();
|
||||
if (blockInfo.contains("verificationprogress"))
|
||||
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
|
||||
if (blockInfo.contains("longestchain")) {
|
||||
int lc = blockInfo["longestchain"].get<int>();
|
||||
// Don't regress to 0 — daemon returns 0 when peers haven't been polled
|
||||
if (lc > 0) state_.longestchain = lc;
|
||||
}
|
||||
// longestchain can lag behind blocks when peer data is stale
|
||||
if (state_.longestchain > 0 && state_.sync.blocks > state_.longestchain)
|
||||
state_.longestchain = state_.sync.blocks;
|
||||
// Use longestchain (actual network tip) for sync check when available,
|
||||
// since headers can be inflated by misbehaving peers.
|
||||
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 (blockInfo.contains("notarized"))
|
||||
state_.notarized = blockInfo["notarized"].get<int>();
|
||||
}
|
||||
// Auto-shield transparent funds if enabled
|
||||
if (balOk && settings_ && settings_->getAutoShield() &&
|
||||
state_.transparent_balance > 0.0001 && !state_.sync.syncing &&
|
||||
!auto_shield_pending_.exchange(true)) {
|
||||
std::string targetZAddr;
|
||||
for (const auto& addr : state_.addresses) {
|
||||
if (addr.isShielded()) {
|
||||
targetZAddr = addr.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!targetZAddr.empty() && rpc_) {
|
||||
DEBUG_LOGF("[AutoShield] Shielding %.8f DRGX to %s\n",
|
||||
state_.transparent_balance, targetZAddr.c_str());
|
||||
rpc_->z_shieldCoinbase("*", targetZAddr, 0.0001, 50,
|
||||
[this](const json& result) {
|
||||
if (result.contains("opid")) {
|
||||
DEBUG_LOGF("[AutoShield] Started: %s\n",
|
||||
result["opid"].get<std::string>().c_str());
|
||||
}
|
||||
auto_shield_pending_ = false;
|
||||
},
|
||||
[this](const std::string& err) {
|
||||
DEBUG_LOGF("[AutoShield] Error: %s\n", err.c_str());
|
||||
auto_shield_pending_ = false;
|
||||
});
|
||||
} else {
|
||||
auto_shield_pending_ = false;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("[refreshData] balance callback error: %s\n", e.what());
|
||||
}
|
||||
|
||||
// --- Addresses ---
|
||||
if (addrOk) {
|
||||
state_.z_addresses = std::move(zAddrs);
|
||||
state_.t_addresses = std::move(tAddrs);
|
||||
address_list_dirty_ = true;
|
||||
addresses_dirty_ = false;
|
||||
}
|
||||
|
||||
// --- Transactions ---
|
||||
if (txOk) {
|
||||
return [this, txns = std::move(txns), currentBlocks,
|
||||
newViewTxEntries = std::move(newViewTxEntries)]() {
|
||||
state_.transactions = std::move(txns);
|
||||
state_.last_tx_update = std::time(nullptr);
|
||||
last_tx_block_height_ = currentBlocks;
|
||||
|
||||
// Merge new z_viewtransaction results into the persistent cache
|
||||
for (auto& [txid, entry] : newViewTxEntries) {
|
||||
viewtx_cache_[txid] = std::move(entry);
|
||||
// Once cached, no need to keep in send_txids_
|
||||
send_txids_.erase(txid);
|
||||
}
|
||||
|
||||
// Rebuild confirmed transaction cache: txns with >= 10
|
||||
// confirmations are stable and won't change, so we keep
|
||||
// them for future cycles.
|
||||
confirmed_tx_cache_.clear();
|
||||
confirmed_tx_ids_.clear();
|
||||
for (const auto& tx : state_.transactions) {
|
||||
@@ -909,17 +917,30 @@ void App::refreshData()
|
||||
}
|
||||
}
|
||||
confirmed_cache_block_ = currentBlocks;
|
||||
tx_refresh_in_progress_.store(false, std::memory_order_release);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// --- Peers ---
|
||||
if (peerOk) {
|
||||
state_.peers = std::move(peers);
|
||||
state_.bannedPeers = std::move(bannedPeers);
|
||||
state_.last_peer_update = std::time(nullptr);
|
||||
}
|
||||
// ============================================================================
|
||||
// Encryption State: wallet info (one-shot on connect, lightweight)
|
||||
// ============================================================================
|
||||
|
||||
// --- Encryption state ---
|
||||
if (encryptOk) {
|
||||
void App::refreshEncryptionState()
|
||||
{
|
||||
if (!worker_ || !rpc_ || !state_.connected) return;
|
||||
|
||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||
json walletInfo;
|
||||
bool ok = false;
|
||||
try {
|
||||
walletInfo = rpc_->call("getwalletinfo");
|
||||
ok = true;
|
||||
} catch (...) {}
|
||||
|
||||
if (!ok) return nullptr;
|
||||
|
||||
return [this, walletInfo]() {
|
||||
try {
|
||||
if (walletInfo.contains("unlocked_until")) {
|
||||
state_.encrypted = true;
|
||||
@@ -933,194 +954,19 @@ void App::refreshData()
|
||||
}
|
||||
state_.encryption_state_known = true;
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
refresh_in_progress_.store(false, std::memory_order_release);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void App::refreshBalance()
|
||||
{
|
||||
if (!worker_ || !rpc_) return;
|
||||
|
||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||
// --- Worker thread: do blocking RPC ---
|
||||
json totalBal, blockInfo;
|
||||
bool balOk = false, blockOk = false;
|
||||
|
||||
try {
|
||||
totalBal = rpc_->call("z_gettotalbalance");
|
||||
balOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Balance error: %s\n", e.what());
|
||||
}
|
||||
|
||||
try {
|
||||
blockInfo = rpc_->call("getblockchaininfo");
|
||||
blockOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
||||
}
|
||||
|
||||
// --- Main thread: apply results to state_ ---
|
||||
return [this, totalBal, blockInfo, balOk, blockOk]() {
|
||||
try {
|
||||
if (balOk) {
|
||||
if (totalBal.contains("private"))
|
||||
state_.shielded_balance = std::stod(totalBal["private"].get<std::string>());
|
||||
if (totalBal.contains("transparent"))
|
||||
state_.transparent_balance = std::stod(totalBal["transparent"].get<std::string>());
|
||||
if (totalBal.contains("total"))
|
||||
state_.total_balance = std::stod(totalBal["total"].get<std::string>());
|
||||
state_.last_balance_update = std::time(nullptr);
|
||||
}
|
||||
if (blockOk) {
|
||||
if (blockInfo.contains("blocks"))
|
||||
state_.sync.blocks = blockInfo["blocks"].get<int>();
|
||||
if (blockInfo.contains("headers"))
|
||||
state_.sync.headers = blockInfo["headers"].get<int>();
|
||||
if (blockInfo.contains("verificationprogress"))
|
||||
state_.sync.verification_progress = blockInfo["verificationprogress"].get<double>();
|
||||
if (blockInfo.contains("longestchain")) {
|
||||
int lc = blockInfo["longestchain"].get<int>();
|
||||
// Don't regress to 0 — daemon returns 0 when peers haven't been polled
|
||||
if (lc > 0) state_.longestchain = lc;
|
||||
}
|
||||
// longestchain can lag behind blocks when peer data is stale
|
||||
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 (blockInfo.contains("notarized"))
|
||||
state_.notarized = blockInfo["notarized"].get<int>();
|
||||
}
|
||||
|
||||
// Auto-shield transparent funds if enabled
|
||||
if (balOk && settings_ && settings_->getAutoShield() &&
|
||||
state_.transparent_balance > 0.0001 && !state_.sync.syncing &&
|
||||
!auto_shield_pending_.exchange(true)) {
|
||||
// Find first shielded address as target
|
||||
std::string targetZAddr;
|
||||
for (const auto& addr : state_.addresses) {
|
||||
if (addr.isShielded()) {
|
||||
targetZAddr = addr.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!targetZAddr.empty() && rpc_) {
|
||||
DEBUG_LOGF("[AutoShield] Shielding %.8f DRGX to %s\n",
|
||||
state_.transparent_balance, targetZAddr.c_str());
|
||||
rpc_->z_shieldCoinbase("*", targetZAddr, 0.0001, 50,
|
||||
[this](const json& result) {
|
||||
if (result.contains("opid")) {
|
||||
DEBUG_LOGF("[AutoShield] Started: %s\n",
|
||||
result["opid"].get<std::string>().c_str());
|
||||
}
|
||||
auto_shield_pending_ = false;
|
||||
},
|
||||
[this](const std::string& err) {
|
||||
DEBUG_LOGF("[AutoShield] Error: %s\n", err.c_str());
|
||||
auto_shield_pending_ = false;
|
||||
});
|
||||
} else {
|
||||
auto_shield_pending_ = false;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("[refreshBalance] callback error: %s\n", e.what());
|
||||
}
|
||||
};
|
||||
});
|
||||
refreshCoreData();
|
||||
}
|
||||
|
||||
void App::refreshAddresses()
|
||||
{
|
||||
if (!worker_ || !rpc_) return;
|
||||
|
||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||
// --- Worker thread: fetch all address data with minimal RPC calls ---
|
||||
std::vector<AddressInfo> zAddrs, tAddrs;
|
||||
|
||||
// 1. Get z-addresses
|
||||
try {
|
||||
json zList = rpc_->call("z_listaddresses");
|
||||
for (const auto& addr : zList) {
|
||||
AddressInfo info;
|
||||
info.address = addr.get<std::string>();
|
||||
info.type = "shielded";
|
||||
zAddrs.push_back(info);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("z_listaddresses error: %s\n", e.what());
|
||||
}
|
||||
|
||||
// 2. P3: Use z_listUnspent (single call) instead of per-addr z_getbalance
|
||||
try {
|
||||
json unspent = rpc_->call("z_listunspent");
|
||||
std::map<std::string, double> zBalances;
|
||||
for (const auto& utxo : unspent) {
|
||||
if (utxo.contains("address") && utxo.contains("amount")) {
|
||||
zBalances[utxo["address"].get<std::string>()] += utxo["amount"].get<double>();
|
||||
}
|
||||
}
|
||||
for (auto& info : zAddrs) {
|
||||
auto it = zBalances.find(info.address);
|
||||
if (it != zBalances.end()) {
|
||||
info.balance = it->second;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
// Fallback: z_listUnspent might not be available — use batched z_getbalance
|
||||
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
|
||||
for (auto& info : zAddrs) {
|
||||
try {
|
||||
json bal = rpc_->call("z_getbalance", json::array({info.address}));
|
||||
if (!bal.is_null()) info.balance = bal.get<double>();
|
||||
} catch (...) {}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Get t-addresses
|
||||
try {
|
||||
json tList = rpc_->call("getaddressesbyaccount", json::array({""}));
|
||||
for (const auto& addr : tList) {
|
||||
AddressInfo info;
|
||||
info.address = addr.get<std::string>();
|
||||
info.type = "transparent";
|
||||
tAddrs.push_back(info);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("getaddressesbyaccount error: %s\n", e.what());
|
||||
}
|
||||
|
||||
// 4. Get unspent for t-address balances
|
||||
try {
|
||||
json utxos = rpc_->call("listunspent");
|
||||
std::map<std::string, double> tBalances;
|
||||
for (const auto& utxo : utxos) {
|
||||
tBalances[utxo["address"].get<std::string>()] += utxo["amount"].get<double>();
|
||||
}
|
||||
for (auto& info : tAddrs) {
|
||||
auto it = tBalances.find(info.address);
|
||||
if (it != tBalances.end()) info.balance = it->second;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
||||
}
|
||||
|
||||
// --- Main thread: apply results and rebuild address list once ---
|
||||
return [this, zAddrs = std::move(zAddrs), tAddrs = std::move(tAddrs)]() {
|
||||
state_.z_addresses = std::move(zAddrs);
|
||||
state_.t_addresses = std::move(tAddrs);
|
||||
// P8: single rebuild via dirty flag (drains in update())
|
||||
address_list_dirty_ = true;
|
||||
// Addresses fetched successfully — clear the demand flag
|
||||
addresses_dirty_ = false;
|
||||
};
|
||||
});
|
||||
addresses_dirty_ = true;
|
||||
refreshAddressData();
|
||||
}
|
||||
|
||||
void App::refreshMiningInfo()
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
||||
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
||||
|
||||
#define DRAGONX_VERSION "1.1.1"
|
||||
#define DRAGONX_VERSION "1.1.2"
|
||||
#define DRAGONX_VERSION_MAJOR 1
|
||||
#define DRAGONX_VERSION_MINOR 1
|
||||
#define DRAGONX_VERSION_PATCH 1
|
||||
#define DRAGONX_VERSION_PATCH 2
|
||||
|
||||
#define DRAGONX_APP_NAME "ObsidianDragon"
|
||||
#define DRAGONX_ORG_NAME "Hush"
|
||||
|
||||
Reference in New Issue
Block a user