2026-06-13 21:15:03 +02:00
|
|
|
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
|
|
|
|
|
{
|
2026-06-16 14:40:06 +02:00
|
|
|
// ---- balance and wallet info ----
|
2026-06-13 21:15:03 +02:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string balanceText = "—";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string unconfirmedText = "";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string networkInfo = "";
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
// ---- receive address and QR ----
|
2026-06-13 21:15:03 +02:00
|
|
|
|
|
|
|
|
[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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
// ---- addresses tab ----
|
2026-06-13 21:15:03 +02:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private AddressRow? selectedAddressRow;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private AddressInfo? addressInfo;
|
|
|
|
|
|
2026-06-21 22:28:28 +02:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isAddressPrivKeyRevealed;
|
|
|
|
|
|
|
|
|
|
// ---- private key password prompt ----
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool isPrivKeyPromptOpen;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string privKeyPromptPassword = "";
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string privKeyPromptError = "";
|
|
|
|
|
|
|
|
|
|
partial void OnAddressInfoChanged(AddressInfo? value)
|
|
|
|
|
{
|
|
|
|
|
IsAddressPrivKeyRevealed = false;
|
|
|
|
|
ClosePrivKeyPromptInternal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ClosePrivKeyPromptInternal()
|
|
|
|
|
{
|
|
|
|
|
IsPrivKeyPromptOpen = false;
|
|
|
|
|
PrivKeyPromptPassword = "";
|
|
|
|
|
PrivKeyPromptError = "";
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 21:15:03 +02:00
|
|
|
public void ShowAddressInfo(AddressRow row) =>
|
|
|
|
|
AddressInfo = new AddressInfo(Loc, row.Indirizzo, row.DerivPath, row.PubKey, row.PrivKey);
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-21 22:28:28 +02:00
|
|
|
private void CloseAddressInfo()
|
|
|
|
|
{
|
|
|
|
|
ClosePrivKeyPromptInternal();
|
|
|
|
|
AddressInfo = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void RequestPrivKeyReveal()
|
|
|
|
|
{
|
|
|
|
|
if (AddressInfo is null || !AddressInfo.HasPrivKey) return;
|
|
|
|
|
if (string.IsNullOrEmpty(_password))
|
|
|
|
|
{
|
|
|
|
|
// No encryption: reveal directly
|
|
|
|
|
IsAddressPrivKeyRevealed = true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Encrypted: open password prompt
|
|
|
|
|
PrivKeyPromptPassword = "";
|
|
|
|
|
PrivKeyPromptError = "";
|
|
|
|
|
IsPrivKeyPromptOpen = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void ConfirmPrivKeyPassword()
|
|
|
|
|
{
|
|
|
|
|
if (PrivKeyPromptPassword != _password)
|
|
|
|
|
{
|
|
|
|
|
PrivKeyPromptError = Loc.Tr("msg.wrongpassword");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
IsAddressPrivKeyRevealed = true;
|
|
|
|
|
ClosePrivKeyPromptInternal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void CancelPrivKeyPrompt() => ClosePrivKeyPromptInternal();
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void HideAddressPrivKey()
|
|
|
|
|
{
|
|
|
|
|
IsAddressPrivKeyRevealed = false;
|
|
|
|
|
}
|
2026-06-13 21:15:03 +02:00
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
// ---- transaction detail overlay ----
|
2026-06-13 21:15:03 +02:00
|
|
|
|
|
|
|
|
[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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 14:40:06 +02:00
|
|
|
// ---- update display from the sync result ----
|
2026-06-13 21:15:03 +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();
|
|
|
|
|
Addresses.Clear();
|
|
|
|
|
for (var i = 0; i < 10; i++)
|
|
|
|
|
Addresses.Add(new AddressRow(_loc["addr.receive"], i,
|
|
|
|
|
_account.GetReceiveAddress(i).ToString(), "—", "—",
|
|
|
|
|
false,
|
2026-06-15 14:23:03 +02:00
|
|
|
_account.GetPublicKey(false, i)?.ToHex() ?? "",
|
2026-06-13 21:15:03 +02:00
|
|
|
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),
|
2026-06-22 00:02:32 +02:00
|
|
|
tx.Txid));
|
2026-06-13 21:15:03 +02:00
|
|
|
|
|
|
|
|
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,
|
2026-06-15 14:23:03 +02:00
|
|
|
_account.GetPublicKey(a.IsChange, a.Index)?.ToHex() ?? "",
|
2026-06-13 21:15:03 +02:00
|
|
|
KeyWif(a.IsChange, a.Index),
|
|
|
|
|
$"m/{_doc!.AccountPath}/{(a.IsChange ? 1 : 0)}/{a.Index}"));
|
|
|
|
|
}
|
|
|
|
|
}
|