feat(app): transaction detail overlay with full on-chain data
Double-clicking a history row opens an in-app overlay (same pattern as address/server/settings overlays) that shows all available data for a transaction. The overlay appears immediately with a spinner; data is fetched from the server in the background and rendered once ready. Backdrop tap and Esc close it; a pending fetch is cancelled. New Core type — TransactionInspector (Core/Wallet/): - FetchAsync() downloads the raw tx, all parent txs (parallel requests, one round-trip instead of N sequential), and the block header - Reconstructs inputs (amounts, addresses, is-mine), outputs, fee, virtual size, RBF flag, block timestamp, confirmations New App types: - TransactionDetails record: full parsed data (fee, net, I/O lists, counterparty addresses, coinbase detection…) - TransactionDetailsViewModel: pre-formats all strings in the current locale and unit (status, date, signed amounts, sat/vB rate…) - TxIoRow record: one row in the input/output tables - MineColorConverter: bool → brush (MediumSeaGreen for own addresses) ViewModel changes: - ShowTransactionDetailsAsync / CloseTransactionDetailsCommand - BuildTransactionDetailsAsync (also public for future RBF/CPFP use) - CancellationTokenSource so opening a new tx cancels the previous fetch - All server work runs on Task.Run to keep the UI thread free XAML: history rows get DoubleTapped handler, Hand cursor, hint label, txid now uses TextTrimming instead of SelectableTextBlock. i18n: 24 new tx.* / history.hint keys in all 6 languages.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using PalladiumWallet.App.Localization;
|
||||
using PalladiumWallet.Core.Wallet;
|
||||
|
||||
namespace PalladiumWallet.App.ViewModels;
|
||||
|
||||
/// <summary>Riga input/output per le tabelle della finestra di dettaglio.</summary>
|
||||
public sealed record TxIoRow(string Position, string Address, string Amount, bool IsMine);
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel della finestra di dettaglio transazione: prende un
|
||||
/// <see cref="TransactionDetails"/> (assemblato dal server) e ne ricava tutte
|
||||
/// le stringhe già formattate e localizzate per la vista. È di sola lettura.
|
||||
/// </summary>
|
||||
public sealed class TransactionDetailsViewModel
|
||||
{
|
||||
private readonly string _unit;
|
||||
|
||||
public Loc Loc { get; }
|
||||
|
||||
public TransactionDetailsViewModel(TransactionDetails d, Loc loc, string unit)
|
||||
{
|
||||
Loc = loc;
|
||||
_unit = unit;
|
||||
|
||||
Txid = d.Txid;
|
||||
StatusText = BuildStatus(d, loc);
|
||||
DateText = d.BlockTime is { } t
|
||||
? t.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
|
||||
: loc["tx.mempool"];
|
||||
|
||||
var counterparties = d.CounterpartyAddresses;
|
||||
CounterpartyHeader = d.IsIncoming ? loc["tx.from"] : loc["tx.to"];
|
||||
CounterpartyText = counterparties.Count > 0
|
||||
? string.Join(Environment.NewLine, counterparties)
|
||||
: "—";
|
||||
|
||||
// Debito (uscita verso terzi) o Credito (entrata netta sui nostri output).
|
||||
AmountHeader = d.IsIncoming ? loc["tx.credit"] : loc["tx.debit"];
|
||||
AmountText = d.IsIncoming
|
||||
? Signed(d.ReceivedSats)
|
||||
: Signed(-d.SentToOthersSats);
|
||||
|
||||
FeeText = d.FeeSats is { } fee
|
||||
? (d.IsIncoming ? Abs(fee) : Signed(-fee))
|
||||
: "—";
|
||||
|
||||
NetText = Signed(d.NetSats);
|
||||
|
||||
TotalSizeText = $"{d.TotalSize} byte";
|
||||
VirtualSizeText = $"{d.VirtualSize} byte";
|
||||
FeeRateText = d.FeeRateSatPerVb is { } r
|
||||
? r.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture) + " sat/vB"
|
||||
: "—";
|
||||
VersionText = d.Version.ToString();
|
||||
LockTimeText = d.LockTime.ToString();
|
||||
RbfText = loc[d.RbfSignaled ? "tx.yes" : "tx.no"];
|
||||
VerifiedText = d.Verified ? "✓ SPV" : "—";
|
||||
|
||||
Inputs = new ObservableCollection<TxIoRow>(d.Inputs.Select((i, n) => new TxIoRow(
|
||||
i.IsCoinbase ? "coinbase" : $"{Shorten(i.PrevTxid)}:{i.PrevIndex}",
|
||||
i.Address ?? "—",
|
||||
i.AmountSats is { } a ? CoinAmount.FormatIn(a, unit) : "—",
|
||||
i.IsMine)));
|
||||
|
||||
Outputs = new ObservableCollection<TxIoRow>(d.Outputs.Select(o => new TxIoRow(
|
||||
$"#{o.Index}",
|
||||
o.Address ?? $"({o.ScriptType})",
|
||||
CoinAmount.FormatIn(o.AmountSats, unit),
|
||||
o.IsMine)));
|
||||
}
|
||||
|
||||
public string Txid { get; }
|
||||
public string StatusText { get; }
|
||||
public string DateText { get; }
|
||||
public string CounterpartyHeader { get; }
|
||||
public string CounterpartyText { get; }
|
||||
public string AmountHeader { get; }
|
||||
public string AmountText { get; }
|
||||
public string FeeText { get; }
|
||||
public string NetText { get; }
|
||||
public string TotalSizeText { get; }
|
||||
public string VirtualSizeText { get; }
|
||||
public string FeeRateText { get; }
|
||||
public string VersionText { get; }
|
||||
public string LockTimeText { get; }
|
||||
public string RbfText { get; }
|
||||
public string VerifiedText { get; }
|
||||
public ObservableCollection<TxIoRow> Inputs { get; }
|
||||
public ObservableCollection<TxIoRow> Outputs { get; }
|
||||
|
||||
private static string BuildStatus(TransactionDetails d, Loc loc)
|
||||
{
|
||||
if (d.Confirmations <= 0)
|
||||
return loc["tx.status.mempool"];
|
||||
return $"{d.Confirmations} {loc["tx.status.confirmations"]} ({loc["tx.status.block"]} {d.Height})";
|
||||
}
|
||||
|
||||
private string Signed(long sats)
|
||||
{
|
||||
var sign = sats > 0 ? "+" : sats < 0 ? "-" : "";
|
||||
return sign + CoinAmount.FormatIn(Math.Abs(sats), _unit);
|
||||
}
|
||||
|
||||
private string Abs(long sats) => CoinAmount.FormatIn(Math.Abs(sats), _unit);
|
||||
|
||||
private static string Shorten(string txid) =>
|
||||
txid.Length > 16 ? $"{txid[..8]}…{txid[^4..]}" : txid;
|
||||
}
|
||||
Reference in New Issue
Block a user