The Core poll ran z_gettotalbalance twice (minconf 0 and 1), each a whole- wallet scan under dragonxd's cs_main. The spendable (minconf=1) call now runs only after the display call succeeded, and only when something is unconfirmed or pending or the exact total moved: about 95% of polls make one scan instead of two. A forced post-send balance can no longer be lost to a refused enqueue, and pending-send debits aren't re-applied each tick. z_getoperationstatus is filtered to our opids with its real argument form ([[id,...]]; the old ["id"] was rejected), falling back to the unfiltered call if a daemon refuses the filter. The Shield/Merge dialog's own polls are filtered and session-guarded too, and finally reach Done; the dead RPCClient wrappers are deleted. The Merge/Consolidate scope scan becomes a tested state machine: one scan at a time, failures latch behind a Retry button, only "node not ready" retries by itself (10/30/60 s, at most 3), Lite never scans. Its 120 s calls are maintenance calls: every main-thread disconnect aborts one in flight first, so a lifecycle action no longer freezes the UI behind it. An aborted merge that was already sent says it may still go through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
317 lines
14 KiB
C++
317 lines
14 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());
|
|
|
|
// 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. When scrubSource is true (secret-bearing calls), the intermediate
|
|
// parse tree's string values are zeroed before it is discarded — parseRpcResult returns a COPY of
|
|
// the result node, so its own tree would otherwise free the secret un-wiped (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"
|
|
bool connected_ = false;
|
|
std::atomic<bool> abort_{false}; // set cross-thread to abort an in-flight transfer
|
|
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
|