2026-06-11 10:47:52 +02:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Collections.ObjectModel;
|
2026-06-11 11:27:41 +02:00
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
2026-06-12 11:47:23 +02:00
|
|
|
using System.Threading;
|
2026-06-11 10:47:52 +02:00
|
|
|
using System.Threading.Tasks;
|
2026-06-12 10:21:16 +02:00
|
|
|
using Avalonia.Media.Imaging;
|
2026-06-11 10:47:52 +02:00
|
|
|
using Avalonia.Threading;
|
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
|
|
|
|
using NBitcoin;
|
2026-06-11 16:23:02 +02:00
|
|
|
using PalladiumWallet.App.Localization;
|
2026-06-11 10:47:52 +02:00
|
|
|
using PalladiumWallet.Core.Chain;
|
|
|
|
|
using PalladiumWallet.Core.Crypto;
|
|
|
|
|
using PalladiumWallet.Core.Net;
|
|
|
|
|
using PalladiumWallet.Core.Spv;
|
|
|
|
|
using PalladiumWallet.Core.Storage;
|
|
|
|
|
using PalladiumWallet.Core.Wallet;
|
2026-06-12 10:21:16 +02:00
|
|
|
using QRCoder;
|
2026-06-11 10:47:52 +02:00
|
|
|
|
|
|
|
|
namespace PalladiumWallet.App.ViewModels;
|
|
|
|
|
|
|
|
|
|
/// <summary>Riga dello storico transazioni per la vista.</summary>
|
|
|
|
|
public sealed record HistoryRow(string Conferma, string Importo, string Txid, string Verificata);
|
|
|
|
|
|
2026-06-11 21:39:32 +02:00
|
|
|
/// <summary>Riga della vista indirizzi con chiavi e derivation path pre-calcolati.</summary>
|
|
|
|
|
public sealed record AddressRow(
|
|
|
|
|
string Tipo, int Indice, string Indirizzo, string Saldo, string NumTx,
|
|
|
|
|
bool IsChange = false, string PubKey = "", string PrivKey = "", string DerivPath = "")
|
|
|
|
|
{
|
|
|
|
|
public bool HasPrivKey => !string.IsNullOrEmpty(PrivKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Dati completi di un indirizzo passati alla finestra di dettaglio.</summary>
|
|
|
|
|
public sealed record AddressInfo(
|
|
|
|
|
Localization.Loc Loc,
|
|
|
|
|
string Address, string DerivPath, string PubKey, string PrivKey)
|
|
|
|
|
{
|
|
|
|
|
public bool HasPrivKey => !string.IsNullOrEmpty(PrivKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Contatto in rubrica: nome + indirizzo blockchain.</summary>
|
|
|
|
|
public sealed record ContactEntry(string Name, string Address);
|
2026-06-11 11:27:41 +02:00
|
|
|
|
2026-06-12 10:13:09 +02:00
|
|
|
/// <summary>Voce della lista di scelta wallet: nome file + percorso completo.</summary>
|
|
|
|
|
public sealed record WalletFileEntry(string Name, string Path);
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// ViewModel unico dell'applicazione (wizard §15 ridotto + dashboard):
|
|
|
|
|
/// pannello di setup (crea/ripristina/apri) e pannello wallet
|
|
|
|
|
/// (saldo, ricevi, storico, invia, server).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public partial class MainWindowViewModel : ViewModelBase
|
|
|
|
|
{
|
|
|
|
|
private WalletDocument? _doc;
|
|
|
|
|
private HdAccount? _account;
|
|
|
|
|
private string? _walletPath;
|
|
|
|
|
private string? _password;
|
|
|
|
|
private ElectrumClient? _client;
|
2026-06-11 13:37:31 +02:00
|
|
|
private WalletSynchronizer? _synchronizer;
|
2026-06-11 10:47:52 +02:00
|
|
|
private IReadOnlyDictionary<string, Transaction>? _lastTransactions;
|
|
|
|
|
private BuiltTransaction? _pendingSend;
|
|
|
|
|
|
2026-06-11 13:37:31 +02:00
|
|
|
/// <summary>Notifica arrivata durante una sync: si risincronizza appena finita.</summary>
|
|
|
|
|
private bool _resyncRequested;
|
|
|
|
|
|
2026-06-11 16:23:02 +02:00
|
|
|
/// <summary>Configurazione globale (§8): lingua e unità.</summary>
|
|
|
|
|
private AppConfig _config = AppConfig.Load();
|
|
|
|
|
|
2026-06-11 18:41:31 +02:00
|
|
|
/// <summary>Istanza Loc corrente: viene rimpiazzata ad ogni cambio lingua
|
|
|
|
|
/// così Avalonia vede un riferimento diverso e rivaluta le binding {Binding Loc[chiave]}.</summary>
|
|
|
|
|
private Loc _loc = Loc.Instance;
|
|
|
|
|
public Loc Loc => _loc;
|
2026-06-11 16:23:02 +02:00
|
|
|
|
|
|
|
|
/// <summary>Unità corrente per il campo importo del pannello Invia.</summary>
|
|
|
|
|
public string UnitLabel => _config.Unit;
|
|
|
|
|
|
|
|
|
|
public AppConfig CurrentConfig => _config;
|
|
|
|
|
|
|
|
|
|
// Spunte del menu Impostazioni (ToggleType Radio).
|
|
|
|
|
public bool IsLangIt => _config.Language == "it";
|
|
|
|
|
public bool IsLangEn => _config.Language == "en";
|
2026-06-11 18:41:31 +02:00
|
|
|
public bool IsLangEs => _config.Language == "es";
|
|
|
|
|
public bool IsLangFr => _config.Language == "fr";
|
|
|
|
|
public bool IsLangPt => _config.Language == "pt";
|
|
|
|
|
public bool IsLangDe => _config.Language == "de";
|
2026-06-11 16:23:02 +02:00
|
|
|
public bool IsUnitPlm => _config.Unit == "PLM";
|
|
|
|
|
public bool IsUnitMilli => _config.Unit == "mPLM";
|
|
|
|
|
public bool IsUnitMicro => _config.Unit == "µPLM";
|
|
|
|
|
public bool IsUnitSat => _config.Unit == "sat";
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void SetLanguage(string language)
|
|
|
|
|
{
|
|
|
|
|
_config.Language = language;
|
|
|
|
|
ApplySettings(_config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void SetUnit(string unit)
|
|
|
|
|
{
|
|
|
|
|
_config.Unit = unit;
|
|
|
|
|
ApplySettings(_config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Applica e persiste le impostazioni (§8).</summary>
|
|
|
|
|
public void ApplySettings(AppConfig config)
|
|
|
|
|
{
|
|
|
|
|
_config = config;
|
|
|
|
|
_config.Save();
|
2026-06-11 18:41:31 +02:00
|
|
|
_loc = Loc.SwitchTo(config.Language);
|
|
|
|
|
OnPropertyChanged(nameof(Loc));
|
2026-06-11 16:23:02 +02:00
|
|
|
OnPropertyChanged(nameof(UnitLabel));
|
|
|
|
|
OnPropertyChanged(nameof(IsLangIt));
|
|
|
|
|
OnPropertyChanged(nameof(IsLangEn));
|
2026-06-11 18:41:31 +02:00
|
|
|
OnPropertyChanged(nameof(IsLangEs));
|
|
|
|
|
OnPropertyChanged(nameof(IsLangFr));
|
|
|
|
|
OnPropertyChanged(nameof(IsLangPt));
|
|
|
|
|
OnPropertyChanged(nameof(IsLangDe));
|
2026-06-11 16:23:02 +02:00
|
|
|
OnPropertyChanged(nameof(IsUnitPlm));
|
|
|
|
|
OnPropertyChanged(nameof(IsUnitMilli));
|
|
|
|
|
OnPropertyChanged(nameof(IsUnitMicro));
|
|
|
|
|
OnPropertyChanged(nameof(IsUnitSat));
|
|
|
|
|
ApplyCache(_doc?.Cache);
|
|
|
|
|
StatusMessage = Loc.Tr("msg.settings.saved");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Formatta un importo nell'unità scelta nelle impostazioni.</summary>
|
|
|
|
|
private string Fmt(long sats, bool withLabel = true) =>
|
|
|
|
|
CoinAmount.FormatIn(sats, _config.Unit, withLabel);
|
|
|
|
|
|
2026-06-11 21:39:32 +02:00
|
|
|
/// <summary>Chiave privata WIF per un indirizzo; stringa vuota se watch-only.</summary>
|
|
|
|
|
private string KeyWif(bool isChange, int index)
|
|
|
|
|
{
|
|
|
|
|
if (_account is null or { IsWatchOnly: true }) return "";
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return _account.GetExtPrivateKey(isChange, index)
|
|
|
|
|
.PrivateKey.GetWif(PalladiumNetworks.For(Net)).ToString();
|
|
|
|
|
}
|
|
|
|
|
catch { return ""; }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 11:27:41 +02:00
|
|
|
/// <summary>File in attesa di password (apertura da menu File → Apri).</summary>
|
|
|
|
|
private string? _pendingOpenPath;
|
|
|
|
|
|
|
|
|
|
/// <summary>Dopo la prima connessione riuscita il timer riconnette da solo.</summary>
|
|
|
|
|
private bool _autoReconnect;
|
|
|
|
|
|
|
|
|
|
private readonly DispatcherTimer _keepAliveTimer;
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
// ---- selezione rete e stato pannelli ----
|
|
|
|
|
|
|
|
|
|
public string[] Networks { get; } = ["mainnet", "testnet", "regtest"];
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string selectedNetwork = "mainnet";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsSetupVisible))]
|
|
|
|
|
private bool isWalletOpen;
|
|
|
|
|
|
|
|
|
|
public bool IsSetupVisible => !IsWalletOpen;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool walletFileExists;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string statusMessage = "";
|
|
|
|
|
|
2026-06-11 16:04:25 +02:00
|
|
|
// ---- wizard di setup (§15): un passo alla volta ----
|
|
|
|
|
|
2026-06-12 09:25:00 +02:00
|
|
|
public const string StepDataLocation = "data-location";
|
2026-06-11 16:04:25 +02:00
|
|
|
public const string StepStart = "start";
|
2026-06-12 10:13:09 +02:00
|
|
|
public const string StepChooseWallet = "choose-wallet";
|
2026-06-11 16:04:25 +02:00
|
|
|
public const string StepOpen = "open";
|
|
|
|
|
public const string StepShowSeed = "show-seed";
|
|
|
|
|
public const string StepConfirmSeed = "confirm-seed";
|
|
|
|
|
public const string StepWords = "words";
|
|
|
|
|
public const string StepPassphrase = "passphrase";
|
|
|
|
|
public const string StepPassword = "password";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2026-06-12 09:25:00 +02:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsStepDataLocation))]
|
2026-06-11 16:04:25 +02:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsStepStart))]
|
2026-06-12 10:13:09 +02:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsStepChooseWallet))]
|
2026-06-11 16:04:25 +02:00
|
|
|
[NotifyPropertyChangedFor(nameof(IsStepOpen))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsStepShowSeed))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsStepConfirmSeed))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsStepWords))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsStepPassphrase))]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsStepPassword))]
|
|
|
|
|
private string setupStep = StepStart;
|
|
|
|
|
|
2026-06-12 09:25:00 +02:00
|
|
|
public bool IsStepDataLocation => SetupStep == StepDataLocation;
|
2026-06-11 16:04:25 +02:00
|
|
|
public bool IsStepStart => SetupStep == StepStart;
|
2026-06-12 10:13:09 +02:00
|
|
|
public bool IsStepChooseWallet => SetupStep == StepChooseWallet;
|
2026-06-11 16:04:25 +02:00
|
|
|
public bool IsStepOpen => SetupStep == StepOpen;
|
|
|
|
|
public bool IsStepShowSeed => SetupStep == StepShowSeed;
|
|
|
|
|
public bool IsStepConfirmSeed => SetupStep == StepConfirmSeed;
|
|
|
|
|
public bool IsStepWords => SetupStep == StepWords;
|
|
|
|
|
public bool IsStepPassphrase => SetupStep == StepPassphrase;
|
|
|
|
|
public bool IsStepPassword => SetupStep == StepPassword;
|
|
|
|
|
|
2026-06-12 09:25:00 +02:00
|
|
|
/// <summary>Percorso dati predefinito, mostrato al primo avvio.</summary>
|
|
|
|
|
public string DefaultDataPath => AppPaths.DefaultDataRoot();
|
|
|
|
|
|
2026-06-11 16:04:25 +02:00
|
|
|
/// <summary>True quando il flusso è "ripristina" (parole inserite dall'utente).</summary>
|
|
|
|
|
private bool _isRestoreFlow;
|
2026-06-11 10:47:52 +02:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string mnemonicInput = "";
|
|
|
|
|
|
2026-06-11 16:04:25 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private string confirmMnemonicInput = "";
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private string passphraseInput = "";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string passwordInput = "";
|
|
|
|
|
|
2026-06-12 10:13:09 +02:00
|
|
|
/// <summary>Conferma password alla creazione (stile Electrum: digitata due volte).</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string confirmPasswordInput = "";
|
|
|
|
|
|
|
|
|
|
/// <summary>Se true il file wallet viene cifrato con la password (default, stile Electrum).</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool encryptWallet = true;
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
// ---- pannello wallet ----
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string balanceText = "—";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string unconfirmedText = "";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string networkInfo = "";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string receiveAddress = "";
|
|
|
|
|
|
2026-06-12 10:21:16 +02:00
|
|
|
/// <summary>QR code dell'indirizzo di ricezione corrente (PNG in-memory).</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private Bitmap? receiveQr;
|
|
|
|
|
|
|
|
|
|
// Rigenera il QR ogni volta che l'indirizzo di ricezione cambia.
|
|
|
|
|
partial void OnReceiveAddressChanged(string value)
|
|
|
|
|
{
|
|
|
|
|
var previous = ReceiveQr;
|
|
|
|
|
ReceiveQr = string.IsNullOrEmpty(value) ? null : GenerateQr(value);
|
|
|
|
|
previous?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 10:47:01 +02:00
|
|
|
/// <summary>Feedback dopo la copia dell'indirizzo negli appunti (chiamato dal code-behind).</summary>
|
|
|
|
|
public void NotifyAddressCopied() => StatusMessage = Loc.Tr("addr.copied");
|
|
|
|
|
|
2026-06-12 10:21:16 +02:00
|
|
|
private static Bitmap? GenerateQr(string text)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using var generator = new QRCodeGenerator();
|
|
|
|
|
using var data = generator.CreateQrCode(text, QRCodeGenerator.ECCLevel.M);
|
|
|
|
|
var png = new PngByteQRCode(data).GetGraphic(8);
|
|
|
|
|
return new Bitmap(new MemoryStream(png));
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
[ObservableProperty]
|
2026-06-12 09:02:14 +02:00
|
|
|
private string serverHost = "";
|
2026-06-11 10:47:52 +02:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2026-06-12 09:02:14 +02:00
|
|
|
private string serverPort = "";
|
|
|
|
|
|
|
|
|
|
/// <summary>Si predilige TLS: la connessione automatica all'apertura del
|
|
|
|
|
/// wallet usa la porta SSL del server. L'utente può disattivarlo.</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool useSsl = true;
|
|
|
|
|
|
2026-06-12 09:09:25 +02:00
|
|
|
/// <summary>Overlay impostazioni server: aperto dall'overlay Impostazioni.</summary>
|
2026-06-12 09:02:14 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isServerSettingsOpen;
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-12 09:09:25 +02:00
|
|
|
private void OpenServerSettings()
|
|
|
|
|
{
|
|
|
|
|
IsSettingsOpen = false;
|
|
|
|
|
IsServerSettingsOpen = true;
|
|
|
|
|
}
|
2026-06-12 09:02:14 +02:00
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void CloseServerSettings() => IsServerSettingsOpen = false;
|
2026-06-11 10:47:52 +02:00
|
|
|
|
2026-06-12 09:09:25 +02:00
|
|
|
/// <summary>Overlay impostazioni (lingua, unità, server): in-app per evitare
|
|
|
|
|
/// i popup di menu annidati, lenti su WSLg.</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isSettingsOpen;
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void OpenSettings() => IsSettingsOpen = true;
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void CloseSettings() => IsSettingsOpen = false;
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
[ObservableProperty]
|
2026-06-11 16:23:02 +02:00
|
|
|
private string connectionStatus = Loc.Tr("conn.none");
|
2026-06-11 10:47:52 +02:00
|
|
|
|
2026-06-11 11:27:41 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isConnected;
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isSyncing;
|
|
|
|
|
|
2026-06-11 11:27:41 +02:00
|
|
|
public ObservableCollection<KnownServer> KnownServers { get; } = [];
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private KnownServer? selectedKnownServer;
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
public ObservableCollection<HistoryRow> History { get; } = [];
|
|
|
|
|
|
2026-06-11 11:27:41 +02:00
|
|
|
public ObservableCollection<AddressRow> Addresses { get; } = [];
|
|
|
|
|
|
2026-06-12 10:13:09 +02:00
|
|
|
/// <summary>Wallet disponibili nella cartella della rete, per la schermata di scelta.</summary>
|
|
|
|
|
public ObservableCollection<WalletFileEntry> WalletList { get; } = [];
|
|
|
|
|
|
2026-06-11 21:39:32 +02:00
|
|
|
// ---- tab indirizzi ----
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private AddressRow? selectedAddressRow;
|
|
|
|
|
|
2026-06-12 08:16:39 +02:00
|
|
|
/// <summary>Dettaglio indirizzo mostrato nell'overlay in-app; null = nascosto.</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private AddressInfo? addressInfo;
|
|
|
|
|
|
|
|
|
|
/// <summary>Apre l'overlay con i dati dell'indirizzo passato.</summary>
|
|
|
|
|
public void ShowAddressInfo(AddressRow row) =>
|
|
|
|
|
AddressInfo = new AddressInfo(Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void CloseAddressInfo() => AddressInfo = null;
|
|
|
|
|
|
2026-06-12 11:47:23 +02:00
|
|
|
// ---- overlay dettaglio transazione ----
|
|
|
|
|
|
|
|
|
|
/// <summary>Overlay dettaglio transazione aperto.</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isTxDetailsOpen;
|
|
|
|
|
|
|
|
|
|
/// <summary>Caricamento dei dati in corso (mostra lo spinner nell'overlay).</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isTxDetailsLoading;
|
|
|
|
|
|
|
|
|
|
/// <summary>Dati della transazione mostrata; null finché non sono pronti.</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private TransactionDetailsViewModel? txDetails;
|
|
|
|
|
|
|
|
|
|
private CancellationTokenSource? _txDetailsCts;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Apre l'overlay dettaglio transazione: appare subito con lo spinner e i
|
|
|
|
|
/// dati arrivano dal server in background. Overlay in-app (come indirizzo e
|
|
|
|
|
/// impostazioni) per apertura/chiusura istantanee, senza una top-level window.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task ShowTransactionDetailsAsync(string txid)
|
|
|
|
|
{
|
|
|
|
|
if (_client is null || !_client.IsConnected)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = Loc.Tr("tx.needconnection");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_txDetailsCts?.Cancel();
|
|
|
|
|
_txDetailsCts = new CancellationTokenSource();
|
|
|
|
|
var ct = _txDetailsCts.Token;
|
|
|
|
|
|
|
|
|
|
TxDetails = null;
|
|
|
|
|
IsTxDetailsLoading = true;
|
|
|
|
|
IsTxDetailsOpen = true;
|
|
|
|
|
|
|
|
|
|
var details = await BuildTransactionDetailsAsync(txid, ct);
|
|
|
|
|
|
|
|
|
|
// L'utente potrebbe aver chiuso l'overlay (o aperto un'altra tx) mentre
|
|
|
|
|
// caricava: non sovrascrivere lo stato in quel caso.
|
|
|
|
|
if (ct.IsCancellationRequested || !IsTxDetailsOpen)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (details is null)
|
|
|
|
|
{
|
|
|
|
|
IsTxDetailsOpen = false;
|
|
|
|
|
IsTxDetailsLoading = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TxDetails = details;
|
|
|
|
|
IsTxDetailsLoading = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void CloseTransactionDetails()
|
|
|
|
|
{
|
|
|
|
|
_txDetailsCts?.Cancel();
|
|
|
|
|
IsTxDetailsOpen = false;
|
|
|
|
|
IsTxDetailsLoading = false;
|
|
|
|
|
TxDetails = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Recupera dal server tutti i dati della transazione <paramref name="txid"/>
|
|
|
|
|
/// (importi, fee, indirizzi, dimensioni, data) e li impacchetta per la
|
|
|
|
|
/// finestra di dettaglio. Restituisce null se non c'è connessione o in errore.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<TransactionDetailsViewModel?> BuildTransactionDetailsAsync(
|
|
|
|
|
string txid, CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
if (_client is null || !_client.IsConnected)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = Loc.Tr("tx.needconnection");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (_doc?.Cache is not { } cache)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
// Snapshot dei dati sull'UI thread, poi tutto il lavoro (round-trip al
|
|
|
|
|
// server + parsing delle transazioni) va su un thread pool: altrimenti i
|
|
|
|
|
// continuation dei fetch riprendono sull'UI thread e bloccano la finestra
|
|
|
|
|
// (spinner fermo, chiusura ritardata).
|
|
|
|
|
var client = _client;
|
|
|
|
|
var network = PalladiumNetworks.For(Net);
|
|
|
|
|
var row = cache.History.FirstOrDefault(t => t.Txid == txid);
|
|
|
|
|
var owned = cache.Addresses.Select(a => a.Address).ToHashSet();
|
|
|
|
|
var tipHeight = cache.TipHeight;
|
|
|
|
|
var height = row?.Height ?? 0;
|
|
|
|
|
var delta = row?.DeltaSats ?? 0;
|
|
|
|
|
var verified = row?.Verified ?? false;
|
|
|
|
|
var transactions = _lastTransactions;
|
|
|
|
|
var loc = _loc;
|
|
|
|
|
var unit = _config.Unit;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return await Task.Run(async () =>
|
|
|
|
|
{
|
|
|
|
|
var details = await TransactionInspector.FetchAsync(
|
|
|
|
|
client, network, txid, tipHeight, height, owned, delta, verified, transactions, ct);
|
|
|
|
|
return new TransactionDetailsViewModel(details, loc, unit);
|
|
|
|
|
}, ct);
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = $"{Loc.Tr("msg.error")}: {ex.Message}";
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 21:39:32 +02:00
|
|
|
// ---- rubrica contatti ----
|
|
|
|
|
|
|
|
|
|
public ObservableCollection<ContactEntry> Contacts { get; } = [];
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private ContactEntry? selectedContactInList;
|
|
|
|
|
|
|
|
|
|
/// <summary>Contatto selezionato nella ComboBox del pannello Invia: riempie SendTo.</summary>
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private ContactEntry? sendToContact;
|
|
|
|
|
|
|
|
|
|
partial void OnSendToContactChanged(ContactEntry? value)
|
|
|
|
|
{
|
|
|
|
|
if (value is not null)
|
|
|
|
|
SendTo = value.Address;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string newContactName = "";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string newContactAddress = "";
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void AddContact()
|
|
|
|
|
{
|
|
|
|
|
var name = NewContactName.Trim();
|
|
|
|
|
var addr = NewContactAddress.Trim();
|
|
|
|
|
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(addr)) return;
|
|
|
|
|
Contacts.Add(new ContactEntry(name, addr));
|
|
|
|
|
NewContactName = NewContactAddress = "";
|
|
|
|
|
PersistContacts();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void RemoveSelectedContact()
|
|
|
|
|
{
|
|
|
|
|
if (SelectedContactInList is { } c)
|
|
|
|
|
{
|
|
|
|
|
Contacts.Remove(c);
|
|
|
|
|
SelectedContactInList = null;
|
|
|
|
|
PersistContacts();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void PersistContacts()
|
|
|
|
|
{
|
|
|
|
|
if (_doc is null || _walletPath is null) return;
|
|
|
|
|
_doc.Contacts = Contacts
|
|
|
|
|
.Select(c => new PalladiumWallet.Core.Storage.StoredContact { Name = c.Name, Address = c.Address })
|
|
|
|
|
.ToList();
|
|
|
|
|
WalletStore.Save(_doc, _walletPath, _password);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void LoadContacts()
|
|
|
|
|
{
|
|
|
|
|
Contacts.Clear();
|
|
|
|
|
if (_doc is null) return;
|
|
|
|
|
foreach (var c in _doc.Contacts)
|
|
|
|
|
Contacts.Add(new ContactEntry(c.Name, c.Address));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
// ---- pannello invia ----
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string sendTo = "";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string sendAmount = "";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string sendFeeRate = "1";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool sendAll;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string sendPreview = "";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool hasPendingSend;
|
|
|
|
|
|
|
|
|
|
public MainWindowViewModel()
|
|
|
|
|
{
|
2026-06-11 18:41:31 +02:00
|
|
|
_loc = Loc.SwitchTo(_config.Language);
|
2026-06-12 09:25:00 +02:00
|
|
|
// Primo avvio: se la posizione dei dati non è ancora determinata,
|
|
|
|
|
// chiediamola prima di tutto il resto del wizard.
|
|
|
|
|
if (!AppPaths.IsDataLocationConfigured())
|
|
|
|
|
SetupStep = StepDataLocation;
|
|
|
|
|
else
|
|
|
|
|
RefreshSetupState();
|
2026-06-11 11:27:41 +02:00
|
|
|
// Aggiornamenti continui (§9): ping periodico per tenere viva la
|
|
|
|
|
// connessione e accorgersi subito della caduta; se cade, riconnette
|
|
|
|
|
// e risincronizza da solo. Le notifiche push restano la via principale.
|
|
|
|
|
_keepAliveTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(20) };
|
|
|
|
|
_keepAliveTimer.Tick += async (_, _) => await KeepAliveTickAsync();
|
|
|
|
|
_keepAliveTimer.Start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task KeepAliveTickAsync()
|
|
|
|
|
{
|
2026-06-12 09:43:59 +02:00
|
|
|
// Vale anche senza wallet aperto: se l'utente si è connesso dalle
|
|
|
|
|
// impostazioni server, la connessione va mantenuta e riconnessa.
|
|
|
|
|
if (IsSyncing)
|
2026-06-11 11:27:41 +02:00
|
|
|
return;
|
|
|
|
|
if (_client is { IsConnected: true })
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await _client.PingAsync();
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// La caduta viene gestita dall'evento Disconnected.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (_autoReconnect)
|
|
|
|
|
{
|
2026-06-11 16:23:02 +02:00
|
|
|
ConnectionStatus = Loc.Tr("conn.reconnecting");
|
2026-06-11 11:27:41 +02:00
|
|
|
await ConnectAndSync();
|
|
|
|
|
}
|
2026-06-11 10:47:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private NetKind Net => Enum.Parse<NetKind>(SelectedNetwork, ignoreCase: true);
|
|
|
|
|
private ChainProfile Profile => ChainProfiles.For(Net);
|
|
|
|
|
|
|
|
|
|
partial void OnSelectedNetworkChanged(string value) => RefreshSetupState();
|
|
|
|
|
|
2026-06-11 11:27:41 +02:00
|
|
|
private ServerRegistry Registry => new(Profile, AppPaths.ServersPath(Net));
|
|
|
|
|
|
2026-06-12 09:25:00 +02:00
|
|
|
/// <summary>Primo avvio: usa il percorso dati predefinito di piattaforma.</summary>
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void UseDefaultDataLocation() => ApplyDataLocation(AppPaths.DefaultDataRoot());
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Memorizza la radice dati scelta (default o personalizzata), ricarica la
|
|
|
|
|
/// configurazione dalla nuova posizione e prosegue col wizard. Chiamata anche
|
|
|
|
|
/// dalla View dopo la scelta di una cartella.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void ApplyDataLocation(string root)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
AppPaths.ConfigureDataLocation(root);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = $"{Loc.Tr("msg.error")}: {ex.Message}";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// La config globale vive nella radice dati: ricaricala da lì.
|
|
|
|
|
_config = AppConfig.Load();
|
|
|
|
|
_loc = Loc.SwitchTo(_config.Language);
|
|
|
|
|
OnPropertyChanged(nameof(Loc));
|
|
|
|
|
OnPropertyChanged(nameof(UnitLabel));
|
|
|
|
|
RefreshSetupState();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
private void RefreshSetupState()
|
|
|
|
|
{
|
2026-06-11 16:04:25 +02:00
|
|
|
SetupStep = StepStart;
|
2026-06-12 10:13:09 +02:00
|
|
|
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ConfirmPasswordInput = "";
|
|
|
|
|
WalletFileExists = AppPaths.WalletFiles(Net).Count > 0;
|
2026-06-11 10:47:52 +02:00
|
|
|
StatusMessage = WalletFileExists
|
2026-06-11 16:23:02 +02:00
|
|
|
? Loc.Tr("msg.welcome.existing")
|
|
|
|
|
: Loc.Tr("msg.welcome.new");
|
2026-06-11 11:27:41 +02:00
|
|
|
RefreshServers();
|
2026-06-12 09:43:59 +02:00
|
|
|
// Come Electrum: ci si connette al server già durante il setup, senza
|
|
|
|
|
// aspettare l'apertura di un wallet (la sync parte poi all'apertura).
|
|
|
|
|
_ = ConnectAndSync();
|
2026-06-11 11:27:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RefreshServers()
|
|
|
|
|
{
|
|
|
|
|
KnownServers.Clear();
|
|
|
|
|
foreach (var server in Registry.All)
|
|
|
|
|
KnownServers.Add(server);
|
|
|
|
|
SelectedKnownServer = KnownServers.FirstOrDefault();
|
|
|
|
|
if (SelectedKnownServer is null)
|
2026-06-12 09:02:14 +02:00
|
|
|
{
|
|
|
|
|
ServerHost = "127.0.0.1";
|
|
|
|
|
ServerPort = (UseSsl ? Profile.DefaultSslPort : Profile.DefaultTcpPort).ToString();
|
|
|
|
|
}
|
2026-06-11 11:27:41 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-12 09:02:14 +02:00
|
|
|
/// <summary>Evita la ricorsione tra OnUseSslChanged e OnServerPortChanged
|
|
|
|
|
/// mentre tengono coerenti porta e flag TLS.</summary>
|
|
|
|
|
private bool _syncingServerFields;
|
|
|
|
|
|
2026-06-11 11:27:41 +02:00
|
|
|
partial void OnSelectedKnownServerChanged(KnownServer? value)
|
|
|
|
|
{
|
2026-06-12 09:02:14 +02:00
|
|
|
if (value is null)
|
|
|
|
|
return;
|
|
|
|
|
_syncingServerFields = true;
|
|
|
|
|
ServerHost = value.Host;
|
|
|
|
|
ServerPort = value.PortFor(UseSsl).ToString();
|
|
|
|
|
_syncingServerFields = false;
|
2026-06-11 11:27:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
partial void OnUseSslChanged(bool value)
|
|
|
|
|
{
|
2026-06-12 09:02:14 +02:00
|
|
|
if (_syncingServerFields)
|
|
|
|
|
return;
|
|
|
|
|
// Attivare/disattivare TLS allinea la porta: porta SSL del server
|
|
|
|
|
// selezionato (o default di profilo) quando TLS è on, porta TCP quando off.
|
|
|
|
|
_syncingServerFields = true;
|
|
|
|
|
ServerPort = SelectedKnownServer is { } server
|
|
|
|
|
? server.PortFor(value).ToString()
|
|
|
|
|
: (value ? Profile.DefaultSslPort : Profile.DefaultTcpPort).ToString();
|
|
|
|
|
_syncingServerFields = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
partial void OnServerPortChanged(string value)
|
|
|
|
|
{
|
|
|
|
|
if (_syncingServerFields)
|
|
|
|
|
return;
|
|
|
|
|
if (!int.TryParse(value.Trim(), out var port))
|
|
|
|
|
return;
|
|
|
|
|
// Scegliere una porta nota allinea il flag TLS, così non si tenta mai
|
|
|
|
|
// una connessione in chiaro sulla porta SSL (causa di "connection error").
|
|
|
|
|
bool? wantSsl =
|
|
|
|
|
SelectedKnownServer is { } s && port == s.SslPort ? true :
|
|
|
|
|
SelectedKnownServer is { } t && port == t.TcpPort ? false :
|
|
|
|
|
port == Profile.DefaultSslPort ? true :
|
|
|
|
|
port == Profile.DefaultTcpPort ? false :
|
|
|
|
|
null;
|
|
|
|
|
if (wantSsl is bool b && b != UseSsl)
|
|
|
|
|
{
|
|
|
|
|
_syncingServerFields = true;
|
|
|
|
|
UseSsl = b;
|
|
|
|
|
_syncingServerFields = false;
|
|
|
|
|
}
|
2026-06-11 10:47:52 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-11 16:04:25 +02:00
|
|
|
// ---------- comandi del wizard (§15): un passo alla volta ----------
|
2026-06-11 10:47:52 +02:00
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-11 16:04:25 +02:00
|
|
|
private void WizardStartOpen()
|
2026-06-11 10:47:52 +02:00
|
|
|
{
|
2026-06-12 10:13:09 +02:00
|
|
|
// Con più wallet nella cartella della rete si sceglie quale aprire;
|
|
|
|
|
// con uno solo si va dritti alla password (multi-wallet §8).
|
|
|
|
|
var files = AppPaths.WalletFiles(Net);
|
|
|
|
|
if (files.Count > 1)
|
|
|
|
|
{
|
|
|
|
|
WalletList.Clear();
|
|
|
|
|
foreach (var path in files)
|
|
|
|
|
WalletList.Add(new WalletFileEntry(Path.GetFileName(path), path));
|
|
|
|
|
SetupStep = StepChooseWallet;
|
|
|
|
|
StatusMessage = Loc.Tr("msg.choose.wallet");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
_pendingOpenPath = files.Count == 1 ? files[0] : AppPaths.DefaultWalletPath(Net);
|
|
|
|
|
PasswordInput = "";
|
|
|
|
|
SetupStep = StepOpen;
|
|
|
|
|
StatusMessage = Loc.Tr("msg.open.password");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Sceglie quale wallet aprire dalla lista e passa alla password.</summary>
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void ChooseWallet(WalletFileEntry? entry)
|
|
|
|
|
{
|
|
|
|
|
if (entry is null)
|
|
|
|
|
return;
|
|
|
|
|
_pendingOpenPath = entry.Path;
|
2026-06-11 16:04:25 +02:00
|
|
|
PasswordInput = "";
|
|
|
|
|
SetupStep = StepOpen;
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("msg.open.password");
|
2026-06-11 16:04:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void WizardStartNew()
|
|
|
|
|
{
|
|
|
|
|
_isRestoreFlow = false;
|
2026-06-11 10:47:52 +02:00
|
|
|
MnemonicInput = Bip39.Generate(MnemonicLength.Twelve).ToString();
|
2026-06-11 16:04:25 +02:00
|
|
|
SetupStep = StepShowSeed;
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("msg.seed.write");
|
2026-06-11 16:04:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void WizardStartRestore()
|
|
|
|
|
{
|
|
|
|
|
_isRestoreFlow = true;
|
|
|
|
|
MnemonicInput = "";
|
|
|
|
|
SetupStep = StepWords;
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("msg.words.enter");
|
2026-06-11 16:04:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void WizardNextFromShowSeed()
|
|
|
|
|
{
|
|
|
|
|
ConfirmMnemonicInput = "";
|
|
|
|
|
SetupStep = StepConfirmSeed;
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("msg.seed.retype");
|
2026-06-11 16:04:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void WizardNextFromConfirmSeed()
|
|
|
|
|
{
|
|
|
|
|
var normalized = string.Join(' ',
|
|
|
|
|
ConfirmMnemonicInput.Split(' ', StringSplitOptions.RemoveEmptyEntries));
|
|
|
|
|
if (!string.Equals(normalized, MnemonicInput, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("msg.seed.mismatch");
|
2026-06-11 16:04:25 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
GoToPassphraseStep();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void WizardNextFromWords()
|
|
|
|
|
{
|
|
|
|
|
if (!Bip39.TryParse(MnemonicInput, out _))
|
|
|
|
|
{
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("msg.words.invalid");
|
2026-06-11 16:04:25 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
GoToPassphraseStep();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void GoToPassphraseStep()
|
|
|
|
|
{
|
|
|
|
|
PassphraseInput = "";
|
|
|
|
|
SetupStep = StepPassphrase;
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("msg.passphrase.info");
|
2026-06-11 16:04:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void WizardNextFromPassphrase()
|
|
|
|
|
{
|
2026-06-12 10:13:09 +02:00
|
|
|
PasswordInput = ConfirmPasswordInput = "";
|
|
|
|
|
EncryptWallet = true;
|
2026-06-11 16:04:25 +02:00
|
|
|
SetupStep = StepPassword;
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("msg.password.info");
|
2026-06-11 16:04:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void WizardBack()
|
|
|
|
|
{
|
|
|
|
|
SetupStep = SetupStep switch
|
|
|
|
|
{
|
2026-06-12 10:13:09 +02:00
|
|
|
StepOpen => WalletList.Count > 1 ? StepChooseWallet : StepStart,
|
|
|
|
|
StepChooseWallet or StepShowSeed or StepWords => StepStart,
|
2026-06-11 16:04:25 +02:00
|
|
|
StepConfirmSeed => StepShowSeed,
|
|
|
|
|
StepPassphrase => _isRestoreFlow ? StepWords : StepConfirmSeed,
|
|
|
|
|
StepPassword => StepPassphrase,
|
|
|
|
|
_ => StepStart,
|
|
|
|
|
};
|
|
|
|
|
if (SetupStep == StepStart)
|
|
|
|
|
RefreshSetupState();
|
2026-06-11 10:47:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void CreateOrRestore()
|
|
|
|
|
{
|
2026-06-12 10:13:09 +02:00
|
|
|
// Scelta cifratura stile Electrum: con cifratura attiva serve una
|
|
|
|
|
// password non vuota, digitata due volte e coincidente. Solo se la
|
|
|
|
|
// cifratura è disattivata il file resta in chiaro (avviso esplicito).
|
|
|
|
|
string? password;
|
|
|
|
|
if (EncryptWallet)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(PasswordInput))
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = Loc.Tr("msg.password.required");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (PasswordInput != ConfirmPasswordInput)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = Loc.Tr("msg.password.mismatch");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
password = PasswordInput;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
password = null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var (doc, account) = WalletLoader.NewFromMnemonic(
|
|
|
|
|
MnemonicInput,
|
|
|
|
|
string.IsNullOrEmpty(PassphraseInput) ? null : PassphraseInput,
|
|
|
|
|
ScriptKind.NativeSegwit,
|
|
|
|
|
Profile);
|
2026-06-11 11:27:41 +02:00
|
|
|
// Mai sovrascrivere un wallet esistente: si cerca il primo nome libero.
|
2026-06-11 10:47:52 +02:00
|
|
|
var path = AppPaths.DefaultWalletPath(Net);
|
2026-06-11 11:27:41 +02:00
|
|
|
for (var n = 2; WalletStore.Exists(path); n++)
|
|
|
|
|
path = Path.Combine(AppPaths.WalletsDir(Net), $"wallet-{n}.wallet.json");
|
2026-06-11 10:47:52 +02:00
|
|
|
WalletStore.Save(doc, path, password);
|
|
|
|
|
OpenLoaded(doc, account, path, password);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = $"Errore: {ex.Message}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void OpenExisting()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-06-11 11:27:41 +02:00
|
|
|
var path = _pendingOpenPath ?? AppPaths.DefaultWalletPath(Net);
|
2026-06-11 10:47:52 +02:00
|
|
|
var password = string.IsNullOrEmpty(PasswordInput) ? null : PasswordInput;
|
|
|
|
|
var doc = WalletStore.Load(path, password);
|
2026-06-11 11:27:41 +02:00
|
|
|
_pendingOpenPath = null;
|
2026-06-11 10:47:52 +02:00
|
|
|
OpenLoaded(doc, WalletLoader.ToAccount(doc), path, password);
|
|
|
|
|
}
|
|
|
|
|
catch (WrongPasswordException)
|
|
|
|
|
{
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("msg.wrongpassword");
|
2026-06-11 10:47:52 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = $"Errore: {ex.Message}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 11:27:41 +02:00
|
|
|
/// <summary>Apertura di un file wallet qualunque (menu File → Apri, multi-wallet §8).</summary>
|
|
|
|
|
public void OpenFromPath(string path)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (IsWalletOpen)
|
|
|
|
|
CloseWallet();
|
|
|
|
|
var doc = WalletStore.Load(path);
|
|
|
|
|
OpenLoaded(doc, WalletLoader.ToAccount(doc), path, password: null);
|
|
|
|
|
}
|
|
|
|
|
catch (WrongPasswordException)
|
|
|
|
|
{
|
2026-06-11 16:04:25 +02:00
|
|
|
// Cifrato: si chiede la password nel passo di apertura del wizard.
|
2026-06-11 11:27:41 +02:00
|
|
|
if (IsWalletOpen)
|
|
|
|
|
CloseWallet();
|
|
|
|
|
_pendingOpenPath = path;
|
|
|
|
|
WalletFileExists = true;
|
2026-06-11 16:04:25 +02:00
|
|
|
PasswordInput = "";
|
|
|
|
|
SetupStep = StepOpen;
|
2026-06-11 11:27:41 +02:00
|
|
|
StatusMessage = $"Il wallet \"{Path.GetFileName(path)}\" è cifrato: inserisci la password e premi Apri.";
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = $"Errore: {ex.Message}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Torna al pannello di setup per creare/ripristinare un altro wallet.</summary>
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void NewWallet()
|
|
|
|
|
{
|
|
|
|
|
if (IsWalletOpen)
|
|
|
|
|
CloseWallet();
|
|
|
|
|
_pendingOpenPath = null;
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("msg.welcome.new");
|
2026-06-11 11:27:41 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
private void OpenLoaded(WalletDocument doc, HdAccount account, string path, string? password)
|
|
|
|
|
{
|
2026-06-11 11:27:41 +02:00
|
|
|
// La rete del wallet comanda (registry, pin TLS, indirizzi).
|
|
|
|
|
SelectedNetwork = doc.Network;
|
2026-06-11 10:47:52 +02:00
|
|
|
_doc = doc;
|
|
|
|
|
_account = account;
|
|
|
|
|
_walletPath = path;
|
|
|
|
|
_password = password;
|
2026-06-12 10:13:09 +02:00
|
|
|
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ConfirmPasswordInput = "";
|
2026-06-11 16:04:25 +02:00
|
|
|
SetupStep = StepStart;
|
2026-06-11 10:47:52 +02:00
|
|
|
|
|
|
|
|
NetworkInfo = $"{doc.Network} · {doc.ScriptKind} · m/{doc.AccountPath}"
|
|
|
|
|
+ (doc.IsWatchOnly ? " · watch-only" : "");
|
2026-06-11 21:39:32 +02:00
|
|
|
LoadContacts();
|
2026-06-11 10:47:52 +02:00
|
|
|
ApplyCache(doc.Cache);
|
|
|
|
|
IsWalletOpen = true;
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("msg.opened");
|
2026-06-11 13:37:31 +02:00
|
|
|
// Come Electrum: ci si connette da soli al server selezionato,
|
|
|
|
|
// senza aspettare un click.
|
|
|
|
|
_ = ConnectAndSync();
|
2026-06-11 10:47:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplyCache(SyncCache? cache)
|
|
|
|
|
{
|
|
|
|
|
if (_account is null)
|
|
|
|
|
return;
|
|
|
|
|
if (cache is null)
|
|
|
|
|
{
|
|
|
|
|
BalanceText = $"0.00000000 {Profile.CoinUnit}";
|
|
|
|
|
UnconfirmedText = "";
|
|
|
|
|
ReceiveAddress = _account.GetReceiveAddress(0).ToString();
|
|
|
|
|
History.Clear();
|
2026-06-11 11:27:41 +02:00
|
|
|
// Prima della sincronizzazione si mostrano i primi indirizzi derivati.
|
|
|
|
|
Addresses.Clear();
|
|
|
|
|
for (var i = 0; i < 10; i++)
|
2026-06-11 21:39:32 +02:00
|
|
|
Addresses.Add(new AddressRow(_loc["addr.receive"], i,
|
|
|
|
|
_account.GetReceiveAddress(i).ToString(), "—", "—",
|
|
|
|
|
false,
|
|
|
|
|
_account.GetPublicKey(false, i).ToHex(),
|
|
|
|
|
KeyWif(false, i),
|
|
|
|
|
$"m/{_doc!.AccountPath}/0/{i}"));
|
2026-06-11 10:47:52 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2026-06-11 16:23:02 +02:00
|
|
|
BalanceText = Fmt(cache.ConfirmedSats);
|
2026-06-11 11:27:41 +02:00
|
|
|
// Saldo in attesa: somma delle tx in mempool (può essere negativo per
|
2026-06-11 11:34:13 +02:00
|
|
|
// gli invii in uscita non ancora confermati). Non è spendibile finché
|
|
|
|
|
// non conferma: la TransactionFactory usa solo UTXO confermati.
|
2026-06-11 11:27:41 +02:00
|
|
|
var pending = cache.History.Where(t => t.Height <= 0).Sum(t => t.DeltaSats);
|
|
|
|
|
UnconfirmedText = pending != 0
|
2026-06-11 16:23:02 +02:00
|
|
|
? $"{Loc.Tr("msg.pending")}: {(pending > 0 ? "+" : "")}{Fmt(pending)} — {Loc.Tr("msg.notspendable")}"
|
2026-06-11 10:47:52 +02:00
|
|
|
: "";
|
|
|
|
|
ReceiveAddress = _account.GetReceiveAddress(cache.NextReceiveIndex).ToString();
|
|
|
|
|
History.Clear();
|
|
|
|
|
foreach (var tx in cache.History)
|
|
|
|
|
History.Add(new HistoryRow(
|
|
|
|
|
tx.Height > 0 ? tx.Height.ToString() : "mempool",
|
2026-06-11 16:23:02 +02:00
|
|
|
(tx.DeltaSats >= 0 ? "+" : "") + Fmt(tx.DeltaSats, withLabel: false),
|
2026-06-11 10:47:52 +02:00
|
|
|
tx.Txid,
|
|
|
|
|
tx.Verified ? "✓ SPV" : "—"));
|
2026-06-11 11:27:41 +02:00
|
|
|
|
|
|
|
|
Addresses.Clear();
|
|
|
|
|
foreach (var a in cache.Addresses)
|
|
|
|
|
Addresses.Add(new AddressRow(
|
2026-06-11 21:39:32 +02:00
|
|
|
a.IsChange ? _loc["addr.change"] : _loc["addr.receive"],
|
2026-06-11 11:27:41 +02:00
|
|
|
a.Index,
|
|
|
|
|
a.Address,
|
2026-06-11 16:23:02 +02:00
|
|
|
a.BalanceSats > 0 ? Fmt(a.BalanceSats, withLabel: false) : (a.TxCount > 0 ? "0" : "—"),
|
2026-06-11 21:39:32 +02:00
|
|
|
a.TxCount > 0 ? a.TxCount.ToString() : "—",
|
|
|
|
|
a.IsChange,
|
|
|
|
|
_account.GetPublicKey(a.IsChange, a.Index).ToHex(),
|
|
|
|
|
KeyWif(a.IsChange, a.Index),
|
|
|
|
|
$"m/{_doc!.AccountPath}/{(a.IsChange ? 1 : 0)}/{a.Index}"));
|
2026-06-11 10:47:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------- comandi wallet ----------
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task ConnectAndSync()
|
|
|
|
|
{
|
2026-06-11 13:37:31 +02:00
|
|
|
if (IsSyncing)
|
|
|
|
|
{
|
|
|
|
|
_resyncRequested = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-11 10:47:52 +02:00
|
|
|
IsSyncing = true;
|
|
|
|
|
StatusMessage = "";
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-06-12 09:02:14 +02:00
|
|
|
var (host, port) = ParseServer();
|
|
|
|
|
|
|
|
|
|
// Se l'endpoint richiesto (host/porta/TLS) è diverso da quello della
|
|
|
|
|
// connessione attiva, la chiudo: l'utente ha cambiato server o ha
|
|
|
|
|
// attivato TLS e si aspetta che la nuova scelta abbia effetto.
|
|
|
|
|
if (_client is { } current &&
|
|
|
|
|
(current.Host != host || current.Port != port || current.UseSsl != UseSsl))
|
|
|
|
|
{
|
|
|
|
|
await DisconnectAsync();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
if (_client is null || !_client.IsConnected)
|
|
|
|
|
{
|
2026-06-11 16:23:02 +02:00
|
|
|
ConnectionStatus = $"{Loc.Tr("conn.connectingto")} {host}:{port}…";
|
2026-06-11 10:47:52 +02:00
|
|
|
var pins = new CertificatePinStore(AppPaths.CertificatePinsPath(Net));
|
|
|
|
|
_client = await ElectrumClient.ConnectAsync(host, port, UseSsl, pins);
|
|
|
|
|
_client.NotificationReceived += OnServerNotification;
|
|
|
|
|
_client.Disconnected += _ => Dispatcher.UIThread.Post(() =>
|
2026-06-11 11:27:41 +02:00
|
|
|
{
|
|
|
|
|
IsConnected = false;
|
2026-06-11 16:23:02 +02:00
|
|
|
ConnectionStatus = Loc.Tr("conn.disconnected");
|
2026-06-11 11:27:41 +02:00
|
|
|
});
|
|
|
|
|
IsConnected = true;
|
|
|
|
|
_autoReconnect = true;
|
2026-06-11 16:23:02 +02:00
|
|
|
ConnectionStatus = $"{Loc.Tr("conn.connectedto")} {host}:{port}{(UseSsl ? " (TLS)" : "")}";
|
2026-06-12 09:43:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Senza un wallet aperto ci si limita a stabilire/verificare la
|
|
|
|
|
// connessione: la sincronizzazione richiede un account.
|
|
|
|
|
if (_account is null || _doc is null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// Synchronizer legato all'account corrente, creato alla prima sync
|
|
|
|
|
// dopo la connessione: conserva la cache di tx e prove verificate,
|
|
|
|
|
// così le risincronizzazioni sono incrementali.
|
|
|
|
|
if (_synchronizer is null)
|
|
|
|
|
{
|
2026-06-11 13:37:31 +02:00
|
|
|
_synchronizer = new WalletSynchronizer(_account, _client, _doc.GapLimit);
|
|
|
|
|
_synchronizer.Progress += msg => Dispatcher.UIThread.Post(() => StatusMessage = msg);
|
2026-06-11 10:47:52 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-11 13:37:31 +02:00
|
|
|
// Se durante la sync arrivano notifiche, si ripete subito: nessun
|
|
|
|
|
// aggiornamento del server va perso (modello Electrum).
|
|
|
|
|
do
|
2026-06-11 10:47:52 +02:00
|
|
|
{
|
2026-06-11 13:37:31 +02:00
|
|
|
_resyncRequested = false;
|
2026-06-12 09:43:59 +02:00
|
|
|
var result = await _synchronizer.SyncOnceAsync();
|
2026-06-11 13:37:31 +02:00
|
|
|
_lastTransactions = result.Transactions;
|
|
|
|
|
|
|
|
|
|
_doc.Cache = new SyncCache
|
|
|
|
|
{
|
|
|
|
|
TipHeight = result.TipHeight,
|
|
|
|
|
ConfirmedSats = result.ConfirmedSats,
|
|
|
|
|
UnconfirmedSats = result.UnconfirmedSats,
|
|
|
|
|
NextReceiveIndex = result.NextReceiveIndex,
|
|
|
|
|
NextChangeIndex = result.NextChangeIndex,
|
|
|
|
|
History = [.. result.History],
|
|
|
|
|
Utxos = [.. result.Utxos],
|
|
|
|
|
Addresses = [.. result.AddressRows],
|
|
|
|
|
};
|
|
|
|
|
WalletStore.Save(_doc, _walletPath!, _password);
|
|
|
|
|
ApplyCache(_doc.Cache);
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = $"{Loc.Tr("msg.synced")}: {Loc.Tr("msg.height")} {result.TipHeight}, " +
|
|
|
|
|
$"{result.History.Count} {Loc.Tr("msg.synced.detail")}";
|
2026-06-11 13:37:31 +02:00
|
|
|
} while (_resyncRequested);
|
2026-06-11 10:47:52 +02:00
|
|
|
}
|
|
|
|
|
catch (CertificatePinMismatchException ex)
|
|
|
|
|
{
|
2026-06-11 11:27:41 +02:00
|
|
|
IsConnected = false;
|
2026-06-11 16:23:02 +02:00
|
|
|
ConnectionStatus = Loc.Tr("conn.certchanged");
|
2026-06-11 10:47:52 +02:00
|
|
|
StatusMessage = ex.Message;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-06-11 11:27:41 +02:00
|
|
|
IsConnected = _client?.IsConnected == true;
|
2026-06-11 16:23:02 +02:00
|
|
|
ConnectionStatus = IsConnected ? ConnectionStatus : Loc.Tr("conn.error");
|
2026-06-11 10:47:52 +02:00
|
|
|
StatusMessage = $"Errore: {ex.Message}";
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
IsSyncing = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 11:27:41 +02:00
|
|
|
/// <summary>Scopre nuovi server dai peer annunciati dal server connesso (§9).</summary>
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task DiscoverServers()
|
|
|
|
|
{
|
|
|
|
|
if (_client is null || !_client.IsConnected)
|
|
|
|
|
{
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("conn.none") + ".";
|
2026-06-11 11:27:41 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var added = await Registry.DiscoverAsync(_client);
|
|
|
|
|
var selected = SelectedKnownServer;
|
|
|
|
|
RefreshServers();
|
|
|
|
|
SelectedKnownServer = KnownServers.FirstOrDefault(s => s.Host == selected?.Host)
|
|
|
|
|
?? KnownServers.FirstOrDefault();
|
|
|
|
|
StatusMessage = added > 0
|
|
|
|
|
? $"Trovati {added} nuovi server dai peer (totale {KnownServers.Count})."
|
|
|
|
|
: $"Nessun nuovo server annunciato (totale {KnownServers.Count}).";
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = $"Errore nella scoperta peer: {ex.Message}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
private void OnServerNotification(string method, System.Text.Json.JsonElement payload)
|
|
|
|
|
{
|
|
|
|
|
// Cambiamento su un nostro indirizzo o nuovo blocco: risincronizza.
|
2026-06-11 13:37:31 +02:00
|
|
|
// Se una sync è già in corso, si accoda (il loop in ConnectAndSync la
|
|
|
|
|
// ripete subito dopo): nessuna notifica viene persa.
|
2026-06-11 10:47:52 +02:00
|
|
|
if (method is "blockchain.scripthash.subscribe" or "blockchain.headers.subscribe")
|
|
|
|
|
Dispatcher.UIThread.Post(() =>
|
|
|
|
|
{
|
2026-06-11 13:37:31 +02:00
|
|
|
if (IsSyncing)
|
|
|
|
|
_resyncRequested = true;
|
|
|
|
|
else
|
2026-06-11 10:47:52 +02:00
|
|
|
_ = ConnectAndSync();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void ResetCertificates()
|
|
|
|
|
{
|
|
|
|
|
new CertificatePinStore(AppPaths.CertificatePinsPath(Net)).ResetAll();
|
2026-06-11 16:23:02 +02:00
|
|
|
StatusMessage = Loc.Tr("msg.certreset");
|
2026-06-11 10:47:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task PrepareSend()
|
|
|
|
|
{
|
|
|
|
|
if (_account is null || _doc?.Cache is null)
|
|
|
|
|
{
|
|
|
|
|
SendPreview = "Sincronizza prima di inviare.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (_lastTransactions is null)
|
|
|
|
|
{
|
|
|
|
|
SendPreview = "Connettiti al server e sincronizza prima di inviare.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var destination = BitcoinAddress.Create(SendTo.Trim(), PalladiumNetworks.For(Net));
|
|
|
|
|
long amount = 0;
|
2026-06-11 16:23:02 +02:00
|
|
|
if (!SendAll && !CoinAmount.TryParseIn(SendAmount, _config.Unit, out amount))
|
2026-06-11 10:47:52 +02:00
|
|
|
{
|
|
|
|
|
SendPreview = "Importo non valido.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!decimal.TryParse(SendFeeRate, out var feeRate) || feeRate <= 0)
|
|
|
|
|
{
|
|
|
|
|
SendPreview = "Fee rate non valido.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_pendingSend = new TransactionFactory(_account).Build(
|
|
|
|
|
_doc.Cache.Utxos, _lastTransactions, destination, amount, feeRate,
|
|
|
|
|
_doc.Cache.NextChangeIndex, SendAll);
|
|
|
|
|
|
|
|
|
|
SendPreview = $"txid {_pendingSend.Txid[..16]}… · " +
|
2026-06-11 16:23:02 +02:00
|
|
|
$"fee {Fmt(_pendingSend.Fee.Satoshi)} " +
|
2026-06-11 10:47:52 +02:00
|
|
|
$"({_pendingSend.Transaction.GetVirtualSize()} vB)" +
|
|
|
|
|
(_pendingSend.Signed ? "" : " · NON firmata (watch-only)");
|
|
|
|
|
HasPendingSend = _pendingSend.Signed;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_pendingSend = null;
|
|
|
|
|
HasPendingSend = false;
|
|
|
|
|
SendPreview = $"Errore: {ex.Message}";
|
|
|
|
|
}
|
|
|
|
|
await Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task ConfirmSend()
|
|
|
|
|
{
|
|
|
|
|
if (_pendingSend is null || _client is null)
|
|
|
|
|
return;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var txid = await _client.BroadcastAsync(_pendingSend.ToHex());
|
|
|
|
|
SendPreview = $"Trasmessa: {txid}";
|
|
|
|
|
SendTo = SendAmount = "";
|
|
|
|
|
_pendingSend = null;
|
|
|
|
|
HasPendingSend = false;
|
|
|
|
|
await ConnectAndSync();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
SendPreview = $"Errore broadcast: {ex.Message}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 09:02:14 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// Chiude la connessione corrente lasciando il wallet aperto: usata quando
|
|
|
|
|
/// l'utente cambia server o attiva TLS e serve riconnettersi al nuovo
|
|
|
|
|
/// endpoint. Attende la chiusura del socket prima di tornare.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task DisconnectAsync()
|
|
|
|
|
{
|
|
|
|
|
if (_client is { } client)
|
|
|
|
|
{
|
|
|
|
|
_client = null;
|
|
|
|
|
_synchronizer = null;
|
|
|
|
|
try { await client.DisposeAsync(); } catch { /* in chiusura */ }
|
|
|
|
|
}
|
|
|
|
|
IsConnected = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
[RelayCommand]
|
|
|
|
|
private void CloseWallet()
|
|
|
|
|
{
|
|
|
|
|
_ = _client?.DisposeAsync().AsTask();
|
|
|
|
|
_client = null;
|
2026-06-11 13:37:31 +02:00
|
|
|
_synchronizer = null;
|
|
|
|
|
_autoReconnect = false;
|
|
|
|
|
_resyncRequested = false;
|
2026-06-11 10:47:52 +02:00
|
|
|
_doc = null;
|
|
|
|
|
_account = null;
|
|
|
|
|
_lastTransactions = null;
|
|
|
|
|
_pendingSend = null;
|
|
|
|
|
HasPendingSend = false;
|
|
|
|
|
History.Clear();
|
2026-06-11 21:39:32 +02:00
|
|
|
Contacts.Clear();
|
|
|
|
|
SelectedContactInList = null;
|
|
|
|
|
SendToContact = null;
|
|
|
|
|
SelectedAddressRow = null;
|
2026-06-11 10:47:52 +02:00
|
|
|
IsWalletOpen = false;
|
2026-06-11 11:27:41 +02:00
|
|
|
IsConnected = false;
|
2026-06-11 16:23:02 +02:00
|
|
|
ConnectionStatus = Loc.Tr("conn.none");
|
2026-06-11 10:47:52 +02:00
|
|
|
RefreshSetupState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private (string Host, int Port) ParseServer()
|
|
|
|
|
{
|
2026-06-12 09:02:14 +02:00
|
|
|
var host = ServerHost.Trim();
|
|
|
|
|
var port = int.TryParse(ServerPort.Trim(), out var p)
|
2026-06-11 10:47:52 +02:00
|
|
|
? p
|
|
|
|
|
: UseSsl ? Profile.DefaultSslPort : Profile.DefaultTcpPort;
|
|
|
|
|
return (host, port);
|
|
|
|
|
}
|
|
|
|
|
}
|