// 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 #include #include #include #include #include #include "../util/logger.h" namespace dragonx { namespace rpc { namespace { 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 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 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; 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(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()) { } RPCClient::~RPCClient() = default; 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 lk(curl_mutex_); 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; 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()); // 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); // 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; 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_info_ = result; DEBUG_LOGF("Connected to dragonxd v%d\n", result["version"].get()); return true; } } catch (const std::exception& e) { last_connect_error_ = e.what(); // Daemon warmup messages (Loading block index, Verifying blocks, etc.) // are 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. std::string msg = e.what(); // Warmup is JSON-RPC error code -28 (RPC_IN_WARMUP) — the robust signal. Fall back // to message substrings for any path that didn't carry the numeric code. int code = 0; if (const auto* re = dynamic_cast(&e)) code = re->code; bool isWarmup = (code == -28) || (msg.find("Loading") != std::string::npos || msg.find("Verifying") != std::string::npos || msg.find("Activating") != std::string::npos || msg.find("Rewinding") != std::string::npos || msg.find("Rescanning") != std::string::npos || msg.find("Pruning") != std::string::npos); if (isWarmup) { connected_ = true; warming_up_ = true; warmup_status_ = msg; DEBUG_LOGF("Daemon warming up: %s\n", msg.c_str()); return true; } else { DEBUG_LOGF("Connection failed: %s\n", msg.c_str()); } } connected_ = false; warming_up_ = false; warmup_status_.clear(); last_connect_info_ = json(); return false; } json RPCClient::getLastConnectInfo() const { std::lock_guard 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 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; } } 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 lk(curl_mutex_); if (!impl_->curl) { throw std::runtime_error("Not connected"); } 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) { 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) { int errCode = 0; try { json response = json::parse(response_data); if (response.contains("error") && response["error"].is_object()) { if (response["error"].contains("code") && response["error"]["code"].is_number_integer()) errCode = response["error"]["code"].get(); if (response["error"].contains("message") && response["error"]["message"].is_string()) throw RpcError(errCode, response["error"]["message"].get()); // message missing/non-string — keep the detail instead of a bare HTTP code throw RpcError(errCode, "RPC error: " + response["error"].dump()); } } catch (const json::exception&) { // Body wasn't valid JSON — fall through to generic HTTP error } throw RpcError(errCode, "RPC error: HTTP " + std::to_string(http_code)); } json response = json::parse(response_data); 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(); if (response["error"].contains("message") && response["error"]["message"].is_string()) err_msg = response["error"]["message"].get(); } if (err_msg.empty()) err_msg = response["error"].dump(); throw RpcError(errCode, "RPC error: " + err_msg); } return response["result"]; } json RPCClient::call(const std::string& method, const json& params, long timeoutSec) { std::lock_guard lk(curl_mutex_); if (!impl_->curl) { throw std::runtime_error("Not connected"); } emitRpcTrace(method); // 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) { int errCode = 0; try { json response = json::parse(response_data); if (response.contains("error") && response["error"].is_object()) { if (response["error"].contains("code") && response["error"]["code"].is_number_integer()) errCode = response["error"]["code"].get(); if (response["error"].contains("message") && response["error"]["message"].is_string()) throw RpcError(errCode, response["error"]["message"].get()); throw RpcError(errCode, "RPC error: " + response["error"].dump()); } } catch (const json::exception&) {} throw RpcError(errCode, "RPC error: HTTP " + std::to_string(http_code)); } json response = json::parse(response_data); 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(); if (response["error"].contains("message") && response["error"]["message"].is_string()) err_msg = response["error"]["message"].get(); } if (err_msg.empty()) err_msg = response["error"].dump(); throw RpcError(errCode, "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 lk(curl_mutex_); if (!impl_->curl) { throw std::runtime_error("Not connected"); } emitRpcTrace(method); 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(); 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(); 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(); } 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& 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& 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& 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& 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& 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(); 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