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:
2026-04-04 13:05:00 -05:00
parent d755f6816b
commit 50e9e7d75e
5 changed files with 508 additions and 610 deletions

View File

@@ -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"
) )

View File

@@ -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()

View File

@@ -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();
}; };

View File

@@ -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()

View File

@@ -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"