diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index 0564b4a..7226775 100644 Binary files a/res/fonts/NotoSansCJK-Subset.ttf and b/res/fonts/NotoSansCJK-Subset.ttf differ diff --git a/res/lang/de.json b/res/lang/de.json index ccb2a08..8a55bc3 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -416,18 +416,30 @@ "history": "Verlauf", "immature_type": "Unreif", "import": "Importieren", + "import_key_address": "Adresse:", "import_key_btn": "Schlüssel importieren", + "import_key_done": "Importiert. Wallet wird neu gescannt.", + "import_key_field": "Schlüssel", "import_key_formats": "Unterstützte Schlüsselformate:", "import_key_full_rescan": "(0 = vollständiger Rescan)", + "import_key_import": "Importieren", "import_key_label": "Privater Schlüssel:", + "import_key_need_node": "Verbinden Sie einen laufenden Node, um einen Schlüssel zu importieren.", "import_key_no_valid": "Keine gültigen Schlüssel in der Eingabe gefunden", "import_key_progress": "Importiere %d/%d...", "import_key_rescan": "Blockchain nach Import neu scannen", + "import_key_rescanning": "Import & erneuter Scan — das kann einige Minuten dauern", + "import_key_reveal_tip": "Schlüssel ein-/ausblenden", "import_key_start_height": "Starthöhe:", "import_key_success": "Schlüssel erfolgreich importiert", "import_key_t_format": "T-Adresse WIF private Schlüssel", "import_key_title": "Privaten Schlüssel importieren", "import_key_tooltip": "Geben Sie einen oder mehrere private Schlüssel ein, einen pro Zeile.\nUnterstützt sowohl z-Adresse als auch t-Adresse Schlüssel.\nZeilen die mit # beginnen werden als Kommentare behandelt.", + "import_key_type_tkey": "Transparenter privater Schlüssel", + "import_key_type_unknown": "Unbekanntes Schlüsselformat", + "import_key_type_zspend": "Abgeschirmter Ausgabeschlüssel", + "import_key_type_zview": "Abgeschirmter Anzeigeschlüssel (nur Lesen)", + "import_key_warn": "Importieren Sie nur einen Schlüssel, der Ihnen gehört — er gewährt Zugriff auf dessen Guthaben.", "import_key_warning": "Warnung: Teilen Sie niemals Ihre privaten Schlüssel! Das Importieren von Schlüsseln aus nicht vertrauenswürdigen Quellen kann Ihr Wallet gefährden.", "import_key_z_format": "Z-Adresse Ausgabeschlüssel (secret-extended-key-...)", "import_private_key": "Privaten Schlüssel importieren...", diff --git a/res/lang/es.json b/res/lang/es.json index ca19149..ec7869b 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -416,18 +416,30 @@ "history": "Historial", "immature_type": "Inmaduro", "import": "Importar", + "import_key_address": "Dirección:", "import_key_btn": "Importar Clave(s)", + "import_key_done": "Importada. El monedero está reescaneando.", + "import_key_field": "Clave", "import_key_formats": "Formatos de clave soportados:", "import_key_full_rescan": "(0 = re-escaneo completo)", + "import_key_import": "Importar", "import_key_label": "Clave(s) Privada(s):", + "import_key_need_node": "Conecta un nodo en ejecución para importar una clave.", "import_key_no_valid": "No se encontraron claves válidas en la entrada", "import_key_progress": "Importando %d/%d...", "import_key_rescan": "Re-escanear blockchain después de importar", + "import_key_rescanning": "Importando y reescaneando: puede tardar varios minutos", + "import_key_reveal_tip": "Mostrar/ocultar la clave", "import_key_start_height": "Altura inicial:", "import_key_success": "Claves importadas exitosamente", "import_key_t_format": "Claves privadas WIF de direcciones T", "import_key_title": "Importar Clave Privada", "import_key_tooltip": "Ingresa una o más claves privadas, una por línea.\nSoporta claves de direcciones z y t.\nLas líneas que empiezan con # se tratan como comentarios.", + "import_key_type_tkey": "Clave privada transparente", + "import_key_type_unknown": "Formato de clave no reconocido", + "import_key_type_zspend": "Clave de gasto blindada", + "import_key_type_zview": "Clave de visualización blindada (solo lectura)", + "import_key_warn": "Importa solo una clave que te pertenezca: da acceso a sus fondos.", "import_key_warning": "Advertencia: ¡Nunca compartas tus claves privadas! Importar claves de fuentes no confiables puede comprometer tu cartera.", "import_key_z_format": "Claves de gasto de direcciones Z (secret-extended-key-...)", "import_private_key": "Importar Clave Privada...", diff --git a/res/lang/fr.json b/res/lang/fr.json index 3e5177d..bd10f9a 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -416,18 +416,30 @@ "history": "Historique", "immature_type": "Immature", "import": "Importer", + "import_key_address": "Adresse :", "import_key_btn": "Importer clé(s)", + "import_key_done": "Importée. Le portefeuille réanalyse.", + "import_key_field": "Clé", "import_key_formats": "Formats de clés pris en charge :", "import_key_full_rescan": "(0 = rescan complet)", + "import_key_import": "Importer", "import_key_label": "Clé(s) privée(s) :", + "import_key_need_node": "Connectez un nœud en cours d'exécution pour importer une clé.", "import_key_no_valid": "Aucune clé valide trouvée dans l'entrée", "import_key_progress": "Importation %d/%d...", "import_key_rescan": "Re-scanner la blockchain après l'importation", + "import_key_rescanning": "Import et réanalyse — cela peut prendre plusieurs minutes", + "import_key_reveal_tip": "Afficher/masquer la clé", "import_key_start_height": "Hauteur de départ :", "import_key_success": "Clés importées avec succès", "import_key_t_format": "Clés privées WIF d'adresses T", "import_key_title": "Importer une clé privée", "import_key_tooltip": "Entrez une ou plusieurs clés privées, une par ligne.\nPrend en charge les clés z-adresse et t-adresse.\nLes lignes commençant par # sont traitées comme des commentaires.", + "import_key_type_tkey": "Clé privée transparente", + "import_key_type_unknown": "Format de clé non reconnu", + "import_key_type_zspend": "Clé de dépense blindée", + "import_key_type_zview": "Clé de visualisation blindée (lecture seule)", + "import_key_warn": "N'importez qu'une clé qui vous appartient — elle donne accès à ses fonds.", "import_key_warning": "Attention : Ne partagez jamais vos clés privées ! L'importation de clés provenant de sources non fiables peut compromettre votre portefeuille.", "import_key_z_format": "Clés de dépenses z-adresse (secret-extended-key-...)", "import_private_key": "Importer une clé privée...", diff --git a/res/lang/ja.json b/res/lang/ja.json index 409a44f..a8f03fe 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -416,18 +416,30 @@ "history": "履歴", "immature_type": "未成熟", "import": "インポート", + "import_key_address": "アドレス:", "import_key_btn": "鍵をインポート", + "import_key_done": "インポートしました。ウォレットを再スキャンしています。", + "import_key_field": "鍵", "import_key_formats": "サポートされる鍵形式:", "import_key_full_rescan": "(0 = 完全再スキャン)", + "import_key_import": "インポート", "import_key_label": "秘密鍵:", + "import_key_need_node": "鍵をインポートするには稼働中のノードに接続してください。", "import_key_no_valid": "入力に有効な鍵が見つかりません", "import_key_progress": "インポート中 %d/%d...", "import_key_rescan": "インポート後にブロックチェーンを再スキャン", + "import_key_rescanning": "インポートと再スキャン中 — 数分かかることがあります", + "import_key_reveal_tip": "鍵の表示/非表示", "import_key_start_height": "開始高:", "import_key_success": "鍵のインポートに成功しました", "import_key_t_format": "TアドレスWIF秘密鍵", "import_key_title": "秘密鍵をインポート", "import_key_tooltip": "1行に1つずつ秘密鍵を入力してください。\nzアドレスとtアドレスの鍵の両方に対応しています。\n#で始まる行はコメントとして扱われます。", + "import_key_type_tkey": "透明な秘密鍵", + "import_key_type_unknown": "認識できない鍵の形式", + "import_key_type_zspend": "シールド支払鍵", + "import_key_type_zview": "シールド閲覧鍵(監視のみ)", + "import_key_warn": "自分が所有する鍵のみをインポートしてください — その資金へのアクセスを許可します。", "import_key_warning": "警告:秘密鍵を決して共有しないでください!信頼できないソースからの鍵のインポートはウォレットを危険にさらす可能性があります。", "import_key_z_format": "Zアドレス支出鍵 (secret-extended-key-...)", "import_private_key": "秘密鍵をインポート...", diff --git a/res/lang/ko.json b/res/lang/ko.json index 992e01b..3982c41 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -416,18 +416,30 @@ "history": "내역", "immature_type": "미성숙", "import": "가져오기", + "import_key_address": "주소:", "import_key_btn": "키 가져오기", + "import_key_done": "가져왔습니다. 지갑을 재검색 중입니다.", + "import_key_field": "키", "import_key_formats": "지원되는 키 형식:", "import_key_full_rescan": "(0 = 전체 재스캔)", + "import_key_import": "가져오기", "import_key_label": "개인 키:", + "import_key_need_node": "키를 가져오려면 실행 중인 노드에 연결하세요.", "import_key_no_valid": "입력에서 유효한 키를 찾을 수 없습니다", "import_key_progress": "가져오는 중 %d/%d...", "import_key_rescan": "가져오기 후 블록체인 재스캔", + "import_key_rescanning": "가져오기 및 재검색 중 — 몇 분 걸릴 수 있습니다", + "import_key_reveal_tip": "키 표시/숨기기", "import_key_start_height": "시작 높이:", "import_key_success": "키 가져오기 성공", "import_key_t_format": "T 주소 WIF 개인 키", "import_key_title": "개인 키 가져오기", "import_key_tooltip": "한 줄에 하나의 개인 키를 입력하세요.\nz 주소와 t 주소 키 모두 지원됩니다.\n#으로 시작하는 줄은 주석으로 처리됩니다.", + "import_key_type_tkey": "투명 개인 키", + "import_key_type_unknown": "알 수 없는 키 형식", + "import_key_type_zspend": "차폐 지출 키", + "import_key_type_zview": "차폐 조회 키 (읽기 전용)", + "import_key_warn": "본인이 소유한 키만 가져오세요 — 해당 자금에 접근할 수 있습니다.", "import_key_warning": "경고: 개인 키를 절대 공유하지 마세요! 신뢰할 수 없는 소스의 키를 가져오면 지갑이 위험해질 수 있습니다.", "import_key_z_format": "Z 주소 지출 키 (secret-extended-key-...)", "import_private_key": "개인 키 가져오기...", diff --git a/res/lang/pt.json b/res/lang/pt.json index 9c98fd6..3de0ff8 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -416,18 +416,30 @@ "history": "Histórico", "immature_type": "Imaturo", "import": "Importar", + "import_key_address": "Endereço:", "import_key_btn": "Importar Chave(s)", + "import_key_done": "Importada. A carteira está reescaneando.", + "import_key_field": "Chave", "import_key_formats": "Formatos de chave suportados:", "import_key_full_rescan": "(0 = rescan completo)", + "import_key_import": "Importar", "import_key_label": "Chave(s) Privada(s):", + "import_key_need_node": "Conecte um nó em execução para importar uma chave.", "import_key_no_valid": "Nenhuma chave válida encontrada na entrada", "import_key_progress": "Importando %d/%d...", "import_key_rescan": "Reescanear blockchain após importação", + "import_key_rescanning": "Importando e reescaneando — pode levar vários minutos", + "import_key_reveal_tip": "Mostrar/ocultar a chave", "import_key_start_height": "Altura inicial:", "import_key_success": "Chaves importadas com sucesso", "import_key_t_format": "Chaves privadas WIF de endereços T", "import_key_title": "Importar Chave Privada", "import_key_tooltip": "Digite uma ou mais chaves privadas, uma por linha.\nSuporta chaves de z-endereço e t-endereço.\nLinhas começando com # são tratadas como comentários.", + "import_key_type_tkey": "Chave privada transparente", + "import_key_type_unknown": "Formato de chave não reconhecido", + "import_key_type_zspend": "Chave de gasto blindada", + "import_key_type_zview": "Chave de visualização blindada (somente leitura)", + "import_key_warn": "Importe apenas uma chave que você possui — ela concede acesso aos seus fundos.", "import_key_warning": "Aviso: Nunca compartilhe suas chaves privadas! Importar chaves de fontes não confiáveis pode comprometer sua carteira.", "import_key_z_format": "Chaves de gasto de z-endereço (secret-extended-key-...)", "import_private_key": "Importar Chave Privada...", diff --git a/res/lang/ru.json b/res/lang/ru.json index f97746d..9d8feaa 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -416,18 +416,30 @@ "history": "История", "immature_type": "Незрелая", "import": "Импорт", + "import_key_address": "Адрес:", "import_key_btn": "Импорт ключей", + "import_key_done": "Импортировано. Кошелёк выполняет повторное сканирование.", + "import_key_field": "Ключ", "import_key_formats": "Поддерживаемые форматы ключей:", "import_key_full_rescan": "(0 = полное сканирование)", + "import_key_import": "Импорт", "import_key_label": "Приватный ключ(и):", + "import_key_need_node": "Подключите запущенный узел, чтобы импортировать ключ.", "import_key_no_valid": "В введённых данных не найдено действительных ключей", "import_key_progress": "Импорт %d/%d...", "import_key_rescan": "Пересканировать блокчейн после импорта", + "import_key_rescanning": "Импорт и повторное сканирование — это может занять несколько минут", + "import_key_reveal_tip": "Показать/скрыть ключ", "import_key_start_height": "Начальная высота:", "import_key_success": "Ключи успешно импортированы", "import_key_t_format": "Приватные ключи WIF для T-адресов", "import_key_title": "Импорт приватного ключа", "import_key_tooltip": "Введите один или несколько приватных ключей, по одному на строку.\nПоддерживаются ключи z-адресов и t-адресов.\nСтроки, начинающиеся с #, считаются комментариями.", + "import_key_type_tkey": "Прозрачный приватный ключ", + "import_key_type_unknown": "Нераспознанный формат ключа", + "import_key_type_zspend": "Экранированный ключ траты", + "import_key_type_zview": "Экранированный ключ просмотра (только чтение)", + "import_key_warn": "Импортируйте только свой ключ — он даёт доступ к его средствам.", "import_key_warning": "Предупреждение: Никогда не делитесь своими приватными ключами! Импорт ключей из ненадёжных источников может скомпрометировать ваш кошелёк.", "import_key_z_format": "Ключи расходования z-адресов (secret-extended-key-...)", "import_private_key": "Импорт приватного ключа...", diff --git a/res/lang/zh.json b/res/lang/zh.json index 52c01be..5099e7c 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -416,18 +416,30 @@ "history": "历史", "immature_type": "未成熟", "import": "导入", + "import_key_address": "地址:", "import_key_btn": "导入密钥", + "import_key_done": "已导入。钱包正在重新扫描。", + "import_key_field": "密钥", "import_key_formats": "支持的密钥格式:", "import_key_full_rescan": "(0 = 完整重扫)", + "import_key_import": "导入", "import_key_label": "私钥:", + "import_key_need_node": "请连接一个运行中的节点以导入密钥。", "import_key_no_valid": "输入中未找到有效密钥", "import_key_progress": "正在导入 %d/%d...", "import_key_rescan": "导入后重新扫描区块链", + "import_key_rescanning": "正在导入并重新扫描——可能需要几分钟", + "import_key_reveal_tip": "显示/隐藏密钥", "import_key_start_height": "起始高度:", "import_key_success": "密钥导入成功", "import_key_t_format": "T 地址 WIF 私钥", "import_key_title": "导入私钥", "import_key_tooltip": "输入一个或多个私钥,每行一个。\n支持 z 地址和 t 地址密钥。\n以 # 开头的行视为注释。", + "import_key_type_tkey": "透明私钥", + "import_key_type_unknown": "无法识别的密钥格式", + "import_key_type_zspend": "屏蔽花费密钥", + "import_key_type_zview": "屏蔽查看密钥(仅查看)", + "import_key_warn": "只导入你自己拥有的密钥——它可访问该地址的资金。", "import_key_warning": "警告:切勿分享您的私钥!从不可信来源导入密钥可能会危及您的钱包安全。", "import_key_z_format": "Z 地址花费密钥 (secret-extended-key-...)", "import_private_key": "导入私钥...", diff --git a/src/app.cpp b/src/app.cpp index e2113d1..aa3c360 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -66,6 +66,7 @@ #include "ui/material/type.h" #include "ui/material/typography.h" #include "ui/material/draw_helpers.h" +#include "ui/widgets/copy_field.h" #include "ui/notifications.h" #include "util/i18n.h" #include "util/platform.h" @@ -2755,106 +2756,151 @@ void App::renderLiteUnlockPrompt() void App::renderImportKeyDialog() { - auto dlg = ui::schema::UI().drawElement("inline-dialogs", "import-key"); - auto dlgF = [&](const char* key, float fb) -> 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...";