2026-06-11 10:47:52 +02:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Collections.ObjectModel;
|
2026-06-12 11:47:23 +02:00
|
|
|
using System.Threading;
|
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;
|
|
|
|
|
|
|
|
|
|
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(
|
2026-06-13 21:15:03 +02:00
|
|
|
Loc Loc,
|
2026-06-11 21:39:32 +02:00
|
|
|
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>
|
2026-06-13 21:15:03 +02:00
|
|
|
/// ViewModel unico dell'applicazione (wizard + dashboard). Suddiviso in file
|
|
|
|
|
/// partial per area: Wizard, Settings, Sync, Send, Contacts, Receive.
|
2026-06-11 10:47:52 +02:00
|
|
|
/// </summary>
|
|
|
|
|
public partial class MainWindowViewModel : ViewModelBase
|
|
|
|
|
{
|
2026-06-13 21:15:03 +02:00
|
|
|
// ---- stato sessione wallet ----
|
2026-06-11 10:47:52 +02:00
|
|
|
private WalletDocument? _doc;
|
|
|
|
|
private HdAccount? _account;
|
|
|
|
|
private string? _walletPath;
|
|
|
|
|
private string? _password;
|
2026-06-13 20:33:59 +02:00
|
|
|
private WalletLock? _walletLock;
|
2026-06-13 21:15:03 +02:00
|
|
|
|
|
|
|
|
// ---- rete ----
|
2026-06-11 10:47:52 +02:00
|
|
|
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;
|
2026-06-13 21:15:03 +02:00
|
|
|
private bool _resyncRequested;
|
|
|
|
|
|
|
|
|
|
// ---- invio ----
|
2026-06-11 10:47:52 +02:00
|
|
|
private BuiltTransaction? _pendingSend;
|
|
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
// ---- dettaglio transazione ----
|
|
|
|
|
private CancellationTokenSource? _txDetailsCts;
|
2026-06-11 13:37:31 +02:00
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
// ---- configurazione e localizzazione ----
|
2026-06-11 16:23:02 +02:00
|
|
|
private AppConfig _config = AppConfig.Load();
|
2026-06-11 18:41:31 +02:00
|
|
|
private Loc _loc = Loc.Instance;
|
|
|
|
|
public Loc Loc => _loc;
|
2026-06-11 16:23:02 +02:00
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
// ---- wizard ----
|
|
|
|
|
private string? _pendingOpenPath;
|
|
|
|
|
private bool _isRestoreFlow;
|
|
|
|
|
|
|
|
|
|
// ---- keep-alive ----
|
|
|
|
|
private bool _autoReconnect;
|
|
|
|
|
private readonly DispatcherTimer _keepAliveTimer;
|
|
|
|
|
|
|
|
|
|
// ---- server UI sync ----
|
|
|
|
|
private bool _syncingServerFields;
|
|
|
|
|
|
|
|
|
|
// ---- proprietà di app ----
|
|
|
|
|
|
2026-06-12 12:01:08 +02:00
|
|
|
public static string AppVersion =>
|
|
|
|
|
typeof(MainWindowViewModel).Assembly.GetName().Version is { } v
|
|
|
|
|
? $"{v.Major}.{v.Minor}.{v.Build}"
|
|
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
public string WindowTitle => $"Palladium Wallet {AppVersion}";
|
|
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
/// <summary>true su desktop; false su Android/iOS — nasconde le funzioni legate al filesystem libero.</summary>
|
2026-06-12 16:06:37 +02:00
|
|
|
public bool IsDesktop => !OperatingSystem.IsAndroid() && !OperatingSystem.IsIOS();
|
|
|
|
|
|
2026-06-14 19:54:07 +02:00
|
|
|
public bool IsMobile => !IsDesktop;
|
|
|
|
|
|
2026-06-11 16:23:02 +02:00
|
|
|
public string UnitLabel => _config.Unit;
|
|
|
|
|
public AppConfig CurrentConfig => _config;
|
|
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
// ---- stato pannelli ----
|
2026-06-11 10:47:52 +02:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(IsSetupVisible))]
|
|
|
|
|
private bool isWalletOpen;
|
|
|
|
|
|
|
|
|
|
public bool IsSetupVisible => !IsWalletOpen;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string statusMessage = "";
|
|
|
|
|
|
2026-06-15 08:24:39 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private int selectedTabIndex;
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void SelectTab(string index) => SelectedTabIndex = int.Parse(index);
|
|
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
// ---- collections per la dashboard ----
|
2026-06-11 11:27:41 +02:00
|
|
|
|
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-13 21:15:03 +02:00
|
|
|
// ---- helpers ----
|
2026-06-12 11:47:23 +02:00
|
|
|
|
2026-06-15 10:35:38 +02:00
|
|
|
private NetKind Net => NetKind.Mainnet;
|
2026-06-13 21:15:03 +02:00
|
|
|
private ChainProfile Profile => ChainProfiles.For(Net);
|
|
|
|
|
private ServerRegistry Registry => new(Profile, AppPaths.ServersPath(Net));
|
2026-06-12 11:47:23 +02:00
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
private string Fmt(long sats, bool withLabel = true) =>
|
|
|
|
|
CoinAmount.FormatIn(sats, _config.Unit, withLabel);
|
2026-06-12 11:47:23 +02:00
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
private string KeyWif(bool isChange, int index)
|
2026-06-12 11:47:23 +02:00
|
|
|
{
|
2026-06-13 21:15:03 +02:00
|
|
|
if (_account is null or { IsWatchOnly: true }) return "";
|
2026-06-12 11:47:23 +02:00
|
|
|
try
|
|
|
|
|
{
|
2026-06-13 21:15:03 +02:00
|
|
|
return _account.GetExtPrivateKey(isChange, index)
|
|
|
|
|
.PrivateKey.GetWif(PalladiumNetworks.For(Net)).ToString();
|
2026-06-11 21:39:32 +02:00
|
|
|
}
|
2026-06-13 21:15:03 +02:00
|
|
|
catch { return ""; }
|
2026-06-11 21:39:32 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
// ---- costruttore ----
|
2026-06-11 10:47:52 +02:00
|
|
|
|
|
|
|
|
public MainWindowViewModel()
|
|
|
|
|
{
|
2026-06-11 18:41:31 +02:00
|
|
|
_loc = Loc.SwitchTo(_config.Language);
|
2026-06-12 09:25:00 +02:00
|
|
|
if (!AppPaths.IsDataLocationConfigured())
|
|
|
|
|
SetupStep = StepDataLocation;
|
|
|
|
|
else
|
|
|
|
|
RefreshSetupState();
|
2026-06-13 21:15:03 +02:00
|
|
|
_keepAliveTimer = new DispatcherTimer { Interval = System.TimeSpan.FromSeconds(20) };
|
2026-06-11 11:27:41 +02:00
|
|
|
_keepAliveTimer.Tick += async (_, _) => await KeepAliveTickAsync();
|
|
|
|
|
_keepAliveTimer.Start();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
private async System.Threading.Tasks.Task KeepAliveTickAsync()
|
2026-06-11 11:27:41 +02:00
|
|
|
{
|
2026-06-12 09:43:59 +02:00
|
|
|
if (IsSyncing)
|
2026-06-11 11:27:41 +02:00
|
|
|
return;
|
|
|
|
|
if (_client is { IsConnected: true })
|
|
|
|
|
{
|
2026-06-13 21:15:03 +02:00
|
|
|
try { await _client.PingAsync(); }
|
|
|
|
|
catch { }
|
2026-06-11 11:27:41 +02:00
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
// ---- ciclo di vita wallet ----
|
2026-06-12 09:02:14 +02:00
|
|
|
|
2026-06-11 10:47:52 +02:00
|
|
|
[RelayCommand]
|
|
|
|
|
private void CloseWallet()
|
|
|
|
|
{
|
2026-06-13 20:33:59 +02:00
|
|
|
_walletLock?.Dispose();
|
|
|
|
|
_walletLock = null;
|
2026-06-11 10:47:52 +02:00
|
|
|
_ = _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();
|
|
|
|
|
}
|
|
|
|
|
}
|