feat(wallet): surface immature balance separately from confirmed
Extract confirmation-threshold logic into UtxoSpendability (shared by TransactionFactory and WalletSynchronizer) and use it to compute ImmatureSats, so coinbase/under-confirmed amounts are shown separately from spendable balance in the GUI and CLI instead of being lumped into "confirmed".
This commit is contained in:
@@ -52,20 +52,8 @@ public sealed class TransactionFactory(IWalletAccount account)
|
||||
int tipHeight,
|
||||
bool sendAll = false)
|
||||
{
|
||||
var coinbaseMaturity = account.Profile.CoinbaseMaturity;
|
||||
var minConf = account.Profile.MinConfirmations;
|
||||
|
||||
// A UTXO is spendable when it has enough confirmations:
|
||||
// - coinbase: COINBASE_MATURITY + 1 confirmations (matches the Qt wallet: the consensus
|
||||
// rule is nSpendHeight - nHeight >= 120, so at the current tip a TX can be mined in
|
||||
// the next block when tipHeight - height + 1 >= 121)
|
||||
// - regular: MinConfirmations (wallet policy, no consensus rule)
|
||||
int coinbaseThreshold = coinbaseMaturity + 1;
|
||||
var spendable = utxos.Where(u =>
|
||||
!u.Frozen &&
|
||||
u.Height > 0 &&
|
||||
(tipHeight - u.Height + 1) >= (u.IsCoinbase ? coinbaseThreshold : minConf)
|
||||
).ToList();
|
||||
var profile = account.Profile;
|
||||
var spendable = utxos.Where(u => u.IsSpendable(profile, tipHeight)).ToList();
|
||||
|
||||
if (spendable.Count == 0)
|
||||
{
|
||||
@@ -77,20 +65,21 @@ public sealed class TransactionFactory(IWalletAccount account)
|
||||
|
||||
var immature = utxos.Where(u =>
|
||||
!u.Frozen && u.Height > 0 && u.IsCoinbase &&
|
||||
(tipHeight - u.Height + 1) < coinbaseThreshold).ToList();
|
||||
u.Confirmations(tipHeight) < u.RequiredConfirmations(profile)).ToList();
|
||||
if (immature.Count > 0)
|
||||
{
|
||||
var best = immature.Max(u => tipHeight - u.Height + 1);
|
||||
reasons.Append($"{immature.Count} coinbase output(s) not yet mature ({best}/{coinbaseThreshold} confirmations). ");
|
||||
var best = immature.Max(u => u.Confirmations(tipHeight));
|
||||
var threshold = profile.CoinbaseMaturity + 1;
|
||||
reasons.Append($"{immature.Count} coinbase output(s) not yet mature ({best}/{threshold} confirmations). ");
|
||||
}
|
||||
|
||||
var underConf = utxos.Where(u =>
|
||||
!u.Frozen && u.Height > 0 && !u.IsCoinbase &&
|
||||
(tipHeight - u.Height + 1) < minConf).ToList();
|
||||
u.Confirmations(tipHeight) < u.RequiredConfirmations(profile)).ToList();
|
||||
if (underConf.Count > 0)
|
||||
{
|
||||
var best = underConf.Max(u => tipHeight - u.Height + 1);
|
||||
reasons.Append($"{underConf.Count} output(s) need {minConf} confirmations ({best} so far). ");
|
||||
var best = underConf.Max(u => u.Confirmations(tipHeight));
|
||||
reasons.Append($"{underConf.Count} output(s) need {profile.MinConfirmations} confirmations ({best} so far). ");
|
||||
}
|
||||
|
||||
throw new WalletSpendException(reasons.Length > 0
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using PalladiumWallet.Core.Chain;
|
||||
using PalladiumWallet.Core.Storage;
|
||||
|
||||
namespace PalladiumWallet.Core.Wallet;
|
||||
|
||||
/// <summary>
|
||||
/// Confirmation-threshold rules shared between coin selection (<see cref="TransactionFactory"/>)
|
||||
/// and balance reporting: a coinbase output needs COINBASE_MATURITY + 1 confirmations
|
||||
/// (consensus rule nSpendHeight - nHeight >= 120, plus one block of safety margin, matching
|
||||
/// the Qt wallet); a regular output needs <see cref="ChainProfile.MinConfirmations"/> (wallet
|
||||
/// policy, no consensus rule).
|
||||
/// </summary>
|
||||
public static class UtxoSpendability
|
||||
{
|
||||
public static int RequiredConfirmations(this CachedUtxo utxo, ChainProfile profile) =>
|
||||
utxo.IsCoinbase ? profile.CoinbaseMaturity + 1 : profile.MinConfirmations;
|
||||
|
||||
public static int Confirmations(this CachedUtxo utxo, int tipHeight) =>
|
||||
utxo.Height <= 0 ? 0 : tipHeight - utxo.Height + 1;
|
||||
|
||||
/// <summary>True when the UTXO has met its confirmation threshold and is not frozen.</summary>
|
||||
public static bool IsSpendable(this CachedUtxo utxo, ChainProfile profile, int tipHeight) =>
|
||||
!utxo.Frozen && utxo.Height > 0 && utxo.Confirmations(tipHeight) >= utxo.RequiredConfirmations(profile);
|
||||
}
|
||||
Reference in New Issue
Block a user