fix(rpc): detect mid-session disconnects and stop blocking the UI thread

The connection state machine never tore down on a lost connection: refresh-loop
RPC errors were swallowed, rpc_->isConnected() stayed true after a daemon
crash/restart/socket drop, and the UI showed stale balances with no reconnect.
Several operations also ran synchronous curl straight from ImGui handlers.

- Add handleLostConnection(): after N consecutive cycles where BOTH core RPCs
  fail (warmup excluded, so no reconnect loop), disconnect so update()'s
  reconnect branch re-enters tryConnect().
- Move banPeer/unbanPeer/clearBans and key export/import onto the worker thread
  (import requests a rescan that could freeze the UI for the curl timeout).
- Run the block-info dialog's two chained RPCs on the worker thread (+ guard the
  getblockhash result type).
- Detect daemon warmup via the JSON-RPC -28 code (new RpcError carrying the code;
  message text preserved so 401/warmup string-matching is unaffected), and widen
  CONNECTTIMEOUT to 10s for remote/TLS hosts (2s localhost).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 14:17:17 -05:00
parent 1bc7f5c8cd
commit 53a10e149d
5 changed files with 221 additions and 78 deletions

View File

@@ -5,6 +5,7 @@
#include "block_info_dialog.h"
#include "../../app.h"
#include "../../rpc/rpc_client.h"
#include "../../rpc/rpc_worker.h"
#include "../../util/i18n.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
@@ -124,14 +125,31 @@ void BlockInfoDialog::render(App* app)
}
if (material::StyledButton(TR("block_get_info"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
if (rpc && rpc->isConnected()) {
if (rpc && rpc->isConnected() && app->worker()) {
s_loading = true;
s_error.clear();
s_has_data = false;
s_pending_app = app;
// Use getBlock(height) which uses UnifiedCallback
rpc->getBlock(s_height, handleBlockResponseUnified);
// Run the two chained RPCs (getblockhash → getblock) on the worker thread;
// doing them inline froze the UI for two round-trips. Guard the hash type.
int height = s_height;
app->worker()->post([rpc, height]() -> rpc::RPCWorker::MainCb {
json block;
std::string error;
try {
rpc::RPCClient::TraceScope trace("Explorer / Block info");
auto hashResult = rpc->call("getblockhash", {height});
if (!hashResult.is_string()) {
error = "unexpected getblockhash result";
} else {
block = rpc->call("getblock", {hashResult.get<std::string>()});
}
} catch (const std::exception& e) {
error = e.what();
}
return [block, error]() { handleBlockResponseUnified(block, error); };
});
}
}