// 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 #include #include #include #include #include #include namespace dragonx { namespace rpc { using json = nlohmann::json; using Callback = std::function; using ErrorCallback = std::function; /** * @brief JSON-RPC client for dragonxd * * Handles all communication with the dragonxd daemon via JSON-RPC. */ class RPCClient { public: using TraceCallback = std::function; 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 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& 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 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& 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 splitUnified(UnifiedCallback cb); std::string host_; std::string port_; std::string auth_; // Base64 encoded "user:password" std::atomic connected_{false}; std::atomic abort_{false}; // set cross-thread to abort an in-flight transfer std::atomic warming_up_{false}; std::string warmup_status_; std::string last_connect_error_; RpcFailureKind last_connect_kind_ = RpcFailureKind::None; std::shared_ptr 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_; }; } // namespace rpc } // namespace dragonx