diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index e24cba4..403b797 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 71c5d6d..7600fcb 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -641,6 +641,20 @@ "sb_warming_up": "Aufwärmen...", "search_placeholder": "Suchen...", "security": "SICHERHEIT", + "seed_backup_button": "Wiederherstellungsphrase", + "seed_backup_close": "Schließen", + "seed_backup_copy": "Kopieren", + "seed_backup_intro": "Diese Wörter sind die Master-Sicherung Ihrer Wallet. Wer sie besitzt, kontrolliert Ihre Gelder.", + "seed_backup_load_failed": "Die Wiederherstellungsphrase konnte nicht geladen werden.", + "seed_backup_loading": "Ihre Wiederherstellungsphrase wird geladen…", + "seed_backup_locked": "Entsperren Sie Ihre Wallet, um ihre Wiederherstellungsphrase anzuzeigen.", + "seed_backup_none": "Diese Wallet ist älter als Wiederherstellungsphrasen und hat daher keine 24-Wort-Phrase. Sichern Sie sie stattdessen mit „Sicherung...“ oder durch den Export Ihrer privaten Schlüssel.", + "seed_backup_reminder": "Ihre Wallet hat eine 24-Wort-Wiederherstellungsphrase. Sichern Sie sie jetzt unter Einstellungen → Node & Sicherheit.", + "seed_backup_save": "In Datei speichern…", + "seed_backup_save_failed": "Konnte nicht schreiben ", + "seed_backup_saved": "Gespeichert unter ", + "seed_backup_title": "Wiederherstellungsphrase sichern", + "seed_backup_warning": "Schreiben Sie sie der Reihe nach auf, bewahren Sie sie offline auf und teilen oder fotografieren Sie sie niemals. Wenn Sie sie verlieren, können Ihre Gelder nicht wiederhergestellt werden.", "select_address": "Adresse auswählen...", "select_receiving_address": "Empfangsadresse auswählen...", "select_source_address": "Quelladresse auswählen...", @@ -889,6 +903,7 @@ "tt_save_ztx": "Z-Adresse-Transaktionsverlauf lokal für schnelleres Laden speichern", "tt_scan_themes": "Nach neuen Themes suchen.\\nTheme-Ordner ablegen in:\\n%s", "tt_scanline": "CRT-Scanlinieneffekt in der Konsole", + "tt_seed_backup": "Die 24-Wort-Wiederherstellungsphrase Ihrer Wallet anzeigen und sichern", "tt_set_pin": "Eine 4-8-stellige PIN für schnelles Entsperren festlegen", "tt_shield_mining": "Transparente Mining-Belohnungen an eine geschirmte Adresse verschieben", "tt_simple_bg": "Einen einfachen Verlauf für den Hintergrund verwenden\\nHotkey: Ctrl+Up", diff --git a/res/lang/es.json b/res/lang/es.json index 593c4bc..7fb3da9 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -641,6 +641,20 @@ "sb_warming_up": "Calentando...", "search_placeholder": "Buscar...", "security": "SEGURIDAD", + "seed_backup_button": "Frase de recuperación", + "seed_backup_close": "Cerrar", + "seed_backup_copy": "Copiar", + "seed_backup_intro": "Estas palabras son el respaldo maestro de tu cartera. Cualquiera que las tenga controla tus fondos.", + "seed_backup_load_failed": "No se pudo cargar la frase de recuperación.", + "seed_backup_loading": "Cargando tu frase de recuperación…", + "seed_backup_locked": "Desbloquea tu cartera para revelar su frase de recuperación.", + "seed_backup_none": "Esta cartera es anterior a las frases de recuperación, por lo que no tiene una frase de 24 palabras. Respáldala con «Respaldo...» o exportando tus claves privadas en su lugar.", + "seed_backup_reminder": "Tu cartera tiene una frase de recuperación de 24 palabras. Respáldala ahora en Configuración → Nodo y Seguridad.", + "seed_backup_save": "Guardar en archivo…", + "seed_backup_save_failed": "No se pudo escribir ", + "seed_backup_saved": "Guardado en ", + "seed_backup_title": "Respaldar frase de recuperación", + "seed_backup_warning": "Anótalas en orden, guárdalas sin conexión y nunca las compartas ni las fotografíes. Si las pierdes, tus fondos no se podrán recuperar.", "select_address": "Seleccionar dirección...", "select_receiving_address": "Seleccionar dirección de recepción...", "select_source_address": "Seleccionar dirección de origen...", @@ -889,6 +903,7 @@ "tt_save_ztx": "Almacenar historial de transacciones de z-address localmente para carga más rápida", "tt_scan_themes": "Buscar nuevos temas.\\nColoque carpetas de temas en:\\n%s", "tt_scanline": "Efecto de líneas de escaneo CRT en la consola", + "tt_seed_backup": "Muestra y respalda la frase de recuperación de 24 palabras de tu cartera", "tt_set_pin": "Establecer un PIN de 4-8 dígitos para desbloqueo rápido", "tt_shield_mining": "Mover recompensas de minería transparentes a una dirección blindada", "tt_simple_bg": "Usar un gradiente simple para el fondo\\nAtajo: Ctrl+Up", diff --git a/res/lang/fr.json b/res/lang/fr.json index e8909f2..c6974ae 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -641,6 +641,20 @@ "sb_warming_up": "Démarrage...", "search_placeholder": "Rechercher...", "security": "SÉCURITÉ", + "seed_backup_button": "Phrase de récupération", + "seed_backup_close": "Fermer", + "seed_backup_copy": "Copier", + "seed_backup_intro": "Ces mots sont la sauvegarde principale de votre portefeuille. Quiconque les possède contrôle vos fonds.", + "seed_backup_load_failed": "Impossible de charger la phrase de récupération.", + "seed_backup_loading": "Chargement de votre phrase de récupération…", + "seed_backup_locked": "Déverrouillez votre portefeuille pour révéler sa phrase de récupération.", + "seed_backup_none": "Ce portefeuille est antérieur aux phrases de récupération, il n'a donc pas de phrase de 24 mots. Sauvegardez-le plutôt avec « Sauvegarde » ou en exportant vos clés privées.", + "seed_backup_reminder": "Votre portefeuille possède une phrase de récupération de 24 mots. Sauvegardez-la maintenant dans Paramètres → Nœud et sécurité.", + "seed_backup_save": "Enregistrer dans un fichier…", + "seed_backup_save_failed": "Impossible d'écrire ", + "seed_backup_saved": "Enregistré dans ", + "seed_backup_title": "Sauvegarder la phrase de récupération", + "seed_backup_warning": "Notez-les dans l'ordre, conservez-les hors ligne, et ne les partagez ni ne les photographiez jamais. Si vous les perdez, vos fonds ne pourront pas être récupérés.", "select_address": "Sélectionner une adresse...", "select_receiving_address": "Sélectionner une adresse de réception...", "select_source_address": "Sélectionner une adresse source...", @@ -889,6 +903,7 @@ "tt_save_ztx": "Stocker l'historique des transactions z-address localement pour un chargement plus rapide", "tt_scan_themes": "Rechercher de nouveaux thèmes.\\nPlacez les dossiers de thèmes dans :\\n%s", "tt_scanline": "Effet de lignes de balayage CRT dans la console", + "tt_seed_backup": "Afficher et sauvegarder la phrase de récupération de 24 mots de votre portefeuille", "tt_set_pin": "Définir un PIN de 4-8 chiffres pour un déverrouillage rapide", "tt_shield_mining": "Déplacer les récompenses de minage transparentes vers une adresse blindée", "tt_simple_bg": "Utiliser un dégradé simple pour l'arrière-plan\\nRaccourci : Ctrl+Up", diff --git a/res/lang/ja.json b/res/lang/ja.json index 444e103..f01a096 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -641,6 +641,20 @@ "sb_warming_up": "ウォームアップ中...", "search_placeholder": "検索...", "security": "セキュリティ", + "seed_backup_button": "シードフレーズ", + "seed_backup_close": "閉じる", + "seed_backup_copy": "コピー", + "seed_backup_intro": "これらの単語はウォレットのマスターバックアップです。これらを持つ人は誰でもあなたの資金を管理できます。", + "seed_backup_load_failed": "シードフレーズを読み込めませんでした。", + "seed_backup_loading": "シードフレーズを読み込み中...", + "seed_backup_locked": "シードフレーズを表示するにはウォレットのロックを解除してください。", + "seed_backup_none": "このウォレットはシードフレーズより前のものなので、24単語のフレーズはありません。代わりに「バックアップ...」で、または秘密鍵をエクスポートしてバックアップしてください。", + "seed_backup_reminder": "ウォレットには24単語の復元シードフレーズがあります。今すぐ 設定 → ノードとセキュリティ でバックアップしてください。", + "seed_backup_save": "ファイルに保存...", + "seed_backup_save_failed": "書き込めませんでした: ", + "seed_backup_saved": "保存先: ", + "seed_backup_title": "シードフレーズをバックアップ", + "seed_backup_warning": "順番どおりに書き写し、オフラインで保管してください。決して共有や撮影をしないでください。紛失すると資金を復元できません。", "select_address": "アドレスを選択...", "select_receiving_address": "受信アドレスを選択...", "select_source_address": "送信元アドレスを選択...", @@ -889,6 +903,7 @@ "tt_save_ztx": "z-address トランザクション履歴をローカルに保存して高速読み込み", "tt_scan_themes": "新しいテーマをスキャン。\\nテーマフォルダーをここに配置:\\n%s", "tt_scanline": "コンソールでの CRT スキャンライン効果", + "tt_seed_backup": "ウォレットの24単語の復元シードフレーズを表示してバックアップします", "tt_set_pin": "クイックアンロック用の 4-8 桁 PIN を設定", "tt_shield_mining": "透明マイニング報酬をシールドアドレスに移動", "tt_simple_bg": "背景にシンプルなグラデーションを使用\\nホットキー:Ctrl+Up", diff --git a/res/lang/ko.json b/res/lang/ko.json index 490cf24..cc080dd 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -641,6 +641,20 @@ "sb_warming_up": "워밍업 중...", "search_placeholder": "검색...", "security": "보안", + "seed_backup_button": "시드 문구", + "seed_backup_close": "닫기", + "seed_backup_copy": "복사", + "seed_backup_intro": "이 단어들은 지갑의 마스터 백업입니다. 이 단어들을 가진 사람은 누구나 당신의 자금을 통제할 수 있습니다.", + "seed_backup_load_failed": "시드 문구를 불러올 수 없습니다.", + "seed_backup_loading": "시드 문구를 불러오는 중…", + "seed_backup_locked": "시드 문구를 표시하려면 지갑의 잠금을 해제하세요.", + "seed_backup_none": "이 지갑은 시드 문구 도입 이전에 만들어져 24단어 문구가 없습니다. 대신 \"백업...\"을 사용하거나 개인 키를 내보내 백업하세요.", + "seed_backup_reminder": "지갑에 24단어 복구 시드 문구가 있습니다. 지금 설정 → 노드 및 보안에서 백업하세요.", + "seed_backup_save": "파일로 저장…", + "seed_backup_save_failed": "쓸 수 없습니다: ", + "seed_backup_saved": "저장됨: ", + "seed_backup_title": "시드 문구 백업", + "seed_backup_warning": "순서대로 종이에 적어 오프라인으로 보관하고, 절대 공유하거나 사진으로 찍지 마세요. 이 단어들을 잃어버리면 자금을 복구할 수 없습니다.", "select_address": "주소 선택...", "select_receiving_address": "수신 주소 선택...", "select_source_address": "보낼 주소 선택...", @@ -889,6 +903,7 @@ "tt_save_ztx": "z-address 거래 기록을 로컬에 저장하여 빠른 로딩", "tt_scan_themes": "새 테마 검색.\\n테마 폴더를 여기에 배치:\\n%s", "tt_scanline": "콘솔에서 CRT 스캔라인 효과", + "tt_seed_backup": "지갑의 24단어 복구 시드 문구를 표시하고 백업합니다", "tt_set_pin": "빠른 잠금 해제를 위한 4-8자리 PIN 설정", "tt_shield_mining": "투명 채굴 보상을 차폐 주소로 이동", "tt_simple_bg": "배경에 단순 그라데이션 사용\\n단축키: Ctrl+Up", diff --git a/res/lang/pt.json b/res/lang/pt.json index 2d7a19c..bc3a905 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -641,6 +641,20 @@ "sb_warming_up": "Aquecendo...", "search_placeholder": "Pesquisar...", "security": "SEGURANÇA", + "seed_backup_button": "Frase de recuperação", + "seed_backup_close": "Fechar", + "seed_backup_copy": "Copiar", + "seed_backup_intro": "Estas palavras são o backup mestre da sua carteira. Qualquer pessoa que as tenha controla seus fundos.", + "seed_backup_load_failed": "Não foi possível carregar a frase de recuperação.", + "seed_backup_loading": "Carregando sua frase de recuperação…", + "seed_backup_locked": "Desbloqueie sua carteira para revelar sua frase de recuperação.", + "seed_backup_none": "Esta carteira é anterior às frases de recuperação, portanto não possui uma frase de 24 palavras. Faça o backup dela com \"Backup...\" ou exportando suas chaves privadas.", + "seed_backup_reminder": "Sua carteira tem uma frase de recuperação de 24 palavras. Faça o backup dela agora em Configurações → Nó e Segurança.", + "seed_backup_save": "Salvar em arquivo…", + "seed_backup_save_failed": "Não foi possível gravar ", + "seed_backup_saved": "Salvo em ", + "seed_backup_title": "Backup da frase de recuperação", + "seed_backup_warning": "Anote-as em ordem, guarde-as offline e nunca as compartilhe ou fotografe. Se você as perder, seus fundos não poderão ser recuperados.", "select_address": "Selecionar endereço...", "select_receiving_address": "Selecionar endereço de recebimento...", "select_source_address": "Selecionar endereço de origem...", @@ -889,6 +903,7 @@ "tt_save_ztx": "Armazenar histórico de transações z-address localmente para carregamento mais rápido", "tt_scan_themes": "Procurar novos temas.\\nColoque pastas de temas em:\\n%s", "tt_scanline": "Efeito de linhas de varredura CRT no console", + "tt_seed_backup": "Mostrar e fazer backup da frase de recuperação de 24 palavras da sua carteira", "tt_set_pin": "Definir um PIN de 4-8 dígitos para desbloqueio rápido", "tt_shield_mining": "Mover recompensas de mineração transparentes para um endereço blindado", "tt_simple_bg": "Usar um gradiente simples para o fundo\\nAtalho: Ctrl+Up", diff --git a/res/lang/ru.json b/res/lang/ru.json index 631d06f..3d4efbd 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -641,6 +641,20 @@ "sb_warming_up": "Прогрев...", "search_placeholder": "Поиск...", "security": "БЕЗОПАСНОСТЬ", + "seed_backup_button": "Сид-фраза", + "seed_backup_close": "Закрыть", + "seed_backup_copy": "Копировать", + "seed_backup_intro": "Эти слова — главная резервная копия вашего кошелька. Любой, у кого они есть, управляет вашими средствами.", + "seed_backup_load_failed": "Не удалось загрузить сид-фразу.", + "seed_backup_loading": "Загрузка вашей сид-фразы…", + "seed_backup_locked": "Разблокируйте кошелёк, чтобы показать его сид-фразу.", + "seed_backup_none": "Этот кошелёк создан до появления сид-фраз, поэтому у него нет фразы из 24 слов. Создайте резервную копию с помощью «Резервная копия...» или вместо этого экспортируйте свои приватные ключи.", + "seed_backup_reminder": "У вашего кошелька есть сид-фраза восстановления из 24 слов. Создайте её резервную копию прямо сейчас в Настройки → Узел и безопасность.", + "seed_backup_save": "Сохранить в файл…", + "seed_backup_save_failed": "Не удалось записать ", + "seed_backup_saved": "Сохранено в ", + "seed_backup_title": "Резервная копия сид-фразы", + "seed_backup_warning": "Запишите их по порядку, храните офлайн и никогда не делитесь ими и не фотографируйте их. Если вы их потеряете, ваши средства не удастся восстановить.", "select_address": "Выбрать адрес...", "select_receiving_address": "Выбрать адрес получения...", "select_source_address": "Выбрать адрес-источник...", @@ -889,6 +903,7 @@ "tt_save_ztx": "Хранить историю транзакций z-адреса локально для более быстрой загрузки", "tt_scan_themes": "Поиск новых тем.\\nРазместите папки тем в:\\n%s", "tt_scanline": "Эффект развёртки ЭЛТ в консоли", + "tt_seed_backup": "Показать и создать резервную копию сид-фразы восстановления вашего кошелька из 24 слов", "tt_set_pin": "Установить 4-8-значный PIN для быстрой разблокировки", "tt_shield_mining": "Перевести прозрачные вознаграждения за майнинг на экранированный адрес", "tt_simple_bg": "Использовать простой градиент для фона\\nГорячая клавиша: Ctrl+Up", diff --git a/res/lang/zh.json b/res/lang/zh.json index c34e962..793b67d 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -641,6 +641,20 @@ "sb_warming_up": "正在预热...", "search_placeholder": "搜索...", "security": "安全", + "seed_backup_button": "助记词", + "seed_backup_close": "关闭", + "seed_backup_copy": "复制", + "seed_backup_intro": "这些词是您钱包的主备份。任何拥有它们的人都能控制您的资金。", + "seed_backup_load_failed": "无法加载助记词。", + "seed_backup_loading": "正在加载您的助记词…", + "seed_backup_locked": "解锁您的钱包以显示其助记词。", + "seed_backup_none": "此钱包早于助记词功能,因此没有 24 词助记词。请改用“备份钱包”或导出私钥来备份它。", + "seed_backup_reminder": "您的钱包有一个 24 词恢复助记词。请立即在“设置 → 节点与安全”中备份它。", + "seed_backup_save": "保存到文件…", + "seed_backup_save_failed": "无法写入 ", + "seed_backup_saved": "已保存到 ", + "seed_backup_title": "备份助记词", + "seed_backup_warning": "请按顺序将它们抄写下来,离线保存,切勿分享或拍照。一旦丢失,您的资金将无法恢复。", "select_address": "选择地址...", "select_receiving_address": "选择接收地址...", "select_source_address": "选择来源地址...", @@ -889,6 +903,7 @@ "tt_save_ztx": "将 z-address 交易历史存储在本地以加快加载速度", "tt_scan_themes": "扫描新主题。\\n将主题文件夹放在:\\n%s", "tt_scanline": "控制台中的 CRT 扫描线效果", + "tt_seed_backup": "显示并备份您钱包的 24 词恢复助记词", "tt_set_pin": "设置 4-8 位 PIN 以快速解锁", "tt_shield_mining": "将透明挖矿奖励转移到屏蔽地址", "tt_simple_bg": "使用简单渐变作为背景\\n快捷键:Ctrl+Up", diff --git a/src/app.cpp b/src/app.cpp index 4ee09e7..8e8fdb8 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -33,6 +33,7 @@ #include "ui/windows/network_tab.h" #include "ui/windows/console_command_executor.h" #include "ui/windows/explorer_tab.h" +#include "ui/windows/seed_display.h" #include "ui/windows/market_tab.h" #include "ui/windows/settings_window.h" #include "ui/windows/about_dialog.h" @@ -715,6 +716,9 @@ void App::update() // it can act; compiled away when the feature is off (constexpr gate inside). maybeProvisionChatIdentity(); + // One-time reminder to back up the wallet's seed phrase (mnemonic wallets only). + maybeRemindSeedBackup(); + // Auto-lock check (only when connected + encrypted + unlocked) if (state_.connected && state_.isUnlocked()) { checkAutoLock(); @@ -1893,6 +1897,10 @@ void App::render() renderBackupDialog(); } + if (show_seed_backup_) { + renderSeedBackupDialog(); + } + // Security overlay dialogs (encrypt, decrypt, PIN) are rendered AFTER ImGui::End() // to ensure they appear on top of all other content @@ -2921,7 +2929,103 @@ void App::renderBackupDialog() backup_status_.clear(); s_backup_confirm_overwrite = false; } - + + ui::material::EndOverlayDialog(); +} + +void App::renderSeedBackupDialog() +{ + // Wipe the revealed phrase and reset the dialog's state. + auto closeAndWipe = [this]() { + if (!seed_backup_phrase_.empty()) + sodium_memzero(&seed_backup_phrase_[0], seed_backup_phrase_.size()); + seed_backup_phrase_.clear(); + seed_backup_status_.clear(); + seed_backup_fetch_started_ = false; + seed_backup_loading_ = false; + seed_backup_no_mnemonic_ = false; + show_seed_backup_ = false; + }; + + // Kick off the async z_exportmnemonic fetch once, and only while unlocked. + if (!seed_backup_fetch_started_ && !state_.isLocked()) { + seed_backup_fetch_started_ = true; + seed_backup_loading_ = true; + seed_backup_no_mnemonic_ = false; + seed_backup_status_.clear(); + exportSeedPhrase([this](bool ok, bool noMnemonic, const std::string& phrase, + const std::string& error) { + if (!show_seed_backup_) return; // dialog closed while the fetch was in flight + seed_backup_loading_ = false; + if (ok) { + seed_backup_phrase_ = phrase; // working copy — wiped on close + } else if (noMnemonic) { + seed_backup_no_mnemonic_ = true; + } else { + seed_backup_status_ = error.empty() ? std::string(TR("seed_backup_load_failed")) : error; + } + }); + } + + if (!ui::material::BeginOverlayDialog(TR("seed_backup_title"), &show_seed_backup_, 540.0f, 0.94f)) { + // Dismissed (Esc / outside click / X) — scrub the revealed secret. + if (seed_backup_fetch_started_) closeAndWipe(); + return; + } + + ui::material::Type().text(ui::material::TypeStyle::H6, TR("seed_backup_title")); + ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); + + if (state_.isLocked()) { + ImGui::TextWrapped("%s", TR("seed_backup_locked")); + } else if (seed_backup_loading_) { + ImGui::TextWrapped("%s", TR("seed_backup_loading")); + } else if (seed_backup_no_mnemonic_) { + ImGui::TextWrapped("%s", TR("seed_backup_none")); + } else if (!seed_backup_phrase_.empty()) { + ImGui::TextWrapped("%s", TR("seed_backup_intro")); + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, ui::material::Error()); + ImGui::TextWrapped("%s", TR("seed_backup_warning")); + ImGui::PopStyleColor(); + ImGui::Spacing(); + ui::RenderSeedWordGrid(ui::SplitSeedWords(seed_backup_phrase_)); + ImGui::Spacing(); + if (!seed_backup_status_.empty()) { + ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", seed_backup_status_.c_str()); + ImGui::Spacing(); + } + ImGui::Separator(); + if (ui::material::StyledButton(TR("seed_backup_copy"), ImVec2(120, 0))) { + copySecretToClipboard(seed_backup_phrase_); + } + ImGui::SameLine(); + if (ui::material::StyledButton(TR("seed_backup_save"), ImVec2(150, 0))) { + std::string path = util::Platform::getConfigDir() + "/dragonx-seed-backup.txt"; + std::string content = seed_backup_phrase_ + "\n"; + const bool ok = util::Platform::writeFileAtomically(path, content, + /*restrictPermissions=*/true); + sodium_memzero(&content[0], content.size()); + seed_backup_status_ = (ok ? std::string(TR("seed_backup_saved")) + : std::string(TR("seed_backup_save_failed"))) + path; + } + ImGui::SameLine(); + if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120, 0))) { + closeAndWipe(); + ui::material::EndOverlayDialog(); + return; + } + ui::material::EndOverlayDialog(); + return; + } else if (!seed_backup_status_.empty()) { + ImGui::TextColored(ImVec4(0.8f, 0.3f, 0.3f, 1.0f), "%s", seed_backup_status_.c_str()); + } + + ImGui::Spacing(); + ImGui::Separator(); + if (ui::material::StyledButton(TR("seed_backup_close"), ImVec2(120, 0))) { + closeAndWipe(); + } ui::material::EndOverlayDialog(); } diff --git a/src/app.h b/src/app.h index 5063eb6..4a327f1 100644 --- a/src/app.h +++ b/src/app.h @@ -295,6 +295,13 @@ public: // Wallet backup void backupWallet(const std::string& destination, std::function callback); + // Export the wallet's BIP39 seed phrase (z_exportmnemonic). The callback receives + // (ok, noMnemonic, phrase, error): ok+phrase on success; noMnemonic=true when the + // wallet's seed is not mnemonic-derived (legacy wallet). Full-node only; the phrase + // is a secret and is wiped after the callback returns. + void exportSeedPhrase(std::function callback); // Transaction operations void sendTransaction(const std::string& from, const std::string& to, @@ -344,6 +351,7 @@ public: void showImportKeyDialog() { show_import_key_ = true; } void showExportKeyDialog() { show_export_key_ = true; } void showBackupDialog() { show_backup_ = true; } + void showSeedBackupDialog() { show_seed_backup_ = true; } void showAboutDialog() { show_about_ = true; } // Legacy tab compat — maps int to NavPage @@ -599,6 +607,9 @@ private: // variants); derives via deriveChatIdentityFromSecret and wipes the secret. No-op when the // feature is off, already provisioned, in flight, or unavailable. void maybeProvisionChatIdentity(); + // One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per + // install) to back up their seed phrase. Cheap early-outs keep it idle until it can act. + void maybeRemindSeedBackup(); void provisionChatIdentityFromSecret(std::string secret); std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr for chat std::string generateChatLocalId(const char* prefix, int numBytes) const; // unique echo id / cid @@ -656,6 +667,16 @@ private: bool show_import_key_ = false; bool show_export_key_ = false; bool show_backup_ = false; + bool show_seed_backup_ = false; + + // Seed-phrase backup dialog state. seed_backup_phrase_ holds a SECRET (the revealed + // mnemonic) and is wiped with sodium_memzero when the dialog closes. + std::string seed_backup_phrase_; + std::string seed_backup_status_; + bool seed_backup_fetch_started_ = false; + bool seed_backup_loading_ = false; + bool seed_backup_no_mnemonic_ = false; + bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe // Embedded daemon state bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities()); @@ -918,6 +939,7 @@ private: void renderImportKeyDialog(); void renderExportKeyDialog(); void renderBackupDialog(); + void renderSeedBackupDialog(); // full-node "Back up seed phrase" modal (z_exportmnemonic) void renderFirstRunWizard(); void renderLockScreen(); void renderEncryptWalletDialog(); diff --git a/src/app_network.cpp b/src/app_network.cpp index 2ef10b1..89096bf 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -2863,6 +2863,69 @@ void App::importPrivateKey(const std::string& rawKey, std::function callback) +{ + if (!state_.connected || !rpc_ || !worker_) { + if (callback) callback(false, false, std::string(), "Not connected to the daemon."); + return; + } + worker_->post([this, callback]() -> rpc::RPCWorker::MainCb { + std::string phrase; // SECRET — wiped after the callback runs + std::string err; + bool ok = false; + bool noMnemonic = false; + rpc::RPCClient::TraceScope trace("Settings / Export seed phrase"); + try { + auto response = rpc_->call("z_exportmnemonic"); + if (response.contains("mnemonic") && response["mnemonic"].is_string()) { + auto& m = response["mnemonic"].get_ref(); + phrase = m; + if (!m.empty()) sodium_memzero(&m[0], m.size()); // scrub the json's own copy + ok = !phrase.empty(); + } else { + err = "The daemon returned no seed phrase."; + } + } catch (const rpc::RpcError& e) { + err = e.what(); + // The daemon errors with this specific message when the wallet's seed is not + // mnemonic-derived (a legacy wallet). Any other error (e.g. locked) stays generic. + if (err.find("not derived from a mnemonic") != std::string::npos) + noMnemonic = true; + } catch (const std::exception& e) { + err = e.what(); + } + return [callback, ok, noMnemonic, phrase = std::move(phrase), err]() mutable { + if (callback) callback(ok, noMnemonic, phrase, err); + if (!phrase.empty()) sodium_memzero(&phrase[0], phrase.size()); + }; + }); +} + +// One-time nudge to back up the seed phrase on a mnemonic-backed full-node wallet. Fires at +// most one z_exportmnemonic probe per install; the phrase is not retained (the probe only +// learns whether a mnemonic exists). Legacy (non-mnemonic) wallets are not nagged. +void App::maybeRemindSeedBackup() +{ + if (lite_wallet_) return; // lite has its own seed UX + if (!settings_ || settings_->getSeedBackupReminded()) return; + if (!state_.connected || !state_.encryption_state_known) return; + if (state_.isLocked()) return; // wait until unlocked to read it + if (seed_backup_reminder_in_flight_) return; + seed_backup_reminder_in_flight_ = true; + exportSeedPhrase([this](bool ok, bool noMnemonic, const std::string& /*phrase*/, + const std::string& /*error*/) { + seed_backup_reminder_in_flight_ = false; + // Only mark "reminded" once we have a definitive answer, so a transient failure retries + // next session rather than silently suppressing the reminder forever. + if (ok || noMnemonic) { + settings_->setSeedBackupReminded(true); + settings_->save(); + } + if (ok) + ui::Notifications::instance().info(TR("seed_backup_reminder"), 12.0f); + }); +} + void App::backupWallet(const std::string& destination, std::function callback) { if (!state_.connected || !rpc_) { diff --git a/src/config/settings.cpp b/src/config/settings.cpp index 1f0cb78..9dfc2de 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -199,6 +199,7 @@ bool Settings::load(const std::string& path) } } loadScalar(j, "wizard_completed", wizard_completed_); + loadScalar(j, "seed_backup_reminded", seed_backup_reminded_); loadScalar(j, "auto_lock_timeout", auto_lock_timeout_); loadScalar(j, "unlock_duration", unlock_duration_); loadScalar(j, "pin_enabled", pin_enabled_); @@ -432,6 +433,7 @@ bool Settings::save(const std::string& path) j["address_meta"] = meta_obj; } j["wizard_completed"] = wizard_completed_; + j["seed_backup_reminded"] = seed_backup_reminded_; j["auto_lock_timeout"] = auto_lock_timeout_; j["unlock_duration"] = unlock_duration_; j["pin_enabled"] = pin_enabled_; diff --git a/src/config/settings.h b/src/config/settings.h index 3efea93..ed8b367 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -246,6 +246,10 @@ public: bool getWizardCompleted() const { return wizard_completed_; } void setWizardCompleted(bool v) { wizard_completed_ = v; } + // Whether the one-time "back up your seed phrase" reminder has already been shown. + bool getSeedBackupReminded() const { return seed_backup_reminded_; } + void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; } + // Security — auto-lock timeout (seconds; 0 = disabled) int getAutoLockTimeout() const { return auto_lock_timeout_; } void setAutoLockTimeout(int seconds) { auto_lock_timeout_ = seconds; } @@ -444,6 +448,7 @@ private: std::set favorite_addresses_; std::map address_meta_; bool wizard_completed_ = false; + bool seed_backup_reminded_ = false; int auto_lock_timeout_ = 900; // 15 minutes int unlock_duration_ = 600; // 10 minutes bool pin_enabled_ = false; diff --git a/src/daemon/embedded_daemon.cpp b/src/daemon/embedded_daemon.cpp index ba21684..af37011 100644 --- a/src/daemon/embedded_daemon.cpp +++ b/src/daemon/embedded_daemon.cpp @@ -212,6 +212,11 @@ std::vector EmbeddedDaemon::getChainParams() "-addnode=node4.dragonx.is", "-experimentalfeatures", "-developerencryptwallet", + // Create fresh wallets from a BIP39 mnemonic so their 24-word phrase can be + // exported (z_exportmnemonic) and is portable to SDXLite/ObsidianDragonLite. + // The daemon reads this ONLY inside GenerateNewSeed() when a wallet has no seed + // yet, so it is inert on existing wallets — safe to pass unconditionally. + "-usemnemonic=1", dbcache_arg }; } diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index c3b4079..b2d6fdc 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -1304,8 +1304,10 @@ void RenderSettingsPage(App* app) { naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX; float wizW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(wizLabel).x + btnPadX : 0.0f; float bsW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(bsLabel).x + btnPadX : 0.0f; + // Full-node-only "Seed phrase" button trails the Backup button (see below). + float seedW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(TR("seed_backup_button")).x + btnPadX : 0.0f; float totalW = naturalW + sp * 5; - if (showFullNodeLifecycleActions) totalW += wizW + bsW + sp * 2; + if (showFullNodeLifecycleActions) totalW += wizW + bsW + seedW + sp * 3; float scale = (totalW > contentW) ? contentW / totalW : 1.0f; if (scale < 1.0f) ImGui::SetWindowFontScale(scale); @@ -1326,6 +1328,13 @@ void RenderSettingsPage(App* app) { if (TactileButton(r1[3], ImVec2(0, 0), btnFont)) app->showBackupDialog(); if (ImGui::IsItemHovered()) material::Tooltip("%s", t1[3]); + if (showFullNodeLifecycleActions) { + // Back up the wallet's BIP39 seed phrase (full-node mnemonic wallets). + ImGui::SameLine(0, scaledSp); + if (TactileButton(TR("seed_backup_button"), ImVec2(0, 0), btnFont)) + app->showSeedBackupDialog(); + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_backup")); + } ImGui::SameLine(0, scaledSp); if (TactileButton(r1[4], ImVec2(0, 0), btnFont)) ExportTransactionsDialog::show(); diff --git a/src/ui/windows/seed_display.h b/src/ui/windows/seed_display.h new file mode 100644 index 0000000..f0b2d62 --- /dev/null +++ b/src/ui/windows/seed_display.h @@ -0,0 +1,46 @@ +#pragma once + +// Shared helpers for rendering a BIP39 seed phrase as a numbered word grid and for +// splitting a whitespace-separated phrase into words. Used by the full-node seed-phrase +// backup dialog and the "migrate to a seed wallet" flow so both present the phrase the +// same way the lite welcome wizard does. + +#include +#include +#include + +#include "imgui.h" + +namespace dragonx { +namespace ui { + +// Split a whitespace-separated seed phrase into its individual words. +inline std::vector SplitSeedWords(const std::string& phrase) +{ + std::vector words; + std::string cur; + for (char c : phrase) { + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + if (!cur.empty()) { words.push_back(cur); cur.clear(); } + } else { + cur.push_back(c); + } + } + if (!cur.empty()) words.push_back(cur); + return words; +} + +// Render the words as a numbered 4-column grid (matches the lite welcome wizard layout). +inline void RenderSeedWordGrid(const std::vector& words, float colSpacingPx = 130.0f) +{ + for (size_t i = 0; i < words.size(); ++i) { + char cell[96]; + std::snprintf(cell, sizeof(cell), "%2zu. %s", i + 1, words[i].c_str()); + ImGui::TextUnformatted(cell); + if ((i % 4) != 3 && i + 1 < words.size()) + ImGui::SameLine(((i % 4) + 1) * colSpacingPx); + } +} + +} // namespace ui +} // namespace dragonx diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 90f7906..2ccc3ef 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -233,6 +233,22 @@ void I18n::loadBuiltinEnglish() strings_["chat_toast_compose_failed"] = "Could not compose the message (too long?)."; strings_["chat_toast_request_compose_failed"] = "Could not compose the contact request (invalid address / text?)."; strings_["chat_toast_request_queued"] = "Contact request queued."; + // Seed-phrase backup (full-node) + strings_["seed_backup_button"] = "Seed phrase"; + strings_["tt_seed_backup"] = "Show and back up your wallet's 24-word recovery seed phrase"; + strings_["seed_backup_title"] = "Back up seed phrase"; + strings_["seed_backup_intro"] = "These words are the master backup of your wallet. Anyone who has them controls your funds."; + strings_["seed_backup_warning"] = "Write them down in order, store them offline, and never share or photograph them. If you lose them, your funds cannot be recovered."; + strings_["seed_backup_loading"] = "Loading your seed phrase…"; + strings_["seed_backup_locked"] = "Unlock your wallet to reveal its seed phrase."; + strings_["seed_backup_none"] = "This wallet predates seed phrases, so it has no 24-word phrase. Back it up with \"Backup wallet\" or by exporting your private keys instead."; + strings_["seed_backup_load_failed"] = "Could not load the seed phrase."; + strings_["seed_backup_copy"] = "Copy"; + strings_["seed_backup_save"] = "Save to file…"; + strings_["seed_backup_saved"] = "Saved to "; + strings_["seed_backup_save_failed"] = "Could not write "; + strings_["seed_backup_close"] = "Close"; + strings_["seed_backup_reminder"] = "Your wallet has a 24-word recovery seed phrase. Back it up now in Settings → Node & Security."; strings_["contacts_search_placeholder"] = "Search contacts..."; strings_["contacts_search_no_match"] = "No matching contacts"; strings_["address_book_confirm_delete"] = "Confirm delete?";