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:
224
src/rpc/connection.cpp
Normal file
224
src/rpc/connection.cpp
Normal 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
80
src/rpc/connection.h
Normal 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
599
src/rpc/rpc_client.cpp
Normal 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
179
src/rpc/rpc_client.h
Normal 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
135
src/rpc/rpc_worker.cpp
Normal 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
93
src/rpc/rpc_worker.h
Normal 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
95
src/rpc/types.h
Normal 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
|
||||
Reference in New Issue
Block a user