diff --git a/src/App/ViewModels/MainWindowViewModel.cs b/src/App/ViewModels/MainWindowViewModel.cs index 8124fe2..906e43c 100644 --- a/src/App/ViewModels/MainWindowViewModel.cs +++ b/src/App/ViewModels/MainWindowViewModel.cs @@ -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(); diff --git a/src/Cli/Program.cs b/src/Cli/Program.cs index b6d1405..4eeac97 100644 --- a/src/Cli/Program.cs +++ b/src/Cli/Program.cs @@ -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 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} " + diff --git a/src/Core/Wallet/TransactionFactory.cs b/src/Core/Wallet/TransactionFactory.cs index a5ac913..67217ab 100644 --- a/src/Core/Wallet/TransactionFactory.cs +++ b/src/Core/Wallet/TransactionFactory.cs @@ -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), diff --git a/tests/PalladiumWallet.Tests/Wallet/TransactionFactoryTests.cs b/tests/PalladiumWallet.Tests/Wallet/TransactionFactoryTests.cs index 682e702..d17e71c 100644 --- a/tests/PalladiumWallet.Tests/Wallet/TransactionFactoryTests.cs +++ b/tests/PalladiumWallet.Tests/Wallet/TransactionFactoryTests.cs @@ -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(() => 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() {