Files
ObsidianDragon/src/rpc/rpc_client.h
T
DanSandClaude Opus 4.8 998788d351 perf(rpc): move the result subtree out of the parse tree
parseRpcResult copied the "result" node out of the parsed reply,
because indexing the JSON object yields an lvalue reference. On large
replies - full transaction lists, unspent-note sets - that duplicated
the entire subtree for no reason, and it also meant a secret-bearing
result then existed in two trees at once.

- The result node is now moved out of the parse tree instead of
  copied, so the secret lives only in the returned value that the
  calling code already wipes. The leftover result slot is moved-from
  (null), so the existing secret-scrub of the discarded tree still
  runs harmlessly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-14 07:21:07 -05:00

323 lines
15 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "types.h"
#include "rpc_failure.h" // RpcError, RpcTransportError, RpcFailureKind
#include "rpc_busy_gate.h"
#include <atomic>
#include <string>
#include <functional>
#include <memory>
#include <mutex>
#include <stdexcept>
#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:
using TraceCallback = std::function<void(const std::string& source, const std::string& method)>;
class TraceScope {
public:
explicit TraceScope(std::string source);
~TraceScope();
TraceScope(const TraceScope&) = delete;
TraceScope& operator=(const TraceScope&) = delete;
private:
std::string previous_;
};
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);
bool connect(const std::string& host, const std::string& port,
const std::string& user, const std::string& password,
bool useTls);
/**
* @brief Set up the connection without probing it (connect() = open() + a getinfo probe)
*
* For sending "stop": the getinfo probe needs the daemon's cs_main, so against a busy node it
* times out (holding one of the daemon's few RPC slots) or is refused, and stop never went out.
* isConnected() stays false; call() works once this returns true.
*/
bool open(const std::string& host, const std::string& port,
const std::string& user, const std::string& password, bool useTls);
/**
* @brief Send "stop" once, with its own timeout; returns how it went (None = the daemon ran it)
*
* A ReplyTimeout here is NOT a failure: the request reached the daemon and waits for a worker.
*/
RpcFailureKind trySendStop(long timeoutSec = 2);
/**
* @brief Share a busy gate with the app's other polling clients (nullptr: none)
*
* While the gate is busy, calls that need the daemon's cs_main fail at once with a Deferred
* RpcTransportError instead of being sent; every sent call reports how it went to the gate.
*/
void setBusyGate(std::shared_ptr<RpcBusyGate> gate) { busy_gate_ = std::move(gate); }
/**
* @brief Disconnect from dragonxd
*/
void disconnect();
/**
* @brief Check if connected
*/
bool isConnected() const { return connected_; }
/**
* @brief Ask an in-flight call() to abort as soon as possible (thread-safe).
*
* Set from another thread (e.g. before stop()-ing the worker on disconnect/shutdown):
* a curl progress callback aborts the transfer, so a blocked curl_easy_perform returns
* promptly instead of freezing the UI thread's join() until the request timeout. Cleared
* on the next connect(); abortRequested() is read by the progress callback.
*/
void requestAbort();
void clearAbort();
bool abortRequested() const noexcept { return abort_.load(std::memory_order_relaxed); }
/**
* @brief True if the last connect() succeeded but daemon returned a warmup error.
* The curl handle is valid and auth succeeded — RPC calls will throw warmup errors
* until the daemon finishes initializing.
*/
bool isWarmingUp() const { return warming_up_; }
/**
* @brief The warmup status message (e.g. "Activating best chain...").
* Empty when not in warmup.
*/
const std::string& getWarmupStatus() const { return warmup_status_; }
/**
* @brief Get the error message from the last failed connect() attempt.
*/
const std::string& getLastConnectError() const { return last_connect_error_; }
/** @brief How the last connect() probe went: None on success, Warmup for a warming-up node, else the failure. */
RpcFailureKind getLastConnectFailureKind() const { return last_connect_kind_; }
json getLastConnectInfo() const;
static void setTraceCallback(TraceCallback callback);
static void setTraceEnabled(bool enabled);
static bool isTraceEnabled();
static std::string currentTraceSource();
static void setTraceSource(std::string source);
/**
* @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 Like call(), but zeroes the raw HTTP response body after parsing (B7).
*
* For RPCs whose response carries a secret (z_exportmnemonic / z_exportkey), the parsed
* value is scrubbed by the caller — but the raw response string this method builds also
* holds that secret and is otherwise freed without zeroing. Use this variant so the largest,
* longest-lived plaintext copy doesn't linger in freed heap. Slightly slower (an extra wipe);
* only worth it for secret-bearing calls.
*/
json callSecret(const std::string& method, const json& params = json::array());
/**
* @brief callSecret() for RPCs whose result is a bare secret string (z_exportkey / dumpprivkey /
* z_exportviewingkey). Returns the result string with BOTH the raw response body AND the
* parsed json's own copy of the secret zeroed — so no un-scrubbed heap copy survives the
* call. The returned std::string is the only remaining copy; the caller owns it and must
* wipe it (sodium_memzero) when done. Throws if the result is not a JSON string.
*/
std::string callSecretString(const std::string& method, const json& params = json::array());
/**
* @brief Make a raw RPC call with a custom timeout
* @param method RPC method name
* @param params Method parameters
* @param timeoutSec Timeout in seconds (0 = no timeout)
* @return JSON response or throws on error
*/
json call(const std::string& method, const json& params, long timeoutSec);
/**
* @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());
/**
* @brief callRaw() with a custom timeout, restored to the default afterwards (like the 3-arg call())
* @param timeoutSec Timeout in seconds (0 = no timeout)
*/
std::string callRaw(const std::string& method, const json& params, long timeoutSec);
// 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);
// 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 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);
// Shared blocking-call core: dumps the payload, performs the request (throwing on a
// curl transport error) and returns the response body plus HTTP status. Caller must
// hold curl_mutex_ and have verified impl_->curl.
std::string performCall(const std::string& method, const json& params, long& httpCodeOut);
// Centralizes the HTTP-code check and JSON error->RpcError extraction, returning
// response["result"] on success. The result subtree is moved out of the parse tree, so a secret lives
// only in the returned node (which callSecret/callSecretString callers wipe). When scrubSource is true
// (secret-bearing calls), any remaining parse-tree string values are zeroed before it is discarded (B7).
static json parseRpcResult(long httpCode, const std::string& body, bool scrubSource = false);
// A non-200 reply: RpcError when the body carries a JSON-RPC error, else RpcTransportError classified
// from the status and plain-text body. hideBody keeps the body out of the message (secret calls).
[[noreturn]] static void throwHttpFailure(long httpCode, const std::string& body, bool hideBody);
bool probeConnection(const std::string& host); // connect()'s getinfo probe, after open()
void logConnectFailure(const std::string& msg);
// Splits a UnifiedCallback into the (Callback, ErrorCallback) pair used by doRPC.
static std::pair<Callback, ErrorCallback> splitUnified(UnifiedCallback cb);
std::string host_;
std::string port_;
std::string auth_; // Base64 encoded "user:password"
std::atomic<bool> connected_{false};
std::atomic<bool> abort_{false}; // set cross-thread to abort an in-flight transfer
std::atomic<bool> warming_up_{false};
std::string warmup_status_;
std::string last_connect_error_;
RpcFailureKind last_connect_kind_ = RpcFailureKind::None;
std::shared_ptr<RpcBusyGate> busy_gate_;
std::string logged_connect_error_; // last "Connection failed" text logged (rate limit)
unsigned connect_error_repeats_ = 0; // identical failures since it was logged
json last_connect_info_;
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