2026-06-13 21:15:03 +02:00
|
|
|
using System;
|
2026-06-15 14:23:23 +02:00
|
|
|
using System.Collections.Generic;
|
2026-06-13 21:15:03 +02:00
|
|
|
using System.Collections.ObjectModel;
|
|
|
|
|
using System.Linq;
|
2026-07-02 14:56:47 +02:00
|
|
|
using System.Threading;
|
2026-06-13 21:15:03 +02:00
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Avalonia.Threading;
|
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
|
|
|
|
using PalladiumWallet.App.Localization;
|
2026-06-15 14:23:23 +02:00
|
|
|
using PalladiumWallet.Core.Chain;
|
2026-06-13 21:15:03 +02:00
|
|
|
using PalladiumWallet.Core.Net;
|
|
|
|
|
using PalladiumWallet.Core.Spv;
|
|
|
|
|
using PalladiumWallet.Core.Storage;
|
|
|
|
|
|
|
|
|
|
namespace PalladiumWallet.App.ViewModels;
|
|
|
|
|
|
|
|
|
|
public partial class MainWindowViewModel
|
|
|
|
|
{
|
2026-06-16 14:40:06 +02:00
|
|
|
// ---- server and connection ----
|
2026-06-13 21:15:03 +02:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string serverHost = "";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string serverPort = "";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool useSsl = true;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isServerSettingsOpen;
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-18 19:28:19 +02:00
|
|
|
private async Task OpenServerSettings()
|
2026-06-13 21:15:03 +02:00
|
|
|
{
|
|
|
|
|
IsSettingsOpen = false;
|
2026-06-18 19:28:19 +02:00
|
|
|
RefreshServers();
|
2026-06-13 21:15:03 +02:00
|
|
|
IsServerSettingsOpen = true;
|
2026-06-18 19:28:19 +02:00
|
|
|
if (_client is { IsConnected: true })
|
|
|
|
|
await DiscoverServers();
|
2026-06-13 21:15:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void CloseServerSettings() => IsServerSettingsOpen = false;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string connectionStatus = Loc.Tr("conn.none");
|
|
|
|
|
|
2026-06-18 19:34:36 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private string connectionStatusShort = Loc.Tr("conn.none");
|
|
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isConnected;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isSyncing;
|
|
|
|
|
|
2026-07-02 14:56:47 +02:00
|
|
|
private CancellationTokenSource _syncCts = new();
|
|
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
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);
|
2026-06-18 19:34:36 +02:00
|
|
|
|
|
|
|
|
if (_config.LastServerHost is { Length: > 0 } savedHost && _config.LastServerPort is { } savedPort)
|
2026-06-13 21:15:03 +02:00
|
|
|
{
|
2026-06-18 19:34:36 +02:00
|
|
|
_syncingServerFields = true;
|
|
|
|
|
UseSsl = _config.LastServerUseSsl;
|
|
|
|
|
ServerHost = savedHost;
|
|
|
|
|
ServerPort = savedPort.ToString();
|
|
|
|
|
SelectedKnownServer = KnownServers.FirstOrDefault(s => s.Host == savedHost);
|
|
|
|
|
_syncingServerFields = false;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
SelectedKnownServer = KnownServers.FirstOrDefault();
|
|
|
|
|
if (SelectedKnownServer is null)
|
|
|
|
|
{
|
|
|
|
|
ServerHost = "127.0.0.1";
|
|
|
|
|
ServerPort = (UseSsl ? Profile.DefaultSslPort : Profile.DefaultTcpPort).ToString();
|
|
|
|
|
}
|
2026-06-13 21:15:03 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 14:23:23 +02:00
|
|
|
/// <summary>
|
2026-06-16 14:40:06 +02:00
|
|
|
/// Returns servers to try in order: first the one selected in the UI
|
|
|
|
|
/// (or manually typed), then the remaining KnownServers, with duplicates skipped.
|
2026-06-15 14:23:23 +02:00
|
|
|
/// </summary>
|
|
|
|
|
private IEnumerable<(string Host, int Port)> BuildServerCandidates(string selectedHost, int selectedPort)
|
|
|
|
|
{
|
|
|
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
2026-06-16 14:40:06 +02:00
|
|
|
// 1. Current server (selected or manually typed).
|
2026-06-15 14:23:23 +02:00
|
|
|
if (!string.IsNullOrWhiteSpace(selectedHost))
|
|
|
|
|
{
|
|
|
|
|
var key = $"{selectedHost}:{selectedPort}";
|
|
|
|
|
if (seen.Add(key))
|
|
|
|
|
yield return (selectedHost, selectedPort);
|
|
|
|
|
}
|
2026-06-16 14:40:06 +02:00
|
|
|
// 2. Other known servers, in list order.
|
2026-06-15 14:23:23 +02:00
|
|
|
foreach (var s in KnownServers)
|
|
|
|
|
{
|
|
|
|
|
var p = s.PortFor(UseSsl);
|
|
|
|
|
var key = $"{s.Host}:{p}";
|
|
|
|
|
if (seen.Add(key))
|
|
|
|
|
yield return (s.Host, p);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task ConnectAndSync()
|
|
|
|
|
{
|
|
|
|
|
if (IsSyncing)
|
|
|
|
|
{
|
2026-07-02 14:56:47 +02:00
|
|
|
var (newHost, newPort) = ParseServer();
|
|
|
|
|
bool serverChanged = _client is null
|
|
|
|
|
|| _client.Host != newHost
|
|
|
|
|
|| _client.Port != newPort
|
|
|
|
|
|| _client.UseSsl != UseSsl;
|
|
|
|
|
if (serverChanged)
|
|
|
|
|
_syncCts.Cancel();
|
|
|
|
|
else
|
|
|
|
|
_resyncRequested = true;
|
2026-06-13 21:15:03 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2026-07-02 14:56:47 +02:00
|
|
|
|
|
|
|
|
_syncCts = new CancellationTokenSource();
|
|
|
|
|
var ct = _syncCts.Token;
|
2026-06-13 21:15:03 +02:00
|
|
|
IsSyncing = true;
|
|
|
|
|
StatusMessage = "";
|
2026-07-02 14:56:47 +02:00
|
|
|
bool cancelled = false;
|
2026-06-13 21:15:03 +02:00
|
|
|
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)
|
|
|
|
|
{
|
2026-06-16 14:40:06 +02:00
|
|
|
// Persist caches before destroying the synchronizer: the new
|
|
|
|
|
// synchronizer will use a different client and must be recreated,
|
|
|
|
|
// but already-downloaded data is preserved via _doc.Cache.
|
2026-06-15 14:23:23 +02:00
|
|
|
PersistPartialTxCache();
|
|
|
|
|
_synchronizer = null;
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
// Try all known servers in order; starts with the selected one
|
|
|
|
|
// and walks the list until the first that responds.
|
2026-06-15 14:23:23 +02:00
|
|
|
var candidates = BuildServerCandidates(host, port);
|
|
|
|
|
Exception? lastError = null;
|
|
|
|
|
foreach (var (h, p) in candidates)
|
2026-06-13 21:15:03 +02:00
|
|
|
{
|
2026-07-02 14:56:47 +02:00
|
|
|
ct.ThrowIfCancellationRequested();
|
2026-06-15 14:23:23 +02:00
|
|
|
ConnectionStatus = $"{Loc.Tr("conn.connectingto")} {h}:{p}…";
|
2026-06-18 19:34:36 +02:00
|
|
|
ConnectionStatusShort = Loc.Tr("conn.connectingto") + "…";
|
2026-06-15 14:23:23 +02:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var pins = new CertificatePinStore(AppPaths.CertificatePinsPath(Net));
|
2026-07-02 14:56:47 +02:00
|
|
|
_client = await ElectrumClient.ConnectAsync(h, p, UseSsl, pins, ct);
|
2026-06-15 14:23:23 +02:00
|
|
|
_client.NotificationReceived += OnServerNotification;
|
|
|
|
|
_client.Disconnected += _ => Dispatcher.UIThread.Post(() =>
|
|
|
|
|
{
|
|
|
|
|
IsConnected = false;
|
|
|
|
|
ConnectionStatus = Loc.Tr("conn.none");
|
2026-06-18 19:34:36 +02:00
|
|
|
ConnectionStatusShort = Loc.Tr("conn.none");
|
2026-06-15 14:23:23 +02:00
|
|
|
});
|
|
|
|
|
IsConnected = true;
|
|
|
|
|
_autoReconnect = true;
|
2026-06-18 19:28:19 +02:00
|
|
|
ConnectionStatus = $"{Loc.Tr("conn.connectedto")} {h}:{p}";
|
2026-06-18 19:34:36 +02:00
|
|
|
ConnectionStatusShort = Loc.Tr("conn.connectedto");
|
2026-06-16 14:40:06 +02:00
|
|
|
// Update the UI to reflect the server actually connected.
|
2026-06-15 14:23:23 +02:00
|
|
|
_syncingServerFields = true;
|
|
|
|
|
ServerHost = h;
|
|
|
|
|
ServerPort = p.ToString();
|
|
|
|
|
SelectedKnownServer = KnownServers.FirstOrDefault(s => s.Host == h)
|
|
|
|
|
?? SelectedKnownServer;
|
|
|
|
|
_syncingServerFields = false;
|
2026-06-18 19:34:36 +02:00
|
|
|
// Persist the last-used server so it is restored on next launch.
|
|
|
|
|
_config.LastServerHost = h;
|
|
|
|
|
_config.LastServerPort = p;
|
|
|
|
|
_config.LastServerUseSsl = UseSsl;
|
|
|
|
|
_config.Save();
|
2026-06-15 14:23:23 +02:00
|
|
|
lastError = null;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-07-02 14:56:47 +02:00
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
throw;
|
|
|
|
|
}
|
2026-06-15 14:23:23 +02:00
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
lastError = ex;
|
|
|
|
|
_client = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (lastError is not null)
|
|
|
|
|
throw lastError;
|
2026-06-13 21:15:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_account is null || _doc is null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (_synchronizer is null)
|
|
|
|
|
{
|
2026-06-15 14:23:23 +02:00
|
|
|
_synchronizer = new WalletSynchronizer(_account, _client!, _doc.GapLimit);
|
2026-06-16 14:40:06 +02:00
|
|
|
// Reload from disk cache: avoids re-downloading already known transactions
|
|
|
|
|
// (essential for wallets with thousands of historical txs — prevents -101).
|
2026-06-15 14:23:23 +02:00
|
|
|
var net = PalladiumNetworks.For(_account.Profile.Kind);
|
|
|
|
|
_synchronizer.PreloadCaches(
|
|
|
|
|
_doc.Cache?.RawTxHex ?? [],
|
|
|
|
|
_doc.Cache?.VerifiedAt ?? [],
|
2026-06-16 09:27:40 +02:00
|
|
|
_doc.Cache?.BlockHeaders,
|
|
|
|
|
_doc.Cache?.NextReceiveIndex ?? 0,
|
|
|
|
|
_doc.Cache?.NextChangeIndex ?? 0,
|
2026-06-15 14:23:23 +02:00
|
|
|
net);
|
2026-06-13 21:15:03 +02:00
|
|
|
_synchronizer.Progress += msg => Dispatcher.UIThread.Post(() => StatusMessage = msg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
_resyncRequested = false;
|
2026-07-02 14:56:47 +02:00
|
|
|
var result = await _synchronizer.SyncOnceAsync(ct);
|
2026-06-13 21:15:03 +02:00
|
|
|
_lastTransactions = result.Transactions;
|
|
|
|
|
|
2026-06-16 09:27:40 +02:00
|
|
|
var (rawHex, verifiedAt, blockHeaders) = _synchronizer.ExportCaches(
|
2026-06-15 14:23:23 +02:00
|
|
|
PalladiumNetworks.For(_account.Profile.Kind));
|
2026-06-13 21:15:03 +02:00
|
|
|
_doc.Cache = new SyncCache
|
|
|
|
|
{
|
|
|
|
|
TipHeight = result.TipHeight,
|
|
|
|
|
ConfirmedSats = result.ConfirmedSats,
|
|
|
|
|
UnconfirmedSats = result.UnconfirmedSats,
|
2026-07-02 18:24:26 +02:00
|
|
|
ImmatureSats = result.ImmatureSats,
|
2026-06-13 21:15:03 +02:00
|
|
|
NextReceiveIndex = result.NextReceiveIndex,
|
|
|
|
|
NextChangeIndex = result.NextChangeIndex,
|
|
|
|
|
History = [.. result.History],
|
|
|
|
|
Utxos = [.. result.Utxos],
|
|
|
|
|
Addresses = [.. result.AddressRows],
|
2026-06-15 14:23:23 +02:00
|
|
|
RawTxHex = rawHex,
|
|
|
|
|
VerifiedAt = verifiedAt,
|
2026-06-16 09:27:40 +02:00
|
|
|
BlockHeaders = blockHeaders,
|
2026-06-13 21:15:03 +02:00
|
|
|
};
|
|
|
|
|
WalletStore.Save(_doc, _walletPath!, _password);
|
|
|
|
|
ApplyCache(_doc.Cache);
|
2026-06-15 14:23:23 +02:00
|
|
|
_syncFailed = false;
|
2026-06-13 21:15:03 +02:00
|
|
|
StatusMessage = $"{Loc.Tr("msg.synced")}: {Loc.Tr("msg.height")} {result.TipHeight}, " +
|
|
|
|
|
$"{result.History.Count} {Loc.Tr("msg.synced.detail")}";
|
2026-07-02 14:56:47 +02:00
|
|
|
} while (_resyncRequested && !ct.IsCancellationRequested);
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
// Intentional cancellation due to server change request — not an error.
|
|
|
|
|
cancelled = true;
|
|
|
|
|
IsConnected = false;
|
|
|
|
|
ConnectionStatus = Loc.Tr("conn.none");
|
|
|
|
|
ConnectionStatusShort = Loc.Tr("conn.none");
|
|
|
|
|
StatusMessage = "";
|
2026-06-13 21:15:03 +02:00
|
|
|
}
|
|
|
|
|
catch (CertificatePinMismatchException ex)
|
|
|
|
|
{
|
|
|
|
|
IsConnected = false;
|
2026-06-14 21:40:15 +02:00
|
|
|
ConnectionStatus = Loc.Tr("conn.none");
|
2026-06-18 19:34:36 +02:00
|
|
|
ConnectionStatusShort = Loc.Tr("conn.none");
|
2026-06-13 21:15:03 +02:00
|
|
|
StatusMessage = ex.Message;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
IsConnected = _client?.IsConnected == true;
|
2026-06-14 21:40:15 +02:00
|
|
|
ConnectionStatus = IsConnected ? ConnectionStatus : Loc.Tr("conn.none");
|
2026-06-18 19:34:36 +02:00
|
|
|
ConnectionStatusShort = IsConnected ? Loc.Tr("conn.connectedto") : Loc.Tr("conn.none");
|
2026-06-13 21:15:03 +02:00
|
|
|
StatusMessage = $"Errore: {ex.Message}";
|
2026-06-15 14:23:23 +02:00
|
|
|
if (_account is not null)
|
|
|
|
|
{
|
|
|
|
|
_syncFailed = true;
|
2026-06-16 14:40:06 +02:00
|
|
|
// Persist already-downloaded transactions: on retry the synchronizer
|
|
|
|
|
// resumes from here instead of starting from scratch (e.g. after -101).
|
2026-06-15 14:23:23 +02:00
|
|
|
PersistPartialTxCache();
|
|
|
|
|
}
|
2026-06-13 21:15:03 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
IsSyncing = false;
|
|
|
|
|
}
|
2026-07-02 14:56:47 +02:00
|
|
|
|
|
|
|
|
// If cancelled due to a server change, restart immediately with the new server.
|
|
|
|
|
if (cancelled)
|
|
|
|
|
_ = ConnectAndSync();
|
2026-06-13 21:15:03 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-02 19:24:09 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// Peer discovery. Always clickable: if the wallet is already connected, it reuses
|
|
|
|
|
/// that connection; otherwise it opens a short-lived connection to a candidate server
|
|
|
|
|
/// just to query its peer list, without touching the wallet's connection state.
|
|
|
|
|
/// </summary>
|
2026-06-13 21:15:03 +02:00
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task DiscoverServers()
|
|
|
|
|
{
|
2026-07-02 19:24:09 +02:00
|
|
|
if (_client is { IsConnected: true } connected)
|
2026-06-13 21:15:03 +02:00
|
|
|
{
|
2026-07-02 19:24:09 +02:00
|
|
|
await DiscoverServersUsing(connected);
|
2026-06-13 21:15:03 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2026-07-02 19:24:09 +02:00
|
|
|
|
|
|
|
|
var (host, port) = ParseServer();
|
|
|
|
|
ElectrumClient? temp = null;
|
|
|
|
|
Exception? lastError = null;
|
|
|
|
|
foreach (var (h, p) in BuildServerCandidates(host, port))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var pins = new CertificatePinStore(AppPaths.CertificatePinsPath(Net));
|
|
|
|
|
temp = await ElectrumClient.ConnectAsync(h, p, UseSsl, pins);
|
|
|
|
|
lastError = null;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
lastError = ex;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (temp is null)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = $"Errore nella scoperta peer: {lastError?.Message ?? Loc.Tr("conn.none")}";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await DiscoverServersUsing(temp);
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
await temp.DisposeAsync();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task DiscoverServersUsing(ElectrumClient client)
|
|
|
|
|
{
|
2026-06-13 21:15:03 +02:00
|
|
|
try
|
|
|
|
|
{
|
2026-07-02 19:24:09 +02:00
|
|
|
var added = await Registry.DiscoverAsync(client);
|
2026-06-13 21:15:03 +02:00
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 14:23:23 +02:00
|
|
|
/// <summary>
|
2026-06-16 14:40:06 +02:00
|
|
|
/// Persists the transactions and Merkle proofs already accumulated by the synchronizer
|
|
|
|
|
/// into SyncCache, even if sync is not yet complete. Allows the next retry
|
|
|
|
|
/// (or app restart) to resume without re-downloading from scratch.
|
2026-06-15 14:23:23 +02:00
|
|
|
/// </summary>
|
|
|
|
|
private void PersistPartialTxCache()
|
|
|
|
|
{
|
|
|
|
|
if (_synchronizer is null || _doc is null || _walletPath is null || _account is null)
|
|
|
|
|
return;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var net = PalladiumNetworks.For(_account.Profile.Kind);
|
2026-06-16 09:27:40 +02:00
|
|
|
var (rawHex, verifiedAt, blockHeaders) = _synchronizer.ExportCaches(net);
|
2026-06-15 14:23:23 +02:00
|
|
|
if (rawHex.Count == 0 && verifiedAt.Count == 0)
|
|
|
|
|
return;
|
|
|
|
|
(_doc.Cache ??= new SyncCache()).RawTxHex = rawHex;
|
|
|
|
|
_doc.Cache.VerifiedAt = verifiedAt;
|
2026-06-16 09:27:40 +02:00
|
|
|
_doc.Cache.BlockHeaders = blockHeaders;
|
2026-06-15 14:23:23 +02:00
|
|
|
WalletStore.Save(_doc, _walletPath, _password);
|
|
|
|
|
}
|
2026-06-16 14:40:06 +02:00
|
|
|
catch { /* non-fatal: the next full save will recover */ }
|
2026-06-15 14:23:23 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
private async Task DisconnectAsync()
|
|
|
|
|
{
|
|
|
|
|
if (_client is { } client)
|
|
|
|
|
{
|
|
|
|
|
_client = null;
|
|
|
|
|
_synchronizer = null;
|
|
|
|
|
try { await client.DisposeAsync(); } catch { }
|
|
|
|
|
}
|
|
|
|
|
IsConnected = false;
|
|
|
|
|
}
|
|
|
|
|
}
|