diff --git a/src/App/Localization/Loc.cs b/src/App/Localization/Loc.cs index c67e4eb..0fffb34 100644 --- a/src/App/Localization/Loc.cs +++ b/src/App/Localization/Loc.cs @@ -93,6 +93,7 @@ public sealed class Loc ["tab.history"] = ["Storico", "History", "Historial", "Historique", "Histórico", "Verlauf"], ["tab.addresses"] = ["Indirizzi", "Addresses", "Direcciones", "Adresses", "Endereços", "Adressen"], ["tab.send"] = ["Invia", "Send", "Enviar", "Envoyer", "Enviar", "Senden"], + ["tab.contacts"] = ["Contatti", "Contacts", "Contactos", "Contacts", "Contatos", "Kontakte"], ["receive.next"] = ["Prossimo indirizzo non usato:", "Next unused address:", "Próxima dirección no usada:", "Prochaine adresse non utilisée :", "Próximo endereço não usado:", "Nächste ungenutzte Adresse:"], ["receive.hint"] = [ "Ogni pagamento ricevuto qui comparirà nello storico alla prossima sincronizzazione.", @@ -105,8 +106,18 @@ public sealed class Loc ["addr.index"] = ["Indice", "Index", "Índice", "Index", "Índice", "Index"], ["addr.address"] = ["Indirizzo", "Address", "Dirección", "Adresse", "Endereço", "Adresse"], ["addr.balance"] = ["Saldo", "Balance", "Saldo", "Solde", "Saldo", "Saldo"], + ["addr.copied"] = ["Indirizzo copiato negli appunti", "Address copied to clipboard", "Dirección copiada al portapapeles", "Adresse copiée dans le presse-papiers", "Endereço copiado para a área de transferência", "Adresse in die Zwischenablage kopiert"], + ["addr.derivpath"] = ["Percorso di derivazione:", "Derivation path:", "Ruta de derivación:", "Chemin de dérivation :", "Caminho de derivação:", "Ableitungspfad:"], + ["addr.pubkey"] = ["Chiave pubblica:", "Public key:", "Clave pública:", "Clé publique :", "Chave pública:", "Öffentlicher Schlüssel:"], + ["addr.privkey"] = ["Chiave privata (WIF):", "Private key (WIF):", "Clave privada (WIF):", "Clé privée (WIF) :", "Chave privada (WIF):", "Privater Schlüssel (WIF):"], + ["addr.show.privkey"] = ["Mostra", "Show", "Mostrar", "Afficher", "Mostrar", "Anzeigen"], + ["addr.hide.privkey"] = ["Nascondi", "Hide", "Ocultar", "Masquer", "Ocultar", "Ausblenden"], ["addr.receive"] = ["ricezione", "receive", "recepción", "réception", "recebimento", "Empfang"], ["addr.change"] = ["change", "change", "cambio", "monnaie", "troco", "Wechselgeld"], + ["addr.info.title"] = ["Informazioni indirizzo", "Address information", "Información de dirección", "Informations sur l'adresse", "Informações do endereço", "Adressinformationen"], + ["addr.close"] = ["Chiudi", "Close", "Cerrar", "Fermer", "Fechar", "Schließen"], + ["send.from.contact"] = ["Da contatti:", "From contacts:", "De contactos:", "Depuis les contacts :", "De contatos:", "Aus Kontakten:"], + ["send.contact.hint"] = ["seleziona per riempire l'indirizzo", "select to fill address", "selecciona para rellenar la dirección", "sélectionner pour remplir l'adresse", "selecione para preencher o endereço", "auswählen um Adresse einzufügen"], ["send.to"] = ["Indirizzo destinatario", "Recipient address", "Dirección destinataria", "Adresse du destinataire", "Endereço do destinatário", "Empfängeradresse"], ["send.amount"] = ["Importo", "Amount", "Importe", "Montant", "Valor", "Betrag"], ["send.all"] = ["Invia tutto", "Send all", "Enviar todo", "Tout envoyer", "Enviar tudo", "Alles senden"], @@ -217,6 +228,15 @@ public sealed class Loc "SSL-Zertifikate zurückgesetzt: Verbindung erneut versuchen."], ["msg.error"] = ["Errore", "Error", "Error", "Erreur", "Erro", "Fehler"], + // Contatti + ["contacts.name"] = ["Nome", "Name", "Nombre", "Nom", "Nome", "Name"], + ["contacts.address"] = ["Indirizzo", "Address", "Dirección", "Adresse", "Endereço", "Adresse"], + ["contacts.name.ph"] = ["Nome contatto", "Contact name", "Nombre del contacto", "Nom du contact", "Nome do contato", "Kontaktname"], + ["contacts.address.ph"] = ["Indirizzo blockchain", "Blockchain address", "Dirección blockchain", "Adresse blockchain", "Endereço blockchain", "Blockchain-Adresse"], + ["contacts.add"] = ["Aggiungi", "Add", "Agregar", "Ajouter", "Adicionar", "Hinzufügen"], + ["contacts.remove"] = ["Rimuovi selezionato", "Remove selected", "Eliminar seleccionado", "Supprimer la sélection", "Remover selecionado", "Auswahl entfernen"], + ["contacts.empty"] = ["Nessun contatto salvato.", "No saved contacts.", "No hay contactos guardados.", "Aucun contact enregistré.", "Nenhum contato salvo.", "Keine gespeicherten Kontakte."], + // Finestra impostazioni ["settings.title"] = ["Impostazioni", "Settings", "Configuración", "Paramètres", "Configurações", "Einstellungen"], ["settings.language"] = ["Lingua", "Language", "Idioma", "Langue", "Idioma", "Sprache"], diff --git a/src/App/ViewModels/MainWindowViewModel.cs b/src/App/ViewModels/MainWindowViewModel.cs index 4ac4cc7..f8679fc 100644 --- a/src/App/ViewModels/MainWindowViewModel.cs +++ b/src/App/ViewModels/MainWindowViewModel.cs @@ -21,8 +21,24 @@ namespace PalladiumWallet.App.ViewModels; /// Riga dello storico transazioni per la vista. public sealed record HistoryRow(string Conferma, string Importo, string Txid, string Verificata); -/// Riga della vista indirizzi (stile Electrum): saldo e uso per indirizzo. -public sealed record AddressRow(string Tipo, int Indice, string Indirizzo, string Saldo, string NumTx); +/// Riga della vista indirizzi con chiavi e derivation path pre-calcolati. +public sealed record AddressRow( + string Tipo, int Indice, string Indirizzo, string Saldo, string NumTx, + bool IsChange = false, string PubKey = "", string PrivKey = "", string DerivPath = "") +{ + public bool HasPrivKey => !string.IsNullOrEmpty(PrivKey); +} + +/// Dati completi di un indirizzo passati alla finestra di dettaglio. +public sealed record AddressInfo( + Localization.Loc Loc, + string Address, string DerivPath, string PubKey, string PrivKey) +{ + public bool HasPrivKey => !string.IsNullOrEmpty(PrivKey); +} + +/// Contatto in rubrica: nome + indirizzo blockchain. +public sealed record ContactEntry(string Name, string Address); /// /// ViewModel unico dell'applicazione (wizard §15 ridotto + dashboard): @@ -108,6 +124,18 @@ public partial class MainWindowViewModel : ViewModelBase private string Fmt(long sats, bool withLabel = true) => CoinAmount.FormatIn(sats, _config.Unit, withLabel); + /// Chiave privata WIF per un indirizzo; stringa vuota se watch-only. + private string KeyWif(bool isChange, int index) + { + if (_account is null or { IsWatchOnly: true }) return ""; + try + { + return _account.GetExtPrivateKey(isChange, index) + .PrivateKey.GetWif(PalladiumNetworks.For(Net)).ToString(); + } + catch { return ""; } + } + /// File in attesa di password (apertura da menu File → Apri). private string? _pendingOpenPath; @@ -216,6 +244,73 @@ public partial class MainWindowViewModel : ViewModelBase public ObservableCollection Addresses { get; } = []; + // ---- tab indirizzi ---- + + [ObservableProperty] + private AddressRow? selectedAddressRow; + + // ---- rubrica contatti ---- + + public ObservableCollection Contacts { get; } = []; + + [ObservableProperty] + private ContactEntry? selectedContactInList; + + /// Contatto selezionato nella ComboBox del pannello Invia: riempie SendTo. + [ObservableProperty] + private ContactEntry? sendToContact; + + partial void OnSendToContactChanged(ContactEntry? value) + { + if (value is not null) + SendTo = value.Address; + } + + [ObservableProperty] + private string newContactName = ""; + + [ObservableProperty] + private string newContactAddress = ""; + + [RelayCommand] + private void AddContact() + { + var name = NewContactName.Trim(); + var addr = NewContactAddress.Trim(); + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(addr)) return; + Contacts.Add(new ContactEntry(name, addr)); + NewContactName = NewContactAddress = ""; + PersistContacts(); + } + + [RelayCommand] + private void RemoveSelectedContact() + { + if (SelectedContactInList is { } c) + { + Contacts.Remove(c); + SelectedContactInList = null; + PersistContacts(); + } + } + + private void PersistContacts() + { + if (_doc is null || _walletPath is null) return; + _doc.Contacts = Contacts + .Select(c => new PalladiumWallet.Core.Storage.StoredContact { Name = c.Name, Address = c.Address }) + .ToList(); + WalletStore.Save(_doc, _walletPath, _password); + } + + private void LoadContacts() + { + Contacts.Clear(); + if (_doc is null) return; + foreach (var c in _doc.Contacts) + Contacts.Add(new ContactEntry(c.Name, c.Address)); + } + // ---- pannello invia ---- [ObservableProperty] @@ -495,6 +590,7 @@ public partial class MainWindowViewModel : ViewModelBase NetworkInfo = $"{doc.Network} · {doc.ScriptKind} · m/{doc.AccountPath}" + (doc.IsWatchOnly ? " · watch-only" : ""); + LoadContacts(); ApplyCache(doc.Cache); IsWalletOpen = true; StatusMessage = Loc.Tr("msg.opened"); @@ -516,8 +612,12 @@ public partial class MainWindowViewModel : ViewModelBase // Prima della sincronizzazione si mostrano i primi indirizzi derivati. Addresses.Clear(); for (var i = 0; i < 10; i++) - Addresses.Add(new AddressRow("ricezione", i, - _account.GetReceiveAddress(i).ToString(), "—", "—")); + Addresses.Add(new AddressRow(_loc["addr.receive"], i, + _account.GetReceiveAddress(i).ToString(), "—", "—", + false, + _account.GetPublicKey(false, i).ToHex(), + KeyWif(false, i), + $"m/{_doc!.AccountPath}/0/{i}")); return; } BalanceText = Fmt(cache.ConfirmedSats); @@ -540,11 +640,15 @@ public partial class MainWindowViewModel : ViewModelBase Addresses.Clear(); foreach (var a in cache.Addresses) Addresses.Add(new AddressRow( - a.IsChange ? "change" : "ricezione", + a.IsChange ? _loc["addr.change"] : _loc["addr.receive"], a.Index, a.Address, a.BalanceSats > 0 ? Fmt(a.BalanceSats, withLabel: false) : (a.TxCount > 0 ? "0" : "—"), - a.TxCount > 0 ? a.TxCount.ToString() : "—")); + a.TxCount > 0 ? a.TxCount.ToString() : "—", + a.IsChange, + _account.GetPublicKey(a.IsChange, a.Index).ToHex(), + KeyWif(a.IsChange, a.Index), + $"m/{_doc!.AccountPath}/{(a.IsChange ? 1 : 0)}/{a.Index}")); } // ---------- comandi wallet ---------- @@ -757,6 +861,10 @@ public partial class MainWindowViewModel : ViewModelBase _pendingSend = null; HasPendingSend = false; History.Clear(); + Contacts.Clear(); + SelectedContactInList = null; + SendToContact = null; + SelectedAddressRow = null; IsWalletOpen = false; IsConnected = false; ConnectionStatus = Loc.Tr("conn.none"); diff --git a/src/App/Views/AddressInfoWindow.axaml b/src/App/Views/AddressInfoWindow.axaml new file mode 100644 index 0000000..c451f1b --- /dev/null +++ b/src/App/Views/AddressInfoWindow.axaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +