ObsidianDragon - DragonX ImGui Wallet

Full-node GUI wallet for DragonX cryptocurrency.
Built with Dear ImGui, SDL3, and OpenGL3/DX11.

Features:
- Send/receive shielded and transparent transactions
- Autoshield with merged transaction display
- Built-in CPU mining (xmrig)
- Peer management and network monitoring
- Wallet encryption with PIN lock
- QR code generation for receive addresses
- Transaction history with pagination
- Console for direct RPC commands
- Cross-platform (Linux, Windows)
This commit is contained in:
2026-02-26 02:31:52 -06:00
commit 3aee55b49c
306 changed files with 177789 additions and 0 deletions

184
src/data/address_book.cpp Normal file
View File

@@ -0,0 +1,184 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "address_book.h"
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>
#include "../util/logger.h"
#ifdef _WIN32
#include <shlobj.h>
#else
#include <pwd.h>
#include <unistd.h>
#endif
namespace fs = std::filesystem;
using json = nlohmann::json;
namespace dragonx {
namespace data {
AddressBook::AddressBook() = default;
AddressBook::~AddressBook() = default;
std::string AddressBook::getDefaultPath()
{
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
std::string dir = std::string(path) + "\\ObsidianDragon";
fs::create_directories(dir);
return dir + "\\addressbook.json";
}
return "addressbook.json";
#elif defined(__APPLE__)
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/Library/Application Support/ObsidianDragon";
fs::create_directories(dir);
return dir + "/addressbook.json";
#else
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/.config/ObsidianDragon";
fs::create_directories(dir);
return dir + "/addressbook.json";
#endif
}
bool AddressBook::load()
{
file_path_ = getDefaultPath();
std::ifstream file(file_path_);
if (!file.is_open()) {
// No file yet - that's OK
return true;
}
try {
json j;
file >> j;
entries_.clear();
if (j.contains("entries") && j["entries"].is_array()) {
for (const auto& entry : j["entries"]) {
AddressBookEntry e;
e.label = entry.value("label", "");
e.address = entry.value("address", "");
e.notes = entry.value("notes", "");
if (!e.address.empty()) {
entries_.push_back(e);
}
}
}
DEBUG_LOGF("Address book loaded: %zu entries\n", entries_.size());
return true;
} catch (const std::exception& e) {
DEBUG_LOGF("Error loading address book: %s\n", e.what());
return false;
}
}
bool AddressBook::save()
{
if (file_path_.empty()) {
file_path_ = getDefaultPath();
}
try {
json j;
j["entries"] = json::array();
for (const auto& entry : entries_) {
json e;
e["label"] = entry.label;
e["address"] = entry.address;
e["notes"] = entry.notes;
j["entries"].push_back(e);
}
// Ensure directory exists
fs::path p(file_path_);
fs::create_directories(p.parent_path());
std::ofstream file(file_path_);
if (!file.is_open()) {
DEBUG_LOGF("Could not open address book for writing: %s\n", file_path_.c_str());
return false;
}
file << j.dump(2);
DEBUG_LOGF("Address book saved: %zu entries\n", entries_.size());
return true;
} catch (const std::exception& e) {
DEBUG_LOGF("Error saving address book: %s\n", e.what());
return false;
}
}
bool AddressBook::addEntry(const AddressBookEntry& entry)
{
// Check for duplicate address
if (findByAddress(entry.address) >= 0) {
return false;
}
entries_.push_back(entry);
return save();
}
bool AddressBook::updateEntry(size_t index, const AddressBookEntry& entry)
{
if (index >= entries_.size()) {
return false;
}
// Check for duplicate address (excluding current entry)
int existing = findByAddress(entry.address);
if (existing >= 0 && static_cast<size_t>(existing) != index) {
return false;
}
entries_[index] = entry;
return save();
}
bool AddressBook::removeEntry(size_t index)
{
if (index >= entries_.size()) {
return false;
}
entries_.erase(entries_.begin() + index);
return save();
}
int AddressBook::findByAddress(const std::string& address) const
{
for (size_t i = 0; i < entries_.size(); i++) {
if (entries_[i].address == address) {
return static_cast<int>(i);
}
}
return -1;
}
} // namespace data
} // namespace dragonx

103
src/data/address_book.h Normal file
View File

@@ -0,0 +1,103 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <vector>
namespace dragonx {
namespace data {
/**
* @brief A single address book entry
*/
struct AddressBookEntry {
std::string label;
std::string address;
std::string notes;
AddressBookEntry() = default;
AddressBookEntry(const std::string& l, const std::string& a, const std::string& n = "")
: label(l), address(a), notes(n) {}
};
/**
* @brief Address book manager
*
* Stores labeled addresses for easy access when sending.
*/
class AddressBook {
public:
AddressBook();
~AddressBook();
/**
* @brief Load address book from disk
* @return true if loaded successfully
*/
bool load();
/**
* @brief Save address book to disk
* @return true if saved successfully
*/
bool save();
/**
* @brief Get default address book file path
*/
static std::string getDefaultPath();
/**
* @brief Add a new entry
* @param entry The entry to add
* @return true if added (false if duplicate address)
*/
bool addEntry(const AddressBookEntry& entry);
/**
* @brief Update an existing entry
* @param index Index of entry to update
* @param entry New entry data
* @return true if updated
*/
bool updateEntry(size_t index, const AddressBookEntry& entry);
/**
* @brief Remove an entry
* @param index Index of entry to remove
* @return true if removed
*/
bool removeEntry(size_t index);
/**
* @brief Find entry by address
* @param address Address to search for
* @return Index or -1 if not found
*/
int findByAddress(const std::string& address) const;
/**
* @brief Get all entries
*/
const std::vector<AddressBookEntry>& entries() const { return entries_; }
/**
* @brief Get entry count
*/
size_t size() const { return entries_.size(); }
/**
* @brief Check if empty
*/
bool empty() const { return entries_.empty(); }
private:
std::vector<AddressBookEntry> entries_;
std::string file_path_;
};
} // namespace data
} // namespace dragonx

View File

@@ -0,0 +1,34 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "exchange_info.h"
namespace dragonx {
namespace data {
const std::vector<ExchangeInfo>& getExchangeRegistry()
{
static const std::vector<ExchangeInfo> registry = {
{
"TradeOgre",
"https://tradeogre.com",
{
{"DRGX", "BTC", "DRGX/BTC", "https://tradeogre.com/exchange/DRGX-BTC"},
{"DRGX", "LTC", "DRGX/LTC", "https://tradeogre.com/exchange/DRGX-LTC"},
{"DRGX", "USDT", "DRGX/USDT", "https://tradeogre.com/exchange/DRGX-USDT"},
}
},
{
"Exbitron",
"https://www.exbitron.com",
{
{"DRGX", "USDT", "DRGX/USDT", "https://www.exbitron.com/trading/drgxusdt"},
}
},
};
return registry;
}
} // namespace data
} // namespace dragonx

38
src/data/exchange_info.h Normal file
View File

@@ -0,0 +1,38 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <vector>
namespace dragonx {
namespace data {
/**
* @brief A single trading pair on an exchange (e.g. DRGX/BTC)
*/
struct ExchangePair {
std::string base; ///< e.g. "DRGX"
std::string quote; ///< e.g. "BTC"
std::string displayName; ///< e.g. "DRGX/BTC"
std::string tradeUrl; ///< Link to the exchange pair page
};
/**
* @brief Metadata for a supported exchange
*/
struct ExchangeInfo {
std::string name; ///< e.g. "TradeOgre"
std::string baseUrl; ///< e.g. "https://tradeogre.com"
std::vector<ExchangePair> pairs;
};
/**
* @brief Returns the static registry of supported exchanges + pairs
*/
const std::vector<ExchangeInfo>& getExchangeRegistry();
} // namespace data
} // namespace dragonx

57
src/data/wallet_state.cpp Normal file
View File

@@ -0,0 +1,57 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "wallet_state.h"
#include <ctime>
#include <sstream>
#include <iomanip>
namespace dragonx {
std::string TransactionInfo::getTimeString() const
{
if (timestamp == 0) return "Unknown";
std::time_t t = static_cast<std::time_t>(timestamp);
std::tm* tm = std::localtime(&t);
std::stringstream ss;
ss << std::put_time(tm, "%Y-%m-%d %H:%M");
return ss.str();
}
std::string TransactionInfo::getTypeDisplay() const
{
if (type == "send") return "Sent";
if (type == "receive") return "Received";
if (type == "mined" || type == "generate" || type == "immature") return "Mined";
return type;
}
std::string PeerInfo::getConnectionTime() const
{
if (conntime == 0) return "Unknown";
int64_t now = std::time(nullptr);
int64_t diff = now - conntime;
if (diff < 60) return std::to_string(diff) + "s";
if (diff < 3600) return std::to_string(diff / 60) + "m";
if (diff < 86400) return std::to_string(diff / 3600) + "h";
return std::to_string(diff / 86400) + "d";
}
std::string BannedPeer::getBannedUntilString() const
{
if (banned_until == 0) return "Never";
std::time_t t = static_cast<std::time_t>(banned_until);
std::tm* tm = std::localtime(&t);
std::stringstream ss;
ss << std::put_time(tm, "%Y-%m-%d %H:%M");
return ss.str();
}
} // namespace dragonx

267
src/data/wallet_state.h Normal file
View File

@@ -0,0 +1,267 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <chrono>
namespace dragonx {
/**
* @brief Represents an address with its balance
*/
struct AddressInfo {
std::string address;
double balance = 0.0;
std::string type; // "shielded" or "transparent"
// For display
std::string label;
// Derived
bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
bool isShielded() const { return type == "shielded"; }
};
/**
* @brief Represents a wallet transaction
*/
struct TransactionInfo {
std::string txid;
std::string type; // "send", "receive", "mined"
double amount = 0.0;
int64_t timestamp = 0; // Unix timestamp
int confirmations = 0;
std::string address; // destination (send) or source (receive)
std::string from_address; // source address for sends
std::string memo;
// Computed fields
std::string getTimeString() const;
std::string getTypeDisplay() const;
bool isConfirmed() const { return confirmations >= 1; }
bool isMature() const { return confirmations >= 100; }
};
/**
* @brief Represents a connected peer
*/
struct PeerInfo {
int id = 0;
std::string addr;
std::string subver;
std::string services;
int version = 0;
int64_t conntime = 0;
int banscore = 0;
double pingtime = 0.0;
int64_t bytessent = 0;
int64_t bytesrecv = 0;
int startingheight = 0;
int synced_headers = 0;
int synced_blocks = 0;
bool inbound = false;
// TLS info
std::string tls_cipher;
bool tls_verified = false;
std::string getConnectionTime() const;
};
/**
* @brief Represents a banned peer
*/
struct BannedPeer {
std::string address;
std::string subnet;
int64_t banned_until = 0;
std::string getBannedUntilString() const;
};
/**
* @brief Mining statistics
*/
struct MiningInfo {
bool generate = false;
int genproclimit = 0; // -1 means max CPUs
double localHashrate = 0.0; // Local hashrate (H/s) from getlocalsolps RPC (RandomX)
double networkHashrate = 0.0; // Network hashrate (H/s)
int blocks = 0;
double difficulty = 0.0;
std::string chain;
double daemon_memory_mb = 0.0; // Daemon process RSS in MB
// History for chart
std::vector<double> hashrate_history; // Last N samples
static constexpr int MAX_HISTORY = 300; // 5 minutes at 1s intervals
};
/**
* @brief Blockchain synchronization info
*/
struct SyncInfo {
int blocks = 0;
int headers = 0;
double verification_progress = 0.0;
bool syncing = false;
std::string best_blockhash;
bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; }
};
/**
* @brief Market/price information
*/
struct MarketInfo {
double price_usd = 0.0;
double price_btc = 0.0;
double volume_24h = 0.0;
double change_24h = 0.0;
double market_cap = 0.0;
std::string last_updated;
// Price history for chart
std::vector<double> price_history;
static constexpr int MAX_HISTORY = 24; // 24 hours
};
/**
* @brief Pool mining state (from xmrig HTTP API)
*/
struct PoolMiningState {
bool pool_mode = false; // UI toggle: solo vs pool
bool xmrig_running = false;
std::string pool_url;
std::string algo;
double hashrate_10s = 0;
double hashrate_60s = 0;
double hashrate_15m = 0;
int64_t accepted = 0;
int64_t rejected = 0;
int64_t uptime_sec = 0;
double pool_diff = 0;
bool connected = false;
// Memory/thread usage (bytes for memory)
int64_t memory_used = 0;
int threads_active = 0;
// Hashrate history for chart (mirrors MiningInfo::hashrate_history)
std::vector<double> hashrate_history;
static constexpr int MAX_HISTORY = 60; // 5 minutes at ~5s intervals
// Recent log lines for the log panel
std::vector<std::string> log_lines;
};
/**
* @brief Complete wallet state - all data fetched from daemon
*/
struct WalletState {
// Connection
bool connected = false;
int daemon_version = 0;
std::string daemon_subversion;
int protocol_version = 0;
int p2p_port = 0;
int longestchain = 0;
int notarized = 0;
// Sync status
SyncInfo sync;
// Balances (named to match UI usage)
double privateBalance = 0.0; // shielded balance
double transparentBalance = 0.0;
double totalBalance = 0.0;
double unconfirmedBalance = 0.0;
// Aliases for backward compatibility
double& shielded_balance = privateBalance;
double& transparent_balance = transparentBalance;
double& total_balance = totalBalance;
double& unconfirmed_balance = unconfirmedBalance;
// Addresses - combined list for UI convenience
std::vector<AddressInfo> addresses;
// Also keep separate lists for legacy code
std::vector<AddressInfo> z_addresses;
std::vector<AddressInfo> t_addresses;
// Transactions
std::vector<TransactionInfo> transactions;
// Peers
std::vector<PeerInfo> peers;
std::vector<BannedPeer> bannedPeers;
// Aliases for banned_peers
std::vector<BannedPeer>& banned_peers = bannedPeers;
// Mining
MiningInfo mining;
// Pool mining (xmrig)
PoolMiningState pool_mining;
// Market
MarketInfo market;
// Wallet encryption state (populated from getwalletinfo)
bool encrypted = false; // true if wallet has ever been encrypted
bool locked = false; // true if encrypted && unlocked_until <= now
int64_t unlocked_until = 0; // 0 = locked, >0 = unix timestamp when auto-lock fires
bool encryption_state_known = false; // true once first getwalletinfo response processed
bool isEncrypted() const { return encrypted; }
bool isLocked() const { return encrypted && locked; }
bool isUnlocked() const { return encrypted && !locked; }
// Timestamps for refresh logic
int64_t last_balance_update = 0;
int64_t last_tx_update = 0;
int64_t last_peer_update = 0;
int64_t last_mining_update = 0;
// Helper methods
int getAddressCount() const { return addresses.size(); }
double getBalanceUSD() const { return totalBalance * market.price_usd; }
void clear() {
connected = false;
privateBalance = transparentBalance = totalBalance = 0.0;
encrypted = false;
locked = false;
unlocked_until = 0;
encryption_state_known = false;
addresses.clear();
z_addresses.clear();
t_addresses.clear();
transactions.clear();
peers.clear();
bannedPeers.clear();
}
// Rebuild combined addresses list from z/t lists
void rebuildAddressList() {
addresses.clear();
addresses.reserve(z_addresses.size() + t_addresses.size());
for (const auto& addr : z_addresses) {
addresses.push_back(addr);
}
for (const auto& addr : t_addresses) {
addresses.push_back(addr);
}
}
};
} // namespace dragonx