// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once // Long wallet RPCs (C61). A rescan or a key import holds the daemon's cs_main for as long as the wallet scan // takes: minutes to hours on a big wallet. On the shared clients they used to hit the 30 s curl timeout, report // a failure that wasn't one and flip the app into "node busy" (a reply timeout is recorded by the busy gate). // They now run one at a time on a dedicated, ungated client with no client timeout, abortable from any thread. #include "connection.h" // ConnectionConfig #include "rpc_client.h" #include #include #include namespace dragonx { namespace rpc { // The wallet RPCs that scan the chain inside the call: they run on the long-call lane, never on the shared // clients (rpc_ / the console's fast lane), whose 30 s timeout they outlast. inline bool isLongWalletMethod(const std::string& method) { return method == "rescan" || method == "importprivkey" || method == "importaddress" || method == "importwallet" || method == "z_importkey" || method == "z_importviewingkey" || method == "z_importwallet"; } // How a long wallet call that threw an RpcTransportError ended, which picks the wording shown to the user. enum class LongCallCutOff { None, // the request never reached the node: its error is the whole story Cancelled, // our own abort cut the transfer; the node may still be scanning Interrupted, // the request was sent but no answer came back (a dropped connection, or a proxy's 502/504 // while the node still runs it): the key may already be in the wallet Refused, // the node's HTTP layer turned it away before the method ran (401, 503, queue full, // unhandled), so nothing was imported }; inline LongCallCutOff classifyLongCallCutOff(RpcFailureKind kind, bool requestSent) { switch (kind) { case RpcFailureKind::Aborted: return LongCallCutOff::Cancelled; case RpcFailureKind::Auth: case RpcFailureKind::ShuttingDown: case RpcFailureKind::QueueFull: case RpcFailureKind::Unhandled: return LongCallCutOff::Refused; default: // Any other HTTP status (a proxy's 502/504) or a lost reply proves nothing about the node's side. return requestSent ? LongCallCutOff::Interrupted : LongCallCutOff::None; } } // One long wallet RPC at a time on its own ungated client, with no client timeout, abortable from any thread. class LongCallLane { public: // Main thread: claim the lane. false when a long call is already running. A new call starts un-aborted. bool tryBegin() { bool idle = false; if (!active_.compare_exchange_strong(idle, true)) return false; abort_ = false; return true; } // The call and its continuation are done: the lane is free again. void end() { active_ = false; } bool active() const { return active_.load(); } bool aborted() const { return abort_.load(); } // Any thread. Sticky until the next tryBegin(): a call that has not opened its client yet is aborted as soon // as it does, and an in-flight transfer is cut through the client's progress callback (about a second). void abort() { std::lock_guard lk(mx_); abort_ = true; if (client_) client_->requestAbort(); } // Lane thread. open() FIRST (it clears the client's abort flag), THEN publish the client, THEN re-check the // sticky flag under the same mutex, so an abort that landed before or during open() is never erased. // An open() failure is not handled here: the body's first call then throws "Not connected". template void run(RPCClient& c, const ConnectionConfig& cfg, Body&& body) { c.open(cfg.host, cfg.port, cfg.rpcuser, cfg.rpcpassword, cfg.use_tls); { std::lock_guard lk(mx_); client_ = &c; if (abort_) c.requestAbort(); } struct Unpublish { LongCallLane& lane; ~Unpublish() { std::lock_guard lk(lane.mx_); lane.client_ = nullptr; } } unpublish{*this}; body(c); } private: std::mutex mx_; RPCClient* client_ = nullptr; // guarded by mx_; set only while run() executes the body std::atomic abort_{false}; std::atomic active_{false}; }; } // namespace rpc } // namespace dragonx