listtransactions was called with the default "" account, which returns only rows in the unnamed account. Transactions tied to a labelled address were left out of the History tab entirely. - All three listtransactions call sites now pass "*", which spans every account, so labelled-address rows appear alongside the rest. - With "*" the daemon also emits "move" accounting rows that have no txid, address or vout. Those are not wallet transactions, so they are skipped and never become blank History rows. No real transaction row is dropped by this filter. Tests cover that a "move" row is skipped while a labelled receive row is kept, and that the transparent collectors now page with "*". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
930 lines
33 KiB
C++
930 lines
33 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
//
|
|
// rpc_client.cpp — JSON-RPC client over HTTPS using libcurl.
|
|
// All calls are blocking; run on RPCWorker threads, never on main thread.
|
|
|
|
#include "rpc_client.h"
|
|
#include "connection.h"
|
|
#include "../config/version.h"
|
|
#include "../util/base64.h"
|
|
|
|
#include <curl/curl.h>
|
|
#include <sodium.h>
|
|
#include <atomic>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <utility>
|
|
#include "../util/logger.h"
|
|
|
|
namespace dragonx {
|
|
namespace rpc {
|
|
|
|
namespace {
|
|
|
|
// Recursively zero every string value in a JSON tree in place — used to wipe a discarded parse tree
|
|
// that held a secret (B7). Operates on the underlying std::string buffers via get_ref.
|
|
// Templated so it works on both nlohmann::json and nlohmann::ordered_json (callRaw uses the latter).
|
|
template <typename J>
|
|
void scrubJsonSecrets(J& j) {
|
|
if (j.is_string()) {
|
|
auto& s = j.template get_ref<std::string&>();
|
|
if (!s.empty()) sodium_memzero(&s[0], s.size());
|
|
} else if (j.is_object() || j.is_array()) {
|
|
for (auto& el : j) scrubJsonSecrets(el);
|
|
}
|
|
}
|
|
|
|
std::mutex g_trace_mutex;
|
|
RPCClient::TraceCallback g_trace_callback;
|
|
std::atomic_bool g_trace_enabled{false};
|
|
thread_local std::string g_trace_source;
|
|
|
|
void emitRpcTrace(const std::string& method)
|
|
{
|
|
if (!g_trace_enabled.load(std::memory_order_relaxed)) return;
|
|
|
|
RPCClient::TraceCallback callback;
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_trace_mutex);
|
|
callback = g_trace_callback;
|
|
}
|
|
if (!callback) return;
|
|
|
|
std::string source = g_trace_source.empty() ? std::string("App") : g_trace_source;
|
|
callback(source, method);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
RPCClient::TraceScope::TraceScope(std::string source)
|
|
: previous_(RPCClient::currentTraceSource())
|
|
{
|
|
RPCClient::setTraceSource(std::move(source));
|
|
}
|
|
|
|
RPCClient::TraceScope::~TraceScope()
|
|
{
|
|
RPCClient::setTraceSource(std::move(previous_));
|
|
}
|
|
|
|
void RPCClient::setTraceCallback(TraceCallback callback)
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_trace_mutex);
|
|
g_trace_callback = std::move(callback);
|
|
}
|
|
|
|
void RPCClient::setTraceEnabled(bool enabled)
|
|
{
|
|
g_trace_enabled.store(enabled, std::memory_order_relaxed);
|
|
}
|
|
|
|
bool RPCClient::isTraceEnabled()
|
|
{
|
|
return g_trace_enabled.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
std::string RPCClient::currentTraceSource()
|
|
{
|
|
return g_trace_source;
|
|
}
|
|
|
|
void RPCClient::setTraceSource(std::string source)
|
|
{
|
|
g_trace_source = std::move(source);
|
|
}
|
|
|
|
// 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;
|
|
// Bound accumulation so a hostile/compromised daemon cannot OOM the client with an unbounded
|
|
// response body. 256 MiB is far above any legitimate JSON-RPC response yet prevents exhaustion.
|
|
static constexpr size_t kMaxRpcResponseBytes = 256u * 1024 * 1024;
|
|
if (userp->size() + totalSize > kMaxRpcResponseBytes) return 0; // short count aborts the transfer
|
|
userp->append((char*)contents, totalSize);
|
|
return totalSize;
|
|
}
|
|
|
|
// curl progress callback: a non-zero return aborts the in-flight transfer. This lets a
|
|
// requestAbort() from another thread (disconnect/shutdown) unblock curl_easy_perform so the
|
|
// UI thread's worker join() returns promptly instead of waiting out the request timeout.
|
|
static int xferInfoCallback(void* clientp, curl_off_t, curl_off_t, curl_off_t, curl_off_t) {
|
|
const auto* self = static_cast<const RPCClient*>(clientp);
|
|
return (self != nullptr && self->abortRequested()) ? 1 : 0;
|
|
}
|
|
|
|
// 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() {
|
|
// Scrub the persistent Basic-auth secret on destruction (disconnect() may not have run). impl_ is
|
|
// still destroyed normally afterward (curl cleanup unchanged). (L-04)
|
|
if (!auth_.empty()) sodium_memzero(auth_.data(), auth_.size());
|
|
}
|
|
|
|
bool RPCClient::connect(const std::string& host, const std::string& port,
|
|
const std::string& user, const std::string& password)
|
|
{
|
|
return connect(host, port, user, password, false);
|
|
}
|
|
|
|
bool RPCClient::connect(const std::string& host, const std::string& port,
|
|
const std::string& user, const std::string& password,
|
|
bool useTls)
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
|
if (!open(host, port, user, password, useTls)) return false;
|
|
return probeConnection(host);
|
|
}
|
|
|
|
bool RPCClient::open(const std::string& host, const std::string& port,
|
|
const std::string& user, const std::string& password,
|
|
bool useTls)
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
|
connected_ = false; // only a successful probe (connect()) sets these
|
|
warming_up_ = false;
|
|
host_ = host;
|
|
port_ = port;
|
|
last_connect_info_ = json();
|
|
|
|
// Create Basic auth header with proper base64 encoding, then wipe the plaintext
|
|
// "user:password" temporary (std::string does not zero its buffer on destruction).
|
|
std::string credentials = user + ":" + password;
|
|
if (!auth_.empty()) sodium_memzero(auth_.data(), auth_.size()); // wipe any prior secret before overwrite (L-04)
|
|
auth_ = util::base64_encode(credentials);
|
|
if (!credentials.empty()) sodium_memzero(credentials.data(), credentials.size());
|
|
|
|
impl_->url = std::string(useTls ? "https://" : "http://") + host + ":" + port + "/";
|
|
VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());
|
|
|
|
// Clean up previous curl handle/headers to avoid leaks on retries
|
|
if (impl_->headers) {
|
|
curl_slist_free_all(impl_->headers);
|
|
impl_->headers = nullptr;
|
|
}
|
|
if (impl_->curl) {
|
|
curl_easy_cleanup(impl_->curl);
|
|
impl_->curl = nullptr;
|
|
}
|
|
|
|
// 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(nullptr, "Content-Type: text/plain");
|
|
std::string auth_header = "Authorization: Basic " + auth_;
|
|
impl_->headers = curl_slist_append(impl_->headers, auth_header.c_str());
|
|
if (!auth_header.empty()) sodium_memzero(auth_header.data(), auth_header.size()); // curl copied it (L-04)
|
|
|
|
// 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);
|
|
// Progress callback so requestAbort() can unblock an in-flight curl_easy_perform.
|
|
clearAbort(); // a fresh connection must not start in the aborted state
|
|
curl_easy_setopt(impl_->curl, CURLOPT_NOPROGRESS, 0L);
|
|
curl_easy_setopt(impl_->curl, CURLOPT_XFERINFOFUNCTION, xferInfoCallback);
|
|
curl_easy_setopt(impl_->curl, CURLOPT_XFERINFODATA, this);
|
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, 30L);
|
|
// Localhost fails fast if nothing is listening; a remote/TLS daemon needs a larger
|
|
// budget for the TCP + TLS handshake over real network latency (1s would spuriously fail).
|
|
const long connectTimeout = Connection::isLocalHost(host) ? 2L : 10L;
|
|
curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, connectTimeout);
|
|
// Loopback RPC carries basic-auth creds + wallet data: never route it through an env proxy
|
|
// (http_proxy / all_proxy). Remote daemons keep proxy support. NOPROXY also overrides no_proxy.
|
|
if (Connection::isLocalHost(host))
|
|
curl_easy_setopt(impl_->curl, CURLOPT_NOPROXY, "*");
|
|
// Enforce TLS certificate + hostname verification explicitly rather than relying on libcurl's
|
|
// build defaults. Harmless on the localhost http:// case; essential for a remote https daemon.
|
|
curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
|
curl_easy_setopt(impl_->curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
|
return true;
|
|
}
|
|
|
|
bool RPCClient::probeConnection(const std::string& host)
|
|
{
|
|
// Test connection with getinfo. Use a SHORT timeout for the probe on localhost: a healthy
|
|
// local daemon answers in milliseconds and a warming one returns -28 just as fast, so a long
|
|
// hang means a wedged/loading occupant — no point blocking the full 30s before we retry and
|
|
// update the UI. (call(timeoutSec) restores the persistent 30s afterwards, so normal RPC calls
|
|
// that legitimately take longer are unaffected.) Remote/TLS daemons keep the full budget.
|
|
const long probeTimeout = Connection::isLocalHost(host) ? 8L : 30L;
|
|
last_connect_kind_ = RpcFailureKind::Other; // until the probe says otherwise
|
|
try {
|
|
json result = call("getinfo", json::array(), probeTimeout);
|
|
if (result.contains("version")) {
|
|
connected_ = true;
|
|
warming_up_ = false;
|
|
warmup_status_.clear();
|
|
last_connect_error_.clear();
|
|
last_connect_kind_ = RpcFailureKind::None;
|
|
last_connect_info_ = result;
|
|
if (connect_error_repeats_ > 0)
|
|
DEBUG_LOGF("Connection failed: the last error repeated %u more time(s)\n", connect_error_repeats_);
|
|
logged_connect_error_.clear();
|
|
connect_error_repeats_ = 0;
|
|
DEBUG_LOGF("Connected to dragonxd v%d\n", result["version"].get<int>());
|
|
return true;
|
|
}
|
|
} catch (const std::exception& e) {
|
|
last_connect_error_ = e.what();
|
|
last_connect_kind_ = rpcFailureKindOf(e);
|
|
// Daemon warmup (Loading block index, Verifying blocks, etc.) is normal startup progress — the
|
|
// daemon is reachable and auth works, it just hasn't finished initializing yet. Mark as
|
|
// connected+warmup so the wallet can show the UI instead of a blocking overlay. Only the
|
|
// JSON-RPC code (-28, RPC_IN_WARMUP) counts: matching the message text would also match a
|
|
// plain-text HTTP body that happens to say "Loading".
|
|
std::string msg = e.what();
|
|
if (last_connect_kind_ == RpcFailureKind::Warmup) {
|
|
connected_ = true;
|
|
warming_up_ = true;
|
|
warmup_status_ = msg;
|
|
DEBUG_LOGF("Daemon warming up: %s\n", msg.c_str());
|
|
return true;
|
|
} else {
|
|
logConnectFailure(msg);
|
|
}
|
|
}
|
|
|
|
connected_ = false;
|
|
warming_up_ = false;
|
|
warmup_status_.clear();
|
|
last_connect_info_ = json();
|
|
return false;
|
|
}
|
|
|
|
// A busy or absent daemon makes every connect() fail the same way, and logging each one flooded the
|
|
// log (60k lines in one incident). Log a new error at once; a repeat only every 100th time.
|
|
void RPCClient::logConnectFailure(const std::string& msg)
|
|
{
|
|
const std::string line = std::string("[") + rpcFailureKindName(last_connect_kind_) + "] " + msg;
|
|
if (line != logged_connect_error_) {
|
|
if (connect_error_repeats_ > 0)
|
|
DEBUG_LOGF("Connection failed: the last error repeated %u more time(s)\n", connect_error_repeats_);
|
|
logged_connect_error_ = line;
|
|
connect_error_repeats_ = 0;
|
|
DEBUG_LOGF("Connection failed %s\n", line.c_str());
|
|
return;
|
|
}
|
|
if (++connect_error_repeats_ % 100 == 0)
|
|
DEBUG_LOGF("Connection failed %s (repeated %u times)\n", line.c_str(), connect_error_repeats_);
|
|
}
|
|
|
|
RpcFailureKind RPCClient::trySendStop(long timeoutSec)
|
|
{
|
|
try {
|
|
TraceScope trace("Daemon lifecycle / Stop command");
|
|
call("stop", json::array(), timeoutSec);
|
|
return RpcFailureKind::None;
|
|
} catch (const std::exception& e) {
|
|
return rpcFailureKindOf(e);
|
|
}
|
|
}
|
|
|
|
json RPCClient::getLastConnectInfo() const
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
|
return last_connect_info_;
|
|
}
|
|
|
|
void RPCClient::requestAbort()
|
|
{
|
|
// Deliberately NOT taking curl_mutex_ — the whole point is to interrupt a call() that is
|
|
// currently holding it inside curl_easy_perform. The atomic is read by xferInfoCallback.
|
|
abort_.store(true, std::memory_order_relaxed);
|
|
}
|
|
|
|
void RPCClient::clearAbort()
|
|
{
|
|
abort_.store(false, std::memory_order_relaxed);
|
|
}
|
|
|
|
void RPCClient::disconnect()
|
|
{
|
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
|
connected_ = false;
|
|
warming_up_ = false;
|
|
warmup_status_.clear();
|
|
last_connect_info_ = json();
|
|
if (impl_->curl) {
|
|
curl_easy_cleanup(impl_->curl);
|
|
impl_->curl = nullptr;
|
|
}
|
|
if (impl_->headers) {
|
|
curl_slist_free_all(impl_->headers);
|
|
impl_->headers = nullptr;
|
|
}
|
|
if (!auth_.empty()) { sodium_memzero(auth_.data(), auth_.size()); auth_.clear(); } // scrub Basic-auth secret (L-04)
|
|
}
|
|
|
|
json RPCClient::makePayload(const std::string& method, const json& params)
|
|
{
|
|
return {
|
|
{"jsonrpc", "1.0"},
|
|
{"id", "ObsidianDragon"},
|
|
{"method", method},
|
|
{"params", params}
|
|
};
|
|
}
|
|
|
|
std::string RPCClient::performCall(const std::string& method, const json& params, long& httpCodeOut)
|
|
{
|
|
if (busy_gate_ && !busy_gate_->admit(method))
|
|
throw RpcTransportError(RpcFailureKind::Deferred, "RPC deferred: the node is busy", CURLE_OK, 0, false);
|
|
emitRpcTrace(method);
|
|
|
|
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) {
|
|
// libcurl reports connect- and reply-phase timeouts with the same code. The request size
|
|
// (reset for every transfer) tells them apart: 0 means it never reached the daemon.
|
|
long requestSize = 0;
|
|
curl_easy_getinfo(impl_->curl, CURLINFO_REQUEST_SIZE, &requestSize);
|
|
const bool sent = requestSize > 0;
|
|
const RpcFailureKind kind = classifyRpcFailure(res, sent, 0, 0, std::string());
|
|
if (busy_gate_) busy_gate_->record(method, kind);
|
|
throw RpcTransportError(kind, "RPC request failed: " + std::string(curl_easy_strerror(res)), res, 0, sent);
|
|
}
|
|
|
|
// Check HTTP response code
|
|
httpCodeOut = 0;
|
|
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &httpCodeOut);
|
|
if (busy_gate_) {
|
|
// Any HTTP answer means a daemon worker ran our request, except the queue-full refusal.
|
|
const bool queueFull = httpCodeOut == 500 &&
|
|
response_data.find("Work queue depth exceeded") != std::string::npos;
|
|
busy_gate_->record(method, queueFull ? RpcFailureKind::QueueFull : RpcFailureKind::None);
|
|
}
|
|
|
|
return response_data;
|
|
}
|
|
|
|
void RPCClient::throwHttpFailure(long httpCode, const std::string& body, bool hideBody)
|
|
{
|
|
// Bitcoin/Hush RPC returns HTTP 500 for application-level errors (insufficient funds, bad
|
|
// params, etc.), and 400/404 for a bad request / unknown method, with a JSON body carrying the
|
|
// real message. Parse the body first to extract it.
|
|
bool isJson = false;
|
|
try {
|
|
json response = json::parse(body);
|
|
isJson = true;
|
|
if (response.contains("error") && response["error"].is_object()) {
|
|
int errCode = 0;
|
|
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
|
|
errCode = response["error"]["code"].get<int>();
|
|
if (response["error"].contains("message") && response["error"]["message"].is_string())
|
|
throw RpcError(errCode, response["error"]["message"].get<std::string>(), httpCode);
|
|
// message missing/non-string — keep the detail instead of a bare HTTP code
|
|
throw RpcError(errCode, "RPC error: " + response["error"].dump(), httpCode);
|
|
}
|
|
} catch (const json::exception&) {
|
|
// Not JSON: the daemon's HTTP layer refused the request itself ("Work queue depth exceeded",
|
|
// "Unhandled request") or sent a 401/503 page.
|
|
}
|
|
const std::string plain = isJson ? std::string() : body;
|
|
throw RpcTransportError(classifyRpcFailure(CURLE_OK, true, httpCode, 0, plain),
|
|
formatHttpError(httpCode, hideBody ? std::string() : plain),
|
|
CURLE_OK, httpCode, true);
|
|
}
|
|
|
|
json RPCClient::parseRpcResult(long httpCode, const std::string& body, bool scrubSource)
|
|
{
|
|
if (httpCode != 200) throwHttpFailure(httpCode, body, /*hideBody=*/scrubSource);
|
|
|
|
json response = json::parse(body);
|
|
|
|
if (response.contains("error") && !response["error"].is_null()) {
|
|
int errCode = 0;
|
|
std::string err_msg;
|
|
if (response["error"].is_object()) {
|
|
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
|
|
errCode = response["error"]["code"].get<int>();
|
|
if (response["error"].contains("message") && response["error"]["message"].is_string())
|
|
err_msg = response["error"]["message"].get<std::string>();
|
|
}
|
|
if (err_msg.empty()) err_msg = response["error"].dump();
|
|
throw RpcError(errCode, "RPC error: " + err_msg, httpCode);
|
|
}
|
|
|
|
json result = std::move(response["result"]); // move the subtree out: the secret now lives only here
|
|
if (scrubSource) scrubJsonSecrets(response); // response["result"] is now moved-from (null); harmless (B7)
|
|
return result;
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
long http_code = 0;
|
|
std::string response_data = performCall(method, params, http_code);
|
|
return parseRpcResult(http_code, response_data);
|
|
}
|
|
|
|
json RPCClient::callSecret(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");
|
|
}
|
|
|
|
// Zero the raw response body whether parsing succeeds or throws — it holds the secret in the
|
|
// clear (the curl write buffer performCall returns), and would otherwise be freed un-wiped (B7).
|
|
long http_code = 0;
|
|
std::string response_data = performCall(method, params, http_code);
|
|
try {
|
|
json result = parseRpcResult(http_code, response_data, /*scrubSource=*/true);
|
|
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
|
return result;
|
|
} catch (...) {
|
|
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
|
throw;
|
|
}
|
|
}
|
|
|
|
std::string RPCClient::callSecretString(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");
|
|
}
|
|
|
|
long http_code = 0;
|
|
std::string response_data = performCall(method, params, http_code);
|
|
try {
|
|
json result = parseRpcResult(http_code, response_data, /*scrubSource=*/true);
|
|
std::string out;
|
|
if (result.is_string()) {
|
|
// Copy the secret out, then zero the json node's OWN heap buffer — parseRpcResult's
|
|
// json::parse allocates this independently of response_data, so scrubbing the raw body
|
|
// alone would leave it behind (B7). `out` is now the only live copy; the caller wipes it.
|
|
auto& s = result.get_ref<std::string&>();
|
|
out = s;
|
|
if (!s.empty()) sodium_memzero(&s[0], s.size());
|
|
}
|
|
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
|
if (!result.is_string()) throw std::runtime_error("RPC result is not a string");
|
|
return out;
|
|
} catch (...) {
|
|
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
|
throw;
|
|
}
|
|
}
|
|
|
|
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 {
|
|
long http_code = 0;
|
|
std::string response_data = performCall(method, params, http_code);
|
|
// Restore original timeout
|
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
|
return parseRpcResult(http_code, response_data);
|
|
} 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");
|
|
}
|
|
|
|
long http_code = 0;
|
|
std::string response_data = performCall(method, params, http_code);
|
|
if (http_code != 200) throwHttpFailure(http_code, response_data, /*hideBody=*/false);
|
|
|
|
// 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()) {
|
|
// A daemon error object normally has a string "message", but don't assume it — a malformed
|
|
// error (missing/non-string message) must yield a clean RPC error, not a json type-exception.
|
|
const auto& err = oj["error"];
|
|
std::string err_msg;
|
|
int errCode = 0;
|
|
if (err.is_object() && err.contains("code") && err["code"].is_number_integer())
|
|
errCode = err["code"].get<int>();
|
|
if (err.is_object() && err.contains("message") && err["message"].is_string())
|
|
err_msg = err["message"].get<std::string>();
|
|
else
|
|
err_msg = err.dump();
|
|
throw RpcError(errCode, "RPC error: " + err_msg, http_code);
|
|
}
|
|
|
|
auto& result = oj["result"];
|
|
std::string out;
|
|
if (result.is_null()) {
|
|
out = "null";
|
|
} else if (result.is_string()) {
|
|
// Return the raw string (not JSON-encoded) — caller wraps as needed
|
|
out = result.get<std::string>();
|
|
} else {
|
|
out = result.dump(4);
|
|
}
|
|
// B7: this raw path serves arbitrary console commands including dumpprivkey / z_exportkey,
|
|
// whose response carries plaintext key material. Zero the raw buffer and the parsed tree so
|
|
// the secret does not linger in freed heap (matching callSecret). The single returned copy is
|
|
// the caller's to manage.
|
|
scrubJsonSecrets(oj);
|
|
if (!response_data.empty()) sodium_memzero(&response_data[0], response_data.size());
|
|
return out;
|
|
}
|
|
|
|
std::string RPCClient::callRaw(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 the timeout, as call(method, params, timeoutSec) does
|
|
long prevTimeout = 30L;
|
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);
|
|
|
|
try {
|
|
std::string out = callRaw(method, params);
|
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
|
return out;
|
|
} catch (...) {
|
|
// Ensure timeout is always restored
|
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
// hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
|
|
doRPC("rescan", {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_listReceivedByAddress(const std::string& address, int minconf, Callback cb, ErrorCallback err)
|
|
{
|
|
doRPC("z_listreceivedbyaddress", {address, minconf}, cb, err);
|
|
}
|
|
|
|
// Adapts a UnifiedCallback into the (Callback, ErrorCallback) pair doRPC expects: the
|
|
// success lambda forwards (result, ""), the error lambda forwards ({}, error). The cb
|
|
// null-check is preserved on each invocation.
|
|
std::pair<Callback, ErrorCallback> RPCClient::splitUnified(UnifiedCallback cb)
|
|
{
|
|
return {
|
|
[cb](const json& result) {
|
|
if (cb) cb(result, "");
|
|
},
|
|
[cb](const std::string& error) {
|
|
if (cb) cb(json{}, error);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Unified callback versions
|
|
void RPCClient::getInfo(UnifiedCallback cb)
|
|
{
|
|
auto handlers = splitUnified(std::move(cb));
|
|
doRPC("getinfo", {}, handlers.first, handlers.second);
|
|
}
|
|
|
|
void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb)
|
|
{
|
|
// hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
|
|
auto handlers = splitUnified(std::move(cb));
|
|
doRPC("rescan", {startHeight}, handlers.first, handlers.second);
|
|
}
|
|
|
|
void RPCClient::z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
|
|
double fee, int limit, UnifiedCallback cb)
|
|
{
|
|
auto handlers = splitUnified(std::move(cb));
|
|
doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit}, handlers.first, handlers.second);
|
|
}
|
|
|
|
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);
|
|
}
|
|
auto handlers = splitUnified(std::move(cb));
|
|
doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit}, handlers.first, handlers.second);
|
|
}
|
|
|
|
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>();
|
|
auto handlers = splitUnified(cb);
|
|
getBlock(hash, handlers.first, handlers.second);
|
|
},
|
|
[cb](const std::string& error) {
|
|
if (cb) cb(json{}, error);
|
|
}
|
|
);
|
|
}
|
|
|
|
} // namespace rpc
|
|
} // namespace dragonx
|