From bbf53a130c77a68baf4b1913858d19921e629451 Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 4 Apr 2026 13:05:00 -0500 Subject: [PATCH] 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. --- CMakeLists.txt | 2 +- src/app.cpp | 50 ++- src/app.h | 56 ++- src/app_network.cpp | 1006 ++++++++++++++++++------------------------ src/config/version.h | 4 +- 5 files changed, 508 insertions(+), 610 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 790af08..b574d05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ if(APPLE) endif() project(ObsidianDragon - VERSION 1.1.1 + VERSION 1.1.2 LANGUAGES C CXX DESCRIPTION "DragonX Cryptocurrency Wallet" ) diff --git a/src/app.cpp b/src/app.cpp index e2c785a..32329bf 100644 --- a/src/app.cpp +++ b/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,20 +594,37 @@ 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_ && - wizard_phase_ == WizardPhase::None) { + if (state_.connected && !state_.isLocked()) { + 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(sizeof(kTabMap)/sizeof(kTabMap[0]))) - current_page_ = kTabMap[tab]; + setCurrentPage(kTabMap[tab]); } bool App::startEmbeddedDaemon() diff --git a/src/app.h b/src/app.h index f3b78e3..6d0e9b3 100644 --- a/src/app.h +++ b/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 shutting_down_{false}; std::atomic shutdown_complete_{false}; - std::atomic 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 core_refresh_in_progress_{false}; + std::atomic address_refresh_in_progress_{false}; + std::atomic tx_refresh_in_progress_{false}; + // Mining refresh guard (prevents worker queue pileup) std::atomic 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(); }; diff --git a/src/app_network.cpp b/src/app_network.cpp index 6dddf64..cb4d37a 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -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,576 +394,155 @@ 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; - - // 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; - // 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; + // Dispatch each category independently — results trickle into the UI + // as each completes, rather than waiting for the slowest phase. + refreshCoreData(); + + if (addresses_dirty_) + refreshAddressData(); + + if (shouldRefreshTransactions()) + refreshTransactionData(); + + if (current_page_ == ui::NavPage::Peers) + refreshPeerInfo(); + + if (!encryption_state_prefetched_) { + encryption_state_prefetched_ = false; + refreshEncryptionState(); } - - // Snapshot z-addresses for transaction fetch (needed on worker thread) - std::vector txZAddrs; - if (doTransactions) { - for (const auto& za : state_.z_addresses) { - if (!za.address.empty()) txZAddrs.push_back(za.address); - } - } - - // 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 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. - // These are deeply-confirmed txns we can reuse without re-fetching. - std::vector cachedConfirmedTxns; - std::unordered_set cachedConfirmedIds; - if (doTransactions && !confirmed_tx_cache_.empty()) { - cachedConfirmedTxns = confirmed_tx_cache_; - cachedConfirmedIds = confirmed_tx_ids_; - } - - // Snapshot viewtx cache for the worker thread - auto viewtxCacheSnap = viewtx_cache_; - - // Snapshot send txids so the worker can include them in enrichment - auto sendTxidsSnap = send_txids_; - - // 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 - // ================================================================ - 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()); - } - - // ================================================================ - // Phase 2: Addresses (only when dirtied) - // ================================================================ - std::vector zAddrs, tAddrs; - bool addrOk = false; - if (doAddresses) { - addrOk = true; - // z-addresses - try { - json zList = rpc_->call("z_listaddresses"); - for (const auto& addr : zList) { - AddressInfo info; - info.address = addr.get(); - info.type = "shielded"; - zAddrs.push_back(info); - } - } catch (const std::exception& e) { - DEBUG_LOGF("z_listaddresses error: %s\n", e.what()); - } - // z-balances via z_listunspent (single call) - try { - json unspent = rpc_->call("z_listunspent"); - std::map zBalances; - for (const auto& utxo : unspent) { - if (utxo.contains("address") && utxo.contains("amount")) { - zBalances[utxo["address"].get()] += utxo["amount"].get(); - } - } - for (auto& info : zAddrs) { - auto it = zBalances.find(info.address); - if (it != zBalances.end()) info.balance = it->second; - } - } catch (const std::exception& e) { - 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(); - } catch (...) {} - } - } - // t-addresses - try { - json tList = rpc_->call("getaddressesbyaccount", json::array({""})); - for (const auto& addr : tList) { - AddressInfo info; - info.address = addr.get(); - info.type = "transparent"; - tAddrs.push_back(info); - } - } catch (const std::exception& e) { - DEBUG_LOGF("getaddressesbyaccount error: %s\n", e.what()); - } - // t-balances via listunspent - try { - json utxos = rpc_->call("listunspent"); - std::map tBalances; - for (const auto& utxo : utxos) { - tBalances[utxo["address"].get()] += utxo["amount"].get(); - } - 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()); - } - } - - // ================================================================ - // Phase 3: Transactions (only when new blocks) - // ================================================================ - std::vector txns; - std::unordered_map newViewTxEntries; - bool txOk = false; - if (doTransactions) { - txOk = true; - std::set knownTxids; - - // Phase 3a: transparent transactions - try { - json result = rpc_->call("listtransactions", json::array({"", 9999})); - for (const auto& tx : result) { - TransactionInfo info; - if (tx.contains("txid")) info.txid = tx["txid"].get(); - if (tx.contains("category")) info.type = tx["category"].get(); - if (tx.contains("amount")) info.amount = tx["amount"].get(); - if (tx.contains("time")) info.timestamp = tx["time"].get(); - else if (tx.contains("timereceived")) info.timestamp = tx["timereceived"].get(); - if (tx.contains("confirmations")) info.confirmations = tx["confirmations"].get(); - if (tx.contains("address")) info.address = tx["address"].get(); - knownTxids.insert(info.txid); - txns.push_back(info); - } - } catch (const std::exception& e) { - DEBUG_LOGF("listtransactions error: %s\n", e.what()); - } - - // Phase 3b: shielded receives - for (const auto& addr : txZAddrs) { - try { - json zresult = rpc_->call("z_listreceivedbyaddress", json::array({addr, 0})); - if (zresult.is_null() || !zresult.is_array()) continue; - for (const auto& note : zresult) { - std::string txid; - if (note.contains("txid")) txid = note["txid"].get(); - if (txid.empty()) continue; - if (note.contains("change") && note["change"].get()) continue; - bool dominated = false; - for (const auto& existing : txns) { - if (existing.txid == txid && existing.type == "receive") { - dominated = true; break; - } - } - if (dominated) continue; - TransactionInfo info; - info.txid = txid; - info.type = "receive"; - info.address = addr; - if (note.contains("amount")) info.amount = note["amount"].get(); - if (note.contains("confirmations")) info.confirmations = note["confirmations"].get(); - if (note.contains("time")) info.timestamp = note["time"].get(); - if (note.contains("memoStr")) info.memo = note["memoStr"].get(); - knownTxids.insert(txid); - txns.push_back(info); - } - } catch (const std::exception& e) { - DEBUG_LOGF("z_listreceivedbyaddress error for %s: %s\n", - addr.substr(0, 12).c_str(), e.what()); - } - } - - // Include txids from completed z_sendmany operations so that - // pure shielded sends (which don't appear in listtransactions - // or z_listreceivedbyaddress) are discoverable. - 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, - const ViewTxCacheEntry& entry) { - for (const auto& out : entry.outgoing_outputs) { - bool alreadyTracked = false; - for (const auto& existing : txns) { - if (existing.txid == txid && existing.type == "send" - && std::abs(existing.amount + out.value) < 0.00000001) { - alreadyTracked = true; break; - } - } - if (alreadyTracked) continue; - TransactionInfo info; - info.txid = txid; - info.type = "send"; - info.address = out.address; - info.amount = -out.value; - info.memo = out.memo; - info.from_address = entry.from_address; - // Copy confirmations/timestamp from an existing entry for this txid - for (const auto& existing : txns) { - if (existing.txid == txid) { - info.confirmations = existing.confirmations; - info.timestamp = existing.timestamp; - break; - } - } - txns.push_back(info); - } - }; - - 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); - continue; - } - - if (viewTxCount >= MAX_VIEWTX_PER_CYCLE) break; - ++viewTxCount; - - try { - 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"]) { - if (spend.contains("address")) { - entry.from_address = spend["address"].get(); - break; - } - } - } - if (vtx.contains("outputs") && vtx["outputs"].is_array()) { - for (const auto& output : vtx["outputs"]) { - bool outgoing = false; - if (output.contains("outgoing")) - outgoing = output["outgoing"].get(); - if (!outgoing) continue; - ViewTxCacheEntry::Output out; - if (output.contains("address")) - out.address = output["address"].get(); - if (output.contains("value")) - out.value = output["value"].get(); - if (output.contains("memoStr")) - out.memo = output["memoStr"].get(); - entry.outgoing_outputs.push_back(std::move(out)); - } - } - - // 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 { - json rawtx = rpc_->call("gettransaction", json::array({txid})); - if (!rawtx.is_null() && rawtx.contains("time")) - info.timestamp = rawtx["time"].get(); - if (!rawtx.is_null() && rawtx.contains("confirmations")) - info.confirmations = rawtx["confirmations"].get(); - } catch (...) {} - break; // only need to fetch once per txid - } - } - - // 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 - } - } - - std::sort(txns.begin(), txns.end(), - [](const TransactionInfo& a, const TransactionInfo& b) { - return a.timestamp > b.timestamp; - }); - } - - // ================================================================ - // Phase 4: Peers (only when tab is active) - // ================================================================ - std::vector peers; - std::vector 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(); - if (peer.contains("addr")) info.addr = peer["addr"].get(); - if (peer.contains("subver")) info.subver = peer["subver"].get(); - if (peer.contains("services")) info.services = peer["services"].get(); - if (peer.contains("version")) info.version = peer["version"].get(); - if (peer.contains("conntime")) info.conntime = peer["conntime"].get(); - if (peer.contains("banscore")) info.banscore = peer["banscore"].get(); - if (peer.contains("pingtime")) info.pingtime = peer["pingtime"].get(); - if (peer.contains("bytessent")) info.bytessent = peer["bytessent"].get(); - if (peer.contains("bytesrecv")) info.bytesrecv = peer["bytesrecv"].get(); - if (peer.contains("startingheight")) info.startingheight = peer["startingheight"].get(); - if (peer.contains("synced_headers")) info.synced_headers = peer["synced_headers"].get(); - if (peer.contains("synced_blocks")) info.synced_blocks = peer["synced_blocks"].get(); - if (peer.contains("inbound")) info.inbound = peer["inbound"].get(); - if (peer.contains("tls_cipher")) info.tls_cipher = peer["tls_cipher"].get(); - if (peer.contains("tls_verified")) info.tls_verified = peer["tls_verified"].get(); - 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(); - if (ban.contains("banned_until")) info.banned_until = ban["banned_until"].get(); - 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()); - if (totalBal.contains("transparent")) - state_.transparent_balance = std::stod(totalBal["transparent"].get()); - if (totalBal.contains("total")) - state_.total_balance = std::stod(totalBal["total"].get()); - state_.last_balance_update = std::time(nullptr); - } - if (blockOk) { - if (blockInfo.contains("blocks")) - state_.sync.blocks = blockInfo["blocks"].get(); - if (blockInfo.contains("headers")) - state_.sync.headers = blockInfo["headers"].get(); - if (blockInfo.contains("verificationprogress")) - state_.sync.verification_progress = blockInfo["verificationprogress"].get(); - if (blockInfo.contains("longestchain")) { - int lc = blockInfo["longestchain"].get(); - // 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(); - } - // 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().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_.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) { - if (tx.confirmations >= 10 && tx.timestamp != 0) { - confirmed_tx_ids_.insert(tx.txid); - confirmed_tx_cache_.push_back(tx); - } - } - confirmed_cache_block_ = currentBlocks; - } - - // --- Peers --- - if (peerOk) { - state_.peers = std::move(peers); - state_.bannedPeers = std::move(bannedPeers); - state_.last_peer_update = std::time(nullptr); - } - - // --- Encryption state --- - if (encryptOk) { - try { - if (walletInfo.contains("unlocked_until")) { - state_.encrypted = true; - int64_t until = walletInfo["unlocked_until"].get(); - state_.unlocked_until = until; - state_.locked = (until == 0); - } else { - state_.encrypted = false; - state_.locked = false; - state_.unlocked_until = 0; - } - state_.encryption_state_known = true; - } catch (...) {} - } - - refresh_in_progress_.store(false, std::memory_order_release); - }; - }); } -void App::refreshBalance() +// ============================================================================ +// Core Data: balance + blockchain info (~50-100ms, 2 RPC calls) +// Uses fast_worker_ when on Overview tab for lower latency. +// ============================================================================ + +void App::refreshCoreData() { - if (!worker_ || !rpc_) return; - - worker_->post([this]() -> rpc::RPCWorker::MainCb { - // --- Worker thread: do blocking RPC --- + if (!state_.connected) return; + + // 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; + + if (core_refresh_in_progress_.exchange(true)) return; + + 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()); } - - // --- Main thread: apply results to state_ --- + return [this, totalBal, blockInfo, balOk, blockOk]() { try { if (balOk) { @@ -984,10 +563,8 @@ void App::refreshBalance() state_.sync.verification_progress = blockInfo["verificationprogress"].get(); if (blockInfo.contains("longestchain")) { int lc = blockInfo["longestchain"].get(); - // 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) @@ -997,12 +574,10 @@ void App::refreshBalance() if (blockInfo.contains("notarized")) state_.notarized = blockInfo["notarized"].get(); } - // 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()) { @@ -1030,21 +605,26 @@ void App::refreshBalance() } } } catch (const std::exception& e) { - DEBUG_LOGF("[refreshBalance] callback error: %s\n", e.what()); + DEBUG_LOGF("[refreshCoreData] callback error: %s\n", e.what()); } + core_refresh_in_progress_.store(false, std::memory_order_release); }; }); } -void App::refreshAddresses() +// ============================================================================ +// Address Data: z/t address lists + per-address balances +// ============================================================================ + +void App::refreshAddressData() { - if (!worker_ || !rpc_) return; - + if (!worker_ || !rpc_ || !state_.connected) return; + if (address_refresh_in_progress_.exchange(true)) return; + worker_->post([this]() -> rpc::RPCWorker::MainCb { - // --- Worker thread: fetch all address data with minimal RPC calls --- std::vector zAddrs, tAddrs; - - // 1. Get z-addresses + + // z-addresses try { json zList = rpc_->call("z_listaddresses"); for (const auto& addr : zList) { @@ -1056,8 +636,7 @@ void App::refreshAddresses() } 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 + // z-balances via z_listunspent (single call) try { json unspent = rpc_->call("z_listunspent"); std::map zBalances; @@ -1068,12 +647,9 @@ void App::refreshAddresses() } for (auto& info : zAddrs) { auto it = zBalances.find(info.address); - if (it != zBalances.end()) { - info.balance = it->second; - } + 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 { @@ -1082,8 +658,7 @@ void App::refreshAddresses() } catch (...) {} } } - - // 3. Get t-addresses + // t-addresses try { json tList = rpc_->call("getaddressesbyaccount", json::array({""})); for (const auto& addr : tList) { @@ -1095,8 +670,7 @@ void App::refreshAddresses() } catch (const std::exception& e) { DEBUG_LOGF("getaddressesbyaccount error: %s\n", e.what()); } - - // 4. Get unspent for t-address balances + // t-balances via listunspent try { json utxos = rpc_->call("listunspent"); std::map tBalances; @@ -1110,19 +684,291 @@ void App::refreshAddresses() } 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; + address_refresh_in_progress_.store(false, std::memory_order_release); }; }); } +// ============================================================================ +// 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 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 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 txns; + std::unordered_map newViewTxEntries; + std::set knownTxids; + + // Phase 3a: transparent transactions + try { + json result = rpc_->call("listtransactions", json::array({"", 9999})); + for (const auto& tx : result) { + TransactionInfo info; + if (tx.contains("txid")) info.txid = tx["txid"].get(); + if (tx.contains("category")) info.type = tx["category"].get(); + if (tx.contains("amount")) info.amount = tx["amount"].get(); + if (tx.contains("time")) info.timestamp = tx["time"].get(); + else if (tx.contains("timereceived")) info.timestamp = tx["timereceived"].get(); + if (tx.contains("confirmations")) info.confirmations = tx["confirmations"].get(); + if (tx.contains("address")) info.address = tx["address"].get(); + knownTxids.insert(info.txid); + txns.push_back(info); + } + } catch (const std::exception& e) { + DEBUG_LOGF("listtransactions error: %s\n", e.what()); + } + + // Phase 3b: shielded receives + for (const auto& addr : txZAddrs) { + try { + json zresult = rpc_->call("z_listreceivedbyaddress", json::array({addr, 0})); + if (zresult.is_null() || !zresult.is_array()) continue; + for (const auto& note : zresult) { + std::string txid; + if (note.contains("txid")) txid = note["txid"].get(); + if (txid.empty()) continue; + if (note.contains("change") && note["change"].get()) continue; + bool dominated = false; + for (const auto& existing : txns) { + if (existing.txid == txid && existing.type == "receive") { + dominated = true; break; + } + } + if (dominated) continue; + TransactionInfo info; + info.txid = txid; + info.type = "receive"; + info.address = addr; + if (note.contains("amount")) info.amount = note["amount"].get(); + if (note.contains("confirmations")) info.confirmations = note["confirmations"].get(); + if (note.contains("time")) info.timestamp = note["time"].get(); + if (note.contains("memoStr")) info.memo = note["memoStr"].get(); + knownTxids.insert(txid); + txns.push_back(info); + } + } catch (const std::exception& e) { + DEBUG_LOGF("z_listreceivedbyaddress error for %s: %s\n", + addr.substr(0, 12).c_str(), e.what()); + } + } + + // Include txids from completed z_sendmany operations + for (const auto& txid : sendTxidsSnap) { + knownTxids.insert(txid); + } + + // Phase 3c: detect shielded sends via z_viewtransaction + int viewTxCount = 0; + + auto applyViewTxEntry = [&](const std::string& txid, + const ViewTxCacheEntry& entry) { + for (const auto& out : entry.outgoing_outputs) { + bool alreadyTracked = false; + for (const auto& existing : txns) { + if (existing.txid == txid && existing.type == "send" + && std::abs(existing.amount + out.value) < 0.00000001) { + alreadyTracked = true; break; + } + } + if (alreadyTracked) continue; + TransactionInfo info; + info.txid = txid; + info.type = "send"; + info.address = out.address; + info.amount = -out.value; + info.memo = out.memo; + info.from_address = entry.from_address; + for (const auto& existing : txns) { + if (existing.txid == txid) { + info.confirmations = existing.confirmations; + info.timestamp = existing.timestamp; + break; + } + } + txns.push_back(info); + } + }; + + for (const std::string& txid : knownTxids) { + if (fullyEnriched.count(txid)) continue; + + auto cit = viewtxCacheSnap.find(txid); + if (cit != viewtxCacheSnap.end()) { + applyViewTxEntry(txid, cit->second); + continue; + } + + if (viewTxCount >= MAX_VIEWTX_PER_CYCLE) break; + ++viewTxCount; + + try { + json vtx = rpc_->call("z_viewtransaction", json::array({txid})); + if (vtx.is_null() || !vtx.is_object()) continue; + + ViewTxCacheEntry entry; + if (vtx.contains("spends") && vtx["spends"].is_array()) { + for (const auto& spend : vtx["spends"]) { + if (spend.contains("address")) { + entry.from_address = spend["address"].get(); + break; + } + } + } + if (vtx.contains("outputs") && vtx["outputs"].is_array()) { + for (const auto& output : vtx["outputs"]) { + bool outgoing = false; + if (output.contains("outgoing")) + outgoing = output["outgoing"].get(); + if (!outgoing) continue; + ViewTxCacheEntry::Output out; + if (output.contains("address")) + out.address = output["address"].get(); + if (output.contains("value")) + out.value = output["value"].get(); + if (output.contains("memoStr")) + out.memo = output["memoStr"].get(); + entry.outgoing_outputs.push_back(std::move(out)); + } + } + + applyViewTxEntry(txid, entry); + + for (auto& info : txns) { + if (info.txid == txid && info.timestamp == 0) { + try { + json rawtx = rpc_->call("gettransaction", json::array({txid})); + if (!rawtx.is_null() && rawtx.contains("time")) + info.timestamp = rawtx["time"].get(); + if (!rawtx.is_null() && rawtx.contains("confirmations")) + info.confirmations = rawtx["confirmations"].get(); + } catch (...) {} + break; + } + } + + newViewTxEntries[txid] = std::move(entry); + } catch (const std::exception& e) { + (void)e; + } + } + + std::sort(txns.begin(), txns.end(), + [](const TransactionInfo& a, const TransactionInfo& b) { + return a.timestamp > b.timestamp; + }); + + 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; + + for (auto& [txid, entry] : newViewTxEntries) { + viewtx_cache_[txid] = std::move(entry); + send_txids_.erase(txid); + } + + confirmed_tx_cache_.clear(); + confirmed_tx_ids_.clear(); + for (const auto& tx : state_.transactions) { + if (tx.confirmations >= 10 && tx.timestamp != 0) { + confirmed_tx_ids_.insert(tx.txid); + confirmed_tx_cache_.push_back(tx); + } + } + confirmed_cache_block_ = currentBlocks; + tx_refresh_in_progress_.store(false, std::memory_order_release); + }; + }); +} + +// ============================================================================ +// Encryption State: wallet info (one-shot on connect, lightweight) +// ============================================================================ + +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; + int64_t until = walletInfo["unlocked_until"].get(); + state_.unlocked_until = until; + state_.locked = (until == 0); + } else { + state_.encrypted = false; + state_.locked = false; + state_.unlocked_until = 0; + } + state_.encryption_state_known = true; + } catch (...) {} + }; + }); +} + +void App::refreshBalance() +{ + refreshCoreData(); +} + +void App::refreshAddresses() +{ + addresses_dirty_ = true; + refreshAddressData(); +} + void App::refreshMiningInfo() { // Use the dedicated fast-lane worker + connection so mining polls diff --git a/src/config/version.h b/src/config/version.h index 05485b5..35fca6a 100644 --- a/src/config/version.h +++ b/src/config/version.h @@ -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"