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:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user