ObsidianDragon - DragonX ImGui Wallet

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

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

224
src/rpc/connection.cpp Normal file
View File

@@ -0,0 +1,224 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "connection.h"
#include "../config/version.h"
#include "../resources/embedded_resources.h"
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <ctime>
#include <filesystem>
#include "../util/logger.h"
#ifdef _WIN32
#include <shlobj.h>
#else
#include <pwd.h>
#include <unistd.h>
#endif
namespace fs = std::filesystem;
namespace dragonx {
namespace rpc {
Connection::Connection() = default;
Connection::~Connection() = default;
std::string Connection::getDefaultDataDir()
{
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
return std::string(path) + "\\Hush\\DRAGONX";
}
return "";
#elif defined(__APPLE__)
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
// Match SilentDragonX path: Library/Application Support/Hush/DRAGONX
return std::string(home) + "/Library/Application Support/Hush/DRAGONX";
#else
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
return std::string(home) + "/.hush/DRAGONX";
#endif
}
std::string Connection::getDefaultConfPath()
{
return getDefaultDataDir() + "/" + DRAGONX_CONF_FILENAME;
}
std::string Connection::getSaplingParamsDir()
{
// Sapling params are now extracted alongside the daemon binaries
// in <ObsidianDragonDir>/hush3/ — no longer in the legacy ZcashParams dir.
return resources::getDaemonDirectory();
}
bool Connection::verifySaplingParams()
{
std::string params_dir = getSaplingParamsDir();
if (params_dir.empty()) {
DEBUG_LOGF("verifySaplingParams: params dir is empty\n");
return false;
}
#ifdef _WIN32
std::string spend_path = params_dir + "\\sapling-spend.params";
std::string output_path = params_dir + "\\sapling-output.params";
#else
std::string spend_path = params_dir + "/sapling-spend.params";
std::string output_path = params_dir + "/sapling-output.params";
#endif
bool spend_exists = fs::exists(spend_path);
bool output_exists = fs::exists(output_path);
DEBUG_LOGF("verifySaplingParams: dir=%s\n", params_dir.c_str());
DEBUG_LOGF(" spend: %s -> %s\n", spend_path.c_str(), spend_exists ? "found" : "MISSING");
DEBUG_LOGF(" output: %s -> %s\n", output_path.c_str(), output_exists ? "found" : "MISSING");
return spend_exists && output_exists;
}
ConnectionConfig Connection::parseConfFile(const std::string& path)
{
ConnectionConfig config;
std::ifstream file(path);
if (!file.is_open()) {
return config;
}
std::string line;
while (std::getline(file, line)) {
// Skip empty lines and comments
if (line.empty() || line[0] == '#') {
continue;
}
// Parse key=value
size_t eq_pos = line.find('=');
if (eq_pos == std::string::npos) {
continue;
}
std::string key = line.substr(0, eq_pos);
std::string value = line.substr(eq_pos + 1);
// Trim whitespace
while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) {
key.pop_back();
}
while (!value.empty() && (value[0] == ' ' || value[0] == '\t')) {
value.erase(0, 1);
}
// Map to config
if (key == "rpcuser") {
config.rpcuser = value;
} else if (key == "rpcpassword") {
config.rpcpassword = value;
} else if (key == "rpcport") {
config.port = value;
} else if (key == "rpchost" || key == "rpcconnect") {
config.host = value;
} else if (key == "proxy") {
config.proxy = value;
}
}
return config;
}
ConnectionConfig Connection::autoDetectConfig()
{
ConnectionConfig config;
// Ensure data directory exists
std::string data_dir = getDefaultDataDir();
if (!fs::exists(data_dir)) {
DEBUG_LOGF("Creating data directory: %s\n", data_dir.c_str());
fs::create_directories(data_dir);
}
// Try to find DRAGONX.conf
std::string conf_path = getDefaultConfPath();
if (fs::exists(conf_path)) {
config = parseConfFile(conf_path);
config.hush_dir = data_dir;
} else {
// Create a default config file
if (createDefaultConfig(conf_path)) {
config = parseConfFile(conf_path);
config.hush_dir = data_dir;
}
}
// Set defaults for missing values
if (config.host.empty()) {
config.host = DRAGONX_DEFAULT_RPC_HOST;
}
if (config.port.empty()) {
config.port = DRAGONX_DEFAULT_RPC_PORT;
}
return config;
}
bool Connection::createDefaultConfig(const std::string& path)
{
// Generate random rpcuser/rpcpassword
auto generateRandomString = [](int length) -> std::string {
const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
std::string result;
result.reserve(length);
std::srand(static_cast<unsigned>(std::time(nullptr)));
for (int i = 0; i < length; i++) {
result += charset[std::rand() % (sizeof(charset) - 1)];
}
return result;
};
std::string rpcuser = generateRandomString(16);
std::string rpcpassword = generateRandomString(32);
std::ofstream file(path);
if (!file.is_open()) {
DEBUG_LOGF("Failed to create config file: %s\n", path.c_str());
return false;
}
file << "# DragonX configuration file\n";
file << "# Auto-generated by DragonX Wallet\n";
file << "\n";
file << "rpcuser=" << rpcuser << "\n";
file << "rpcpassword=" << rpcpassword << "\n";
file << "rpcport=" << DRAGONX_DEFAULT_RPC_PORT << "\n";
file << "server=1\n";
file << "txindex=1\n";
file << "addnode=195.201.20.230\n";
file << "addnode=195.201.137.219\n";
file.close();
DEBUG_LOGF("Created default config file: %s\n", path.c_str());
return true;
}
} // namespace rpc
} // namespace dragonx

80
src/rpc/connection.h Normal file
View File

@@ -0,0 +1,80 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
namespace dragonx {
namespace rpc {
/**
* @brief Connection configuration
*/
struct ConnectionConfig {
std::string host = "127.0.0.1";
std::string port = "21769";
std::string rpcuser;
std::string rpcpassword;
std::string hush_dir;
std::string proxy; // SOCKS5 proxy for Tor
bool use_embedded = true;
};
/**
* @brief Manages connection to dragonxd
*
* Handles auto-detection of DRAGONX.conf, starting embedded daemon,
* and connection lifecycle.
*/
class Connection {
public:
Connection();
~Connection();
/**
* @brief Auto-detect and load connection config
* @return Config from DRAGONX.conf or defaults
*/
static ConnectionConfig autoDetectConfig();
/**
* @brief Get the default DRAGONX.conf location
*/
static std::string getDefaultConfPath();
/**
* @brief Get the default DragonX data directory
*/
static std::string getDefaultDataDir();
/**
* @brief Parse a DRAGONX.conf file
* @param path Path to conf file
* @return Parsed configuration
*/
static ConnectionConfig parseConfFile(const std::string& path);
/**
* @brief Check if Sapling params exist
*/
static bool verifySaplingParams();
/**
* @brief Get the Sapling params directory
*/
static std::string getSaplingParamsDir();
/**
* @brief Create a default DRAGONX.conf file
* @param path Path to create the file
* @return true if created successfully
*/
static bool createDefaultConfig(const std::string& path);
private:
};
} // namespace rpc
} // namespace dragonx

599
src/rpc/rpc_client.cpp Normal file
View File

@@ -0,0 +1,599 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "rpc_client.h"
#include "../config/version.h"
#include "../util/base64.h"
#include <curl/curl.h>
#include <cstdio>
#include <cstring>
#include "../util/logger.h"
namespace dragonx {
namespace rpc {
// Callback for libcurl to write response data
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
size_t totalSize = size * nmemb;
userp->append((char*)contents, totalSize);
return totalSize;
}
// Private implementation using libcurl
class RPCClient::Impl {
public:
CURL* curl = nullptr;
struct curl_slist* headers = nullptr;
std::string url;
~Impl() {
if (headers) {
curl_slist_free_all(headers);
}
if (curl) {
curl_easy_cleanup(curl);
}
}
};
// Initialize curl globally (once)
static bool initCurl() {
static bool initialized = false;
if (!initialized) {
curl_global_init(CURL_GLOBAL_DEFAULT);
initialized = true;
}
return true;
}
static bool curl_init = initCurl();
RPCClient::RPCClient() : impl_(std::make_unique<Impl>())
{
}
RPCClient::~RPCClient() = default;
bool RPCClient::connect(const std::string& host, const std::string& port,
const std::string& user, const std::string& password)
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
host_ = host;
port_ = port;
// Create Basic auth header with proper base64 encoding
std::string credentials = user + ":" + password;
auth_ = util::base64_encode(credentials);
// Build URL - use HTTP for localhost RPC (TLS not always enabled)
impl_->url = "http://" + host + ":" + port + "/";
DEBUG_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());
// Initialize curl handle
impl_->curl = curl_easy_init();
if (!impl_->curl) {
DEBUG_LOGF("Failed to initialize curl\n");
return false;
}
// Set up headers - daemon expects text/plain, not application/json
impl_->headers = curl_slist_append(impl_->headers, "Content-Type: text/plain");
std::string auth_header = "Authorization: Basic " + auth_;
impl_->headers = curl_slist_append(impl_->headers, auth_header.c_str());
// Configure curl
curl_easy_setopt(impl_->curl, CURLOPT_URL, impl_->url.c_str());
curl_easy_setopt(impl_->curl, CURLOPT_HTTPHEADER, impl_->headers);
curl_easy_setopt(impl_->curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, 3L);
// Test connection with getinfo
try {
json result = call("getinfo");
if (result.contains("version")) {
connected_ = true;
DEBUG_LOGF("Connected to dragonxd v%d\n", result["version"].get<int>());
return true;
}
} catch (const std::exception& e) {
DEBUG_LOGF("Connection failed: %s\n", e.what());
}
connected_ = false;
return false;
}
void RPCClient::disconnect()
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
connected_ = false;
if (impl_->curl) {
curl_easy_cleanup(impl_->curl);
impl_->curl = nullptr;
}
if (impl_->headers) {
curl_slist_free_all(impl_->headers);
impl_->headers = nullptr;
}
}
json RPCClient::makePayload(const std::string& method, const json& params)
{
return {
{"jsonrpc", "1.0"},
{"id", "ObsidianDragon"},
{"method", method},
{"params", params}
};
}
json RPCClient::call(const std::string& method, const json& params)
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
if (!impl_->curl) {
throw std::runtime_error("Not connected");
}
json payload = makePayload(method, params);
std::string body = payload.dump();
std::string response_data;
// Set POST data
curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDSIZE, (long)body.size());
curl_easy_setopt(impl_->curl, CURLOPT_WRITEDATA, &response_data);
// Perform request
CURLcode res = curl_easy_perform(impl_->curl);
if (res != CURLE_OK) {
throw std::runtime_error("RPC request failed: " + std::string(curl_easy_strerror(res)));
}
// Check HTTP response code
long http_code = 0;
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code);
// Bitcoin/Hush RPC returns HTTP 500 for application-level errors
// (insufficient funds, bad params, etc.) with a valid JSON body.
// Parse the body first to extract the real error message.
if (http_code != 200) {
try {
json response = json::parse(response_data);
if (response.contains("error") && !response["error"].is_null()) {
std::string err_msg = response["error"]["message"].get<std::string>();
throw std::runtime_error(err_msg);
}
} catch (const json::exception&) {
// Body wasn't valid JSON — fall through to generic HTTP error
}
throw std::runtime_error("RPC error: HTTP " + std::to_string(http_code));
}
json response = json::parse(response_data);
if (response.contains("error") && !response["error"].is_null()) {
std::string err_msg = response["error"]["message"].get<std::string>();
throw std::runtime_error("RPC error: " + err_msg);
}
return response["result"];
}
std::string RPCClient::callRaw(const std::string& method, const json& params)
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
if (!impl_->curl) {
throw std::runtime_error("Not connected");
}
json payload = makePayload(method, params);
std::string body = payload.dump();
std::string response_data;
curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDSIZE, (long)body.size());
curl_easy_setopt(impl_->curl, CURLOPT_WRITEDATA, &response_data);
CURLcode res = curl_easy_perform(impl_->curl);
if (res != CURLE_OK) {
throw std::runtime_error("RPC request failed: " + std::string(curl_easy_strerror(res)));
}
long http_code = 0;
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code);
if (http_code != 200) {
try {
json response = json::parse(response_data);
if (response.contains("error") && !response["error"].is_null()) {
std::string err_msg = response["error"]["message"].get<std::string>();
throw std::runtime_error(err_msg);
}
} catch (const json::exception&) {}
throw std::runtime_error("RPC error: HTTP " + std::to_string(http_code));
}
// Parse with ordered_json to preserve the daemon's original key order
nlohmann::ordered_json oj = nlohmann::ordered_json::parse(response_data);
if (oj.contains("error") && !oj["error"].is_null()) {
std::string err_msg = oj["error"]["message"].get<std::string>();
throw std::runtime_error("RPC error: " + err_msg);
}
auto& result = oj["result"];
if (result.is_null()) {
return "null";
} else if (result.is_string()) {
// Return the raw string (not JSON-encoded) — caller wraps as needed
return result.get<std::string>();
} else {
return result.dump(4);
}
}
void RPCClient::doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err)
{
try {
json result = call(method, params);
if (cb) cb(result);
} catch (const std::exception& e) {
if (err) {
err(e.what());
} else {
DEBUG_LOGF("RPC error (%s): %s\n", method.c_str(), e.what());
}
}
}
// High-level API implementations
void RPCClient::getInfo(Callback cb, ErrorCallback err)
{
doRPC("getinfo", {}, cb, err);
}
void RPCClient::getBlockchainInfo(Callback cb, ErrorCallback err)
{
doRPC("getblockchaininfo", {}, cb, err);
}
void RPCClient::getMiningInfo(Callback cb, ErrorCallback err)
{
doRPC("getmininginfo", {}, cb, err);
}
void RPCClient::getBalance(Callback cb, ErrorCallback err)
{
doRPC("getbalance", {}, cb, err);
}
void RPCClient::z_getTotalBalance(Callback cb, ErrorCallback err)
{
doRPC("z_gettotalbalance", {}, cb, err);
}
void RPCClient::listUnspent(Callback cb, ErrorCallback err)
{
doRPC("listunspent", {0}, cb, err);
}
void RPCClient::z_listUnspent(Callback cb, ErrorCallback err)
{
doRPC("z_listunspent", {0}, cb, err);
}
void RPCClient::getAddressesByAccount(Callback cb, ErrorCallback err)
{
doRPC("getaddressesbyaccount", {""}, cb, err);
}
void RPCClient::z_listAddresses(Callback cb, ErrorCallback err)
{
doRPC("z_listaddresses", {}, cb, err);
}
void RPCClient::getNewAddress(Callback cb, ErrorCallback err)
{
doRPC("getnewaddress", {}, cb, err);
}
void RPCClient::z_getNewAddress(Callback cb, ErrorCallback err)
{
doRPC("z_getnewaddress", {}, cb, err);
}
void RPCClient::listTransactions(int count, Callback cb, ErrorCallback err)
{
doRPC("listtransactions", {"", count}, cb, err);
}
void RPCClient::z_viewTransaction(const std::string& txid, Callback cb, ErrorCallback err)
{
doRPC("z_viewtransaction", {txid}, cb, err);
}
void RPCClient::getRawTransaction(const std::string& txid, Callback cb, ErrorCallback err)
{
doRPC("getrawtransaction", {txid, 1}, cb, err);
}
void RPCClient::sendToAddress(const std::string& address, double amount, Callback cb, ErrorCallback err)
{
doRPC("sendtoaddress", {address, amount}, cb, err);
}
void RPCClient::z_sendMany(const std::string& from, const json& recipients, Callback cb, ErrorCallback err)
{
doRPC("z_sendmany", {from, recipients}, cb, err);
}
void RPCClient::setGenerate(bool generate, int threads, Callback cb, ErrorCallback err)
{
doRPC("setgenerate", {generate, threads}, cb, err);
}
void RPCClient::getNetworkHashPS(Callback cb, ErrorCallback err)
{
doRPC("getnetworkhashps", {}, cb, err);
}
void RPCClient::getLocalHashrate(Callback cb, ErrorCallback err)
{
// RPC name is "getlocalsolps" (inherited from HUSH/Zcash daemon API)
// but DragonX uses RandomX, so the value is H/s not Sol/s
doRPC("getlocalsolps", {}, cb, err);
}
void RPCClient::getPeerInfo(Callback cb, ErrorCallback err)
{
doRPC("getpeerinfo", {}, cb, err);
}
void RPCClient::listBanned(Callback cb, ErrorCallback err)
{
doRPC("listbanned", {}, cb, err);
}
void RPCClient::setBan(const std::string& ip, const std::string& command, Callback cb, ErrorCallback err, int bantime)
{
// setban "ip" "add|remove" [bantime] [absolute]
doRPC("setban", {ip, command, bantime}, cb, err);
}
void RPCClient::clearBanned(Callback cb, ErrorCallback err)
{
doRPC("clearbanned", {}, cb, err);
}
void RPCClient::dumpPrivKey(const std::string& address, Callback cb, ErrorCallback err)
{
doRPC("dumpprivkey", {address}, cb, err);
}
void RPCClient::z_exportKey(const std::string& address, Callback cb, ErrorCallback err)
{
doRPC("z_exportkey", {address}, cb, err);
}
void RPCClient::z_exportViewingKey(const std::string& address, Callback cb, ErrorCallback err)
{
doRPC("z_exportviewingkey", {address}, cb, err);
}
void RPCClient::importPrivKey(const std::string& key, bool rescan, Callback cb, ErrorCallback err)
{
doRPC("importprivkey", {key, "", rescan}, cb, err);
}
void RPCClient::z_importKey(const std::string& key, bool rescan, Callback cb, ErrorCallback err)
{
doRPC("z_importkey", {key, rescan ? "yes" : "no"}, cb, err);
}
void RPCClient::validateAddress(const std::string& address, Callback cb, ErrorCallback err)
{
doRPC("validateaddress", {address}, cb, err);
}
void RPCClient::getBlock(const std::string& hash_or_height, Callback cb, ErrorCallback err)
{
doRPC("getblock", {hash_or_height}, cb, err);
}
void RPCClient::stop(Callback cb, ErrorCallback err)
{
doRPC("stop", {}, cb, err);
}
void RPCClient::rescanBlockchain(int startHeight, Callback cb, ErrorCallback err)
{
doRPC("rescanblockchain", {startHeight}, cb, err);
}
void RPCClient::z_validateAddress(const std::string& address, Callback cb, ErrorCallback err)
{
doRPC("z_validateaddress", {address}, cb, err);
}
void RPCClient::getBlockHash(int height, Callback cb, ErrorCallback err)
{
doRPC("getblockhash", {height}, cb, err);
}
void RPCClient::getTransaction(const std::string& txid, Callback cb, ErrorCallback err)
{
doRPC("gettransaction", {txid}, cb, err);
}
void RPCClient::getWalletInfo(Callback cb, ErrorCallback err)
{
doRPC("getwalletinfo", {}, cb, err);
}
void RPCClient::encryptWallet(const std::string& passphrase, Callback cb, ErrorCallback err)
{
doRPC("encryptwallet", {passphrase}, cb, err);
}
void RPCClient::walletPassphrase(const std::string& passphrase, int timeout, Callback cb, ErrorCallback err)
{
doRPC("walletpassphrase", {passphrase, timeout}, cb, err);
}
void RPCClient::walletLock(Callback cb, ErrorCallback err)
{
doRPC("walletlock", {}, cb, err);
}
void RPCClient::walletPassphraseChange(const std::string& oldPass, const std::string& newPass,
Callback cb, ErrorCallback err)
{
doRPC("walletpassphrasechange", {oldPass, newPass}, cb, err);
}
void RPCClient::z_exportWallet(const std::string& filename, Callback cb, ErrorCallback err)
{
doRPC("z_exportwallet", {filename}, cb, err);
}
void RPCClient::z_importWallet(const std::string& filename, Callback cb, ErrorCallback err)
{
doRPC("z_importwallet", {filename}, cb, err);
}
void RPCClient::z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
double fee, int limit, Callback cb, ErrorCallback err)
{
doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit}, cb, err);
}
void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, const std::string& toAddr,
double fee, int limit, Callback cb, ErrorCallback err)
{
json addrs = json::array();
for (const auto& addr : fromAddrs) {
addrs.push_back(addr);
}
doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit}, cb, err);
}
void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, Callback cb, ErrorCallback err)
{
json ids = json::array();
for (const auto& id : opids) {
ids.push_back(id);
}
doRPC("z_getoperationstatus", {ids}, cb, err);
}
void RPCClient::z_getOperationResult(const std::vector<std::string>& opids, Callback cb, ErrorCallback err)
{
json ids = json::array();
for (const auto& id : opids) {
ids.push_back(id);
}
doRPC("z_getoperationresult", {ids}, cb, err);
}
void RPCClient::z_listReceivedByAddress(const std::string& address, int minconf, Callback cb, ErrorCallback err)
{
doRPC("z_listreceivedbyaddress", {address, minconf}, cb, err);
}
// Unified callback versions
void RPCClient::getInfo(UnifiedCallback cb)
{
doRPC("getinfo", {},
[cb](const json& result) {
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
}
void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb)
{
doRPC("rescanblockchain", {startHeight},
[cb](const json& result) {
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
}
void RPCClient::z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
double fee, int limit, UnifiedCallback cb)
{
doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit},
[cb](const json& result) {
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
}
void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, const std::string& toAddr,
double fee, int limit, UnifiedCallback cb)
{
json addrs = json::array();
for (const auto& addr : fromAddrs) {
addrs.push_back(addr);
}
doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit},
[cb](const json& result) {
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
}
void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, UnifiedCallback cb)
{
json ids = json::array();
for (const auto& id : opids) {
ids.push_back(id);
}
doRPC("z_getoperationstatus", {ids},
[cb](const json& result) {
if (cb) cb(result, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
}
void RPCClient::getBlock(int height, UnifiedCallback cb)
{
// First get block hash, then get block
getBlockHash(height,
[this, cb](const json& hashResult) {
std::string hash = hashResult.get<std::string>();
getBlock(hash,
[cb](const json& blockResult) {
if (cb) cb(blockResult, "");
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
},
[cb](const std::string& error) {
if (cb) cb(json{}, error);
}
);
}
} // namespace rpc
} // namespace dragonx

179
src/rpc/rpc_client.h Normal file
View File

@@ -0,0 +1,179 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "types.h"
#include <string>
#include <functional>
#include <memory>
#include <mutex>
#include <nlohmann/json.hpp>
namespace dragonx {
namespace rpc {
using json = nlohmann::json;
using Callback = std::function<void(const json&)>;
using ErrorCallback = std::function<void(const std::string&)>;
/**
* @brief JSON-RPC client for dragonxd
*
* Handles all communication with the dragonxd daemon via JSON-RPC.
*/
class RPCClient {
public:
RPCClient();
~RPCClient();
// Non-copyable
RPCClient(const RPCClient&) = delete;
RPCClient& operator=(const RPCClient&) = delete;
/**
* @brief Connect to dragonxd
* @param host RPC host (default: 127.0.0.1)
* @param port RPC port (default: 18031)
* @param user RPC username
* @param password RPC password
* @return true if connection successful
*/
bool connect(const std::string& host, const std::string& port,
const std::string& user, const std::string& password);
/**
* @brief Disconnect from dragonxd
*/
void disconnect();
/**
* @brief Check if connected
*/
bool isConnected() const { return connected_; }
/**
* @brief Make a raw RPC call
* @param method RPC method name
* @param params Method parameters (optional)
* @return JSON response or null on error
*/
json call(const std::string& method, const json& params = json::array());
/**
* @brief Make a raw RPC call and return the result field as a string
* @param method RPC method name
* @param params Method parameters (optional)
* @return Raw JSON string of the "result" field (preserves key order)
*/
std::string callRaw(const std::string& method, const json& params = json::array());
// High-level API methods - mirror the Qt version
// Info methods
void getInfo(Callback cb, ErrorCallback err = nullptr);
void getBlockchainInfo(Callback cb, ErrorCallback err = nullptr);
void getMiningInfo(Callback cb, ErrorCallback err = nullptr);
// Balance methods
void getBalance(Callback cb, ErrorCallback err = nullptr);
void z_getTotalBalance(Callback cb, ErrorCallback err = nullptr);
void listUnspent(Callback cb, ErrorCallback err = nullptr);
void z_listUnspent(Callback cb, ErrorCallback err = nullptr);
// Address methods
void getAddressesByAccount(Callback cb, ErrorCallback err = nullptr);
void z_listAddresses(Callback cb, ErrorCallback err = nullptr);
void getNewAddress(Callback cb, ErrorCallback err = nullptr);
void z_getNewAddress(Callback cb, ErrorCallback err = nullptr);
// Transaction methods
void listTransactions(int count, Callback cb, ErrorCallback err = nullptr);
void z_viewTransaction(const std::string& txid, Callback cb, ErrorCallback err = nullptr);
void getRawTransaction(const std::string& txid, Callback cb, ErrorCallback err = nullptr);
void sendToAddress(const std::string& address, double amount, Callback cb, ErrorCallback err = nullptr);
void z_sendMany(const std::string& from, const json& recipients, Callback cb, ErrorCallback err = nullptr);
// Mining methods
void setGenerate(bool generate, int threads, Callback cb, ErrorCallback err = nullptr);
void getNetworkHashPS(Callback cb, ErrorCallback err = nullptr);
void getLocalHashrate(Callback cb, ErrorCallback err = nullptr);
// Peer methods
void getPeerInfo(Callback cb, ErrorCallback err = nullptr);
void listBanned(Callback cb, ErrorCallback err = nullptr);
void setBan(const std::string& ip, const std::string& command, Callback cb, ErrorCallback err = nullptr, int bantime = 86400);
void clearBanned(Callback cb, ErrorCallback err = nullptr);
// Key management
void dumpPrivKey(const std::string& address, Callback cb, ErrorCallback err = nullptr);
void z_exportKey(const std::string& address, Callback cb, ErrorCallback err = nullptr);
void z_exportViewingKey(const std::string& address, Callback cb, ErrorCallback err = nullptr);
void importPrivKey(const std::string& key, bool rescan, Callback cb, ErrorCallback err = nullptr);
void z_importKey(const std::string& key, bool rescan, Callback cb, ErrorCallback err = nullptr);
// Utility
void validateAddress(const std::string& address, Callback cb, ErrorCallback err = nullptr);
void z_validateAddress(const std::string& address, Callback cb, ErrorCallback err = nullptr);
void getBlock(const std::string& hash_or_height, Callback cb, ErrorCallback err = nullptr);
void getBlockHash(int height, Callback cb, ErrorCallback err = nullptr);
void getTransaction(const std::string& txid, Callback cb, ErrorCallback err = nullptr);
void getWalletInfo(Callback cb, ErrorCallback err = nullptr);
void stop(Callback cb, ErrorCallback err = nullptr);
// Wallet maintenance
void rescanBlockchain(int startHeight, Callback cb, ErrorCallback err = nullptr);
// Wallet encryption & locking
void encryptWallet(const std::string& passphrase, Callback cb, ErrorCallback err = nullptr);
void walletPassphrase(const std::string& passphrase, int timeout, Callback cb, ErrorCallback err = nullptr);
void walletLock(Callback cb, ErrorCallback err = nullptr);
void walletPassphraseChange(const std::string& oldPass, const std::string& newPass,
Callback cb, ErrorCallback err = nullptr);
// Wallet export/import (for decrypt flow)
void z_exportWallet(const std::string& filename, Callback cb, ErrorCallback err = nullptr);
void z_importWallet(const std::string& filename, Callback cb, ErrorCallback err = nullptr);
// Shielding operations
void z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
double fee, int limit, Callback cb, ErrorCallback err = nullptr);
void z_mergeToAddress(const std::vector<std::string>& fromAddrs, const std::string& toAddr,
double fee, int limit, Callback cb, ErrorCallback err = nullptr);
// Operation status monitoring
void z_getOperationStatus(const std::vector<std::string>& opids, Callback cb, ErrorCallback err = nullptr);
void z_getOperationResult(const std::vector<std::string>& opids, Callback cb, ErrorCallback err = nullptr);
// Received transactions
void z_listReceivedByAddress(const std::string& address, int minconf, Callback cb, ErrorCallback err = nullptr);
// Unified callback versions (result + error)
using UnifiedCallback = std::function<void(const json& result, const std::string& error)>;
void getInfo(UnifiedCallback cb);
void rescanBlockchain(int startHeight, UnifiedCallback cb);
void z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
double fee, int limit, UnifiedCallback cb);
void z_mergeToAddress(const std::vector<std::string>& fromAddrs, const std::string& toAddr,
double fee, int limit, UnifiedCallback cb);
void z_getOperationStatus(const std::vector<std::string>& opids, UnifiedCallback cb);
void getBlock(int height, UnifiedCallback cb);
private:
json makePayload(const std::string& method, const json& params = json::array());
void doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err);
std::string host_;
std::string port_;
std::string auth_; // Base64 encoded "user:password"
bool connected_ = false;
mutable std::recursive_mutex curl_mutex_; // serializes all curl handle access
// HTTP client (implementation hidden)
class Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace rpc
} // namespace dragonx

135
src/rpc/rpc_worker.cpp Normal file
View File

@@ -0,0 +1,135 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "rpc_worker.h"
#include <cstdio>
#include "../util/logger.h"
namespace dragonx {
namespace rpc {
RPCWorker::RPCWorker() = default;
RPCWorker::~RPCWorker()
{
stop();
}
void RPCWorker::start()
{
if (running_.load(std::memory_order_relaxed)) return;
running_.store(true, std::memory_order_release);
thread_ = std::thread(&RPCWorker::run, this);
}
void RPCWorker::stop()
{
if (!running_.load(std::memory_order_relaxed) && !thread_.joinable()) return;
// Signal stop if not already signaled
requestStop();
if (thread_.joinable()) {
thread_.join();
}
// Discard pending tasks
{
std::lock_guard<std::mutex> lk(taskMtx_);
tasks_.clear();
}
}
void RPCWorker::requestStop()
{
if (!running_.load(std::memory_order_relaxed)) return;
{
std::lock_guard<std::mutex> lk(taskMtx_);
running_.store(false, std::memory_order_release);
}
taskCv_.notify_one();
}
void RPCWorker::post(WorkFn work)
{
{
std::lock_guard<std::mutex> lk(taskMtx_);
tasks_.push_back(std::move(work));
}
taskCv_.notify_one();
}
int RPCWorker::drainResults()
{
// Swap the result queue under the lock, then execute outside the lock
// to minimise contention with the worker thread.
std::deque<MainCb> batch;
{
std::lock_guard<std::mutex> lk(resultMtx_);
batch.swap(results_);
}
int count = 0;
for (auto& cb : batch) {
if (cb) {
try {
cb();
} catch (const std::exception& e) {
DEBUG_LOGF("[RPCWorker] Main-thread callback threw: %s\n", e.what());
} catch (...) {
DEBUG_LOGF("[RPCWorker] Main-thread callback threw unknown exception\n");
}
++count;
}
}
return count;
}
bool RPCWorker::hasPendingResults() const
{
std::lock_guard<std::mutex> lk(resultMtx_);
return !results_.empty();
}
void RPCWorker::run()
{
while (true) {
WorkFn task;
// Wait for a task or stop signal
{
std::unique_lock<std::mutex> lk(taskMtx_);
taskCv_.wait(lk, [this] {
return !tasks_.empty() || !running_.load(std::memory_order_acquire);
});
if (!running_.load(std::memory_order_acquire) && tasks_.empty()) {
break;
}
if (!tasks_.empty()) {
task = std::move(tasks_.front());
tasks_.pop_front();
}
}
if (!task) continue;
// Execute the work function (blocking I/O happens here)
try {
MainCb result = task();
if (result) {
std::lock_guard<std::mutex> lk(resultMtx_);
results_.push_back(std::move(result));
}
} catch (const std::exception& e) {
DEBUG_LOGF("[RPCWorker] Task threw: %s\n", e.what());
}
}
}
} // namespace rpc
} // namespace dragonx

93
src/rpc/rpc_worker.h Normal file
View File

@@ -0,0 +1,93 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <atomic>
#include <condition_variable>
#include <deque>
#include <functional>
#include <mutex>
#include <thread>
namespace dragonx {
namespace rpc {
/**
* @brief Background worker thread for RPC calls
*
* Provides a single-threaded task queue so that all RPC/HTTP work happens
* off the UI thread. The caller submits a *work function* that runs on the
* worker thread and returns a *result callback* that is queued for execution
* on the main (UI) thread during drainResults().
*
* Usage from the main thread:
*
* worker.post([&rpc]() -> RPCWorker::MainCb {
* json r = rpc.call("getinfo"); // runs on worker thread
* return [r]() { applyToState(r); }; // runs on main thread
* });
*
* // Each frame:
* worker.drainResults();
*/
class RPCWorker {
public:
/// Callback executed on the main thread after work completes.
using MainCb = std::function<void()>;
/// Work function executed on the background thread.
/// Must return a MainCb (may return nullptr to skip main-thread step).
using WorkFn = std::function<MainCb()>;
RPCWorker();
~RPCWorker();
// Non-copyable
RPCWorker(const RPCWorker&) = delete;
RPCWorker& operator=(const RPCWorker&) = delete;
/// Start the worker thread. Safe to call if already running.
void start();
/// Stop the worker thread and join. Pending tasks are discarded.
void stop();
/// Signal the worker thread to stop (non-blocking, no join).
/// Call stop() later to join the thread.
void requestStop();
/// Submit work to run on the background thread.
/// @param work Function that performs blocking I/O and returns a MainCb.
void post(WorkFn work);
/// Drain completed result callbacks on the main thread.
/// Call once per frame from update().
/// @return Number of callbacks executed.
int drainResults();
/// True when there are completed results waiting for the main thread.
bool hasPendingResults() const;
/// True when the worker thread is running.
bool isRunning() const { return running_.load(std::memory_order_relaxed); }
private:
void run(); // worker thread entry point
std::thread thread_;
std::atomic<bool> running_{false};
// ---- Task queue (produced by main thread, consumed by worker) ----
std::mutex taskMtx_;
std::condition_variable taskCv_;
std::deque<WorkFn> tasks_;
// ---- Result queue (produced by worker, consumed by main thread) ----
mutable std::mutex resultMtx_;
std::deque<MainCb> results_;
};
} // namespace rpc
} // namespace dragonx

95
src/rpc/types.h Normal file
View File

@@ -0,0 +1,95 @@
// 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 rpc {
// Transaction types
enum class TxType {
Sent,
Received,
Mined,
Unknown
};
// Transaction info
struct Transaction {
std::string txid;
TxType type = TxType::Unknown;
int64_t timestamp = 0;
std::string address;
double amount = 0.0;
int confirmations = 0;
std::string memo;
std::string from_address;
};
// Peer info
struct Peer {
int64_t id = 0;
std::string address;
std::string tls_cipher;
bool tls_verified = false;
int64_t conntime = 0;
int banscore = 0;
int protocol_version = 0;
std::string subver;
int64_t bytes_sent = 0;
int64_t bytes_received = 0;
double pingtime = 0.0;
};
// Banned peer info
struct BannedPeer {
std::string address;
std::string subnet;
int64_t banned_until = 0;
int64_t asn = 0;
};
// Unspent output (UTXO)
struct UnspentOutput {
std::string address;
std::string txid;
int vout = 0;
double amount = 0.0;
int confirmations = 0;
bool spendable = true;
};
// Address balance
struct AddressBalance {
std::string address;
double balance = 0.0;
bool is_shielded = false;
};
// Blockchain info
struct BlockchainInfo {
int blocks = 0;
int headers = 0;
std::string bestblockhash;
double difficulty = 0.0;
double verificationprogress = 0.0;
bool syncing = false;
};
// Mining info
struct MiningInfo {
int blocks = 0;
double difficulty = 0.0;
double networkhashps = 0.0;
double localhashps = 0.0;
int genproclimit = 0;
bool generate = false;
};
} // namespace rpc
} // namespace dragonx