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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App/Views/AddressInfoWindow.axaml.cs b/src/App/Views/AddressInfoWindow.axaml.cs
new file mode 100644
index 0000000..59db832
--- /dev/null
+++ b/src/App/Views/AddressInfoWindow.axaml.cs
@@ -0,0 +1,20 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using PalladiumWallet.App.ViewModels;
+
+namespace PalladiumWallet.App.Views;
+
+public partial class AddressInfoWindow : Window
+{
+ public AddressInfoWindow()
+ {
+ InitializeComponent();
+ }
+
+ public AddressInfoWindow(AddressInfo info) : this()
+ {
+ DataContext = info;
+ }
+
+ private void OnCloseClick(object? sender, RoutedEventArgs e) => Close();
+}
diff --git a/src/App/Views/MainWindow.axaml b/src/App/Views/MainWindow.axaml
index 9d24946..223fc95 100644
--- a/src/App/Views/MainWindow.axaml
+++ b/src/App/Views/MainWindow.axaml
@@ -222,18 +222,10 @@
-
+
-
-
-
-
-
-
-
+
@@ -250,34 +242,23 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -302,6 +283,90 @@
FontSize="13"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App/Views/MainWindow.axaml.cs b/src/App/Views/MainWindow.axaml.cs
index 6b937ec..f255620 100644
--- a/src/App/Views/MainWindow.axaml.cs
+++ b/src/App/Views/MainWindow.axaml.cs
@@ -1,7 +1,12 @@
using System.Linq;
+using System.Threading.Tasks;
+using Avalonia;
using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
+using Avalonia.VisualTree;
using PalladiumWallet.App.ViewModels;
namespace PalladiumWallet.App.Views;
@@ -13,7 +18,6 @@ public partial class MainWindow : Window
InitializeComponent();
}
- /// File → Apri wallet da file (il picker richiede il TopLevel, da qui).
private async void OnOpenWalletFileClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel vm)
@@ -32,4 +36,26 @@ public partial class MainWindow : Window
if (files.FirstOrDefault()?.TryGetLocalPath() is { } path)
vm.OpenFromPath(path);
}
+
+ private async void OnAddressListTapped(object? sender, TappedEventArgs e)
+ {
+ if (DataContext is not MainWindowViewModel vm || vm.SelectedAddressRow is not { } row)
+ return;
+ var info = new AddressInfo(vm.Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
+ await new AddressInfoWindow(info).ShowDialog(this);
+ }
+
+ private async void OnAddressListPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (!e.GetCurrentPoint(null).Properties.IsRightButtonPressed) return;
+ if (sender is not ListBox lb || DataContext is not MainWindowViewModel vm) return;
+
+ var item = (e.Source as Visual)?.FindAncestorOfType();
+ if (item is not { DataContext: AddressRow row }) return;
+
+ lb.SelectedItem = row;
+
+ var info = new AddressInfo(vm.Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
+ await new AddressInfoWindow(info).ShowDialog(this);
+ }
}