Files
ObsidianDragon/src/rpc/rpc_client.cpp
dan_s 0ca1caf148 feat: RPC caching, background decrypt import, fast-lane peers, mining fix
RPC client:
- Add call() overload with per-call timeout parameter
- z_exportwallet uses 300s, z_importwallet uses 1200s timeout

Decrypt wallet (app_security.cpp, app.cpp):
- Show per-step and overall elapsed timers during decrypt flow
- Reduce dialog to 5 steps; close before key import begins
- Run z_importwallet on detached background thread
- Add pulsing "Importing keys..." status bar indicator
- Report success/failure via notifications instead of dialog

RPC caching (app_network.cpp, app.h):
- Cache z_viewtransaction results in viewtx_cache_ across refresh cycles
- Skip RPC calls for already-cached txids (biggest perf win)
- Build confirmed_tx_cache_ for deeply-confirmed transactions
- Clear all caches on disconnect
- Remove unused refreshTransactions() dead code

Peers (app_network.cpp, peers_tab.cpp):
- Route refreshPeerInfo() through fast_worker_ to avoid head-of-line blocking
- Replace footer "Refresh Peers" button with ICON_MD_REFRESH in toggle header
- Refresh button triggers both peer list and full blockchain data refresh

Mining (mining_tab.cpp):
- Allow pool mining toggle when blockchain is not synced
- Pool mining only needs xmrig, not local daemon sync
2026-03-04 15:12:24 -06:00

659 lines
20 KiB
C++

// 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"];
}
json RPCClient::call(const std::string& method, const json& params, long timeoutSec)
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
if (!impl_->curl) {
throw std::runtime_error("Not connected");
}
// Temporarily override timeout
long prevTimeout = 30L;
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);
try {
// Unlock before calling to avoid recursive lock issues — but we already hold it,
// and call() also locks with recursive_mutex, so just delegate to the body directly.
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);
// Restore original timeout
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
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));
}
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"];
} catch (...) {
// Ensure timeout is always restored
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
throw;
}
}
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