Files
ObsidianDragon/src/app.h

592 lines
22 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <memory>
#include <string>
#include <functional>
#include <thread>
#include <atomic>
#include <chrono>
#include <unordered_map>
#include <unordered_set>
#include "data/wallet_state.h"
#include "rpc/connection.h"
#include "ui/sidebar.h"
#include "ui/windows/console_tab.h"
#include "imgui.h"
// Forward declarations
namespace dragonx {
namespace rpc {
class RPCClient;
class RPCWorker;
}
namespace config { class Settings; }
namespace daemon { class EmbeddedDaemon; class XmrigManager; }
namespace util { class Bootstrap; class SecureVault; }
}
namespace dragonx {
/**
* @brief First-run wizard states
*/
enum class WizardPhase {
None, // No wizard — normal operation
BootstrapOffer, // Step 1: offer bootstrap download
BootstrapInProgress,// downloading / extracting
BootstrapFailed, // error with retry/skip option
Appearance, // Step 2: visual effects / performance options
EncryptOffer, // Step 3: offer wallet encryption
EncryptInProgress, // encrypting + daemon restarting
PinSetup, // Step 4: optional PIN setup (after encryption)
Done // wizard complete, launch normally
};
/**
* @brief Encrypt-wallet dialog phases (settings page flow)
*/
enum class EncryptDialogPhase {
PassphraseEntry, // Enter & confirm passphrase
Encrypting, // In-progress animation
PinSetup, // Offer PIN after successful encryption
Done // Finished — close dialog
};
/**
* @brief Main application class
*
* Manages application state, RPC connection, and coordinates UI rendering.
*/
class App {
public:
App();
~App();
// Non-copyable
App(const App&) = delete;
App& operator=(const App&) = delete;
/**
* @brief Initialize the application
* @return true if initialization succeeded
*/
bool init();
/**
* @brief Update application state (called every frame)
*/
void update();
/**
* @brief Pre-frame tasks that must run BEFORE ImGui::NewFrame().
*
* Font atlas rebuilds (hot-reload, user font scale changes) must
* happen before NewFrame() because NewFrame() caches font pointers.
* Rebuilding mid-frame causes dangling pointer crashes.
*/
void preFrame();
/**
* @brief Render the application UI (called every frame)
*/
void render();
/**
* @brief Clean shutdown
*/
void shutdown();
/**
* @brief Check if app should exit (render loop can stop)
* Enforces minimum 1-second display of shutdown screen so user sees feedback.
*/
bool shouldQuit() const {
if (!quit_requested_ || !shutdown_complete_) return false;
auto elapsed = std::chrono::steady_clock::now() - shutdown_start_time_;
return elapsed >= std::chrono::seconds(1);
}
/**
* @brief Request application exit — begins async shutdown
*/
void requestQuit();
/**
* @brief Begin graceful shutdown (called on window close)
* Starts daemon stop on a background thread while UI keeps rendering.
*/
void beginShutdown();
/**
* @brief Whether we are in the shutdown phase
*/
bool isShuttingDown() const { return shutting_down_; }
/**
* @brief Render the shutdown overlay (called instead of normal UI during shutdown)
*/
void renderShutdownScreen();
/**
* @brief Render loading overlay in content area while daemon is starting/syncing
* @param contentH Height of the content area child window
*/
void renderLoadingOverlay(float contentH);
// Accessors for subsystems
rpc::RPCClient* rpc() { return rpc_.get(); }
rpc::RPCWorker* worker() { return worker_.get(); }
config::Settings* settings() { return settings_.get(); }
WalletState& state() { return state_; }
const WalletState& state() const { return state_; }
const WalletState& getWalletState() const { return state_; }
// Connection state (convenience wrappers)
bool isConnected() const { return state_.connected; }
int getBlockHeight() const { return state_.sync.blocks; }
const std::string& getConnectionStatus() const { return connection_status_; }
const std::string& getDaemonStatus() const { return daemon_status_; }
// Balance info (convenience wrappers)
double getShieldedBalance() const { return state_.shielded_balance; }
double getTransparentBalance() const { return state_.transparent_balance; }
double getTotalBalance() const { return state_.total_balance; }
double getUnconfirmedBalance() const { return state_.unconfirmed_balance; }
// Addresses
const std::vector<AddressInfo>& getZAddresses() const { return state_.z_addresses; }
const std::vector<AddressInfo>& getTAddresses() const { return state_.t_addresses; }
// Transactions
const std::vector<TransactionInfo>& getTransactions() const { return state_.transactions; }
// Mining
const MiningInfo& getMiningInfo() const { return state_.mining; }
void startMining(int threads);
void stopMining();
bool isMiningToggleInProgress() const { return mining_toggle_in_progress_.load(std::memory_order_relaxed); }
// Pool mining (xmrig)
void startPoolMining(int threads);
void stopPoolMining();
// Peers
const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
void banPeer(const std::string& ip, int duration_seconds = 86400);
void unbanPeer(const std::string& ip);
void clearBans();
// Address operations
void createNewZAddress(std::function<void(const std::string&)> callback = nullptr);
void createNewTAddress(std::function<void(const std::string&)> callback = nullptr);
// Hide/unhide addresses from the address list (persisted in settings)
void hideAddress(const std::string& addr);
void unhideAddress(const std::string& addr);
bool isAddressHidden(const std::string& addr) const;
int getHiddenAddressCount() const;
void favoriteAddress(const std::string& addr);
void unfavoriteAddress(const std::string& addr);
bool isAddressFavorite(const std::string& addr) const;
// Key export/import
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
void exportAllKeys(std::function<void(const std::string&)> callback);
void importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback);
// Wallet backup
void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback);
// Transaction operations
void sendTransaction(const std::string& from, const std::string& to,
double amount, double fee, const std::string& memo,
std::function<void(bool success, const std::string& result)> callback);
// Force refresh
void refreshNow();
void refreshMiningInfo();
void refreshPeerInfo();
void refreshMarketData();
// UI navigation
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_; }
// Dialog triggers (used by settings page to open modal dialogs)
void showImportKeyDialog() { show_import_key_ = true; }
void showExportKeyDialog() { show_export_key_ = true; }
void showBackupDialog() { show_backup_ = true; }
void showAboutDialog() { show_about_ = true; }
// Legacy tab compat — maps int to NavPage
void setCurrentTab(int tab);
int getCurrentTab() const { return static_cast<int>(current_page_); }
// Payment URI handling
void handlePaymentURI(const std::string& uri);
bool hasPendingPayment() const { return pending_payment_valid_; }
void clearPendingPayment() { pending_payment_valid_ = false; }
std::string getPendingToAddress() const { return pending_to_address_; }
double getPendingAmount() const { return pending_amount_; }
std::string getPendingMemo() const { return pending_memo_; }
std::string getPendingLabel() const { return pending_label_; }
// Embedded daemon control
bool startEmbeddedDaemon();
void stopEmbeddedDaemon();
bool isEmbeddedDaemonRunning() const;
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
void rescanBlockchain(); // restart daemon with -rescan flag
// Get daemon memory usage in MB (uses embedded daemon handle if available,
// falls back to platform-level process scan for external daemons)
double getDaemonMemoryUsageMB() const;
// Diagnostic string describing daemon memory detection path (for debugging)
const std::string& getDaemonMemDiag() const { return daemon_mem_diag_; }
// Background gradient overlay texture
void setGradientTexture(ImTextureID tex) { gradient_tex_ = tex; }
ImTextureID getGradientTexture() const { return gradient_tex_; }
// Logo texture accessor (wallet branding icon)
ImTextureID getLogoTexture() const { return logo_tex_; }
// Coin logo texture accessor (DragonX currency icon for balance tab)
ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }
/**
* @brief Reload theme images (background gradient + logo) from new paths
* @param bgPath Path to background image override (empty = use default)
* @param logoPath Path to logo image override (empty = use default)
*/
void reloadThemeImages(const std::string& bgPath, const std::string& logoPath);
// Wizard / first-run
WizardPhase getWizardPhase() const { return wizard_phase_; }
bool isFirstRun() const;
/**
* @brief Stop daemon and re-run the setup wizard
* Called from Settings. Daemon restarts automatically when wizard completes.
*/
void restartWizard();
/**
* @brief Restart the embedded daemon (e.g. after changing debug categories)
* Shows "Restarting daemon..." in the loading overlay while the daemon cycles.
*/
void restartDaemon();
// Wallet encryption helpers
void encryptWalletWithPassphrase(const std::string& passphrase);
void unlockWallet(const std::string& passphrase, int timeout);
void lockWallet();
void changePassphrase(const std::string& oldPass, const std::string& newPass);
// Dialog triggers for encryption (from settings page)
void showEncryptDialog() {
show_encrypt_dialog_ = true;
encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
encrypt_status_.clear();
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
memset(enc_dlg_pin_buf_, 0, sizeof(enc_dlg_pin_buf_));
memset(enc_dlg_pin_confirm_buf_, 0, sizeof(enc_dlg_pin_confirm_buf_));
enc_dlg_saved_passphrase_.clear();
enc_dlg_pin_status_.clear();
}
void showChangePassphraseDialog() { show_change_passphrase_ = true; }
void showDecryptDialog() {
show_decrypt_dialog_ = true;
decrypt_phase_ = 0; // passphrase entry
decrypt_step_ = 0;
decrypt_status_.clear();
decrypt_in_progress_ = false;
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
}
// Dialog triggers for PIN (from settings page)
void showPinSetupDialog() { show_pin_setup_ = true; pin_status_.clear(); }
void showPinChangeDialog() { show_pin_change_ = true; pin_status_.clear(); }
void showPinRemoveDialog() { show_pin_remove_ = true; pin_status_.clear(); }
bool hasPinVault() const;
/// @brief Check if RPC worker has queued results waiting to be processed
bool hasPendingRPCResults() const;
private:
// Subsystems
std::unique_ptr<rpc::RPCClient> rpc_;
std::unique_ptr<rpc::RPCWorker> worker_;
// Fast-lane: dedicated RPC connection + worker for 1-second mining polls.
// Runs on its own thread with its own curl handle so it never blocks behind
// the main refresh batch.
std::unique_ptr<rpc::RPCClient> fast_rpc_;
std::unique_ptr<rpc::RPCWorker> fast_worker_;
// Saved connection credentials (needed to open the fast-lane connection)
rpc::ConnectionConfig saved_config_;
std::unique_ptr<config::Settings> settings_;
std::unique_ptr<daemon::EmbeddedDaemon> embedded_daemon_;
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
// Wallet state
WalletState state_;
// Shutdown state
std::atomic<bool> shutting_down_{false};
std::atomic<bool> shutdown_complete_{false};
std::atomic<bool> refresh_in_progress_{false};
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
std::string shutdown_status_;
std::thread shutdown_thread_;
float shutdown_timer_ = 0.0f;
std::chrono::steady_clock::time_point shutdown_start_time_;
// Daemon restart (e.g. after changing debug log categories)
std::atomic<bool> daemon_restarting_{false};
std::thread daemon_restart_thread_;
// UI State
bool quit_requested_ = false;
bool show_demo_window_ = false;
bool show_settings_ = false;
bool show_about_ = false;
bool show_import_key_ = false;
bool show_export_key_ = false;
bool show_backup_ = false;
bool show_address_book_ = false;
// Embedded daemon state
bool use_embedded_daemon_ = true;
std::string daemon_status_;
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
// Export/Import state
std::string export_result_;
char import_key_input_[512] = {0};
std::string export_address_;
std::string import_status_;
bool import_success_ = false;
std::string backup_status_;
bool backup_success_ = false;
// Connection
std::string connection_status_ = "Disconnected";
bool connection_in_progress_ = false;
float loading_timer_ = 0.0f; // spinner animation for loading overlay
// Current page (sidebar navigation)
ui::NavPage current_page_ = ui::NavPage::Overview;
ui::NavPage prev_page_ = ui::NavPage::Overview;
float page_alpha_ = 1.0f; // 0→1 fade on page switch
bool sidebar_collapsed_ = false; // true = icon-only mode
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
// Background gradient overlay
ImTextureID gradient_tex_ = 0;
// Logo texture (reload on skin change / dark↔light switch)
ImTextureID logo_tex_ = 0;
int logo_w_ = 0;
int logo_h_ = 0;
bool logo_loaded_ = false;
bool logo_is_dark_variant_ = true; // tracks which variant is currently loaded
// Coin logo texture (DragonX currency icon, separate from wallet branding)
ImTextureID coin_logo_tex_ = 0;
int coin_logo_w_ = 0;
int coin_logo_h_ = 0;
bool coin_logo_loaded_ = false;
// Console tab
ui::ConsoleTab console_tab_;
// Pending payment from URI
bool pending_payment_valid_ = false;
std::string pending_to_address_;
double pending_amount_ = 0.0;
std::string pending_memo_;
std::string pending_label_;
// Timers (in seconds since last update)
float refresh_timer_ = 0.0f;
float price_timer_ = 0.0f;
float fast_refresh_timer_ = 0.0f; // For mining stats
// Refresh intervals (seconds)
static constexpr float REFRESH_INTERVAL = 5.0f;
static constexpr float PRICE_INTERVAL = 60.0f;
static constexpr float FAST_REFRESH_INTERVAL = 1.0f;
// Mining refresh guard (prevents worker queue pileup)
std::atomic<bool> mining_refresh_in_progress_{false};
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
// Mining toggle guard (prevents concurrent setgenerate calls)
std::atomic<bool> mining_toggle_in_progress_{false};
// Auto-shield guard (prevents concurrent auto-shield operations)
std::atomic<bool> auto_shield_pending_{false};
// P4: Incremental transaction cache
int last_tx_block_height_ = -1; // block height at last full tx fetch
float tx_age_timer_ = 0.0f; // seconds since last tx fetch
static constexpr float TX_MAX_AGE = 15.0f; // force tx refresh every N seconds even without new blocks
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for
// txids we've already enriched. Keyed by txid.
struct ViewTxCacheEntry {
std::string from_address; // first spend address
struct Output {
std::string address;
double value = 0.0;
std::string memo;
};
std::vector<Output> outgoing_outputs;
};
std::unordered_map<std::string, ViewTxCacheEntry> viewtx_cache_;
// P4c: Confirmed transaction cache — deeply-confirmed txns (>= 10 confs)
// are accumulated here and reused across refresh cycles. Only
// recent/unconfirmed txns are re-fetched from the daemon each time.
std::vector<TransactionInfo> confirmed_tx_cache_;
std::unordered_set<std::string> confirmed_tx_ids_; // fast lookup
int confirmed_cache_block_ = -1; // block height when cache was last built
// Dirty flags for demand-driven refresh
bool addresses_dirty_ = true; // true → refreshAddresses() will run
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
// Pending z_sendmany operation tracking
std::vector<std::string> pending_opids_; // opids to poll for completion
float opid_poll_timer_ = 0.0f;
static constexpr float OPID_POLL_INTERVAL = 2.0f;
// First-run wizard state
WizardPhase wizard_phase_ = WizardPhase::None;
std::unique_ptr<util::Bootstrap> bootstrap_;
std::string wizard_pending_passphrase_; // held until daemon connects
std::string wizard_saved_passphrase_; // held until PinSetup completes/skipped
// Deferred encryption (wizard background task)
std::string deferred_encrypt_passphrase_;
std::string deferred_encrypt_pin_;
bool deferred_encrypt_pending_ = false;
// Wizard: stopping an external daemon before bootstrap
bool wizard_stopping_external_ = false;
std::string wizard_stop_status_;
std::thread wizard_stop_thread_;
// PIN vault
std::unique_ptr<util::SecureVault> vault_;
// Lock screen state
bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus
bool lock_use_pin_ = true; // true = PIN input, false = passphrase input
char lock_pin_buf_[16] = {};
char lock_passphrase_buf_[256] = {};
std::string lock_error_msg_;
float lock_error_timer_ = 0.0f;
int lock_attempts_ = 0;
float lock_lockout_timer_ = 0.0f;
bool lock_unlock_in_progress_ = false;
// Encrypt wallet dialog state
bool show_encrypt_dialog_ = false;
bool show_change_passphrase_ = false;
EncryptDialogPhase encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
char encrypt_pass_buf_[256] = {};
char encrypt_confirm_buf_[256] = {};
char change_old_pass_buf_[256] = {};
char change_new_pass_buf_[256] = {};
char change_confirm_buf_[256] = {};
std::string encrypt_status_;
bool encrypt_in_progress_ = false;
std::string enc_dlg_saved_passphrase_; // held for PIN setup after encrypt
char enc_dlg_pin_buf_[16] = {};
char enc_dlg_pin_confirm_buf_[16] = {};
std::string enc_dlg_pin_status_;
// PIN setup dialog state (settings page)
bool show_pin_setup_ = false;
bool show_pin_change_ = false;
bool show_pin_remove_ = false;
char pin_buf_[16] = {};
char pin_confirm_buf_[16] = {};
char pin_old_buf_[16] = {};
char pin_passphrase_buf_[256] = {};
std::string pin_status_;
bool pin_in_progress_ = false;
// Decrypt wallet dialog state
bool show_decrypt_dialog_ = false;
int decrypt_phase_ = 0; // 0=passphrase, 1=working, 2=done, 3=error
int decrypt_step_ = 0; // 0=unlock, 1=export, 2=stop, 3=rename, 4=restart, 5=import
char decrypt_pass_buf_[256] = {};
std::string decrypt_status_;
bool decrypt_in_progress_ = false;
std::chrono::steady_clock::time_point decrypt_step_start_time_{};
std::chrono::steady_clock::time_point decrypt_overall_start_time_{};
std::atomic<bool> decrypt_import_active_{false}; // background z_importwallet running
// Wizard PIN setup state
char wizard_pin_buf_[16] = {};
char wizard_pin_confirm_buf_[16] = {};
std::string wizard_pin_status_;
// Auto-lock on idle
std::chrono::steady_clock::time_point last_interaction_ = std::chrono::steady_clock::now();
// Private methods - rendering
void renderStatusBar();
void renderAboutDialog();
void renderImportKeyDialog();
void renderExportKeyDialog();
void renderBackupDialog();
void renderFirstRunWizard();
void renderLockScreen();
void renderEncryptWalletDialog();
void renderDecryptWalletDialog();
void renderPinDialogs();
void renderAntivirusHelpDialog();
void processDeferredEncryption();
// Private methods - connection
void tryConnect();
void onConnected();
void onDisconnected(const std::string& reason);
// Private methods - data refresh
void refreshData();
void refreshBalance();
void refreshAddresses();
void refreshPrice();
void refreshWalletEncryptionState();
void checkAutoLock();
};
} // namespace dragonx