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()
|
endif()
|
||||||
|
|
||||||
project(ObsidianDragon
|
project(ObsidianDragon
|
||||||
VERSION 1.1.1
|
VERSION 1.1.2
|
||||||
LANGUAGES C CXX
|
LANGUAGES C CXX
|
||||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||||
)
|
)
|
||||||
|
|||||||
46
src/app.cpp
46
src/app.cpp
@@ -343,7 +343,10 @@ void App::update()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update timers
|
// 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;
|
price_timer_ += io.DeltaTime;
|
||||||
fast_refresh_timer_ += io.DeltaTime;
|
fast_refresh_timer_ += io.DeltaTime;
|
||||||
tx_age_timer_ += io.DeltaTime;
|
tx_age_timer_ += io.DeltaTime;
|
||||||
@@ -591,19 +594,36 @@ void App::update()
|
|||||||
transactions_dirty_ = true;
|
transactions_dirty_ = true;
|
||||||
addresses_dirty_ = true;
|
addresses_dirty_ = true;
|
||||||
last_tx_block_height_ = -1;
|
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.
|
// Skip when wallet is locked — same reason as above.
|
||||||
if (refresh_timer_ >= REFRESH_INTERVAL) {
|
|
||||||
refresh_timer_ = 0.0f;
|
|
||||||
if (state_.connected && !state_.isLocked()) {
|
if (state_.connected && !state_.isLocked()) {
|
||||||
refreshData();
|
if (core_timer_ >= active_core_interval_) {
|
||||||
} else if (!connection_in_progress_ &&
|
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) {
|
wizard_phase_ == WizardPhase::None) {
|
||||||
tryConnect();
|
tryConnect();
|
||||||
}
|
}
|
||||||
@@ -619,7 +639,7 @@ void App::update()
|
|||||||
|
|
||||||
// Keyboard shortcut: Ctrl+, to open Settings page
|
// Keyboard shortcut: Ctrl+, to open Settings page
|
||||||
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Comma)) {
|
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Comma)) {
|
||||||
current_page_ = ui::NavPage::Settings;
|
setCurrentPage(ui::NavPage::Settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyboard shortcut: Ctrl+Left/Right to cycle themes
|
// Keyboard shortcut: Ctrl+Left/Right to cycle themes
|
||||||
@@ -1886,7 +1906,11 @@ void App::renderAntivirusHelpDialog()
|
|||||||
|
|
||||||
void App::refreshNow()
|
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
|
transactions_dirty_ = true; // Force transaction list update
|
||||||
addresses_dirty_ = true; // Force address/balance update
|
addresses_dirty_ = true; // Force address/balance update
|
||||||
last_tx_block_height_ = -1; // Reset tx cache
|
last_tx_block_height_ = -1; // Reset tx cache
|
||||||
@@ -1909,7 +1933,7 @@ void App::handlePaymentURI(const std::string& uri)
|
|||||||
pending_label_ = payment.label;
|
pending_label_ = payment.label;
|
||||||
|
|
||||||
// Switch to Send page
|
// Switch to Send page
|
||||||
current_page_ = ui::NavPage::Send;
|
setCurrentPage(ui::NavPage::Send);
|
||||||
|
|
||||||
// Notify user
|
// Notify user
|
||||||
std::string msg = "Payment request loaded";
|
std::string msg = "Payment request loaded";
|
||||||
@@ -1936,7 +1960,7 @@ void App::setCurrentTab(int tab) {
|
|||||||
ui::NavPage::Settings, // 9 = Settings
|
ui::NavPage::Settings, // 9 = Settings
|
||||||
};
|
};
|
||||||
if (tab >= 0 && tab < static_cast<int>(sizeof(kTabMap)/sizeof(kTabMap[0])))
|
if (tab >= 0 && tab < static_cast<int>(sizeof(kTabMap)/sizeof(kTabMap[0])))
|
||||||
current_page_ = kTabMap[tab];
|
setCurrentPage(kTabMap[tab]);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool App::startEmbeddedDaemon()
|
bool App::startEmbeddedDaemon()
|
||||||
|
|||||||
56
src/app.h
56
src/app.h
@@ -221,13 +221,19 @@ public:
|
|||||||
void refreshPeerInfo();
|
void refreshPeerInfo();
|
||||||
void refreshMarketData();
|
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
|
// UI navigation
|
||||||
void setCurrentPage(ui::NavPage page) {
|
void setCurrentPage(ui::NavPage page);
|
||||||
if (page != current_page_) {
|
|
||||||
current_page_ = page;
|
|
||||||
if (page == ui::NavPage::Peers) refreshPeerInfo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ui::NavPage getCurrentPage() const { return current_page_; }
|
ui::NavPage getCurrentPage() const { return current_page_; }
|
||||||
|
|
||||||
// Dialog triggers (used by settings page to open modal dialogs)
|
// Dialog triggers (used by settings page to open modal dialogs)
|
||||||
@@ -362,7 +368,6 @@ private:
|
|||||||
// Shutdown state
|
// Shutdown state
|
||||||
std::atomic<bool> shutting_down_{false};
|
std::atomic<bool> shutting_down_{false};
|
||||||
std::atomic<bool> shutdown_complete_{false};
|
std::atomic<bool> shutdown_complete_{false};
|
||||||
std::atomic<bool> refresh_in_progress_{false};
|
|
||||||
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
||||||
std::string shutdown_status_;
|
std::string shutdown_status_;
|
||||||
std::thread shutdown_thread_;
|
std::thread shutdown_thread_;
|
||||||
@@ -438,16 +443,33 @@ private:
|
|||||||
std::string pending_memo_;
|
std::string pending_memo_;
|
||||||
std::string pending_label_;
|
std::string pending_label_;
|
||||||
|
|
||||||
// Timers (in seconds since last update)
|
// Per-category timers (in seconds since last refresh)
|
||||||
float refresh_timer_ = 0.0f;
|
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 price_timer_ = 0.0f;
|
||||||
float fast_refresh_timer_ = 0.0f; // For mining stats
|
float fast_refresh_timer_ = 0.0f; // For mining stats
|
||||||
|
|
||||||
// Refresh intervals (seconds)
|
// Default refresh intervals (seconds)
|
||||||
static constexpr float REFRESH_INTERVAL = 5.0f;
|
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 PRICE_INTERVAL = 60.0f;
|
||||||
static constexpr float FAST_REFRESH_INTERVAL = 1.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)
|
// Mining refresh guard (prevents worker queue pileup)
|
||||||
std::atomic<bool> mining_refresh_in_progress_{false};
|
std::atomic<bool> mining_refresh_in_progress_{false};
|
||||||
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
|
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
|
||||||
@@ -604,11 +626,17 @@ private:
|
|||||||
void applyDefaultBanlist();
|
void applyDefaultBanlist();
|
||||||
|
|
||||||
// Private methods - data refresh
|
// Private methods - data refresh
|
||||||
void refreshData();
|
void refreshData(); // Orchestrator: dispatches per-category refreshes
|
||||||
void refreshBalance();
|
void refreshCoreData(); // Balance + blockchain info (can use fast_worker_)
|
||||||
void refreshAddresses();
|
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 refreshPrice();
|
||||||
void refreshWalletEncryptionState();
|
void refreshWalletEncryptionState();
|
||||||
|
void applyRefreshPolicy(ui::NavPage page);
|
||||||
|
bool shouldRefreshTransactions() const;
|
||||||
void checkAutoLock();
|
void checkAutoLock();
|
||||||
void checkIdleMining();
|
void checkIdleMining();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ void App::tryConnect()
|
|||||||
if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) {
|
if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) {
|
||||||
connection_status_ = "Waiting for daemon config...";
|
connection_status_ = "Waiting for daemon config...";
|
||||||
VERBOSE_LOGF("[connect #%d] External daemon detected on port, waiting for config file to appear\n", connect_attempt);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +89,11 @@ void App::tryConnect()
|
|||||||
if (startEmbeddedDaemon()) {
|
if (startEmbeddedDaemon()) {
|
||||||
// Will retry connection after daemon starts
|
// Will retry connection after daemon starts
|
||||||
VERBOSE_LOGF("[connect #%d] Embedded daemon starting, will retry connection...\n", connect_attempt);
|
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()) {
|
} else if (embedded_daemon_ && embedded_daemon_->externalDaemonDetected()) {
|
||||||
connection_status_ = "Waiting for daemon config...";
|
connection_status_ = "Waiting for daemon config...";
|
||||||
VERBOSE_LOGF("[connect #%d] External daemon detected but no config yet, will retry...\n", connect_attempt);
|
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 {
|
} else {
|
||||||
VERBOSE_LOGF("[connect #%d] startEmbeddedDaemon() failed — lastError: %s, binary: %s\n",
|
VERBOSE_LOGF("[connect #%d] startEmbeddedDaemon() failed — lastError: %s, binary: %s\n",
|
||||||
connect_attempt,
|
connect_attempt,
|
||||||
@@ -208,13 +208,13 @@ void App::tryConnect()
|
|||||||
connection_status_ = "Waiting for dragonxd to start...";
|
connection_status_ = "Waiting for dragonxd to start...";
|
||||||
VERBOSE_LOGF("[connect #%d] RPC connection failed — daemon still starting, will retry...\n", attempt);
|
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
|
// Fast retry: force the refresh timer to fire on the next cycle
|
||||||
// instead of waiting the full 5-second REFRESH_INTERVAL.
|
// instead of waiting the full 5-second interval.
|
||||||
refresh_timer_ = REFRESH_INTERVAL - 1.0f;
|
core_timer_ = CORE_INTERVAL_DEFAULT - 1.0f;
|
||||||
} else if (externalDetected) {
|
} else if (externalDetected) {
|
||||||
state_.connected = false;
|
state_.connected = false;
|
||||||
connection_status_ = "Connecting to daemon...";
|
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);
|
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 ||
|
} else if (connectErr.find("Loading") != std::string::npos ||
|
||||||
connectErr.find("Verifying") != std::string::npos ||
|
connectErr.find("Verifying") != std::string::npos ||
|
||||||
connectErr.find("Activating") != std::string::npos ||
|
connectErr.find("Activating") != std::string::npos ||
|
||||||
@@ -225,7 +225,7 @@ void App::tryConnect()
|
|||||||
state_.connected = false;
|
state_.connected = false;
|
||||||
connection_status_ = connectErr;
|
connection_status_ = connectErr;
|
||||||
VERBOSE_LOGF("[connect #%d] Daemon warmup: %s\n", attempt, connectErr.c_str());
|
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 {
|
} else {
|
||||||
onDisconnected("Connection failed");
|
onDisconnected("Connection failed");
|
||||||
VERBOSE_LOGF("[connect #%d] RPC connection failed — no daemon starting, no external detected\n", attempt);
|
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()
|
void App::refreshData()
|
||||||
{
|
{
|
||||||
if (!state_.connected || !rpc_ || !worker_) return;
|
if (!state_.connected || !rpc_ || !worker_) return;
|
||||||
|
|
||||||
// Prevent overlapping refreshes — skip if one is still running
|
// Dispatch each category independently — results trickle into the UI
|
||||||
if (refresh_in_progress_.exchange(true)) return;
|
// as each completes, rather than waiting for the slowest phase.
|
||||||
|
refreshCoreData();
|
||||||
|
|
||||||
// Capture decision flags on the main thread before posting to worker.
|
if (addresses_dirty_)
|
||||||
const bool doAddresses = addresses_dirty_;
|
refreshAddressData();
|
||||||
const bool doPeers = (current_page_ == ui::NavPage::Peers);
|
|
||||||
const bool doEncrypt = !encryption_state_prefetched_;
|
|
||||||
if (encryption_state_prefetched_) encryption_state_prefetched_ = false;
|
|
||||||
|
|
||||||
// P4a: Refresh transactions when new blocks arrive, when explicitly
|
if (shouldRefreshTransactions())
|
||||||
// dirtied (e.g. after a send), or when the max-age timer expires
|
refreshTransactionData();
|
||||||
// (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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot z-addresses for transaction fetch (needed on worker thread)
|
if (current_page_ == ui::NavPage::Peers)
|
||||||
std::vector<std::string> txZAddrs;
|
refreshPeerInfo();
|
||||||
if (doTransactions) {
|
|
||||||
for (const auto& za : state_.z_addresses) {
|
if (!encryption_state_prefetched_) {
|
||||||
if (!za.address.empty()) txZAddrs.push_back(za.address);
|
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
|
// Core Data: balance + blockchain info (~50-100ms, 2 RPC calls)
|
||||||
// another z_viewtransaction RPC call.
|
// Uses fast_worker_ when on Overview tab for lower latency.
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// P4c: Snapshot the confirmed transaction cache for the worker thread.
|
void App::refreshCoreData()
|
||||||
// These are deeply-confirmed txns we can reuse without re-fetching.
|
{
|
||||||
std::vector<TransactionInfo> cachedConfirmedTxns;
|
if (!state_.connected) return;
|
||||||
std::unordered_set<std::string> cachedConfirmedIds;
|
|
||||||
if (doTransactions && !confirmed_tx_cache_.empty()) {
|
|
||||||
cachedConfirmedTxns = confirmed_tx_cache_;
|
|
||||||
cachedConfirmedIds = confirmed_tx_ids_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot viewtx cache for the worker thread
|
// Use fast-lane on Overview for snappier balance updates
|
||||||
auto viewtxCacheSnap = viewtx_cache_;
|
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
|
if (core_refresh_in_progress_.exchange(true)) return;
|
||||||
auto sendTxidsSnap = send_txids_;
|
|
||||||
|
|
||||||
// Single consolidated worker task — all RPC calls happen back-to-back
|
w->post([this, rpc]() -> rpc::RPCWorker::MainCb {
|
||||||
// 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
|
|
||||||
// ================================================================
|
|
||||||
json totalBal, blockInfo;
|
json totalBal, blockInfo;
|
||||||
bool balOk = false, blockOk = false;
|
bool balOk = false, blockOk = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
totalBal = rpc_->call("z_gettotalbalance");
|
totalBal = rpc->call("z_gettotalbalance");
|
||||||
balOk = true;
|
balOk = true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("Balance error: %s\n", e.what());
|
DEBUG_LOGF("Balance error: %s\n", e.what());
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
blockInfo = rpc_->call("getblockchaininfo");
|
blockInfo = rpc->call("getblockchaininfo");
|
||||||
blockOk = true;
|
blockOk = true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
return [this, totalBal, blockInfo, balOk, blockOk]() {
|
||||||
// Phase 2: Addresses (only when dirtied)
|
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;
|
std::vector<AddressInfo> zAddrs, tAddrs;
|
||||||
bool addrOk = false;
|
|
||||||
if (doAddresses) {
|
|
||||||
addrOk = true;
|
|
||||||
// z-addresses
|
// z-addresses
|
||||||
try {
|
try {
|
||||||
json zList = rpc_->call("z_listaddresses");
|
json zList = rpc_->call("z_listaddresses");
|
||||||
@@ -559,16 +684,59 @@ void App::refreshData()
|
|||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
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::vector<TransactionInfo> txns;
|
||||||
std::unordered_map<std::string, ViewTxCacheEntry> newViewTxEntries;
|
std::unordered_map<std::string, ViewTxCacheEntry> newViewTxEntries;
|
||||||
bool txOk = false;
|
|
||||||
if (doTransactions) {
|
|
||||||
txOk = true;
|
|
||||||
std::set<std::string> knownTxids;
|
std::set<std::string> knownTxids;
|
||||||
|
|
||||||
// Phase 3a: transparent transactions
|
// Phase 3a: transparent transactions
|
||||||
@@ -624,16 +792,12 @@ void App::refreshData()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include txids from completed z_sendmany operations so that
|
// Include txids from completed z_sendmany operations
|
||||||
// pure shielded sends (which don't appear in listtransactions
|
|
||||||
// or z_listreceivedbyaddress) are discoverable.
|
|
||||||
for (const auto& txid : sendTxidsSnap) {
|
for (const auto& txid : sendTxidsSnap) {
|
||||||
knownTxids.insert(txid);
|
knownTxids.insert(txid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3c: detect shielded sends via z_viewtransaction
|
// 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;
|
int viewTxCount = 0;
|
||||||
|
|
||||||
auto applyViewTxEntry = [&](const std::string& txid,
|
auto applyViewTxEntry = [&](const std::string& txid,
|
||||||
@@ -654,7 +818,6 @@ void App::refreshData()
|
|||||||
info.amount = -out.value;
|
info.amount = -out.value;
|
||||||
info.memo = out.memo;
|
info.memo = out.memo;
|
||||||
info.from_address = entry.from_address;
|
info.from_address = entry.from_address;
|
||||||
// Copy confirmations/timestamp from an existing entry for this txid
|
|
||||||
for (const auto& existing : txns) {
|
for (const auto& existing : txns) {
|
||||||
if (existing.txid == txid) {
|
if (existing.txid == txid) {
|
||||||
info.confirmations = existing.confirmations;
|
info.confirmations = existing.confirmations;
|
||||||
@@ -669,7 +832,6 @@ void App::refreshData()
|
|||||||
for (const std::string& txid : knownTxids) {
|
for (const std::string& txid : knownTxids) {
|
||||||
if (fullyEnriched.count(txid)) continue;
|
if (fullyEnriched.count(txid)) continue;
|
||||||
|
|
||||||
// Check viewtx cache first — avoid RPC call
|
|
||||||
auto cit = viewtxCacheSnap.find(txid);
|
auto cit = viewtxCacheSnap.find(txid);
|
||||||
if (cit != viewtxCacheSnap.end()) {
|
if (cit != viewtxCacheSnap.end()) {
|
||||||
applyViewTxEntry(txid, cit->second);
|
applyViewTxEntry(txid, cit->second);
|
||||||
@@ -683,7 +845,6 @@ void App::refreshData()
|
|||||||
json vtx = rpc_->call("z_viewtransaction", json::array({txid}));
|
json vtx = rpc_->call("z_viewtransaction", json::array({txid}));
|
||||||
if (vtx.is_null() || !vtx.is_object()) continue;
|
if (vtx.is_null() || !vtx.is_object()) continue;
|
||||||
|
|
||||||
// Build cache entry from RPC result
|
|
||||||
ViewTxCacheEntry entry;
|
ViewTxCacheEntry entry;
|
||||||
if (vtx.contains("spends") && vtx["spends"].is_array()) {
|
if (vtx.contains("spends") && vtx["spends"].is_array()) {
|
||||||
for (const auto& spend : vtx["spends"]) {
|
for (const auto& spend : vtx["spends"]) {
|
||||||
@@ -710,10 +871,8 @@ void App::refreshData()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply to current txns list
|
|
||||||
applyViewTxEntry(txid, entry);
|
applyViewTxEntry(txid, entry);
|
||||||
|
|
||||||
// Fetch timestamp if missing for any new txns we just added
|
|
||||||
for (auto& info : txns) {
|
for (auto& info : txns) {
|
||||||
if (info.txid == txid && info.timestamp == 0) {
|
if (info.txid == txid && info.timestamp == 0) {
|
||||||
try {
|
try {
|
||||||
@@ -723,14 +882,13 @@ void App::refreshData()
|
|||||||
if (!rawtx.is_null() && rawtx.contains("confirmations"))
|
if (!rawtx.is_null() && rawtx.contains("confirmations"))
|
||||||
info.confirmations = rawtx["confirmations"].get<int>();
|
info.confirmations = rawtx["confirmations"].get<int>();
|
||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
break; // only need to fetch once per txid
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store for cache update on main thread
|
|
||||||
newViewTxEntries[txid] = std::move(entry);
|
newViewTxEntries[txid] = std::move(entry);
|
||||||
} catch (const std::exception& e) {
|
} 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) {
|
[](const TransactionInfo& a, const TransactionInfo& b) {
|
||||||
return a.timestamp > b.timestamp;
|
return a.timestamp > b.timestamp;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
return [this, txns = std::move(txns), currentBlocks,
|
||||||
// Phase 4: Peers (only when tab is active)
|
newViewTxEntries = std::move(newViewTxEntries)]() {
|
||||||
// ================================================================
|
|
||||||
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) {
|
|
||||||
state_.transactions = std::move(txns);
|
state_.transactions = std::move(txns);
|
||||||
state_.last_tx_update = std::time(nullptr);
|
state_.last_tx_update = std::time(nullptr);
|
||||||
last_tx_block_height_ = currentBlocks;
|
last_tx_block_height_ = currentBlocks;
|
||||||
|
|
||||||
// Merge new z_viewtransaction results into the persistent cache
|
|
||||||
for (auto& [txid, entry] : newViewTxEntries) {
|
for (auto& [txid, entry] : newViewTxEntries) {
|
||||||
viewtx_cache_[txid] = std::move(entry);
|
viewtx_cache_[txid] = std::move(entry);
|
||||||
// Once cached, no need to keep in send_txids_
|
|
||||||
send_txids_.erase(txid);
|
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_cache_.clear();
|
||||||
confirmed_tx_ids_.clear();
|
confirmed_tx_ids_.clear();
|
||||||
for (const auto& tx : state_.transactions) {
|
for (const auto& tx : state_.transactions) {
|
||||||
@@ -909,17 +917,30 @@ void App::refreshData()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
confirmed_cache_block_ = currentBlocks;
|
confirmed_cache_block_ = currentBlocks;
|
||||||
|
tx_refresh_in_progress_.store(false, std::memory_order_release);
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Peers ---
|
// ============================================================================
|
||||||
if (peerOk) {
|
// Encryption State: wallet info (one-shot on connect, lightweight)
|
||||||
state_.peers = std::move(peers);
|
// ============================================================================
|
||||||
state_.bannedPeers = std::move(bannedPeers);
|
|
||||||
state_.last_peer_update = std::time(nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Encryption state ---
|
void App::refreshEncryptionState()
|
||||||
if (encryptOk) {
|
{
|
||||||
|
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 {
|
try {
|
||||||
if (walletInfo.contains("unlocked_until")) {
|
if (walletInfo.contains("unlocked_until")) {
|
||||||
state_.encrypted = true;
|
state_.encrypted = true;
|
||||||
@@ -933,194 +954,19 @@ void App::refreshData()
|
|||||||
}
|
}
|
||||||
state_.encryption_state_known = true;
|
state_.encryption_state_known = true;
|
||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
}
|
|
||||||
|
|
||||||
refresh_in_progress_.store(false, std::memory_order_release);
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::refreshBalance()
|
void App::refreshBalance()
|
||||||
{
|
{
|
||||||
if (!worker_ || !rpc_) return;
|
refreshCoreData();
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::refreshAddresses()
|
void App::refreshAddresses()
|
||||||
{
|
{
|
||||||
if (!worker_ || !rpc_) return;
|
addresses_dirty_ = true;
|
||||||
|
refreshAddressData();
|
||||||
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;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::refreshMiningInfo()
|
void App::refreshMiningInfo()
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
// !! 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 ...)
|
// !! 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_MAJOR 1
|
||||||
#define DRAGONX_VERSION_MINOR 1
|
#define DRAGONX_VERSION_MINOR 1
|
||||||
#define DRAGONX_VERSION_PATCH 1
|
#define DRAGONX_VERSION_PATCH 2
|
||||||
|
|
||||||
#define DRAGONX_APP_NAME "ObsidianDragon"
|
#define DRAGONX_APP_NAME "ObsidianDragon"
|
||||||
#define DRAGONX_ORG_NAME "Hush"
|
#define DRAGONX_ORG_NAME "Hush"
|
||||||
|
|||||||
Reference in New Issue
Block a user