feat(explorer,keys): distinguish lookup failures; key-export unlock affordance

- Explorer: a hash lookup now tells a transport/RPC failure apart from a genuine
  not-found (RPC -5/-8), showing "Lookup failed: <friendly error>" instead of
  "no block or transaction found"; block-detail errors routed through
  friendlyRpcError. getblock/getblockhash/getrawtransaction calls unchanged.
- Key-export: when the wallet is locked, the dialog now says "Unlock your wallet
  first" with an Unlock button (lite → requestLiteUnlock; full-node → close to
  reveal the lock screen) instead of a raw error, and the fetching state animates
  with LoadingDots. The export RPC/args are unchanged.

(i18n.cpp carries the additive keys for these and the chat/CSV commits below.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 23:51:06 -05:00
co-authored by Claude Opus 4.8
parent e94d88e723
commit 2e74a3ca69
3 changed files with 34 additions and 6 deletions
+14 -4
View File
@@ -269,7 +269,7 @@ static void fetchBlockDetail(App* app, int height) {
auto hashResult = rpc->call("getblockhash", {height});
std::string hash = hashResult.get<std::string>();
result = rpc->call("getblock", {hash});
} catch (const std::exception& e) { error = e.what(); }
} catch (const std::exception& e) { error = util::friendlyRpcError(e.what()); }
return [result, error]() {
applyBlockDetailResult(result, error);
};
@@ -383,19 +383,27 @@ static void navigateToHash(App* app, const std::string& hash) {
worker->post([rpc, hash]() -> rpc::RPCWorker::MainCb {
json blockResult, txResult;
bool gotBlock = false, gotTx = false;
bool rpcFailed = false; std::string rpcErr;
// Distinguish a genuine "not found" (RPC -5/-8) from a transport/timeout failure, so a flaky
// connection isn't reported as "no block or transaction found".
auto noteErr = [&](const std::exception& e) {
const auto* re = dynamic_cast<const rpc::RpcError*>(&e);
if (re && (re->code == -5 || re->code == -8)) return; // not-found: a real negative answer
if (!rpcFailed) { rpcFailed = true; rpcErr = e.what(); }
};
try {
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
blockResult = rpc->call("getblock", {hash});
gotBlock = true;
} catch (...) {}
} catch (const std::exception& e) { noteErr(e); }
if (!gotBlock) {
try {
rpc::RPCClient::TraceScope trace("Explorer tab / Search");
txResult = rpc->call("getrawtransaction", {hash, 1});
gotTx = true;
} catch (...) {}
} catch (const std::exception& e) { noteErr(e); }
}
return [blockResult, txResult, gotBlock, gotTx]() {
return [blockResult, txResult, gotBlock, gotTx, rpcFailed, rpcErr]() {
s_search_loading = false;
if (gotBlock) {
int h = blockResult.value("height", -1);
@@ -407,6 +415,8 @@ static void navigateToHash(App* app, const std::string& hash) {
s_tx_loading = false;
s_expanded_tx_idx = 0;
s_show_detail_modal = false;
} else if (rpcFailed) {
s_search_error = std::string(TR("explorer_lookup_failed")) + ": " + util::friendlyRpcError(rpcErr);
} else {
s_search_error = TR("explorer_hash_not_found");
}
+12 -2
View File
@@ -121,7 +121,7 @@ void KeyExportDialog::render(App* app)
ImGui::Text("%s", key_label);
if (s_fetching) {
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "%s", TR("key_export_fetching"));
ImGui::TextColored(material::WarningVec4(), "%s%s", TR("key_export_fetching_base"), material::LoadingDots());
} else if (!s_error.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error.c_str());
// Not a dead-end: clearing the error falls back through to the Reveal button next frame.
@@ -133,6 +133,16 @@ void KeyExportDialog::render(App* app)
// Guard against an empty/blatantly-malformed address before dispatching a doomed reveal.
// (Also avoids reading s_address[0] on an empty string in the Reveal branch below.)
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("invalid_address"));
} else if (s_key.empty() && app->getWalletState().isLocked()) {
// Reveal would just fail with a locked-wallet RPC error — say so and offer to unlock.
ImGui::PushStyleColor(ImGuiCol_Text, material::Warning());
ImGui::TextWrapped("%s", TR("key_export_unlock_first"));
ImGui::PopStyleColor();
ImGui::Spacing();
if (material::TactileButton(TR("lite_unlock"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
if (app->liteWallet()) app->requestLiteUnlock();
else s_open = false; // full node: closing reveals the lock screen already shown while locked
}
} else if (s_key.empty()) {
// Show button to fetch key
if (material::TactileButton(TR("key_export_reveal"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) {
@@ -173,7 +183,7 @@ void KeyExportDialog::render(App* app)
s_key = found;
s_show_key = wantViewing; // viewing keys are less sensitive
} else {
s_error = r.ok ? std::string(TR("grpc_key_not_available")) : r.error;
s_error = r.ok ? std::string(TR("grpc_key_not_available")) : util::friendlyRpcError(r.error);
}
wallet::secureWipeLiteSecret(found);
s_fetching = false;