feat(wallet): show pending mempool balance and exclude unconfirmed UTXOs from spending
TransactionFactory now selects only confirmed UTXOs (height > 0), so mempool funds are visible as pending balance but never spendable, with a clear error when only unconfirmed funds are available. GUI and CLI labels updated to "in attesa di conferma (non spendibile)".
This commit is contained in:
@@ -332,10 +332,11 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
BalanceText = CoinAmount.Format(cache.ConfirmedSats, Profile.CoinUnit);
|
||||
// Saldo in attesa: somma delle tx in mempool (può essere negativo per
|
||||
// gli invii in uscita non ancora confermati).
|
||||
// gli invii in uscita non ancora confermati). Non è spendibile finché
|
||||
// non conferma: la TransactionFactory usa solo UTXO confermati.
|
||||
var pending = cache.History.Where(t => t.Height <= 0).Sum(t => t.DeltaSats);
|
||||
UnconfirmedText = pending != 0
|
||||
? $"in mempool: {(pending > 0 ? "+" : "")}{CoinAmount.Format(pending)} (in attesa di conferma)"
|
||||
? $"in attesa di conferma: {(pending > 0 ? "+" : "")}{CoinAmount.Format(pending)} — non ancora spendibile"
|
||||
: "";
|
||||
ReceiveAddress = _account.GetReceiveAddress(cache.NextReceiveIndex).ToString();
|
||||
History.Clear();
|
||||
|
||||
+2
-2
@@ -105,7 +105,7 @@ static int Info(string[] o)
|
||||
if (doc.Cache is { } cache)
|
||||
{
|
||||
Console.WriteLine($"saldo: {CoinAmount.Format(cache.ConfirmedSats, account.Profile.CoinUnit)} confermato"
|
||||
+ (cache.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(cache.UnconfirmedSats)} non conf." : ""));
|
||||
+ (cache.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(cache.UnconfirmedSats)} in attesa (non spendibile)." : ""));
|
||||
Console.WriteLine($"sync: altezza {cache.TipHeight}, {cache.History.Count} transazioni");
|
||||
Console.WriteLine($"ricezione: {account.GetReceiveAddress(cache.NextReceiveIndex)}");
|
||||
}
|
||||
@@ -147,7 +147,7 @@ static async Task<int> Sync(string[] o)
|
||||
WalletStore.Save(doc, path, Opt(o, "--password"));
|
||||
|
||||
Console.WriteLine($"Saldo: {CoinAmount.Format(result.ConfirmedSats, account.Profile.CoinUnit)} confermato"
|
||||
+ (result.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(result.UnconfirmedSats)} non confermato" : ""));
|
||||
+ (result.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(result.UnconfirmedSats)} in attesa di conferma (non spendibile)" : ""));
|
||||
Console.WriteLine($"Storico ({result.History.Count}):");
|
||||
foreach (var tx in result.History)
|
||||
Console.WriteLine($" {(tx.Height > 0 ? tx.Height.ToString() : "mempool"),7} " +
|
||||
|
||||
@@ -50,9 +50,16 @@ public sealed class TransactionFactory(HdAccount account)
|
||||
int changeIndex,
|
||||
bool sendAll = false)
|
||||
{
|
||||
var spendable = utxos.Where(u => !u.Frozen).ToList();
|
||||
// Si spendono solo UTXO confermati: i fondi in mempool si vedono nel
|
||||
// saldo "in attesa" ma non sono spendibili finché non confermano.
|
||||
var spendable = utxos.Where(u => !u.Frozen && u.Height > 0).ToList();
|
||||
if (spendable.Count == 0)
|
||||
throw new WalletSpendException("Nessun UTXO spendibile selezionato.");
|
||||
{
|
||||
var pending = utxos.Where(u => !u.Frozen && u.Height <= 0).Sum(u => u.ValueSats);
|
||||
throw new WalletSpendException(pending > 0
|
||||
? $"Nessun fondo confermato: {CoinAmount.Format(pending)} in attesa di conferma (non spendibile)."
|
||||
: "Nessun UTXO spendibile selezionato.");
|
||||
}
|
||||
|
||||
var coins = spendable.Select(u => new Coin(
|
||||
new OutPoint(uint256.Parse(u.Txid), (uint)u.Vout),
|
||||
|
||||
@@ -91,6 +91,19 @@ public class TransactionFactoryTests
|
||||
feeRateSatPerVByte: 2, changeIndex: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gli_utxo_in_mempool_non_sono_spendibili()
|
||||
{
|
||||
var account = Account();
|
||||
var (utxos, txs) = Fund(account, 1_000_000);
|
||||
utxos[0].Height = 0; // in mempool: visibile nel saldo pending, non spendibile
|
||||
|
||||
var ex = Assert.Throws<WalletSpendException>(() => new TransactionFactory(account).Build(
|
||||
utxos, txs, account.GetReceiveAddress(1), amountSats: 100_000,
|
||||
feeRateSatPerVByte: 2, changeIndex: 0));
|
||||
Assert.Contains("in attesa di conferma", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gli_utxo_congelati_sono_esclusi_dalla_spesa()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user