docs: translate all code comments to English (language policy)
Translate every Italian /// XML doc comment, <!-- --> XAML comment, and // inline comment to English across all source files (Core, App, tests). Add the language policy to CLAUDE.md (conversation Italian; all code and docs English). Update one test assertion that checked an Italian exception message that was also translated.
This commit is contained in:
@@ -10,9 +10,9 @@ using PalladiumWallet.Core.Storage;
|
||||
|
||||
namespace PalladiumWallet.Mobile;
|
||||
|
||||
// In Avalonia 12 l'AppBuilder Android si configura nella sottoclasse Application
|
||||
// (AvaloniaAndroidApplication<TApp>), non più nell'Activity. allowBackup=false:
|
||||
// il file wallet cifrato/seed non deve finire nei backup cloud automatici.
|
||||
// In Avalonia 12 the Android AppBuilder is configured in the Application subclass
|
||||
// (AvaloniaAndroidApplication<TApp>), no longer in the Activity. allowBackup=false:
|
||||
// the encrypted wallet file/seed must not end up in automatic cloud backups.
|
||||
[Application(Label = "Palladium Wallet", AllowBackup = false,
|
||||
Icon = "@mipmap/ic_launcher", RoundIcon = "@mipmap/ic_launcher_round")]
|
||||
public class MainApplication : AvaloniaAndroidApplication<global::PalladiumWallet.App.App>
|
||||
@@ -26,7 +26,7 @@ public class MainApplication : AvaloniaAndroidApplication<global::PalladiumWalle
|
||||
{
|
||||
AppPaths.OverrideDataRoot = FilesDir?.AbsolutePath;
|
||||
|
||||
// Registra lo scanner QR: apre ScannerActivity e ne attende il risultato.
|
||||
// Registers the QR scanner: opens ScannerActivity and waits for its result.
|
||||
PlatformServices.ScanQrAsync = async () =>
|
||||
{
|
||||
var activity = MainActivity.Current;
|
||||
|
||||
@@ -16,8 +16,8 @@ using AndroidResult = Android.App.Result;
|
||||
namespace PalladiumWallet.Mobile;
|
||||
|
||||
/// <summary>
|
||||
/// Activity full-screen per la scansione QR via Camera2 + ZXing.Net.
|
||||
/// Torna a MainActivity via SetResult con l'extra "qr" = testo del codice.
|
||||
/// Full-screen activity for QR scanning via Camera2 + ZXing.Net.
|
||||
/// Returns to MainActivity via SetResult with the extra "qr" = code text.
|
||||
/// </summary>
|
||||
[Activity(Theme = "@style/MyTheme.NoActionBar",
|
||||
ScreenOrientation = Android.Content.PM.ScreenOrientation.Portrait)]
|
||||
@@ -162,7 +162,7 @@ internal sealed class ScannerActivity : Activity, TextureView.ISurfaceTextureLis
|
||||
bmp.GetPixels(pixels, 0, w, 0, 0, w, h);
|
||||
bmp.Recycle();
|
||||
|
||||
// ARGB int[] → RGB byte[] per ZXing.RGBLuminanceSource
|
||||
// ARGB int[] → RGB byte[] for ZXing.RGBLuminanceSource
|
||||
var rgb = new byte[pixels.Length * 3];
|
||||
for (int i = 0; i < pixels.Length; i++)
|
||||
{
|
||||
|
||||
+3
-3
@@ -9,8 +9,8 @@
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
|
||||
<!-- Design token semantici (colori, raggi, accento brand): definiti per
|
||||
entrambe le varianti chiaro/scuro. Vedi Styles/Theme.axaml. -->
|
||||
<!-- Semantic design tokens (colors, radii, brand accent): defined for
|
||||
both light/dark variants. See Styles/Theme.axaml. -->
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<!-- Rifiniture di controllo e classi riutilizzabili (sopra a Fluent) -->
|
||||
<!-- Control refinements and reusable classes (on top of Fluent) -->
|
||||
<StyleInclude Source="/Styles/Controls.axaml"/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@@ -17,8 +17,8 @@ public partial class App : Application
|
||||
{
|
||||
var vm = new MainWindowViewModel();
|
||||
|
||||
// Desktop (Windows/Linux): finestra classica. Mobile (Android): vista
|
||||
// singola. Stessa UI condivisa (MainView) e stesso ViewModel.
|
||||
// Desktop (Windows/Linux): classic window. Mobile (Android): single
|
||||
// view. Same shared UI (MainView) and same ViewModel.
|
||||
switch (ApplicationLifetime)
|
||||
{
|
||||
case IClassicDesktopStyleApplicationLifetime desktop:
|
||||
|
||||
+15
-15
@@ -3,9 +3,9 @@ using System.Collections.Generic;
|
||||
namespace PalladiumWallet.App.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// Localizzazione UI: dizionario chiave → traduzioni per lingua, con
|
||||
/// indicizzatore bindabile da XAML ({Binding Loc[chiave]}). Al cambio lingua
|
||||
/// il ViewModel sostituisce l'istanza così Avalonia rivaluta tutte le binding.
|
||||
/// UI localisation: key → per-language translations dictionary, with an
|
||||
/// indexer bindable from XAML ({Binding Loc[key]}). On language change the
|
||||
/// ViewModel replaces the instance so Avalonia re-evaluates all bindings.
|
||||
/// </summary>
|
||||
public sealed class Loc
|
||||
{
|
||||
@@ -20,10 +20,10 @@ public sealed class Loc
|
||||
private Loc(string language) { Language = language; }
|
||||
|
||||
/// <summary>
|
||||
/// Crea una nuova istanza con la lingua specificata e aggiorna il singleton
|
||||
/// usato da <see cref="Tr"/>. Il ViewModel assegna questa istanza alla
|
||||
/// propria property Loc così Avalonia vede un riferimento diverso e
|
||||
/// rivaluta tutte le binding {Binding Loc[chiave]}.
|
||||
/// Creates a new instance for the specified language and updates the singleton
|
||||
/// used by <see cref="Tr"/>. The ViewModel assigns this instance to its Loc
|
||||
/// property so Avalonia sees a different reference and re-evaluates all
|
||||
/// {Binding Loc[key]} bindings.
|
||||
/// </summary>
|
||||
internal static Loc SwitchTo(string language)
|
||||
{
|
||||
@@ -146,13 +146,13 @@ public sealed class Loc
|
||||
"Fügen Sie einen oder mehrere WIF-Schlüssel ein (einer pro Zeile). Sie können mehrere Schlüssel importieren, um mehrere Adressen mit demselben Wallet zu verwalten."],
|
||||
["wiz.importwif.placeholder"] = ["K… / L… / 5… (una chiave per riga)", "K… / L… / 5… (one key per line)", "K… / L… / 5… (una clave por línea)", "K… / L… / 5… (une clé par ligne)", "K… / L… / 5… (uma chave por linha)", "K… / L… / 5… (ein Schlüssel pro Zeile)"],
|
||||
|
||||
// Messaggi di errore import
|
||||
// Import error messages
|
||||
["msg.xkey.required"] = ["Incolla una chiave estesa (xpub/xprv o variante).", "Paste an extended key (xpub/xprv or variant).", "Pega una clave extendida (xpub/xprv o variante).", "Collez une clé étendue (xpub/xprv ou variante).", "Cole uma chave estendida (xpub/xprv ou variante).", "Fügen Sie einen erweiterten Schlüssel ein (xpub/xprv oder Variante)."],
|
||||
["msg.xkey.invalid"] = ["Chiave estesa non riconosciuta per questa rete.", "Extended key not recognised for this network.", "Clave extendida no reconocida para esta red.", "Clé étendue non reconnue pour ce réseau.", "Chave estendida não reconhecida para esta rede.", "Erweiterter Schlüssel für dieses Netzwerk nicht erkannt."],
|
||||
["msg.wif.required"] = ["Incolla almeno una chiave WIF.", "Paste at least one WIF key.", "Pega al menos una clave WIF.", "Collez au moins une clé WIF.", "Cole pelo menos uma chave WIF.", "Fügen Sie mindestens einen WIF-Schlüssel ein."],
|
||||
["msg.wif.invalid"] = ["Chiave WIF non valida per questa rete.", "WIF key not valid for this network.", "Clave WIF no válida para esta red.", "Clé WIF invalide pour ce réseau.", "Chave WIF inválida para esta rede.", "WIF-Schlüssel für dieses Netzwerk ungültig."],
|
||||
|
||||
// Pannello wallet
|
||||
// Wallet panel
|
||||
["wallet.close"] = ["Chiudi wallet", "Close wallet", "Cerrar wallet", "Fermer le wallet", "Fechar carteira", "Wallet schließen"],
|
||||
["wallet.server"] = ["Server:", "Server:", "Servidor:", "Serveur :", "Servidor:", "Server:"],
|
||||
["wallet.connect"] = ["Connetti e sincronizza", "Connect and sync", "Conectar y sincronizar", "Connecter et synchroniser", "Conectar e sincronizar", "Verbinden und synchronisieren"],
|
||||
@@ -188,7 +188,7 @@ public sealed class Loc
|
||||
["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"],
|
||||
|
||||
// Storico → dettaglio transazione
|
||||
// History → transaction detail
|
||||
["history.hint"] = ["Doppio click su una transazione per i dettagli.", "Double-click a transaction for details.", "Doble clic en una transacción para ver los detalles.", "Double-cliquez sur une transaction pour les détails.", "Clique duplo numa transação para ver os detalhes.", "Doppelklick auf eine Transaktion für Details."],
|
||||
["tx.title"] = ["Dettagli transazione", "Transaction details", "Detalles de la transacción", "Détails de la transaction", "Detalhes da transação", "Transaktionsdetails"],
|
||||
["tx.close"] = ["Chiudi", "Close", "Cerrar", "Fermer", "Fechar", "Schließen"],
|
||||
@@ -231,7 +231,7 @@ public sealed class Loc
|
||||
["send.scan"] = ["Scansiona QR", "Scan QR", "Escanear QR", "Scanner QR", "Escanear QR", "QR scannen"],
|
||||
["send.confirm"] = ["CONFERMA E TRASMETTI", "CONFIRM AND BROADCAST", "CONFIRMAR Y TRANSMITIR", "CONFIRMER ET DIFFUSER", "CONFIRMAR E TRANSMITIR", "BESTÄTIGEN UND SENDEN"],
|
||||
|
||||
// Stato connessione
|
||||
// Connection status
|
||||
["conn.none"] = ["non connesso", "not connected", "no conectado", "non connecté", "não conectado", "nicht verbunden"],
|
||||
["conn.disconnected"] = ["disconnesso", "disconnected", "desconectado", "déconnecté", "desconectado", "getrennt"],
|
||||
["conn.reconnecting"] = ["riconnessione…", "reconnecting…", "reconectando…", "reconnexion…", "reconectando…", "Verbindung wird wiederhergestellt…"],
|
||||
@@ -240,7 +240,7 @@ public sealed class Loc
|
||||
["conn.connectedto"] = ["connesso", "connected", "conectado", "connecté", "conectado", "verbunden"],
|
||||
["conn.connectingto"] = ["connessione a", "connecting to", "conectando a", "connexion à", "conectando a", "Verbindung zu"],
|
||||
|
||||
// Messaggi di stato principali
|
||||
// Main status messages
|
||||
["msg.welcome.existing"] = [
|
||||
"Trovato un wallet esistente su questa rete: aprilo, oppure creane un altro.",
|
||||
"Found an existing wallet on this network: open it, or create another one.",
|
||||
@@ -351,7 +351,7 @@ public sealed class Loc
|
||||
"SSL-Zertifikate zurückgesetzt: Verbindung erneut versuchen."],
|
||||
["msg.error"] = ["Errore", "Error", "Error", "Erreur", "Erro", "Fehler"],
|
||||
|
||||
// Contatti
|
||||
// Contacts
|
||||
["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"],
|
||||
@@ -360,7 +360,7 @@ public sealed class Loc
|
||||
["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 window
|
||||
["settings.title"] = ["Impostazioni", "Settings", "Configuración", "Paramètres", "Configurações", "Einstellungen"],
|
||||
["settings.language"] = ["Lingua", "Language", "Idioma", "Langue", "Idioma", "Sprache"],
|
||||
["settings.unit"] = ["Unità degli importi", "Amount unit", "Unidad de importes", "Unité des montants", "Unidade dos valores", "Betrageinheit"],
|
||||
@@ -368,7 +368,7 @@ public sealed class Loc
|
||||
["settings.cancel"] = ["Annulla", "Cancel", "Cancelar", "Annuler", "Cancelar", "Abbrechen"],
|
||||
["settings.server"] = ["Server di indicizzazione…", "Indexing server…", "Servidor de indexación…", "Serveur d'indexation…", "Servidor de indexação…", "Indexierungsserver…"],
|
||||
|
||||
// Finestra server
|
||||
// Server window
|
||||
["server.title"] = ["Server di indicizzazione", "Indexing server", "Servidor de indexación", "Serveur d'indexation", "Servidor de indexação", "Indexierungsserver"],
|
||||
["server.host"] = ["Host", "Host", "Host", "Hôte", "Host", "Host"],
|
||||
["server.port"] = ["Porta", "Port", "Puerto", "Port", "Porta", "Port"],
|
||||
|
||||
@@ -4,14 +4,14 @@ using System.Threading.Tasks;
|
||||
namespace PalladiumWallet.App.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Seam per servizi specifici della piattaforma (Android/desktop).
|
||||
/// Il head Android imposta i delegate in OnCreate; desktop li lascia null.
|
||||
/// Seam for platform-specific services (Android/desktop).
|
||||
/// The Android head sets the delegates in OnCreate; desktop leaves them null.
|
||||
/// </summary>
|
||||
public static class PlatformServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Apre lo scanner QR nativo e restituisce il testo raw del codice,
|
||||
/// oppure null se l'utente annulla o lo scanner non è disponibile.
|
||||
/// Opens the native QR scanner and returns the raw code text,
|
||||
/// or null if the user cancels or the scanner is unavailable.
|
||||
/// </summary>
|
||||
public static Func<Task<string?>>? ScanQrAsync { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- ============================================================
|
||||
Stili di controllo del wallet Palladium ("fintech pulito").
|
||||
Restano sopra al FluentTheme: rifiniscono spaziatura, stati e
|
||||
classi riutilizzabili senza ritemplare i controlli. I bottoni
|
||||
"accent" restano gestiti da Fluent ma ereditano il blu del brand
|
||||
via SystemAccentColor (vedi Theme.axaml).
|
||||
Palladium wallet control styles ("clean fintech").
|
||||
Applied on top of FluentTheme: refine spacing, states and
|
||||
reusable classes without re-templating controls. The "accent"
|
||||
buttons remain managed by Fluent but inherit the brand blue
|
||||
via SystemAccentColor (see Theme.axaml).
|
||||
============================================================ -->
|
||||
|
||||
<Design.PreviewWith>
|
||||
@@ -22,7 +22,7 @@
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<!-- ===== Tipografia ===== -->
|
||||
<!-- ===== Typography ===== -->
|
||||
<Style Selector="TextBlock.h1">
|
||||
<Setter Property="FontSize" Value="20"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
@@ -36,7 +36,7 @@
|
||||
<Style Selector="TextBlock.muted">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
|
||||
</Style>
|
||||
<!-- Importi e dati numerici: cifre tabulari per evitare salti di layout -->
|
||||
<!-- Amounts and numeric data: tabular figures to prevent layout jumps -->
|
||||
<Style Selector="TextBlock.amount">
|
||||
<Setter Property="FontSize" Value="34"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
@@ -47,7 +47,7 @@
|
||||
<Setter Property="FontFamily" Value="monospace"/>
|
||||
<Setter Property="FontFeatures" Value="+tnum"/>
|
||||
</Style>
|
||||
<!-- Intestazione di sezione (es. dettaglio transazione) -->
|
||||
<!-- Section heading (e.g. transaction detail) -->
|
||||
<Style Selector="TextBlock.section">
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
@@ -55,7 +55,7 @@
|
||||
<Setter Property="LetterSpacing" Value="0.8"/>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Bottoni ===== -->
|
||||
<!-- ===== Buttons ===== -->
|
||||
<Style Selector="Button">
|
||||
<Setter Property="MinHeight" Value="36"/>
|
||||
<Setter Property="Padding" Value="14,8"/>
|
||||
@@ -64,19 +64,19 @@
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
<!-- I bottoni accent (CTA) sono già blu via Fluent+SystemAccentColor.
|
||||
Garantiamo testo bianco leggibile in entrambe le varianti. -->
|
||||
<!-- Accent (CTA) buttons are already blue via Fluent+SystemAccentColor.
|
||||
Ensure readable white text in both light and dark variants. -->
|
||||
<Style Selector="Button.accent">
|
||||
<Setter Property="Foreground" Value="#FFFFFF"/>
|
||||
</Style>
|
||||
<!-- Variante "ghost": azione secondaria discreta, senza riempimento -->
|
||||
<!-- "ghost" variant: subtle secondary action, no fill -->
|
||||
<Style Selector="Button.ghost">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Input ===== -->
|
||||
<!-- ===== Inputs ===== -->
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="MinHeight" Value="36"/>
|
||||
<Setter Property="Padding" Value="10,7"/>
|
||||
@@ -86,7 +86,7 @@
|
||||
<Setter Property="Padding" Value="10,6"/>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Liste ===== -->
|
||||
<!-- ===== Lists ===== -->
|
||||
<Style Selector="ListBox">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
@@ -97,7 +97,7 @@
|
||||
<Setter Property="CornerRadius" Value="8"/>
|
||||
<Setter Property="Margin" Value="0,1"/>
|
||||
</Style>
|
||||
<!-- Hover: tinta neutra; selezione: tinta blu tenue del brand -->
|
||||
<!-- Hover: neutral tint; selected: subtle brand-blue tint -->
|
||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource SurfaceAltBrush}"/>
|
||||
</Style>
|
||||
@@ -105,8 +105,8 @@
|
||||
<Setter Property="Background" Value="{DynamicResource PrimarySoftBrush}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Tab (dashboard) ===== -->
|
||||
<!-- Voce selezionata in blu brand + grassetto; non selezionata smorzata -->
|
||||
<!-- ===== Tabs (dashboard) ===== -->
|
||||
<!-- Selected item: brand blue + bold; unselected: muted -->
|
||||
<Style Selector="TabItem">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
@@ -118,8 +118,8 @@
|
||||
<Setter Property="TextElement.Foreground" Value="{DynamicResource PrimaryBrush}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Classi riutilizzabili ===== -->
|
||||
<!-- Card: superficie elevata con bordo sottile e angoli morbidi -->
|
||||
<!-- ===== Reusable classes ===== -->
|
||||
<!-- Card: elevated surface with a subtle border and soft corners -->
|
||||
<Style Selector="Border.card">
|
||||
<Setter Property="Background" Value="{DynamicResource SurfaceBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtleBrush}"/>
|
||||
@@ -135,7 +135,7 @@
|
||||
<Style Selector="Border.card:pointerover">
|
||||
<Setter Property="BoxShadow" Value="0 6 16 -4 #26000000"/>
|
||||
</Style>
|
||||
<!-- Card del saldo: accento blu sottile, leggermente più marcata -->
|
||||
<!-- Balance card: subtle blue accent, slightly more prominent -->
|
||||
<Style Selector="Border.balance-card">
|
||||
<Setter Property="Background" Value="{DynamicResource SurfaceBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderSubtleBrush}"/>
|
||||
@@ -144,8 +144,8 @@
|
||||
<Setter Property="Padding" Value="20,16"/>
|
||||
<Setter Property="BoxShadow" Value="0 2 8 0 #1A000000"/>
|
||||
</Style>
|
||||
<!-- Card del saldo "hero": gradiente brand + bagliore blu. Elemento
|
||||
distintivo della dashboard; il testo sopra è bianco (classe on-hero). -->
|
||||
<!-- Hero balance card: brand gradient + blue glow. Signature dashboard
|
||||
element; overlaid text is white (on-hero class). -->
|
||||
<Style Selector="Border.balance-hero">
|
||||
<Setter Property="Background" Value="{DynamicResource BalanceGradientBrush}"/>
|
||||
<Setter Property="CornerRadius" Value="16"/>
|
||||
@@ -158,7 +158,7 @@
|
||||
<Style Selector="Border.balance-hero TextBlock.on-hero">
|
||||
<Setter Property="Foreground" Value="#D6E2FF"/>
|
||||
</Style>
|
||||
<!-- Pillola/badge di stato -->
|
||||
<!-- Status pill / badge -->
|
||||
<Style Selector="Border.chip">
|
||||
<Setter Property="CornerRadius" Value="20"/>
|
||||
<Setter Property="Padding" Value="10,5"/>
|
||||
|
||||
+25
-25
@@ -1,34 +1,34 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- ============================================================
|
||||
Design token del wallet Palladium.
|
||||
Identità: blu cobalto (fiducia) + verde (denaro/positivo),
|
||||
derivati dal logo. Stile "fintech pulito".
|
||||
Palladium wallet design tokens.
|
||||
Identity: cobalt blue (trust) + green (money/positive),
|
||||
derived from the logo. "Clean fintech" style.
|
||||
|
||||
Tutti i colori sono SEMANTICI (ruolo, non nome): i componenti
|
||||
non devono mai usare hex inline ma questi brush. Definiti per
|
||||
entrambe le varianti (chiaro/scuro) via ThemeDictionaries, così
|
||||
l'app segue il tema di sistema senza ulteriore configurazione.
|
||||
All colors are SEMANTIC (role, not name): components
|
||||
must never use inline hex but these brushes. Defined for
|
||||
both variants (light/dark) via ThemeDictionaries, so
|
||||
the app follows the system theme with no further configuration.
|
||||
============================================================ -->
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<!-- ============ TEMA CHIARO ============ -->
|
||||
<!-- ============ LIGHT THEME ============ -->
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<!-- Superfici -->
|
||||
<!-- Surfaces -->
|
||||
<SolidColorBrush x:Key="AppBackgroundBrush" Color="#F1F5F9"/> <!-- slate-100 -->
|
||||
<SolidColorBrush x:Key="SurfaceBrush" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="SurfaceAltBrush" Color="#F8FAFC"/> <!-- slate-50 -->
|
||||
<SolidColorBrush x:Key="OverlayCardBrush" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="ScrimBrush" Color="#66000000"/> <!-- 40% -->
|
||||
<!-- Bordi e separatori -->
|
||||
<!-- Borders and separators -->
|
||||
<SolidColorBrush x:Key="BorderSubtleBrush" Color="#E2E8F0"/> <!-- slate-200 -->
|
||||
<SolidColorBrush x:Key="BorderStrongBrush" Color="#CBD5E1"/> <!-- slate-300 -->
|
||||
<!-- Testo -->
|
||||
<!-- Text -->
|
||||
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#0F172A"/> <!-- slate-900 -->
|
||||
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#475569"/> <!-- slate-600 -->
|
||||
<SolidColorBrush x:Key="TextMutedBrush" Color="#94A3B8"/> <!-- slate-400 -->
|
||||
<!-- Brand / stato -->
|
||||
<!-- Brand / state -->
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="#2563EB"/> <!-- blue-600 -->
|
||||
<SolidColorBrush x:Key="PrimaryHoverBrush" Color="#1D4ED8"/> <!-- blue-700 -->
|
||||
<SolidColorBrush x:Key="OnPrimaryBrush" Color="#FFFFFF"/>
|
||||
@@ -36,29 +36,29 @@
|
||||
<SolidColorBrush x:Key="SuccessBrush" Color="#16A34A"/> <!-- green-600 -->
|
||||
<SolidColorBrush x:Key="WarningBrush" Color="#D97706"/> <!-- amber-600 -->
|
||||
<SolidColorBrush x:Key="DangerBrush" Color="#DC2626"/> <!-- red-600 -->
|
||||
<!-- Gradiente hero della card saldo: blu → indaco -->
|
||||
<!-- Hero gradient of the balance card: blue → indigo -->
|
||||
<LinearGradientBrush x:Key="BalanceGradientBrush" StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0" Color="#2563EB"/>
|
||||
<GradientStop Offset="1" Color="#4F46E5"/>
|
||||
</LinearGradientBrush>
|
||||
</ResourceDictionary>
|
||||
|
||||
<!-- ============ TEMA SCURO ============ -->
|
||||
<!-- ============ DARK THEME ============ -->
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<!-- Superfici -->
|
||||
<!-- Surfaces -->
|
||||
<SolidColorBrush x:Key="AppBackgroundBrush" Color="#0F172A"/> <!-- slate-900 -->
|
||||
<SolidColorBrush x:Key="SurfaceBrush" Color="#1E293B"/> <!-- slate-800 -->
|
||||
<SolidColorBrush x:Key="SurfaceAltBrush" Color="#273449"/>
|
||||
<SolidColorBrush x:Key="OverlayCardBrush" Color="#1E293B"/>
|
||||
<SolidColorBrush x:Key="ScrimBrush" Color="#99000000"/> <!-- 60% -->
|
||||
<!-- Bordi e separatori -->
|
||||
<!-- Borders and separators -->
|
||||
<SolidColorBrush x:Key="BorderSubtleBrush" Color="#334155"/> <!-- slate-700 -->
|
||||
<SolidColorBrush x:Key="BorderStrongBrush" Color="#475569"/> <!-- slate-600 -->
|
||||
<!-- Testo -->
|
||||
<!-- Text -->
|
||||
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#F8FAFC"/> <!-- slate-50 -->
|
||||
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#94A3B8"/> <!-- slate-400 -->
|
||||
<SolidColorBrush x:Key="TextMutedBrush" Color="#64748B"/> <!-- slate-500 -->
|
||||
<!-- Brand / stato (toni più luminosi su fondo scuro) -->
|
||||
<!-- Brand / state (brighter tones on dark background) -->
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="#3B82F6"/> <!-- blue-500 -->
|
||||
<SolidColorBrush x:Key="PrimaryHoverBrush" Color="#60A5FA"/> <!-- blue-400 -->
|
||||
<SolidColorBrush x:Key="OnPrimaryBrush" Color="#0B1120"/>
|
||||
@@ -66,7 +66,7 @@
|
||||
<SolidColorBrush x:Key="SuccessBrush" Color="#22C55E"/> <!-- green-500 -->
|
||||
<SolidColorBrush x:Key="WarningBrush" Color="#F59E0B"/> <!-- amber-500 -->
|
||||
<SolidColorBrush x:Key="DangerBrush" Color="#EF4444"/> <!-- red-500 -->
|
||||
<!-- Gradiente hero della card saldo: blu → indaco (toni più profondi) -->
|
||||
<!-- Hero gradient of the balance card: blue → indigo (deeper tones) -->
|
||||
<LinearGradientBrush x:Key="BalanceGradientBrush" StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0" Color="#1D4ED8"/>
|
||||
<GradientStop Offset="1" Color="#4338CA"/>
|
||||
@@ -76,10 +76,10 @@
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<!-- ============================================================
|
||||
Override dell'accento Fluent → blu del brand. Recolora in modo
|
||||
coerente i controlli che leggono SystemAccentColor (bottoni
|
||||
accent, selezioni, indicatore tab, focus) senza ritemplare nulla.
|
||||
Indipendente dalla variante: #2563EB funziona su chiaro e scuro.
|
||||
Override of the Fluent accent → brand blue. Recolors
|
||||
consistently the controls that read SystemAccentColor (accent
|
||||
buttons, selections, tab indicator, focus) without retemplating anything.
|
||||
Variant-independent: #2563EB works on light and dark.
|
||||
============================================================ -->
|
||||
<Color x:Key="SystemAccentColor">#2563EB</Color>
|
||||
<Color x:Key="SystemAccentColorDark1">#1D4ED8</Color>
|
||||
@@ -89,8 +89,8 @@
|
||||
<Color x:Key="SystemAccentColorLight2">#60A5FA</Color>
|
||||
<Color x:Key="SystemAccentColorLight3">#93C5FD</Color>
|
||||
|
||||
<!-- Raggi globali dei controlli Fluent: arrotonda bottoni, input, combo,
|
||||
liste in modo uniforme senza dover ritemplare ogni controllo. -->
|
||||
<!-- Global Fluent control corner radii: rounds buttons, inputs, combos,
|
||||
and lists uniformly without retemplating each control. -->
|
||||
<CornerRadius x:Key="ControlCornerRadius">8</CornerRadius>
|
||||
<CornerRadius x:Key="OverlayCornerRadius">12</CornerRadius>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -6,8 +6,8 @@ using Avalonia.Data.Converters;
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// true (mobile) → Dock.Bottom — tab strip in basso, standard Android.
|
||||
/// false (desktop) → Dock.Top — comportamento predefinito Avalonia.
|
||||
/// true (mobile) → Dock.Bottom — tab strip at the bottom, Android standard.
|
||||
/// false (desktop) → Dock.Top — Avalonia default behavior.
|
||||
/// </summary>
|
||||
public sealed class BoolToTabPlacementConverter : IValueConverter
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ public partial class MainWindowViewModel
|
||||
[ObservableProperty]
|
||||
private ContactEntry? selectedContactInList;
|
||||
|
||||
/// <summary>Contatto selezionato nella ComboBox del pannello Invia: riempie SendTo.</summary>
|
||||
/// <summary>Contact selected in the Send panel's ComboBox: fills SendTo.</summary>
|
||||
[ObservableProperty]
|
||||
private ContactEntry? sendToContact;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel
|
||||
{
|
||||
// ---- saldo e info wallet ----
|
||||
// ---- balance and wallet info ----
|
||||
|
||||
[ObservableProperty]
|
||||
private string balanceText = "—";
|
||||
@@ -28,7 +28,7 @@ public partial class MainWindowViewModel
|
||||
[ObservableProperty]
|
||||
private string networkInfo = "";
|
||||
|
||||
// ---- indirizzo di ricezione e QR ----
|
||||
// ---- receive address and QR ----
|
||||
|
||||
[ObservableProperty]
|
||||
private string receiveAddress = "";
|
||||
@@ -60,7 +60,7 @@ public partial class MainWindowViewModel
|
||||
}
|
||||
}
|
||||
|
||||
// ---- tab indirizzi ----
|
||||
// ---- addresses tab ----
|
||||
|
||||
[ObservableProperty]
|
||||
private AddressRow? selectedAddressRow;
|
||||
@@ -74,7 +74,7 @@ public partial class MainWindowViewModel
|
||||
[RelayCommand]
|
||||
private void CloseAddressInfo() => AddressInfo = null;
|
||||
|
||||
// ---- overlay dettaglio transazione ----
|
||||
// ---- transaction detail overlay ----
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isTxDetailsOpen;
|
||||
@@ -169,7 +169,7 @@ public partial class MainWindowViewModel
|
||||
}
|
||||
}
|
||||
|
||||
// ---- aggiorna display dal risultato della sync ----
|
||||
// ---- update display from the sync result ----
|
||||
|
||||
private void ApplyCache(SyncCache? cache)
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ public partial class MainWindowViewModel
|
||||
if (PlatformServices.ScanQrAsync is not { } scan) return;
|
||||
var raw = await scan();
|
||||
if (string.IsNullOrWhiteSpace(raw)) return;
|
||||
// Gestisce URI tipo "palladium:ADDRESS?amount=X" estraendo solo l'indirizzo
|
||||
// Handle URIs like "palladium:ADDRESS?amount=X" — extract address only
|
||||
var address = raw.Contains(':') ? raw.Split(':')[1] : raw;
|
||||
if (address.Contains('?')) address = address.Split('?')[0];
|
||||
SendTo = address.Trim();
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel
|
||||
{
|
||||
// ---- server e connessione ----
|
||||
// ---- server and connection ----
|
||||
|
||||
[ObservableProperty]
|
||||
private string serverHost = "";
|
||||
@@ -118,20 +118,20 @@ public partial class MainWindowViewModel
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restituisce i server da provare in ordine: prima quello selezionato nella UI
|
||||
/// (o digitato manualmente), poi gli altri in KnownServers, evitando duplicati.
|
||||
/// Returns servers to try in order: first the one selected in the UI
|
||||
/// (or manually typed), then the remaining KnownServers, with duplicates skipped.
|
||||
/// </summary>
|
||||
private IEnumerable<(string Host, int Port)> BuildServerCandidates(string selectedHost, int selectedPort)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
// 1. Server corrente (selezionato o digitato manualmente).
|
||||
// 1. Current server (selected or manually typed).
|
||||
if (!string.IsNullOrWhiteSpace(selectedHost))
|
||||
{
|
||||
var key = $"{selectedHost}:{selectedPort}";
|
||||
if (seen.Add(key))
|
||||
yield return (selectedHost, selectedPort);
|
||||
}
|
||||
// 2. Altri server noti, in ordine di lista.
|
||||
// 2. Other known servers, in list order.
|
||||
foreach (var s in KnownServers)
|
||||
{
|
||||
var p = s.PortFor(UseSsl);
|
||||
@@ -163,14 +163,14 @@ public partial class MainWindowViewModel
|
||||
|
||||
if (_client is null || !_client.IsConnected)
|
||||
{
|
||||
// Salva le cache prima di distruggere il synchronizer: il nuovo
|
||||
// synchronizer userà un client diverso e deve essere ricreato,
|
||||
// ma i dati già scaricati vengono preservati via _doc.Cache.
|
||||
// Persist caches before destroying the synchronizer: the new
|
||||
// synchronizer will use a different client and must be recreated,
|
||||
// but already-downloaded data is preserved via _doc.Cache.
|
||||
PersistPartialTxCache();
|
||||
_synchronizer = null;
|
||||
|
||||
// Prova tutti i server noti in ordine; parte da quello selezionato
|
||||
// e scorre la lista fino al primo che risponde.
|
||||
// Try all known servers in order; starts with the selected one
|
||||
// and walks the list until the first that responds.
|
||||
var candidates = BuildServerCandidates(host, port);
|
||||
Exception? lastError = null;
|
||||
foreach (var (h, p) in candidates)
|
||||
@@ -189,7 +189,7 @@ public partial class MainWindowViewModel
|
||||
IsConnected = true;
|
||||
_autoReconnect = true;
|
||||
ConnectionStatus = Loc.Tr("conn.connectedto");
|
||||
// Aggiorna la UI per riflettere il server effettivamente connesso.
|
||||
// Update the UI to reflect the server actually connected.
|
||||
_syncingServerFields = true;
|
||||
ServerHost = h;
|
||||
ServerPort = p.ToString();
|
||||
@@ -215,8 +215,8 @@ public partial class MainWindowViewModel
|
||||
if (_synchronizer is null)
|
||||
{
|
||||
_synchronizer = new WalletSynchronizer(_account, _client!, _doc.GapLimit);
|
||||
// Ricarica dalla cache su disco: evita di riscaricale le tx già note
|
||||
// (fondamentale per wallet con migliaia di tx storiche — previene -101).
|
||||
// Reload from disk cache: avoids re-downloading already known transactions
|
||||
// (essential for wallets with thousands of historical txs — prevents -101).
|
||||
var net = PalladiumNetworks.For(_account.Profile.Kind);
|
||||
_synchronizer.PreloadCaches(
|
||||
_doc.Cache?.RawTxHex ?? [],
|
||||
@@ -271,8 +271,8 @@ public partial class MainWindowViewModel
|
||||
if (_account is not null)
|
||||
{
|
||||
_syncFailed = true;
|
||||
// Salva le tx già scaricate: al retry il synchronizer riparte
|
||||
// da qui invece di ricominciare da zero (es. dopo -101).
|
||||
// Persist already-downloaded transactions: on retry the synchronizer
|
||||
// resumes from here instead of starting from scratch (e.g. after -101).
|
||||
PersistPartialTxCache();
|
||||
}
|
||||
}
|
||||
@@ -327,9 +327,9 @@ public partial class MainWindowViewModel
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Salva nel SyncCache le tx e le prove di Merkle già accumulate dal synchronizer,
|
||||
/// anche se la sync non è ancora completa. Consente al retry successivo
|
||||
/// (o al riavvio dell'app) di riprendere senza riscaricale da zero.
|
||||
/// Persists the transactions and Merkle proofs already accumulated by the synchronizer
|
||||
/// into SyncCache, even if sync is not yet complete. Allows the next retry
|
||||
/// (or app restart) to resume without re-downloading from scratch.
|
||||
/// </summary>
|
||||
private void PersistPartialTxCache()
|
||||
{
|
||||
@@ -346,7 +346,7 @@ public partial class MainWindowViewModel
|
||||
_doc.Cache.BlockHeaders = blockHeaders;
|
||||
WalletStore.Save(_doc, _walletPath, _password);
|
||||
}
|
||||
catch { /* non fatale: il prossimo salvataggio completo recupererà */ }
|
||||
catch { /* non-fatal: the next full save will recover */ }
|
||||
}
|
||||
|
||||
private async Task DisconnectAsync()
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel
|
||||
{
|
||||
// ---- wizard di setup
|
||||
// ---- setup wizard
|
||||
|
||||
public const string StepDataLocation = "data-location";
|
||||
public const string StepStart = "start";
|
||||
@@ -85,7 +85,7 @@ public partial class MainWindowViewModel
|
||||
[ObservableProperty]
|
||||
private string importWifInput = "";
|
||||
|
||||
// Tipo di script rilevato durante la decodifica dell'xkey (per mostrarlo all'utente)
|
||||
// Script type detected during xkey decoding (to display to the user)
|
||||
[ObservableProperty]
|
||||
private string importXkeyDetectedKind = "";
|
||||
|
||||
@@ -268,7 +268,7 @@ public partial class MainWindowViewModel
|
||||
StatusMessage = Loc.Tr("msg.xkey.required");
|
||||
return;
|
||||
}
|
||||
// Valida e rileva il tipo: prova prima come xpub, poi come xprv.
|
||||
// Validate and detect the type: try as xpub first, then as xprv.
|
||||
if (Slip132.TryDecodePublic(ImportXkeyInput.Trim(), Profile, out _, out var pubKind))
|
||||
{
|
||||
SelectedScriptKind = pubKind;
|
||||
@@ -298,7 +298,7 @@ public partial class MainWindowViewModel
|
||||
StatusMessage = Loc.Tr("msg.wif.required");
|
||||
return;
|
||||
}
|
||||
// Valida il WIF con un parsing anticipato.
|
||||
// Validate the WIF with an early parse.
|
||||
try
|
||||
{
|
||||
_ = new NBitcoin.BitcoinSecret(ImportWifInput.Trim(), PalladiumNetworks.For(Net));
|
||||
@@ -421,9 +421,9 @@ public partial class MainWindowViewModel
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Costruisce il percorso file dal nome inserito dall'utente.
|
||||
/// Se il nome è vuoto genera un nome automatico (default, wallet-2, …).
|
||||
/// Rimuove i caratteri non validi per il filesystem.
|
||||
/// Builds the file path from the name entered by the user.
|
||||
/// If the name is empty, generates an automatic name (default, wallet-2, …).
|
||||
/// Removes characters that are invalid for the filesystem.
|
||||
/// </summary>
|
||||
private string WalletPathFromName(string name)
|
||||
{
|
||||
@@ -435,7 +435,7 @@ public partial class MainWindowViewModel
|
||||
|
||||
if (string.IsNullOrEmpty(clean))
|
||||
{
|
||||
// Nessun nome → nome automatico
|
||||
// No name provided → auto-generated name
|
||||
var def = AppPaths.DefaultWalletPath(Net);
|
||||
for (var n = 2; WalletStore.Exists(def); n++)
|
||||
def = Path.Combine(AppPaths.WalletsDir(Net), $"wallet-{n}.wallet.json");
|
||||
@@ -547,8 +547,8 @@ public partial class MainWindowViewModel
|
||||
ApplyCache(doc.Cache);
|
||||
IsWalletOpen = true;
|
||||
StatusMessage = Loc.Tr("msg.opened");
|
||||
_autoReconnect = true; // keepalive riprova la connessione anche se il primo tentativo fallisce
|
||||
_syncFailed = true; // forza la prima sync automatica
|
||||
_autoReconnect = true; // keepalive retries the connection even if the first attempt fails
|
||||
_syncFailed = true; // force the first automatic sync
|
||||
_ = ConnectAndSync();
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,10 @@ using PalladiumWallet.Core.Wallet;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
/// <summary>Riga dello storico transazioni per la vista.</summary>
|
||||
/// <summary>Transaction history row for the view.</summary>
|
||||
public sealed record HistoryRow(string Conferma, string Importo, string Txid, string Verificata);
|
||||
|
||||
/// <summary>Riga della vista indirizzi con chiavi e derivation path pre-calcolati.</summary>
|
||||
/// <summary>Address view row with pre-computed keys and derivation path.</summary>
|
||||
public sealed record AddressRow(
|
||||
string Tipo, int Indice, string Indirizzo, string Saldo, string NumTx,
|
||||
bool IsChange = false, string PubKey = "", string PrivKey = "", string DerivPath = "")
|
||||
@@ -27,7 +27,7 @@ public sealed record AddressRow(
|
||||
public bool HasPrivKey => !string.IsNullOrEmpty(PrivKey);
|
||||
}
|
||||
|
||||
/// <summary>Dati completi di un indirizzo passati alla finestra di dettaglio.</summary>
|
||||
/// <summary>Full address data passed to the address detail overlay.</summary>
|
||||
public sealed record AddressInfo(
|
||||
Loc Loc,
|
||||
string Address, string DerivPath, string PubKey, string PrivKey)
|
||||
@@ -35,38 +35,38 @@ public sealed record AddressInfo(
|
||||
public bool HasPrivKey => !string.IsNullOrEmpty(PrivKey);
|
||||
}
|
||||
|
||||
/// <summary>Contatto in rubrica: nome + indirizzo blockchain.</summary>
|
||||
/// <summary>Address book contact: name + blockchain address.</summary>
|
||||
public sealed record ContactEntry(string Name, string Address);
|
||||
|
||||
/// <summary>Voce della lista di scelta wallet: nome file + percorso completo.</summary>
|
||||
/// <summary>Wallet list entry: display name + full file path.</summary>
|
||||
public sealed record WalletFileEntry(string Name, string Path);
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel unico dell'applicazione (wizard + dashboard). Suddiviso in file
|
||||
/// partial per area: Wizard, Settings, Sync, Send, Contacts, Receive.
|
||||
/// Single application ViewModel (wizard + dashboard). Split into partial files
|
||||
/// by area: Wizard, Settings, Sync, Send, Contacts, Receive.
|
||||
/// </summary>
|
||||
public partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
// ---- stato sessione wallet ----
|
||||
// ---- wallet session state ----
|
||||
private WalletDocument? _doc;
|
||||
private IWalletAccount? _account;
|
||||
private string? _walletPath;
|
||||
private string? _password;
|
||||
private WalletLock? _walletLock;
|
||||
|
||||
// ---- rete ----
|
||||
// ---- network ----
|
||||
private ElectrumClient? _client;
|
||||
private WalletSynchronizer? _synchronizer;
|
||||
private IReadOnlyDictionary<string, Transaction>? _lastTransactions;
|
||||
private bool _resyncRequested;
|
||||
|
||||
// ---- invio ----
|
||||
// ---- send ----
|
||||
private BuiltTransaction? _pendingSend;
|
||||
|
||||
// ---- dettaglio transazione ----
|
||||
// ---- transaction detail ----
|
||||
private CancellationTokenSource? _txDetailsCts;
|
||||
|
||||
// ---- configurazione e localizzazione ----
|
||||
// ---- configuration and localisation ----
|
||||
private AppConfig _config = AppConfig.Load();
|
||||
private Loc _loc = Loc.Instance;
|
||||
public Loc Loc => _loc;
|
||||
@@ -82,7 +82,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
// ---- server UI sync ----
|
||||
private bool _syncingServerFields;
|
||||
|
||||
// ---- proprietà di app ----
|
||||
// ---- app properties ----
|
||||
|
||||
public static string AppVersion =>
|
||||
typeof(MainWindowViewModel).Assembly.GetName().Version is { } v
|
||||
@@ -91,7 +91,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
public string WindowTitle => $"Palladium Wallet {AppVersion}";
|
||||
|
||||
/// <summary>true su desktop; false su Android/iOS — nasconde le funzioni legate al filesystem libero.</summary>
|
||||
/// <summary>True on desktop; false on Android/iOS — hides filesystem-dependent features.</summary>
|
||||
public bool IsDesktop => !OperatingSystem.IsAndroid() && !OperatingSystem.IsIOS();
|
||||
|
||||
public bool IsMobile => !IsDesktop;
|
||||
@@ -99,7 +99,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
public string UnitLabel => _config.Unit;
|
||||
public AppConfig CurrentConfig => _config;
|
||||
|
||||
// ---- stato pannelli ----
|
||||
// ---- panel state ----
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsSetupVisible))]
|
||||
@@ -116,7 +116,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void SelectTab(string index) => SelectedTabIndex = int.Parse(index);
|
||||
|
||||
// ---- collections per la dashboard ----
|
||||
// ---- dashboard collections ----
|
||||
|
||||
public ObservableCollection<HistoryRow> History { get; } = [];
|
||||
public ObservableCollection<AddressRow> Addresses { get; } = [];
|
||||
@@ -141,7 +141,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
catch { return ""; }
|
||||
}
|
||||
|
||||
// ---- costruttore ----
|
||||
// ---- constructor ----
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
@@ -161,7 +161,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
return;
|
||||
if (_client is { IsConnected: true })
|
||||
{
|
||||
// Se il wallet è aperto e l'ultima sync è fallita, riprova automaticamente.
|
||||
// If the wallet is open and the last sync failed, retry automatically.
|
||||
if (_syncFailed && _account is not null)
|
||||
{
|
||||
await ConnectAndSync();
|
||||
@@ -177,7 +177,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
// ---- ciclo di vita wallet ----
|
||||
// ---- wallet lifecycle ----
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseWallet()
|
||||
|
||||
@@ -7,8 +7,8 @@ using Avalonia.Media;
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// bool → pennello: gli indirizzi del wallet (input/output "nostri") sono
|
||||
/// evidenziati in verde, gli altri usano il colore di testo predefinito.
|
||||
/// bool → brush: the wallet's own addresses ("our" inputs/outputs) are
|
||||
/// highlighted in green, the others use the default text color.
|
||||
/// </summary>
|
||||
public sealed class MineColorConverter : IValueConverter
|
||||
{
|
||||
|
||||
@@ -7,13 +7,13 @@ using PalladiumWallet.Core.Wallet;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
/// <summary>Riga input/output per le tabelle della finestra di dettaglio.</summary>
|
||||
/// <summary>Input/output row for the transaction detail tables.</summary>
|
||||
public sealed record TxIoRow(string Position, string Address, string Amount, bool IsMine);
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel della finestra di dettaglio transazione: prende un
|
||||
/// <see cref="TransactionDetails"/> (assemblato dal server) e ne ricava tutte
|
||||
/// le stringhe già formattate e localizzate per la vista. È di sola lettura.
|
||||
/// ViewModel for the transaction detail view: takes a
|
||||
/// <see cref="TransactionDetails"/> (assembled from server data) and produces
|
||||
/// all strings already formatted and localised for the view. Read-only.
|
||||
/// </summary>
|
||||
public sealed class TransactionDetailsViewModel
|
||||
{
|
||||
@@ -35,14 +35,14 @@ public sealed class TransactionDetailsViewModel
|
||||
|
||||
var counterparties = d.CounterpartyAddresses;
|
||||
CounterpartyHeader = d.IsIncoming ? loc["tx.from"] : loc["tx.to"];
|
||||
// Una coinbase non ha mittente: le monete sono generate dal mining.
|
||||
// Coinbase has no sender: coins are newly generated by mining.
|
||||
CounterpartyText = d.IsCoinbase
|
||||
? loc["tx.coinbase.newcoins"]
|
||||
: counterparties.Count > 0
|
||||
? string.Join(Environment.NewLine, counterparties)
|
||||
: "—";
|
||||
|
||||
// Debito (uscita verso terzi) o Credito (entrata netta sui nostri output).
|
||||
// Debit (outflow to third parties) or Credit (net inflow on our outputs).
|
||||
AmountHeader = d.IsIncoming ? loc["tx.credit"] : loc["tx.debit"];
|
||||
AmountText = d.IsIncoming
|
||||
? Signed(d.ReceivedSats)
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
</Design.DataContext>
|
||||
|
||||
<UserControl.Styles>
|
||||
<!-- Su desktop gli overlay card sono centrati con dimensione massima;
|
||||
su mobile il default (stretch, nessun margine) occupa tutta la schermata. -->
|
||||
<!-- On desktop, overlay cards are centred with a max size;
|
||||
on mobile the default (stretch, no margin) fills the whole screen. -->
|
||||
<Style Selector="Border.desktop-overlay">
|
||||
<Setter Property="MaxWidth" Value="540"/>
|
||||
<Setter Property="MaxHeight" Value="540"/>
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto" Background="{DynamicResource AppBackgroundBrush}">
|
||||
|
||||
<!-- ============ MENU (stile Electrum) ============ -->
|
||||
<!-- ============ MENU (Electrum style) ============ -->
|
||||
<Menu Grid.Row="0" IsVisible="{Binding IsDesktop}">
|
||||
<MenuItem Header="{Binding Loc[menu.file]}">
|
||||
<MenuItem Header="{Binding Loc[menu.file.new]}" Command="{Binding NewWalletCommand}"/>
|
||||
@@ -43,7 +43,7 @@
|
||||
Command="{Binding OpenHelpCommand}"/>
|
||||
</Menu>
|
||||
|
||||
<!-- Pulsanti Settings/Help mobile-only (il menu è nascosto su mobile) -->
|
||||
<!-- Settings/Help buttons — mobile only (the menu is hidden on mobile) -->
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8"
|
||||
HorizontalAlignment="Right" Margin="8,4"
|
||||
IsVisible="{Binding IsMobile}">
|
||||
@@ -51,14 +51,14 @@
|
||||
<Button Content="{Binding Loc[menu.help]}" Command="{Binding OpenHelpCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ WIZARD DI SETUP (§15): un passo alla volta ============ -->
|
||||
<!-- ============ SETUP WIZARD (§15): one step at a time ============ -->
|
||||
<ScrollViewer Grid.Row="1" IsVisible="{Binding IsSetupVisible}">
|
||||
<StackPanel MaxWidth="560" Margin="24,40" Spacing="18"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock Text="Palladium Wallet" FontSize="28" FontWeight="Bold"
|
||||
HorizontalAlignment="Center"/>
|
||||
|
||||
<!-- Passo 0 (primo avvio): dove salvare i dati -->
|
||||
<!-- Step 0 (first launch): where to store data -->
|
||||
<StackPanel IsVisible="{Binding IsStepDataLocation}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.data.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding Loc[wiz.data.info]}" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
@@ -75,7 +75,7 @@
|
||||
Click="OnChooseDataFolderClick"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo 1: scelta iniziale -->
|
||||
<!-- Step 1: initial choice -->
|
||||
<StackPanel IsVisible="{Binding IsStepStart}" Spacing="12">
|
||||
<Button Content="{Binding Loc[wiz.open.btn]}" FontSize="16"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
|
||||
@@ -95,7 +95,7 @@
|
||||
Command="{Binding WizardStartImportWifCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo: scelta del wallet (più file presenti) -->
|
||||
<!-- Step: choose wallet (multiple files present) -->
|
||||
<StackPanel IsVisible="{Binding IsStepChooseWallet}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.choose.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<ItemsControl ItemsSource="{Binding WalletList}">
|
||||
@@ -112,7 +112,7 @@
|
||||
<Button Content="{Binding Loc[wiz.back]}" Command="{Binding WizardBackCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo: password del wallet esistente -->
|
||||
<!-- Step: password for existing wallet -->
|
||||
<StackPanel IsVisible="{Binding IsStepOpen}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.open.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<TextBox PlaceholderText="{Binding Loc[wiz.open.placeholder]}"
|
||||
@@ -124,7 +124,7 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo: mostra il nuovo seed -->
|
||||
<!-- Step: show the new seed -->
|
||||
<StackPanel IsVisible="{Binding IsStepShowSeed}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.seed.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<Border BorderBrush="{DynamicResource SystemAccentColor}" BorderThickness="1"
|
||||
@@ -141,7 +141,7 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo: conferma del seed -->
|
||||
<!-- Step: seed confirmation -->
|
||||
<StackPanel IsVisible="{Binding IsStepConfirmSeed}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.confirm.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<TextBox PlaceholderText="{Binding Loc[wiz.confirm.placeholder]}"
|
||||
@@ -153,7 +153,7 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo: inserimento seed (ripristino) -->
|
||||
<!-- Step: enter seed words (restore) -->
|
||||
<StackPanel IsVisible="{Binding IsStepWords}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.words.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<TextBox PlaceholderText="{Binding Loc[wiz.words.placeholder]}"
|
||||
@@ -165,7 +165,7 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo: passphrase opzionale -->
|
||||
<!-- Step: optional passphrase -->
|
||||
<StackPanel IsVisible="{Binding IsStepPassphrase}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.passphrase.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<TextBox PlaceholderText="{Binding Loc[wiz.passphrase.placeholder]}"
|
||||
@@ -177,7 +177,7 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo: import xpub/xprv estesa (SLIP-132) -->
|
||||
<!-- Step: import extended xpub/xprv (SLIP-132) -->
|
||||
<StackPanel IsVisible="{Binding IsStepImportXkey}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.importxkey.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding Loc[wiz.importxkey.hint]}" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
@@ -193,7 +193,7 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo: import chiave WIF singola -->
|
||||
<!-- Step: import single WIF key -->
|
||||
<StackPanel IsVisible="{Binding IsStepImportWif}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.importwif.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding Loc[wiz.importwif.hint]}" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
@@ -206,7 +206,7 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo: scelta del tipo di script/indirizzi -->
|
||||
<!-- Step: choose script/address type -->
|
||||
<StackPanel IsVisible="{Binding IsStepScriptType}" Spacing="12">
|
||||
<TextBlock Text="{Binding Loc[wiz.scripttype.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
<TextBlock Text="{Binding Loc[wiz.scripttype.hint]}" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
@@ -254,11 +254,11 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passo finale: nome + password del file (cifratura, stile Electrum) -->
|
||||
<!-- Final step: wallet name + file password (encryption, Electrum style) -->
|
||||
<StackPanel IsVisible="{Binding IsStepPassword}" Spacing="16">
|
||||
<TextBlock Text="{Binding Loc[wiz.password.title]}" FontSize="18" FontWeight="Bold"/>
|
||||
|
||||
<!-- Sezione nome: bordo distinto dal gruppo password -->
|
||||
<!-- Name section: visually distinct from the password group -->
|
||||
<Border BorderBrush="{DynamicResource SystemAccentColor}"
|
||||
BorderThickness="0,0,0,0"
|
||||
Background="{DynamicResource SurfaceAltBrush}"
|
||||
@@ -271,10 +271,10 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Separatore visivo -->
|
||||
<!-- Visual separator -->
|
||||
<Separator Margin="0,0"/>
|
||||
|
||||
<!-- Sezione cifratura -->
|
||||
<!-- Encryption section -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBox PlaceholderText="{Binding Loc[wiz.password.placeholder]}"
|
||||
PasswordChar="●" Text="{Binding PasswordInput}"/>
|
||||
@@ -296,11 +296,11 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ============ PANNELLO WALLET ============ -->
|
||||
<!-- ============ WALLET PANEL ============ -->
|
||||
<Grid Grid.Row="1" RowDefinitions="Auto,*" IsVisible="{Binding IsWalletOpen}" Margin="16">
|
||||
|
||||
<!-- Testata: card del saldo. Connetti/sincronizza è automatico; chiudi
|
||||
wallet è in File. Lo stato connessione è nella barra in basso. -->
|
||||
<!-- Header: balance card. Connect/sync is automatic; close wallet is in File.
|
||||
Connection status is shown in the bottom bar. -->
|
||||
<Border Grid.Row="0" Classes="balance-hero" Margin="0,0,0,16">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding Loc[addr.balance]}" Classes="label on-hero"/>
|
||||
@@ -313,7 +313,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Tab: Storico / Invia / Ricevi / Indirizzi / Contatti -->
|
||||
<!-- Tabs: History / Send / Receive / Addresses / Contacts -->
|
||||
<TabControl Grid.Row="1"
|
||||
SelectedIndex="{Binding SelectedTabIndex}"
|
||||
TabStripPlacement="Top">
|
||||
@@ -336,7 +336,7 @@
|
||||
</Style>
|
||||
</TabControl.Styles>
|
||||
|
||||
<!-- 1. Storico -->
|
||||
<!-- 1. History -->
|
||||
<TabItem>
|
||||
<TabItem.Header>
|
||||
<StackPanel Spacing="2" HorizontalAlignment="Center">
|
||||
@@ -359,7 +359,7 @@
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:HistoryRow">
|
||||
<Panel Cursor="Hand">
|
||||
<!-- Desktop: 4 colonne fisse -->
|
||||
<!-- Desktop: 4 fixed columns -->
|
||||
<Grid ColumnDefinitions="90,160,*,70"
|
||||
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsDesktop}">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Conferma}" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
@@ -369,7 +369,7 @@
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding Verificata}" Foreground="{DynamicResource SuccessBrush}"/>
|
||||
</Grid>
|
||||
<!-- Mobile: card verticale -->
|
||||
<!-- Mobile: vertical card -->
|
||||
<StackPanel Spacing="2"
|
||||
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsMobile}">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
@@ -389,7 +389,7 @@
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- 2. Invia -->
|
||||
<!-- 2. Send -->
|
||||
<TabItem>
|
||||
<TabItem.Header>
|
||||
<StackPanel Spacing="2" HorizontalAlignment="Center">
|
||||
@@ -404,7 +404,7 @@
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
<StackPanel Spacing="10" Margin="8" MaxWidth="640" HorizontalAlignment="Left">
|
||||
<!-- Contatto rapido -->
|
||||
<!-- Quick contact -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="{Binding Loc[send.from.contact]}" VerticalAlignment="Center"/>
|
||||
<ComboBox ItemsSource="{Binding Contacts}"
|
||||
@@ -433,7 +433,7 @@
|
||||
</Viewbox>
|
||||
</Button>
|
||||
</Grid>
|
||||
<!-- Desktop: riga amount + fee in 4 colonne -->
|
||||
<!-- Desktop: amount + fee row in 4 columns -->
|
||||
<Grid ColumnDefinitions="*,Auto,Auto,Auto" IsVisible="{Binding IsDesktop}">
|
||||
<TextBox Grid.Column="0" PlaceholderText="{Binding Loc[send.amount]}"
|
||||
Text="{Binding SendAmount}" IsEnabled="{Binding !SendAll}"/>
|
||||
@@ -446,7 +446,7 @@
|
||||
<TextBox Text="{Binding SendFeeRate}" MinWidth="60"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<!-- Mobile: amount e fee su righe separate -->
|
||||
<!-- Mobile: amount and fee on separate rows -->
|
||||
<StackPanel Spacing="6" IsVisible="{Binding IsMobile}">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBox Grid.Column="0" PlaceholderText="{Binding Loc[send.amount]}"
|
||||
@@ -472,7 +472,7 @@
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
|
||||
<!-- 3. Ricevi -->
|
||||
<!-- 3. Receive -->
|
||||
<TabItem>
|
||||
<TabItem.Header>
|
||||
<StackPanel Spacing="2" HorizontalAlignment="Center">
|
||||
@@ -488,7 +488,7 @@
|
||||
</TabItem.Header>
|
||||
<StackPanel Spacing="10" Margin="8">
|
||||
<TextBlock Text="{Binding Loc[receive.next]}"/>
|
||||
<!-- Desktop: indirizzo + copia su una riga -->
|
||||
<!-- Desktop: address + copy on one row -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" IsVisible="{Binding IsDesktop}">
|
||||
<SelectableTextBlock Text="{Binding ReceiveAddress}"
|
||||
FontFamily="monospace" FontSize="16"
|
||||
@@ -497,7 +497,7 @@
|
||||
Click="OnCopyReceiveAddressClick"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<!-- Mobile: indirizzo su riga intera + copia sotto -->
|
||||
<!-- Mobile: address on full row + copy below -->
|
||||
<StackPanel Spacing="6" IsVisible="{Binding IsMobile}">
|
||||
<SelectableTextBlock Text="{Binding ReceiveAddress}"
|
||||
FontFamily="monospace" FontSize="13"
|
||||
@@ -518,7 +518,7 @@
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
|
||||
<!-- 4. Indirizzi -->
|
||||
<!-- 4. Addresses -->
|
||||
<TabItem>
|
||||
<TabItem.Header>
|
||||
<StackPanel Spacing="2" HorizontalAlignment="Center">
|
||||
@@ -533,7 +533,7 @@
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
<Grid RowDefinitions="Auto,*" Margin="4">
|
||||
<!-- Intestazione colonne — solo desktop -->
|
||||
<!-- Column header — desktop only -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="90,60,*,140,60" Margin="12,4"
|
||||
IsVisible="{Binding IsDesktop}">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Loc[addr.type]}" FontWeight="Bold"/>
|
||||
@@ -550,7 +550,7 @@
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AddressRow">
|
||||
<Panel>
|
||||
<!-- Desktop: 5 colonne fisse -->
|
||||
<!-- Desktop: 5 fixed columns -->
|
||||
<Grid ColumnDefinitions="90,60,*,140,60"
|
||||
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsDesktop}">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Tipo}" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
@@ -560,7 +560,7 @@
|
||||
<TextBlock Grid.Column="3" Text="{Binding Saldo}" FontFamily="monospace"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding NumTx}" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
</Grid>
|
||||
<!-- Mobile: card verticale -->
|
||||
<!-- Mobile: vertical card -->
|
||||
<StackPanel Spacing="1" Margin="0,3"
|
||||
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsMobile}">
|
||||
<TextBlock Text="{Binding Indirizzo}"
|
||||
@@ -580,7 +580,7 @@
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- 5. Contatti -->
|
||||
<!-- 5. Contacts -->
|
||||
<TabItem>
|
||||
<TabItem.Header>
|
||||
<StackPanel Spacing="2" HorizontalAlignment="Center">
|
||||
@@ -595,19 +595,19 @@
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
<Grid RowDefinitions="Auto,*,Auto,Auto" Margin="4">
|
||||
<!-- Intestazioni colonne — solo desktop -->
|
||||
<!-- Column headers — desktop only -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="180,*" Margin="12,4"
|
||||
IsVisible="{Binding IsDesktop}">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Loc[contacts.name]}" FontWeight="Bold"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Loc[contacts.address]}" FontWeight="Bold"/>
|
||||
</Grid>
|
||||
<!-- Lista contatti -->
|
||||
<!-- Contacts list -->
|
||||
<ListBox Grid.Row="1" ItemsSource="{Binding Contacts}"
|
||||
SelectedItem="{Binding SelectedContactInList}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ContactEntry">
|
||||
<Panel>
|
||||
<!-- Desktop: 2 colonne -->
|
||||
<!-- Desktop: 2 columns -->
|
||||
<Grid ColumnDefinitions="180,*"
|
||||
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsDesktop}">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Name}"
|
||||
@@ -616,7 +616,7 @@
|
||||
FontFamily="monospace" FontSize="12"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<!-- Mobile: card verticale -->
|
||||
<!-- Mobile: vertical card -->
|
||||
<StackPanel Spacing="1" Margin="0,3"
|
||||
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsMobile}">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" FontSize="13"
|
||||
@@ -629,12 +629,12 @@
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<!-- Pulsante rimuovi selezionato -->
|
||||
<!-- Remove selected button -->
|
||||
<Button Grid.Row="2" Content="{Binding Loc[contacts.remove]}"
|
||||
Command="{Binding RemoveSelectedContactCommand}"
|
||||
IsEnabled="{Binding SelectedContactInList, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
Margin="0,6,0,0"/>
|
||||
<!-- Form aggiungi contatto — Desktop: 3 colonne -->
|
||||
<!-- Add contact form — Desktop: 3 columns -->
|
||||
<Grid Grid.Row="3" ColumnDefinitions="180,*,Auto" Margin="0,8,0,0"
|
||||
IsVisible="{Binding IsDesktop}">
|
||||
<TextBox Grid.Column="0" PlaceholderText="{Binding Loc[contacts.name.ph]}"
|
||||
@@ -645,7 +645,7 @@
|
||||
<Button Grid.Column="2" Content="{Binding Loc[contacts.add]}"
|
||||
Command="{Binding AddContactCommand}"/>
|
||||
</Grid>
|
||||
<!-- Form aggiungi contatto — Mobile: verticale -->
|
||||
<!-- Add contact form — Mobile: vertical -->
|
||||
<StackPanel Grid.Row="3" Spacing="6" Margin="0,8,0,0"
|
||||
IsVisible="{Binding IsMobile}">
|
||||
<TextBox PlaceholderText="{Binding Loc[contacts.name.ph]}"
|
||||
@@ -664,8 +664,8 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Barra di stato: messaggio a sinistra, stato connessione a destra.
|
||||
Lo stato connessione è cliccabile e apre le impostazioni del server. -->
|
||||
<!-- Status bar: message on the left, connection status on the right.
|
||||
The connection status is tappable and opens the server settings. -->
|
||||
<Border Grid.Row="2" Background="{DynamicResource SurfaceBrush}"
|
||||
BorderBrush="{DynamicResource BorderSubtleBrush}" BorderThickness="0,1,0,0"
|
||||
Padding="12,7">
|
||||
@@ -688,9 +688,9 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ============ OVERLAY DETTAGLIO INDIRIZZO ============ -->
|
||||
<!-- Overlay in-app invece di una Window separata: apertura/chiusura
|
||||
istantanee (niente create/destroy di una top-level window). -->
|
||||
<!-- ============ ADDRESS DETAIL OVERLAY ============ -->
|
||||
<!-- In-app overlay instead of a separate Window: instant open/close
|
||||
(no top-level window create/destroy). -->
|
||||
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||
Background="{DynamicResource ScrimBrush}"
|
||||
Tapped="OnAddressOverlayBackdropTapped"
|
||||
@@ -705,7 +705,7 @@
|
||||
<TextBlock Text="{Binding Loc[addr.info.title]}"
|
||||
FontSize="18" FontWeight="Bold"/>
|
||||
|
||||
<!-- Indirizzo -->
|
||||
<!-- Address -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding Loc[addr.address]}" FontSize="11" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
<SelectableTextBlock Text="{Binding Address}"
|
||||
@@ -720,7 +720,7 @@
|
||||
FontFamily="monospace" FontSize="13"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Chiave pubblica -->
|
||||
<!-- Public key -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding Loc[addr.pubkey]}" FontSize="11" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
<SelectableTextBlock Text="{Binding PubKey}"
|
||||
@@ -728,7 +728,7 @@
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Chiave privata (solo se disponibile) -->
|
||||
<!-- Private key (only when available) -->
|
||||
<StackPanel Spacing="4" IsVisible="{Binding HasPrivKey}">
|
||||
<TextBlock Text="{Binding Loc[addr.privkey]}" FontSize="11" Foreground="{DynamicResource DangerBrush}"/>
|
||||
<SelectableTextBlock Text="{Binding PrivKey}"
|
||||
@@ -745,9 +745,9 @@
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<!-- ============ OVERLAY DETTAGLIO TRANSAZIONE ============ -->
|
||||
<!-- Overlay in-app (come indirizzo): appare subito con lo spinner, i dati
|
||||
arrivano dal server in background; chiusura istantanea. -->
|
||||
<!-- ============ TRANSACTION DETAIL OVERLAY ============ -->
|
||||
<!-- In-app overlay (like the address overlay): appears immediately with a
|
||||
spinner; data arrives from the server in the background; instant close. -->
|
||||
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||
Background="{DynamicResource ScrimBrush}"
|
||||
Tapped="OnTxDetailsOverlayBackdropTapped"
|
||||
@@ -758,7 +758,7 @@
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Panel Margin="24">
|
||||
|
||||
<!-- Caricamento -->
|
||||
<!-- Loading -->
|
||||
<StackPanel IsVisible="{Binding IsTxDetailsLoading}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="14">
|
||||
<ProgressBar IsIndeterminate="True" Width="220"/>
|
||||
@@ -766,8 +766,8 @@
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Contenuto (IsVisible sul contenitore esterno, così resta
|
||||
legato al VM principale; il DataContext interno è TxDetails) -->
|
||||
<!-- Content (IsVisible on the outer container so it stays bound
|
||||
to the main VM; the inner DataContext is TxDetails) -->
|
||||
<Grid IsVisible="{Binding !IsTxDetailsLoading}">
|
||||
<DockPanel DataContext="{Binding TxDetails}">
|
||||
<TextBlock DockPanel.Dock="Top" Text="{Binding Loc[tx.title]}"
|
||||
@@ -780,14 +780,14 @@
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="18">
|
||||
|
||||
<!-- Header: importo netto in evidenza + badge stato/SPV -->
|
||||
<!-- Header: net amount highlighted + status/SPV badges -->
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="{Binding AmountHeader}" Classes="label"/>
|
||||
<SelectableTextBlock Text="{Binding NetText}"
|
||||
FontFamily="monospace" FontFeatures="+tnum"
|
||||
FontSize="28" FontWeight="Bold"/>
|
||||
<WrapPanel>
|
||||
<!-- Badge coinbase: monete generate dal mining (nessun mittente) -->
|
||||
<!-- Coinbase badge: coins generated by mining (no sender) -->
|
||||
<Border Classes="chip" Margin="0,0,8,6" IsVisible="{Binding IsCoinbase}"
|
||||
Background="{DynamicResource WarningBrush}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
@@ -823,7 +823,7 @@
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Sezione: Panoramica -->
|
||||
<!-- Section: Overview -->
|
||||
<TextBlock Classes="section" Text="{Binding Loc[tx.sect.overview]}"/>
|
||||
<Border Classes="card" Padding="16,12">
|
||||
<StackPanel Spacing="10">
|
||||
@@ -839,7 +839,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Sezione: Importi e commissioni -->
|
||||
<!-- Section: Amounts and fees -->
|
||||
<TextBlock Classes="section" Text="{Binding Loc[tx.sect.amounts]}"/>
|
||||
<Border Classes="card" Padding="16,12">
|
||||
<StackPanel Spacing="10">
|
||||
@@ -864,7 +864,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Sezione: Dettagli tecnici -->
|
||||
<!-- Section: Technical details -->
|
||||
<TextBlock Classes="section" Text="{Binding Loc[tx.sect.tech]}"/>
|
||||
<Border Classes="card" Padding="16,12">
|
||||
<StackPanel Spacing="10">
|
||||
@@ -935,9 +935,9 @@
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<!-- ============ OVERLAY IMPOSTAZIONI SERVER ============ -->
|
||||
<!-- Stesso pattern dell'overlay indirizzo: apertura/chiusura istantanee.
|
||||
Accessibile da Impostazioni → Server. -->
|
||||
<!-- ============ SERVER SETTINGS OVERLAY ============ -->
|
||||
<!-- Same pattern as the address overlay: instant open/close.
|
||||
Accessible from Settings → Server. -->
|
||||
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||
Background="{DynamicResource ScrimBrush}"
|
||||
Tapped="OnServerOverlayBackdropTapped"
|
||||
@@ -950,7 +950,7 @@
|
||||
<TextBlock Text="{Binding Loc[server.title]}"
|
||||
FontSize="18" FontWeight="Bold"/>
|
||||
|
||||
<!-- Host + porta — Desktop: 3 colonne -->
|
||||
<!-- Host + port — Desktop: 3 columns -->
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" RowDefinitions="Auto,Auto"
|
||||
IsVisible="{Binding IsDesktop}">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Loc[server.host]}"
|
||||
@@ -966,7 +966,7 @@
|
||||
<CheckBox Grid.Row="1" Grid.Column="2" IsChecked="{Binding UseSsl}"
|
||||
Margin="10,2,0,0" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<!-- Host + porta — Mobile: verticale, font più grandi -->
|
||||
<!-- Host + port — Mobile: vertical, larger font -->
|
||||
<StackPanel Spacing="10" IsVisible="{Binding IsMobile}">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding Loc[server.host]}" FontSize="13" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
@@ -987,7 +987,7 @@
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Azioni — Desktop: in riga -->
|
||||
<!-- Actions — Desktop: in a row -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" IsVisible="{Binding IsDesktop}">
|
||||
<Button Content="{Binding Loc[wallet.connect]}" Classes="accent"
|
||||
Command="{Binding ConnectAndSyncCommand}" IsEnabled="{Binding !IsSyncing}"/>
|
||||
@@ -996,7 +996,7 @@
|
||||
<Button Content="{Binding Loc[wallet.resetcert]}"
|
||||
Command="{Binding ResetCertificatesCommand}"/>
|
||||
</StackPanel>
|
||||
<!-- Azioni — Mobile: in colonna -->
|
||||
<!-- Actions — Mobile: stacked -->
|
||||
<StackPanel Spacing="6" IsVisible="{Binding IsMobile}">
|
||||
<Button Content="{Binding Loc[wallet.connect]}" Classes="accent"
|
||||
Command="{Binding ConnectAndSyncCommand}" IsEnabled="{Binding !IsSyncing}"
|
||||
@@ -1009,7 +1009,7 @@
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Stato connessione -->
|
||||
<!-- Connection status -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Ellipse Width="10" Height="10" Fill="{DynamicResource SuccessBrush}" VerticalAlignment="Center"
|
||||
IsVisible="{Binding IsConnected}"/>
|
||||
@@ -1019,7 +1019,7 @@
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Server conosciuti: clicca per riempire host/porta -->
|
||||
<!-- Known servers: click to populate host/port -->
|
||||
<TextBlock Text="{Binding Loc[server.known]}" FontSize="11" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
<ListBox ItemsSource="{Binding KnownServers}"
|
||||
SelectedItem="{Binding SelectedKnownServer}"
|
||||
@@ -1038,7 +1038,7 @@
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="net:KnownServer">
|
||||
<Panel>
|
||||
<!-- Desktop: host + porte in 2 colonne -->
|
||||
<!-- Desktop: host + ports in 2 columns -->
|
||||
<Grid ColumnDefinitions="*,Auto"
|
||||
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsDesktop}">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Host}"
|
||||
@@ -1050,7 +1050,7 @@
|
||||
<Run Text=" ssl "/><Run Text="{Binding SslPort}"/>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<!-- Mobile: host sopra, porte sotto -->
|
||||
<!-- Mobile: host above, ports below -->
|
||||
<StackPanel Spacing="2" Margin="0,4"
|
||||
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsMobile}">
|
||||
<TextBlock Text="{Binding Host}"
|
||||
@@ -1078,8 +1078,8 @@
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<!-- ============ OVERLAY IMPOSTAZIONI ============ -->
|
||||
<!-- In-app invece dei sottomenu annidati: niente popup OS lenti su WSLg. -->
|
||||
<!-- ============ SETTINGS OVERLAY ============ -->
|
||||
<!-- In-app instead of nested submenus: avoids slow OS popups on WSLg. -->
|
||||
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||
Background="{DynamicResource ScrimBrush}"
|
||||
Tapped="OnSettingsOverlayBackdropTapped"
|
||||
@@ -1093,7 +1093,7 @@
|
||||
<TextBlock Text="{Binding Loc[settings.title]}"
|
||||
FontSize="18" FontWeight="Bold"/>
|
||||
|
||||
<!-- Lingua -->
|
||||
<!-- Language -->
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="{Binding Loc[settings.language]}" FontSize="11" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
<WrapPanel>
|
||||
@@ -1118,7 +1118,7 @@
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Unità -->
|
||||
<!-- Unit -->
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="{Binding Loc[settings.unit]}" FontSize="11" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
<WrapPanel>
|
||||
@@ -1137,7 +1137,7 @@
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Server di indicizzazione (configurabile anche prima di aprire un wallet) -->
|
||||
<!-- Indexing server (configurable even before a wallet is open) -->
|
||||
<Button Content="{Binding Loc[settings.server]}"
|
||||
HorizontalAlignment="Left"
|
||||
Command="{Binding OpenServerSettingsCommand}"/>
|
||||
@@ -1150,8 +1150,8 @@
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<!-- ============ OVERLAY HELP / INFORMAZIONI ============ -->
|
||||
<!-- Stesso pattern dell'overlay impostazioni: apertura/chiusura istantanee. -->
|
||||
<!-- ============ HELP / ABOUT OVERLAY ============ -->
|
||||
<!-- Same pattern as the settings overlay: instant open/close. -->
|
||||
<Border Grid.Row="0" Grid.RowSpan="3"
|
||||
Background="{DynamicResource ScrimBrush}"
|
||||
Tapped="OnHelpOverlayBackdropTapped"
|
||||
|
||||
@@ -11,10 +11,10 @@ using PalladiumWallet.App.ViewModels;
|
||||
namespace PalladiumWallet.App.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Vista radice dell'app, condivisa tra desktop (ospitata in <see cref="MainWindow"/>)
|
||||
/// e mobile (root single-view). Tutti gli overlay sono in-app, quindi non servono
|
||||
/// finestre separate. Le API legate al top-level (file picker, clipboard) si
|
||||
/// raggiungono via <see cref="TopLevel.GetTopLevel"/> perché un UserControl non le espone.
|
||||
/// App root view, shared between desktop (hosted in <see cref="MainWindow"/>)
|
||||
/// and mobile (single-view root). All overlays are in-app so no separate windows
|
||||
/// are needed. Top-level APIs (file picker, clipboard) are reached via
|
||||
/// <see cref="TopLevel.GetTopLevel"/> because a UserControl does not expose them.
|
||||
/// </summary>
|
||||
public partial class MainView : UserControl
|
||||
{
|
||||
@@ -47,8 +47,8 @@ public partial class MainView : UserControl
|
||||
if (sender is not ListBox lb || DataContext is not MainWindowViewModel vm) return;
|
||||
if (lb.SelectedItem is not HistoryRow row) return;
|
||||
|
||||
// Overlay in-app: appare subito con lo spinner, i dati arrivano dal
|
||||
// server in background. Niente top-level window (lenta da aprire/chiudere).
|
||||
// In-app overlay: appears immediately with a spinner; data arrives from
|
||||
// the server in the background. No top-level window (slow to open/close).
|
||||
_ = vm.ShowTransactionDetailsAsync(row.Txid);
|
||||
}
|
||||
|
||||
@@ -78,8 +78,8 @@ public partial class MainView : UserControl
|
||||
vm.ShowAddressInfo(row);
|
||||
}
|
||||
|
||||
// Chiusura dell'overlay dettaglio indirizzo: click sullo sfondo scuro
|
||||
// (solo sullo sfondo, non sulla scheda) o tasto Esc.
|
||||
// Close the address-detail overlay: click on the dark backdrop
|
||||
// (backdrop only, not the card itself) or press Esc.
|
||||
private void OnAddressOverlayBackdropTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (!ReferenceEquals(e.Source, sender)) return;
|
||||
@@ -140,7 +140,7 @@ public partial class MainView : UserControl
|
||||
vm.IsHelpOpen = false;
|
||||
}
|
||||
|
||||
// Esc (desktop) o tasto Back (Android) chiudono l'overlay in primo piano.
|
||||
// Esc (desktop) or Back (Android) closes the topmost open overlay.
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if ((e.Key == Key.Escape || e.Key == Key.Back) && DataContext is MainWindowViewModel vm)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace PalladiumWallet.Core.Chain;
|
||||
|
||||
/// <summary>
|
||||
/// Tipo di rete supportato (selettore unico per tutto il wallet, blueprint §3).
|
||||
/// Supported network type (single selector for the whole wallet, blueprint §3).
|
||||
/// </summary>
|
||||
public enum NetKind
|
||||
{
|
||||
@@ -11,7 +11,7 @@ public enum NetKind
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tipo di script/indirizzo supportato (blueprint §4.3).
|
||||
/// Supported script/address type (blueprint §4.3).
|
||||
/// </summary>
|
||||
public enum ScriptKind
|
||||
{
|
||||
@@ -21,7 +21,7 @@ public enum ScriptKind
|
||||
/// <summary>P2SH-P2WPKH (segwit wrapped).</summary>
|
||||
WrappedSegwit,
|
||||
|
||||
/// <summary>P2WPKH (native segwit) — default consigliato.</summary>
|
||||
/// <summary>P2WPKH (native segwit) — recommended default.</summary>
|
||||
NativeSegwit,
|
||||
|
||||
/// <summary>P2SH-P2WSH (multisig wrapped).</summary>
|
||||
@@ -35,15 +35,15 @@ public enum ScriptKind
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coppia di header di versione BIP32 (4 byte big-endian) per serializzare
|
||||
/// xprv/xpub e varianti SLIP-132 (y/z/Y/Z) — blueprint §3.
|
||||
/// Pair of BIP32 version headers (4-byte big-endian) to serialize
|
||||
/// xprv/xpub and SLIP-132 variants (y/z/Y/Z) — blueprint §3.
|
||||
/// </summary>
|
||||
public readonly record struct ExtKeyHeaders(uint Private, uint Public)
|
||||
{
|
||||
/// <summary>Header privato come 4 byte big-endian (da anteporre al payload BIP32).</summary>
|
||||
/// <summary>Private header as 4 big-endian bytes (to prepend to the BIP32 payload).</summary>
|
||||
public byte[] PrivateBytes() => ToBytes(Private);
|
||||
|
||||
/// <summary>Header pubblico come 4 byte big-endian.</summary>
|
||||
/// <summary>Public header as 4 big-endian bytes.</summary>
|
||||
public byte[] PublicBytes() => ToBytes(Public);
|
||||
|
||||
internal static byte[] ToBytes(uint header) =>
|
||||
@@ -56,82 +56,82 @@ public readonly record struct ExtKeyHeaders(uint Private, uint Public)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server di indicizzazione di bootstrap: host + porta TCP + porta SSL (blueprint §3/§9).
|
||||
/// Bootstrap indexing server: host + TCP port + SSL port (blueprint §3/§9).
|
||||
/// </summary>
|
||||
public readonly record struct ServerEndpoint(string Host, int TcpPort, int SslPort);
|
||||
|
||||
/// <summary>
|
||||
/// Checkpoint di catena: ancora la fiducia quando la verifica PoW è disattivata
|
||||
/// (blueprint §7.3). Target serializzato come "bits" compatti.
|
||||
/// Chain checkpoint: anchors trust when PoW validation is disabled
|
||||
/// (blueprint §7.3). Target serialized as compact "bits".
|
||||
/// </summary>
|
||||
public readonly record struct Checkpoint(int Height, string BlockHash, uint Bits);
|
||||
|
||||
/// <summary>
|
||||
/// Profilo di rete/catena (blueprint §3): tutte le costanti specifiche della catena,
|
||||
/// centralizzate in un solo punto. Nessun magic number altrove nel codice.
|
||||
/// Network/chain profile (blueprint §3): all chain-specific constants,
|
||||
/// centralized in one place. No magic numbers elsewhere in the code.
|
||||
/// </summary>
|
||||
public sealed record ChainProfile
|
||||
{
|
||||
public required NetKind Kind { get; init; }
|
||||
|
||||
/// <summary>Nome rete, usato anche come sottocartella dati.</summary>
|
||||
/// <summary>Network name, also used as the data subfolder.</summary>
|
||||
public required string NetName { get; init; }
|
||||
|
||||
/// <summary>Simbolo dell'unità (es. PLM).</summary>
|
||||
/// <summary>Unit symbol (e.g. PLM).</summary>
|
||||
public required string CoinUnit { get; init; }
|
||||
|
||||
/// <summary>Prefisso WIF delle chiavi private.</summary>
|
||||
/// <summary>WIF prefix for private keys.</summary>
|
||||
public required byte WifPrefix { get; init; }
|
||||
|
||||
/// <summary>Byte di versione indirizzi P2PKH.</summary>
|
||||
/// <summary>Version byte for P2PKH addresses.</summary>
|
||||
public required byte AddrP2pkh { get; init; }
|
||||
|
||||
/// <summary>Byte di versione indirizzi P2SH.</summary>
|
||||
/// <summary>Version byte for P2SH addresses.</summary>
|
||||
public required byte AddrP2sh { get; init; }
|
||||
|
||||
/// <summary>Human-readable part bech32/bech32m.</summary>
|
||||
public required string SegwitHrp { get; init; }
|
||||
|
||||
/// <summary>HRP fatture BOLT11 (Lightning, fase successiva — §11).</summary>
|
||||
/// <summary>HRP for BOLT11 invoices (Lightning, later phase — §11).</summary>
|
||||
public required string Bolt11Hrp { get; init; }
|
||||
|
||||
/// <summary>Hash del blocco genesi (hex, byte order da explorer).</summary>
|
||||
/// <summary>Genesis block hash (hex, explorer byte order).</summary>
|
||||
public required string GenesisHash { get; init; }
|
||||
|
||||
/// <summary>Porta TCP del server di indicizzazione (non è la porta P2P del nodo).</summary>
|
||||
/// <summary>TCP port of the indexing server (not the node's P2P port).</summary>
|
||||
public required int DefaultTcpPort { get; init; }
|
||||
|
||||
/// <summary>Porta SSL del server di indicizzazione.</summary>
|
||||
/// <summary>SSL port of the indexing server.</summary>
|
||||
public required int DefaultSslPort { get; init; }
|
||||
|
||||
/// <summary>Porta P2P del nodo — solo informativa: il wallet SPV non la usa.</summary>
|
||||
/// <summary>Node's P2P port — informational only: the SPV wallet does not use it.</summary>
|
||||
public required int NodeP2pPort { get; init; }
|
||||
|
||||
/// <summary>Coin type SLIP-0044 nei derivation path (convenzione wallet).</summary>
|
||||
/// <summary>SLIP-0044 coin type in derivation paths (wallet convention).</summary>
|
||||
public required int Bip44CoinType { get; init; }
|
||||
|
||||
/// <summary>Schema URI pagamenti BIP21 (senza i due punti).</summary>
|
||||
/// <summary>BIP21 payment URI scheme (without the colon).</summary>
|
||||
public required string UriScheme { get; init; }
|
||||
|
||||
/// <summary>Block explorer di default.</summary>
|
||||
/// <summary>Default block explorer.</summary>
|
||||
public required string ExplorerUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// La catena usa LWMA (retargeting per-blocco, 2 minuti): un client SPV non può
|
||||
/// ricalcolarla, quindi la verifica bits/target va saltata e la fiducia ancorata
|
||||
/// ai checkpoint (§3/§7). Per la catena di riferimento è sempre true.
|
||||
/// The chain uses LWMA (per-block retargeting, 2 minutes): an SPV client cannot
|
||||
/// recompute it, so bits/target validation must be skipped and trust anchored
|
||||
/// to checkpoints (§3/§7). For the reference chain it is always true.
|
||||
/// </summary>
|
||||
public required bool SkipPowValidation { get; init; }
|
||||
|
||||
/// <summary>Tempo di blocco target in secondi (LWMA v2: 120s).</summary>
|
||||
/// <summary>Target block time in seconds (LWMA v2: 120s).</summary>
|
||||
public required int BlockTimeSeconds { get; init; }
|
||||
|
||||
/// <summary>Header BIP32/SLIP-132 per ciascun tipo di script.</summary>
|
||||
/// <summary>BIP32/SLIP-132 headers for each script type.</summary>
|
||||
public required IReadOnlyDictionary<ScriptKind, ExtKeyHeaders> ExtKeyHeaders { get; init; }
|
||||
|
||||
/// <summary>Server di indicizzazione per il primo contatto (bootstrap).</summary>
|
||||
/// <summary>Indexing servers for first contact (bootstrap).</summary>
|
||||
public required IReadOnlyList<ServerEndpoint> BootstrapServers { get; init; }
|
||||
|
||||
/// <summary>Checkpoint hardcoded, aggiornati a ogni release (§7.3).</summary>
|
||||
/// <summary>Hardcoded checkpoints, updated at every release (§7.3).</summary>
|
||||
public required IReadOnlyList<Checkpoint> Checkpoints { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
namespace PalladiumWallet.Core.Chain;
|
||||
|
||||
/// <summary>
|
||||
/// I tre profili di rete del wallet (blueprint §3). Valori mainnet verificati
|
||||
/// contro il sorgente del nodo (chainparams.cpp / pow.cpp) secondo il blueprint.
|
||||
/// The wallet's three network profiles (blueprint §3). Mainnet values verified
|
||||
/// against the node source (chainparams.cpp / pow.cpp) per the blueprint.
|
||||
/// </summary>
|
||||
public static class ChainProfiles
|
||||
{
|
||||
@@ -12,11 +12,11 @@ public static class ChainProfiles
|
||||
NetName = "mainnet",
|
||||
CoinUnit = "PLM",
|
||||
WifPrefix = 0x80,
|
||||
AddrP2pkh = 55, // indirizzi che iniziano con 'P'
|
||||
AddrP2sh = 5, // indirizzi che iniziano con '3'
|
||||
AddrP2pkh = 55, // addresses starting with 'P'
|
||||
AddrP2sh = 5, // addresses starting with '3'
|
||||
SegwitHrp = "plm",
|
||||
Bolt11Hrp = "plm",
|
||||
// La mainnet riusa la genesi di Bitcoin (blueprint §3).
|
||||
// Mainnet reuses Bitcoin's genesis (blueprint §3).
|
||||
GenesisHash = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
|
||||
DefaultTcpPort = 50001,
|
||||
DefaultSslPort = 50002,
|
||||
@@ -33,11 +33,11 @@ public static class ChainProfiles
|
||||
[ScriptKind.WrappedSegwitMultisig] = new(0x0295b005, 0x0295b43f), // Yprv / Ypub
|
||||
[ScriptKind.NativeSegwit] = new(0x04b2430c, 0x04b24746), // zprv / zpub
|
||||
[ScriptKind.NativeSegwitMultisig] = new(0x02aa7a99, 0x02aa7ed3), // Zprv / Zpub
|
||||
// Nessun SLIP-132 per P2TR: il contesto è dato dal path m/86'/…
|
||||
// No SLIP-132 for P2TR: the context is given by the m/86'/… path
|
||||
[ScriptKind.Taproot] = new(0x0488ade4, 0x0488b21e), // xprv / xpub (BIP32 standard)
|
||||
},
|
||||
// Server di indicizzazione noti per il primo contatto (§3/§9); altri
|
||||
// peer vengono scoperti via server.peers.subscribe.
|
||||
// Known indexing servers for first contact (§3/§9); other
|
||||
// peers are discovered via server.peers.subscribe.
|
||||
BootstrapServers =
|
||||
[
|
||||
new ServerEndpoint("173.212.224.67", 50001, 50002),
|
||||
@@ -45,7 +45,7 @@ public static class ChainProfiles
|
||||
new ServerEndpoint("66.94.115.80", 50001, 50002),
|
||||
new ServerEndpoint("89.117.149.130", 50001, 50002),
|
||||
],
|
||||
// TODO: popolare con [hash, bits] reali della catena (§7.3) prima della release.
|
||||
// TODO: populate with the chain's real [hash, bits] (§7.3) before release.
|
||||
Checkpoints = [],
|
||||
};
|
||||
|
||||
@@ -58,15 +58,15 @@ public static class ChainProfiles
|
||||
AddrP2sh = 115,
|
||||
SegwitHrp = "tplm",
|
||||
Bolt11Hrp = "tplm",
|
||||
// TODO: verificare contro chainparams.cpp del nodo (il blueprint non la riporta;
|
||||
// qui si assume la genesi di Bitcoin testnet3, da confermare).
|
||||
// TODO: verify against the node's chainparams.cpp (the blueprint does not report it;
|
||||
// here Bitcoin testnet3 genesis is assumed, to be confirmed).
|
||||
GenesisHash = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943",
|
||||
NodeP2pPort = 12333,
|
||||
Bip44CoinType = 1,
|
||||
ExtKeyHeaders = new Dictionary<ScriptKind, ExtKeyHeaders>
|
||||
{
|
||||
// tprv/tpub dal blueprint; le varianti SLIP-132 testnet (u/v/U/V) sono
|
||||
// i valori standard Electrum.
|
||||
// tprv/tpub from the blueprint; the testnet SLIP-132 variants (u/v/U/V) are
|
||||
// the standard Electrum values.
|
||||
[ScriptKind.Legacy] = new(0x04358394, 0x043587cf), // tprv / tpub
|
||||
[ScriptKind.WrappedSegwit] = new(0x044a4e28, 0x044a5262), // uprv / upub
|
||||
[ScriptKind.WrappedSegwitMultisig] = new(0x024285b5, 0x024289ef), // Uprv / Upub
|
||||
@@ -84,7 +84,7 @@ public static class ChainProfiles
|
||||
NetName = "regtest",
|
||||
SegwitHrp = "rplm",
|
||||
Bolt11Hrp = "rplm",
|
||||
// TODO: verificare contro chainparams.cpp del nodo (assunta genesi regtest Bitcoin).
|
||||
// TODO: verify against the node's chainparams.cpp (Bitcoin regtest genesis assumed).
|
||||
GenesisHash = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206",
|
||||
NodeP2pPort = 28444,
|
||||
Checkpoints = [],
|
||||
|
||||
@@ -4,13 +4,13 @@ using NBitcoin.DataEncoders;
|
||||
namespace PalladiumWallet.Core.Chain;
|
||||
|
||||
/// <summary>
|
||||
/// Costruisce e registra le <see cref="Network"/> NBitcoin di Palladium a partire dai
|
||||
/// <see cref="ChainProfiles"/> (blueprint §19.3). Tutto il resto del codice ottiene la
|
||||
/// rete da qui, mai da costanti sparse.
|
||||
/// Builds and registers the Palladium NBitcoin <see cref="Network"/> instances from the
|
||||
/// <see cref="ChainProfiles"/> (blueprint §19.3). All other code obtains the network
|
||||
/// from here — never from scattered constants.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Identità della moneta per NBitcoin: raggruppa le tre reti sotto il codice PLM.
|
||||
/// I getter sono lazy, quindi nessuna ricorsione durante la costruzione.
|
||||
/// Coin identity for NBitcoin: groups the three networks under the PLM code.
|
||||
/// Getters are lazy, so no recursion occurs during construction.
|
||||
/// </summary>
|
||||
public sealed class PalladiumNetworkSet : INetworkSet
|
||||
{
|
||||
@@ -34,13 +34,13 @@ public sealed class PalladiumNetworkSet : INetworkSet
|
||||
|
||||
public static class PalladiumNetworks
|
||||
{
|
||||
// Blocco genesi di Bitcoin (la mainnet Palladium lo riusa, blueprint §3).
|
||||
// Bitcoin genesis block (Palladium mainnet reuses it, blueprint §3).
|
||||
private const string MainGenesisHex =
|
||||
"0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c" +
|
||||
Coinbase;
|
||||
|
||||
// Genesi testnet3/regtest di Bitcoin: stessa coinbase, cambiano time/bits/nonce.
|
||||
// TODO: confermare contro chainparams.cpp del nodo (vedi ChainProfiles).
|
||||
// Bitcoin testnet3/regtest genesis: same coinbase, different time/bits/nonce.
|
||||
// TODO: confirm against the node's chainparams.cpp (see ChainProfiles).
|
||||
private const string TestGenesisHex =
|
||||
"0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae18" +
|
||||
Coinbase;
|
||||
@@ -81,9 +81,9 @@ public static class PalladiumNetworks
|
||||
NetKind.Testnet => ChainName.Testnet,
|
||||
_ => ChainName.Regtest,
|
||||
})
|
||||
// Magic P2P solo segnaposto: il wallet SPV non apre mai connessioni P2P
|
||||
// (parla solo col server di indicizzazione, §3). Serve a NBitcoin per
|
||||
// distinguere le reti registrate.
|
||||
// P2P magic is a placeholder only: the SPV wallet never opens P2P connections
|
||||
// (it only talks to the indexing server, §3). Required by NBitcoin to
|
||||
// distinguish registered networks.
|
||||
.SetMagic(magic)
|
||||
.SetPort(p.NodeP2pPort)
|
||||
.SetRPCPort(p.NodeP2pPort + 1)
|
||||
@@ -91,9 +91,9 @@ public static class PalladiumNetworks
|
||||
.SetBase58Bytes(Base58Type.PUBKEY_ADDRESS, [p.AddrP2pkh])
|
||||
.SetBase58Bytes(Base58Type.SCRIPT_ADDRESS, [p.AddrP2sh])
|
||||
.SetBase58Bytes(Base58Type.SECRET_KEY, [p.WifPrefix])
|
||||
// NBitcoin gestisce una sola coppia di header BIP32 per rete: quella
|
||||
// standard xprv/xpub. Le varianti SLIP-132 (y/z/Y/Z) si serializzano
|
||||
// a mano con ChainProfile.ExtKeyHeaders quando servono.
|
||||
// NBitcoin handles only one BIP32 header pair per network: the standard
|
||||
// xprv/xpub. SLIP-132 variants (y/z/Y/Z) are serialised manually via
|
||||
// ChainProfile.ExtKeyHeaders when needed.
|
||||
.SetBase58Bytes(Base58Type.EXT_SECRET_KEY, legacy.PrivateBytes())
|
||||
.SetBase58Bytes(Base58Type.EXT_PUBLIC_KEY, legacy.PublicBytes())
|
||||
.SetBech32(Bech32Type.WITNESS_PUBKEY_ADDRESS, Encoders.Bech32(p.SegwitHrp))
|
||||
@@ -102,8 +102,8 @@ public static class PalladiumNetworks
|
||||
.SetUriScheme(p.UriScheme)
|
||||
.SetConsensus(new Consensus
|
||||
{
|
||||
// Valori usati solo dalle API NBitcoin che li richiedono: la validazione
|
||||
// header reale è custom (Core/Spv) con skip PoW + checkpoint (§7).
|
||||
// Values used only by NBitcoin APIs that require them: real header
|
||||
// validation is custom (Core/Spv) with PoW skip + checkpoints (§7).
|
||||
PowLimit = new Target(new uint256("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff")),
|
||||
PowTargetSpacing = TimeSpan.FromSeconds(p.BlockTimeSeconds),
|
||||
PowTargetTimespan = TimeSpan.FromDays(14),
|
||||
|
||||
+21
-21
@@ -3,8 +3,8 @@ using NBitcoin;
|
||||
namespace PalladiumWallet.Core.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Lingue wordlist BIP39 supportate (blueprint §4.1). Le liste sono incorporate
|
||||
/// in NBitcoin: nessun accesso a rete o filesystem.
|
||||
/// Supported BIP39 wordlist languages (blueprint §4.1). The lists are embedded
|
||||
/// in NBitcoin: no network or filesystem access required.
|
||||
/// </summary>
|
||||
public enum MnemonicLanguage
|
||||
{
|
||||
@@ -18,7 +18,7 @@ public enum MnemonicLanguage
|
||||
Czech,
|
||||
}
|
||||
|
||||
/// <summary>Numero di parole supportato in creazione (blueprint §4.1: 12 o 24).</summary>
|
||||
/// <summary>Supported word counts for mnemonic creation (blueprint §4.1: 12 or 24).</summary>
|
||||
public enum MnemonicLength
|
||||
{
|
||||
Twelve = 12,
|
||||
@@ -26,16 +26,16 @@ public enum MnemonicLength
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Facciata BIP39 su NBitcoin.Mnemonic (blueprint §4.1). NBitcoin copre già
|
||||
/// entropia→parole, checksum, normalizzazione NFKD e PBKDF2-HMAC-SHA512 a 2048
|
||||
/// round con salt "mnemonic"+passphrase: qui si restringe l'API ai casi del
|
||||
/// blueprint e si centralizza la mappa delle lingue. Quando arriverà il seed
|
||||
/// nativo versionato (§4.1 punto 1), il riconoscimento multi-schema sarà una
|
||||
/// catena di TryParse per schema.
|
||||
/// BIP39 facade over NBitcoin.Mnemonic (blueprint §4.1). NBitcoin already handles
|
||||
/// entropy→words, checksum, NFKD normalisation, and PBKDF2-HMAC-SHA512 with 2048
|
||||
/// rounds using salt "mnemonic"+passphrase. This class narrows the API to the
|
||||
/// blueprint use-cases and centralises the language map. When the native versioned
|
||||
/// seed arrives (§4.1 point 1), multi-scheme recognition will be a chain of
|
||||
/// TryParse calls per scheme.
|
||||
/// </summary>
|
||||
public static class Bip39
|
||||
{
|
||||
/// <summary>Genera una nuova mnemonica con entropia dal CSPRNG di sistema.</summary>
|
||||
/// <summary>Generates a new mnemonic using entropy from the system CSPRNG.</summary>
|
||||
public static Mnemonic Generate(MnemonicLength length, MnemonicLanguage language = MnemonicLanguage.English)
|
||||
{
|
||||
var wordCount = length == MnemonicLength.Twelve ? WordCount.Twelve : WordCount.TwentyFour;
|
||||
@@ -43,10 +43,10 @@ public static class Bip39
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Riconosce e valida una mnemonica BIP39: numero di parole, appartenenza alla
|
||||
/// wordlist e checksum. Se <paramref name="language"/> è indicata viene provata
|
||||
/// per prima; altrimenti la lingua è auto-rilevata (parole condivise tra liste
|
||||
/// possono rendere ambiguo l'autodetect: in import l'utente può forzarla).
|
||||
/// Recognises and validates a BIP39 mnemonic: word count, wordlist membership,
|
||||
/// and checksum. If <paramref name="language"/> is specified it is tried first;
|
||||
/// otherwise the language is auto-detected (words shared between lists can make
|
||||
/// autodetect ambiguous — the user can override it on import).
|
||||
/// </summary>
|
||||
public static bool TryParse(string text, out Mnemonic? mnemonic, MnemonicLanguage? language = null)
|
||||
{
|
||||
@@ -54,8 +54,8 @@ public static class Bip39
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return false;
|
||||
|
||||
// Solo trim esterno: la normalizzazione NFKD e lo spazio ideografico
|
||||
// giapponese li gestisce NBitcoin, il testo non va alterato (§4.1).
|
||||
// Outer trim only: NBitcoin handles NFKD normalisation and Japanese
|
||||
// ideographic spaces — the text must not be altered further (§4.1).
|
||||
text = text.Trim();
|
||||
|
||||
IEnumerable<Wordlist> candidates = language is not null
|
||||
@@ -67,7 +67,7 @@ public static class Bip39
|
||||
try
|
||||
{
|
||||
var parsed = new Mnemonic(text, wordlist);
|
||||
// Il costruttore NON verifica il checksum: controllo esplicito obbligatorio.
|
||||
// The constructor does NOT verify the checksum — explicit check is mandatory.
|
||||
if (parsed.IsValidChecksum && parsed.Words.Length is 12 or 15 or 18 or 21 or 24)
|
||||
{
|
||||
mnemonic = parsed;
|
||||
@@ -76,7 +76,7 @@ public static class Bip39
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// Parole fuori wordlist o conteggio non valido: si prova oltre.
|
||||
// Words outside the wordlist or invalid count — try the next candidate.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ public static class Bip39
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mnemonica → seed di 64 byte (PBKDF2-HMAC-SHA512, 2048 round, salt
|
||||
/// "mnemonic"+passphrase). La passphrase cambia completamente il wallet
|
||||
/// derivato: avvisi UI obbligatori (§4.1).
|
||||
/// Mnemonic → 64-byte seed (PBKDF2-HMAC-SHA512, 2048 rounds, salt
|
||||
/// "mnemonic"+passphrase). The passphrase completely changes the derived
|
||||
/// wallet — mandatory UI warnings required (§4.1).
|
||||
/// </summary>
|
||||
public static byte[] ToSeed(Mnemonic mnemonic, string? passphrase = null) =>
|
||||
mnemonic.DeriveSeed(passphrase);
|
||||
|
||||
@@ -4,25 +4,25 @@ using PalladiumWallet.Core.Chain;
|
||||
namespace PalladiumWallet.Core.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Costruzione dei derivation path BIP44/49/84 (blueprint §4.2) e mappatura
|
||||
/// ScriptKind → purpose / ScriptPubKeyType. I purpose sono costanti di protocollo
|
||||
/// BIP (chain-independent); il coin_type viene sempre dal ChainProfile.
|
||||
/// BIP44/49/84 derivation path construction (blueprint §4.2) and mapping of
|
||||
/// ScriptKind → purpose / ScriptPubKeyType. Purposes are BIP protocol constants
|
||||
/// (chain-independent); coin_type always comes from the ChainProfile.
|
||||
/// </summary>
|
||||
public static class DerivationPaths
|
||||
{
|
||||
/// <summary>Purpose BIP44 (P2PKH legacy).</summary>
|
||||
/// <summary>BIP44 purpose (P2PKH legacy).</summary>
|
||||
public const int PurposeLegacy = 44;
|
||||
|
||||
/// <summary>Purpose BIP49 (P2SH-P2WPKH).</summary>
|
||||
/// <summary>BIP49 purpose (P2SH-P2WPKH).</summary>
|
||||
public const int PurposeWrappedSegwit = 49;
|
||||
|
||||
/// <summary>Purpose BIP84 (P2WPKH nativo).</summary>
|
||||
/// <summary>BIP84 purpose (native P2WPKH).</summary>
|
||||
public const int PurposeNativeSegwit = 84;
|
||||
|
||||
/// <summary>Purpose BIP48 (multisig — fase successiva, §16 passo 8).</summary>
|
||||
/// <summary>BIP48 purpose (multisig — next phase, §16 step 8).</summary>
|
||||
public const int PurposeMultisig = 48;
|
||||
|
||||
/// <summary>Purpose BIP86 (P2TR key-path, Taproot).</summary>
|
||||
/// <summary>BIP86 purpose (P2TR key-path, Taproot).</summary>
|
||||
public const int PurposeTaproot = 86;
|
||||
|
||||
public static int PurposeFor(ScriptKind kind) => kind switch
|
||||
@@ -36,9 +36,9 @@ public static class DerivationPaths
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Tipo di scriptPubKey NBitcoin per la derivazione da pubkey singola.
|
||||
/// I tipi multisig derivano da redeem script, non da una pubkey: arriveranno
|
||||
/// con i wallet M-di-N (§4.5).
|
||||
/// NBitcoin scriptPubKey type for single-pubkey derivation.
|
||||
/// Multisig types are derived from a redeem script, not a pubkey — they will
|
||||
/// arrive with M-of-N wallet support (§4.5).
|
||||
/// </summary>
|
||||
public static ScriptPubKeyType ScriptPubKeyTypeFor(ScriptKind kind) => kind switch
|
||||
{
|
||||
@@ -48,13 +48,13 @@ public static class DerivationPaths
|
||||
ScriptKind.Taproot => ScriptPubKeyType.TaprootBIP86,
|
||||
ScriptKind.WrappedSegwitMultisig or ScriptKind.NativeSegwitMultisig =>
|
||||
throw new NotSupportedException(
|
||||
"I tipi multisig derivano da redeem script: supporto previsto con i wallet M-di-N (§4.5)."),
|
||||
"Multisig types are derived from a redeem script: support planned with M-of-N wallets (§4.5)."),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Path di account relativo alla root: purpose'/coin'/account' (§4.2).
|
||||
/// Il coin_type è quello del profilo (746 mainnet, 1 testnet).
|
||||
/// Account path relative to the root: purpose'/coin'/account' (§4.2).
|
||||
/// coin_type is taken from the profile (746 mainnet, 1 testnet).
|
||||
/// </summary>
|
||||
public static KeyPath AccountPath(ScriptKind kind, ChainProfile profile, int account = 0)
|
||||
{
|
||||
@@ -64,7 +64,7 @@ public static class DerivationPaths
|
||||
$"{PurposeFor(kind)}'/{profile.Bip44CoinType}'/{account}'");
|
||||
}
|
||||
|
||||
/// <summary>Sottopath non-hardened change/index sotto l'account (change=0 receiving, change=1 change).</summary>
|
||||
/// <summary>Non-hardened change/index sub-path under the account (change=0 receiving, change=1 change).</summary>
|
||||
public static KeyPath AddressSubPath(bool isChange, int index)
|
||||
{
|
||||
if (index < 0)
|
||||
@@ -73,8 +73,8 @@ public static class DerivationPaths
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsing di derivation path personalizzati in import (Sparrow-like, §4.2):
|
||||
/// accetta il prefisso "m/" opzionale e gli hardened marker ' oppure h.
|
||||
/// Parsing of custom derivation paths on import (Sparrow-like, §4.2):
|
||||
/// accepts the optional "m/" prefix and hardened markers ' or h.
|
||||
/// </summary>
|
||||
public static bool TryParse(string path, out KeyPath? keyPath)
|
||||
{
|
||||
|
||||
@@ -4,12 +4,12 @@ using PalladiumWallet.Core.Chain;
|
||||
namespace PalladiumWallet.Core.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Account HD single-sig (blueprint §4.2/§4.4): chiave estesa di account (xprv,
|
||||
/// oppure solo xpub = watch-only) + tipo di script + profilo di rete. Deriva
|
||||
/// indirizzi receiving (change=0) e change (change=1) on-demand per indice.
|
||||
/// Gli indirizzi si derivano sempre dall'xpub di account (sottopath non-hardened),
|
||||
/// quindi il watch-only funziona per costruzione. Il keystore completo
|
||||
/// (cifratura, factory dal file wallet, §4.5) arriva con la persistenza (§8).
|
||||
/// Single-sig HD account (blueprint §4.2/§4.4): extended account key (xprv,
|
||||
/// or xpub-only = watch-only) + script type + network profile. Derives receiving
|
||||
/// (change=0) and change (change=1) addresses on-demand by index.
|
||||
/// Addresses are always derived from the account xpub (non-hardened sub-path),
|
||||
/// so watch-only works by construction. The full keystore
|
||||
/// (encryption, factory from wallet file, §4.5) arrives with persistence (§8).
|
||||
/// </summary>
|
||||
public sealed class HdAccount : IWalletAccount
|
||||
{
|
||||
@@ -18,25 +18,25 @@ public sealed class HdAccount : IWalletAccount
|
||||
public ScriptKind Kind { get; }
|
||||
public ChainProfile Profile { get; }
|
||||
|
||||
/// <summary>Path dell'account relativo alla root (es. 84'/746'/0', o personalizzato).</summary>
|
||||
/// <summary>Account path relative to the root (e.g. 84'/746'/0', or custom).</summary>
|
||||
public KeyPath AccountPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Fingerprint della chiave master: con <see cref="AccountPath"/> forma le
|
||||
/// origin info richieste dalle PSBT (§6.5). Zero se ignota (xpub importata
|
||||
/// senza metadati).
|
||||
/// Master key fingerprint: together with <see cref="AccountPath"/> forms the
|
||||
/// origin info required by PSBTs (§6.5). Zero if unknown (imported xpub
|
||||
/// without metadata).
|
||||
/// </summary>
|
||||
public HDFingerprint MasterFingerprint { get; }
|
||||
|
||||
public ExtPubKey AccountXpub { get; }
|
||||
|
||||
/// <summary>True se l'account conosce solo la xpub: costruisce ma non firma (§4.5).</summary>
|
||||
/// <summary>True if the account only knows the xpub: can build but not sign (§4.5).</summary>
|
||||
public bool IsWatchOnly => _accountXprv is null;
|
||||
|
||||
private HdAccount(ExtKey? accountXprv, ExtPubKey accountXpub, ScriptKind kind,
|
||||
ChainProfile profile, KeyPath accountPath, HDFingerprint masterFingerprint)
|
||||
{
|
||||
// Valida subito la mappatura (i tipi multisig non sono supportati qui).
|
||||
// Validate the mapping immediately (multisig types are not supported here).
|
||||
DerivationPaths.ScriptPubKeyTypeFor(kind);
|
||||
_accountXprv = accountXprv;
|
||||
AccountXpub = accountXpub;
|
||||
@@ -46,7 +46,7 @@ public sealed class HdAccount : IWalletAccount
|
||||
MasterFingerprint = masterFingerprint;
|
||||
}
|
||||
|
||||
/// <summary>Caso principale: mnemonica BIP39 (+ passphrase opzionale) → account standard.</summary>
|
||||
/// <summary>Main case: BIP39 mnemonic (+ optional passphrase) → standard account.</summary>
|
||||
public static HdAccount FromMnemonic(Mnemonic mnemonic, string? passphrase,
|
||||
ScriptKind kind, ChainProfile profile, int account = 0) =>
|
||||
FromSeed(Bip39.ToSeed(mnemonic, passphrase), kind, profile, account);
|
||||
@@ -55,22 +55,22 @@ public sealed class HdAccount : IWalletAccount
|
||||
FromSeed(seed, kind, profile, DerivationPaths.AccountPath(kind, profile, account));
|
||||
|
||||
/// <summary>
|
||||
/// Import con derivation path personalizzato (§4.2, Sparrow-like):
|
||||
/// <paramref name="kind"/> determina solo il tipo di indirizzo generato.
|
||||
/// Import with a custom derivation path (§4.2, Sparrow-like):
|
||||
/// <paramref name="kind"/> only determines the generated address type.
|
||||
/// </summary>
|
||||
public static HdAccount FromSeed(byte[] seed, ScriptKind kind, ChainProfile profile, KeyPath accountPath)
|
||||
{
|
||||
var root = ExtKey.CreateFromSeed(seed);
|
||||
// L'account si deriva sempre dalla xprv: i livelli hardened del path
|
||||
// non sono derivabili da una xpub.
|
||||
// The account is always derived from the xprv: hardened path levels
|
||||
// cannot be derived from an xpub alone.
|
||||
var accountXprv = root.Derive(accountPath);
|
||||
return new HdAccount(accountXprv, accountXprv.Neuter(), kind, profile,
|
||||
accountPath, root.Neuter().PubKey.GetHDFingerPrint());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Watch-only da xpub di account (§4.4): fingerprint e path sono noti solo
|
||||
/// se forniti da chi importa (servono per le origin info PSBT).
|
||||
/// Watch-only from account xpub (§4.4): fingerprint and path are known only
|
||||
/// if supplied by the importer (required for PSBT origin info).
|
||||
/// </summary>
|
||||
public static HdAccount FromAccountXpub(ExtPubKey accountXpub, ScriptKind kind,
|
||||
ChainProfile profile, KeyPath? accountPath = null, HDFingerprint? masterFingerprint = null) =>
|
||||
@@ -78,7 +78,7 @@ public sealed class HdAccount : IWalletAccount
|
||||
accountPath ?? DerivationPaths.AccountPath(kind, profile),
|
||||
masterFingerprint ?? default);
|
||||
|
||||
/// <summary>Import spendibile da xprv di account.</summary>
|
||||
/// <summary>Spendable import from account xprv.</summary>
|
||||
public static HdAccount FromAccountXprv(ExtKey accountXprv, ScriptKind kind,
|
||||
ChainProfile profile, KeyPath? accountPath = null, HDFingerprint? masterFingerprint = null) =>
|
||||
new(accountXprv, accountXprv.Neuter(), kind, profile,
|
||||
@@ -97,27 +97,27 @@ public sealed class HdAccount : IWalletAccount
|
||||
public PubKey? GetPublicKey(bool isChange, int index) =>
|
||||
AccountXpub.Derive(DerivationPaths.AddressSubPath(isChange, index)).PubKey;
|
||||
|
||||
/// <summary>Chiave privata di un indirizzo; null se watch-only (§17).</summary>
|
||||
/// <summary>Private key for an address; null if watch-only (§17).</summary>
|
||||
public Key? GetPrivateKey(bool isChange, int index) =>
|
||||
IsWatchOnly ? null : GetExtPrivateKey(isChange, index).PrivateKey;
|
||||
|
||||
/// <summary>Gli account HD usano il gap limit: nessuna lista fissa di indirizzi.</summary>
|
||||
/// <summary>HD accounts use the gap limit: no fixed address list.</summary>
|
||||
public IReadOnlyList<(BitcoinAddress Address, bool IsChange, int Index)>? FixedAddresses => null;
|
||||
|
||||
/// <summary>
|
||||
/// Chiave privata estesa di un indirizzo. Lancia se watch-only: nessuna
|
||||
/// chiave privata è derivabile dalle sole pubbliche (§17).
|
||||
/// Extended private key for an address. Throws if watch-only: no private key
|
||||
/// can be derived from public keys alone (§17).
|
||||
/// </summary>
|
||||
public ExtKey GetExtPrivateKey(bool isChange, int index) =>
|
||||
(_accountXprv ?? throw new InvalidOperationException("Account watch-only: non può firmare."))
|
||||
(_accountXprv ?? throw new InvalidOperationException("Watch-only account: cannot sign."))
|
||||
.Derive(DerivationPaths.AddressSubPath(isChange, index));
|
||||
|
||||
/// <summary>Xpub di account in formato SLIP-132 (xpub/ypub/zpub secondo Kind).</summary>
|
||||
/// <summary>Account xpub in SLIP-132 format (xpub/ypub/zpub according to Kind).</summary>
|
||||
public string ToSlip132() => Slip132.Encode(AccountXpub, Kind, Profile);
|
||||
|
||||
/// <summary>Xprv di account in formato SLIP-132. Lancia se watch-only.</summary>
|
||||
/// <summary>Account xprv in SLIP-132 format. Throws if watch-only.</summary>
|
||||
public string ToSlip132Private() =>
|
||||
Slip132.Encode(
|
||||
_accountXprv ?? throw new InvalidOperationException("Account watch-only: nessuna chiave privata."),
|
||||
_accountXprv ?? throw new InvalidOperationException("Watch-only account: no private key."),
|
||||
Kind, Profile);
|
||||
}
|
||||
|
||||
@@ -4,31 +4,31 @@ using PalladiumWallet.Core.Chain;
|
||||
namespace PalladiumWallet.Core.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Astrazione su tutti i tipi di account wallet (HD da seed, HD da xpub/xprv importata,
|
||||
/// chiavi WIF importate). Consente a WalletSynchronizer e TransactionFactory di operare
|
||||
/// indipendentemente dal tipo di keystore sottostante (blueprint §4.4–§4.5).
|
||||
/// Abstraction over all wallet account types (HD from seed, HD from imported xpub/xprv,
|
||||
/// imported WIF keys). Allows WalletSynchronizer and TransactionFactory to operate
|
||||
/// independently of the underlying keystore type (blueprint §4.4–§4.5).
|
||||
/// </summary>
|
||||
public interface IWalletAccount
|
||||
{
|
||||
ScriptKind Kind { get; }
|
||||
ChainProfile Profile { get; }
|
||||
|
||||
/// <summary>True se l'account non può firmare (assenza di chiavi private).</summary>
|
||||
/// <summary>True if the account cannot sign (no private keys present).</summary>
|
||||
bool IsWatchOnly { get; }
|
||||
|
||||
BitcoinAddress GetAddress(bool isChange, int index);
|
||||
BitcoinAddress GetReceiveAddress(int index);
|
||||
BitcoinAddress GetChangeAddress(int index);
|
||||
|
||||
/// <summary>Null se la chiave pubblica non è ricavabile dall'account (indirizzi puri watch-only).</summary>
|
||||
/// <summary>Null if the public key cannot be derived from the account (pure watch-only addresses).</summary>
|
||||
PubKey? GetPublicKey(bool isChange, int index);
|
||||
|
||||
/// <summary>Null se l'account è watch-only o l'indice è fuori range.</summary>
|
||||
/// <summary>Null if the account is watch-only or the index is out of range.</summary>
|
||||
Key? GetPrivateKey(bool isChange, int index);
|
||||
|
||||
/// <summary>
|
||||
/// Per gli account con indirizzi fissi (WIF importati) restituisce la lista
|
||||
/// completa da scansionare; null per gli account HD che usano il gap limit.
|
||||
/// For accounts with fixed addresses (imported WIF keys) returns the full list
|
||||
/// to scan; null for HD accounts that use the gap limit.
|
||||
/// </summary>
|
||||
IReadOnlyList<(BitcoinAddress Address, bool IsChange, int Index)>? FixedAddresses { get; }
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ using PalladiumWallet.Core.Chain;
|
||||
namespace PalladiumWallet.Core.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Account da chiavi WIF singole importate (blueprint §4.4 — "Imported"):
|
||||
/// lista fissa di indirizzi, nessuna derivazione HD, nessuna catena di change.
|
||||
/// Il change va sempre al primo indirizzo importato.
|
||||
/// Account built from individually imported WIF keys (blueprint §4.4 — "Imported"):
|
||||
/// fixed list of addresses, no HD derivation, no change chain.
|
||||
/// Change always goes back to the first imported address.
|
||||
/// </summary>
|
||||
public sealed class ImportedKeyAccount : IWalletAccount
|
||||
{
|
||||
@@ -24,7 +24,7 @@ public sealed class ImportedKeyAccount : IWalletAccount
|
||||
ScriptKind kind, ChainProfile profile)
|
||||
{
|
||||
if (entries.Count == 0)
|
||||
throw new ArgumentException("Almeno un indirizzo richiesto.", nameof(entries));
|
||||
throw new ArgumentException("At least one address is required.", nameof(entries));
|
||||
_entries = [.. entries];
|
||||
Kind = kind;
|
||||
Profile = profile;
|
||||
@@ -39,7 +39,7 @@ public sealed class ImportedKeyAccount : IWalletAccount
|
||||
|
||||
public BitcoinAddress GetReceiveAddress(int index) => GetAddress(false, index);
|
||||
|
||||
/// <summary>Il change torna sempre al primo indirizzo importato.</summary>
|
||||
/// <summary>Change always returns to the first imported address.</summary>
|
||||
public BitcoinAddress GetChangeAddress(int index) => _entries[0].Address;
|
||||
|
||||
public PubKey? GetPublicKey(bool isChange, int index)
|
||||
|
||||
@@ -20,8 +20,8 @@ public static class Slip132
|
||||
Encoders.Base58Check.EncodeData([.. profile.ExtKeyHeaders[kind].PrivateBytes(), .. key.ToBytes()]);
|
||||
|
||||
/// <summary>
|
||||
/// Riconosce l'header (→ ScriptKind) e decodifica una chiave pubblica estesa:
|
||||
/// è la via dell'import watch-only (§4.4).
|
||||
/// Recognises the header (→ ScriptKind) and decodes an extended public key:
|
||||
/// this is the watch-only import path (§4.4).
|
||||
/// </summary>
|
||||
public static bool TryDecodePublic(string encoded, ChainProfile profile,
|
||||
out ExtPubKey? key, out ScriptKind kind)
|
||||
|
||||
@@ -5,10 +5,10 @@ using System.Text.Json;
|
||||
namespace PalladiumWallet.Core.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Pinning TLS "trust on first use" (blueprint §9): al primo contatto la
|
||||
/// fingerprint SHA-256 del certificato del server viene salvata; ai successivi
|
||||
/// viene confrontata. Se cambia, la connessione va rifiutata e l'utente può
|
||||
/// sbloccare con un reset esplicito (caso tipico: server self-signed rinnovato).
|
||||
/// TLS "trust on first use" pinning (blueprint §9): on the first contact the
|
||||
/// server certificate's SHA-256 fingerprint is saved; on subsequent ones it is
|
||||
/// compared. If it changes, the connection must be rejected and the user can
|
||||
/// unlock with an explicit reset (typical case: renewed self-signed server).
|
||||
/// </summary>
|
||||
public sealed class CertificatePinStore(string filePath)
|
||||
{
|
||||
@@ -18,8 +18,8 @@ public sealed class CertificatePinStore(string filePath)
|
||||
Convert.ToHexString(SHA256.HashData(certificate.GetRawCertData())).ToLowerInvariant();
|
||||
|
||||
/// <summary>
|
||||
/// Verifica TOFU: true se il certificato è quello già visto (o se è il primo
|
||||
/// contatto, nel qual caso viene salvato). False = certificato cambiato.
|
||||
/// TOFU check: true if the certificate is the one already seen (or if it is the
|
||||
/// first contact, in which case it is saved). False = certificate changed.
|
||||
/// </summary>
|
||||
public bool VerifyOrPin(string host, int port, X509Certificate certificate)
|
||||
{
|
||||
@@ -36,7 +36,7 @@ public sealed class CertificatePinStore(string filePath)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reset del certificato salvato per un server ("reset certificati SSL", §9).</summary>
|
||||
/// <summary>Reset the saved certificate for a server ("reset SSL certificates", §9).</summary>
|
||||
public bool Reset(string host, int port)
|
||||
{
|
||||
lock (_lock)
|
||||
|
||||
+13
-13
@@ -2,24 +2,24 @@ using System.Text.Json;
|
||||
|
||||
namespace PalladiumWallet.Core.Net;
|
||||
|
||||
/// <summary>Voce di storico di uno scripthash (blockchain.scripthash.get_history).</summary>
|
||||
/// <summary>History entry for a scripthash (blockchain.scripthash.get_history).</summary>
|
||||
public readonly record struct HistoryItem(string TxHash, int Height, long FeeSats);
|
||||
|
||||
/// <summary>UTXO riportato dal server (blockchain.scripthash.listunspent).</summary>
|
||||
/// <summary>UTXO reported by the server (blockchain.scripthash.listunspent).</summary>
|
||||
public readonly record struct UnspentItem(string TxHash, int TxPos, long ValueSats, int Height);
|
||||
|
||||
/// <summary>Prova di Merkle (blockchain.transaction.get_merkle).</summary>
|
||||
/// <summary>Merkle proof (blockchain.transaction.get_merkle).</summary>
|
||||
public sealed record MerkleProofResponse(int BlockHeight, int Pos, IReadOnlyList<string> Merkle);
|
||||
|
||||
/// <summary>Tip della catena notificato da blockchain.headers.subscribe.</summary>
|
||||
/// <summary>Chain tip notified by blockchain.headers.subscribe.</summary>
|
||||
public readonly record struct ChainTip(int Height, string HeaderHex);
|
||||
|
||||
/// <summary>Peer annunciato da server.peers.subscribe (porte null = non offerte).</summary>
|
||||
/// <summary>Peer announced by server.peers.subscribe (null ports = not offered).</summary>
|
||||
public sealed record PeerInfo(string Host, int? TcpPort, int? SslPort, string? Version);
|
||||
|
||||
/// <summary>
|
||||
/// Helper tipizzati sui metodi del protocollo (blueprint §10), sopra il
|
||||
/// trasporto JSON-RPC generico di <see cref="ElectrumClient"/>.
|
||||
/// Typed helpers over the protocol methods (blueprint §10), on top of the
|
||||
/// generic JSON-RPC transport of <see cref="ElectrumClient"/>.
|
||||
/// </summary>
|
||||
public static class ElectrumApi
|
||||
{
|
||||
@@ -29,7 +29,7 @@ public static class ElectrumApi
|
||||
return new ChainTip(r.GetProperty("height").GetInt32(), r.GetProperty("hex").GetString()!);
|
||||
}
|
||||
|
||||
/// <summary>Status corrente dello scripthash (null = mai usato).</summary>
|
||||
/// <summary>Current status of the scripthash (null = never used).</summary>
|
||||
public static async Task<string?> SubscribeScripthashAsync(this ElectrumClient c, string scripthash,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -89,7 +89,7 @@ public static class ElectrumApi
|
||||
return r.GetString()!;
|
||||
}
|
||||
|
||||
/// <summary>Fee stimata in coin/kB per il target dato; -1 se il server non sa stimare.</summary>
|
||||
/// <summary>Estimated fee in coin/kB for the given target; -1 if the server cannot estimate.</summary>
|
||||
public static async Task<decimal> EstimateFeeAsync(this ElectrumClient c, int targetBlocks,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -112,7 +112,7 @@ public static class ElectrumApi
|
||||
public static Task PingAsync(this ElectrumClient c, CancellationToken ct = default) =>
|
||||
c.RequestAsync("server.ping", ct);
|
||||
|
||||
/// <summary>Peer annunciati dal server (scoperta di altri server, §9).</summary>
|
||||
/// <summary>Peers announced by the server (discovery of other servers, §9).</summary>
|
||||
public static async Task<IReadOnlyList<PeerInfo>> GetPeersAsync(this ElectrumClient c,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
@@ -121,9 +121,9 @@ public static class ElectrumApi
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsa la risposta di server.peers.subscribe: lista di
|
||||
/// [ip, hostname, ["v1.4.2", "pN", "tPORTA", "sPORTA", ...]];
|
||||
/// "t"/"s" senza numero = porta di default della rete (risolta dal chiamante).
|
||||
/// Parses the server.peers.subscribe response: a list of
|
||||
/// [ip, hostname, ["v1.4.2", "pN", "tPORT", "sPORT", ...]];
|
||||
/// "t"/"s" without a number = network default port (resolved by the caller).
|
||||
/// </summary>
|
||||
public static IReadOnlyList<PeerInfo> ParsePeers(JsonElement response)
|
||||
{
|
||||
|
||||
@@ -9,9 +9,9 @@ using System.Threading.Channels;
|
||||
namespace PalladiumWallet.Core.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Client del protocollo del server di indicizzazione (blueprint §10):
|
||||
/// JSON-RPC 2.0 newline-delimited su TCP, opzionalmente TLS con pinning TOFU.
|
||||
/// Le notifiche (subscription) arrivano sull'evento <see cref="NotificationReceived"/>.
|
||||
/// Client for the indexing server protocol (blueprint §10):
|
||||
/// newline-delimited JSON-RPC 2.0 over TCP, optionally TLS with TOFU pinning.
|
||||
/// Notifications (subscriptions) arrive on the <see cref="NotificationReceived"/> event.
|
||||
/// </summary>
|
||||
public sealed class ElectrumClient : IAsyncDisposable
|
||||
{
|
||||
@@ -25,17 +25,17 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
private readonly Task _readLoop;
|
||||
private readonly Task _writeLoop;
|
||||
|
||||
// Channel single-reader: le task scrivono i payload senza lock;
|
||||
// il write loop drena tutto in un unico WriteAsync+FlushAsync —
|
||||
// identico al buffered writer asyncio di Electrum.
|
||||
// Single-reader channel: tasks write the payloads without a lock;
|
||||
// the write loop drains everything in a single WriteAsync+FlushAsync —
|
||||
// identical to Electrum's asyncio buffered writer.
|
||||
private readonly Channel<byte[]> _outgoing = Channel.CreateUnbounded<byte[]>(
|
||||
new UnboundedChannelOptions { SingleReader = true, AllowSynchronousContinuations = false });
|
||||
|
||||
// Tetto alle richieste in volo (non in coda di scrittura, ma in attesa di
|
||||
// risposta sul server). Il write-loop batcha già l'invio in un solo segmento;
|
||||
// questo gate evita di sommergere il server con migliaia di richieste
|
||||
// simultanee su wallet grandi → niente -101/-102 a raffica né drop della
|
||||
// connessione. Le scritture restano comunque pipelinate fino a questo grado.
|
||||
// Cap on in-flight requests (not in the write queue, but awaiting a
|
||||
// response from the server). The write loop already batches the send into a
|
||||
// single segment; this gate avoids flooding the server with thousands of
|
||||
// simultaneous requests on large wallets → no bursts of -101/-102 nor
|
||||
// connection drops. Writes still stay pipelined up to this degree.
|
||||
private const int MaxInFlight = 32;
|
||||
private readonly SemaphoreSlim _inFlight = new(MaxInFlight, MaxInFlight);
|
||||
|
||||
@@ -104,8 +104,8 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
public async Task<JsonElement> RequestAsync(string method, CancellationToken ct = default,
|
||||
params object?[] parameters)
|
||||
{
|
||||
// Gate prima di mettere in volo: oltre MaxInFlight richieste in attesa
|
||||
// si attende che una risposta liberi uno slot, invece di sommergere il server.
|
||||
// Gate before going in-flight: beyond MaxInFlight pending requests
|
||||
// we wait for a response to free a slot, instead of flooding the server.
|
||||
await _inFlight.WaitAsync(ct);
|
||||
try
|
||||
{
|
||||
@@ -137,9 +137,9 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drain loop: svuota il channel in un unico buffer → un solo WriteAsync+FlushAsync
|
||||
/// per tutti i messaggi in coda. Quando N richieste sono in coda, vengono
|
||||
/// trasmesse in un singolo segmento TCP invece di N flush seriali.
|
||||
/// Drain loop: empties the channel into a single buffer → one WriteAsync+FlushAsync
|
||||
/// for all queued messages. When N requests are queued, they are
|
||||
/// transmitted in a single TCP segment instead of N serial flushes.
|
||||
/// </summary>
|
||||
private async Task WriteLoopAsync()
|
||||
{
|
||||
@@ -166,8 +166,8 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read loop con PipeReader: buffer pooled, zero allocazioni per-risposta
|
||||
/// (nessuna stringa intermedia), parsing JSON direttamente da byte span.
|
||||
/// Read loop with PipeReader: pooled buffers, zero per-response allocations
|
||||
/// (no intermediate string), JSON parsing directly from a byte span.
|
||||
/// </summary>
|
||||
private async Task ReadLoopAsync()
|
||||
{
|
||||
@@ -190,8 +190,8 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
}
|
||||
finally
|
||||
{
|
||||
// consumed = tutto ciò che abbiamo consumato (fino all'ultimo \n)
|
||||
// examined = tutto ciò che abbiamo guardato (fino alla fine del buffer)
|
||||
// consumed = everything we have consumed (up to the last \n)
|
||||
// examined = everything we have looked at (up to the end of the buffer)
|
||||
pipe.AdvanceTo(buffer.Start, buffer.End);
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
{
|
||||
await pipe.CompleteAsync();
|
||||
foreach (var (_, tcs) in _pending)
|
||||
tcs.TrySetException(failure ?? new IOException("Connessione al server chiusa."));
|
||||
tcs.TrySetException(failure ?? new IOException("Connection to the server closed."));
|
||||
_pending.Clear();
|
||||
Disconnected?.Invoke(failure);
|
||||
}
|
||||
@@ -229,7 +229,7 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
DispatchSpan(line.FirstSpan);
|
||||
return;
|
||||
}
|
||||
// Multi-segmento (risposta molto lunga): copia su ArrayPool poi parsa.
|
||||
// Multi-segment (very long response): copy to ArrayPool then parse.
|
||||
var len = (int)line.Length;
|
||||
var buf = ArrayPool<byte>.Shared.Rent(len);
|
||||
try
|
||||
@@ -242,8 +242,8 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
|
||||
private void DispatchSpan(ReadOnlySpan<byte> utf8)
|
||||
{
|
||||
// JsonDocument.Parse via Utf8JsonReader: nessuna stringa intermedia,
|
||||
// parsing direttamente dallo span pooled.
|
||||
// JsonDocument.Parse via Utf8JsonReader: no intermediate string,
|
||||
// parsing directly from the pooled span.
|
||||
var reader = new Utf8JsonReader(utf8);
|
||||
using var doc = JsonDocument.ParseValue(ref reader);
|
||||
var root = doc.RootElement;
|
||||
@@ -276,12 +276,12 @@ public sealed class ElectrumClient : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Errore restituito dal server (campo "error" della risposta JSON-RPC).</summary>
|
||||
/// <summary>Error returned by the server ("error" field of the JSON-RPC response).</summary>
|
||||
public sealed class ElectrumServerException(string error) : Exception(error);
|
||||
|
||||
/// <summary>
|
||||
/// Il certificato del server è cambiato rispetto a quello salvato (TOFU, §9).
|
||||
/// Si sblocca con il reset esplicito dei certificati.
|
||||
/// The server certificate changed with respect to the saved one (TOFU, §9).
|
||||
/// It is unlocked with an explicit reset of the certificates.
|
||||
/// </summary>
|
||||
public sealed class CertificatePinMismatchException(string host, int port) : Exception(
|
||||
$"Il certificato TLS di {host}:{port} è cambiato rispetto a quello salvato. " +
|
||||
|
||||
@@ -3,7 +3,7 @@ using PalladiumWallet.Core.Chain;
|
||||
|
||||
namespace PalladiumWallet.Core.Net;
|
||||
|
||||
/// <summary>Server di indicizzazione noto (bootstrap o scoperto dai peer).</summary>
|
||||
/// <summary>Known indexing server (bootstrap or discovered from peers).</summary>
|
||||
public sealed record KnownServer(string Host, int TcpPort, int SslPort, string? Version = null)
|
||||
{
|
||||
public int PortFor(bool useSsl) => useSsl ? SslPort : TcpPort;
|
||||
@@ -13,9 +13,9 @@ public sealed record KnownServer(string Host, int TcpPort, int SslPort, string?
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registro dei server (blueprint §9): parte dai bootstrap del profilo (§3),
|
||||
/// si arricchisce con i peer annunciati via server.peers.subscribe e persiste
|
||||
/// le scoperte su file per le sessioni successive.
|
||||
/// Server registry (blueprint §9): starts from the profile bootstrap list (§3),
|
||||
/// is enriched with peers announced via server.peers.subscribe, and persists
|
||||
/// discovered servers to file for subsequent sessions.
|
||||
/// </summary>
|
||||
public sealed class ServerRegistry
|
||||
{
|
||||
@@ -42,7 +42,7 @@ public sealed class ServerRegistry
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// File corrotto: si riparte dai soli bootstrap.
|
||||
// Corrupted file: fall back to bootstrap servers only.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,15 +52,15 @@ public sealed class ServerRegistry
|
||||
get { lock (_lock) return [.. _servers]; }
|
||||
}
|
||||
|
||||
/// <summary>Primo server noto: default quando l'utente non ne indica uno.</summary>
|
||||
/// <summary>First known server: default when the user does not specify one.</summary>
|
||||
public KnownServer? Default
|
||||
{
|
||||
get { lock (_lock) return _servers.FirstOrDefault(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chiede al server connesso i peer che annuncia e integra i nuovi nel
|
||||
/// registro (porte mancanti → default del profilo). Ritorna quanti sono nuovi.
|
||||
/// Queries the connected server for its announced peers and adds new ones to the
|
||||
/// registry (missing ports fall back to the profile defaults). Returns the count of new entries.
|
||||
/// </summary>
|
||||
public async Task<int> DiscoverAsync(ElectrumClient client, CancellationToken ct = default)
|
||||
{
|
||||
@@ -95,7 +95,7 @@ public sealed class ServerRegistry
|
||||
private void Save()
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
|
||||
// Si salvano solo gli scoperti: i bootstrap vengono dal profilo.
|
||||
// Only discovered servers are saved; bootstrap servers come from the profile.
|
||||
var discovered = _servers
|
||||
.Where(s => !_profile.BootstrapServers.Any(b =>
|
||||
string.Equals(b.Host, s.Host, StringComparison.OrdinalIgnoreCase)))
|
||||
|
||||
@@ -5,8 +5,8 @@ using PalladiumWallet.Core.Chain;
|
||||
namespace PalladiumWallet.Core.Spv;
|
||||
|
||||
/// <summary>
|
||||
/// Header di blocco parsato dagli 80 byte canonici (blueprint §7.2):
|
||||
/// versione, prev_hash, merkle_root, timestamp, bits, nonce.
|
||||
/// Block header parsed from the canonical 80 bytes (blueprint §7.2):
|
||||
/// version, prev_hash, merkle_root, timestamp, bits, nonce.
|
||||
/// </summary>
|
||||
public sealed record BlockHeaderInfo(
|
||||
int Version, uint256 PrevHash, uint256 MerkleRoot, uint Timestamp, uint Bits, uint Nonce, uint256 Hash)
|
||||
@@ -18,7 +18,7 @@ public sealed record BlockHeaderInfo(
|
||||
public static BlockHeaderInfo Parse(byte[] raw)
|
||||
{
|
||||
if (raw.Length != Size)
|
||||
throw new ArgumentException($"Header di {raw.Length} byte (attesi {Size}).", nameof(raw));
|
||||
throw new ArgumentException($"Header is {raw.Length} bytes (expected {Size}).", nameof(raw));
|
||||
|
||||
return new BlockHeaderInfo(
|
||||
Version: BitConverter.ToInt32(raw, 0),
|
||||
@@ -31,11 +31,10 @@ public sealed record BlockHeaderInfo(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validazione SPV dell'header (§7.2): collegamento al precedente e,
|
||||
/// se il profilo non impone lo skip (catena LWMA), verifica PoW
|
||||
/// hash <= target dai bits. Il controllo di coerenza dei bits col
|
||||
/// retargeting completo richiede la storia: per la catena di riferimento
|
||||
/// è sostituito dai checkpoint (§7.3).
|
||||
/// SPV header validation (§7.2): linkage to the previous header and,
|
||||
/// if the profile does not mandate skip (LWMA chain), PoW verification
|
||||
/// hash <= target from bits. Full bits/retargeting consistency requires
|
||||
/// history — for this chain it is replaced by checkpoints (§7.3).
|
||||
/// </summary>
|
||||
public bool IsValidChild(uint256 expectedPrevHash, ChainProfile profile)
|
||||
{
|
||||
@@ -47,7 +46,7 @@ public sealed record BlockHeaderInfo(
|
||||
return Hash <= target;
|
||||
}
|
||||
|
||||
/// <summary>Confronto con un checkpoint hardcoded (§7.3).</summary>
|
||||
/// <summary>Comparison against a hardcoded checkpoint (§7.3).</summary>
|
||||
public bool MatchesCheckpoint(Checkpoint checkpoint) =>
|
||||
Hash == uint256.Parse(checkpoint.BlockHash);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ using NBitcoin;
|
||||
namespace PalladiumWallet.Core.Spv;
|
||||
|
||||
/// <summary>
|
||||
/// Scripthash del protocollo di indicizzazione (blueprint §0/§10): SHA-256
|
||||
/// dello scriptPubKey con i byte in ordine inverso, esadecimale.
|
||||
/// Script hash for the indexing protocol (blueprint §0/§10): SHA-256 of the
|
||||
/// scriptPubKey with bytes reversed, encoded as lowercase hex.
|
||||
/// </summary>
|
||||
public static class Scripthash
|
||||
{
|
||||
|
||||
@@ -8,14 +8,14 @@ using PalladiumWallet.Core.Storage;
|
||||
|
||||
namespace PalladiumWallet.Core.Spv;
|
||||
|
||||
/// <summary>Indirizzo derivato e tracciato durante la sincronizzazione.</summary>
|
||||
/// <summary>Address derived and tracked during synchronisation.</summary>
|
||||
public sealed record TrackedAddress(
|
||||
BitcoinAddress Address, string ScriptHash, bool IsChange, int Index)
|
||||
{
|
||||
public Script ScriptPubKey => Address.ScriptPubKey;
|
||||
}
|
||||
|
||||
/// <summary>Esito di una passata di sincronizzazione.</summary>
|
||||
/// <summary>Result of a synchronisation pass.</summary>
|
||||
public sealed class SyncResult
|
||||
{
|
||||
public required int TipHeight { get; init; }
|
||||
@@ -31,26 +31,26 @@ public sealed class SyncResult
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sincronizzazione del wallet (blueprint §7.4).
|
||||
/// Wallet synchronisation (blueprint §7.4).
|
||||
/// </summary>
|
||||
public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient client, int gapLimit = 20)
|
||||
{
|
||||
/// <summary>Avanzamento leggibile (per CLI e barra di stato GUI).</summary>
|
||||
/// <summary>Human-readable progress (for CLI and GUI status bar).</summary>
|
||||
public event Action<string>? Progress;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Transaction> _txCache = new();
|
||||
private readonly Dictionary<string, int> _verifiedAtHeight = [];
|
||||
private readonly ConcurrentDictionary<int, Task<string>> _headerFetches = new();
|
||||
|
||||
// Indici noti dal sync precedente: usati da ScanChainAsync per la discovery
|
||||
// incrementale — gli indirizzi già usati vengono fetchati in un unico burst
|
||||
// invece di batches sequenziali, riducendo i round-trip da O(used/gapLimit) a O(1).
|
||||
// Indices known from the previous sync: used by ScanChainAsync for incremental
|
||||
// discovery — already-used addresses are fetched in a single burst instead of
|
||||
// sequential batches, reducing round-trips from O(used/gapLimit) to O(1).
|
||||
private int _knownReceiveIndex;
|
||||
private int _knownChangeIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-popola le cache interne da dati salvati su disco.
|
||||
/// Chiamare prima di SyncOnceAsync per evitare di riscaricale le tx già note.
|
||||
/// Pre-populates internal caches from data saved on disk.
|
||||
/// Call before SyncOnceAsync to avoid re-downloading already known transactions.
|
||||
/// </summary>
|
||||
public void PreloadCaches(
|
||||
Dictionary<string, string> rawTxHex,
|
||||
@@ -73,9 +73,9 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Esporta le cache correnti in forma serializzabile su disco.
|
||||
/// Solo le tx confermate (height > 0) vengono incluse: le non confermate
|
||||
/// possono cambiare (RBF) e vanno sempre riscaricate.
|
||||
/// Exports the current caches in a serialisable form for disk storage.
|
||||
/// Only confirmed transactions (height > 0) are included: unconfirmed ones
|
||||
/// may change (RBF) and must always be re-downloaded.
|
||||
/// </summary>
|
||||
public (Dictionary<string, string> RawTxHex,
|
||||
Dictionary<string, int> VerifiedAt,
|
||||
@@ -86,8 +86,8 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
.Where(_txCache.ContainsKey)
|
||||
.ToDictionary(txid => txid, txid => _txCache[txid].ToHex());
|
||||
|
||||
// Solo gli header già completati: Task<string> non ancora completate
|
||||
// non vengono persistite (verranno rifetchate al prossimo sync se necessario).
|
||||
// Only already-completed headers: in-progress Task<string> instances
|
||||
// are not persisted (they will be re-fetched on the next sync if needed).
|
||||
var headers = new Dictionary<int, string>();
|
||||
foreach (var (height, task) in _headerFetches)
|
||||
if (task.IsCompletedSuccessfully)
|
||||
@@ -99,9 +99,9 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
public async Task<SyncResult> SyncOnceAsync(CancellationToken ct = default)
|
||||
{
|
||||
var tip = await client.SubscribeHeadersAsync(ct);
|
||||
Progress?.Invoke($"tip della catena: {tip.Height}");
|
||||
Progress?.Invoke($"chain tip: {tip.Height}");
|
||||
|
||||
// 1-2. Scansione indirizzi.
|
||||
// 1-2. Address scanning.
|
||||
var tracked = new List<TrackedAddress>();
|
||||
var historyByAddress = new Dictionary<string, IReadOnlyList<HistoryItem>>();
|
||||
int nextReceive, nextChange;
|
||||
@@ -122,9 +122,9 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
}
|
||||
else
|
||||
{
|
||||
// Receive e change chain in parallelo (indipendenti per definizione).
|
||||
// ScanChainAsync usa _knownReceiveIndex/_knownChangeIndex per la discovery
|
||||
// incrementale: gli indirizzi già usati vengono fetchati in un burst unico.
|
||||
// Receive and change chains in parallel (independent by definition).
|
||||
// ScanChainAsync uses _knownReceiveIndex/_knownChangeIndex for incremental
|
||||
// discovery: already-used addresses are fetched in a single burst.
|
||||
var receiveTask = ScanChainAsync(isChange: false, _knownReceiveIndex, ct);
|
||||
var changeTask = ScanChainAsync(isChange: true, _knownChangeIndex, ct);
|
||||
var rxScan = await receiveTask;
|
||||
@@ -145,12 +145,12 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
RetryOnBusyAsync(() => client.SubscribeScripthashAsync(t.ScriptHash, ct), ct)));
|
||||
}
|
||||
|
||||
// 3. Storico unico (txid → altezza massima riportata).
|
||||
// 3. Merged history (txid → highest reported height).
|
||||
var txHeights = new Dictionary<string, int>();
|
||||
foreach (var item in historyByAddress.Values.SelectMany(h => h))
|
||||
txHeights[item.TxHash] = item.Height;
|
||||
|
||||
// 4+5. Download tx mancanti e verifica Merkle in parallelo senza semaforo.
|
||||
// 4+5. Download missing transactions and verify Merkle proofs in parallel.
|
||||
var network = PalladiumNetworks.For(account.Profile.Kind);
|
||||
var missing = txHeights.Keys.Where(txid => !_txCache.ContainsKey(txid)).ToList();
|
||||
var toVerify = txHeights
|
||||
@@ -160,7 +160,7 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
|
||||
if (missing.Count > 0 || toVerify.Count > 0)
|
||||
{
|
||||
Progress?.Invoke($"scarico {missing.Count} tx, verifico {toVerify.Count} prove…");
|
||||
Progress?.Invoke($"downloading {missing.Count} txs, verifying {toVerify.Count} proofs…");
|
||||
var dlDone = 0;
|
||||
var merkDone = 0;
|
||||
|
||||
@@ -170,7 +170,7 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
_txCache[txid] = Transaction.Parse(raw, network);
|
||||
var n = Interlocked.Increment(ref dlDone);
|
||||
if (n % 50 == 0 || n == missing.Count)
|
||||
Progress?.Invoke($"tx {n}/{missing.Count}, prove {merkDone}/{toVerify.Count}…");
|
||||
Progress?.Invoke($"tx {n}/{missing.Count}, proofs {merkDone}/{toVerify.Count}…");
|
||||
}, ct));
|
||||
|
||||
var merkTasks = toVerify.Select(kv => RetryOnBusyAsync(async () =>
|
||||
@@ -185,10 +185,10 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
uint256.Parse(txid), proof.Pos,
|
||||
proof.Merkle.Select(uint256.Parse), header.MerkleRoot))
|
||||
throw new SpvVerificationException(
|
||||
$"Prova di Merkle non valida per {txid} (blocco {height}): server non affidabile.");
|
||||
$"Invalid Merkle proof for {txid} (block {height}): server is not trustworthy.");
|
||||
var n = Interlocked.Increment(ref merkDone);
|
||||
if (n % 50 == 0 || n == toVerify.Count)
|
||||
Progress?.Invoke($"tx {dlDone}/{missing.Count}, prove {n}/{toVerify.Count}…");
|
||||
Progress?.Invoke($"tx {dlDone}/{missing.Count}, proofs {n}/{toVerify.Count}…");
|
||||
}, ct));
|
||||
|
||||
await Task.WhenAll(dlTasks.Concat(merkTasks));
|
||||
@@ -199,7 +199,7 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
var transactions = txHeights.Keys.ToDictionary(txid => txid, txid => _txCache[txid]);
|
||||
var verified = txHeights.ToDictionary(kv => kv.Key, kv => kv.Value > 0);
|
||||
|
||||
// 6. Ricostruzione locale degli UTXO.
|
||||
// 6. Local UTXO reconstruction.
|
||||
var byScript = tracked.ToDictionary(t => t.ScriptPubKey, t => t);
|
||||
var spent = transactions.Values
|
||||
.SelectMany(tx => tx.Inputs)
|
||||
@@ -229,7 +229,7 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Delta per voce di storico.
|
||||
// 7. Delta per history entry.
|
||||
var history = new List<CachedTx>();
|
||||
foreach (var (txid, tx) in transactions)
|
||||
{
|
||||
@@ -286,14 +286,14 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scansiona una catena (receiving o change).
|
||||
/// Scans one chain (receiving or change).
|
||||
///
|
||||
/// Phase 1 — indirizzi noti (0..fromIndex-1): tutti i GetHistoryAsync partono
|
||||
/// in un unico burst parallelo, senza batching sequenziale. Per un wallet con
|
||||
/// 100 indirizzi usati → 1 RTT invece di 5 round sequenziali di gapLimit.
|
||||
/// Phase 1 — known addresses (0..fromIndex-1): all GetHistoryAsync calls are
|
||||
/// fired in a single parallel burst, with no sequential batching. A wallet
|
||||
/// with 100 used addresses costs 1 RTT instead of 5 sequential gap-limit rounds.
|
||||
///
|
||||
/// Phase 2 — discovery dal fromIndex in poi: batching con gap limit come prima,
|
||||
/// necessario per sapere dove fermarsi.
|
||||
/// Phase 2 — discovery from fromIndex onwards: gap-limit batching as before,
|
||||
/// required to know when to stop.
|
||||
/// </summary>
|
||||
private async Task<(int NextIndex,
|
||||
List<TrackedAddress> Tracked,
|
||||
@@ -303,7 +303,7 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
var tracked = new List<TrackedAddress>();
|
||||
var history = new Dictionary<string, IReadOnlyList<HistoryItem>>();
|
||||
|
||||
// Phase 1: burst unico per tutti gli indirizzi già noti.
|
||||
// Phase 1: single burst for all already-known addresses.
|
||||
if (fromIndex > 0)
|
||||
{
|
||||
var known = Enumerable.Range(0, fromIndex).Select(i =>
|
||||
@@ -321,7 +321,7 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
history[known[i].ScriptHash] = knownHistories[i];
|
||||
}
|
||||
|
||||
// Phase 2: discovery gap-limit dal fromIndex in poi.
|
||||
// Phase 2: gap-limit discovery from fromIndex onwards.
|
||||
var consecutiveEmpty = 0;
|
||||
var index = fromIndex;
|
||||
var firstUnused = fromIndex;
|
||||
@@ -395,5 +395,5 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
||||
ex.Message.Contains("excessive resource usage", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>La verifica SPV è fallita: i dati del server contraddicono le prove (§17).</summary>
|
||||
/// <summary>SPV verification failed: server data contradicts the proofs (§17).</summary>
|
||||
public sealed class SpvVerificationException(string message) : Exception(message);
|
||||
|
||||
@@ -3,16 +3,16 @@ using System.Text.Json;
|
||||
namespace PalladiumWallet.Core.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Configurazione globale dell'applicazione (blueprint §8), separata dai file
|
||||
/// wallet: lingua, unità di visualizzazione, ecc. Persistita in config.json
|
||||
/// nella radice dei dati (vale per tutte le reti).
|
||||
/// Global application configuration (blueprint §8), separate from wallet
|
||||
/// files: language, display unit, etc. Persisted in config.json
|
||||
/// in the data root (applies to all networks).
|
||||
/// </summary>
|
||||
public sealed class AppConfig
|
||||
{
|
||||
/// <summary>Codice lingua UI.</summary>
|
||||
/// <summary>UI language code.</summary>
|
||||
public string Language { get; set; } = "en";
|
||||
|
||||
/// <summary>Unità di visualizzazione degli importi (vedi <see cref="Wallet.CoinAmount.Units"/>).</summary>
|
||||
/// <summary>Display unit for amounts (see <see cref="Wallet.CoinAmount.Units"/>).</summary>
|
||||
public string Unit { get; set; } = "PLM";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
@@ -28,7 +28,7 @@ public sealed class AppConfig
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Config corrotta: si riparte dai default senza bloccare l'avvio.
|
||||
// Corrupted config: fall back to defaults without blocking startup.
|
||||
return new AppConfig();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,29 +3,29 @@ using PalladiumWallet.Core.Chain;
|
||||
namespace PalladiumWallet.Core.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Percorsi dati per piattaforma (blueprint §8). La radice dati può essere:
|
||||
/// 1. <b>portable</b>: cartella "palladium-data" accanto all'eseguibile;
|
||||
/// 2. <b>personalizzata</b>: scelta dall'utente al primo avvio e memorizzata in
|
||||
/// un piccolo file "puntatore" in una posizione di bootstrap fissa;
|
||||
/// 3. <b>legacy</b>: vecchia posizione (%APPDATA%/PalladiumWallet) se contiene già dati;
|
||||
/// 4. <b>default</b>: ~/.PalladiumWallet (Linux/macOS) o %ProgramFiles%\PalladiumWallet (Windows).
|
||||
/// Sotto la radice c'è una sottocartella per rete (config, wallet, header, certificati).
|
||||
/// Per-platform data paths (blueprint §8). The data root can be:
|
||||
/// 1. <b>portable</b>: "palladium-data" folder next to the executable;
|
||||
/// 2. <b>custom</b>: chosen by the user at first launch and stored in
|
||||
/// a small "pointer" file at a fixed bootstrap location;
|
||||
/// 3. <b>legacy</b>: old location (%APPDATA%/PalladiumWallet) if it already holds data;
|
||||
/// 4. <b>default</b>: ~/.PalladiumWallet (Linux/macOS) or %ProgramFiles%\PalladiumWallet (Windows).
|
||||
/// Under the root there is a per-network subfolder (config, wallet, header, certificates).
|
||||
/// </summary>
|
||||
public static class AppPaths
|
||||
{
|
||||
public const string PortableDirName = "palladium-data";
|
||||
|
||||
/// <summary>Nome cartella applicazione, usato nei vari percorsi.</summary>
|
||||
/// <summary>Application folder name, used in the various paths.</summary>
|
||||
public const string AppDirName = "PalladiumWallet";
|
||||
|
||||
/// <summary>Override esplicito della radice dati (es. CLI --data-dir). Ha priorità su tutto.</summary>
|
||||
/// <summary>Explicit override of the data root (e.g. CLI --data-dir). Takes priority over everything.</summary>
|
||||
public static string? OverrideDataRoot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Radice dati predefinita, secondo la convenzione di ogni piattaforma:
|
||||
/// Windows → %APPDATA%\PalladiumWallet (PascalCase, come Electrum/Bitcoin);
|
||||
/// Linux/macOS → ~/.palladium-wallet (dotfolder minuscolo, come ~/.bitcoin).
|
||||
/// Per-utente e sempre scrivibile, senza privilegi di amministratore.
|
||||
/// Default data root, following each platform's convention:
|
||||
/// Windows → %APPDATA%\PalladiumWallet (PascalCase, like Electrum/Bitcoin);
|
||||
/// Linux/macOS → ~/.palladium-wallet (lowercase dotfolder, like ~/.bitcoin).
|
||||
/// Per-user and always writable, without administrator privileges.
|
||||
/// </summary>
|
||||
public static string DefaultDataRoot()
|
||||
{
|
||||
@@ -37,8 +37,8 @@ public static class AppPaths
|
||||
return Path.Combine(home, ".palladium-wallet");
|
||||
}
|
||||
|
||||
/// <summary>File puntatore alla radice dati scelta dall'utente. Vive in una
|
||||
/// posizione di bootstrap sempre scrivibile e indipendente dalla radice dati.</summary>
|
||||
/// <summary>Pointer file to the user-chosen data root. Lives at a
|
||||
/// bootstrap location that is always writable and independent of the data root.</summary>
|
||||
private static string LocationPointerPath() =>
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
@@ -50,7 +50,7 @@ public static class AppPaths
|
||||
private static bool HasData(string root) =>
|
||||
Directory.Exists(root) && Directory.EnumerateFileSystemEntries(root).Any();
|
||||
|
||||
/// <summary>Radice dati effettiva, secondo l'ordine di precedenza documentato in classe.</summary>
|
||||
/// <summary>Effective data root, following the precedence order documented on the class.</summary>
|
||||
public static string DataRoot()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(OverrideDataRoot))
|
||||
@@ -67,9 +67,9 @@ public static class AppPaths
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 predefinita.
|
||||
/// true if the data location is already determined and need not be asked
|
||||
/// of the user: portable mode, override, pointer already written, or
|
||||
/// data already present at the default location.
|
||||
/// </summary>
|
||||
public static bool IsDataLocationConfigured() =>
|
||||
!string.IsNullOrEmpty(OverrideDataRoot)
|
||||
@@ -77,7 +77,7 @@ public static class AppPaths
|
||||
|| ReadPointer() is not null
|
||||
|| HasData(DefaultDataRoot());
|
||||
|
||||
/// <summary>Memorizza la radice dati scelta dall'utente e la crea su disco.</summary>
|
||||
/// <summary>Stores the user-chosen data root and creates it on disk.</summary>
|
||||
public static void ConfigureDataLocation(string root)
|
||||
{
|
||||
root = Path.GetFullPath(root.Trim());
|
||||
@@ -96,7 +96,7 @@ public static class AppPaths
|
||||
return string.IsNullOrEmpty(path) ? null : path;
|
||||
}
|
||||
|
||||
/// <summary>Cartella dati della rete (config, wallet, header, certificati).</summary>
|
||||
/// <summary>Network data folder (config, wallet, header, certificates).</summary>
|
||||
public static string ForNetwork(NetKind net)
|
||||
{
|
||||
var dir = Path.Combine(DataRoot(), ChainProfiles.For(net).NetName);
|
||||
@@ -114,7 +114,7 @@ public static class AppPaths
|
||||
public static string DefaultWalletPath(NetKind net) =>
|
||||
Path.Combine(WalletsDir(net), "default.wallet.json");
|
||||
|
||||
/// <summary>Tutti i file wallet della rete, ordinati per nome (multi-wallet §8).</summary>
|
||||
/// <summary>All wallet files for the network, ordered by name (multi-wallet §8).</summary>
|
||||
public static IReadOnlyList<string> WalletFiles(NetKind net)
|
||||
{
|
||||
var dir = WalletsDir(net);
|
||||
|
||||
@@ -5,9 +5,9 @@ using System.Text.Json;
|
||||
namespace PalladiumWallet.Core.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Cifratura del file wallet (blueprint §8/§17): AES-256-GCM con chiave derivata
|
||||
/// dalla password via PBKDF2-HMAC-SHA512. Il contenitore è JSON autodescrittivo
|
||||
/// (kdf, parametri, salt, nonce) per consentire upgrade futuri dei parametri.
|
||||
/// Wallet file encryption (blueprint §8/§17): AES-256-GCM with a key derived
|
||||
/// from the password via PBKDF2-HMAC-SHA512. The container is self-describing JSON
|
||||
/// (kdf, parameters, salt, nonce) to allow future parameter upgrades.
|
||||
/// </summary>
|
||||
public static class EncryptedFile
|
||||
{
|
||||
@@ -54,7 +54,7 @@ public static class EncryptedFile
|
||||
Convert.ToBase64String(tag), Convert.ToBase64String(cipher)));
|
||||
}
|
||||
|
||||
/// <summary>Lancia <see cref="WrongPasswordException"/> se la password è errata o il file è manomesso.</summary>
|
||||
/// <summary>Throws <see cref="WrongPasswordException"/> if the password is wrong or the file is tampered with.</summary>
|
||||
public static string Decrypt(string fileContent, string password)
|
||||
{
|
||||
var container = JsonSerializer.Deserialize<Container>(fileContent)
|
||||
@@ -74,7 +74,7 @@ public static class EncryptedFile
|
||||
}
|
||||
catch (AuthenticationTagMismatchException)
|
||||
{
|
||||
// Il tag GCM autentica: password errata e manomissione sono indistinguibili.
|
||||
// The GCM tag authenticates: a wrong password and tampering are indistinguishable.
|
||||
throw new WrongPasswordException();
|
||||
}
|
||||
finally
|
||||
@@ -88,7 +88,7 @@ public static class EncryptedFile
|
||||
Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA512, KeySize);
|
||||
}
|
||||
|
||||
/// <summary>Password errata (o file wallet manomesso: il tag GCM non distingue i due casi).</summary>
|
||||
/// <summary>Wrong password (or tampered wallet file: the GCM tag does not distinguish the two cases).</summary>
|
||||
public sealed class WrongPasswordException : Exception
|
||||
{
|
||||
public WrongPasswordException() : base("Password errata o file wallet danneggiato.") { }
|
||||
|
||||
@@ -4,9 +4,9 @@ using System.Text.Json.Serialization;
|
||||
namespace PalladiumWallet.Core.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Schema del file wallet (blueprint §8), versionato per consentire migrazioni
|
||||
/// automatiche all'apertura. La cifratura (quando c'è una password) avvolge
|
||||
/// l'intero documento via <see cref="EncryptedFile"/>.
|
||||
/// Wallet file schema (blueprint §8), versioned to allow automatic
|
||||
/// migrations on opening. Encryption (when there is a password) wraps
|
||||
/// the entire document via <see cref="EncryptedFile"/>.
|
||||
/// </summary>
|
||||
public sealed class WalletDocument
|
||||
{
|
||||
@@ -14,42 +14,42 @@ public sealed class WalletDocument
|
||||
|
||||
public int Version { get; set; } = CurrentVersion;
|
||||
|
||||
/// <summary>Nome rete (mainnet/testnet/regtest), come ChainProfile.NetName.</summary>
|
||||
/// <summary>Network name (mainnet/testnet/regtest), as ChainProfile.NetName.</summary>
|
||||
public required string Network { get; set; }
|
||||
|
||||
/// <summary>Tipo di script (nome dell'enum ScriptKind).</summary>
|
||||
/// <summary>Script type (name of the ScriptKind enum).</summary>
|
||||
public required string ScriptKind { get; set; }
|
||||
|
||||
/// <summary>Mnemonica BIP39 in chiaro nel documento (il documento va cifrato!); null se watch-only.</summary>
|
||||
/// <summary>BIP39 mnemonic in plaintext in the document (the document must be encrypted!); null if watch-only.</summary>
|
||||
public string? Mnemonic { get; set; }
|
||||
|
||||
/// <summary>Extension word BIP39 (§4.1); null se assente.</summary>
|
||||
/// <summary>BIP39 extension word (§4.1); null if absent.</summary>
|
||||
public string? Passphrase { get; set; }
|
||||
|
||||
/// <summary>Path di account (es. "84'/746'/0'"); null per importati WIF.</summary>
|
||||
/// <summary>Account path (e.g. "84'/746'/0'"); null for imported WIF.</summary>
|
||||
public string? AccountPath { get; set; }
|
||||
|
||||
/// <summary>Xpub di account in SLIP-132 per i wallet HD; null per importati WIF.</summary>
|
||||
/// <summary>Account xpub in SLIP-132 for HD wallets; null for imported WIF.</summary>
|
||||
public string? AccountXpub { get; set; }
|
||||
|
||||
/// <summary>Xprv di account in SLIP-132; presente solo per import da xprv senza seed.</summary>
|
||||
/// <summary>Account xprv in SLIP-132; present only for import from xprv without seed.</summary>
|
||||
public string? AccountXprv { get; set; }
|
||||
|
||||
public string? MasterFingerprint { get; set; }
|
||||
|
||||
/// <summary>Chiavi WIF importate (in chiaro nel documento — va cifrato!).</summary>
|
||||
/// <summary>Imported WIF keys (in plaintext in the document — must be encrypted!).</summary>
|
||||
public List<string>? WifKeys { get; set; }
|
||||
|
||||
/// <summary>Gap limit per la scansione indirizzi (§5), configurabile.</summary>
|
||||
/// <summary>Gap limit for address scanning (§5), configurable.</summary>
|
||||
public int GapLimit { get; set; } = 20;
|
||||
|
||||
/// <summary>Etichette per indirizzo/txid (§12).</summary>
|
||||
/// <summary>Labels by address/txid (§12).</summary>
|
||||
public Dictionary<string, string> Labels { get; set; } = [];
|
||||
|
||||
/// <summary>Rubrica contatti (nome + indirizzo blockchain).</summary>
|
||||
/// <summary>Contact address book (name + blockchain address).</summary>
|
||||
public List<StoredContact> Contacts { get; set; } = [];
|
||||
|
||||
/// <summary>Cache dell'ultimo stato sincronizzato (saldo/storico mostrabili offline).</summary>
|
||||
/// <summary>Cache of the last synced state (balance/history viewable offline).</summary>
|
||||
public SyncCache? Cache { get; set; }
|
||||
|
||||
public bool IsWatchOnly =>
|
||||
@@ -69,6 +69,7 @@ public sealed class WalletDocument
|
||||
{
|
||||
var doc = JsonSerializer.Deserialize<WalletDocument>(json, JsonOptions)
|
||||
?? throw new InvalidDataException("File wallet non valido.");
|
||||
// string literal above intentionally left untranslated
|
||||
return Migrate(doc);
|
||||
}
|
||||
|
||||
@@ -101,28 +102,28 @@ public sealed class SyncCache
|
||||
public List<CachedAddress> Addresses { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Cache raw delle transazioni confermate (txid → hex). Evita di riscaricale
|
||||
/// ad ogni avvio dell'app: le tx confermate sono immutabili per definizione.
|
||||
/// Popolata anche parzialmente in caso di sync interrotta (es. -101):
|
||||
/// il synchronizer riprende dal punto in cui si era fermato.
|
||||
/// Raw transaction cache (txid → hex). Avoids re-downloading confirmed
|
||||
/// transactions on every launch: confirmed txs are immutable by definition.
|
||||
/// May be partially populated after an interrupted sync (e.g. -101 error);
|
||||
/// the synchronizer resumes from where it left off.
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? RawTxHex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Prove di Merkle già verificate (txid → altezza blocco). Evita di
|
||||
/// riverificare le stesse prove ad ogni avvio: le conferme sono immutabili.
|
||||
/// Already-verified Merkle proofs (txid → block height). Avoids re-verifying
|
||||
/// the same proofs on every launch: confirmations are immutable.
|
||||
/// </summary>
|
||||
public Dictionary<string, int>? VerifiedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Header grezzi per altezza (altezza → hex). Immutabili: non vengono mai
|
||||
/// rifetchati una volta salvati. Elimina i GetBlockHeader sulle prove Merkle
|
||||
/// già verificate nei sync successivi.
|
||||
/// Raw headers by height (height → hex). Immutable: never re-fetched once saved.
|
||||
/// Eliminates GetBlockHeader calls for Merkle proofs already verified in
|
||||
/// subsequent syncs.
|
||||
/// </summary>
|
||||
public Dictionary<int, string>? BlockHeaders { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Indirizzo scansionato con saldo proprio e numero di transazioni (vista indirizzi).</summary>
|
||||
/// <summary>Scanned address with its own balance and transaction count (address view).</summary>
|
||||
public sealed class CachedAddress
|
||||
{
|
||||
public required string Address { get; set; }
|
||||
|
||||
@@ -3,17 +3,17 @@ using System.Globalization;
|
||||
namespace PalladiumWallet.Core.Wallet;
|
||||
|
||||
/// <summary>
|
||||
/// Conversione satoshi ↔ unità coin (8 decimali) per visualizzazione e input.
|
||||
/// Si lavora sempre in satoshi internamente; la stringa è solo presentazione.
|
||||
/// Satoshi ↔ coin unit conversion (8 decimal places) for display and input.
|
||||
/// All internal computation uses satoshis; the formatted string is presentation only.
|
||||
/// </summary>
|
||||
public static class CoinAmount
|
||||
{
|
||||
public const long SatsPerCoin = 100_000_000;
|
||||
|
||||
/// <summary>Unità di visualizzazione selezionabili (config §8).</summary>
|
||||
/// <summary>Selectable display units (config §8).</summary>
|
||||
public static readonly string[] Units = ["PLM", "mPLM", "µPLM", "sat"];
|
||||
|
||||
/// <summary>(satoshi per unità, decimali mostrati) di ciascuna unità.</summary>
|
||||
/// <summary>(satoshis per unit, displayed decimals) for each unit.</summary>
|
||||
private static (long Factor, int Decimals) Of(string unit) => unit switch
|
||||
{
|
||||
"PLM" => (SatsPerCoin, 8),
|
||||
@@ -27,7 +27,7 @@ public static class CoinAmount
|
||||
(sats / (decimal)SatsPerCoin).ToString("0.00000000", CultureInfo.InvariantCulture)
|
||||
+ (unit.Length > 0 ? " " + unit : "");
|
||||
|
||||
/// <summary>Formatta nell'unità scelta (es. 150000 sat → "1.50000 mPLM").</summary>
|
||||
/// <summary>Formats in the chosen unit (e.g. 150000 sat → "1.50000 mPLM").</summary>
|
||||
public static string FormatIn(long sats, string unit, bool withLabel = true)
|
||||
{
|
||||
var (factor, decimals) = Of(unit);
|
||||
@@ -36,7 +36,7 @@ public static class CoinAmount
|
||||
return withLabel ? $"{value} {unit}" : value;
|
||||
}
|
||||
|
||||
/// <summary>Parsa un importo espresso nell'unità scelta in satoshi.</summary>
|
||||
/// <summary>Parses an amount expressed in the chosen unit into satoshis.</summary>
|
||||
public static bool TryParseIn(string text, string unit, out long sats)
|
||||
{
|
||||
sats = 0;
|
||||
@@ -59,7 +59,7 @@ public static class CoinAmount
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Parsa un importo in coin (punto o virgola decimale) in satoshi.</summary>
|
||||
/// <summary>Parses a coin amount (decimal point or comma) into satoshis.</summary>
|
||||
public static bool TryParseCoins(string text, out long sats)
|
||||
{
|
||||
sats = 0;
|
||||
|
||||
@@ -6,7 +6,7 @@ using PalladiumWallet.Core.Storage;
|
||||
|
||||
namespace PalladiumWallet.Core.Wallet;
|
||||
|
||||
/// <summary>Esito della costruzione di una transazione.</summary>
|
||||
/// <summary>Result of a transaction build operation.</summary>
|
||||
public sealed class BuiltTransaction
|
||||
{
|
||||
public required Transaction Transaction { get; init; }
|
||||
@@ -14,7 +14,7 @@ public sealed class BuiltTransaction
|
||||
public required FeeRate FeeRate { get; init; }
|
||||
public required bool Signed { get; init; }
|
||||
|
||||
/// <summary>PSBT per i flussi watch-only/air-gapped/multisig (§6.5).</summary>
|
||||
/// <summary>PSBT for watch-only/air-gapped/multisig flows (§6.5).</summary>
|
||||
public required PSBT Psbt { get; init; }
|
||||
|
||||
public string ToHex() => Transaction.ToHex();
|
||||
@@ -22,25 +22,25 @@ public sealed class BuiltTransaction
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Costruzione e firma delle transazioni (blueprint §6) sopra le primitive
|
||||
/// NBitcoin: selezione monete (manuale o automatica), fee a rate fisso,
|
||||
/// invia-tutto con fee sottratta, change sulla catena interna, RBF di default.
|
||||
/// Con un account watch-only produce la PSBT non firmata (§6.5).
|
||||
/// Transaction construction and signing (blueprint §6) on top of NBitcoin primitives:
|
||||
/// coin selection (manual or automatic), fixed fee rate, send-all with fee subtracted,
|
||||
/// change on the internal chain, RBF on by default.
|
||||
/// With a watch-only account produces an unsigned PSBT (§6.5).
|
||||
/// </summary>
|
||||
public sealed class TransactionFactory(IWalletAccount account)
|
||||
{
|
||||
private Network Network => PalladiumNetworks.For(account.Profile.Kind);
|
||||
|
||||
/// <summary>
|
||||
/// Costruisce (e se possibile firma) una transazione.
|
||||
/// Builds (and signs if possible) a transaction.
|
||||
/// </summary>
|
||||
/// <param name="utxos">UTXO selezionati (coin control §6.2) o tutti quelli spendibili.</param>
|
||||
/// <param name="transactions">Tx di provenienza degli UTXO (txid → tx), dalla sincronizzazione.</param>
|
||||
/// <param name="destination">Indirizzo destinatario.</param>
|
||||
/// <param name="amountSats">Importo; ignorato se <paramref name="sendAll"/>.</param>
|
||||
/// <param name="feeRateSatPerVByte">Fee rate fisso in sat/vByte (§6.4).</param>
|
||||
/// <param name="changeIndex">Indice del prossimo indirizzo di change (catena interna).</param>
|
||||
/// <param name="sendAll">Invia tutto: fee sottratta dall'importo (§6.1).</param>
|
||||
/// <param name="utxos">Selected UTXOs (coin control §6.2) or all spendable ones.</param>
|
||||
/// <param name="transactions">Source transactions for the UTXOs (txid → tx), from sync.</param>
|
||||
/// <param name="destination">Recipient address.</param>
|
||||
/// <param name="amountSats">Amount; ignored when <paramref name="sendAll"/> is true.</param>
|
||||
/// <param name="feeRateSatPerVByte">Fixed fee rate in sat/vByte (§6.4).</param>
|
||||
/// <param name="changeIndex">Index of the next change address (internal chain).</param>
|
||||
/// <param name="sendAll">Send all: fee subtracted from the amount (§6.1).</param>
|
||||
public BuiltTransaction Build(
|
||||
IReadOnlyList<CachedUtxo> utxos,
|
||||
IReadOnlyDictionary<string, Transaction> transactions,
|
||||
@@ -50,15 +50,15 @@ public sealed class TransactionFactory(IWalletAccount account)
|
||||
int changeIndex,
|
||||
bool sendAll = false)
|
||||
{
|
||||
// Si spendono solo UTXO confermati: i fondi in mempool si vedono nel
|
||||
// saldo "in attesa" ma non sono spendibili finché non confermano.
|
||||
// Only confirmed UTXOs are spent: mempool funds appear in the "pending"
|
||||
// balance but are not spendable until confirmed.
|
||||
var spendable = utxos.Where(u => !u.Frozen && u.Height > 0).ToList();
|
||||
if (spendable.Count == 0)
|
||||
{
|
||||
var pending = utxos.Where(u => !u.Frozen && u.Height <= 0).Sum(u => u.ValueSats);
|
||||
throw new WalletSpendException(pending > 0
|
||||
? $"Nessun fondo confermato: {CoinAmount.Format(pending)} in attesa di conferma (non spendibile)."
|
||||
: "Nessun UTXO spendibile selezionato.");
|
||||
? $"No confirmed funds: {CoinAmount.Format(pending)} pending confirmation (not spendable)."
|
||||
: "No spendable UTXOs selected.");
|
||||
}
|
||||
|
||||
var coins = spendable.Select(u => new Coin(
|
||||
@@ -68,7 +68,7 @@ public sealed class TransactionFactory(IWalletAccount account)
|
||||
var feeRate = new FeeRate(Money.Satoshis(feeRateSatPerVByte * 1000m), 1000);
|
||||
var builder = Network.CreateTransactionBuilder();
|
||||
builder.SetVersion(2);
|
||||
// Sequence RBF per consentire il bump della fee (§6.6).
|
||||
// RBF sequence to allow fee bumping (§6.6).
|
||||
builder.OptInRBF = true;
|
||||
builder.AddCoins(coins);
|
||||
builder.SetChange(account.GetChangeAddress(changeIndex));
|
||||
@@ -94,14 +94,14 @@ public sealed class TransactionFactory(IWalletAccount account)
|
||||
}
|
||||
catch (NotEnoughFundsException ex)
|
||||
{
|
||||
throw new WalletSpendException($"Fondi insufficienti: {ex.Message}");
|
||||
throw new WalletSpendException($"Insufficient funds: {ex.Message}");
|
||||
}
|
||||
|
||||
if (!account.IsWatchOnly)
|
||||
{
|
||||
if (!builder.Verify(tx, out TransactionPolicyError[] errors))
|
||||
throw new WalletSpendException(
|
||||
"Transazione non valida: " + string.Join("; ", errors.Select(e => e.ToString())));
|
||||
"Invalid transaction: " + string.Join("; ", errors.Select(e => e.ToString())));
|
||||
}
|
||||
|
||||
return new BuiltTransaction
|
||||
@@ -123,5 +123,5 @@ public sealed class TransactionFactory(IWalletAccount account)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Errore di costruzione/firma della spesa (fondi, policy, parametri).</summary>
|
||||
/// <summary>Error during transaction construction/signing (funds, policy, parameters).</summary>
|
||||
public sealed class WalletSpendException(string message) : Exception(message);
|
||||
|
||||
@@ -4,36 +4,36 @@ using PalladiumWallet.Core.Spv;
|
||||
|
||||
namespace PalladiumWallet.Core.Wallet;
|
||||
|
||||
/// <summary>Un input di una transazione, con l'output speso risolto dal server.</summary>
|
||||
/// <summary>An input of a transaction, with the spent output resolved from the server.</summary>
|
||||
public sealed record TxInputInfo(
|
||||
string PrevTxid, uint PrevIndex, long? AmountSats, string? Address, bool IsMine, bool IsCoinbase);
|
||||
|
||||
/// <summary>Un output di una transazione.</summary>
|
||||
/// <summary>An output of a transaction.</summary>
|
||||
public sealed record TxOutputInfo(
|
||||
uint Index, long AmountSats, string? Address, string ScriptType, bool IsMine);
|
||||
|
||||
/// <summary>
|
||||
/// Dati completi di una transazione, assemblati interrogando il server: la tx
|
||||
/// grezza più gli output spesi dagli input (per ricavare importi, indirizzi e
|
||||
/// fee) e l'header del blocco (per la data). Tutto ciò che il protocollo
|
||||
/// ElectrumX-like (§10) permette di sapere su una transazione.
|
||||
/// Complete transaction data assembled by querying the server: the raw transaction
|
||||
/// plus the outputs spent by each input (to derive amounts, addresses, and fees)
|
||||
/// and the block header (for the timestamp). Everything the ElectrumX-like protocol
|
||||
/// (§10) can tell us about a transaction.
|
||||
/// </summary>
|
||||
public sealed class TransactionDetails
|
||||
{
|
||||
public required string Txid { get; init; }
|
||||
/// <summary>Altezza del blocco; ≤0 = ancora in mempool.</summary>
|
||||
/// <summary>Block height; ≤0 = still in mempool.</summary>
|
||||
public required int Height { get; init; }
|
||||
public required int Confirmations { get; init; }
|
||||
/// <summary>Effetto netto sul saldo del wallet (delta calcolato in sincronizzazione).</summary>
|
||||
/// <summary>Net effect on the wallet balance (delta computed during sync).</summary>
|
||||
public required long NetSats { get; init; }
|
||||
/// <summary>Fee della transazione; null se un input ha importo non risolvibile (es. coinbase).</summary>
|
||||
/// <summary>Transaction fee; null if any input amount cannot be resolved (e.g. coinbase).</summary>
|
||||
public required long? FeeSats { get; init; }
|
||||
public required int TotalSize { get; init; }
|
||||
public required int VirtualSize { get; init; }
|
||||
public required uint Version { get; init; }
|
||||
public required uint LockTime { get; init; }
|
||||
public required bool RbfSignaled { get; init; }
|
||||
/// <summary>Merkle proof verificata in sincronizzazione (§7.4).</summary>
|
||||
/// <summary>Merkle proof verified during sync (§7.4).</summary>
|
||||
public required bool Verified { get; init; }
|
||||
public required DateTimeOffset? BlockTime { get; init; }
|
||||
public required long TotalOutSats { get; init; }
|
||||
@@ -43,13 +43,13 @@ public sealed class TransactionDetails
|
||||
|
||||
public bool IsCoinbase => Inputs.Count > 0 && Inputs[0].IsCoinbase;
|
||||
public bool IsIncoming => NetSats >= 0;
|
||||
/// <summary>Importo verso destinatari esterni (output non nostri): l'importo "inviato".</summary>
|
||||
/// <summary>Amount sent to external recipients (outputs not ours): the "sent" amount.</summary>
|
||||
public long SentToOthersSats => Outputs.Where(o => !o.IsMine).Sum(o => o.AmountSats);
|
||||
public long ReceivedSats => Outputs.Where(o => o.IsMine).Sum(o => o.AmountSats);
|
||||
public double? FeeRateSatPerVb => FeeSats is { } f && VirtualSize > 0 ? (double)f / VirtualSize : null;
|
||||
/// <summary>
|
||||
/// Indirizzi della controparte: i destinatari esterni per un invio (output non
|
||||
/// nostri), i mittenti esterni per una ricezione (input non nostri).
|
||||
/// Counterparty addresses: external recipients for a send (outputs not ours),
|
||||
/// external senders for a receive (inputs not ours).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> CounterpartyAddresses => IsIncoming
|
||||
? [.. Inputs.Where(i => !i.IsMine && i.Address is not null).Select(i => i.Address!).Distinct()]
|
||||
@@ -57,9 +57,9 @@ public sealed class TransactionDetails
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recupera dal server tutti i dati di una singola transazione (blueprint §10):
|
||||
/// la transazione grezza e gli output spesi dai suoi input, per ricostruire
|
||||
/// importi, fee e indirizzi che il server non riassume.
|
||||
/// Fetches all data for a single transaction from the server (blueprint §10):
|
||||
/// the raw transaction and the outputs spent by its inputs, to reconstruct
|
||||
/// amounts, fees, and addresses that the server does not summarise.
|
||||
/// </summary>
|
||||
public static class TransactionInspector
|
||||
{
|
||||
@@ -112,10 +112,10 @@ public static class TransactionInspector
|
||||
|
||||
var rbf = tx.Inputs.Any(i => i.Sequence.IsRBF);
|
||||
|
||||
// Le transazioni degli input servono per importi/indirizzi/fee. Si
|
||||
// scaricano in parallelo (id univoci, richieste concorrenti supportate
|
||||
// da ElectrumClient): in sequenza la finestra impiegava un round-trip
|
||||
// per input. Anche l'header del blocco è recuperato in parallelo.
|
||||
// Input transactions are needed for amounts/addresses/fees. Fetched in
|
||||
// parallel (unique ids, concurrent requests supported by ElectrumClient):
|
||||
// sequential fetching costs one round-trip per input. The block header
|
||||
// is also fetched in parallel.
|
||||
var prevTxids = tx.IsCoinBase
|
||||
? []
|
||||
: tx.Inputs.Select(i => i.PrevOut.Hash.ToString()).Distinct().ToList();
|
||||
|
||||
@@ -6,8 +6,8 @@ using PalladiumWallet.Core.Storage;
|
||||
namespace PalladiumWallet.Core.Wallet;
|
||||
|
||||
/// <summary>
|
||||
/// Factory dei tipi di wallet (blueprint §4.5): dal documento ricostruisce il tipo
|
||||
/// di account corretto (HD da seed, HD da xprv importata, importato WIF, watch-only).
|
||||
/// Wallet type factory (blueprint §4.5): reconstructs the correct account type
|
||||
/// from a document (HD from seed, HD from imported xprv, imported WIF, watch-only).
|
||||
/// </summary>
|
||||
public static class WalletLoader
|
||||
{
|
||||
@@ -20,25 +20,25 @@ public static class WalletLoader
|
||||
var kind = Enum.Parse<ScriptKind>(doc.ScriptKind);
|
||||
var network = PalladiumNetworks.For(profile.Kind);
|
||||
|
||||
// 1. HD da seed (caso più comune)
|
||||
// 1. HD from seed (most common case)
|
||||
if (doc.Mnemonic is { } words)
|
||||
{
|
||||
if (!Bip39.TryParse(words, out var mnemonic))
|
||||
throw new InvalidDataException("Mnemonica del file wallet non valida.");
|
||||
throw new InvalidDataException("Invalid mnemonic in wallet file.");
|
||||
var path = KeyPath.Parse(doc.AccountPath ?? DerivationPaths.AccountPath(kind, profile).ToString());
|
||||
return HdAccount.FromSeed(Bip39.ToSeed(mnemonic!, doc.Passphrase), kind, profile, path);
|
||||
}
|
||||
|
||||
// 2. HD da xprv importata (spendibile, senza seed)
|
||||
// 2. HD from imported xprv (spendable, no seed)
|
||||
if (doc.AccountXprv is { } xprvStr)
|
||||
{
|
||||
if (!Slip132.TryDecodePrivate(xprvStr, profile, out var xprv, out _))
|
||||
throw new InvalidDataException("Xprv del file wallet non valida per questa rete.");
|
||||
throw new InvalidDataException("Xprv in wallet file is invalid for this network.");
|
||||
var path = doc.AccountPath is { Length: > 0 } p ? KeyPath.Parse(p) : null;
|
||||
return HdAccount.FromAccountXprv(xprv!, kind, profile, path);
|
||||
}
|
||||
|
||||
// 3. Chiavi WIF importate
|
||||
// 3. Imported WIF keys
|
||||
if (doc.WifKeys is { Count: > 0 } wifKeys)
|
||||
{
|
||||
var entries = wifKeys.Select(wif =>
|
||||
@@ -51,21 +51,21 @@ public static class WalletLoader
|
||||
return new ImportedKeyAccount(entries, kind, profile);
|
||||
}
|
||||
|
||||
// 4. Watch-only da xpub
|
||||
// 4. Watch-only from xpub
|
||||
if (doc.AccountXpub is null)
|
||||
throw new InvalidDataException("File wallet senza xpub e senza seed.");
|
||||
throw new InvalidDataException("Wallet file has no xpub and no seed.");
|
||||
if (!Slip132.TryDecodePublic(doc.AccountXpub, profile, out var xpub, out _))
|
||||
throw new InvalidDataException("Xpub del file wallet non valida per questa rete.");
|
||||
throw new InvalidDataException("Xpub in wallet file is invalid for this network.");
|
||||
var accountPath = doc.AccountPath is { Length: > 0 } ap ? KeyPath.Parse(ap) : null;
|
||||
return HdAccount.FromAccountXpub(xpub!, kind, profile, accountPath);
|
||||
}
|
||||
|
||||
/// <summary>Crea il documento per un nuovo wallet da seed.</summary>
|
||||
/// <summary>Creates the document for a new seed wallet.</summary>
|
||||
public static (WalletDocument Doc, HdAccount Account) NewFromMnemonic(
|
||||
string words, string? passphrase, ScriptKind kind, ChainProfile profile, KeyPath? customPath = null)
|
||||
{
|
||||
if (!Bip39.TryParse(words, out var mnemonic))
|
||||
throw new InvalidDataException("Mnemonica non valida (parole o checksum errati).");
|
||||
throw new InvalidDataException("Invalid mnemonic (wrong words or checksum).");
|
||||
|
||||
var account = customPath is null
|
||||
? HdAccount.FromMnemonic(mnemonic!, passphrase, kind, profile)
|
||||
@@ -85,15 +85,15 @@ public static class WalletLoader
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crea il documento da una xpub SLIP-132 importata (watch-only).
|
||||
/// Rileva il ScriptKind dagli header SLIP-132; <paramref name="kindOverride"/> lo sovrascrive
|
||||
/// per i prefissi ambigui (xpub può essere Legacy o Taproot).
|
||||
/// Creates the document from an imported SLIP-132 xpub (watch-only).
|
||||
/// Detects the ScriptKind from the SLIP-132 header; <paramref name="kindOverride"/> overrides
|
||||
/// it for ambiguous prefixes (xpub can be Legacy or Taproot).
|
||||
/// </summary>
|
||||
public static (WalletDocument Doc, HdAccount Account) NewFromXpub(
|
||||
string slip132, ChainProfile profile, ScriptKind? kindOverride = null)
|
||||
{
|
||||
if (!Slip132.TryDecodePublic(slip132.Trim(), profile, out var xpub, out var detectedKind))
|
||||
throw new InvalidDataException("Chiave pubblica estesa non valida o non riconosciuta per questa rete.");
|
||||
throw new InvalidDataException("Extended public key is invalid or unrecognised for this network.");
|
||||
var kind = kindOverride ?? detectedKind;
|
||||
var account = HdAccount.FromAccountXpub(xpub!, kind, profile);
|
||||
|
||||
@@ -108,14 +108,14 @@ public static class WalletLoader
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crea il documento da una xprv SLIP-132 importata (spendibile senza seed).
|
||||
/// Il documento contiene la xprv in chiaro: deve essere obbligatoriamente cifrato.
|
||||
/// Creates the document from an imported SLIP-132 xprv (spendable without seed).
|
||||
/// The document contains the xprv in plaintext — it must be encrypted.
|
||||
/// </summary>
|
||||
public static (WalletDocument Doc, HdAccount Account) NewFromXprv(
|
||||
string slip132, ChainProfile profile, ScriptKind? kindOverride = null)
|
||||
{
|
||||
if (!Slip132.TryDecodePrivate(slip132.Trim(), profile, out var xprv, out var detectedKind))
|
||||
throw new InvalidDataException("Chiave privata estesa non valida o non riconosciuta per questa rete.");
|
||||
throw new InvalidDataException("Extended private key is invalid or unrecognised for this network.");
|
||||
var kind = kindOverride ?? detectedKind;
|
||||
var account = HdAccount.FromAccountXprv(xprv!, kind, profile);
|
||||
|
||||
@@ -131,14 +131,14 @@ public static class WalletLoader
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crea il documento da una o più chiavi WIF importate.
|
||||
/// Il documento contiene le chiavi WIF in chiaro: deve essere obbligatoriamente cifrato.
|
||||
/// Creates the document from one or more imported WIF keys.
|
||||
/// The document contains the WIF keys in plaintext — it must be encrypted.
|
||||
/// </summary>
|
||||
public static (WalletDocument Doc, ImportedKeyAccount Account) NewFromWif(
|
||||
IReadOnlyList<string> wifKeys, ScriptKind kind, ChainProfile profile)
|
||||
{
|
||||
if (wifKeys.Count == 0)
|
||||
throw new InvalidDataException("Almeno una chiave WIF richiesta.");
|
||||
throw new InvalidDataException("At least one WIF key is required.");
|
||||
|
||||
var network = PalladiumNetworks.For(profile.Kind);
|
||||
var entries = new List<(BitcoinAddress, Key?)>();
|
||||
@@ -153,7 +153,7 @@ public static class WalletLoader
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidDataException($"Chiave WIF non valida: {ex.Message}");
|
||||
throw new InvalidDataException($"Invalid WIF key: {ex.Message}");
|
||||
}
|
||||
var addr = secret.PrivateKey.PubKey
|
||||
.GetAddress(DerivationPaths.ScriptPubKeyTypeFor(kind), network);
|
||||
|
||||
Reference in New Issue
Block a user