diff --git a/locales/de.yml b/locales/de.yml index b033ad81..d9552d34 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -551,9 +551,9 @@ goblin: choose_backup_file: "Eine .backup-Datei wählen" backup_read_failed: "Datei konnte nicht gelesen werden." backup_saved: "Sicherung gespeichert" - backup_saved_sub: "Bewahre die .backup-Datei sicher auf — wer sie UND dein Passwort hat, kann deine Identität wiederherstellen." - backup_file_title: "Identität sichern" - backup_file_blurb: "Erstellt eine verschlüsselte .backup-Datei mit Benutzername und Schlüssel. Gib dein Wallet-Passwort ein, um sie zu versiegeln." + backup_saved_sub: "Bewahre die .backup-Datei sicher auf. Wer sie UND dein Passwort hat, kann dein gesamtes Wallet wiederherstellen." + backup_file_title: "Wallet sichern" + backup_file_blurb: "Erstellt eine verschlüsselte .backup-Datei mit deinem Wallet-Seed und allen Identitäten. Gib dein Wallet-Passwort ein, um sie zu versiegeln." backup_write_failed: "Datei konnte nicht gespeichert werden." create_backup: "Sicherung erstellen" registered: "%{name} registriert" @@ -633,7 +633,7 @@ goblin: show_qr: "QR anzeigen" hide_qr: "QR ausblenden" nostr_section: "Erweiterte Nostr-Einstellungen" - backup_caption: "Enthält den Schlüssel und Benutzernamen deiner aktuellen Identität." + backup_caption: "Enthält dein Wallet und alle Identitäten." danger_zone: "Gefahrenzone" delete_warning: "Dies löscht deine Wiederherstellungsphrase und jede Identität auf diesem Gerät. Alles, was du nicht gesichert hast, ist unwiederbringlich verloren." download_backup: "Backup herunterladen" @@ -842,6 +842,8 @@ goblin: restore_wallet: "Wallet wiederherstellen" wrote_them_down: "Ich habe sie aufgeschrieben" fill_every_word: "Fülle jedes Wort aus — tippe ein Wort an, um es zu bearbeiten, oder füge die Phrase ein." + unlock_backup: "Backup entsperren" + backup_unlocked: "Backup entsperrt. Deine Identitäten werden beim Öffnen des Wallets wiederhergestellt." confirm: kicker: "SCHRITT 2 VON 3 · WALLET" title: "Jetzt beweise es" @@ -856,6 +858,7 @@ goblin: connected_nym: "über Tor verbunden" connecting_nym: "verbinde über Tor…" fresh_key_blurb: "Ein Zahlungsschlüssel, der nicht Teil deines Seeds ist — jederzeit rotierbar, ohne deine Mittel zu berühren." + restoring: "Deine Identitäten werden wiederhergestellt…" clean_slate_blurb: "Lust auf einen Neuanfang? Tausche jederzeit einen brandneuen Schlüssel ein — das neue Du ist nicht mit dem alten verknüpft. Gleiches Wallet, frisches Gesicht." pick_username: "Benutzernamen wählen — optional" username_blurb: "Freunde zahlen an deinen Namen statt an einen langen Schlüssel. Optional — jederzeit beanspruchbar." diff --git a/locales/en.yml b/locales/en.yml index 182ca5d8..9e8af8c8 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -551,9 +551,9 @@ goblin: choose_backup_file: "Choose a .backup file" backup_read_failed: "Couldn't read that file." backup_saved: "Backup saved" - backup_saved_sub: "Keep the .backup file safe — anyone with it AND your password can restore your identity." - backup_file_title: "Back up identity" - backup_file_blurb: "Creates one encrypted .backup file with your username and key. Enter your wallet password to seal it." + backup_saved_sub: "Keep the .backup file safe. Anyone with it AND your password can restore your whole wallet." + backup_file_title: "Back up wallet" + backup_file_blurb: "Creates one encrypted .backup file with your wallet seed and every identity. Enter your wallet password to seal it." backup_write_failed: "Couldn't save the file." create_backup: "Create backup" registered: "Registered %{name}" @@ -633,7 +633,7 @@ goblin: show_qr: "Show QR" hide_qr: "Hide QR" nostr_section: "Advanced nostr settings" - backup_caption: "Contains your current identity's key and username." + backup_caption: "Contains your wallet and all identities." danger_zone: "Danger zone" delete_warning: "This erases your recovery phrase and every identity on this device. Anything you haven't backed up is gone for good." download_backup: "Download backup" @@ -842,6 +842,8 @@ goblin: restore_wallet: "Restore wallet" wrote_them_down: "I wrote them down" fill_every_word: "Fill every word — tap a word to edit it, or paste the phrase." + unlock_backup: "Unlock backup" + backup_unlocked: "Backup unlocked. Your identities restore when the wallet opens." confirm: kicker: "STEP 2 OF 3 · WALLET" title: "Now prove it" @@ -856,6 +858,7 @@ goblin: connected_nym: "connected over Tor" connecting_nym: "connecting over Tor…" fresh_key_blurb: "A payment key that isn't part of your seed — rotate it anytime to stay private, without touching your funds." + restoring: "Restoring your identities…" clean_slate_blurb: "Want a clean slate? Swap in a brand-new key any time — the new you isn't linked to the old one. Same wallet, fresh face." pick_username: "Pick a username — optional" username_blurb: "Friends pay your name instead of a long key. Optional — claim one any time." diff --git a/locales/es.yml b/locales/es.yml index 8b4f8d24..da94117a 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -551,9 +551,9 @@ goblin: choose_backup_file: "Elegir un archivo .backup" backup_read_failed: "No se pudo leer ese archivo." backup_saved: "Respaldo guardado" - backup_saved_sub: "Mantén el archivo .backup a salvo — cualquiera que lo tenga JUNTO CON tu contraseña puede restaurar tu identidad." - backup_file_title: "Respaldar identidad" - backup_file_blurb: "Crea un archivo .backup cifrado con tu nombre de usuario y clave. Ingresa tu contraseña de billetera para sellarlo." + backup_saved_sub: "Mantén el archivo .backup a salvo. Cualquiera que lo tenga JUNTO CON tu contraseña puede restaurar toda tu billetera." + backup_file_title: "Respaldar billetera" + backup_file_blurb: "Crea un archivo .backup cifrado con la semilla de tu billetera y todas tus identidades. Ingresa tu contraseña de billetera para sellarlo." backup_write_failed: "No se pudo guardar el archivo." create_backup: "Crear respaldo" registered: "%{name} registrado" @@ -633,7 +633,7 @@ goblin: show_qr: "Mostrar QR" hide_qr: "Ocultar QR" nostr_section: "Ajustes avanzados de nostr" - backup_caption: "Contiene la clave y el nombre de usuario de tu identidad actual." + backup_caption: "Contiene tu billetera y todas tus identidades." danger_zone: "Zona de peligro" delete_warning: "Esto borra tu frase de recuperación y todas las identidades de este dispositivo. Todo lo que no hayas respaldado se perderá para siempre." download_backup: "Descargar copia de seguridad" @@ -842,6 +842,8 @@ goblin: restore_wallet: "Restaurar billetera" wrote_them_down: "Ya las anoté" fill_every_word: "Completa cada palabra — toca una palabra para editarla, o pega la frase." + unlock_backup: "Desbloquear respaldo" + backup_unlocked: "Respaldo desbloqueado. Tus identidades se restauran cuando se abre la billetera." confirm: kicker: "PASO 2 DE 3 · BILLETERA" title: "Ahora demuéstralo" @@ -856,6 +858,7 @@ goblin: connected_nym: "conectado por Tor" connecting_nym: "conectando por Tor…" fresh_key_blurb: "Una clave de pago que no forma parte de tu frase de recuperación — rótala cuando quieras para mantener la privacidad, sin tocar tus fondos." + restoring: "Restaurando tus identidades…" clean_slate_blurb: "¿Quieres empezar de cero? Cambia a una clave totalmente nueva cuando quieras — el nuevo tú no está vinculado al anterior. Misma billetera, cara nueva." pick_username: "Elige un nombre de usuario — opcional" username_blurb: "Tus amigos pagan a tu nombre en lugar de a una clave larga. Opcional — reclama uno cuando quieras." diff --git a/locales/fr.yml b/locales/fr.yml index 6206a709..2545a4d2 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -551,9 +551,9 @@ goblin: choose_backup_file: "Choisir un fichier .backup" backup_read_failed: "Impossible de lire ce fichier." backup_saved: "Sauvegarde enregistrée" - backup_saved_sub: "Conservez le fichier .backup en lieu sûr — quiconque l'a AVEC votre mot de passe peut restaurer votre identité." - backup_file_title: "Sauvegarder l'identité" - backup_file_blurb: "Crée un fichier .backup chiffré avec votre nom d'utilisateur et votre clé. Saisissez le mot de passe du portefeuille pour le sceller." + backup_saved_sub: "Conservez le fichier .backup en lieu sûr. Quiconque l'a AVEC votre mot de passe peut restaurer tout votre portefeuille." + backup_file_title: "Sauvegarder le portefeuille" + backup_file_blurb: "Crée un fichier .backup chiffré avec la graine de votre portefeuille et toutes vos identités. Saisissez le mot de passe du portefeuille pour le sceller." backup_write_failed: "Impossible d'enregistrer le fichier." create_backup: "Créer la sauvegarde" registered: "%{name} enregistré" @@ -633,7 +633,7 @@ goblin: show_qr: "Afficher le QR" hide_qr: "Masquer le QR" nostr_section: "Paramètres nostr avancés" - backup_caption: "Contient la clé et le nom d'utilisateur de votre identité actuelle." + backup_caption: "Contient votre portefeuille et toutes vos identités." danger_zone: "Zone de danger" delete_warning: "Ceci efface votre phrase de récupération et toutes les identités de cet appareil. Tout ce qui n'a pas été sauvegardé est perdu définitivement." download_backup: "Télécharger la sauvegarde" @@ -842,6 +842,8 @@ goblin: restore_wallet: "Restaurer le portefeuille" wrote_them_down: "Je les ai notés" fill_every_word: "Remplissez chaque mot — touchez un mot pour le modifier, ou collez la phrase." + unlock_backup: "Déverrouiller la sauvegarde" + backup_unlocked: "Sauvegarde déverrouillée. Vos identités seront restaurées à l'ouverture du portefeuille." confirm: kicker: "ÉTAPE 2 SUR 3 · PORTEFEUILLE" title: "Maintenant prouvez-le" @@ -856,6 +858,7 @@ goblin: connected_nym: "connecté via Tor" connecting_nym: "connexion via Tor…" fresh_key_blurb: "Une clé de paiement qui ne fait pas partie de votre seed — renouvelable à tout moment, sans toucher à vos fonds." + restoring: "Restauration de vos identités…" clean_slate_blurb: "Envie de repartir à zéro ? Remplacez par une toute nouvelle clé à tout moment — le nouveau vous n'est pas lié à l'ancien. Même portefeuille, nouveau visage." pick_username: "Choisir un nom d'utilisateur — facultatif" username_blurb: "Vos amis paient votre nom au lieu d'une longue clé. Facultatif — réclamez-en un à tout moment." diff --git a/locales/ja.yml b/locales/ja.yml index d6560299..0dc43029 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -551,9 +551,9 @@ goblin: choose_backup_file: ".backupファイルを選択" backup_read_failed: "そのファイルを読み込めませんでした。" backup_saved: "バックアップを保存しました" - backup_saved_sub: ".backupファイルは安全に保管してください — このファイルとパスワードの両方を持つ人は誰でもアイデンティティを復元できます。" - backup_file_title: "アイデンティティをバックアップ" - backup_file_blurb: "ユーザー名とキーを含む暗号化された.backupファイルを1つ作成します。ウォレットのパスワードを入力して保護してください。" + backup_saved_sub: ".backupファイルは安全に保管してください。このファイルとパスワードの両方を持つ人は誰でもウォレット全体を復元できます。" + backup_file_title: "ウォレットをバックアップ" + backup_file_blurb: "ウォレットのシードとすべてのアイデンティティを含む暗号化された.backupファイルを1つ作成します。ウォレットのパスワードを入力して保護してください。" backup_write_failed: "ファイルを保存できませんでした。" create_backup: "バックアップを作成" registered: "%{name}を登録しました" @@ -633,7 +633,7 @@ goblin: show_qr: "QRを表示" hide_qr: "QRを非表示" nostr_section: "詳細なnostr設定" - backup_caption: "現在のアイデンティティのキーとユーザー名が含まれます。" + backup_caption: "ウォレットとすべてのアイデンティティが含まれます。" danger_zone: "危険ゾーン" delete_warning: "この端末のリカバリーフレーズとすべてのアイデンティティが消去されます。バックアップしていないものは完全に失われます。" download_backup: "バックアップをダウンロード" @@ -843,6 +843,8 @@ goblin: restore_wallet: "ウォレットを復元" wrote_them_down: "書き留めました" fill_every_word: "すべての単語を入力してください — 単語をタップして編集するか、フレーズを貼り付けてください。" + unlock_backup: "バックアップを解除" + backup_unlocked: "バックアップを解除しました。ウォレットを開くとアイデンティティが復元されます。" confirm: kicker: "ステップ2/3 · ウォレット" title: "確認してみましょう" @@ -857,6 +859,7 @@ goblin: connected_nym: "Tor経由で接続済み" connecting_nym: "Tor経由で接続中…" fresh_key_blurb: "リカバリーフレーズの一部ではない支払い用のキーです — 資金に影響を与えることなく、いつでもローテーションしてプライバシーを保てます。" + restoring: "アイデンティティを復元しています…" clean_slate_blurb: "まっさらな状態にしたいですか? いつでもまったく新しいキーに切り替えられます — 新しいあなたは古いあなたと結び付きません。同じウォレットで、新しい顔になります。" pick_username: "ユーザー名を選択 — 任意" username_blurb: "友人は長いキーの代わりに、あなたの名前で支払います。任意です — いつでも登録できます。" diff --git a/locales/ko.yml b/locales/ko.yml index 2da1d04c..b8211e1e 100644 --- a/locales/ko.yml +++ b/locales/ko.yml @@ -551,9 +551,9 @@ goblin: choose_backup_file: ".backup 파일 선택" backup_read_failed: "해당 파일을 읽을 수 없습니다." backup_saved: "백업 저장됨" - backup_saved_sub: ".backup 파일을 안전하게 보관하세요 — 이 파일과 비밀번호를 모두 가진 사람은 신원을 복원할 수 있습니다." - backup_file_title: "신원 백업" - backup_file_blurb: "사용자 이름과 키가 담긴 암호화된 .backup 파일을 만듭니다. 지갑 비밀번호를 입력해 봉인하세요." + backup_saved_sub: ".backup 파일을 안전하게 보관하세요. 이 파일과 비밀번호를 모두 가진 사람은 지갑 전체를 복원할 수 있습니다." + backup_file_title: "지갑 백업" + backup_file_blurb: "지갑 시드와 모든 신원이 담긴 암호화된 .backup 파일을 만듭니다. 지갑 비밀번호를 입력해 봉인하세요." backup_write_failed: "파일을 저장할 수 없습니다." create_backup: "백업 만들기" registered: "%{name} 등록됨" @@ -633,7 +633,7 @@ goblin: show_qr: "QR 표시" hide_qr: "QR 숨기기" nostr_section: "고급 nostr 설정" - backup_caption: "현재 신원의 키와 사용자 이름이 포함됩니다." + backup_caption: "지갑과 모든 신원이 포함됩니다." danger_zone: "위험 구역" delete_warning: "이 기기의 복구 문구와 모든 신원이 삭제됩니다. 백업하지 않은 항목은 영구적으로 사라집니다." download_backup: "백업 다운로드" @@ -842,6 +842,8 @@ goblin: restore_wallet: "지갑 복원" wrote_them_down: "다 적었습니다" fill_every_word: "모든 단어를 채우세요 — 단어를 탭해서 수정하거나, 문구를 붙여넣으세요." + unlock_backup: "백업 잠금 해제" + backup_unlocked: "백업이 잠금 해제되었습니다. 지갑을 열면 신원이 복원됩니다." confirm: kicker: "2/3단계 · 지갑" title: "이제 확인해 보세요" @@ -856,6 +858,7 @@ goblin: connected_nym: "Tor로 연결됨" connecting_nym: "Tor로 연결하는 중…" fresh_key_blurb: "복구 문구에 속하지 않는 결제 키입니다 — 자금은 그대로 둔 채 언제든 교체해 프라이버시를 지키세요." + restoring: "신원을 복원하는 중…" clean_slate_blurb: "새 출발을 원하시나요? 언제든 완전히 새로운 키로 바꾸세요 — 새로운 나는 이전과 연결되지 않습니다. 같은 지갑, 새로운 얼굴입니다." pick_username: "사용자 이름 선택 — 선택 사항" username_blurb: "친구들이 긴 키 대신 내 이름으로 결제합니다. 선택 사항이며 — 언제든 등록할 수 있습니다." diff --git a/locales/ru.yml b/locales/ru.yml index e0c4fd0a..6f15932e 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -551,9 +551,9 @@ goblin: choose_backup_file: "Выбрать файл .backup" backup_read_failed: "Не удалось прочитать файл." backup_saved: "Резервная копия сохранена" - backup_saved_sub: "Храните файл .backup в безопасности — любой, у кого есть он И ваш пароль, может восстановить вашу личность." - backup_file_title: "Резервная копия личности" - backup_file_blurb: "Создаёт один зашифрованный файл .backup с именем и ключом. Введите пароль кошелька, чтобы запечатать его." + backup_saved_sub: "Храните файл .backup в безопасности. Любой, у кого есть он И ваш пароль, может восстановить весь ваш кошелёк." + backup_file_title: "Резервная копия кошелька" + backup_file_blurb: "Создаёт один зашифрованный файл .backup с seed-фразой кошелька и всеми личностями. Введите пароль кошелька, чтобы запечатать его." backup_write_failed: "Не удалось сохранить файл." create_backup: "Создать копию" registered: "Зарегистрировано %{name}" @@ -633,7 +633,7 @@ goblin: show_qr: "Показать QR" hide_qr: "Скрыть QR" nostr_section: "Расширенные настройки nostr" - backup_caption: "Содержит ключ и имя пользователя вашей текущей личности." + backup_caption: "Содержит ваш кошелёк и все личности." danger_zone: "Опасная зона" delete_warning: "Это удалит вашу фразу восстановления и все личности на этом устройстве. Всё, что не сохранено в резервной копии, будет потеряно навсегда." download_backup: "Скачать резервную копию" @@ -842,6 +842,8 @@ goblin: restore_wallet: "Восстановить кошелёк" wrote_them_down: "Я записал их" fill_every_word: "Заполните каждое слово — коснитесь слова для редактирования или вставьте фразу." + unlock_backup: "Разблокировать копию" + backup_unlocked: "Копия разблокирована. Ваши личности восстановятся при открытии кошелька." confirm: kicker: "ШАГ 2 ИЗ 3 · КОШЕЛЁК" title: "Теперь подтвердите" @@ -856,6 +858,7 @@ goblin: connected_nym: "подключено через Tor" connecting_nym: "подключение через Tor…" fresh_key_blurb: "Платёжный ключ, не связанный с seed-фразой — меняйте его в любой момент, не трогая средства." + restoring: "Восстановление ваших личностей…" clean_slate_blurb: "Хотите начать с чистого листа? Подставьте совершенно новый ключ в любой момент — новый вы не связан со старым. Тот же кошелёк, новое лицо." pick_username: "Выберите имя — необязательно" username_blurb: "Друзья платят на ваше имя, а не на длинный ключ. Необязательно — можно занять в любой момент." diff --git a/locales/tr.yml b/locales/tr.yml index a700284f..75eaa91c 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -551,9 +551,9 @@ goblin: choose_backup_file: "Bir .backup dosyası seç" backup_read_failed: "Dosya okunamadı." backup_saved: "Yedek kaydedildi" - backup_saved_sub: ".backup dosyasını güvende tut — hem ona hem de parolana sahip olan kimliğini geri yükleyebilir." - backup_file_title: "Kimliği yedekle" - backup_file_blurb: "Kullanıcı adın ve anahtarınla tek bir şifreli .backup dosyası oluşturur. Mühürlemek için cüzdan parolanı gir." + backup_saved_sub: ".backup dosyasını güvende tut. Hem ona hem de parolana sahip olan tüm cüzdanını geri yükleyebilir." + backup_file_title: "Cüzdanı yedekle" + backup_file_blurb: "Cüzdan tohumun ve tüm kimliklerinle tek bir şifreli .backup dosyası oluşturur. Mühürlemek için cüzdan parolanı gir." backup_write_failed: "Dosya kaydedilemedi." create_backup: "Yedek oluştur" registered: "%{name} kaydedildi" @@ -633,7 +633,7 @@ goblin: show_qr: "QR göster" hide_qr: "QR gizle" nostr_section: "Gelişmiş nostr ayarları" - backup_caption: "Mevcut kimliğinizin anahtarını ve kullanıcı adını içerir." + backup_caption: "Cüzdanını ve tüm kimliklerini içerir." danger_zone: "Tehlikeli bölge" delete_warning: "Bu, kurtarma kelimelerinizi ve bu cihazdaki tüm kimlikleri siler. Yedeklemediğiniz her şey kalıcı olarak kaybolur." download_backup: "Yedeği indir" @@ -842,6 +842,8 @@ goblin: restore_wallet: "Cüzdanı geri yükle" wrote_them_down: "Onları yazdım" fill_every_word: "Her kelimeyi doldur — düzenlemek için bir kelimeye dokun ya da ifadeyi yapıştır." + unlock_backup: "Yedeği aç" + backup_unlocked: "Yedek açıldı. Kimliklerin cüzdan açıldığında geri yüklenir." confirm: kicker: "ADIM 2 / 3 · CÜZDAN" title: "Şimdi kanıtla" @@ -856,6 +858,7 @@ goblin: connected_nym: "Tor üzerinden bağlı" connecting_nym: "Tor üzerinden bağlanılıyor…" fresh_key_blurb: "Seed'inin parçası olmayan bir ödeme anahtarı — paranı hiç ellemeden istediğin an döndür." + restoring: "Kimliklerin geri yükleniyor…" clean_slate_blurb: "Temiz bir sayfa mı istiyorsun? İstediğin zaman yepyeni bir anahtar tak — yeni sen eskisine bağlı değil. Aynı cüzdan, yeni yüz." pick_username: "Bir kullanıcı adı seç — isteğe bağlı" username_blurb: "Arkadaşların uzun bir anahtar yerine adına öder. İsteğe bağlı — istediğin an al." diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index b1991932..0ab6106f 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -551,9 +551,9 @@ goblin: choose_backup_file: "选择 .backup 文件" backup_read_failed: "无法读取该文件。" backup_saved: "备份已保存" - backup_saved_sub: "妥善保管 .backup 文件——同时拥有它和你密码的人都能恢复你的身份。" - backup_file_title: "备份身份" - backup_file_blurb: "创建一个包含你的用户名和密钥的加密 .backup 文件。输入钱包密码以封存它。" + backup_saved_sub: "妥善保管 .backup 文件。同时拥有它和你密码的人都能恢复你的整个钱包。" + backup_file_title: "备份钱包" + backup_file_blurb: "创建一个包含你的钱包助记词和所有身份的加密 .backup 文件。输入钱包密码以封存它。" backup_write_failed: "无法保存文件。" create_backup: "创建备份" registered: "已注册 %{name}" @@ -633,7 +633,7 @@ goblin: show_qr: "显示二维码" hide_qr: "隐藏二维码" nostr_section: "高级 nostr 设置" - backup_caption: "包含您当前身份的密钥和用户名。" + backup_caption: "包含你的钱包和所有身份。" danger_zone: "危险区域" delete_warning: "这将删除此设备上的恢复助记词和所有身份。未备份的内容将永久丢失。" download_backup: "下载备份" @@ -842,6 +842,8 @@ goblin: restore_wallet: "恢复钱包" wrote_them_down: "我已抄好" fill_every_word: "填写每个词 — 点击某个词进行编辑,或粘贴整个短语。" + unlock_backup: "解锁备份" + backup_unlocked: "备份已解锁。钱包打开时将恢复你的身份。" confirm: kicker: "步骤 2 / 3 · 钱包" title: "现在来验证" @@ -856,6 +858,7 @@ goblin: connected_nym: "已通过 Tor 连接" connecting_nym: "正在通过 Tor 连接…" fresh_key_blurb: "一个不属于助记词的支付密钥——可随时轮换以保护隐私,且不影响你的资金。" + restoring: "正在恢复你的身份…" clean_slate_blurb: "想要全新开始?随时换上一个全新密钥 — 新的你与旧的毫无关联。同一个钱包,焕然一新。" pick_username: "选择用户名 — 可选" username_blurb: "朋友支付给你的名称,而不是一长串密钥。可选——随时认领。" diff --git a/src/gui/views/goblin/mod.rs b/src/gui/views/goblin/mod.rs index 27a59fd1..73373d0b 100644 --- a/src/gui/views/goblin/mod.rs +++ b/src/gui/views/goblin/mod.rs @@ -5413,7 +5413,7 @@ impl GoblinWalletView { if w::big_action(ui, &t!("goblin.settings.create_backup"), false) .clicked() { - match wallet.create_nostr_backup(&bk.password) { + match wallet.create_full_backup(&bk.password) { Ok(envelope) => { let stamp = chrono::Local::now().format("%Y-%m-%d-%H%M"); let fname = format!("GOBLIN-{stamp}.backup"); diff --git a/src/gui/views/goblin/onboarding.rs b/src/gui/views/goblin/onboarding.rs index 2f5b2fcc..ebda7504 100644 --- a/src/gui/views/goblin/onboarding.rs +++ b/src/gui/views/goblin/onboarding.rs @@ -82,6 +82,34 @@ pub struct OnboardingContent { import: Option, /// Moment the recovery phrase was copied, for the transient "Copied" check. words_copied: Option, + /// Full-backup restore (restore-from-seed path): pick a `.backup` file that + /// carries the money seed AND every identity, unlock it, and let the seed feed + /// the normal 24-word creation path. + backup_restore: Option, + /// Captured at full-backup unlock: `(file contents, backup password)` to + /// reinstate every identity once the wallet has been created and opened. + pending_restore: Option<(String, String)>, + /// A full-backup identity restore is running in a worker after wallet open. + restore_busy: bool, + /// Worker result for the identity restore: `Ok(())` or an error message. + restore_result: std::sync::Arc>>>, + /// Sticky error from the identity restore, shown on the identity step. + restore_error: Option, +} + +/// Full-backup restore sub-state on the restore-from-seed words step. +#[derive(Default)] +struct BackupRestore { + /// The picked `.backup` file contents. + blob: String, + /// The password the backup was sealed under. + password: String, + /// Last error (bad file / wrong password), shown inline. + error: String, + /// A native file pick is in flight (Android resolves the path asynchronously). + picking: bool, + /// The seed was decrypted and loaded into the word grid. + unlocked: bool, } /// Onboarding identity-import state. Reuses the wallet password the user just @@ -124,6 +152,11 @@ impl Default for OnboardingContent { claim: ClaimState::default(), import: None, words_copied: None, + backup_restore: None, + pending_restore: None, + restore_busy: false, + restore_result: std::sync::Arc::new(std::sync::Mutex::new(None)), + restore_error: None, } } } @@ -601,6 +634,9 @@ impl OnboardingContent { ); }); ui.add_space(14.0); + // Restore a FULL .backup file: its seed feeds this same word grid, + // and its identities are reinstated once the wallet opens. + self.backup_restore_ui(ui, cb); } else { // Transient "Copied" feedback (the Build 82/89 pattern): a silent // copy of the recovery phrase reads as a dead button. @@ -652,6 +688,111 @@ impl OnboardingContent { self.error_ui(ui); } + /// Restore-from-seed helper: pick a FULL `.backup` file (seed + all + /// identities), unlock it with its password, and load the recovered seed into + /// the word grid so the standard creation path takes over. The identities are + /// stashed in `pending_restore` and reinstated after the wallet opens. + fn backup_restore_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let t = theme::tokens(); + // Already unlocked: the seed is in the grid; just confirm and stop. + if self.backup_restore.as_ref().is_some_and(|b| b.unlocked) { + ui.label( + RichText::new(t!("goblin.onboarding.words.backup_unlocked")) + .font(FontId::new(12.5, fonts::regular())) + .color(t.pos), + ); + ui.add_space(14.0); + return; + } + if self.backup_restore.is_none() { + self.backup_restore = Some(BackupRestore::default()); + } + // Recovered seed to apply AFTER the `br` borrow ends (avoids a double + // mutable borrow of `self`): (seed phrase, backup blob, backup password). + let mut apply: Option<(String, String, String)> = None; + { + let br = self.backup_restore.as_mut().unwrap(); + // Poll an async (Android) file pick. + if br.picking { + if let Some(path) = cb.picked_file() { + br.picking = false; + if !path.is_empty() { + match std::fs::read_to_string(&path) { + Ok(c) => br.blob = c.trim().to_string(), + Err(_) => { + br.error = t!("goblin.settings.backup_read_failed").to_string() + } + } + } + } else { + ui.ctx().request_repaint(); + } + } + if w::chip(ui, &t!("goblin.settings.choose_backup_file"), false).clicked() { + br.error.clear(); + match cb.pick_file() { + Some(path) if !path.is_empty() => match std::fs::read_to_string(&path) { + Ok(c) => br.blob = c.trim().to_string(), + Err(_) => br.error = t!("goblin.settings.backup_read_failed").to_string(), + }, + // Empty string = Android async pick in flight. + Some(_) => br.picking = true, + None => {} + } + } + // Once a full backup is loaded, ask for its password and unlock it. + if crate::nostr::is_full_backup(&br.blob) { + ui.add_space(8.0); + w::field_well(ui, |ui| { + TextEdit::new(egui::Id::from("onb_restore_bpw")) + .focus(false) + .hint_text(t!("goblin.settings.backup_password_hint")) + .password() + .text_color(t.surface_text) + .body() + .ui(ui, &mut br.password, cb); + }); + ui.add_space(8.0); + ui.add_enabled_ui(!br.password.is_empty(), |ui| { + if w::chip(ui, &t!("goblin.onboarding.words.unlock_backup"), false).clicked() { + match crate::nostr::open_full_backup(&br.blob, &br.password) { + Ok(full) => { + apply = Some(( + full.seed_phrase.clone(), + br.blob.clone(), + br.password.clone(), + )); + } + Err(_) => br.error = t!("goblin.advanced.wrong_password").to_string(), + } + } + }); + } + if !br.error.is_empty() { + ui.add_space(6.0); + ui.label( + RichText::new(&br.error) + .font(FontId::new(12.5, fonts::regular())) + .color(t.neg), + ); + } + } + ui.add_space(14.0); + // Apply outside the `br` borrow: fill the word grid with the recovered + // seed and stash the identities for post-open restore. + if let Some((phrase, blob, pw)) = apply { + self.mnemonic_setup + .mnemonic + .import(&ZeroingString::from(phrase)); + self.pending_restore = Some((blob, pw)); + if let Some(br) = self.backup_restore.as_mut() { + br.unlocked = true; + br.password.clear(); + br.error.clear(); + } + } + } + fn confirm_ui( &mut self, ui: &mut egui::Ui, @@ -741,6 +882,21 @@ impl OnboardingContent { wallets.add(w.clone()); match w.open(pass) { Ok(_) => { + // A full-backup restore: reinstate every identity from the + // backup in a worker once nostr is up (the seed itself was + // already restored through this creation path). + if let Some((blob, bpw)) = self.pending_restore.take() { + let wallet_pw = self.pass.clone(); + let wallet = w.clone(); + let slot = self.restore_result.clone(); + self.restore_busy = true; + self.restore_error = None; + std::thread::spawn(move || { + let res = + wallet.restore_full_backup_identities(&blob, &bpw, &wallet_pw); + *slot.lock().unwrap() = Some(res); + }); + } self.wallet = Some(w); self.error = None; self.step = Step::Identity; @@ -860,6 +1016,47 @@ impl OnboardingContent { }); ui.add_space(14.0); + // Full-backup identity restore in flight: poll the worker, and while it + // runs show a restoring card instead of the claim/import UI so the user + // never acts on the throwaway key it is replacing. + if self.restore_busy + && let Some(res) = self.restore_result.lock().unwrap().take() + { + self.restore_busy = false; + if let Err(e) = res { + self.restore_error = Some(e); + } + } + if self.restore_busy { + w::card(ui, |ui| { + ui.set_min_width(ui.available_width()); + ui.horizontal(|ui| { + View::small_loading_spinner(ui); + ui.add_space(8.0); + ui.label( + RichText::new(t!("goblin.onboarding.identity.restoring")) + .font(FontId::new(13.0, fonts::regular())) + .color(t.surface_text_dim), + ); + }); + }); + ui.add_space(14.0); + ui.ctx() + .request_repaint_after(std::time::Duration::from_millis(300)); + if w::big_action(ui, &t!("goblin.onboarding.identity.open_wallet"), false).clicked() { + return Some(wallet); + } + return None; + } + if let Some(err) = &self.restore_error { + ui.label( + RichText::new(err) + .font(FontId::new(12.5, fonts::regular())) + .color(t.neg), + ); + ui.add_space(14.0); + } + // Optional username claim — the same machinery as Settings. if let Some(msg) = self.claim.result.lock().unwrap().take() { self.claim.checking = false; diff --git a/src/nostr/identity.rs b/src/nostr/identity.rs index bbda8658..073b5f13 100644 --- a/src/nostr/identity.rs +++ b/src/nostr/identity.rs @@ -331,10 +331,217 @@ impl NostrIdentity { } } +/// The decrypted contents of a full wallet backup (see [`build_full_backup`]): +/// the money seed phrase plus every held identity (each with its unlocked keys), +/// and the hex of the identity that was active when the backup was made. +pub struct FullBackup { + /// The 24-word grin recovery phrase, in memory only. + pub seed_phrase: String, + /// Hex pubkey of the identity that was active at backup time (may be empty). + pub active: String, + /// Every held identity with its unlocked keys, re-openable by the restorer. + pub identities: Vec<(NostrIdentity, Keys)>, +} + +/// Seal an arbitrary UTF-8 string under a password with NO plaintext, reusing the +/// exact two-layer scheme of [`NostrIdentity::to_encrypted_backup`]: a fresh +/// random wrapper key is password-protected as a NIP-49 ncryptsec (scrypt), and +/// the text is NIP-44-sealed to that key. Returns `(k, d)` — the ncryptsec and +/// the sealed blob. The wrapper key is otherwise meaningless; only recovering the +/// text matters. No new crypto and no new dependency: same primitives the +/// identity backup already uses. +pub fn seal_secret_text( + plaintext: &str, + password: &str, +) -> Result<(String, String), IdentityError> { + let wrapper = Keys::generate(); + let encrypted = EncryptedSecretKey::new( + wrapper.secret_key(), + password, + NCRYPTSEC_LOG_N, + KeySecurity::Medium, + ) + .map_err(|e| IdentityError::Key(format!("encrypt failed: {e}")))?; + let k = encrypted + .to_bech32() + .map_err(|e| IdentityError::Key(format!("bech32 failed: {e}")))?; + let d = nip44::encrypt( + wrapper.secret_key(), + &wrapper.public_key(), + plaintext, + nip44::Version::V2, + ) + .map_err(|e| IdentityError::Key(format!("seal failed: {e}")))?; + Ok((k, d)) +} + +/// Reverse [`seal_secret_text`]: unlock the wrapper key with the password, then +/// open the NIP-44-sealed text. A wrong password fails at the ncryptsec layer. +pub fn open_secret_text(k: &str, d: &str, password: &str) -> Result { + let enc = EncryptedSecretKey::from_bech32(k) + .map_err(|e| IdentityError::Key(format!("invalid backup: {e}")))?; + let secret = enc + .decrypt(password) + .map_err(|_| IdentityError::WrongPassword)?; + let keys = Keys::new(secret); + nip44::decrypt(keys.secret_key(), &keys.public_key(), d) + .map_err(|_| IdentityError::WrongPassword) +} + +/// Build the contents of a FULL wallet `.backup` file (format version 2): the +/// money seed AND every held identity, all sealed under one password. The seed is +/// sealed with [`seal_secret_text`]; each identity is sealed with the SAME +/// per-identity scheme as the single-identity backup ([`NostrIdentity::to_encrypted_backup`]), +/// so every element is itself a valid v1 identity envelope. No plaintext (no +/// seed, no npub, no name) ever appears in the output. `identities` carries each +/// identity with its already-unlocked keys; `active_hex` is the identity active +/// at backup time. +pub fn build_full_backup( + seed_phrase: &str, + identities: &[(NostrIdentity, Keys)], + active_hex: &str, + password: &str, +) -> Result { + let (k, d) = seal_secret_text(seed_phrase, password)?; + let mut elems = Vec::with_capacity(identities.len()); + for (id, keys) in identities { + elems.push(id.to_encrypted_backup(keys)?); + } + let envelope = serde_json::json!({ + "goblin_backup": 2, + "seed": { "k": k, "d": d }, + "identities": elems, + "active": active_hex, + }); + serde_json::to_string(&envelope).map_err(IdentityError::from) +} + +/// True if `s` is a FULL wallet backup (format version 2 — seed + identities), +/// as opposed to a v1 single-identity backup or a bare nsec. Both formats set +/// `goblin_backup`; only v2 carries a seed, so callers that must create the +/// wallet check this FIRST. +pub fn is_full_backup(s: &str) -> bool { + serde_json::from_str::(s.trim()) + .ok() + .and_then(|v| v.get("goblin_backup").and_then(|x| x.as_u64())) + .map(|ver| ver >= 2) + .unwrap_or(false) +} + +/// Open a full wallet backup with its password, returning the seed phrase, the +/// active identity's hex, and every held identity with its unlocked keys. A wrong +/// password fails at the seed's ncryptsec layer. +pub fn open_full_backup(blob: &str, password: &str) -> Result { + let v: serde_json::Value = serde_json::from_str(blob.trim())?; + let seed = v + .get("seed") + .ok_or_else(|| IdentityError::Key("backup missing seed".into()))?; + let k = seed + .get("k") + .and_then(|x| x.as_str()) + .ok_or_else(|| IdentityError::Key("backup missing seed key".into()))?; + let d = seed + .get("d") + .and_then(|x| x.as_str()) + .ok_or_else(|| IdentityError::Key("backup missing seed data".into()))?; + let seed_phrase = open_secret_text(k, d, password)?; + let active = v + .get("active") + .and_then(|x| x.as_str()) + .unwrap_or("") + .to_string(); + let mut identities = Vec::new(); + if let Some(arr) = v.get("identities").and_then(|x| x.as_array()) { + for elem in arr { + let elem_str = elem + .as_str() + .ok_or_else(|| IdentityError::Key("malformed identity element".into()))?; + let (id, keys) = NostrIdentity::from_encrypted_backup(elem_str, password)?; + identities.push((id, keys)); + } + } + Ok(FullBackup { + seed_phrase, + active, + identities, + }) +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn full_backup_roundtrips_seed_identities_and_active() { + // Build a full backup from a seed + several identities, then reopen it: + // the seed text, every identity's key, and the active marker must survive, + // and NOTHING sensitive (seed word, npub, name) may appear in the file. + let seed = "abandon abandon abandon abandon abandon abandon abandon abandon \ + abandon abandon abandon abandon abandon abandon abandon abandon abandon \ + abandon abandon abandon abandon abandon abandon art"; + let (mut a, ka) = NostrIdentity::create_random("walletpw").unwrap(); + a.nip05 = Some("alice@goblin.st".to_string()); + a.anonymous = false; + let (b, kb) = NostrIdentity::create_random("walletpw").unwrap(); + let active_hex = b.pubkey_hex().unwrap(); + let ids = vec![(a.clone(), ka.clone()), (b.clone(), kb.clone())]; + + let blob = build_full_backup(seed, &ids, &active_hex, "walletpw").unwrap(); + assert!(is_full_backup(&blob)); + // A full backup is NOT a v1 single-identity backup, but the shared + // `goblin_backup` marker still reads as "an encrypted backup". + assert!(NostrIdentity::is_encrypted_backup(&blob)); + // Opaque: no seed word, no npub, no username leaks. + assert!(!blob.contains("abandon")); + assert!(!blob.contains(&a.npub)); + assert!(!blob.contains(&b.npub)); + assert!(!blob.contains("alice")); + + let opened = open_full_backup(&blob, "walletpw").unwrap(); + assert_eq!(opened.seed_phrase, seed); + assert_eq!(opened.active, active_hex); + assert_eq!(opened.identities.len(), 2); + // Both identities and their keys restore exactly, metadata intact. + let npubs: Vec<_> = opened + .identities + .iter() + .map(|(i, _)| i.npub.clone()) + .collect(); + assert!(npubs.contains(&a.npub)); + assert!(npubs.contains(&b.npub)); + let restored_a = opened + .identities + .iter() + .find(|(i, _)| i.npub == a.npub) + .unwrap(); + assert_eq!(restored_a.0.nip05.as_deref(), Some("alice@goblin.st")); + assert!(!restored_a.0.anonymous); + assert_eq!(restored_a.1.public_key(), ka.public_key()); + // Wrong password opens nothing. + assert!(open_full_backup(&blob, "wrong").is_err()); + } + + #[test] + fn old_single_identity_backup_is_not_a_full_backup() { + // A v1 single-identity envelope must NOT be mistaken for a full backup, and + // must keep restoring through the v1 path exactly as before. + let (a, keys) = NostrIdentity::create_random("pw-1").unwrap(); + let v1 = a.to_encrypted_backup(&keys).unwrap(); + assert!(NostrIdentity::is_encrypted_backup(&v1)); + assert!(!is_full_backup(&v1), "v1 must not read as a full backup"); + let (restored, _) = NostrIdentity::from_encrypted_backup(&v1, "pw-1").unwrap(); + assert_eq!(restored.npub, a.npub); + } + + #[test] + fn seal_secret_text_roundtrips_and_is_opaque() { + let secret = "the quick brown fox"; + let (k, d) = seal_secret_text(secret, "pw").unwrap(); + assert!(!k.contains(secret) && !d.contains(secret)); + assert_eq!(open_secret_text(&k, &d, "pw").unwrap(), secret); + assert!(open_secret_text(&k, &d, "nope").is_err()); + } + #[test] fn backup_restores_under_new_password() { // Export under one wallet password, restore on a device with another. diff --git a/src/nostr/mod.rs b/src/nostr/mod.rs index e30d2ab1..6a9f208b 100644 --- a/src/nostr/mod.rs +++ b/src/nostr/mod.rs @@ -29,7 +29,9 @@ mod store; pub use store::NostrStore; mod identity; -pub use identity::{IdentitySource, NostrIdentity}; +pub use identity::{ + FullBackup, IdentitySource, NostrIdentity, build_full_backup, is_full_backup, open_full_backup, +}; pub mod identities; pub use identities::{HeldError, HeldIdentities, MAX_IDENTITIES, catchup_since}; diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 0d20af6d..f0f8fc47 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -622,6 +622,14 @@ impl Wallet { old.unlock(&password) .map_err(|_| "Wrong password".to_string())?; let input = input.trim(); + // A FULL wallet backup carries a seed and belongs to wallet creation, not + // to swapping one identity in an existing wallet. Reject it here with a + // clear pointer rather than failing obscurely on the v1 path below. + if crate::nostr::is_full_backup(input) { + return Err("That's a full wallet backup. Restore it when creating a \ + wallet, not here." + .to_string()); + } let bpw = backup_password .as_deref() .filter(|s| !s.is_empty()) @@ -702,21 +710,137 @@ impl Wallet { Ok(new_npub) } - /// Build the contents of a `GOBLIN-*.backup` file: the whole nostr identity, - /// fully sealed under the wallet password. Verifies the password first. - pub fn create_nostr_backup(&self, password: &str) -> Result { - let svc = self - .nostr_service() - .ok_or_else(|| "nostr is not running".to_string())?; - let identity = svc.identity.read().clone(); - let keys = identity - .unlock(password) + /// Build the contents of a `GOBLIN-*.backup` file: the FULL wallet — the money + /// seed AND every held identity — sealed under the wallet password (format + /// version 2). The password both unlocks the seed (via [`get_recovery`], which + /// proves it) and unlocks each identity, so a wrong password fails before any + /// bytes are produced. The plaintext seed is assembled and sealed in memory and + /// never written to disk; the returned string is what the caller saves. + /// + /// [`get_recovery`]: Self::get_recovery + pub fn create_full_backup(&self, password: &str) -> Result { + // Unlock the seed first: this both verifies the wallet password and yields + // the phrase, held only in a zeroizing string that drops at end of scope. + let phrase = self + .get_recovery(password.to_string()) .map_err(|_| "Wrong password".to_string())?; - identity - .to_encrypted_backup(&keys) + let nostr_dir = self.get_config().get_nostr_path(); + let index = + HeldIdentities::load(&nostr_dir).ok_or_else(|| "identities unavailable".to_string())?; + // Unlock every held identity under the same wallet password so each can be + // re-sealed into the backup with its own recoverable key. + let mut identities = Vec::new(); + for entry in &index.identities { + let id = entry + .load(&nostr_dir) + .ok_or_else(|| "identity file unreadable".to_string())?; + let keys = id + .unlock(password) + .map_err(|_| "Wrong password".to_string())?; + identities.push((id, keys)); + } + crate::nostr::build_full_backup(&phrase, &identities, &index.active, password) .map_err(|e| format!("backup failed: {e}")) } + /// Restore every identity from a FULL backup into THIS freshly created wallet, + /// re-encrypting each under the wallet password and making the backup's active + /// identity active. The seed itself was already restored through the normal + /// 24-word wallet-creation path (the caller decrypted the seed to feed that + /// path); this method only reinstates the identities. It replaces the fresh + /// random identity the new wallet minted on open with the backed-up active one, + /// then adds the rest as held identities — reusing the same on-disk index and + /// service-rebuild plumbing as add/import. `blob` is the full-backup file, + /// `backup_password` the password it was sealed under (may differ from this + /// wallet's on a new device), `wallet_password` this wallet's password. + pub fn restore_full_backup_identities( + &self, + blob: &str, + backup_password: &str, + wallet_password: &str, + ) -> Result<(), String> { + let full = crate::nostr::open_full_backup(blob, backup_password) + .map_err(|_| "Couldn't open the backup — wrong password?".to_string())?; + if full.identities.is_empty() { + return Ok(()); + } + // The new wallet starts nostr asynchronously on open; wait for the service + // so we replace a fully-initialized identity set rather than racing it. + let mut svc = None; + for _ in 0..200 { + if let Some(s) = self.nostr_service() { + svc = Some(s); + break; + } + thread::sleep(Duration::from_millis(100)); + } + let svc = svc.ok_or_else(|| "nostr didn't start".to_string())?; + let config = self.get_config(); + let nostr_dir = config.get_nostr_path(); + // Re-encrypt every backed-up identity under THIS wallet's password. + let mut rebuilt: Vec = Vec::new(); + for (backup, keys) in &full.identities { + let mut ident = NostrIdentity::from_unlocked_keys(keys, wallet_password, backup.source) + .map_err(|e| format!("re-encryption failed: {e}"))?; + ident.nip05 = backup.nip05.clone(); + ident.anonymous = backup.anonymous; + ident.prev_npubs = backup.prev_npubs.clone(); + ident.private_tag = backup.private_tag.clone(); + rebuilt.push(ident); + } + // The active identity (as marked at backup time; else the first) becomes + // identity #1 in identity.json, overwriting the throwaway random key the + // fresh wallet minted on open. A clean single-entry index is then written + // so the discarded random key leaves no held entry behind. + let active_pos = rebuilt + .iter() + .position(|i| i.pubkey_hex().as_deref() == Some(full.active.as_str())) + .unwrap_or(0); + let primary = rebuilt[active_pos].clone(); + primary + .save(&nostr_dir) + .map_err(|e| format!("identity save failed: {e}"))?; + let mut index = HeldIdentities::from_legacy(&primary) + .ok_or_else(|| "identity has a malformed key".to_string())?; + index + .save(&nostr_dir) + .map_err(|e| format!("index save failed: {e}"))?; + let primary_hex = primary.pubkey_hex(); + for (i, ident) in rebuilt.iter().enumerate() { + if i == active_pos || ident.pubkey_hex() == primary_hex { + continue; + } + // Skip duplicates / cap overflow without aborting the whole restore. + if let Err(e) = index.add(&nostr_dir, ident) { + warn!("nostr: skipping identity during restore: {e}"); + } + } + // Rebuild the live service on the restored set, active identity active. + svc.stop(); + for _ in 0..100 { + if !svc.is_running() { + break; + } + thread::sleep(Duration::from_millis(100)); + } + let nostr_config = NostrConfig::load(PathBuf::from(config.get_data_path())); + let store = NostrStore::new(config.get_nostr_db_path()); + let (recv, active_hex) = self + .unlock_all_identities(&nostr_dir, wallet_password) + .ok_or_else(|| "identity unlock failed".to_string())?; + let new_svc = NostrService::new(recv, &active_hex, nostr_config, store, nostr_dir); + { + let mut w_nostr = self.nostr.write(); + *w_nostr = Some(new_svc.clone()); + } + new_svc.start(self.clone()); + info!( + "nostr: restored {} identit(ies) from full backup", + rebuilt.len() + ); + Ok(()) + } + // ── Held nostr identities (one wallet, one balance, many front doors) ────── // // One grin seed / one balance, but the wallet can HOLD several nostr @@ -802,6 +926,11 @@ impl Wallet { let (identity, _keys) = match import { Some(blob) => { let blob = blob.trim(); + if crate::nostr::is_full_backup(blob) { + return Err("That's a full wallet backup. Restore it when \ + creating a wallet, not here." + .to_string()); + } if NostrIdentity::is_encrypted_backup(blob) { // A .backup: open it (same wallet password, since a backup made // by this wallet is sealed under it), then re-encrypt under the