Files
PalladiumWallet/src/App/ViewModels/MainWindowViewModel.Donate.cs
T
davide 9aecdb1aaa feat(wallet): enforce confirmation thresholds before spending UTXOs
Coinbase outputs require COINBASE_MATURITY + 1 = 121 confirmations before
they can be spent. The consensus rule (palladiumcore tx_verify.cpp) is
nSpendHeight - nHeight >= 120; the +1 adds one block of safety margin,
matching the Qt wallet (wallet.cpp: COINBASE_MATURITY+1). Regular UTXOs
require MinConfirmations (6 on mainnet, 1 on testnet/regtest — wallet
policy, no consensus rule).

Changes:
- ChainProfile: add CoinbaseMaturity and MinConfirmations fields
- ChainProfiles: mainnet CoinbaseMaturity=120 / MinConfirmations=6;
  testnet/regtest inherit 120, override MinConfirmations=1
- PalladiumNetworks: align NBitcoin Consensus.CoinbaseMaturity to 120
- CachedUtxo: add IsCoinbase flag (ElectrumX listunspent does not expose
  this; derived from Transaction.IsCoinBase on the raw tx)
- WalletSynchronizer: set IsCoinbase during local UTXO reconstruction
- TransactionFactory.Build: add tipHeight param; filter by threshold;
  error reports reason (immature coinbase with X/121 counter, under-
  confirmed regulars with best-seen count, mempool amounts)
- All Build() call sites updated (App, Donate, CLI)
- Two new tests: coinbase maturity boundary (119→220 tip) and mainnet
  min-conf boundary (5→6 confirmations)
2026-07-02 16:25:42 +02:00

97 lines
3.0 KiB
C#

using System;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NBitcoin;
using PalladiumWallet.Core.Chain;
using PalladiumWallet.Core.Net;
using PalladiumWallet.Core.Wallet;
namespace PalladiumWallet.App.ViewModels;
public partial class MainWindowViewModel
{
public const string DevAddress = "plm1qdq3gu2zvg9lyr8gxd6yln4wavc5tlp8prmvfay";
[ObservableProperty]
private string donateAmount = "";
[ObservableProperty]
private string donatePreview = "";
[ObservableProperty]
private bool hasPendingDonate;
private BuiltTransaction? _pendingDonate;
[RelayCommand]
private async Task PrepareDonate()
{
_pendingDonate = null;
HasPendingDonate = false;
if (_account is null || _doc?.Cache is null || _lastTransactions is null)
{
DonatePreview = "Sincronizza prima di inviare.";
return;
}
try
{
var destination = BitcoinAddress.Create(DevAddress, PalladiumNetworks.For(Net));
if (!CoinAmount.TryParseIn(DonateAmount.Trim(), _config.Unit, out var amount) || amount <= 0)
{
DonatePreview = "Importo non valido.";
return;
}
if (!decimal.TryParse(SendFeeRate, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var feeRate) || feeRate <= 0)
feeRate = 1m;
_pendingDonate = new TransactionFactory(_account).Build(
_doc.Cache.Utxos, _lastTransactions, destination, amount, feeRate,
_doc.Cache.NextChangeIndex, _doc.Cache.TipHeight, sendAll: false);
DonatePreview = $"txid {_pendingDonate.Txid[..16]}… · " +
$"fee {Fmt(_pendingDonate.Fee.Satoshi)} " +
$"({_pendingDonate.Transaction.GetVirtualSize()} vB)" +
(_pendingDonate.Signed ? "" : " · watch-only");
HasPendingDonate = _pendingDonate.Signed;
}
catch (Exception ex)
{
_pendingDonate = null;
HasPendingDonate = false;
DonatePreview = $"Errore: {ex.Message}";
}
await Task.CompletedTask;
}
[RelayCommand]
private async Task ConfirmDonate()
{
if (_pendingDonate is null || _client is null)
return;
try
{
var txid = await _client.BroadcastAsync(_pendingDonate.ToHex());
DonatePreview = $"Grazie! txid: {txid}";
DonateAmount = "";
_pendingDonate = null;
HasPendingDonate = false;
await ConnectAndSync();
}
catch (Exception ex)
{
DonatePreview = $"Errore broadcast: {ex.Message}";
}
}
private void ResetDonate()
{
DonateAmount = "";
DonatePreview = "";
HasPendingDonate = false;
_pendingDonate = null;
}
}