feat(app): Avalonia UI shell
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="PalladiumWallet.App.App"
|
||||||
|
xmlns:local="using:PalladiumWallet.App"
|
||||||
|
RequestedThemeVariant="Default">
|
||||||
|
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||||
|
|
||||||
|
<Application.DataTemplates>
|
||||||
|
<local:ViewLocator/>
|
||||||
|
</Application.DataTemplates>
|
||||||
|
|
||||||
|
<Application.Styles>
|
||||||
|
<FluentTheme />
|
||||||
|
</Application.Styles>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Data.Core;
|
||||||
|
using Avalonia.Data.Core.Plugins;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using PalladiumWallet.App.ViewModels;
|
||||||
|
using PalladiumWallet.App.Views;
|
||||||
|
|
||||||
|
namespace PalladiumWallet.App;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted()
|
||||||
|
{
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
desktop.MainWindow = new MainWindow
|
||||||
|
{
|
||||||
|
DataContext = new MainWindowViewModel(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PalladiumWallet.App;
|
||||||
|
|
||||||
|
sealed class Program
|
||||||
|
{
|
||||||
|
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||||
|
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||||
|
// yet and stuff might break.
|
||||||
|
[STAThread]
|
||||||
|
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||||
|
.StartWithClassicDesktopLifetime(args);
|
||||||
|
|
||||||
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
|
=> AppBuilder.Configure<App>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
#if DEBUG
|
||||||
|
.WithDeveloperTools()
|
||||||
|
#endif
|
||||||
|
.WithInterFont()
|
||||||
|
.LogToTrace();
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Templates;
|
||||||
|
using PalladiumWallet.App.ViewModels;
|
||||||
|
|
||||||
|
namespace PalladiumWallet.App;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given a view model, returns the corresponding view if possible.
|
||||||
|
/// </summary>
|
||||||
|
[RequiresUnreferencedCode(
|
||||||
|
"Default implementation of ViewLocator involves reflection which may be trimmed away.",
|
||||||
|
Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
|
||||||
|
public class ViewLocator : IDataTemplate
|
||||||
|
{
|
||||||
|
public Control? Build(object? param)
|
||||||
|
{
|
||||||
|
if (param is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
|
||||||
|
var type = Type.GetType(name);
|
||||||
|
|
||||||
|
if (type != null)
|
||||||
|
{
|
||||||
|
return (Control)Activator.CreateInstance(type)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TextBlock { Text = "Not Found: " + name };
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Match(object? data)
|
||||||
|
{
|
||||||
|
return data is ViewModelBase;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using NBitcoin;
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
private IReadOnlyDictionary<string, Transaction>? _lastTransactions;
|
||||||
|
private BuiltTransaction? _pendingSend;
|
||||||
|
|
||||||
|
// ---- 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 = "";
|
||||||
|
|
||||||
|
// ---- pannello setup ----
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string mnemonicInput = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string passphraseInput = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string passwordInput = "";
|
||||||
|
|
||||||
|
// ---- pannello wallet ----
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string balanceText = "—";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string unconfirmedText = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string networkInfo = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string receiveAddress = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string serverInput = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool useSsl;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string connectionStatus = "non connesso";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isSyncing;
|
||||||
|
|
||||||
|
public ObservableCollection<HistoryRow> History { get; } = [];
|
||||||
|
|
||||||
|
// ---- 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()
|
||||||
|
{
|
||||||
|
RefreshSetupState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private NetKind Net => Enum.Parse<NetKind>(SelectedNetwork, ignoreCase: true);
|
||||||
|
private ChainProfile Profile => ChainProfiles.For(Net);
|
||||||
|
|
||||||
|
partial void OnSelectedNetworkChanged(string value) => RefreshSetupState();
|
||||||
|
|
||||||
|
private void RefreshSetupState()
|
||||||
|
{
|
||||||
|
WalletFileExists = WalletStore.Exists(AppPaths.DefaultWalletPath(Net));
|
||||||
|
StatusMessage = WalletFileExists
|
||||||
|
? "Trovato un wallet esistente: inserisci la password (se impostata) e apri."
|
||||||
|
: "Nessun wallet su questa rete: creane uno nuovo o ripristina da seed.";
|
||||||
|
ServerInput = $"127.0.0.1:{Profile.DefaultTcpPort}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- comandi setup ----------
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void GenerateMnemonic()
|
||||||
|
{
|
||||||
|
MnemonicInput = Bip39.Generate(MnemonicLength.Twelve).ToString();
|
||||||
|
StatusMessage = "Nuova mnemonica generata: SCRIVILA SU CARTA prima di continuare.";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CreateOrRestore()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (doc, account) = WalletLoader.NewFromMnemonic(
|
||||||
|
MnemonicInput,
|
||||||
|
string.IsNullOrEmpty(PassphraseInput) ? null : PassphraseInput,
|
||||||
|
ScriptKind.NativeSegwit,
|
||||||
|
Profile);
|
||||||
|
var path = AppPaths.DefaultWalletPath(Net);
|
||||||
|
var password = string.IsNullOrEmpty(PasswordInput) ? null : PasswordInput;
|
||||||
|
WalletStore.Save(doc, path, password);
|
||||||
|
OpenLoaded(doc, account, path, password);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusMessage = $"Errore: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenExisting()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = AppPaths.DefaultWalletPath(Net);
|
||||||
|
var password = string.IsNullOrEmpty(PasswordInput) ? null : PasswordInput;
|
||||||
|
var doc = WalletStore.Load(path, password);
|
||||||
|
OpenLoaded(doc, WalletLoader.ToAccount(doc), path, password);
|
||||||
|
}
|
||||||
|
catch (WrongPasswordException)
|
||||||
|
{
|
||||||
|
StatusMessage = "Password errata.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusMessage = $"Errore: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenLoaded(WalletDocument doc, HdAccount account, string path, string? password)
|
||||||
|
{
|
||||||
|
_doc = doc;
|
||||||
|
_account = account;
|
||||||
|
_walletPath = path;
|
||||||
|
_password = password;
|
||||||
|
MnemonicInput = PassphraseInput = PasswordInput = "";
|
||||||
|
|
||||||
|
NetworkInfo = $"{doc.Network} · {doc.ScriptKind} · m/{doc.AccountPath}"
|
||||||
|
+ (doc.IsWatchOnly ? " · watch-only" : "");
|
||||||
|
ApplyCache(doc.Cache);
|
||||||
|
IsWalletOpen = true;
|
||||||
|
StatusMessage = doc.Cache is null
|
||||||
|
? "Wallet aperto. Connettiti a un server per sincronizzare."
|
||||||
|
: "Wallet aperto (dati dell'ultima sincronizzazione).";
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BalanceText = CoinAmount.Format(cache.ConfirmedSats, Profile.CoinUnit);
|
||||||
|
UnconfirmedText = cache.UnconfirmedSats != 0
|
||||||
|
? $"+ {CoinAmount.Format(cache.UnconfirmedSats)} non confermato"
|
||||||
|
: "";
|
||||||
|
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 ? "+" : "") + CoinAmount.Format(tx.DeltaSats),
|
||||||
|
tx.Txid,
|
||||||
|
tx.Verified ? "✓ SPV" : "—"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- comandi wallet ----------
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ConnectAndSync()
|
||||||
|
{
|
||||||
|
if (_account is null || _doc is null)
|
||||||
|
return;
|
||||||
|
IsSyncing = true;
|
||||||
|
StatusMessage = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_client is null || !_client.IsConnected)
|
||||||
|
{
|
||||||
|
var (host, port) = ParseServer();
|
||||||
|
ConnectionStatus = $"connessione a {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(() =>
|
||||||
|
ConnectionStatus = "disconnesso");
|
||||||
|
ConnectionStatus = $"connesso a {host}:{port}{(UseSsl ? " (TLS)" : "")}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var sync = new WalletSynchronizer(_account, _client, _doc.GapLimit);
|
||||||
|
sync.Progress += msg => Dispatcher.UIThread.Post(() => StatusMessage = msg);
|
||||||
|
var result = await sync.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],
|
||||||
|
};
|
||||||
|
WalletStore.Save(_doc, _walletPath!, _password);
|
||||||
|
ApplyCache(_doc.Cache);
|
||||||
|
StatusMessage = $"Sincronizzato: altezza {result.TipHeight}, " +
|
||||||
|
$"{result.History.Count} transazioni verificate SPV.";
|
||||||
|
}
|
||||||
|
catch (CertificatePinMismatchException ex)
|
||||||
|
{
|
||||||
|
ConnectionStatus = "certificato cambiato";
|
||||||
|
StatusMessage = ex.Message;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConnectionStatus = "errore";
|
||||||
|
StatusMessage = $"Errore: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsSyncing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnServerNotification(string method, System.Text.Json.JsonElement payload)
|
||||||
|
{
|
||||||
|
// Cambiamento su un nostro indirizzo o nuovo blocco: risincronizza.
|
||||||
|
if (method is "blockchain.scripthash.subscribe" or "blockchain.headers.subscribe")
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (!IsSyncing)
|
||||||
|
_ = ConnectAndSync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ResetCertificates()
|
||||||
|
{
|
||||||
|
new CertificatePinStore(AppPaths.CertificatePinsPath(Net)).ResetAll();
|
||||||
|
StatusMessage = "Certificati SSL azzerati: riprova la connessione.";
|
||||||
|
}
|
||||||
|
|
||||||
|
[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.TryParseCoins(SendAmount, 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 {CoinAmount.Format(_pendingSend.Fee.Satoshi, Profile.CoinUnit)} " +
|
||||||
|
$"({_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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CloseWallet()
|
||||||
|
{
|
||||||
|
_ = _client?.DisposeAsync().AsTask();
|
||||||
|
_client = null;
|
||||||
|
_doc = null;
|
||||||
|
_account = null;
|
||||||
|
_lastTransactions = null;
|
||||||
|
_pendingSend = null;
|
||||||
|
HasPendingSend = false;
|
||||||
|
History.Clear();
|
||||||
|
IsWalletOpen = false;
|
||||||
|
ConnectionStatus = "non connesso";
|
||||||
|
RefreshSetupState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private (string Host, int Port) ParseServer()
|
||||||
|
{
|
||||||
|
var parts = ServerInput.Trim().Split(':');
|
||||||
|
var host = parts[0];
|
||||||
|
var port = parts.Length > 1 && int.TryParse(parts[1], out var p)
|
||||||
|
? p
|
||||||
|
: UseSsl ? Profile.DefaultSslPort : Profile.DefaultTcpPort;
|
||||||
|
return (host, port);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace PalladiumWallet.App.ViewModels;
|
||||||
|
|
||||||
|
public abstract class ViewModelBase : ObservableObject
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:PalladiumWallet.App.ViewModels"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="620"
|
||||||
|
x:Class="PalladiumWallet.App.Views.MainWindow"
|
||||||
|
x:DataType="vm:MainWindowViewModel"
|
||||||
|
Icon="/Assets/avalonia-logo.ico"
|
||||||
|
Width="900" Height="620"
|
||||||
|
Title="Palladium Wallet">
|
||||||
|
|
||||||
|
<Design.DataContext>
|
||||||
|
<vm:MainWindowViewModel/>
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,Auto">
|
||||||
|
|
||||||
|
<!-- ============ PANNELLO SETUP: crea / ripristina / apri ============ -->
|
||||||
|
<ScrollViewer Grid.Row="0" IsVisible="{Binding IsSetupVisible}">
|
||||||
|
<StackPanel MaxWidth="560" Margin="24" Spacing="14">
|
||||||
|
<TextBlock Text="Palladium Wallet" FontSize="28" FontWeight="Bold"/>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||||
|
<TextBlock Text="Rete:" VerticalAlignment="Center"/>
|
||||||
|
<ComboBox ItemsSource="{Binding Networks}"
|
||||||
|
SelectedItem="{Binding SelectedNetwork}"
|
||||||
|
MinWidth="140"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Apri wallet esistente -->
|
||||||
|
<Border IsVisible="{Binding WalletFileExists}"
|
||||||
|
BorderBrush="{DynamicResource SystemAccentColor}" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="14">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="Apri il wallet esistente" FontWeight="Bold"/>
|
||||||
|
<TextBox PlaceholderText="Password del file (vuoto se non impostata)"
|
||||||
|
PasswordChar="●" Text="{Binding PasswordInput}"/>
|
||||||
|
<Button Content="Apri wallet" Command="{Binding OpenExistingCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Crea / ripristina -->
|
||||||
|
<Border BorderBrush="Gray" BorderThickness="1" CornerRadius="6" Padding="14">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="Crea nuovo o ripristina da seed" FontWeight="Bold"/>
|
||||||
|
<TextBox PlaceholderText="Mnemonica BIP39 (12 o 24 parole)"
|
||||||
|
AcceptsReturn="False" Text="{Binding MnemonicInput}"/>
|
||||||
|
<Button Content="Genera nuova mnemonica" Command="{Binding GenerateMnemonicCommand}"/>
|
||||||
|
<TextBox PlaceholderText="Passphrase BIP39 opzionale (cambia il wallet! annotala a parte)"
|
||||||
|
Text="{Binding PassphraseInput}"/>
|
||||||
|
<TextBox PlaceholderText="Password di cifratura del file wallet (consigliata)"
|
||||||
|
PasswordChar="●" Text="{Binding PasswordInput}"/>
|
||||||
|
<Button Content="Crea / ripristina wallet" Command="{Binding CreateOrRestoreCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- ============ PANNELLO WALLET ============ -->
|
||||||
|
<Grid Grid.Row="0" RowDefinitions="Auto,Auto,*" IsVisible="{Binding IsWalletOpen}" Margin="16">
|
||||||
|
|
||||||
|
<!-- Testata: saldo + rete + chiudi -->
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="2">
|
||||||
|
<TextBlock Text="{Binding BalanceText}" FontSize="30" FontWeight="Bold"/>
|
||||||
|
<TextBlock Text="{Binding UnconfirmedText}" Foreground="Orange"/>
|
||||||
|
<TextBlock Text="{Binding NetworkInfo}" FontSize="12" Foreground="Gray"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1" Content="Chiudi wallet" VerticalAlignment="Top"
|
||||||
|
Command="{Binding CloseWalletCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Server -->
|
||||||
|
<Border Grid.Row="1" Margin="0,12,0,12" Padding="10"
|
||||||
|
BorderBrush="Gray" BorderThickness="1" CornerRadius="6">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="Server:" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<TextBox Grid.Column="1" Text="{Binding ServerInput}"
|
||||||
|
PlaceholderText="host:porta del server di indicizzazione"/>
|
||||||
|
<CheckBox Grid.Column="2" Content="TLS" IsChecked="{Binding UseSsl}" Margin="8,0"/>
|
||||||
|
<Button Grid.Column="3" Content="Connetti e sincronizza"
|
||||||
|
Command="{Binding ConnectAndSyncCommand}" IsEnabled="{Binding !IsSyncing}"/>
|
||||||
|
<Button Grid.Column="4" Content="Reset cert." Margin="6,0,0,0"
|
||||||
|
Command="{Binding ResetCertificatesCommand}"/>
|
||||||
|
<TextBlock Grid.Column="5" Text="{Binding ConnectionStatus}"
|
||||||
|
VerticalAlignment="Center" Margin="10,0,0,0" Foreground="Gray"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Tab: Ricevi / Storico / Invia -->
|
||||||
|
<TabControl Grid.Row="2">
|
||||||
|
<TabItem Header="Ricevi">
|
||||||
|
<StackPanel Spacing="10" Margin="8">
|
||||||
|
<TextBlock Text="Prossimo indirizzo non usato:"/>
|
||||||
|
<SelectableTextBlock Text="{Binding ReceiveAddress}"
|
||||||
|
FontFamily="monospace" FontSize="16"/>
|
||||||
|
<TextBlock Text="Ogni pagamento ricevuto qui comparirà nello storico alla prossima sincronizzazione."
|
||||||
|
Foreground="Gray" FontSize="12" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="Storico">
|
||||||
|
<ListBox ItemsSource="{Binding History}" Margin="4">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:HistoryRow">
|
||||||
|
<Grid ColumnDefinitions="90,160,*,70">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding Conferma}" Foreground="Gray"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding Importo}" FontFamily="monospace"/>
|
||||||
|
<SelectableTextBlock Grid.Column="2" Text="{Binding Txid}"
|
||||||
|
FontFamily="monospace" FontSize="12"/>
|
||||||
|
<TextBlock Grid.Column="3" Text="{Binding Verificata}" Foreground="Green"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<TabItem Header="Invia">
|
||||||
|
<StackPanel Spacing="10" Margin="8" MaxWidth="640" HorizontalAlignment="Left">
|
||||||
|
<TextBox PlaceholderText="Indirizzo destinatario" Text="{Binding SendTo}"
|
||||||
|
FontFamily="monospace"/>
|
||||||
|
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||||
|
<TextBox Grid.Column="0" PlaceholderText="Importo (es. 1.5)"
|
||||||
|
Text="{Binding SendAmount}" IsEnabled="{Binding !SendAll}"/>
|
||||||
|
<CheckBox Grid.Column="1" Content="Invia tutto" Margin="10,0"
|
||||||
|
IsChecked="{Binding SendAll}"/>
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6">
|
||||||
|
<TextBlock Text="fee sat/vB:" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Text="{Binding SendFeeRate}" MinWidth="60"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||||
|
<Button Content="Prepara transazione" Command="{Binding PrepareSendCommand}"/>
|
||||||
|
<Button Content="CONFERMA E TRASMETTI" Classes="accent"
|
||||||
|
Command="{Binding ConfirmSendCommand}"
|
||||||
|
IsEnabled="{Binding HasPendingSend}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<SelectableTextBlock Text="{Binding SendPreview}" TextWrapping="Wrap"
|
||||||
|
FontSize="13"/>
|
||||||
|
</StackPanel>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Barra di stato -->
|
||||||
|
<Border Grid.Row="1" Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
|
||||||
|
Padding="10,6">
|
||||||
|
<TextBlock Text="{Binding StatusMessage}" FontSize="12" TextWrapping="Wrap"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace PalladiumWallet.App.Views;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<!-- This manifest is used on Windows only.
|
||||||
|
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||||
|
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="PalladiumWallet.App.Desktop"/>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- A list of the Windows versions that this application has been tested on
|
||||||
|
and is designed to work with. Uncomment the appropriate elements
|
||||||
|
and Windows will automatically select the most compatible environment. -->
|
||||||
|
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
Reference in New Issue
Block a user