From 46f30013607be063e026f307e837e156b6e3257f Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 13 Jul 2026 12:58:36 -0500 Subject: [PATCH] feat(settings): redesign the Import Key dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework the full-node Import Key dialog into a safer, Material-consistent, more capable flow: - Security: the key is masked by default (Password) with a reveal (eye) toggle, a DialogWarningHeader "only import a key you own" note, and the buffer is wiped with sodium_memzero on close and on successful import. - Progress: import triggers a blocking full-chain rescan; the dialog now shows "Importing & rescanning — this can take several minutes" with LoadingDots and disables Import while it runs (no double-submit). Offline shows a "connect a running node" note and disables Import up front. - Viewing keys: the classifier gains isViewingKey / isRecognizedImportKey; importPrivateKey auto-detects a shielded viewing key (zxviews…) and routes to z_importviewingkey (watch-only) vs z_importkey / importprivkey. The live type indicator shows Transparent / Shielded spending / Shielded viewing (watch-only). - Result: on success the imported address is captured from the RPC result and shown via AddressCopyField. - Material restyle (OverlayDialogSpec/BlurFloat + TactileButton + Esc) and full i18n (12 new keys x 8 languages; CJK subset rebuilt). Verified: rendered on dark + light skins; a seeded zxviews… key shows the masked field + "Shielded viewing key (watch-only)" indicator. Co-Authored-By: Claude Opus 4.8 (1M context) --- res/fonts/NotoSansCJK-Subset.ttf | Bin 622296 -> 624100 bytes res/lang/de.json | 12 ++ res/lang/es.json | 12 ++ res/lang/fr.json | 12 ++ res/lang/ja.json | 12 ++ res/lang/ko.json | 12 ++ res/lang/pt.json | 12 ++ res/lang/ru.json | 12 ++ res/lang/zh.json | 12 ++ src/app.cpp | 184 ++++++++++++-------- src/app.h | 8 +- src/app_network.cpp | 41 +++-- src/services/wallet_security_controller.cpp | 15 +- src/services/wallet_security_controller.h | 6 +- src/util/i18n.cpp | 12 ++ 15 files changed, 274 insertions(+), 88 deletions(-) diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index 0564b4ab7aae5ea657f43fe6d79e1b0dbd7a15d2..7226775926a176266c536a73201d58e8172a36a2 100644 GIT binary patch delta 21112 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 C45=>w delta 19341 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 float { - auto it = dlg.extraFloats.find(key); - return it != dlg.extraFloats.end() ? it->second : fb; - }; - - int btnFont = (int)dlgF("button-font", 1); - float btnW = dlgF("button-width", 120.0f); - - if (!ui::material::BeginOverlayDialog("Import Private Key", &show_import_key_, dlgF("width", 500.0f), 0.94f)) { - return; + namespace m = ui::material; + float dp = ui::Layout::dpiScale(); + + m::OverlayDialogSpec ov; + ov.title = TR("import_key_title"); + ov.p_open = &show_import_key_; + ov.style = m::OverlayStyle::BlurFloat; + ov.cardWidth = 520.0f; + ov.idSuffix = "importkey"; + if (!m::BeginOverlayDialog(ov)) return; + + // Fresh dialog on each open (the key buffer was already wiped on the last close). + if (ImGui::IsWindowAppearing()) { + import_success_ = false; + import_status_.clear(); + import_result_address_.clear(); + import_key_reveal_ = false; } - - ImGui::TextWrapped("Enter a private key to import. The wallet will rescan the blockchain for transactions."); + // Esc dismisses (this overlay has no built-in Esc handling). + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) show_import_key_ = false; + + // Handle-with-care note. + m::DialogWarningHeader(TR("import_key_warn")); ImGui::Spacing(); - - ImGui::Text("Private Key:"); - ImGui::SetNextItemWidth(-1); - ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_)); - - // Paste & Clear buttons - if (ui::material::StyledButton(TR("paste"), ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) { + + // A running node is required — say so up front instead of failing only after Import. + if (!state_.connected) { + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s", TR("import_key_need_node")); + ImGui::Spacing(); + } + + // Masked key field with a reveal (eye) toggle + Paste/Clear. + ImGui::TextUnformatted(TR("import_key_field")); + float eyeW = ImGui::GetFrameHeight(); + ImGui::SetNextItemWidth(-(eyeW + ui::Layout::spacingSm())); + ImGuiInputTextFlags flags = import_key_reveal_ ? 0 : ImGuiInputTextFlags_Password; + ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_), flags); + ImGui::SameLine(0, ui::Layout::spacingSm()); + if (m::TactileButton(import_key_reveal_ ? ICON_MD_VISIBILITY_OFF : ICON_MD_VISIBILITY, + ImVec2(eyeW, eyeW), m::Type().iconSmall())) + import_key_reveal_ = !import_key_reveal_; + if (ImGui::IsItemHovered()) m::Tooltip("%s", TR("import_key_reveal_tip")); + + if (m::TactileButton(TR("paste"))) { const char* clipboard = ImGui::GetClipboardText(); if (clipboard) { - snprintf(import_key_input_, sizeof(import_key_input_), "%s", clipboard); - // Trim whitespace - std::string trimmed(import_key_input_); - while (!trimmed.empty() && (trimmed.front() == ' ' || trimmed.front() == '\t' || - trimmed.front() == '\n' || trimmed.front() == '\r')) - trimmed.erase(trimmed.begin()); - while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t' || - trimmed.back() == '\n' || trimmed.back() == '\r')) - trimmed.pop_back(); + std::string trimmed(clipboard); + while (!trimmed.empty() && (trimmed.front()==' '||trimmed.front()=='\t'||trimmed.front()=='\n'||trimmed.front()=='\r')) trimmed.erase(trimmed.begin()); + while (!trimmed.empty() && (trimmed.back()==' '||trimmed.back()=='\t'||trimmed.back()=='\n'||trimmed.back()=='\r')) trimmed.pop_back(); snprintf(import_key_input_, sizeof(import_key_input_), "%s", trimmed.c_str()); } } ImGui::SameLine(); - if (ui::material::StyledButton(TR("clear"), ImVec2(0, 0), ui::material::resolveButtonFont(btnFont))) { - memset(import_key_input_, 0, sizeof(import_key_input_)); - } - - // Trimmed view for validation (importPrivateKey trims again before the RPC). The indicator and the - // Import-button guard below both derive from the SAME shared classifier, so they can't disagree. + if (m::TactileButton(TR("clear"))) sodium_memzero(import_key_input_, sizeof(import_key_input_)); + + // Trimmed view + classification. The indicator and the Import guard share isRecognizedImportKey. std::string keyTrim(import_key_input_); while (!keyTrim.empty() && (keyTrim.front()==' '||keyTrim.front()=='\t'||keyTrim.front()=='\n'||keyTrim.front()=='\r')) keyTrim.erase(keyTrim.begin()); while (!keyTrim.empty() && (keyTrim.back()==' '||keyTrim.back()=='\t'||keyTrim.back()=='\n'||keyTrim.back()=='\r')) keyTrim.pop_back(); - bool keyRecognized = services::WalletSecurityController::isRecognizedPrivateKey(keyTrim); - bool keyIsZ = services::WalletSecurityController::classifyPrivateKey(keyTrim) - == services::WalletSecurityController::KeyKind::Shielded; + bool recognized = services::WalletSecurityController::isRecognizedImportKey(keyTrim); + bool viewing = services::WalletSecurityController::isViewingKey(keyTrim); + bool shielded = services::WalletSecurityController::classifyPrivateKey(keyTrim) + == services::WalletSecurityController::KeyKind::Shielded; - // Key validation indicator if (!keyTrim.empty()) { - if (keyRecognized) { - ImGui::PushFont(ui::material::Type().iconSmall()); - ImGui::TextColored(ui::material::SuccessVec4(), ICON_MD_CHECK_CIRCLE); + if (recognized) { + const char* label = viewing ? TR("import_key_type_zview") + : shielded ? TR("import_key_type_zspend") + : TR("import_key_type_tkey"); + ImGui::PushFont(m::Type().iconSmall()); + ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE); ImGui::PopFont(); - ImGui::SameLine(0, 4.0f); - ImGui::TextColored(ui::material::SuccessVec4(), "%s", - keyIsZ ? "Shielded spending key" : "Transparent private key"); + ImGui::SameLine(0, 4.0f * dp); + ImGui::TextColored(m::SuccessVec4(), "%s", label); } else { - ImGui::PushFont(ui::material::Type().iconSmall()); + ImGui::PushFont(m::Type().iconSmall()); ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), ICON_MD_HELP); ImGui::PopFont(); - ImGui::SameLine(0, 4.0f); - ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "Unrecognized key format"); + ImGui::SameLine(0, 4.0f * dp); + ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), "%s", TR("import_key_type_unknown")); } } - ImGui::Spacing(); - - if (!import_status_.empty()) { - if (import_success_) { - ImGui::TextColored(ui::material::SuccessVec4(), "%s", import_status_.c_str()); - } else { - ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", import_status_.c_str()); + + // Progress / result / error. + if (import_in_progress_) { + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::Primary()), "%s%s", + TR("import_key_rescanning"), m::LoadingDots()); + } else if (import_success_) { + ImGui::PushFont(m::Type().iconSmall()); + ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE); + ImGui::PopFont(); + ImGui::SameLine(0, 4.0f * dp); + ImGui::TextColored(m::SuccessVec4(), "%s", TR("import_key_done")); + if (!import_result_address_.empty()) { + ImGui::Spacing(); + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s", TR("import_key_address")); + ui::widgets::AddressCopyField("##importedaddr", import_result_address_); } + } else if (!import_status_.empty()) { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", import_status_.c_str()); + ImGui::PopTextWrapPos(); } - + ImGui::Spacing(); ImGui::Separator(); - - ImGui::BeginDisabled(!keyRecognized); // only a recognized Z/T key can be imported - if (ui::material::StyledButton("Import", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) { - importPrivateKey(std::string(import_key_input_), [this](bool success, const std::string& msg) { - import_success_ = success; - import_status_ = msg; - if (success) { - memset(import_key_input_, 0, sizeof(import_key_input_)); + ImGui::Spacing(); + + float btnW = 130.0f * dp; + bool canImport = recognized && state_.connected && !import_in_progress_; + ImGui::BeginDisabled(!canImport); + if (m::TactileButton(TR("import_key_import"), ImVec2(btnW, 0))) { + import_in_progress_ = true; + import_success_ = false; + import_status_.clear(); + import_result_address_.clear(); + importPrivateKey(std::string(import_key_input_), + [this](bool ok, const std::string& err, const std::string& addr) { + import_in_progress_ = false; + import_success_ = ok; + if (ok) { + import_result_address_ = addr; + import_status_.clear(); + sodium_memzero(import_key_input_, sizeof(import_key_input_)); // wipe on success + } else { + import_status_ = err; } }); } ImGui::EndDisabled(); ImGui::SameLine(); - if (ui::material::StyledButton("Close", ImVec2(btnW, 0), ui::material::resolveButtonFont(btnFont))) { - show_import_key_ = false; + if (m::TactileButton(TR("close"), ImVec2(btnW, 0))) show_import_key_ = false; + + // Wipe the key the moment the dialog closes (Close button OR outside-click, which clears + // show_import_key_ mid-frame). in_progress is left to the callback to clear. + if (!show_import_key_) { + sodium_memzero(import_key_input_, sizeof(import_key_input_)); import_status_.clear(); - memset(import_key_input_, 0, sizeof(import_key_input_)); + import_result_address_.clear(); + import_success_ = false; + import_key_reveal_ = false; } - - ui::material::EndOverlayDialog(); + + m::EndOverlayDialog(); } void App::renderExportKeyDialog() diff --git a/src/app.h b/src/app.h index 14de6f1..ca5522f 100644 --- a/src/app.h +++ b/src/app.h @@ -300,7 +300,10 @@ public: void exportPrivateKey(const std::string& address, std::function callback); // callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export. void exportAllKeys(std::function callback); - void importPrivateKey(const std::string& key, std::function callback); + // callback(success, errorOrEmpty, importedAddress). address is "" on failure or when the RPC + // returns none; the import routes to z_importviewingkey / z_importkey / importprivkey by key type. + void importPrivateKey(const std::string& key, + std::function callback); // Wallet backup void backupWallet(const std::string& destination, std::function callback); @@ -802,6 +805,9 @@ private: std::string export_address_; std::string import_status_; bool import_success_ = false; + bool import_key_reveal_ = false; // show the key in plaintext (default masked) + bool import_in_progress_ = false; // an import + rescan is running (disable/spinner) + std::string import_result_address_; // address imported on success (shown as a copy field) std::string backup_status_; bool backup_success_ = false; diff --git a/src/app_network.cpp b/src/app_network.cpp index 56a4fba..34076e1 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -2921,10 +2921,11 @@ void App::exportAllKeys(std::function callba } } -void App::importPrivateKey(const std::string& rawKey, std::function callback) +void App::importPrivateKey(const std::string& rawKey, + std::function callback) { if (!state_.connected || !rpc_ || !worker_) { - if (callback) callback(false, "Not connected"); + if (callback) callback(false, "Not connected", ""); return; } @@ -2934,23 +2935,33 @@ void App::importPrivateKey(const std::string& rawKey, std::functionpost([this, key, shielded, callback]() -> rpc::RPCWorker::MainCb { - std::string err; + worker_->post([this, key, viewing, shielded, callback]() -> rpc::RPCWorker::MainCb { + std::string err, addr; try { - rpc::RPCClient::TraceScope trace("Settings / Import private key"); - if (shielded) rpc_->call("z_importkey", {key, "yes"}); // rescan - else rpc_->call("importprivkey", {key, "", true}); // label "", rescan + rpc::RPCClient::TraceScope trace("Settings / Import key"); + std::string method; + nlohmann::json params; + if (viewing) { method = "z_importviewingkey"; params = {key, "yes"}; } // watch-only + else if (shielded) { method = "z_importkey"; params = {key, "yes"}; } + else { method = "importprivkey"; params = {key, "", true}; } + nlohmann::json r = rpc_->call(method, params); + // z_import* return {type,address}; importprivkey returns the t-address string. + if (r.is_object() && r.contains("address") && r["address"].is_string()) + addr = r["address"].get(); + else if (r.is_string()) + addr = r.get(); } catch (const std::exception& e) { err = e.what(); } catch (...) { @@ -2958,16 +2969,14 @@ void App::importPrivateKey(const std::string& rawKey, std::function= 2 && key[0] == 'S' && key[1] == 'K') return KeyKind::Shielded; - if (!key.empty() && key[0] == 's') return KeyKind::Shielded; + // A "z"-prefixed key is a shielded viewing key (zxview…); "s"-prefixed is a legacy Sprout key. + if (!key.empty() && (key[0] == 's' || key[0] == 'z')) return KeyKind::Shielded; return KeyKind::Transparent; } +bool WalletSecurityController::isViewingKey(const std::string& key) +{ + // Sapling extended full viewing key (mainnet HRP "zxviews"; "zxview" also matches the prefix the + // lite backend recognizes). Watch-only: reveals the address's funds but cannot spend them. + return key.rfind("zxview", 0) == 0; +} + bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key) { if (key.rfind("secret-extended-key-", 0) == 0) return true; // Sapling z spending key @@ -115,6 +123,11 @@ bool WalletSecurityController::isRecognizedPrivateKey(const std::string& key) return false; } +bool WalletSecurityController::isRecognizedImportKey(const std::string& key) +{ + return isRecognizedPrivateKey(key) || isViewingKey(key); +} + const char* WalletSecurityController::importSuccessMessage(KeyKind kind) { return kind == KeyKind::Shielded diff --git a/src/services/wallet_security_controller.h b/src/services/wallet_security_controller.h index f300434..239c3ba 100644 --- a/src/services/wallet_security_controller.h +++ b/src/services/wallet_security_controller.h @@ -74,9 +74,13 @@ public: std::size_t minLength = 4); static KeyKind classifyAddress(const std::string& address); static KeyKind classifyPrivateKey(const std::string& key); + // True if `key` is a shielded viewing key (extended full viewing key, "zxview…" — watch-only). + static bool isViewingKey(const std::string& key); // True if `key` looks like a recognized Z (Sapling/Sprout spending) or T (WIF) private key. - // Single source of truth for the import dialog's indicator AND its submit guard. static bool isRecognizedPrivateKey(const std::string& key); + // As above, but also accepts a recognized shielded viewing key — the import dialog auto-detects + // both, so this is the single source of truth for its indicator AND its submit guard. + static bool isRecognizedImportKey(const std::string& key); static const char* importSuccessMessage(KeyKind kind); static std::string decryptExportFileName(std::uint64_t timestampSeconds); static void secureClear(std::string& value); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 6fae6d6..5583af2 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1346,6 +1346,18 @@ void I18n::loadBuiltinEnglish() strings_["import_key_tooltip"] = "Enter one or more private keys, one per line.\nSupports both z-address and t-address keys.\nLines starting with # are treated as comments."; strings_["import_key_warning"] = "Warning: Never share your private keys! Importing keys from untrusted sources can compromise your wallet."; strings_["import_key_z_format"] = "Z-address spending keys (secret-extended-key-...)"; + strings_["import_key_warn"] = "Only import a key you own \xE2\x80\x94 it grants access to its funds."; + strings_["import_key_need_node"] = "Connect a running node to import a key."; + strings_["import_key_field"] = "Key"; + strings_["import_key_reveal_tip"] = "Show/hide the key"; + strings_["import_key_type_tkey"] = "Transparent private key"; + strings_["import_key_type_zspend"] = "Shielded spending key"; + strings_["import_key_type_zview"] = "Shielded viewing key (watch-only)"; + strings_["import_key_type_unknown"] = "Unrecognized key format"; + strings_["import_key_rescanning"] = "Importing & rescanning \xE2\x80\x94 this can take several minutes"; + strings_["import_key_done"] = "Imported. Wallet is rescanning."; + strings_["import_key_address"] = "Address:"; + strings_["import_key_import"] = "Import"; // --- Key Export Dialog --- strings_["key_export_fetching"] = "Fetching key from wallet...";