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}); auto hashResult = rpc->call("getblockhash", {height});
std::string hash = hashResult.get<std::string>(); std::string hash = hashResult.get<std::string>();
result = rpc->call("getblock", {hash}); 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]() { return [result, error]() {
applyBlockDetailResult(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 { worker->post([rpc, hash]() -> rpc::RPCWorker::MainCb {
json blockResult, txResult; json blockResult, txResult;
bool gotBlock = false, gotTx = false; 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 { try {
rpc::RPCClient::TraceScope trace("Explorer tab / Search"); rpc::RPCClient::TraceScope trace("Explorer tab / Search");
blockResult = rpc->call("getblock", {hash}); blockResult = rpc->call("getblock", {hash});
gotBlock = true; gotBlock = true;
} catch (...) {} } catch (const std::exception& e) { noteErr(e); }
if (!gotBlock) { if (!gotBlock) {
try { try {
rpc::RPCClient::TraceScope trace("Explorer tab / Search"); rpc::RPCClient::TraceScope trace("Explorer tab / Search");
txResult = rpc->call("getrawtransaction", {hash, 1}); txResult = rpc->call("getrawtransaction", {hash, 1});
gotTx = true; 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; s_search_loading = false;
if (gotBlock) { if (gotBlock) {
int h = blockResult.value("height", -1); int h = blockResult.value("height", -1);
@@ -407,6 +415,8 @@ static void navigateToHash(App* app, const std::string& hash) {
s_tx_loading = false; s_tx_loading = false;
s_expanded_tx_idx = 0; s_expanded_tx_idx = 0;
s_show_detail_modal = false; s_show_detail_modal = false;
} else if (rpcFailed) {
s_search_error = std::string(TR("explorer_lookup_failed")) + ": " + util::friendlyRpcError(rpcErr);
} else { } else {
s_search_error = TR("explorer_hash_not_found"); 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); ImGui::Text("%s", key_label);
if (s_fetching) { 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()) { } else if (!s_error.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), TR("error_format"), s_error.c_str()); 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. // 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. // 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.) // (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")); 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()) { } else if (s_key.empty()) {
// Show button to fetch key // Show button to fetch key
if (material::TactileButton(TR("key_export_reveal"), ImVec2(revealBtn.width, 0), S.resolveFont(revealBtn.font))) { 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_key = found;
s_show_key = wantViewing; // viewing keys are less sensitive s_show_key = wantViewing; // viewing keys are less sensitive
} else { } 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); wallet::secureWipeLiteSecret(found);
s_fetching = false; s_fetching = false;
+8
View File
@@ -268,6 +268,10 @@ void I18n::loadBuiltinEnglish()
strings_["chat_retry"] = "Retry"; strings_["chat_retry"] = "Retry";
strings_["chat_jump_latest"] = "Latest"; strings_["chat_jump_latest"] = "Latest";
strings_["chat_empty_title"] = "No conversations yet"; strings_["chat_empty_title"] = "No conversations yet";
strings_["chat_intro_title"] = "How chat works";
strings_["chat_intro_body"] = "Messages are end-to-end encrypted and sent as shielded transactions, so only you and your contact can read them. Your chat identity is tied to your wallet seed, so restoring your seed restores your chat address.";
strings_["chat_intro_fee"] = "Each message costs a small network fee. Share your chat address to let people start a conversation with you.";
strings_["chat_intro_dismiss"] = "Got it";
strings_["chat_empty_start"] = "Start one with \"New conversation\"."; strings_["chat_empty_start"] = "Start one with \"New conversation\".";
strings_["chat_copy_my_address"] = "Copy my chat address"; strings_["chat_copy_my_address"] = "Copy my chat address";
strings_["chat_my_address_tip"] = "Share this address so others can message you"; strings_["chat_my_address_tip"] = "Share this address so others can message you";
@@ -2126,6 +2130,7 @@ void I18n::loadBuiltinEnglish()
strings_["export_tx_file_fail"] = "Failed to create CSV file"; strings_["export_tx_file_fail"] = "Failed to create CSV file";
strings_["export_tx_none"] = "No transactions to export"; strings_["export_tx_none"] = "No transactions to export";
strings_["export_tx_success"] = "Transactions exported successfully"; strings_["export_tx_success"] = "Transactions exported successfully";
strings_["export_tx_open_folder"] = "Open folder";
strings_["export_tx_title"] = "Export Transactions to CSV"; strings_["export_tx_title"] = "Export Transactions to CSV";
// --- Import Key Dialog --- // --- Import Key Dialog ---
@@ -2177,6 +2182,8 @@ void I18n::loadBuiltinEnglish()
// --- Key Export Dialog --- // --- Key Export Dialog ---
strings_["key_export_fetching"] = "Fetching key from wallet..."; strings_["key_export_fetching"] = "Fetching key from wallet...";
strings_["key_export_fetching_base"] = "Fetching key from wallet";
strings_["key_export_unlock_first"] = "Unlock your wallet first, then reveal the key.";
strings_["key_export_failed"] = "Couldn't export the key \xE2\x80\x94 unlock the wallet (if encrypted) and try again."; strings_["key_export_failed"] = "Couldn't export the key \xE2\x80\x94 unlock the wallet (if encrypted) and try again.";
strings_["key_export_private_key"] = "Private Key:"; strings_["key_export_private_key"] = "Private Key:";
strings_["key_export_private_warning"] = "Keep this key SECRET! Anyone with this key can spend your funds. Never share it online or with untrusted parties."; strings_["key_export_private_warning"] = "Keep this key SECRET! Anyone with this key can spend your funds. Never share it online or with untrusted parties.";
@@ -2846,6 +2853,7 @@ void I18n::loadBuiltinEnglish()
strings_["explorer_invalid_query"] = "Enter a block height or 64-character hash"; strings_["explorer_invalid_query"] = "Enter a block height or 64-character hash";
strings_["explorer_invalid_response"] = "Invalid response from daemon"; strings_["explorer_invalid_response"] = "Invalid response from daemon";
strings_["explorer_hash_not_found"] = "No block or transaction found for this hash"; strings_["explorer_hash_not_found"] = "No block or transaction found for this hash";
strings_["explorer_lookup_failed"] = "Lookup failed";
strings_["explorer_not_connected"] = "Not connected to daemon — cannot look up a block or transaction hash"; strings_["explorer_not_connected"] = "Not connected to daemon — cannot look up a block or transaction hash";
strings_["explorer_no_results"] = "No matching cached blocks"; strings_["explorer_no_results"] = "No matching cached blocks";
strings_["explorer_beyond_tip"] = "Block %d is beyond the chain tip (%d)"; strings_["explorer_beyond_tip"] = "Block %d is beyond the chain tip (%d)";