A rescan or key import holds the node's cs_main for the whole wallet scan. On the shared clients it hit the 30 s curl timeout, reported a failure that wasn't one, abandoned a daemon RPC slot and flipped the app into "node busy". - One long wallet RPC at a time (rescan, importprivkey, importaddress, importwallet, z_importkey, z_importviewingkey, z_importwallet) runs on its own ungated client with no client timeout (rpc/long_call_lane.h). It can be aborted from any thread, and an abort is never lost, even one that lands before the client opens. - The GUI rescan, key import and sweep use it. The sweep's fund-moving body is unchanged; only the client it calls differs. runtimeRescan() returns bool, and a refused post-bootstrap reconcile stays armed. - The console routes rescan and the import commands through the lane; its input stays usable meanwhile, and a refusal is printed like any other result. RPCClient gains callRaw(.., timeoutSec). - Restart, wallet switch, delete-chain, bootstrap (now also from Settings) and the other node-stopping flows are refused while it runs, with a message saying a key import or rescan is still running; quit asks for confirmation. The lane is aborted at quit. - A second import or rescan is refused before the key is copied anywhere. An import cut off after the request reached the node (a lost reply, or a proxy's 502/504 while the node still imports) says the key may already be in the wallet and to run Settings -> Rescan. Only the node's own HTTP-layer refusals (401, 503, work queue full, unhandled request) say that the key was not imported. A pure classifyLongCallCutOff() makes that choice. - New strings in all 8 languages; console results stay English. Tests: the long-method list and the console's routing; one lane at a time; an abort lands whether it comes before the client opens or during the call; a call with no client timeout outlasts a 3 s scan, where a 1 s gated client times out and marks the node busy; classifyLongCallCutOff() for every failure kind with and without a sent request, and from raw HTTP replies (502, 504, a plain 500 -> cut off; 401, 503, queue full, unhandled -> refused). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
4.5 KiB
C++
114 lines
4.5 KiB
C++
// 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 <atomic>
|
|
#include <mutex>
|
|
#include <string>
|
|
|
|
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<std::mutex> 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 <class Body>
|
|
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<std::mutex> lk(mx_);
|
|
client_ = &c;
|
|
if (abort_) c.requestAbort();
|
|
}
|
|
struct Unpublish {
|
|
LongCallLane& lane;
|
|
~Unpublish()
|
|
{
|
|
std::lock_guard<std::mutex> 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<bool> abort_{false};
|
|
std::atomic<bool> active_{false};
|
|
};
|
|
|
|
} // namespace rpc
|
|
} // namespace dragonx
|