From 7f2759b2fcf1fc193a40fefd601159102d30cec6 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Fri, 12 Jun 2026 10:13:09 +0200 Subject: [PATCH] feat(app): multi-wallet chooser, confirm-password, encrypt toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-wallet (§8): - AppPaths.WalletFiles(net) enumerates all *.wallet.json in the wallets dir; WizardStartOpen shows a chooser step when more than one is found - New StepChooseWallet + ChooseWalletCommand; Back from StepOpen returns to chooser (or to StepStart when only one wallet) - WalletFileEntry record for the chooser ItemsControl Password step (Electrum style): - ConfirmPasswordInput: password must be typed twice before creating - EncryptWallet checkbox (default true); when unchecked, password is null (plaintext file) with an explicit warning - Validation: empty password with encryption on → msg.password.required; mismatch → msg.password.mismatch - ConfirmPasswordInput cleared on wallet open and wizard back AppPaths: - DefaultDataRoot() is now platform-aware: %APPDATA%\PalladiumWallet on Windows, ~/.palladium-wallet on Linux/macOS (matches §8 convention) - Legacy root fallback removed (no prior releases to migrate from) i18n: wiz.choose.title, wiz.password.confirm, wiz.password.encrypt, wiz.password.encrypt.hint, msg.choose.wallet, msg.password.required, msg.password.mismatch — all 6 languages --- src/App/Localization/Loc.cs | 25 ++++++++ src/App/ViewModels/MainWindowViewModel.cs | 78 +++++++++++++++++++++-- src/App/Views/MainWindow.axaml | 26 +++++++- src/Core/Storage/AppPaths.cs | 34 +++++----- 4 files changed, 140 insertions(+), 23 deletions(-) diff --git a/src/App/Localization/Loc.cs b/src/App/Localization/Loc.cs index 71e85cf..9e39719 100644 --- a/src/App/Localization/Loc.cs +++ b/src/App/Localization/Loc.cs @@ -65,6 +65,7 @@ public sealed class Loc ["wiz.data.default"] = ["Percorso predefinito:", "Default path:", "Ruta predeterminada:", "Chemin par défaut :", "Caminho padrão:", "Standardpfad:"], ["wiz.data.usedefault"] = ["Usa il percorso predefinito", "Use the default path", "Usar la ruta predeterminada", "Utiliser le chemin par défaut", "Usar o caminho padrão", "Standardpfad verwenden"], ["wiz.data.choose"] = ["Scegli una cartella…", "Choose a folder…", "Elegir una carpeta…", "Choisir un dossier…", "Escolher uma pasta…", "Ordner wählen…"], + ["wiz.choose.title"] = ["Scegli il wallet da aprire", "Choose the wallet to open", "Elige el wallet a abrir", "Choisissez le wallet à ouvrir", "Escolha a carteira a abrir", "Wallet zum Öffnen wählen"], ["wiz.net"] = ["Rete:", "Network:", "Red:", "Réseau :", "Rede:", "Netzwerk:"], ["wiz.open.btn"] = ["Apri il wallet esistente", "Open existing wallet", "Abrir wallet existente", "Ouvrir le wallet existant", "Abrir carteira existente", "Vorhandenes Wallet öffnen"], ["wiz.new.btn"] = ["Crea un nuovo wallet", "Create a new wallet", "Crear nuevo wallet", "Créer un nouveau wallet", "Criar nova carteira", "Neues Wallet erstellen"], @@ -90,6 +91,15 @@ public sealed class Loc ["wiz.password.title"] = ["Password del file wallet", "Wallet file password", "Contraseña del archivo wallet", "Mot de passe du fichier wallet", "Senha do arquivo da carteira", "Wallet-Dateipasswort"], ["wiz.password.placeholder"] = ["Consigliata (vuoto = file in chiaro su disco)", "Recommended (empty = plaintext file on disk)", "Recomendada (vacío = archivo en texto claro en disco)", "Recommandé (vide = fichier en texte clair sur disque)", "Recomendada (vazio = arquivo em texto simples no disco)", "Empfohlen (leer = Klartextdatei auf Disk)"], ["wiz.password.create"] = ["Crea il wallet", "Create wallet", "Crear wallet", "Créer le wallet", "Criar carteira", "Wallet erstellen"], + ["wiz.password.confirm"] = ["Ripeti la password", "Repeat the password", "Repite la contraseña", "Répétez le mot de passe", "Repita a senha", "Passwort wiederholen"], + ["wiz.password.encrypt"] = ["Cifra il file wallet con la password", "Encrypt the wallet file with the password", "Cifrar el archivo wallet con la contraseña", "Chiffrer le fichier wallet avec le mot de passe", "Criptografar o arquivo da carteira com a senha", "Wallet-Datei mit dem Passwort verschlüsseln"], + ["wiz.password.encrypt.hint"] = [ + "Attenzione: senza cifratura il seed resta in chiaro sul disco.", + "Warning: without encryption the seed stays in plaintext on disk.", + "Atención: sin cifrado la semilla queda en texto claro en el disco.", + "Attention : sans chiffrement, la graine reste en clair sur le disque.", + "Atenção: sem criptografia a semente fica em texto simples no disco.", + "Achtung: ohne Verschlüsselung bleibt der Seed im Klartext auf der Festplatte."], ["wiz.back"] = ["Indietro", "Back", "Atrás", "Retour", "Voltar", "Zurück"], ["wiz.next"] = ["Avanti", "Next", "Siguiente", "Suivant", "Próximo", "Weiter"], @@ -216,6 +226,21 @@ public sealed class Loc "Mot de passe de chiffrement pour le fichier wallet sur disque (recommandé). Ne remplace pas la graine : protège uniquement le fichier.", "Senha de criptografia para o arquivo da carteira no disco (recomendada). Não substitui a semente: apenas protege o arquivo.", "Verschlüsselungspasswort für die Wallet-Datei auf der Festplatte (empfohlen). Ersetzt nicht den Seed: schützt nur die Datei."], + ["msg.choose.wallet"] = ["Più wallet disponibili: scegline uno.", "Multiple wallets available: pick one.", "Varios wallets disponibles: elige uno.", "Plusieurs wallets disponibles : choisissez-en un.", "Várias carteiras disponíveis: escolha uma.", "Mehrere Wallets verfügbar: wählen Sie eines."], + ["msg.password.required"] = [ + "Inserisci una password per cifrare il wallet (o togli la spunta «Cifra il file wallet»).", + "Enter a password to encrypt the wallet (or uncheck “Encrypt the wallet file”).", + "Ingresa una contraseña para cifrar el wallet (o desmarca «Cifrar el archivo wallet»).", + "Entrez un mot de passe pour chiffrer le wallet (ou décochez « Chiffrer le fichier wallet »).", + "Digite uma senha para criptografar a carteira (ou desmarque «Criptografar o arquivo da carteira»).", + "Geben Sie ein Passwort zum Verschlüsseln ein (oder deaktivieren Sie „Wallet-Datei verschlüsseln“)."], + ["msg.password.mismatch"] = [ + "Le due password non coincidono.", + "The two passwords do not match.", + "Las dos contraseñas no coinciden.", + "Les deux mots de passe ne correspondent pas.", + "As duas senhas não coincidem.", + "Die beiden Passwörter stimmen nicht überein."], ["msg.wrongpassword"] = ["Password errata.", "Wrong password.", "Contraseña incorrecta.", "Mot de passe incorrect.", "Senha incorreta.", "Falsches Passwort."], ["msg.opened"] = ["Wallet aperto: connessione al server…", "Wallet opened: connecting to server…", "Wallet abierto: conectando al servidor…", "Wallet ouvert : connexion au serveur…", "Carteira aberta: conectando ao servidor…", "Wallet geöffnet: Verbindung zum Server…"], ["msg.synced"] = ["Sincronizzato", "Synchronized", "Sincronizado", "Synchronisé", "Sincronizado", "Synchronisiert"], diff --git a/src/App/ViewModels/MainWindowViewModel.cs b/src/App/ViewModels/MainWindowViewModel.cs index f3ace12..ce8edfd 100644 --- a/src/App/ViewModels/MainWindowViewModel.cs +++ b/src/App/ViewModels/MainWindowViewModel.cs @@ -40,6 +40,9 @@ public sealed record AddressInfo( /// Contatto in rubrica: nome + indirizzo blockchain. public sealed record ContactEntry(string Name, string Address); +/// Voce della lista di scelta wallet: nome file + percorso completo. +public sealed record WalletFileEntry(string Name, string Path); + /// /// ViewModel unico dell'applicazione (wizard §15 ridotto + dashboard): /// pannello di setup (crea/ripristina/apri) e pannello wallet @@ -167,6 +170,7 @@ public partial class MainWindowViewModel : ViewModelBase public const string StepDataLocation = "data-location"; public const string StepStart = "start"; + public const string StepChooseWallet = "choose-wallet"; public const string StepOpen = "open"; public const string StepShowSeed = "show-seed"; public const string StepConfirmSeed = "confirm-seed"; @@ -177,6 +181,7 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsStepDataLocation))] [NotifyPropertyChangedFor(nameof(IsStepStart))] + [NotifyPropertyChangedFor(nameof(IsStepChooseWallet))] [NotifyPropertyChangedFor(nameof(IsStepOpen))] [NotifyPropertyChangedFor(nameof(IsStepShowSeed))] [NotifyPropertyChangedFor(nameof(IsStepConfirmSeed))] @@ -187,6 +192,7 @@ public partial class MainWindowViewModel : ViewModelBase public bool IsStepDataLocation => SetupStep == StepDataLocation; public bool IsStepStart => SetupStep == StepStart; + public bool IsStepChooseWallet => SetupStep == StepChooseWallet; public bool IsStepOpen => SetupStep == StepOpen; public bool IsStepShowSeed => SetupStep == StepShowSeed; public bool IsStepConfirmSeed => SetupStep == StepConfirmSeed; @@ -212,6 +218,14 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] private string passwordInput = ""; + /// Conferma password alla creazione (stile Electrum: digitata due volte). + [ObservableProperty] + private string confirmPasswordInput = ""; + + /// Se true il file wallet viene cifrato con la password (default, stile Electrum). + [ObservableProperty] + private bool encryptWallet = true; + // ---- pannello wallet ---- [ObservableProperty] @@ -280,6 +294,9 @@ public partial class MainWindowViewModel : ViewModelBase public ObservableCollection Addresses { get; } = []; + /// Wallet disponibili nella cartella della rete, per la schermata di scelta. + public ObservableCollection WalletList { get; } = []; + // ---- tab indirizzi ---- [ObservableProperty] @@ -457,8 +474,8 @@ public partial class MainWindowViewModel : ViewModelBase private void RefreshSetupState() { SetupStep = StepStart; - MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ""; - WalletFileExists = WalletStore.Exists(AppPaths.DefaultWalletPath(Net)); + MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ConfirmPasswordInput = ""; + WalletFileExists = AppPaths.WalletFiles(Net).Count > 0; StatusMessage = WalletFileExists ? Loc.Tr("msg.welcome.existing") : Loc.Tr("msg.welcome.new"); @@ -535,6 +552,31 @@ public partial class MainWindowViewModel : ViewModelBase [RelayCommand] private void WizardStartOpen() { + // Con più wallet nella cartella della rete si sceglie quale aprire; + // con uno solo si va dritti alla password (multi-wallet §8). + var files = AppPaths.WalletFiles(Net); + if (files.Count > 1) + { + WalletList.Clear(); + foreach (var path in files) + WalletList.Add(new WalletFileEntry(Path.GetFileName(path), path)); + SetupStep = StepChooseWallet; + StatusMessage = Loc.Tr("msg.choose.wallet"); + return; + } + _pendingOpenPath = files.Count == 1 ? files[0] : AppPaths.DefaultWalletPath(Net); + PasswordInput = ""; + SetupStep = StepOpen; + StatusMessage = Loc.Tr("msg.open.password"); + } + + /// Sceglie quale wallet aprire dalla lista e passa alla password. + [RelayCommand] + private void ChooseWallet(WalletFileEntry? entry) + { + if (entry is null) + return; + _pendingOpenPath = entry.Path; PasswordInput = ""; SetupStep = StepOpen; StatusMessage = Loc.Tr("msg.open.password"); @@ -600,7 +642,8 @@ public partial class MainWindowViewModel : ViewModelBase [RelayCommand] private void WizardNextFromPassphrase() { - PasswordInput = ""; + PasswordInput = ConfirmPasswordInput = ""; + EncryptWallet = true; SetupStep = StepPassword; StatusMessage = Loc.Tr("msg.password.info"); } @@ -610,7 +653,8 @@ public partial class MainWindowViewModel : ViewModelBase { SetupStep = SetupStep switch { - StepOpen or StepShowSeed or StepWords => StepStart, + StepOpen => WalletList.Count > 1 ? StepChooseWallet : StepStart, + StepChooseWallet or StepShowSeed or StepWords => StepStart, StepConfirmSeed => StepShowSeed, StepPassphrase => _isRestoreFlow ? StepWords : StepConfirmSeed, StepPassword => StepPassphrase, @@ -623,6 +667,29 @@ public partial class MainWindowViewModel : ViewModelBase [RelayCommand] private void CreateOrRestore() { + // Scelta cifratura stile Electrum: con cifratura attiva serve una + // password non vuota, digitata due volte e coincidente. Solo se la + // cifratura è disattivata il file resta in chiaro (avviso esplicito). + string? password; + if (EncryptWallet) + { + if (string.IsNullOrEmpty(PasswordInput)) + { + StatusMessage = Loc.Tr("msg.password.required"); + return; + } + if (PasswordInput != ConfirmPasswordInput) + { + StatusMessage = Loc.Tr("msg.password.mismatch"); + return; + } + password = PasswordInput; + } + else + { + password = null; + } + try { var (doc, account) = WalletLoader.NewFromMnemonic( @@ -634,7 +701,6 @@ public partial class MainWindowViewModel : ViewModelBase var path = AppPaths.DefaultWalletPath(Net); for (var n = 2; WalletStore.Exists(path); n++) path = Path.Combine(AppPaths.WalletsDir(Net), $"wallet-{n}.wallet.json"); - var password = string.IsNullOrEmpty(PasswordInput) ? null : PasswordInput; WalletStore.Save(doc, path, password); OpenLoaded(doc, account, path, password); } @@ -710,7 +776,7 @@ public partial class MainWindowViewModel : ViewModelBase _account = account; _walletPath = path; _password = password; - MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ""; + MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ConfirmPasswordInput = ""; SetupStep = StepStart; NetworkInfo = $"{doc.Network} · {doc.ScriptKind} · m/{doc.AccountPath}" diff --git a/src/App/Views/MainWindow.axaml b/src/App/Views/MainWindow.axaml index d3a45e5..1a33d84 100644 --- a/src/App/Views/MainWindow.axaml +++ b/src/App/Views/MainWindow.axaml @@ -73,6 +73,23 @@ Command="{Binding WizardStartRestoreCommand}"/> + + + + + + + public static string DefaultDataRoot() { + if (OperatingSystem.IsWindows()) + return Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + AppDirName); var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - return Path.Combine(home, "." + AppDirName); + return Path.Combine(home, ".palladium-wallet"); } - /// Vecchia radice (%APPDATA%/PalladiumWallet, ~/.config su Linux): mantenuta - /// per non orfanizzare i dati di installazioni precedenti. - private static string LegacyDataRoot() => - Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - AppDirName); - /// File puntatore alla radice dati scelta dall'utente. Vive in una /// posizione di bootstrap sempre scrivibile e indipendente dalla radice dati. private static string LocationPointerPath() => @@ -65,23 +63,18 @@ public static class AppPaths if (ReadPointer() is { } custom) return custom; - var legacy = LegacyDataRoot(); - if (HasData(legacy)) - return legacy; - return DefaultDataRoot(); } /// /// true se la posizione dei dati è già determinata e non serve chiederla /// all'utente: modalità portable, override, puntatore già scritto, oppure - /// dati già presenti nella posizione legacy o nella default. + /// dati già presenti nella posizione predefinita. /// public static bool IsDataLocationConfigured() => !string.IsNullOrEmpty(OverrideDataRoot) || Directory.Exists(PortableRoot()) || ReadPointer() is not null - || HasData(LegacyDataRoot()) || HasData(DefaultDataRoot()); /// Memorizza la radice dati scelta dall'utente e la crea su disco. @@ -121,6 +114,15 @@ public static class AppPaths public static string DefaultWalletPath(NetKind net) => Path.Combine(WalletsDir(net), "default.wallet.json"); + /// Tutti i file wallet della rete, ordinati per nome (multi-wallet §8). + public static IReadOnlyList WalletFiles(NetKind net) + { + var dir = WalletsDir(net); + return Directory.EnumerateFiles(dir, "*.wallet.json") + .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + public static string CertificatePinsPath(NetKind net) => Path.Combine(ForNetwork(net), "server-certs.json");