feat(settings): redesign the Import Key dialog
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) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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": "秘密鍵をインポート...",
|
||||
|
||||
@@ -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": "개인 키 가져오기...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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": "Импорт приватного ключа...",
|
||||
|
||||
@@ -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": "导入私钥...",
|
||||
|
||||
184
src/app.cpp
184
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()
|
||||
|
||||
@@ -300,7 +300,10 @@ public:
|
||||
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
|
||||
// callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export.
|
||||
void exportAllKeys(std::function<void(const std::string&, int, int)> callback);
|
||||
void importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> 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<void(bool, const std::string&, const std::string&)> callback);
|
||||
|
||||
// Wallet backup
|
||||
void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> 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;
|
||||
|
||||
|
||||
@@ -2921,10 +2921,11 @@ void App::exportAllKeys(std::function<void(const std::string&, int, int)> callba
|
||||
}
|
||||
}
|
||||
|
||||
void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, const std::string&)> callback)
|
||||
void App::importPrivateKey(const std::string& rawKey,
|
||||
std::function<void(bool, const std::string&, const std::string&)> 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::function<void(bool, c
|
||||
while (!key.empty() && (key.front()==' '||key.front()=='\t'||key.front()=='\n'||key.front()=='\r')) key.erase(key.begin());
|
||||
while (!key.empty() && (key.back()==' '||key.back()=='\t'||key.back()=='\n'||key.back()=='\r')) key.pop_back();
|
||||
|
||||
// Reject anything that doesn't look like a Z/T private key before handing it to the daemon (the
|
||||
// dialog's indicator and this guard now share isRecognizedPrivateKey, so they can't disagree).
|
||||
if (!services::WalletSecurityController::isRecognizedPrivateKey(key)) {
|
||||
if (callback) callback(false, "Unrecognized private-key format.");
|
||||
// Reject anything that isn't a recognized Z/T private key or shielded viewing key before handing
|
||||
// it to the daemon (the dialog's indicator and this guard share isRecognizedImportKey).
|
||||
if (!services::WalletSecurityController::isRecognizedImportKey(key)) {
|
||||
if (callback) callback(false, "Unrecognized key format.", "");
|
||||
return;
|
||||
}
|
||||
|
||||
const bool viewing = services::WalletSecurityController::isViewingKey(key);
|
||||
const bool shielded = services::WalletSecurityController::classifyPrivateKey(key)
|
||||
== services::WalletSecurityController::KeyKind::Shielded;
|
||||
// Run on the worker thread — import requests a full rescan (rescan=true), so the
|
||||
// synchronous curl call can take many seconds; never block the UI thread on it.
|
||||
worker_->post([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<std::string>();
|
||||
else if (r.is_string())
|
||||
addr = r.get<std::string>();
|
||||
} catch (const std::exception& e) {
|
||||
err = e.what();
|
||||
} catch (...) {
|
||||
@@ -2958,16 +2969,14 @@ void App::importPrivateKey(const std::string& rawKey, std::function<void(bool, c
|
||||
// below would never run, leaving a stuck "Importing…" spinner.
|
||||
err = "Import failed (unknown error)";
|
||||
}
|
||||
return [this, shielded, err, callback]() {
|
||||
return [this, err, addr, callback]() {
|
||||
if (!err.empty()) {
|
||||
if (callback) callback(false, err);
|
||||
if (callback) callback(false, err, "");
|
||||
return;
|
||||
}
|
||||
invalidateAddressValidationCache();
|
||||
refreshAddresses();
|
||||
if (callback) callback(true, services::WalletSecurityController::importSuccessMessage(
|
||||
shielded ? services::WalletSecurityController::KeyKind::Shielded
|
||||
: services::WalletSecurityController::KeyKind::Transparent));
|
||||
if (callback) callback(true, "", addr);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -101,10 +101,18 @@ WalletSecurityController::KeyKind WalletSecurityController::classifyPrivateKey(c
|
||||
// (The old `key[0]=='s'` test misrouted an uppercase "SK..." shielded key to the transparent RPC.)
|
||||
if (key.rfind("secret-extended-key-", 0) == 0) return KeyKind::Shielded;
|
||||
if (key.size() >= 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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...";
|
||||
|
||||
Reference in New Issue
Block a user