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:
104
src/util/base64.cpp
Normal file
104
src/util/base64.cpp
Normal 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
53
src/util/base64.h
Normal 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
746
src/util/bootstrap.cpp
Normal 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
116
src/util/bootstrap.h
Normal 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
321
src/util/i18n.cpp
Normal 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
79
src/util/i18n.h
Normal 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
91
src/util/logger.cpp
Normal 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
65
src/util/logger.h
Normal 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
168
src/util/noise_texture.cpp
Normal 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: 16–240 (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
35
src/util/noise_texture.h
Normal 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
147
src/util/payment_uri.cpp
Normal 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
51
src/util/payment_uri.h
Normal 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
199
src/util/perf_log.h
Normal 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
600
src/util/platform.cpp
Normal 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
135
src/util/platform.h
Normal 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
249
src/util/secure_vault.cpp
Normal 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
93
src/util/secure_vault.h
Normal 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
|
||||
136
src/util/single_instance.cpp
Normal file
136
src/util/single_instance.cpp
Normal 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
|
||||
55
src/util/single_instance.h
Normal file
55
src/util/single_instance.h
Normal 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
326
src/util/texture_loader.cpp
Normal 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
80
src/util/texture_loader.h
Normal 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
|
||||
Reference in New Issue
Block a user