3460e53b4f
Balance/history now render as soon as tx downloads finish instead of blocking on every historical Merkle proof, critical for mobile where proof-checking can take much longer than the download itself. Proofs continue to be checked in the background and each tx's Verified flag catches up progressively; header ranges are now fetched in batches (blockchain.block.headers) instead of one call per header to keep this fast over high-latency links. Coin selection (UtxoSpendability.IsSpendable) refuses to spend a UTXO until its Merkle proof is actually checked, regardless of confirmation count, so a server that fabricates a confirmed balance can get it displayed early but never spent before the forgery is caught. The disk cache only ever persists the fully-verified end state of a sync. UI surfaces the new PendingVerificationSats/SpendableSats split with a "verifying..." badge, and the sync save now runs off the UI thread to avoid freezing on slower hardware.
117 lines
4.4 KiB
C#
117 lines
4.4 KiB
C#
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>Input/output row for the transaction detail tables.</summary>
|
|
public sealed record TxIoRow(string Position, string Address, string Amount, bool IsMine);
|
|
|
|
/// <summary>
|
|
/// ViewModel for the transaction detail view: takes a
|
|
/// <see cref="TransactionDetails"/> (assembled from server data) and produces
|
|
/// all strings already formatted and localised for the view. Read-only.
|
|
/// </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;
|
|
IsCoinbase = d.IsCoinbase;
|
|
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"];
|
|
// Coinbase has no sender: coins are newly generated by mining.
|
|
CounterpartyText = d.IsCoinbase
|
|
? loc["tx.coinbase.newcoins"]
|
|
: counterparties.Count > 0
|
|
? string.Join(Environment.NewLine, counterparties)
|
|
: "—";
|
|
|
|
// Debit (outflow to third parties) or Credit (net inflow on our outputs).
|
|
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"];
|
|
Inputs = new ObservableCollection<TxIoRow>(d.Inputs.Select((i, n) => new TxIoRow(
|
|
i.IsCoinbase ? loc["tx.coinbase"] : $"{i.PrevTxid}:{i.PrevIndex}",
|
|
i.IsCoinbase
|
|
? i.CoinbaseTag is { Length: > 0 } tag
|
|
? $"{loc["tx.coinbase.newcoins"]} — {tag}"
|
|
: loc["tx.coinbase.newcoins"]
|
|
: 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.OpReturnText is { Length: > 0 } msg ? $"OP_RETURN: {msg}" : $"({o.ScriptType})"),
|
|
CoinAmount.FormatIn(o.AmountSats, unit),
|
|
o.IsMine)));
|
|
}
|
|
|
|
public string Txid { get; }
|
|
public bool IsCoinbase { 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 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"];
|
|
var status = $"{d.Confirmations} {loc["tx.status.confirmations"]} ({loc["tx.status.block"]} {d.Height})";
|
|
return d.Verified ? status : $"{status} — {loc["history.unverified"]}";
|
|
}
|
|
|
|
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);
|
|
}
|