refactor(app): split MainWindowViewModel into partial files by area
1300-line monolith → 7 focused files (max 359 lines each): MainWindowViewModel.cs core fields, constructor, lifecycle MainWindowViewModel.Wizard.cs setup wizard and wallet open/close flows MainWindowViewModel.Settings.cs language, unit, overlay flags MainWindowViewModel.Sync.cs server config, connection, sync loop MainWindowViewModel.Send.cs send panel MainWindowViewModel.Contacts.cs contact management MainWindowViewModel.Receive.cs balance display, QR, addresses, tx details No behaviour change, no XAML change, 127/127 tests pass.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using PalladiumWallet.Core.Storage;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel
|
||||
{
|
||||
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 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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media.Imaging;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using PalladiumWallet.App.Localization;
|
||||
using PalladiumWallet.Core.Chain;
|
||||
using PalladiumWallet.Core.Spv;
|
||||
using PalladiumWallet.Core.Storage;
|
||||
using PalladiumWallet.Core.Wallet;
|
||||
using QRCoder;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel
|
||||
{
|
||||
// ---- saldo e info wallet ----
|
||||
|
||||
[ObservableProperty]
|
||||
private string balanceText = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
private string unconfirmedText = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string networkInfo = "";
|
||||
|
||||
// ---- indirizzo di ricezione e QR ----
|
||||
|
||||
[ObservableProperty]
|
||||
private string receiveAddress = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private Bitmap? receiveQr;
|
||||
|
||||
partial void OnReceiveAddressChanged(string value)
|
||||
{
|
||||
var previous = ReceiveQr;
|
||||
ReceiveQr = string.IsNullOrEmpty(value) ? null : GenerateQr(value);
|
||||
previous?.Dispose();
|
||||
}
|
||||
|
||||
public void NotifyAddressCopied() => StatusMessage = Loc.Tr("addr.copied");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- tab indirizzi ----
|
||||
|
||||
[ObservableProperty]
|
||||
private AddressRow? selectedAddressRow;
|
||||
|
||||
[ObservableProperty]
|
||||
private AddressInfo? addressInfo;
|
||||
|
||||
public void ShowAddressInfo(AddressRow row) =>
|
||||
AddressInfo = new AddressInfo(Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseAddressInfo() => AddressInfo = null;
|
||||
|
||||
// ---- overlay dettaglio transazione ----
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isTxDetailsOpen;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isTxDetailsLoading;
|
||||
|
||||
[ObservableProperty]
|
||||
private TransactionDetailsViewModel? txDetails;
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- aggiorna display dal risultato della sync ----
|
||||
|
||||
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();
|
||||
Addresses.Clear();
|
||||
for (var i = 0; i < 10; i++)
|
||||
Addresses.Add(new AddressRow(_loc["addr.receive"], i,
|
||||
_account.GetReceiveAddress(i).ToString(), "—", "—",
|
||||
false,
|
||||
_account.GetPublicKey(false, i).ToHex(),
|
||||
KeyWif(false, i),
|
||||
$"m/{_doc!.AccountPath}/0/{i}"));
|
||||
return;
|
||||
}
|
||||
BalanceText = Fmt(cache.ConfirmedSats);
|
||||
var pending = cache.History.Where(t => t.Height <= 0).Sum(t => t.DeltaSats);
|
||||
UnconfirmedText = pending != 0
|
||||
? $"{Loc.Tr("msg.pending")}: {(pending > 0 ? "+" : "")}{Fmt(pending)} — {Loc.Tr("msg.notspendable")}"
|
||||
: "";
|
||||
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",
|
||||
(tx.DeltaSats >= 0 ? "+" : "") + Fmt(tx.DeltaSats, withLabel: false),
|
||||
tx.Txid,
|
||||
tx.Verified ? "✓ SPV" : "—"));
|
||||
|
||||
Addresses.Clear();
|
||||
foreach (var a in cache.Addresses)
|
||||
Addresses.Add(new AddressRow(
|
||||
a.IsChange ? _loc["addr.change"] : _loc["addr.receive"],
|
||||
a.Index,
|
||||
a.Address,
|
||||
a.BalanceSats > 0 ? Fmt(a.BalanceSats, withLabel: false) : (a.TxCount > 0 ? "0" : "—"),
|
||||
a.TxCount > 0 ? a.TxCount.ToString() : "—",
|
||||
a.IsChange,
|
||||
_account.GetPublicKey(a.IsChange, a.Index).ToHex(),
|
||||
KeyWif(a.IsChange, a.Index),
|
||||
$"m/{_doc!.AccountPath}/{(a.IsChange ? 1 : 0)}/{a.Index}"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using NBitcoin;
|
||||
using PalladiumWallet.Core.Chain;
|
||||
using PalladiumWallet.Core.Net;
|
||||
using PalladiumWallet.Core.Wallet;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel
|
||||
{
|
||||
[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;
|
||||
|
||||
[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;
|
||||
if (!SendAll && !CoinAmount.TryParseIn(SendAmount, _config.Unit, out amount))
|
||||
{
|
||||
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]}… · " +
|
||||
$"fee {Fmt(_pendingSend.Fee.Satoshi)} " +
|
||||
$"({_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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel
|
||||
{
|
||||
// ---- spunte del menu Impostazioni (ToggleType Radio) ----
|
||||
|
||||
public bool IsLangIt => _config.Language == "it";
|
||||
public bool IsLangEn => _config.Language == "en";
|
||||
public bool IsLangEs => _config.Language == "es";
|
||||
public bool IsLangFr => _config.Language == "fr";
|
||||
public bool IsLangPt => _config.Language == "pt";
|
||||
public bool IsLangDe => _config.Language == "de";
|
||||
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);
|
||||
}
|
||||
|
||||
public void ApplySettings(PalladiumWallet.Core.Storage.AppConfig config)
|
||||
{
|
||||
_config = config;
|
||||
_config.Save();
|
||||
_loc = Localization.Loc.SwitchTo(config.Language);
|
||||
OnPropertyChanged(nameof(Loc));
|
||||
OnPropertyChanged(nameof(UnitLabel));
|
||||
OnPropertyChanged(nameof(IsLangIt));
|
||||
OnPropertyChanged(nameof(IsLangEn));
|
||||
OnPropertyChanged(nameof(IsLangEs));
|
||||
OnPropertyChanged(nameof(IsLangFr));
|
||||
OnPropertyChanged(nameof(IsLangPt));
|
||||
OnPropertyChanged(nameof(IsLangDe));
|
||||
OnPropertyChanged(nameof(IsUnitPlm));
|
||||
OnPropertyChanged(nameof(IsUnitMilli));
|
||||
OnPropertyChanged(nameof(IsUnitMicro));
|
||||
OnPropertyChanged(nameof(IsUnitSat));
|
||||
ApplyCache(_doc?.Cache);
|
||||
StatusMessage = Localization.Loc.Tr("msg.settings.saved");
|
||||
}
|
||||
|
||||
// ---- overlay impostazioni, server, help ----
|
||||
|
||||
[CommunityToolkit.Mvvm.ComponentModel.ObservableProperty]
|
||||
private bool isSettingsOpen;
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenSettings() => IsSettingsOpen = true;
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseSettings() => IsSettingsOpen = false;
|
||||
|
||||
[CommunityToolkit.Mvvm.ComponentModel.ObservableProperty]
|
||||
private bool isHelpOpen;
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenHelp() => IsHelpOpen = true;
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseHelp() => IsHelpOpen = false;
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using PalladiumWallet.App.Localization;
|
||||
using PalladiumWallet.Core.Net;
|
||||
using PalladiumWallet.Core.Spv;
|
||||
using PalladiumWallet.Core.Storage;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel
|
||||
{
|
||||
// ---- server e connessione ----
|
||||
|
||||
[ObservableProperty]
|
||||
private string serverHost = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string serverPort = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool useSsl = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isServerSettingsOpen;
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenServerSettings()
|
||||
{
|
||||
IsSettingsOpen = false;
|
||||
IsServerSettingsOpen = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseServerSettings() => IsServerSettingsOpen = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private string connectionStatus = Loc.Tr("conn.none");
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isConnected;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isSyncing;
|
||||
|
||||
public ObservableCollection<KnownServer> KnownServers { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private KnownServer? selectedKnownServer;
|
||||
|
||||
partial void OnSelectedKnownServerChanged(KnownServer? value)
|
||||
{
|
||||
if (value is null)
|
||||
return;
|
||||
_syncingServerFields = true;
|
||||
ServerHost = value.Host;
|
||||
ServerPort = value.PortFor(UseSsl).ToString();
|
||||
_syncingServerFields = false;
|
||||
}
|
||||
|
||||
partial void OnUseSslChanged(bool value)
|
||||
{
|
||||
if (_syncingServerFields)
|
||||
return;
|
||||
_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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshServers()
|
||||
{
|
||||
KnownServers.Clear();
|
||||
foreach (var server in Registry.All)
|
||||
KnownServers.Add(server);
|
||||
SelectedKnownServer = KnownServers.FirstOrDefault();
|
||||
if (SelectedKnownServer is null)
|
||||
{
|
||||
ServerHost = "127.0.0.1";
|
||||
ServerPort = (UseSsl ? Profile.DefaultSslPort : Profile.DefaultTcpPort).ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private (string Host, int Port) ParseServer()
|
||||
{
|
||||
var host = ServerHost.Trim();
|
||||
var port = int.TryParse(ServerPort.Trim(), out var p)
|
||||
? p
|
||||
: UseSsl ? Profile.DefaultSslPort : Profile.DefaultTcpPort;
|
||||
return (host, port);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ConnectAndSync()
|
||||
{
|
||||
if (IsSyncing)
|
||||
{
|
||||
_resyncRequested = true;
|
||||
return;
|
||||
}
|
||||
IsSyncing = true;
|
||||
StatusMessage = "";
|
||||
try
|
||||
{
|
||||
var (host, port) = ParseServer();
|
||||
|
||||
if (_client is { } current &&
|
||||
(current.Host != host || current.Port != port || current.UseSsl != UseSsl))
|
||||
{
|
||||
await DisconnectAsync();
|
||||
}
|
||||
|
||||
if (_client is null || !_client.IsConnected)
|
||||
{
|
||||
ConnectionStatus = $"{Loc.Tr("conn.connectingto")} {host}:{port}…";
|
||||
var pins = new CertificatePinStore(AppPaths.CertificatePinsPath(Net));
|
||||
_client = await ElectrumClient.ConnectAsync(host, port, UseSsl, pins);
|
||||
_client.NotificationReceived += OnServerNotification;
|
||||
_client.Disconnected += _ => Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
IsConnected = false;
|
||||
ConnectionStatus = Loc.Tr("conn.disconnected");
|
||||
});
|
||||
IsConnected = true;
|
||||
_autoReconnect = true;
|
||||
ConnectionStatus = $"{Loc.Tr("conn.connectedto")} {host}:{port}{(UseSsl ? " (TLS)" : "")}";
|
||||
}
|
||||
|
||||
if (_account is null || _doc is null)
|
||||
return;
|
||||
|
||||
if (_synchronizer is null)
|
||||
{
|
||||
_synchronizer = new WalletSynchronizer(_account, _client, _doc.GapLimit);
|
||||
_synchronizer.Progress += msg => Dispatcher.UIThread.Post(() => StatusMessage = msg);
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
_resyncRequested = false;
|
||||
var result = await _synchronizer.SyncOnceAsync();
|
||||
_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);
|
||||
StatusMessage = $"{Loc.Tr("msg.synced")}: {Loc.Tr("msg.height")} {result.TipHeight}, " +
|
||||
$"{result.History.Count} {Loc.Tr("msg.synced.detail")}";
|
||||
} while (_resyncRequested);
|
||||
}
|
||||
catch (CertificatePinMismatchException ex)
|
||||
{
|
||||
IsConnected = false;
|
||||
ConnectionStatus = Loc.Tr("conn.certchanged");
|
||||
StatusMessage = ex.Message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IsConnected = _client?.IsConnected == true;
|
||||
ConnectionStatus = IsConnected ? ConnectionStatus : Loc.Tr("conn.error");
|
||||
StatusMessage = $"Errore: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSyncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DiscoverServers()
|
||||
{
|
||||
if (_client is null || !_client.IsConnected)
|
||||
{
|
||||
StatusMessage = Loc.Tr("conn.none") + ".";
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnServerNotification(string method, System.Text.Json.JsonElement payload)
|
||||
{
|
||||
if (method is "blockchain.scripthash.subscribe" or "blockchain.headers.subscribe")
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (IsSyncing)
|
||||
_resyncRequested = true;
|
||||
else
|
||||
_ = ConnectAndSync();
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetCertificates()
|
||||
{
|
||||
new CertificatePinStore(AppPaths.CertificatePinsPath(Net)).ResetAll();
|
||||
StatusMessage = Loc.Tr("msg.certreset");
|
||||
}
|
||||
|
||||
private async Task DisconnectAsync()
|
||||
{
|
||||
if (_client is { } client)
|
||||
{
|
||||
_client = null;
|
||||
_synchronizer = null;
|
||||
try { await client.DisposeAsync(); } catch { }
|
||||
}
|
||||
IsConnected = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using PalladiumWallet.App.Localization;
|
||||
using PalladiumWallet.Core.Chain;
|
||||
using PalladiumWallet.Core.Crypto;
|
||||
using PalladiumWallet.Core.Storage;
|
||||
using PalladiumWallet.Core.Wallet;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel
|
||||
{
|
||||
// ---- wizard di setup
|
||||
|
||||
public const string StepDataLocation = "data-location";
|
||||
public const string StepStart = "start";
|
||||
public const string StepChooseWallet = "choose-wallet";
|
||||
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]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepDataLocation))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepStart))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepChooseWallet))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepOpen))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepShowSeed))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepConfirmSeed))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepWords))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepPassphrase))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStepPassword))]
|
||||
private string setupStep = StepStart;
|
||||
|
||||
public bool IsStepDataLocation => SetupStep == StepDataLocation;
|
||||
public bool IsStepStart => SetupStep == StepStart;
|
||||
public bool IsStepChooseWallet => SetupStep == StepChooseWallet;
|
||||
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;
|
||||
|
||||
public string DefaultDataPath => AppPaths.DefaultDataRoot();
|
||||
|
||||
[ObservableProperty]
|
||||
private string mnemonicInput = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string confirmMnemonicInput = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string passphraseInput = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string passwordInput = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string confirmPasswordInput = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool encryptWallet = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool walletFileExists;
|
||||
|
||||
public ObservableCollection<WalletFileEntry> WalletList { get; } = [];
|
||||
|
||||
[RelayCommand]
|
||||
private void UseDefaultDataLocation() => ApplyDataLocation(AppPaths.DefaultDataRoot());
|
||||
|
||||
public void ApplyDataLocation(string root)
|
||||
{
|
||||
try
|
||||
{
|
||||
AppPaths.ConfigureDataLocation(root);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"{Loc.Tr("msg.error")}: {ex.Message}";
|
||||
return;
|
||||
}
|
||||
_config = AppConfig.Load();
|
||||
_loc = Loc.SwitchTo(_config.Language);
|
||||
OnPropertyChanged(nameof(Loc));
|
||||
OnPropertyChanged(nameof(UnitLabel));
|
||||
RefreshSetupState();
|
||||
}
|
||||
|
||||
private void RefreshSetupState()
|
||||
{
|
||||
SetupStep = StepStart;
|
||||
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ConfirmPasswordInput = "";
|
||||
WalletFileExists = AppPaths.WalletFiles(Net).Count > 0;
|
||||
StatusMessage = "";
|
||||
RefreshServers();
|
||||
_ = ConnectAndSync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void WizardStartOpen()
|
||||
{
|
||||
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 = "";
|
||||
return;
|
||||
}
|
||||
_pendingOpenPath = files.Count == 1 ? files[0] : AppPaths.DefaultWalletPath(Net);
|
||||
PasswordInput = "";
|
||||
SetupStep = StepOpen;
|
||||
StatusMessage = "";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ChooseWallet(WalletFileEntry? entry)
|
||||
{
|
||||
if (entry is null)
|
||||
return;
|
||||
_pendingOpenPath = entry.Path;
|
||||
PasswordInput = "";
|
||||
SetupStep = StepOpen;
|
||||
StatusMessage = "";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void WizardStartNew()
|
||||
{
|
||||
_isRestoreFlow = false;
|
||||
MnemonicInput = Bip39.Generate(MnemonicLength.Twelve).ToString();
|
||||
SetupStep = StepShowSeed;
|
||||
StatusMessage = "";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void WizardStartRestore()
|
||||
{
|
||||
_isRestoreFlow = true;
|
||||
MnemonicInput = "";
|
||||
SetupStep = StepWords;
|
||||
StatusMessage = "";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void WizardNextFromShowSeed()
|
||||
{
|
||||
ConfirmMnemonicInput = "";
|
||||
SetupStep = StepConfirmSeed;
|
||||
StatusMessage = "";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void WizardNextFromConfirmSeed()
|
||||
{
|
||||
var normalized = string.Join(' ',
|
||||
ConfirmMnemonicInput.Split(' ', StringSplitOptions.RemoveEmptyEntries));
|
||||
if (!string.Equals(normalized, MnemonicInput, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
StatusMessage = Loc.Tr("msg.seed.mismatch");
|
||||
return;
|
||||
}
|
||||
GoToPassphraseStep();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void WizardNextFromWords()
|
||||
{
|
||||
if (!Bip39.TryParse(MnemonicInput, out _))
|
||||
{
|
||||
StatusMessage = Loc.Tr("msg.words.invalid");
|
||||
return;
|
||||
}
|
||||
GoToPassphraseStep();
|
||||
}
|
||||
|
||||
private void GoToPassphraseStep()
|
||||
{
|
||||
PassphraseInput = "";
|
||||
SetupStep = StepPassphrase;
|
||||
StatusMessage = "";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void WizardNextFromPassphrase()
|
||||
{
|
||||
PasswordInput = ConfirmPasswordInput = "";
|
||||
EncryptWallet = true;
|
||||
SetupStep = StepPassword;
|
||||
StatusMessage = "";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void WizardBack()
|
||||
{
|
||||
SetupStep = SetupStep switch
|
||||
{
|
||||
StepOpen => WalletList.Count > 1 ? StepChooseWallet : StepStart,
|
||||
StepChooseWallet or StepShowSeed or StepWords => StepStart,
|
||||
StepConfirmSeed => StepShowSeed,
|
||||
StepPassphrase => _isRestoreFlow ? StepWords : StepConfirmSeed,
|
||||
StepPassword => StepPassphrase,
|
||||
_ => StepStart,
|
||||
};
|
||||
if (SetupStep == StepStart)
|
||||
RefreshSetupState();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CreateOrRestore()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var (doc, account) = WalletLoader.NewFromMnemonic(
|
||||
MnemonicInput,
|
||||
string.IsNullOrEmpty(PassphraseInput) ? null : PassphraseInput,
|
||||
ScriptKind.NativeSegwit,
|
||||
Profile);
|
||||
var path = AppPaths.DefaultWalletPath(Net);
|
||||
for (var n = 2; WalletStore.Exists(path); n++)
|
||||
path = Path.Combine(AppPaths.WalletsDir(Net), $"wallet-{n}.wallet.json");
|
||||
WalletStore.Save(doc, path, password);
|
||||
var newLock = WalletLock.TryAcquire(path);
|
||||
if (newLock is null) { StatusMessage = Loc.Tr("msg.wallet.locked"); return; }
|
||||
OpenLoaded(doc, account, path, password, newLock);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Errore: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenExisting()
|
||||
{
|
||||
var path = _pendingOpenPath ?? AppPaths.DefaultWalletPath(Net);
|
||||
var password = string.IsNullOrEmpty(PasswordInput) ? null : PasswordInput;
|
||||
|
||||
var newLock = WalletLock.TryAcquire(path);
|
||||
if (newLock is null) { StatusMessage = Loc.Tr("msg.wallet.locked"); return; }
|
||||
|
||||
try
|
||||
{
|
||||
var doc = WalletStore.Load(path, password);
|
||||
_pendingOpenPath = null;
|
||||
OpenLoaded(doc, WalletLoader.ToAccount(doc), path, password, newLock);
|
||||
}
|
||||
catch (WrongPasswordException)
|
||||
{
|
||||
newLock.Dispose();
|
||||
StatusMessage = Loc.Tr("msg.wrongpassword");
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
newLock.Dispose();
|
||||
StatusMessage = Loc.Tr("msg.wallet.noaccess");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
newLock.Dispose();
|
||||
StatusMessage = $"Errore: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenFromPath(string path)
|
||||
{
|
||||
var newLock = WalletLock.TryAcquire(path);
|
||||
if (newLock is null) { StatusMessage = Loc.Tr("msg.wallet.locked"); return; }
|
||||
|
||||
try
|
||||
{
|
||||
var doc = WalletStore.Load(path);
|
||||
if (IsWalletOpen)
|
||||
CloseWallet();
|
||||
OpenLoaded(doc, WalletLoader.ToAccount(doc), path, password: null, newLock);
|
||||
}
|
||||
catch (WrongPasswordException)
|
||||
{
|
||||
newLock.Dispose();
|
||||
if (IsWalletOpen)
|
||||
CloseWallet();
|
||||
_pendingOpenPath = path;
|
||||
WalletFileExists = true;
|
||||
PasswordInput = "";
|
||||
SetupStep = StepOpen;
|
||||
StatusMessage = "";
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
newLock.Dispose();
|
||||
StatusMessage = Loc.Tr("msg.wallet.noaccess");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
newLock.Dispose();
|
||||
StatusMessage = $"Errore: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void NewWallet()
|
||||
{
|
||||
if (IsWalletOpen)
|
||||
CloseWallet();
|
||||
_pendingOpenPath = null;
|
||||
StatusMessage = "";
|
||||
}
|
||||
|
||||
private void OpenLoaded(
|
||||
WalletDocument doc, HdAccount account, string path, string? password, WalletLock walletLock)
|
||||
{
|
||||
_walletLock?.Dispose();
|
||||
_walletLock = walletLock;
|
||||
|
||||
SelectedNetwork = doc.Network;
|
||||
_doc = doc;
|
||||
_account = account;
|
||||
_walletPath = path;
|
||||
_password = password;
|
||||
MnemonicInput = ConfirmMnemonicInput = PassphraseInput = PasswordInput = ConfirmPasswordInput = "";
|
||||
SetupStep = StepStart;
|
||||
|
||||
NetworkInfo = $"{doc.Network} · {doc.ScriptKind} · m/{doc.AccountPath}"
|
||||
+ (doc.IsWatchOnly ? " · watch-only" : "");
|
||||
LoadContacts();
|
||||
ApplyCache(doc.Cache);
|
||||
IsWalletOpen = true;
|
||||
StatusMessage = Loc.Tr("msg.opened");
|
||||
_ = ConnectAndSync();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user