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

104
src/util/base64.cpp Normal file
View File

@@ -0,0 +1,104 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "base64.h"
#include <cstring>
namespace dragonx {
namespace util {
static const char base64_chars[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
static inline bool is_base64(unsigned char c) {
return (isalnum(c) || (c == '+') || (c == '/'));
}
std::string base64_encode(const unsigned char* data, size_t len)
{
std::string ret;
ret.reserve(((len + 2) / 3) * 4);
int i = 0;
unsigned char char_array_3[3];
unsigned char char_array_4[4];
while (len--) {
char_array_3[i++] = *(data++);
if (i == 3) {
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (i = 0; i < 4; i++)
ret += base64_chars[char_array_4[i]];
i = 0;
}
}
if (i) {
for (int j = i; j < 3; j++)
char_array_3[j] = '\0';
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
for (int j = 0; j < i + 1; j++)
ret += base64_chars[char_array_4[j]];
while (i++ < 3)
ret += '=';
}
return ret;
}
std::vector<unsigned char> base64_decode(const std::string& encoded)
{
size_t in_len = encoded.size();
int i = 0;
int in_ = 0;
unsigned char char_array_4[4], char_array_3[3];
std::vector<unsigned char> ret;
while (in_len-- && (encoded[in_] != '=') && is_base64(encoded[in_])) {
char_array_4[i++] = encoded[in_]; in_++;
if (i == 4) {
for (i = 0; i < 4; i++) {
const char* p = strchr(base64_chars, char_array_4[i]);
char_array_4[i] = p ? static_cast<unsigned char>(p - base64_chars) : 0;
}
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (i = 0; i < 3; i++)
ret.push_back(char_array_3[i]);
i = 0;
}
}
if (i) {
for (int j = 0; j < i; j++) {
const char* p = strchr(base64_chars, char_array_4[j]);
char_array_4[j] = p ? static_cast<unsigned char>(p - base64_chars) : 0;
}
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
for (int j = 0; j < i - 1; j++)
ret.push_back(char_array_3[j]);
}
return ret;
}
} // namespace util
} // namespace dragonx

53
src/util/base64.h Normal file
View File

@@ -0,0 +1,53 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <vector>
#include <cstdint>
namespace dragonx {
namespace util {
/**
* @brief Base64 encoding/decoding utilities
*/
/**
* @brief Encode binary data to base64 string
* @param data Input data
* @param len Length of input data
* @return Base64 encoded string
*/
std::string base64_encode(const unsigned char* data, size_t len);
/**
* @brief Encode string to base64
* @param input Input string
* @return Base64 encoded string
*/
inline std::string base64_encode(const std::string& input) {
return base64_encode(reinterpret_cast<const unsigned char*>(input.data()), input.size());
}
/**
* @brief Decode base64 string to binary data
* @param encoded Base64 encoded string
* @return Decoded binary data
*/
std::vector<unsigned char> base64_decode(const std::string& encoded);
/**
* @brief Decode base64 string to string
* @param encoded Base64 encoded string
* @return Decoded string
*/
inline std::string base64_decode_string(const std::string& encoded) {
auto decoded = base64_decode(encoded);
return std::string(decoded.begin(), decoded.end());
}
} // namespace util
} // namespace dragonx

746
src/util/bootstrap.cpp Normal file
View File

@@ -0,0 +1,746 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "bootstrap.h"
#include <curl/curl.h>
#include <miniz.h>
#include <sodium.h>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <algorithm>
#include <sstream>
#include <iomanip>
#include "../util/logger.h"
namespace fs = std::filesystem;
namespace dragonx {
namespace util {
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static bool endsWith(const std::string& s, const std::string& suffix) {
if (suffix.size() > s.size()) return false;
return s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
}
static size_t writeFileCallback(void* contents, size_t size, size_t nmemb, void* userp) {
size_t total = size * nmemb;
FILE* fp = static_cast<FILE*>(userp);
return fwrite(contents, 1, total, fp);
}
std::string Bootstrap::formatSize(double bytes) {
const char* units[] = { "B", "KB", "MB", "GB", "TB" };
int idx = 0;
double v = bytes;
while (v >= 1024.0 && idx < 4) {
v /= 1024.0;
idx++;
}
char buf[64];
if (idx == 0)
snprintf(buf, sizeof(buf), "%.0f %s", v, units[idx]);
else
snprintf(buf, sizeof(buf), "%.2f %s", v, units[idx]);
return buf;
}
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
Bootstrap::~Bootstrap() {
cancel();
if (worker_.joinable()) worker_.join();
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void Bootstrap::start(const std::string& dataDir, const std::string& url) {
if (worker_running_) return; // already running
cancel_requested_ = false;
worker_running_ = true;
// Ensure data dir exists
fs::create_directories(dataDir);
worker_ = std::thread([this, dataDir, url]() {
std::string zipPath = dataDir + "/bootstrap_tmp.zip";
// Step 1: Download (with resume support)
// Check for a partial file from a previous interrupted download.
{
std::error_code ec;
if (fs::exists(zipPath, ec) && fs::file_size(zipPath, ec) > 0 && !ec) {
auto partial = fs::file_size(zipPath, ec);
DEBUG_LOGF("[Bootstrap] Found partial download: %s (%s)\n",
zipPath.c_str(), formatSize((double)partial).c_str());
setProgress(State::Downloading,
"Resuming download (" + formatSize((double)partial) + " already on disk)...");
} else {
setProgress(State::Downloading, "Connecting to dragonx.is...");
}
}
if (!download(url, zipPath)) {
if (cancel_requested_)
setProgress(State::Failed, "Download cancelled");
else
setProgress(State::Failed, "Download failed — check your internet connection");
// Keep the partial file so the next attempt can resume.
// Only delete if the file is empty / doesn't exist.
std::error_code ec;
auto sz = fs::exists(zipPath, ec) ? fs::file_size(zipPath, ec) : 0;
if (sz == 0) fs::remove(zipPath, ec);
worker_running_ = false;
return;
}
if (cancel_requested_) {
setProgress(State::Failed, "Download cancelled");
// Keep partial file for resume on retry.
worker_running_ = false;
return;
}
// Step 2: Verify checksums
{
// Derive base URL from the zip URL (strip filename)
std::string baseUrl = url;
auto lastSlash = baseUrl.rfind('/');
if (lastSlash != std::string::npos)
baseUrl = baseUrl.substr(0, lastSlash);
if (!verifyChecksums(zipPath, baseUrl)) {
if (!cancel_requested_) {
// Checksum failure — delete the corrupt file so next attempt re-downloads
std::error_code ec;
fs::remove(zipPath, ec);
}
worker_running_ = false;
return;
}
}
if (cancel_requested_) {
setProgress(State::Failed, "Verification cancelled");
worker_running_ = false;
return;
}
// Step 3: Clean old chain data
setProgress(State::Extracting, "Removing old chain data...");
cleanChainData(dataDir);
// Step 4: Extract (skipping wallet.dat)
if (!extract(zipPath, dataDir)) {
if (cancel_requested_)
setProgress(State::Failed, "Extraction cancelled");
else
setProgress(State::Failed, "Extraction failed — zip file may be corrupted");
worker_running_ = false;
return;
}
setProgress(State::Completed, "Bootstrap complete!");
worker_running_ = false;
});
worker_.detach();
}
void Bootstrap::cancel() {
cancel_requested_ = true;
}
Bootstrap::Progress Bootstrap::getProgress() const {
std::lock_guard<std::mutex> lk(mutex_);
return progress_;
}
bool Bootstrap::isDone() const {
std::lock_guard<std::mutex> lk(mutex_);
return progress_.state == State::Completed || progress_.state == State::Failed;
}
// ---------------------------------------------------------------------------
// Progress update (thread-safe)
// ---------------------------------------------------------------------------
void Bootstrap::setProgress(State state, const std::string& text, double downloaded, double total) {
std::lock_guard<std::mutex> lk(mutex_);
progress_.state = state;
progress_.status_text = text;
progress_.downloaded_bytes = downloaded;
progress_.total_bytes = total;
progress_.percent = (total > 0) ? (float)(100.0 * downloaded / total) : 0.0f;
if (state == State::Failed) {
progress_.error = text;
}
}
// ---------------------------------------------------------------------------
// HEAD request for remote file size
// ---------------------------------------------------------------------------
long long Bootstrap::getRemoteFileSize(const std::string& url) {
CURL* curl = curl_easy_init();
if (!curl) return -1;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); // HEAD request
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
DEBUG_LOGF("[Bootstrap] HEAD request failed: %s\n", curl_easy_strerror(res));
curl_easy_cleanup(curl);
return -1;
}
curl_off_t cl = -1;
curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &cl);
curl_easy_cleanup(curl);
DEBUG_LOGF("[Bootstrap] Remote file size: %lld bytes\n", (long long)cl);
return (long long)cl;
}
// ---------------------------------------------------------------------------
// Download (libcurl)
// ---------------------------------------------------------------------------
int Bootstrap::progressCallback(void* clientp, long long dltotal, long long dlnow,
long long /*ultotal*/, long long /*ulnow*/) {
auto* self = static_cast<Bootstrap*>(clientp);
if (self->cancel_requested_) return 1; // abort transfer
// When resuming, dlnow/dltotal only reflect the *remaining* portion.
// Add resume_offset_ so the user sees true total progress.
double offset = self->resume_offset_;
double realNow = offset + (double)dlnow;
double realTotal = (dltotal > 0) ? (offset + (double)dltotal) : 0.0;
std::string sizeText = formatSize(realNow) + " / " +
(realTotal > 0 ? formatSize(realTotal) : "unknown");
self->setProgress(State::Downloading, sizeText, realNow, realTotal);
return 0;
}
bool Bootstrap::download(const std::string& url, const std::string& destZip) {
CURL* curl = curl_easy_init();
if (!curl) {
setProgress(State::Failed, "Failed to initialise libcurl");
return false;
}
// --- Resume support with integrity check ---
// Check if a partial file exists from a previous attempt.
long long existing_bytes = 0;
{
std::error_code ec;
if (fs::exists(destZip, ec) && !ec) {
existing_bytes = (long long)fs::file_size(destZip, ec);
if (ec) existing_bytes = 0;
}
}
// If we have a partial file, check the remote file size to detect
// server-side changes that would corrupt a resumed download.
if (existing_bytes > 0) {
long long remoteSize = getRemoteFileSize(url);
if (remoteSize > 0) {
if (existing_bytes >= remoteSize) {
// File is already complete (or bigger — stale from an older zip)
DEBUG_LOGF("[Bootstrap] Local file (%lld bytes) >= remote (%lld bytes), re-downloading\n",
existing_bytes, remoteSize);
std::error_code ec;
fs::remove(destZip, ec);
existing_bytes = 0;
}
} else {
// Could not determine remote size — play it safe and re-download
DEBUG_LOGF("[Bootstrap] Could not determine remote file size, re-downloading to be safe\n");
std::error_code ec;
fs::remove(destZip, ec);
existing_bytes = 0;
}
}
resume_offset_ = (double)existing_bytes;
FILE* fp = nullptr;
if (existing_bytes > 0) {
// Open in append-binary mode and tell curl to resume
fp = fopen(destZip.c_str(), "ab");
if (!fp) {
curl_easy_cleanup(curl);
setProgress(State::Failed, "Failed to open output file for resume: " + destZip);
return false;
}
curl_easy_setopt(curl, CURLOPT_RESUME_FROM_LARGE, (curl_off_t)existing_bytes);
DEBUG_LOGF("[Bootstrap] Resuming download from byte %lld\n", existing_bytes);
} else {
fp = fopen(destZip.c_str(), "wb");
if (!fp) {
curl_easy_cleanup(curl);
setProgress(State::Failed, "Failed to open output file: " + destZip);
return false;
}
}
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFileCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progressCallback);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L); // no timeout for large file
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L); // abort if < 1 KB/s
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 60L); // ... for 60 seconds
// HTTPS certificate verification
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
CURLcode res = curl_easy_perform(curl);
fclose(fp);
curl_easy_cleanup(curl);
if (res == CURLE_RANGE_ERROR) {
// Server does not support range requests — restart from scratch
DEBUG_LOGF("[Bootstrap] Server does not support resume, restarting download\n");
resume_offset_ = 0;
std::error_code ec;
fs::remove(destZip, ec);
return download(url, destZip); // recursive retry without resume
}
if (res != CURLE_OK) {
DEBUG_LOGF("[Bootstrap] curl error: %s\n", curl_easy_strerror(res));
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// Zip extraction (miniz)
// ---------------------------------------------------------------------------
bool Bootstrap::extract(const std::string& zipPath, const std::string& dataDir) {
mz_zip_archive zip = {};
if (!mz_zip_reader_init_file(&zip, zipPath.c_str(), 0)) {
setProgress(State::Failed, "Failed to open zip file");
return false;
}
int numFiles = (int)mz_zip_reader_get_num_files(&zip);
for (int i = 0; i < numFiles; i++) {
if (cancel_requested_) {
mz_zip_reader_end(&zip);
return false;
}
mz_zip_archive_file_stat stat;
if (!mz_zip_reader_file_stat(&zip, i, &stat)) continue;
std::string filename = stat.m_filename;
// *** CRITICAL: Skip wallet.dat ***
if (filename == "wallet.dat" || endsWith(filename, "/wallet.dat")) {
DEBUG_LOGF("[Bootstrap] Skipping wallet.dat (protected)\n");
continue;
}
std::string destPath = dataDir;
// Ensure trailing separator
if (!destPath.empty() && destPath.back() != '/' && destPath.back() != '\\')
destPath += '/';
destPath += filename;
if (mz_zip_reader_is_file_a_directory(&zip, i)) {
std::error_code ec;
fs::create_directories(destPath, ec);
} else {
// Ensure parent directory exists
std::error_code ec;
fs::create_directories(fs::path(destPath).parent_path(), ec);
if (!mz_zip_reader_extract_to_file(&zip, i, destPath.c_str(), 0)) {
DEBUG_LOGF("[Bootstrap] Failed to extract: %s\n", filename.c_str());
// Non-fatal: continue with remaining files
}
}
// Update progress
float pct = (numFiles > 0) ? (100.0f * (i + 1) / numFiles) : 0.0f;
setProgress(State::Extracting,
"Extracting: " + filename,
(double)(i + 1), (double)numFiles);
progress_.percent = pct; // override to use file count ratio
}
mz_zip_reader_end(&zip);
// Clean up zip file
std::error_code ec;
fs::remove(zipPath, ec);
return true;
}
// ---------------------------------------------------------------------------
// Pre-extraction cleanup
// ---------------------------------------------------------------------------
void Bootstrap::cleanChainData(const std::string& dataDir) {
// Directories to remove completely
for (const char* subdir : {"blocks", "chainstate", "notarizations"}) {
fs::path p = fs::path(dataDir) / subdir;
std::error_code ec;
if (fs::exists(p, ec)) {
fs::remove_all(p, ec);
if (ec) DEBUG_LOGF("[Bootstrap] Warning: could not remove %s: %s\n", subdir, ec.message().c_str());
}
}
// Individual files to remove (will be replaced by bootstrap)
for (const char* file : {"fee_estimates.dat", "peers.dat"}) {
fs::path p = fs::path(dataDir) / file;
std::error_code ec;
if (fs::exists(p, ec)) {
fs::remove(p, ec);
}
}
// NEVER remove: wallet.dat, debug.log, .lock, *.conf
}
// ---------------------------------------------------------------------------
// Small-file download (checksums)
// ---------------------------------------------------------------------------
static size_t writeStringCallback(void* contents, size_t size, size_t nmemb, void* userp) {
size_t total = size * nmemb;
auto* str = static_cast<std::string*>(userp);
str->append(static_cast<const char*>(contents), total);
return total;
}
std::string Bootstrap::downloadSmallFile(const std::string& url) {
CURL* curl = curl_easy_init();
if (!curl) return {};
std::string result;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
DEBUG_LOGF("[Bootstrap] Failed to download %s: %s\n", url.c_str(), curl_easy_strerror(res));
return {};
}
return result;
}
// ---------------------------------------------------------------------------
// Checksum helpers
// ---------------------------------------------------------------------------
std::string Bootstrap::parseChecksumFile(const std::string& content) {
// Typical format: "<hex_hash> <filename>" or just "<hex_hash>"
// Extract the first whitespace-delimited token.
std::istringstream iss(content);
std::string token;
if (iss >> token) {
// Normalise to lowercase
std::transform(token.begin(), token.end(), token.begin(),
[](unsigned char c) { return std::tolower(c); });
return token;
}
return {};
}
std::string Bootstrap::computeSHA256(const std::string& filePath) {
FILE* fp = fopen(filePath.c_str(), "rb");
if (!fp) return {};
// Get file size for progress reporting
fseek(fp, 0, SEEK_END);
long long fileSize = ftell(fp);
fseek(fp, 0, SEEK_SET);
crypto_hash_sha256_state state;
crypto_hash_sha256_init(&state);
unsigned char buf[65536];
size_t n;
long long processed = 0;
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
if (cancel_requested_) {
fclose(fp);
return {};
}
crypto_hash_sha256_update(&state, buf, n);
processed += (long long)n;
// Update progress every ~4MB
if (fileSize > 0 && (processed % (4 * 1024 * 1024)) < (long long)sizeof(buf)) {
float pct = (float)(100.0 * processed / fileSize);
char msg[128];
snprintf(msg, sizeof(msg), "Verifying SHA-256... %.0f%% (%s / %s)",
pct, formatSize((double)processed).c_str(),
formatSize((double)fileSize).c_str());
setProgress(State::Downloading, msg, (double)processed, (double)fileSize);
}
}
fclose(fp);
unsigned char hash[crypto_hash_sha256_BYTES];
crypto_hash_sha256_final(&state, hash);
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (int i = 0; i < crypto_hash_sha256_BYTES; i++)
oss << std::setw(2) << (int)hash[i];
return oss.str();
}
// ---------------------------------------------------------------------------
// MD5 — minimal embedded implementation (RFC 1321)
// ---------------------------------------------------------------------------
namespace {
struct MD5Context {
uint32_t state[4];
uint64_t count;
uint8_t buffer[64];
};
static const uint32_t md5_T[64] = {
0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,
0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be,0x6b901122,0xfd987193,0xa679438e,0x49b40821,
0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa,0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8,
0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a,
0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c,0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70,
0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665,
0xf4292244,0x432aff97,0xab9423a7,0xfc93a039,0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1,
0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1,0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391
};
static const int md5_S[64] = {
7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,
5, 9,14,20,5, 9,14,20,5, 9,14,20,5, 9,14,20,
4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,
6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21
};
static inline uint32_t md5_rotl(uint32_t x, int n) { return (x << n) | (x >> (32 - n)); }
static void md5_transform(uint32_t state[4], const uint8_t block[64]) {
uint32_t M[16];
for (int i = 0; i < 16; i++)
M[i] = (uint32_t)block[i*4] | ((uint32_t)block[i*4+1]<<8) |
((uint32_t)block[i*4+2]<<16) | ((uint32_t)block[i*4+3]<<24);
uint32_t a=state[0], b=state[1], c=state[2], d=state[3];
for (int i = 0; i < 64; i++) {
uint32_t f, g;
if (i < 16) { f = (b & c) | (~b & d); g = i; }
else if (i < 32) { f = (d & b) | (~d & c); g = (5*i+1) % 16; }
else if (i < 48) { f = b ^ c ^ d; g = (3*i+5) % 16; }
else { f = c ^ (b | ~d); g = (7*i) % 16; }
uint32_t tmp = d; d = c; c = b;
b = b + md5_rotl(a + f + md5_T[i] + M[g], md5_S[i]);
a = tmp;
}
state[0]+=a; state[1]+=b; state[2]+=c; state[3]+=d;
}
static void md5_init(MD5Context* ctx) {
ctx->state[0]=0x67452301; ctx->state[1]=0xefcdab89;
ctx->state[2]=0x98badcfe; ctx->state[3]=0x10325476;
ctx->count = 0;
memset(ctx->buffer, 0, 64);
}
static void md5_update(MD5Context* ctx, const uint8_t* data, size_t len) {
size_t idx = (size_t)(ctx->count % 64);
ctx->count += len;
for (size_t i = 0; i < len; i++) {
ctx->buffer[idx++] = data[i];
if (idx == 64) { md5_transform(ctx->state, ctx->buffer); idx = 0; }
}
}
static void md5_final(MD5Context* ctx, uint8_t digest[16]) {
uint64_t bits = ctx->count * 8;
uint8_t pad = 0x80;
md5_update(ctx, &pad, 1);
pad = 0;
while (ctx->count % 64 != 56) md5_update(ctx, &pad, 1);
uint8_t bitbuf[8];
for (int i = 0; i < 8; i++) bitbuf[i] = (uint8_t)(bits >> (i*8));
md5_update(ctx, bitbuf, 8);
for (int i = 0; i < 4; i++) {
digest[i*4+0] = (uint8_t)(ctx->state[i]);
digest[i*4+1] = (uint8_t)(ctx->state[i]>>8);
digest[i*4+2] = (uint8_t)(ctx->state[i]>>16);
digest[i*4+3] = (uint8_t)(ctx->state[i]>>24);
}
}
} // anon namespace
std::string Bootstrap::computeMD5(const std::string& filePath) {
FILE* fp = fopen(filePath.c_str(), "rb");
if (!fp) return {};
// Get file size for progress reporting
fseek(fp, 0, SEEK_END);
long long fileSize = ftell(fp);
fseek(fp, 0, SEEK_SET);
MD5Context ctx;
md5_init(&ctx);
uint8_t buf[65536];
size_t n;
long long processed = 0;
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
if (cancel_requested_) {
fclose(fp);
return {};
}
md5_update(&ctx, buf, n);
processed += (long long)n;
// Update progress every ~4MB
if (fileSize > 0 && (processed % (4 * 1024 * 1024)) < (long long)sizeof(buf)) {
float pct = (float)(100.0 * processed / fileSize);
char msg[128];
snprintf(msg, sizeof(msg), "Verifying MD5... %.0f%% (%s / %s)",
pct, formatSize((double)processed).c_str(),
formatSize((double)fileSize).c_str());
setProgress(State::Downloading, msg, (double)processed, (double)fileSize);
}
}
fclose(fp);
uint8_t digest[16];
md5_final(&ctx, digest);
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (int i = 0; i < 16; i++)
oss << std::setw(2) << (int)digest[i];
return oss.str();
}
// ---------------------------------------------------------------------------
// Checksum verification pipeline
// ---------------------------------------------------------------------------
bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& baseUrl) {
setProgress(State::Downloading, "Downloading checksums...");
std::string sha256Url = baseUrl + "/" + kZipName + ".sha256";
std::string md5Url = baseUrl + "/" + kZipName + ".md5";
std::string sha256Content = downloadSmallFile(sha256Url);
std::string md5Content = downloadSmallFile(md5Url);
if (cancel_requested_) return false;
bool haveSHA256 = !sha256Content.empty();
bool haveMD5 = !md5Content.empty();
if (!haveSHA256 && !haveMD5) {
DEBUG_LOGF("[Bootstrap] Warning: no checksum files available — skipping verification\n");
// Allow the process to continue (server may not have checksum files yet)
return true;
}
// --- SHA-256 ---
if (haveSHA256) {
setProgress(State::Downloading, "Verifying SHA-256...");
std::string expected = parseChecksumFile(sha256Content);
std::string actual = computeSHA256(zipPath);
if (cancel_requested_) return false;
if (expected.empty() || actual.empty()) {
DEBUG_LOGF("[Bootstrap] SHA-256: could not compute/parse (expected=%s, actual=%s)\n",
expected.c_str(), actual.c_str());
setProgress(State::Failed, "SHA-256 verification error");
return false;
}
if (expected != actual) {
DEBUG_LOGF("[Bootstrap] SHA-256 MISMATCH!\n expected: %s\n actual: %s\n",
expected.c_str(), actual.c_str());
setProgress(State::Failed,
"SHA-256 mismatch — the server's checksum file may be out of date.\n"
"Expected: " + expected.substr(0, 16) + "...\n"
"Got: " + actual.substr(0, 16) + "...\n"
"Try again or use a fresh download.");
return false;
}
DEBUG_LOGF("[Bootstrap] SHA-256 verified: %s\n", actual.c_str());
}
// --- MD5 ---
if (haveMD5) {
setProgress(State::Downloading, "Verifying MD5...");
std::string expected = parseChecksumFile(md5Content);
std::string actual = computeMD5(zipPath);
if (cancel_requested_) return false;
if (expected.empty() || actual.empty()) {
DEBUG_LOGF("[Bootstrap] MD5: could not compute/parse (expected=%s, actual=%s)\n",
expected.c_str(), actual.c_str());
setProgress(State::Failed, "MD5 verification error");
return false;
}
if (expected != actual) {
DEBUG_LOGF("[Bootstrap] MD5 MISMATCH!\n expected: %s\n actual: %s\n",
expected.c_str(), actual.c_str());
setProgress(State::Failed,
"MD5 mismatch — the server's checksum file may be out of date.\n"
"Expected: " + expected + "\n"
"Got: " + actual + "\n"
"Try again or use a fresh download.");
return false;
}
DEBUG_LOGF("[Bootstrap] MD5 verified: %s\n", actual.c_str());
}
setProgress(State::Downloading, "Checksums verified \xe2\x9c\x93");
return true;
}
} // namespace util
} // namespace dragonx

116
src/util/bootstrap.h Normal file
View File

@@ -0,0 +1,116 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <vector>
#include <mutex>
#include <atomic>
#include <thread>
namespace dragonx {
namespace util {
/**
* @brief Bootstrap downloader + extractor for DRAGONX blockchain data.
*
* Downloads a blockchain snapshot zip from bootstrap.dragonx.is, verifies
* SHA-256 / MD5 checksums, extracts it into the DRAGONX data directory
* (skipping wallet.dat), and cleans old chain data before extraction.
* All work runs on a background thread; progress is queried thread-safely
* from the UI thread.
*/
class Bootstrap {
public:
enum class State {
Idle,
Downloading,
Extracting,
Completed,
Failed
};
struct Progress {
State state = State::Idle;
double downloaded_bytes = 0;
double total_bytes = 0; // 0 if server doesn't send Content-Length
float percent = 0.0f; // 0..100
std::string status_text; // human-readable status
std::string error; // non-empty on failure
};
Bootstrap() = default;
~Bootstrap();
// Non-copyable
Bootstrap(const Bootstrap&) = delete;
Bootstrap& operator=(const Bootstrap&) = delete;
/// Base URL for bootstrap downloads (zip + checksum files).
static constexpr const char* kBaseUrl = "https://bootstrap.dragonx.is";
static constexpr const char* kZipName = "DRAGONX.zip";
/// Start the bootstrap process on a background thread.
/// @param dataDir Path to DRAGONX data dir (e.g. ~/.hush/DRAGONX/)
/// @param url Bootstrap zip URL
void start(const std::string& dataDir,
const std::string& url = "https://bootstrap.dragonx.is/DRAGONX.zip");
/// Cancel an in-progress download/extract.
void cancel();
/// Thread-safe read of current progress.
Progress getProgress() const;
/// Whether the operation is finished (success or failure).
bool isDone() const;
/// Format a byte count as human-readable string (e.g. "1.24 GB")
static std::string formatSize(double bytes);
private:
mutable std::mutex mutex_;
Progress progress_;
std::atomic<bool> cancel_requested_{false};
std::thread worker_;
std::atomic<bool> worker_running_{false};
bool download(const std::string& url, const std::string& destZip);
bool extract(const std::string& zipPath, const std::string& dataDir);
void cleanChainData(const std::string& dataDir);
void setProgress(State state, const std::string& text, double downloaded = 0, double total = 0);
/// Download a small text file (checksum) into a string. Returns empty on failure.
std::string downloadSmallFile(const std::string& url);
/// Compute SHA-256 of a file, return lowercase hex digest.
/// Updates progress with "Verifying SHA-256..." status during computation.
std::string computeSHA256(const std::string& filePath);
/// Compute MD5 of a file, return lowercase hex digest.
/// Updates progress with "Verifying MD5..." status during computation.
std::string computeMD5(const std::string& filePath);
/// Parse the first hex token from a checksum file (handles "<hash> <filename>" format).
static std::string parseChecksumFile(const std::string& content);
/// Verify downloaded zip against remote checksums. Returns true if valid.
bool verifyChecksums(const std::string& zipPath, const std::string& baseUrl);
/// Perform a HEAD request to get remote file size.
/// Returns -1 on failure.
long long getRemoteFileSize(const std::string& url);
/// Byte offset of the partial file when resuming a download.
/// Added to dlnow in progress reports so the bar reflects true total.
double resume_offset_{0};
// libcurl progress callback (static → calls into instance via clientp)
static int progressCallback(void* clientp, long long dltotal, long long dlnow,
long long ultotal, long long ulnow);
};
} // namespace util
} // namespace dragonx

321
src/util/i18n.cpp Normal file
View File

@@ -0,0 +1,321 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "i18n.h"
#include <fstream>
#include <cstdio>
#include <cstring>
#include <nlohmann/json.hpp>
// Embedded language files
#include "embedded/lang_es.h"
#include "embedded/lang_zh.h"
#include "embedded/lang_ru.h"
#include "embedded/lang_de.h"
#include "embedded/lang_fr.h"
#include "embedded/lang_pt.h"
#include "embedded/lang_ja.h"
#include "embedded/lang_ko.h"
#include "../util/logger.h"
namespace dragonx {
namespace util {
using json = nlohmann::json;
I18n::I18n()
{
// Register built-in languages
registerLanguage("en", "English");
registerLanguage("es", "Español");
registerLanguage("zh", "中文");
registerLanguage("ru", "Русский");
registerLanguage("de", "Deutsch");
registerLanguage("fr", "Français");
registerLanguage("pt", "Português");
registerLanguage("ja", "日本語");
registerLanguage("ko", "한국어");
// Load default English strings (built-in fallback)
loadLanguage("en");
}
I18n& I18n::instance()
{
static I18n instance;
return instance;
}
bool I18n::loadLanguage(const std::string& locale)
{
// First, try to load from file (allows user overrides)
std::string lang_file = "res/lang/" + locale + ".json";
std::ifstream file(lang_file);
if (file.is_open()) {
try {
json j;
file >> j;
strings_.clear();
for (auto& [key, value] : j.items()) {
if (value.is_string()) {
strings_[key] = value.get<std::string>();
}
}
current_locale_ = locale;
DEBUG_LOGF("Loaded language file: %s (%zu strings)\n", lang_file.c_str(), strings_.size());
return true;
} catch (const std::exception& e) {
DEBUG_LOGF("Error parsing language file %s: %s\n", lang_file.c_str(), e.what());
}
}
// Try embedded language data
const unsigned char* embedded_data = nullptr;
unsigned int embedded_size = 0;
if (locale == "es") {
embedded_data = res_lang_es_json;
embedded_size = res_lang_es_json_len;
} else if (locale == "zh") {
embedded_data = res_lang_zh_json;
embedded_size = res_lang_zh_json_len;
} else if (locale == "ru") {
embedded_data = res_lang_ru_json;
embedded_size = res_lang_ru_json_len;
} else if (locale == "de") {
embedded_data = res_lang_de_json;
embedded_size = res_lang_de_json_len;
} else if (locale == "fr") {
embedded_data = res_lang_fr_json;
embedded_size = res_lang_fr_json_len;
} else if (locale == "pt") {
embedded_data = res_lang_pt_json;
embedded_size = res_lang_pt_json_len;
} else if (locale == "ja") {
embedded_data = res_lang_ja_json;
embedded_size = res_lang_ja_json_len;
} else if (locale == "ko") {
embedded_data = res_lang_ko_json;
embedded_size = res_lang_ko_json_len;
}
if (embedded_data != nullptr && embedded_size > 0) {
try {
std::string json_str(reinterpret_cast<const char*>(embedded_data), embedded_size);
json j = json::parse(json_str);
strings_.clear();
for (auto& [key, value] : j.items()) {
if (value.is_string()) {
strings_[key] = value.get<std::string>();
}
}
current_locale_ = locale;
DEBUG_LOGF("Loaded embedded language: %s (%zu strings)\n", locale.c_str(), strings_.size());
return true;
} catch (const std::exception& e) {
DEBUG_LOGF("Error parsing embedded language %s: %s\n", locale.c_str(), e.what());
}
}
// If English, use built-in strings
if (locale == "en") {
strings_.clear();
// Navigation & Tabs
strings_["balance"] = "Balance";
strings_["send"] = "Send";
strings_["receive"] = "Receive";
strings_["transactions"] = "Transactions";
strings_["mining"] = "Mining";
strings_["peers"] = "Peers";
strings_["market"] = "Market";
strings_["settings"] = "Settings";
// Balance Tab
strings_["summary"] = "Summary";
strings_["shielded"] = "Shielded";
strings_["transparent"] = "Transparent";
strings_["total"] = "Total";
strings_["unconfirmed"] = "Unconfirmed";
strings_["your_addresses"] = "Your Addresses";
strings_["z_addresses"] = "Z-Addresses";
strings_["t_addresses"] = "T-Addresses";
strings_["no_addresses"] = "No addresses found. Create one using the buttons above.";
strings_["new_z_address"] = "New Z-Address";
strings_["new_t_address"] = "New T-Address";
strings_["type"] = "Type";
strings_["address"] = "Address";
strings_["copy_address"] = "Copy Full Address";
strings_["send_from_this_address"] = "Send From This Address";
strings_["export_private_key"] = "Export Private Key";
strings_["export_viewing_key"] = "Export Viewing Key";
strings_["show_qr_code"] = "Show QR Code";
strings_["not_connected"] = "Not connected to daemon...";
// Send Tab
strings_["pay_from"] = "Pay From";
strings_["send_to"] = "Send To";
strings_["amount"] = "Amount";
strings_["memo"] = "Memo (optional, encrypted)";
strings_["miner_fee"] = "Miner Fee";
strings_["fee"] = "Fee";
strings_["send_transaction"] = "Send Transaction";
strings_["clear"] = "Clear";
strings_["select_address"] = "Select address...";
strings_["paste"] = "Paste";
strings_["max"] = "Max";
strings_["available"] = "Available";
strings_["invalid_address"] = "Invalid address format";
strings_["memo_z_only"] = "Note: Memos are only available when sending to shielded (z) addresses";
strings_["characters"] = "characters";
strings_["from"] = "From";
strings_["to"] = "To";
strings_["sending"] = "Sending transaction";
strings_["confirm_send"] = "Confirm Send";
strings_["confirm_transaction"] = "Confirm Transaction";
strings_["confirm_and_send"] = "Confirm & Send";
strings_["cancel"] = "Cancel";
// Receive Tab
strings_["receiving_addresses"] = "Your Receiving Addresses";
strings_["new_z_shielded"] = "New z-Address (Shielded)";
strings_["new_t_transparent"] = "New t-Address (Transparent)";
strings_["address_details"] = "Address Details";
strings_["view_on_explorer"] = "View on Explorer";
strings_["qr_code"] = "QR Code";
strings_["request_payment"] = "Request Payment";
// Transactions Tab
strings_["date"] = "Date";
strings_["status"] = "Status";
strings_["confirmations"] = "Confirmations";
strings_["confirmed"] = "Confirmed";
strings_["pending"] = "Pending";
strings_["sent"] = "sent";
strings_["received"] = "received";
strings_["mined"] = "mined";
// Mining Tab
strings_["mining_control"] = "Mining Control";
strings_["start_mining"] = "Start Mining";
strings_["stop_mining"] = "Stop Mining";
strings_["mining_threads"] = "Mining Threads";
strings_["mining_statistics"] = "Mining Statistics";
strings_["local_hashrate"] = "Local Hashrate";
strings_["network_hashrate"] = "Network Hashrate";
strings_["difficulty"] = "Difficulty";
strings_["est_time_to_block"] = "Est. Time to Block";
strings_["mining_off"] = "Mining is OFF";
strings_["mining_on"] = "Mining is ON";
// Peers Tab
strings_["connected_peers"] = "Connected Peers";
strings_["banned_peers"] = "Banned Peers";
strings_["ip_address"] = "IP Address";
strings_["version"] = "Version";
strings_["height"] = "Height";
strings_["ping"] = "Ping";
strings_["ban"] = "Ban";
strings_["unban"] = "Unban";
strings_["clear_all_bans"] = "Clear All Bans";
// Market Tab
strings_["price_chart"] = "Price Chart";
strings_["current_price"] = "Current Price";
strings_["24h_change"] = "24h Change";
strings_["24h_volume"] = "24h Volume";
strings_["market_cap"] = "Market Cap";
// Settings
strings_["general"] = "General";
strings_["display"] = "Display";
strings_["network"] = "Network";
strings_["theme"] = "Theme";
strings_["language"] = "Language";
strings_["dragonx_green"] = "DragonX (Green)";
strings_["dark"] = "Dark";
strings_["light"] = "Light";
strings_["allow_custom_fees"] = "Allow custom fees";
strings_["use_embedded_daemon"] = "Use embedded dragonxd";
strings_["save"] = "Save";
strings_["close"] = "Close";
// Menu
strings_["file"] = "File";
strings_["edit"] = "Edit";
strings_["view"] = "View";
strings_["help"] = "Help";
strings_["import_private_key"] = "Import Private Key...";
strings_["backup_wallet"] = "Backup Wallet...";
strings_["exit"] = "Exit";
strings_["about_dragonx"] = "About ObsidianDragon";
strings_["refresh_now"] = "Refresh Now";
// Dialogs
strings_["about"] = "About";
strings_["import"] = "Import";
strings_["export"] = "Export";
strings_["copy_to_clipboard"] = "Copy to Clipboard";
// Status
strings_["connected"] = "Connected";
strings_["disconnected"] = "Disconnected";
strings_["connecting"] = "Connecting...";
strings_["syncing"] = "Syncing...";
strings_["block"] = "Block";
strings_["no_addresses_available"] = "No addresses available";
// Errors & Messages
strings_["error"] = "Error";
strings_["success"] = "Success";
strings_["warning"] = "Warning";
strings_["amount_exceeds_balance"] = "Amount exceeds balance";
strings_["transaction_sent"] = "Transaction sent successfully";
current_locale_ = "en";
DEBUG_LOGF("Using built-in English strings (%zu strings)\n", strings_.size());
return true;
}
// Fallback: reload English so we never leave stale strings
DEBUG_LOGF("Language file not found: %s — falling back to English\n", lang_file.c_str());
if (locale != "en") {
loadLanguage("en");
}
return false;
}
const char* I18n::translate(const char* key) const
{
if (!key) return "";
auto it = strings_.find(key);
if (it != strings_.end()) {
return it->second.c_str();
}
// Return key if translation not found
return key;
}
void I18n::registerLanguage(const std::string& code, const std::string& name)
{
// Check if already registered
for (const auto& lang : available_languages_) {
if (lang.first == code) return;
}
available_languages_.emplace_back(code, name);
}
} // namespace util
} // namespace dragonx

79
src/util/i18n.h Normal file
View File

@@ -0,0 +1,79 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
namespace dragonx {
namespace util {
/**
* @brief Internationalization support
*
* Simple string table implementation for translating UI text.
* Strings are stored in JSON files in res/lang/ directory.
*/
class I18n {
public:
/**
* @brief Get the singleton instance
*/
static I18n& instance();
/**
* @brief Load a language file
* @param locale Language code (e.g., "en", "es", "zh")
* @return true if loaded successfully
*/
bool loadLanguage(const std::string& locale);
/**
* @brief Translate a string
* @param key The string key (usually English text)
* @return Translated string, or key if not found
*/
const char* translate(const char* key) const;
/**
* @brief Get current locale
*/
const std::string& getCurrentLocale() const { return current_locale_; }
/**
* @brief Get list of available languages
*/
const std::vector<std::pair<std::string, std::string>>& getAvailableLanguages() const { return available_languages_; }
/**
* @brief Register an available language
* @param code Language code (e.g., "en")
* @param name Display name (e.g., "English")
*/
void registerLanguage(const std::string& code, const std::string& name);
private:
I18n();
std::string current_locale_ = "en";
std::unordered_map<std::string, std::string> strings_;
std::vector<std::pair<std::string, std::string>> available_languages_;
};
/**
* @brief Convenience function for translation
* @param key The string key
* @return Translated string
*/
inline const char* tr(const char* key) {
return I18n::instance().translate(key);
}
} // namespace util
} // namespace dragonx
// Convenience macro for translations
#define TR(key) dragonx::util::tr(key)

91
src/util/logger.cpp Normal file
View File

@@ -0,0 +1,91 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "logger.h"
#include <cstdarg>
#include <ctime>
#include <chrono>
#include <iomanip>
#include <sstream>
namespace dragonx {
namespace util {
Logger::Logger() = default;
Logger::~Logger()
{
if (file_.is_open()) {
file_.close();
}
}
Logger& Logger::instance()
{
static Logger logger;
return logger;
}
bool Logger::init(const std::string& path)
{
std::lock_guard<std::mutex> lock(mutex_);
if (file_.is_open()) {
file_.close();
}
file_.open(path, std::ios::out | std::ios::app);
initialized_ = file_.is_open();
if (initialized_) {
write("=== Logger initialized ===");
}
return initialized_;
}
void Logger::write(const std::string& message)
{
std::lock_guard<std::mutex> lock(mutex_);
// Get current timestamp
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()) % 1000;
std::stringstream ss;
ss << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S");
ss << '.' << std::setfill('0') << std::setw(3) << ms.count();
ss << " | " << message;
std::string line = ss.str();
// Write to file if open
if (file_.is_open()) {
file_ << line << std::endl;
file_.flush();
}
// Also write to stdout in debug mode
#ifdef DRAGONX_DEBUG
printf("%s\n", line.c_str());
#endif
}
void Logger::writef(const char* format, ...)
{
char buffer[4096];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
write(buffer);
}
} // namespace util
} // namespace dragonx

65
src/util/logger.h Normal file
View File

@@ -0,0 +1,65 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <fstream>
#include <mutex>
namespace dragonx {
namespace util {
/**
* @brief Simple file logger
*/
class Logger {
public:
Logger();
~Logger();
/**
* @brief Initialize logger with output file
* @param path Path to log file
* @return true if opened successfully
*/
bool init(const std::string& path);
/**
* @brief Write a log message
* @param message Message to log
*/
void write(const std::string& message);
/**
* @brief Write a formatted log message
* @param format printf-style format string
*/
void writef(const char* format, ...);
/**
* @brief Get the singleton instance
*/
static Logger& instance();
private:
std::ofstream file_;
std::mutex mutex_;
bool initialized_ = false;
};
// Convenience macros
#define LOG(msg) dragonx::util::Logger::instance().write(msg)
#define LOGF(...) dragonx::util::Logger::instance().writef(__VA_ARGS__)
#ifdef DRAGONX_DEBUG
#define DEBUG_LOG(msg) LOG(msg)
#define DEBUG_LOGF(...) LOGF(__VA_ARGS__)
#else
#define DEBUG_LOG(msg)
#define DEBUG_LOGF(...)
#endif
} // namespace util
} // namespace dragonx

168
src/util/noise_texture.cpp Normal file
View File

@@ -0,0 +1,168 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "noise_texture.h"
#include "texture_loader.h"
#include <cstdlib>
#include <cstring>
namespace dragonx {
namespace util {
static constexpr int kNoiseSize = 256;
ImTextureID GetNoiseTexture(int* texSize)
{
static ImTextureID s_tex = 0;
static bool s_init = false;
if (texSize) *texSize = kNoiseSize;
if (s_init) return s_tex;
s_init = true;
// Generate subtle white-noise RGBA pixels.
// Each pixel is white (RGB=255) with alpha in a narrow band
// around 128 so the grain is uniform and gentle.
const int numPixels = kNoiseSize * kNoiseSize;
unsigned char* pixels = (unsigned char*)malloc(numPixels * 4);
if (!pixels) return 0;
// Simple LCG PRNG (deterministic, fast, no need for crypto quality)
unsigned int seed = 0xDEADBEEF;
for (int i = 0; i < numPixels; ++i) {
seed = seed * 1664525u + 1013904223u;
// Range: 16240 (centered on 128, ±112) for visible, high-contrast grain
unsigned char v = 16 + (unsigned char)((seed >> 24) % 225);
pixels[i * 4 + 0] = 255; // R
pixels[i * 4 + 1] = 255; // G
pixels[i * 4 + 2] = 255; // B
pixels[i * 4 + 3] = v; // A — high contrast variation
}
bool ok = CreateRawTexture(pixels, kNoiseSize, kNoiseSize,
true /* repeat/tile */, &s_tex);
free(pixels);
if (!ok) {
s_tex = 0;
}
return s_tex;
}
// ============================================================================
// Pre-tiled viewport-sized noise texture
// ============================================================================
// Generate the base 256x256 noise pattern into a caller-allocated buffer.
// Same deterministic LCG as GetNoiseTexture().
static void GenerateBaseNoise(unsigned char* out, int sz)
{
unsigned int seed = 0xDEADBEEF;
const int numPixels = sz * sz;
for (int i = 0; i < numPixels; ++i) {
seed = seed * 1664525u + 1013904223u;
unsigned char v = 16 + (unsigned char)((seed >> 24) % 225);
out[i * 4 + 0] = 255;
out[i * 4 + 1] = 255;
out[i * 4 + 2] = 255;
out[i * 4 + 3] = v;
}
}
ImTextureID GetTiledNoiseTexture(int viewportW, int viewportH,
int* outW, int* outH)
{
static ImTextureID s_tex = 0;
static int s_texW = 0;
static int s_texH = 0;
if (outW) *outW = s_texW;
if (outH) *outH = s_texH;
// Re-use if viewport hasn't changed
if (s_tex && s_texW == viewportW && s_texH == viewportH) {
return s_tex;
}
// Destroy old texture
if (s_tex) {
DestroyTexture(s_tex);
s_tex = 0;
s_texW = s_texH = 0;
}
if (viewportW <= 0 || viewportH <= 0) return 0;
// Generate base tile
unsigned char* base = (unsigned char*)malloc(kNoiseSize * kNoiseSize * 4);
if (!base) return 0;
GenerateBaseNoise(base, kNoiseSize);
// Allocate viewport-sized buffer and tile the base pattern
const size_t rowBytes = (size_t)viewportW * 4;
unsigned char* tiled = (unsigned char*)malloc(rowBytes * viewportH);
if (!tiled) { free(base); return 0; }
const int basePitch = kNoiseSize * 4;
for (int y = 0; y < viewportH; ++y) {
int sy = y % kNoiseSize;
unsigned char* dst = tiled + y * rowBytes;
const unsigned char* srcRow = base + sy * basePitch;
int x = 0;
// Copy full tile-width strips
while (x + kNoiseSize <= viewportW) {
memcpy(dst + x * 4, srcRow, basePitch);
x += kNoiseSize;
}
// Partial remaining strip
if (x < viewportW) {
memcpy(dst + x * 4, srcRow, (viewportW - x) * 4);
}
}
free(base);
bool ok = CreateRawTexture(tiled, viewportW, viewportH,
false /* no repeat needed */, &s_tex);
free(tiled);
if (ok) {
s_texW = viewportW;
s_texH = viewportH;
} else {
s_tex = 0;
}
if (outW) *outW = s_texW;
if (outH) *outH = s_texH;
return s_tex;
}
void DrawTiledNoiseRect(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax,
ImU32 tintColor)
{
if (!dl || (tintColor & IM_COL32_A_MASK) == 0) return;
ImGuiViewport* vp = ImGui::GetMainViewport();
int vpW = (int)vp->Size.x;
int vpH = (int)vp->Size.y;
if (vpW <= 0 || vpH <= 0) return;
int texW = 0, texH = 0;
ImTextureID tex = GetTiledNoiseTexture(vpW, vpH, &texW, &texH);
if (!tex || texW <= 0 || texH <= 0) return;
// Compute UVs: map screen-space rect to texture coordinates.
// The tiled texture covers the entire viewport, so UV = screenPos / vpSize.
// Subtract viewport origin for multi-viewport support.
float u0 = (pMin.x - vp->Pos.x) / (float)texW;
float v0 = (pMin.y - vp->Pos.y) / (float)texH;
float u1 = (pMax.x - vp->Pos.x) / (float)texW;
float v1 = (pMax.y - vp->Pos.y) / (float)texH;
dl->AddImage(tex, pMin, pMax, ImVec2(u0, v0), ImVec2(u1, v1), tintColor);
}
} // namespace util
} // namespace dragonx

35
src/util/noise_texture.h Normal file
View File

@@ -0,0 +1,35 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "imgui.h"
namespace dragonx {
namespace util {
// Returns a cached noise texture (created on first call).
// The texture is a 256x256 tileable white-noise pattern with
// per-pixel random alpha (0-255) designed to be overlaid at very
// low opacity to give cards a subtle grain / paper texture.
//
// texSize receives 256 (the width/height of the texture).
ImTextureID GetNoiseTexture(int* texSize = nullptr);
// Returns a viewport-sized pre-tiled noise texture (single draw call).
// The 256x256 base noise is tiled across the given viewport dimensions.
// Re-creates the texture when the viewport size changes.
// outW/outH receive the actual texture dimensions.
ImTextureID GetTiledNoiseTexture(int viewportW, int viewportH,
int* outW = nullptr, int* outH = nullptr);
// Draw a tiled noise overlay in a single AddImage call.
// Uses the pre-tiled viewport-sized texture — call this instead of
// manually looping with AddImage for each tile.
// dl: draw list, pMin/pMax: rect to cover, tintColor: alpha-premultiplied tint.
void DrawTiledNoiseRect(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax,
ImU32 tintColor);
} // namespace util
} // namespace dragonx

147
src/util/payment_uri.cpp Normal file
View File

@@ -0,0 +1,147 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "payment_uri.h"
#include <sstream>
#include <algorithm>
#include <cctype>
namespace dragonx {
namespace util {
std::string urlDecode(const std::string& encoded)
{
std::string result;
result.reserve(encoded.size());
for (size_t i = 0; i < encoded.size(); i++) {
if (encoded[i] == '%' && i + 2 < encoded.size()) {
// Decode hex pair
char hex[3] = { encoded[i + 1], encoded[i + 2], '\0' };
char* end;
long val = strtol(hex, &end, 16);
if (end == hex + 2) {
result += static_cast<char>(val);
i += 2;
continue;
}
} else if (encoded[i] == '+') {
result += ' ';
continue;
}
result += encoded[i];
}
return result;
}
bool isPaymentURI(const std::string& str)
{
// Check for drgx: or hush: prefix (case insensitive)
std::string lower = str;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
return lower.substr(0, 5) == "drgx:" || lower.substr(0, 5) == "hush:";
}
PaymentURI parsePaymentURI(const std::string& uri)
{
PaymentURI result;
result.valid = false;
// Check prefix
if (!isPaymentURI(uri)) {
result.error = "Invalid URI scheme. Expected drgx: or hush:";
return result;
}
// Skip the scheme prefix (drgx: or hush:)
size_t schemeEnd = uri.find(':');
if (schemeEnd == std::string::npos) {
result.error = "Invalid URI format";
return result;
}
std::string remainder = uri.substr(schemeEnd + 1);
// Handle double-slash format (drgx://address)
if (remainder.size() >= 2 && remainder[0] == '/' && remainder[1] == '/') {
remainder = remainder.substr(2);
}
// Split address and query string
size_t queryStart = remainder.find('?');
if (queryStart == std::string::npos) {
// No query parameters, just the address
result.address = remainder;
} else {
result.address = remainder.substr(0, queryStart);
std::string queryString = remainder.substr(queryStart + 1);
// Parse query parameters
std::istringstream queryStream(queryString);
std::string param;
while (std::getline(queryStream, param, '&')) {
size_t eqPos = param.find('=');
if (eqPos == std::string::npos) continue;
std::string key = param.substr(0, eqPos);
std::string value = urlDecode(param.substr(eqPos + 1));
// Convert key to lowercase for case-insensitive matching
std::transform(key.begin(), key.end(), key.begin(), ::tolower);
if (key == "amount" || key == "amt") {
try {
result.amount = std::stod(value);
} catch (...) {
// Invalid amount, keep as 0
}
} else if (key == "label") {
result.label = value;
} else if (key == "memo") {
result.memo = value;
} else if (key == "message" || key == "msg") {
result.message = value;
}
}
}
// Validate address
if (result.address.empty()) {
result.error = "No address specified in URI";
return result;
}
// Basic address format check
bool validFormat = false;
// z-address: starts with 'zs' and is 78+ chars
if (result.address[0] == 'z' && result.address.size() >= 78) {
validFormat = true;
}
// t-address: starts with 'R' (DragonX) or 't' (HUSH) and is ~34 chars
else if ((result.address[0] == 'R' || result.address[0] == 't') &&
result.address.size() >= 26 && result.address.size() <= 36) {
validFormat = true;
}
if (!validFormat) {
result.error = "Invalid address format";
return result;
}
// Validate amount if present
if (result.amount < 0) {
result.error = "Invalid negative amount";
return result;
}
result.valid = true;
return result;
}
} // namespace util
} // namespace dragonx

51
src/util/payment_uri.h Normal file
View File

@@ -0,0 +1,51 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
namespace util {
/**
* @brief Parsed payment URI data
*/
struct PaymentURI {
std::string address;
double amount = 0.0;
std::string label;
std::string memo;
std::string message;
bool valid = false;
std::string error;
};
/**
* @brief Parse a DragonX/HUSH payment URI
*
* Supports format: drgx:<address>?amount=<amount>&label=<label>&memo=<memo>&message=<message>
* Also supports hush: prefix for backwards compatibility
*
* @param uri The payment URI to parse
* @return PaymentURI struct with parsed data
*/
PaymentURI parsePaymentURI(const std::string& uri);
/**
* @brief URL decode a string
* @param encoded URL-encoded string
* @return Decoded string
*/
std::string urlDecode(const std::string& encoded);
/**
* @brief Check if a string is a valid payment URI
* @param str String to check
* @return true if it starts with drgx: or hush:
*/
bool isPaymentURI(const std::string& str);
} // namespace util
} // namespace dragonx

199
src/util/perf_log.h Normal file
View File

@@ -0,0 +1,199 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <chrono>
#include <fstream>
#include <mutex>
#include <vector>
#include <unordered_map>
#include <cstdint>
#include <algorithm>
#include <cstdio>
#include "../util/logger.h"
namespace dragonx {
namespace util {
/**
* @brief Lightweight per-frame performance profiler that writes periodic
* summaries to a log file.
*
* Usage:
* PerfLog::instance().beginFrame();
* { PERF_SCOPE("EventPoll"); ... }
* { PERF_SCOPE("AppUpdate"); ... }
* { PERF_SCOPE("AppRender"); ... }
* PerfLog::instance().endFrame();
*
* Every N frames (default 300 ≈ 5s at 60fps) it appends a summary block
* to <ObsidianDragonDir>/perf.log with min/avg/max/p95 for each zone.
*/
class PerfLog {
public:
static PerfLog& instance() {
static PerfLog s;
return s;
}
/// Open (or re-open) the log file. Called once at startup.
bool init(const std::string& path, int flushIntervalFrames = 300) {
std::lock_guard<std::mutex> lk(mutex_);
flushInterval_ = flushIntervalFrames;
file_.open(path, std::ios::out | std::ios::app);
if (!file_.is_open()) {
DEBUG_LOGF("[PerfLog] Failed to open %s\n", path.c_str());
return false;
}
DEBUG_LOGF("[PerfLog] Logging to %s (flush every %d frames)\n",
path.c_str(), flushInterval_);
// Header
file_ << "\n========== PerfLog started ==========\n";
file_.flush();
ready_ = true;
return true;
}
/// Call at the very start of the frame.
void beginFrame() {
if (!ready_) return;
frameStart_ = Clock::now();
}
/// Call at the very end of the frame (after present/swap).
void endFrame() {
if (!ready_) return;
double totalUs = usFrom(frameStart_);
record("FRAME_TOTAL", totalUs);
frameCount_++;
if (frameCount_ >= flushInterval_) {
flush();
frameCount_ = 0;
}
}
/// Begin a named zone. Returns an opaque time point.
std::chrono::steady_clock::time_point beginZone() {
return Clock::now();
}
/// End a named zone. Pass the time point from beginZone().
void endZone(const char* name, std::chrono::steady_clock::time_point start) {
if (!ready_) return;
record(name, usFrom(start));
}
private:
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
PerfLog() = default;
~PerfLog() {
if (ready_ && frameCount_ > 0) flush();
}
PerfLog(const PerfLog&) = delete;
PerfLog& operator=(const PerfLog&) = delete;
static double usFrom(TimePoint start) {
return std::chrono::duration<double, std::micro>(Clock::now() - start).count();
}
/// Record a single timing sample (microseconds).
void record(const char* name, double us) {
auto& bucket = buckets_[name];
bucket.samples.push_back(us);
}
/// Write accumulated stats and reset.
void flush() {
std::lock_guard<std::mutex> lk(mutex_);
if (!file_.is_open()) return;
auto now = std::chrono::system_clock::now();
auto tt = std::chrono::system_clock::to_time_t(now);
char timeBuf[64];
std::strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", std::localtime(&tt));
file_ << "\n--- " << timeBuf << " (" << frameCount_ << " frames) ---\n";
// Sort zone names for consistent output
std::vector<std::string> names;
names.reserve(buckets_.size());
for (auto& kv : buckets_) names.push_back(kv.first);
std::sort(names.begin(), names.end());
for (auto& name : names) {
auto& b = buckets_[name];
if (b.samples.empty()) continue;
std::sort(b.samples.begin(), b.samples.end());
size_t n = b.samples.size();
double sum = 0;
for (double v : b.samples) sum += v;
double avg = sum / n;
double mn = b.samples.front();
double mx = b.samples.back();
double p95 = b.samples[std::min(n - 1, (size_t)(n * 0.95))];
double p99 = b.samples[std::min(n - 1, (size_t)(n * 0.99))];
char line[256];
std::snprintf(line, sizeof(line),
" %-28s min=%7.0f avg=%7.0f p95=%7.0f p99=%7.0f max=%7.0f us\n",
name.c_str(), mn, avg, p95, p99, mx);
file_ << line;
}
// FPS summary from FRAME_TOTAL
auto it = buckets_.find("FRAME_TOTAL");
if (it != buckets_.end() && !it->second.samples.empty()) {
auto& s = it->second.samples;
double avgFrame = 0;
for (double v : s) avgFrame += v;
avgFrame /= s.size();
double fps = (avgFrame > 0) ? 1000000.0 / avgFrame : 0;
char fpsLine[128];
std::snprintf(fpsLine, sizeof(fpsLine),
" FPS: %.1f (avg frame %.1f us = %.2f ms)\n",
fps, avgFrame, avgFrame / 1000.0);
file_ << fpsLine;
}
file_.flush();
// Reset all buckets
for (auto& kv : buckets_) kv.second.samples.clear();
}
struct Bucket {
std::vector<double> samples; // microseconds
};
std::mutex mutex_;
std::ofstream file_;
bool ready_ = false;
int flushInterval_ = 300;
int frameCount_ = 0;
TimePoint frameStart_;
std::unordered_map<std::string, Bucket> buckets_;
};
/// RAII scope timer — records duration under a named zone.
struct PerfScope {
const char* name;
std::chrono::steady_clock::time_point start;
PerfScope(const char* n) : name(n), start(PerfLog::instance().beginZone()) {}
~PerfScope() { PerfLog::instance().endZone(name, start); }
};
#define PERF_SCOPE(name) ::dragonx::util::PerfScope _perf_##__LINE__(name)
// Manual begin/end for zones that span across scopes
#define PERF_BEGIN(var) auto var = ::dragonx::util::PerfLog::instance().beginZone()
#define PERF_END(name, var) ::dragonx::util::PerfLog::instance().endZone(name, var)
} // namespace util
} // namespace dragonx

600
src/util/platform.cpp Normal file
View File

@@ -0,0 +1,600 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "platform.h"
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <sstream>
#include <filesystem>
// Embedded ui.toml (generated at build time)
#if __has_include("ui_toml_embedded.h")
#include "ui_toml_embedded.h"
#define HAS_EMBEDDED_UI_TOML 1
#else
#define HAS_EMBEDDED_UI_TOML 0
#endif
#include <sys/stat.h>
#ifdef _WIN32
#include <windows.h>
#include <shellapi.h>
#include <shlobj.h>
#include <psapi.h>
#include <tlhelp32.h>
#else
#include <unistd.h>
#include <pwd.h>
#include <dirent.h>
#ifdef __APPLE__
#include <mach-o/dyld.h>
#include <sys/sysctl.h>
#include <mach/mach.h>
#endif
#endif
#include "../util/logger.h"
namespace dragonx {
namespace util {
bool Platform::openUrl(const std::string& url)
{
if (url.empty()) return false;
#ifdef _WIN32
// Windows: Use ShellExecute
HINSTANCE result = ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
return (reinterpret_cast<intptr_t>(result) > 32);
#elif defined(__APPLE__)
// macOS: Use 'open' command
std::string cmd = "open \"" + url + "\" &";
return (system(cmd.c_str()) == 0);
#else
// Linux: Use xdg-open
std::string cmd = "xdg-open \"" + url + "\" >/dev/null 2>&1 &";
return (system(cmd.c_str()) == 0);
#endif
}
bool Platform::openFolder(const std::string& path, bool createIfMissing)
{
if (path.empty()) return false;
// Create directory if it doesn't exist
if (createIfMissing) {
#ifdef _WIN32
// Windows: Create directory recursively
std::string cmd = "mkdir \"" + path + "\" 2>nul";
(void)system(cmd.c_str()); // Ignore return value - dir may already exist
#else
// Linux/macOS: Create directory with parents
std::string cmd = "mkdir -p \"" + path + "\" 2>/dev/null";
(void)system(cmd.c_str()); // Ignore return value - dir may already exist
#endif
}
#ifdef _WIN32
// Windows: Use explorer.exe to open folder
HINSTANCE result = ShellExecuteA(nullptr, "explore", path.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
return (reinterpret_cast<intptr_t>(result) > 32);
#elif defined(__APPLE__)
// macOS: Use 'open' command (works for folders too)
std::string cmd = "open \"" + path + "\" &";
return (system(cmd.c_str()) == 0);
#else
// Linux: Use xdg-open (works for folders too)
std::string cmd = "xdg-open \"" + path + "\" >/dev/null 2>&1 &";
return (system(cmd.c_str()) == 0);
#endif
}
uint64_t Platform::getFileSize(const std::string& path)
{
struct stat st;
if (stat(path.c_str(), &st) == 0) {
return static_cast<uint64_t>(st.st_size);
}
return 0;
}
std::string Platform::formatFileSize(uint64_t bytes)
{
char buf[64];
if (bytes >= 1024ULL * 1024ULL * 1024ULL) {
snprintf(buf, sizeof(buf), "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
} else if (bytes >= 1024ULL * 1024ULL) {
snprintf(buf, sizeof(buf), "%.2f MB", bytes / (1024.0 * 1024.0));
} else if (bytes >= 1024ULL) {
snprintf(buf, sizeof(buf), "%.2f KB", bytes / 1024.0);
} else {
snprintf(buf, sizeof(buf), "%llu bytes", static_cast<unsigned long long>(bytes));
}
return std::string(buf);
}
std::string Platform::getHomeDir()
{
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_PROFILE, nullptr, 0, path))) {
return std::string(path);
}
// Fallback
const char* home = getenv("USERPROFILE");
return home ? home : "C:\\";
#else
const char* home = getenv("HOME");
if (home) return home;
// Fallback to passwd entry
struct passwd* pw = getpwuid(getuid());
if (pw && pw->pw_dir) return pw->pw_dir;
return "/tmp";
#endif
}
std::string Platform::getDragonXDataDir()
{
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) {
return std::string(path) + "\\Hush\\DRAGONX\\";
}
return getHomeDir() + "\\AppData\\Roaming\\Hush\\DRAGONX\\";
#else
return getHomeDir() + "/.hush/DRAGONX/";
#endif
}
std::string Platform::getDataDir()
{
return getDragonXDataDir();
}
std::string Platform::getConfigDir()
{
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(nullptr, CSIDL_APPDATA, nullptr, 0, path))) {
return std::string(path) + "\\ObsidianDragon\\";
}
return getHomeDir() + "\\AppData\\Roaming\\ObsidianDragon\\";
#else
return getHomeDir() + "/.config/ObsidianDragon/";
#endif
}
bool Platform::deleteFile(const std::string& path)
{
#ifdef _WIN32
return DeleteFileA(path.c_str()) != 0 || GetLastError() == ERROR_FILE_NOT_FOUND;
#else
return (remove(path.c_str()) == 0 || errno == ENOENT);
#endif
}
std::string Platform::getExecutableDirectory()
{
#ifdef _WIN32
char path[MAX_PATH];
DWORD len = GetModuleFileNameA(nullptr, path, MAX_PATH);
if (len > 0 && len < MAX_PATH) {
std::string fullPath(path);
size_t pos = fullPath.find_last_of("\\/");
if (pos != std::string::npos) {
return fullPath.substr(0, pos);
}
}
return ".";
#elif defined(__APPLE__)
char path[1024];
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) == 0) {
char* resolved = realpath(path, nullptr);
if (resolved) {
std::string fullPath(resolved);
free(resolved);
size_t pos = fullPath.find_last_of('/');
if (pos != std::string::npos) {
return fullPath.substr(0, pos);
}
}
}
return ".";
#else
// Linux: read /proc/self/exe
char path[1024];
ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1);
if (len > 0) {
path[len] = '\0';
std::string fullPath(path);
size_t pos = fullPath.find_last_of('/');
if (pos != std::string::npos) {
return fullPath.substr(0, pos);
}
}
return ".";
#endif
}
std::string getExecutableDirectory()
{
return Platform::getExecutableDirectory();
}
std::string Platform::getObsidianDragonDir()
{
#ifdef _WIN32
const char* appdata = std::getenv("APPDATA");
if (appdata) {
return (std::filesystem::path(appdata) / "ObsidianDragon").string();
}
return (std::filesystem::path(getHomeDir()) / "AppData" / "Roaming" / "ObsidianDragon").string();
#elif defined(__APPLE__)
return (std::filesystem::path(getHomeDir()) / "Library" / "Application Support" / "ObsidianDragon").string();
#else
const char* xdg_config = std::getenv("XDG_CONFIG_HOME");
if (xdg_config) {
return (std::filesystem::path(xdg_config) / "ObsidianDragon").string();
}
return (std::filesystem::path(getHomeDir()) / ".config" / "ObsidianDragon").string();
#endif
}
void Platform::ensureObsidianDragonSetup()
{
namespace fs = std::filesystem;
fs::path base = getObsidianDragonDir();
fs::path themes_dir = base / "themes";
fs::path example_dir = base / "example" / "example_theme";
fs::path example_img = example_dir / "img";
// Create directory structure
std::error_code ec;
fs::create_directories(themes_dir, ec);
fs::create_directories(example_img, ec);
if (ec) {
DEBUG_LOGF("Warning: Could not create ObsidianDragon directories: %s\n", ec.message().c_str());
return;
}
DEBUG_LOGF("ObsidianDragon: Config directory: %s\n", base.string().c_str());
// Write example theme.toml — a full copy of ui.toml so users can see
// and modify the complete layout+theme structure. UISchema can load
// themes from TOML format.
// Regenerate whenever the binary is newer than the example file, so
// layout changes in ui.toml are always reflected.
fs::path example_toml = example_dir / "theme.toml";
bool needsWrite = !fs::exists(example_toml);
if (!needsWrite) {
// Regenerate if the running binary is newer than the example file
fs::path exePath = fs::path(getExecutableDirectory()) /
#ifdef _WIN32
"ObsidianDragon.exe";
#else
"ObsidianDragon";
#endif
std::error_code tec;
auto exeTime = fs::last_write_time(exePath, tec);
auto fileTime = fs::last_write_time(example_toml, tec);
if (!tec && exeTime > fileTime)
needsWrite = true;
}
if (needsWrite) {
std::ofstream f(example_toml);
if (f.is_open()) {
// Use the build-time embedded copy first; fall back to the
// bundled file on disk.
std::string content;
#if HAS_EMBEDDED_UI_TOML
content.assign(reinterpret_cast<const char*>(embedded::ui_toml_data),
embedded::ui_toml_size);
#else
{
fs::path bundled = fs::path(getExecutableDirectory()) / "res" / "themes" / "ui.toml";
std::ifstream src(bundled);
if (src.is_open()) {
std::ostringstream ss;
ss << src.rdbuf();
content = ss.str();
}
}
#endif
if (!content.empty()) {
// Patch the theme metadata so the example is clearly
// distinguishable from the bundled default.
// In TOML: name = "DragonX" → name = "Example Theme"
auto replaceFirst = [&](const std::string& key,
const std::string& oldVal,
const std::string& newVal) {
// Find 'key = "oldVal"' pattern and replace oldVal
std::string needle = key + " = ";
auto pos = content.find(needle);
if (pos == std::string::npos) return;
auto vpos = content.find(oldVal, pos);
if (vpos != std::string::npos && vpos - pos < 80)
content.replace(vpos, oldVal.size(), newVal);
};
replaceFirst("name", "\"DragonX\"", "\"Example Theme\"");
replaceFirst("author", "\"The Hush Developers\"", "\"Your Name\"");
// Prepend comment block (TOML uses # for comments natively)
std::string header =
"# === DragonX Wallet Theme File ===\n"
"# Copy this entire folder into the 'themes' folder to use it.\n"
"# The folder name becomes the theme ID.\n"
"#\n"
"# === Images ===\n"
"# Place image files in the 'img' subfolder.\n"
"# Specify custom filenames below, or use the defaults:\n"
"# backgrounds/gradient/dark_gradient.png - background gradient overlay\n"
"# logos/logo_ObsidianDragon_dark.png - sidebar logo (128x128 recommended)\n"
"#\n"
"# === Colors ===\n"
"# Colors use CSS-style formats: #RRGGBB, #RRGGBBAA, or rgba(r,g,b,a)\n"
"# Palette variables can be referenced elsewhere with var(--name)\n\n";
content = header + content;
} else {
DEBUG_LOGF("ObsidianDragon: Warning — ui.toml not available, skipping example theme\n");
}
f << content;
f.close();
DEBUG_LOGF("ObsidianDragon: Created example theme: %s\n", example_toml.string().c_str());
}
}
// Copy bundled images into example/example_theme/img/ for reference
fs::path exe_dir = getExecutableDirectory();
fs::path res_img = exe_dir / "res" / "img";
auto copyIfMissing = [&](const char* srcRelPath, const char* dstFilename) {
fs::path src = res_img / srcRelPath;
fs::path dst = example_img / dstFilename;
if (fs::exists(src) && !fs::exists(dst)) {
std::error_code cpec;
fs::copy_file(src, dst, cpec);
if (!cpec) {
DEBUG_LOGF("ObsidianDragon: Copied %s to example theme\n", dstFilename);
}
}
};
copyIfMissing("backgrounds/gradient/dark_gradient.png", "dark_gradient.png");
copyIfMissing("logos/logo_ObsidianDragon_dark.png", "logo_ObsidianDragon_dark.png");
copyIfMissing("logos/logo_ObsidianDragon_light.png", "logo_ObsidianDragon_light.png");
}
double Platform::getTotalSystemRAM_MB()
{
static double cached = 0.0;
if (cached > 0.0) return cached;
#ifdef _WIN32
MEMORYSTATUSEX memInfo;
memInfo.dwLength = sizeof(memInfo);
if (GlobalMemoryStatusEx(&memInfo)) {
cached = (double)memInfo.ullTotalPhys / (1024.0 * 1024.0);
}
#elif defined(__APPLE__)
int64_t memSize = 0;
size_t len = sizeof(memSize);
if (sysctlbyname("hw.memsize", &memSize, &len, nullptr, 0) == 0) {
cached = (double)memSize / (1024.0 * 1024.0);
}
#else
// Linux: use sysconf
long pages = sysconf(_SC_PHYS_PAGES);
long pageSize = sysconf(_SC_PAGE_SIZE);
if (pages > 0 && pageSize > 0) {
cached = (double)pages * (double)pageSize / (1024.0 * 1024.0);
}
#endif
return cached;
}
double Platform::getUsedSystemRAM_MB()
{
#ifdef _WIN32
MEMORYSTATUSEX memInfo;
memInfo.dwLength = sizeof(memInfo);
if (GlobalMemoryStatusEx(&memInfo)) {
double total = (double)memInfo.ullTotalPhys / (1024.0 * 1024.0);
double avail = (double)memInfo.ullAvailPhys / (1024.0 * 1024.0);
return total - avail;
}
return 0.0;
#elif defined(__APPLE__)
// macOS: vm_statistics64 for free/active/inactive/wired pages
vm_size_t pageSize = 0;
host_page_size(mach_host_self(), &pageSize);
vm_statistics64_data_t vmStats;
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
if (host_statistics64(mach_host_self(), HOST_VM_INFO64,
(host_info64_t)&vmStats, &count) == KERN_SUCCESS) {
// "used" = total - free - inactive (matches Activity Monitor "Memory Used")
double total = getTotalSystemRAM_MB();
double freeMB = (double)(vmStats.free_count + vmStats.inactive_count) * (double)pageSize / (1024.0 * 1024.0);
return total - freeMB;
}
return 0.0;
#else
// Linux: parse /proc/meminfo for MemAvailable
FILE* f = fopen("/proc/meminfo", "r");
if (!f) return 0.0;
double totalMB = 0.0, availMB = 0.0;
char line[256];
int found = 0;
while (fgets(line, sizeof(line), f) && found < 2) {
long val = 0;
if (strncmp(line, "MemTotal:", 9) == 0) {
if (sscanf(line + 9, "%ld", &val) == 1) { totalMB = (double)val / 1024.0; found++; }
} else if (strncmp(line, "MemAvailable:", 13) == 0) {
if (sscanf(line + 13, "%ld", &val) == 1) { availMB = (double)val / 1024.0; found++; }
}
}
fclose(f);
if (found == 2) return totalMB - availMB;
return 0.0;
#endif
}
double Platform::getSelfMemoryUsageMB()
{
#ifdef _WIN32
PROCESS_MEMORY_COUNTERS pmc;
if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
return (double)pmc.WorkingSetSize / (1024.0 * 1024.0);
}
return 0.0;
#elif defined(__APPLE__)
struct mach_task_basic_info info;
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO,
(task_info_t)&info, &count) == KERN_SUCCESS) {
return (double)info.resident_size / (1024.0 * 1024.0);
}
return 0.0;
#else
// Linux: read /proc/self/status for VmRSS
FILE* f = fopen("/proc/self/status", "r");
if (!f) return 0.0;
char line[256];
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, "VmRSS:", 6) == 0) {
long val = 0;
if (sscanf(line + 6, "%ld", &val) == 1) {
fclose(f);
return (double)val / 1024.0; // kB -> MB
}
}
}
fclose(f);
return 0.0;
#endif
}
double Platform::getDaemonMemoryUsageMB()
{
#ifdef _WIN32
// Windows: use CreateToolhelp32Snapshot to find hushd.exe processes
// and query their working set size.
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return 0.0;
PROCESSENTRY32 entry;
entry.dwSize = sizeof(entry);
double totalMB = 0.0;
if (Process32First(snap, &entry)) {
do {
if (_stricmp(entry.szExeFile, "hushd.exe") == 0) {
// GetProcessMemoryInfo requires PROCESS_QUERY_INFORMATION + PROCESS_VM_READ.
// Try full access first; fall back to limited for elevated processes.
HANDLE hProc = OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE, entry.th32ProcessID);
if (!hProc) {
hProc = OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION,
FALSE, entry.th32ProcessID);
}
if (hProc) {
PROCESS_MEMORY_COUNTERS pmc;
ZeroMemory(&pmc, sizeof(pmc));
pmc.cb = sizeof(pmc);
if (GetProcessMemoryInfo(hProc, &pmc, sizeof(pmc)))
totalMB += (double)pmc.WorkingSetSize / (1024.0 * 1024.0);
CloseHandle(hProc);
}
}
} while (Process32Next(snap, &entry));
}
CloseHandle(snap);
return totalMB;
#elif defined(__APPLE__)
// macOS: use pgrep to find hushd PIDs, then read RSS via proc_pidinfo
FILE* fp = popen("pgrep -x hushd", "r");
if (!fp) return 0.0;
double totalMB = 0.0;
char line[64];
while (fgets(line, sizeof(line), fp)) {
pid_t pid = (pid_t)atoi(line);
if (pid <= 0) continue;
struct mach_task_basic_info info;
mach_msg_type_number_t cnt = MACH_TASK_BASIC_INFO_COUNT;
// We can't directly task_for_pid without entitlements, so fall back to ps
char cmd[128];
snprintf(cmd, sizeof(cmd), "ps -o rss= -p %d", pid);
FILE* ps = popen(cmd, "r");
if (ps) {
char rssLine[64];
if (fgets(rssLine, sizeof(rssLine), ps)) {
long rssKB = atol(rssLine);
if (rssKB > 0) totalMB += (double)rssKB / 1024.0;
}
pclose(ps);
}
}
pclose(fp);
return totalMB;
#else
// Linux: iterate /proc/<pid>/comm looking for "hushd"
DIR* procDir = opendir("/proc");
if (!procDir) return 0.0;
double totalMB = 0.0;
pid_t selfPid = getpid();
struct dirent* entry;
while ((entry = readdir(procDir)) != nullptr) {
// Only numeric directories (PIDs)
if (entry->d_name[0] < '0' || entry->d_name[0] > '9') continue;
pid_t pid = (pid_t)atoi(entry->d_name);
if (pid <= 0 || pid == selfPid) continue;
// Read comm (process name)
char commPath[64];
snprintf(commPath, sizeof(commPath), "/proc/%d/comm", pid);
FILE* cf = fopen(commPath, "r");
if (!cf) continue;
char comm[256] = {};
if (fgets(comm, sizeof(comm), cf)) {
// Strip trailing newline
size_t len = strlen(comm);
if (len > 0 && comm[len - 1] == '\n') comm[len - 1] = '\0';
}
fclose(cf);
if (strcmp(comm, "hushd") != 0) continue;
// Read VmRSS from /proc/<pid>/status
char statusPath[64];
snprintf(statusPath, sizeof(statusPath), "/proc/%d/status", pid);
FILE* sf = fopen(statusPath, "r");
if (!sf) continue;
char sline[256];
while (fgets(sline, sizeof(sline), sf)) {
if (strncmp(sline, "VmRSS:", 6) == 0) {
long val = 0;
if (sscanf(sline + 6, "%ld", &val) == 1)
totalMB += (double)val / 1024.0; // kB -> MB
break;
}
}
fclose(sf);
}
closedir(procDir);
return totalMB;
#endif
}
} // namespace util
} // namespace dragonx

135
src/util/platform.h Normal file
View File

@@ -0,0 +1,135 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <cstdint>
namespace dragonx {
namespace util {
/**
* @brief Platform-specific utilities
*/
class Platform {
public:
/**
* @brief Open a URL in the default browser
* @param url The URL to open
* @return true if successful
*/
static bool openUrl(const std::string& url);
/**
* @brief Open a folder in the system file manager
* @param path Path to the folder
* @param createIfMissing Create the folder if it doesn't exist
* @return true if successful
*/
static bool openFolder(const std::string& path, bool createIfMissing = true);
/**
* @brief Get the size of a file in bytes
* @param path Path to the file
* @return File size in bytes, or 0 if file doesn't exist
*/
static uint64_t getFileSize(const std::string& path);
/**
* @brief Format a file size as human-readable string
* @param bytes Size in bytes
* @return Formatted string (e.g., "5.23 MB")
*/
static std::string formatFileSize(uint64_t bytes);
/**
* @brief Get the user's home directory
* @return Home directory path
*/
static std::string getHomeDir();
/**
* @brief Get the DragonX data directory
* @return Path like ~/.hush/DRAGONX/ or %APPDATA%\Hush\DRAGONX\
*/
static std::string getDragonXDataDir();
/**
* @brief Get the wallet data directory (alias for getDragonXDataDir)
* @return Path like ~/.hush/DRAGONX/ or %APPDATA%\Hush\DRAGONX\
*/
static std::string getDataDir();
/**
* @brief Get the config directory for storing wallet exports/backups
* @return Path like ~/.config/ObsidianDragon/ or %APPDATA%\ObsidianDragon\
*/
static std::string getConfigDir();
/**
* @brief Delete a file
* @param path Path to the file
* @return true if successful or file didn't exist
*/
static bool deleteFile(const std::string& path);
/**
* @brief Get the directory containing the executable
* @return Path to executable's directory
*/
static std::string getExecutableDirectory();
/**
* @brief Get the ObsidianDragon config directory
* @return Path like ~/.config/ObsidianDragon/ or %APPDATA%\ObsidianDragon\
*/
static std::string getObsidianDragonDir();
/**
* @brief Create ObsidianDragon folder structure and template files on first run
*
* Creates:
* ObsidianDragon/
* ObsidianDragon/themes/
* ObsidianDragon/themes/my_theme.toml (template)
*
* Only writes template files if they don't already exist.
*/
static void ensureObsidianDragonSetup();
/**
* @brief Get total system RAM in megabytes
* @return Total physical RAM in MB, or 0 on failure
*/
static double getTotalSystemRAM_MB();
/**
* @brief Get currently used system RAM in megabytes
* @return Used physical RAM in MB (total - available), or 0 on failure
*/
static double getUsedSystemRAM_MB();
/**
* @brief Get this process's own RSS (resident set size) in megabytes
* @return Self process RSS in MB, or 0 on failure
*/
static double getSelfMemoryUsageMB();
/**
* @brief Get total RSS of all hushd daemon processes in megabytes
* Scans for any running hushd process by name, regardless of how it was launched.
* @return Combined daemon RSS in MB, or 0 if no daemon found
*/
static double getDaemonMemoryUsageMB();
};
/**
* @brief Get the directory containing the executable (free function)
* @return Path to executable's directory
*/
std::string getExecutableDirectory();
} // namespace util
} // namespace dragonx

249
src/util/secure_vault.cpp Normal file
View File

@@ -0,0 +1,249 @@
// ObsidianDragon Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "secure_vault.h"
#include "platform.h"
#include <sodium.h>
#include <fstream>
#include <filesystem>
#include <cstring>
#include <algorithm>
#include "../util/logger.h"
namespace fs = std::filesystem;
namespace dragonx {
namespace util {
// Vault file format version
static constexpr uint8_t VAULT_VERSION = 0x01;
// Argon2id parameters — moderate: ~256 MB, ~3 ops
// Tuned for short PINs to make brute-force costly
static constexpr unsigned long long ARGON2_MEMLIMIT = crypto_pwhash_MEMLIMIT_MODERATE;
static constexpr unsigned long long ARGON2_OPSLIMIT = crypto_pwhash_OPSLIMIT_MODERATE;
SecureVault::SecureVault() {
// Ensure libsodium is initialized
if (sodium_init() < 0) {
// sodium_init returns 0 on success, 1 if already initialized, -1 on failure
// We'll proceed anyway — the functions will fail gracefully
}
}
SecureVault::~SecureVault() = default;
std::string SecureVault::getVaultPath() {
return (fs::path(Platform::getConfigDir()) / "vault.dat").string();
}
bool SecureVault::hasVault() const {
std::error_code ec;
return fs::exists(getVaultPath(), ec);
}
bool SecureVault::isValidPin(const std::string& pin) {
if (pin.size() < 4 || pin.size() > 8) return false;
return std::all_of(pin.begin(), pin.end(), ::isdigit);
}
bool SecureVault::deriveKey(const std::string& pin,
const uint8_t* salt, size_t saltLen,
uint8_t* key, size_t keyLen) const {
if (saltLen < crypto_pwhash_SALTBYTES) return false;
int rc = crypto_pwhash(
key, keyLen,
pin.c_str(), pin.size(),
salt,
ARGON2_OPSLIMIT,
ARGON2_MEMLIMIT,
crypto_pwhash_ALG_ARGON2ID13
);
return rc == 0;
}
void SecureVault::secureZero(void* ptr, size_t len) {
sodium_memzero(ptr, len);
}
bool SecureVault::store(const std::string& pin, const std::string& passphrase) {
if (!isValidPin(pin)) {
DEBUG_LOGF("[SecureVault] Invalid PIN format\n");
return false;
}
if (passphrase.empty()) {
DEBUG_LOGF("[SecureVault] Empty passphrase\n");
return false;
}
// Generate random salt and nonce
uint8_t salt[crypto_pwhash_SALTBYTES];
uint8_t nonce[crypto_secretbox_NONCEBYTES];
randombytes_buf(salt, sizeof(salt));
randombytes_buf(nonce, sizeof(nonce));
// Derive key from PIN
uint8_t key[crypto_secretbox_KEYBYTES];
if (!deriveKey(pin, salt, sizeof(salt), key, sizeof(key))) {
DEBUG_LOGF("[SecureVault] Key derivation failed\n");
return false;
}
// Encrypt passphrase
size_t plainLen = passphrase.size();
size_t cipherLen = plainLen + crypto_secretbox_MACBYTES;
std::vector<uint8_t> ciphertext(cipherLen);
int rc = crypto_secretbox_easy(
ciphertext.data(),
reinterpret_cast<const uint8_t*>(passphrase.c_str()),
plainLen,
nonce,
key
);
// Wipe key immediately
secureZero(key, sizeof(key));
if (rc != 0) {
DEBUG_LOGF("[SecureVault] Encryption failed\n");
return false;
}
// Write vault file: version + salt + nonce + ciphertext
std::string vaultPath = getVaultPath();
fs::create_directories(fs::path(vaultPath).parent_path());
std::ofstream out(vaultPath, std::ios::binary);
if (!out.is_open()) {
DEBUG_LOGF("[SecureVault] Cannot create vault file: %s\n", vaultPath.c_str());
return false;
}
out.write(reinterpret_cast<const char*>(&VAULT_VERSION), 1);
out.write(reinterpret_cast<const char*>(salt), sizeof(salt));
out.write(reinterpret_cast<const char*>(nonce), sizeof(nonce));
out.write(reinterpret_cast<const char*>(ciphertext.data()), cipherLen);
out.close();
if (!out.good()) {
DEBUG_LOGF("[SecureVault] Write failed\n");
fs::remove(vaultPath);
return false;
}
DEBUG_LOGF("[SecureVault] Vault saved (%zu bytes ciphertext)\n", cipherLen);
return true;
}
bool SecureVault::retrieve(const std::string& pin, std::string& passphrase) const {
if (!isValidPin(pin)) return false;
std::string vaultPath = getVaultPath();
std::ifstream in(vaultPath, std::ios::binary);
if (!in.is_open()) return false;
// Read entire file
in.seekg(0, std::ios::end);
size_t fileSize = in.tellg();
in.seekg(0);
// Minimum size: 1 (version) + SALTBYTES + NONCEBYTES + MACBYTES + 1 (min plaintext)
size_t minSize = 1 + crypto_pwhash_SALTBYTES + crypto_secretbox_NONCEBYTES
+ crypto_secretbox_MACBYTES + 1;
if (fileSize < minSize) {
DEBUG_LOGF("[SecureVault] Vault file too small\n");
return false;
}
std::vector<uint8_t> data(fileSize);
in.read(reinterpret_cast<char*>(data.data()), fileSize);
in.close();
// Parse header
size_t offset = 0;
uint8_t version = data[offset++];
if (version != VAULT_VERSION) {
DEBUG_LOGF("[SecureVault] Unknown vault version: %d\n", version);
return false;
}
const uint8_t* salt = &data[offset];
offset += crypto_pwhash_SALTBYTES;
const uint8_t* nonce = &data[offset];
offset += crypto_secretbox_NONCEBYTES;
const uint8_t* ciphertext = &data[offset];
size_t cipherLen = fileSize - offset;
size_t plainLen = cipherLen - crypto_secretbox_MACBYTES;
// Derive key
uint8_t key[crypto_secretbox_KEYBYTES];
if (!deriveKey(pin, salt, crypto_pwhash_SALTBYTES, key, sizeof(key))) {
DEBUG_LOGF("[SecureVault] Key derivation failed during retrieve\n");
return false;
}
// Decrypt
std::vector<uint8_t> plaintext(plainLen);
int rc = crypto_secretbox_open_easy(
plaintext.data(),
ciphertext,
cipherLen,
nonce,
key
);
secureZero(key, sizeof(key));
if (rc != 0) {
// Wrong PIN or corrupted data
return false;
}
passphrase.assign(reinterpret_cast<const char*>(plaintext.data()), plainLen);
secureZero(plaintext.data(), plainLen);
return true;
}
bool SecureVault::changePin(const std::string& oldPin, const std::string& newPin) {
std::string passphrase;
if (!retrieve(oldPin, passphrase)) {
return false;
}
bool ok = store(newPin, passphrase);
secureZero(&passphrase[0], passphrase.size());
return ok;
}
bool SecureVault::removeVault() {
std::string vaultPath = getVaultPath();
std::error_code ec;
if (fs::exists(vaultPath, ec)) {
// Overwrite with zeros before removing (best-effort secure delete)
{
std::ifstream probe(vaultPath, std::ios::binary | std::ios::ate);
size_t sz = probe.is_open() ? (size_t)probe.tellg() : 0;
probe.close();
if (sz > 0) {
std::vector<uint8_t> zeros(sz, 0);
std::ofstream zap(vaultPath, std::ios::binary);
if (zap.is_open()) {
zap.write(reinterpret_cast<const char*>(zeros.data()), sz);
}
}
}
return fs::remove(vaultPath, ec);
}
return true; // Didn't exist — that's fine
}
} // namespace util
} // namespace dragonx

93
src/util/secure_vault.h Normal file
View File

@@ -0,0 +1,93 @@
// ObsidianDragon Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <vector>
#include <cstdint>
namespace dragonx {
namespace util {
/**
* @brief PIN-encrypted passphrase vault
*
* Stores the wallet passphrase encrypted with a PIN-derived key using
* libsodium's Argon2id (key derivation) + XSalsa20-Poly1305 (AEAD).
*
* File format (vault.dat):
* [1 byte] version (0x01)
* [16 bytes] Argon2id salt
* [24 bytes] XSalsa20 nonce
* [N bytes] ciphertext (passphrase + 16-byte Poly1305 tag)
*
* The PIN is 4-8 digits. Argon2id uses moderate ops/mem limits
* to resist brute-force on the small PIN space while keeping
* unlock time under ~1 second.
*/
class SecureVault {
public:
SecureVault();
~SecureVault();
/**
* @brief Check if a vault file exists on disk
*/
bool hasVault() const;
/**
* @brief Encrypt passphrase with PIN and save to vault.dat
* @param pin The user's PIN (4-8 digits)
* @param passphrase The wallet passphrase to protect
* @return true on success
*/
bool store(const std::string& pin, const std::string& passphrase);
/**
* @brief Decrypt passphrase from vault using PIN
* @param pin The user's PIN
* @param[out] passphrase The recovered passphrase
* @return true if PIN was correct and decryption succeeded
*/
bool retrieve(const std::string& pin, std::string& passphrase) const;
/**
* @brief Change the PIN (re-encrypt with new PIN)
* @param oldPin Current PIN
* @param newPin New PIN
* @return true if old PIN was valid and re-encryption succeeded
*/
bool changePin(const std::string& oldPin, const std::string& newPin);
/**
* @brief Remove the vault file
* @return true if deleted or didn't exist
*/
bool removeVault();
/**
* @brief Validate PIN format (4-8 digits)
*/
static bool isValidPin(const std::string& pin);
/**
* @brief Securely zero memory
*/
static void secureZero(void* ptr, size_t len);
/**
* @brief Get the vault file path
*/
static std::string getVaultPath();
private:
// Derive a 32-byte key from PIN + salt using Argon2id
bool deriveKey(const std::string& pin,
const uint8_t* salt, size_t saltLen,
uint8_t* key, size_t keyLen) const;
};
} // namespace util
} // namespace dragonx

View File

@@ -0,0 +1,136 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "single_instance.h"
#include <cstdio>
#include "../util/logger.h"
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#include <sys/file.h>
#include <fcntl.h>
#include <errno.h>
#include <cstring>
#endif
namespace dragonx {
namespace util {
SingleInstance::SingleInstance(const std::string& app_name)
: app_name_(app_name)
{
#ifndef _WIN32
// Construct lock file path
const char* tmpdir = getenv("TMPDIR");
if (!tmpdir) tmpdir = "/tmp";
lock_path_ = std::string(tmpdir) + "/" + app_name_ + ".lock";
#endif
}
SingleInstance::~SingleInstance()
{
unlock();
}
bool SingleInstance::tryLock()
{
if (locked_) {
return true; // Already locked by us
}
#ifdef _WIN32
// Windows: Use named mutex
std::string mutex_name = "Global\\" + app_name_;
mutex_handle_ = CreateMutexA(NULL, TRUE, mutex_name.c_str());
if (mutex_handle_ == NULL) {
DEBUG_LOGF("Failed to create mutex: %lu\n", GetLastError());
return false;
}
if (GetLastError() == ERROR_ALREADY_EXISTS) {
// Another instance is running
CloseHandle(mutex_handle_);
mutex_handle_ = nullptr;
DEBUG_LOGF("Another instance of %s is already running (mutex exists)\n", app_name_.c_str());
return false;
}
locked_ = true;
DEBUG_LOGF("Single instance lock acquired (mutex)\n");
return true;
#else
// Linux/macOS: Use file lock (flock)
lock_fd_ = open(lock_path_.c_str(), O_CREAT | O_RDWR, 0666);
if (lock_fd_ < 0) {
DEBUG_LOGF("Failed to open lock file %s: %s\n", lock_path_.c_str(), strerror(errno));
return false;
}
// Try non-blocking exclusive lock
if (flock(lock_fd_, LOCK_EX | LOCK_NB) != 0) {
if (errno == EWOULDBLOCK) {
// Another instance has the lock
DEBUG_LOGF("Another instance of %s is already running (lock file: %s)\n",
app_name_.c_str(), lock_path_.c_str());
} else {
DEBUG_LOGF("Failed to acquire lock: %s\n", strerror(errno));
}
close(lock_fd_);
lock_fd_ = -1;
return false;
}
// Write PID to lock file (useful for debugging)
char pid_str[32];
int len = snprintf(pid_str, sizeof(pid_str), "%d\n", getpid());
if (ftruncate(lock_fd_, 0) == 0) {
lseek(lock_fd_, 0, SEEK_SET);
ssize_t written = write(lock_fd_, pid_str, len);
(void)written; // Ignore result, not critical
}
locked_ = true;
DEBUG_LOGF("Single instance lock acquired (lock file: %s)\n", lock_path_.c_str());
return true;
#endif
}
void SingleInstance::unlock()
{
if (!locked_) {
return;
}
#ifdef _WIN32
if (mutex_handle_ != nullptr) {
ReleaseMutex(mutex_handle_);
CloseHandle(mutex_handle_);
mutex_handle_ = nullptr;
DEBUG_LOGF("Single instance lock released (mutex)\n");
}
#else
if (lock_fd_ >= 0) {
flock(lock_fd_, LOCK_UN);
close(lock_fd_);
lock_fd_ = -1;
// Remove the lock file
unlink(lock_path_.c_str());
DEBUG_LOGF("Single instance lock released (lock file removed)\n");
}
#endif
locked_ = false;
}
} // namespace util
} // namespace dragonx

View File

@@ -0,0 +1,55 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
namespace util {
/**
* @brief Ensures only one instance of the wallet runs at a time
*
* Uses file locking on Linux/macOS and named mutex on Windows.
*/
class SingleInstance {
public:
SingleInstance(const std::string& app_name = "dragonx-wallet");
~SingleInstance();
// Non-copyable
SingleInstance(const SingleInstance&) = delete;
SingleInstance& operator=(const SingleInstance&) = delete;
/**
* @brief Try to acquire the single instance lock
* @return true if lock acquired (this is the only instance)
*/
bool tryLock();
/**
* @brief Release the lock
*/
void unlock();
/**
* @brief Check if lock is currently held
*/
bool isLocked() const { return locked_; }
private:
std::string app_name_;
bool locked_ = false;
#ifdef _WIN32
void* mutex_handle_ = nullptr;
#else
int lock_fd_ = -1;
std::string lock_path_;
#endif
};
} // namespace util
} // namespace dragonx

326
src/util/texture_loader.cpp Normal file
View File

@@ -0,0 +1,326 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "texture_loader.h"
// stb_image — single-file image loader (public domain)
// Only compiled here; all other files just include the header.
#define STB_IMAGE_IMPLEMENTATION
#define STBI_ONLY_PNG
#define STBI_NO_STDIO // we do our own fread for portability
#include "stb_image.h"
#include <cstdio>
#include <cstdlib>
#include <vector>
#ifdef DRAGONX_USE_DX11
#include <d3d11.h>
// Get D3D11 device from ImGui backend (same pattern as qr_code.cpp)
static ID3D11Device* GetImGuiD3D11Device()
{
ImGuiIO& io = ImGui::GetIO();
if (!io.BackendRendererUserData) return nullptr;
return *reinterpret_cast<ID3D11Device**>(io.BackendRendererUserData);
}
#else
#ifdef DRAGONX_HAS_GLAD
#include <glad/gl.h>
#else
#include <SDL3/SDL_opengl.h>
#endif
#endif
#include "../util/logger.h"
namespace dragonx {
namespace util {
// Read entire file into memory
static bool ReadFileToBuffer(const char* path, std::vector<unsigned char>& buf)
{
FILE* f = fopen(path, "rb");
if (!f) return false;
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz <= 0) { fclose(f); return false; }
buf.resize((size_t)sz);
size_t rd = fread(buf.data(), 1, (size_t)sz, f);
fclose(f);
return rd == (size_t)sz;
}
bool LoadTextureFromFile(const char* path, ImTextureID* outTexID, int* outW, int* outH)
{
std::vector<unsigned char> fileData;
if (!ReadFileToBuffer(path, fileData)) {
DEBUG_LOGF("LoadTextureFromFile: failed to read '%s'\n", path);
return false;
}
int w = 0, h = 0, channels = 0;
unsigned char* pixels = stbi_load_from_memory(
fileData.data(), (int)fileData.size(), &w, &h, &channels, 4);
if (!pixels) {
DEBUG_LOGF("LoadTextureFromFile: stbi failed for '%s'\n", path);
return false;
}
#ifdef DRAGONX_USE_DX11
ID3D11Device* device = GetImGuiD3D11Device();
if (!device) {
stbi_image_free(pixels);
DEBUG_LOGF("LoadTextureFromFile: no D3D11 device available\n");
return false;
}
D3D11_TEXTURE2D_DESC desc = {};
desc.Width = w;
desc.Height = h;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = pixels;
initData.SysMemPitch = w * 4;
ID3D11Texture2D* texture = nullptr;
HRESULT hr = device->CreateTexture2D(&desc, &initData, &texture);
stbi_image_free(pixels);
if (FAILED(hr) || !texture) {
DEBUG_LOGF("LoadTextureFromFile: CreateTexture2D failed for '%s'\n", path);
return false;
}
ID3D11ShaderResourceView* srv = nullptr;
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Format = desc.Format;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;
hr = device->CreateShaderResourceView(texture, &srvDesc, &srv);
texture->Release();
if (FAILED(hr) || !srv) {
DEBUG_LOGF("LoadTextureFromFile: CreateSRV failed for '%s'\n", path);
return false;
}
*outTexID = (ImTextureID)(intptr_t)srv;
*outW = w;
*outH = h;
DEBUG_LOGF("LoadTextureFromFile: loaded '%s' (%dx%d) -> DX11 SRV %p\n", path, w, h, (void*)srv);
return true;
#else
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0,
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
stbi_image_free(pixels);
*outTexID = (ImTextureID)(intptr_t)tex;
*outW = w;
*outH = h;
DEBUG_LOGF("LoadTextureFromFile: loaded '%s' (%dx%d) -> texture %u\n", path, w, h, tex);
return true;
#endif
}
bool LoadTextureFromMemory(const unsigned char* data, size_t dataSize,
ImTextureID* outTexID, int* outW, int* outH)
{
if (!data || dataSize == 0) {
DEBUG_LOGF("LoadTextureFromMemory: null/empty data\n");
return false;
}
int w = 0, h = 0, channels = 0;
unsigned char* pixels = stbi_load_from_memory(
data, (int)dataSize, &w, &h, &channels, 4);
if (!pixels) {
DEBUG_LOGF("LoadTextureFromMemory: stbi decode failed\n");
return false;
}
#ifdef DRAGONX_USE_DX11
ID3D11Device* device = GetImGuiD3D11Device();
if (!device) {
stbi_image_free(pixels);
DEBUG_LOGF("LoadTextureFromMemory: no D3D11 device available\n");
return false;
}
D3D11_TEXTURE2D_DESC desc = {};
desc.Width = w;
desc.Height = h;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = pixels;
initData.SysMemPitch = w * 4;
ID3D11Texture2D* texture = nullptr;
HRESULT hr = device->CreateTexture2D(&desc, &initData, &texture);
stbi_image_free(pixels);
if (FAILED(hr) || !texture) {
DEBUG_LOGF("LoadTextureFromMemory: CreateTexture2D failed\n");
return false;
}
ID3D11ShaderResourceView* srv = nullptr;
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Format = desc.Format;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;
hr = device->CreateShaderResourceView(texture, &srvDesc, &srv);
texture->Release();
if (FAILED(hr) || !srv) {
DEBUG_LOGF("LoadTextureFromMemory: CreateSRV failed\n");
return false;
}
*outTexID = (ImTextureID)(intptr_t)srv;
*outW = w;
*outH = h;
DEBUG_LOGF("LoadTextureFromMemory: %dx%d -> DX11 SRV %p\n", w, h, (void*)srv);
return true;
#else
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0,
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
stbi_image_free(pixels);
*outTexID = (ImTextureID)(intptr_t)tex;
*outW = w;
*outH = h;
DEBUG_LOGF("LoadTextureFromMemory: %dx%d -> texture %u\n", w, h, tex);
return true;
#endif
}
bool CreateRawTexture(const unsigned char* pixels, int w, int h,
bool repeat, ImTextureID* outTexID)
{
if (!pixels || w <= 0 || h <= 0) return false;
#ifdef DRAGONX_USE_DX11
ID3D11Device* device = GetImGuiD3D11Device();
if (!device) return false;
D3D11_TEXTURE2D_DESC desc = {};
desc.Width = w;
desc.Height = h;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = pixels;
initData.SysMemPitch = w * 4;
ID3D11Texture2D* texture = nullptr;
HRESULT hr = device->CreateTexture2D(&desc, &initData, &texture);
if (FAILED(hr) || !texture) return false;
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Format = desc.Format;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;
ID3D11ShaderResourceView* srv = nullptr;
hr = device->CreateShaderResourceView(texture, &srvDesc, &srv);
texture->Release();
if (FAILED(hr) || !srv) return false;
*outTexID = (ImTextureID)(intptr_t)srv;
return true;
#else
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0,
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
if (repeat) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
} else {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
glBindTexture(GL_TEXTURE_2D, 0);
*outTexID = (ImTextureID)(intptr_t)tex;
return true;
#endif
}
unsigned char* LoadRawPixelsFromFile(const char* path, int* outW, int* outH)
{
std::vector<unsigned char> fileData;
if (!ReadFileToBuffer(path, fileData)) return nullptr;
int channels = 0;
return stbi_load_from_memory(fileData.data(), (int)fileData.size(),
outW, outH, &channels, 4);
}
unsigned char* LoadRawPixelsFromMemory(const unsigned char* data, size_t dataSize, int* outW, int* outH)
{
if (!data || dataSize == 0) return nullptr;
int channels = 0;
return stbi_load_from_memory(data, (int)dataSize, outW, outH, &channels, 4);
}
void FreeRawPixels(unsigned char* pixels)
{
if (pixels) stbi_image_free(pixels);
}
void DestroyTexture(ImTextureID texID)
{
if (!texID) return;
#ifdef DRAGONX_USE_DX11
auto* srv = (ID3D11ShaderResourceView*)(intptr_t)texID;
srv->Release();
#else
GLuint tex = (GLuint)(intptr_t)texID;
glDeleteTextures(1, &tex);
#endif
}
} // namespace util
} // namespace dragonx

80
src/util/texture_loader.h Normal file
View File

@@ -0,0 +1,80 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "imgui.h"
namespace dragonx {
namespace util {
/**
* @brief Load a PNG/JPG/BMP image from disk into an OpenGL or DX11 texture.
* @param path File path (relative to working directory or absolute)
* @param outTexID Receives the ImTextureID for ImGui rendering
* @param outW Receives image width
* @param outH Receives image height
* @return true on success
*
* The caller is responsible for destroying the texture.
* On OpenGL, cast outTexID to GLuint and call glDeleteTextures.
*/
bool LoadTextureFromFile(const char* path, ImTextureID* outTexID, int* outW, int* outH);
/**
* @brief Load a PNG/JPG/BMP image from memory into an OpenGL or DX11 texture.
* @param data Pointer to compressed image data (e.g. PNG bytes)
* @param dataSize Size of the data in bytes
* @param outTexID Receives the ImTextureID for ImGui rendering
* @param outW Receives image width
* @param outH Receives image height
* @return true on success
*/
bool LoadTextureFromMemory(const unsigned char* data, size_t dataSize,
ImTextureID* outTexID, int* outW, int* outH);
/**
* @brief Create a texture from raw RGBA pixel data (no decoding needed).
* @param pixels Pointer to RGBA pixel data (4 bytes per pixel, caller-owned)
* @param w Image width
* @param h Image height
* @param repeat If true, set wrap mode to GL_REPEAT (for tiling); else CLAMP_TO_EDGE
* @param outTexID Receives the ImTextureID
* @return true on success
*/
bool CreateRawTexture(const unsigned char* pixels, int w, int h,
bool repeat, ImTextureID* outTexID);
/**
* @brief Load raw RGBA pixel data from a PNG/JPG/BMP file (no GPU texture).
* @param path File path
* @param outW Receives image width
* @param outH Receives image height
* @return Pointer to RGBA pixels (caller must free with FreeRawPixels), or nullptr on failure
*/
unsigned char* LoadRawPixelsFromFile(const char* path, int* outW, int* outH);
/**
* @brief Load raw RGBA pixel data from in-memory compressed image data (no GPU texture).
* @param data Pointer to compressed image data (e.g. PNG bytes)
* @param dataSize Size of the data in bytes
* @param outW Receives image width
* @param outH Receives image height
* @return Pointer to RGBA pixels (caller must free with FreeRawPixels), or nullptr on failure
*/
unsigned char* LoadRawPixelsFromMemory(const unsigned char* data, size_t dataSize, int* outW, int* outH);
/**
* @brief Free pixels returned by LoadRawPixelsFromFile / LoadRawPixelsFromMemory.
*/
void FreeRawPixels(unsigned char* pixels);
/**
* @brief Destroy a GPU texture previously created by Load/CreateRaw functions.
* @param texID Texture handle (GLuint on OpenGL, ID3D11ShaderResourceView* on DX11)
*/
void DestroyTexture(ImTextureID texID);
} // namespace util
} // namespace dragonx