Compare commits
2 Commits
9aecdb1aaa
...
e7797017f6
| Author | SHA1 | Date | |
|---|---|---|---|
| e7797017f6 | |||
| 6c24a8bb46 |
@@ -169,9 +169,9 @@ public sealed class Loc
|
|||||||
// Wallet panel
|
// Wallet panel
|
||||||
["wallet.close"] = ["Chiudi wallet", "Close wallet", "Cerrar wallet", "Fermer le wallet", "Fechar carteira", "Wallet schließen"],
|
["wallet.close"] = ["Chiudi wallet", "Close wallet", "Cerrar wallet", "Fermer le wallet", "Fechar carteira", "Wallet schließen"],
|
||||||
["wallet.server"] = ["Server:", "Server:", "Servidor:", "Serveur :", "Servidor:", "Server:"],
|
["wallet.server"] = ["Server:", "Server:", "Servidor:", "Serveur :", "Servidor:", "Server:"],
|
||||||
["wallet.connect"] = ["Connetti e sincronizza", "Connect and sync", "Conectar y sincronizar", "Connecter et synchroniser", "Conectar e sincronizar", "Verbinden und synchronisieren"],
|
["wallet.connect"] = ["Connetti", "Connect", "Conectar", "Connecter", "Conectar", "Verbinden"],
|
||||||
["wallet.manual"] = ["oppure host:porta manuale", "or manual host:port", "o host:puerto manual", "ou hôte:port manuel", "ou host:porta manual", "oder manuell host:port"],
|
["wallet.manual"] = ["oppure host:porta manuale", "or manual host:port", "o host:puerto manual", "ou hôte:port manuel", "ou host:porta manual", "oder manuell host:port"],
|
||||||
["wallet.discover"] = ["Cerca altri server", "Discover servers", "Buscar otros servidores", "Rechercher des serveurs", "Procurar servidores", "Server suchen"],
|
["wallet.discover"] = ["Sincronizza", "Sync servers", "Sincronizar", "Synchroniser", "Sincronizar", "Synchronisieren"],
|
||||||
["wallet.resetcert"] = ["Reset cert.", "Reset certs", "Restablecer cert.", "Réinit. cert.", "Redefinir cert.", "Zert. zurücksetzen"],
|
["wallet.resetcert"] = ["Reset cert.", "Reset certs", "Restablecer cert.", "Réinit. cert.", "Redefinir cert.", "Zert. zurücksetzen"],
|
||||||
["tab.receive"] = ["Ricevi", "Receive", "Recibir", "Recevoir", "Receber", "Empfangen"],
|
["tab.receive"] = ["Ricevi", "Receive", "Recibir", "Recevoir", "Receber", "Empfangen"],
|
||||||
["tab.history"] = ["Storico", "History", "Historial", "Historique", "Histórico", "Verlauf"],
|
["tab.history"] = ["Storico", "History", "Historial", "Historique", "Histórico", "Verlauf"],
|
||||||
@@ -389,6 +389,7 @@ public sealed class Loc
|
|||||||
["msg.height"] = ["altezza", "height", "altura", "hauteur", "altura", "Höhe"],
|
["msg.height"] = ["altezza", "height", "altura", "hauteur", "altura", "Höhe"],
|
||||||
["msg.pending"] = ["in attesa di conferma", "pending confirmation", "pendiente de confirmación", "en attente de confirmation", "aguardando confirmação", "ausstehende Bestätigung"],
|
["msg.pending"] = ["in attesa di conferma", "pending confirmation", "pendiente de confirmación", "en attente de confirmation", "aguardando confirmação", "ausstehende Bestätigung"],
|
||||||
["msg.notspendable"] = ["non ancora spendibile", "not yet spendable", "aún no gastable", "pas encore dépensable", "ainda não gastável", "noch nicht verwendbar"],
|
["msg.notspendable"] = ["non ancora spendibile", "not yet spendable", "aún no gastable", "pas encore dépensable", "ainda não gastável", "noch nicht verwendbar"],
|
||||||
|
["msg.immature"] = ["in maturazione", "maturing", "en maduración", "en maturation", "em maturação", "in Reifung"],
|
||||||
["msg.settings.saved"] = ["Impostazioni salvate.", "Settings saved.", "Configuración guardada.", "Paramètres enregistrés.", "Configurações salvas.", "Einstellungen gespeichert."],
|
["msg.settings.saved"] = ["Impostazioni salvate.", "Settings saved.", "Configuración guardada.", "Paramètres enregistrés.", "Configurações salvas.", "Einstellungen gespeichert."],
|
||||||
["msg.certreset"] = [
|
["msg.certreset"] = [
|
||||||
"Certificati SSL azzerati: riprova la connessione.",
|
"Certificati SSL azzerati: riprova la connessione.",
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ public partial class MainWindowViewModel
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string unconfirmedText = "";
|
private string unconfirmedText = "";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string immatureText = "";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string networkInfo = "";
|
private string networkInfo = "";
|
||||||
|
|
||||||
@@ -249,6 +252,7 @@ public partial class MainWindowViewModel
|
|||||||
{
|
{
|
||||||
BalanceText = $"0.00000000 {Profile.CoinUnit}";
|
BalanceText = $"0.00000000 {Profile.CoinUnit}";
|
||||||
UnconfirmedText = "";
|
UnconfirmedText = "";
|
||||||
|
ImmatureText = "";
|
||||||
ReceiveAddress = _account.GetReceiveAddress(0).ToString();
|
ReceiveAddress = _account.GetReceiveAddress(0).ToString();
|
||||||
History.Clear();
|
History.Clear();
|
||||||
Addresses.Clear();
|
Addresses.Clear();
|
||||||
@@ -261,11 +265,14 @@ public partial class MainWindowViewModel
|
|||||||
$"m/{_doc!.AccountPath}/0/{i}"));
|
$"m/{_doc!.AccountPath}/0/{i}"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BalanceText = Fmt(cache.ConfirmedSats);
|
BalanceText = Fmt(cache.ConfirmedSats - cache.ImmatureSats);
|
||||||
var pending = cache.History.Where(t => t.Height <= 0).Sum(t => t.DeltaSats);
|
var pending = cache.History.Where(t => t.Height <= 0).Sum(t => t.DeltaSats);
|
||||||
UnconfirmedText = pending != 0
|
UnconfirmedText = pending != 0
|
||||||
? $"{Loc.Tr("msg.pending")}: {(pending > 0 ? "+" : "")}{Fmt(pending)} — {Loc.Tr("msg.notspendable")}"
|
? $"{Loc.Tr("msg.pending")}: {(pending > 0 ? "+" : "")}{Fmt(pending)} — {Loc.Tr("msg.notspendable")}"
|
||||||
: "";
|
: "";
|
||||||
|
ImmatureText = cache.ImmatureSats != 0
|
||||||
|
? $"{Loc.Tr("msg.immature")}: {Fmt(cache.ImmatureSats)} — {Loc.Tr("msg.notspendable")}"
|
||||||
|
: "";
|
||||||
ReceiveAddress = _account.GetReceiveAddress(cache.NextReceiveIndex).ToString();
|
ReceiveAddress = _account.GetReceiveAddress(cache.NextReceiveIndex).ToString();
|
||||||
History.Clear();
|
History.Clear();
|
||||||
foreach (var tx in cache.History)
|
foreach (var tx in cache.History)
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ public partial class MainWindowViewModel
|
|||||||
TipHeight = result.TipHeight,
|
TipHeight = result.TipHeight,
|
||||||
ConfirmedSats = result.ConfirmedSats,
|
ConfirmedSats = result.ConfirmedSats,
|
||||||
UnconfirmedSats = result.UnconfirmedSats,
|
UnconfirmedSats = result.UnconfirmedSats,
|
||||||
|
ImmatureSats = result.ImmatureSats,
|
||||||
NextReceiveIndex = result.NextReceiveIndex,
|
NextReceiveIndex = result.NextReceiveIndex,
|
||||||
NextChangeIndex = result.NextChangeIndex,
|
NextChangeIndex = result.NextChangeIndex,
|
||||||
History = [.. result.History],
|
History = [.. result.History],
|
||||||
@@ -344,17 +345,57 @@ public partial class MainWindowViewModel
|
|||||||
_ = ConnectAndSync();
|
_ = ConnectAndSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Peer discovery. Always clickable: if the wallet is already connected, it reuses
|
||||||
|
/// that connection; otherwise it opens a short-lived connection to a candidate server
|
||||||
|
/// just to query its peer list, without touching the wallet's connection state.
|
||||||
|
/// </summary>
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task DiscoverServers()
|
private async Task DiscoverServers()
|
||||||
{
|
{
|
||||||
if (_client is null || !_client.IsConnected)
|
if (_client is { IsConnected: true } connected)
|
||||||
{
|
{
|
||||||
StatusMessage = Loc.Tr("conn.none") + ".";
|
await DiscoverServersUsing(connected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (host, port) = ParseServer();
|
||||||
|
ElectrumClient? temp = null;
|
||||||
|
Exception? lastError = null;
|
||||||
|
foreach (var (h, p) in BuildServerCandidates(host, port))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pins = new CertificatePinStore(AppPaths.CertificatePinsPath(Net));
|
||||||
|
temp = await ElectrumClient.ConnectAsync(h, p, UseSsl, pins);
|
||||||
|
lastError = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lastError = ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (temp is null)
|
||||||
|
{
|
||||||
|
StatusMessage = $"Errore nella scoperta peer: {lastError?.Message ?? Loc.Tr("conn.none")}";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var added = await Registry.DiscoverAsync(_client);
|
await DiscoverServersUsing(temp);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await temp.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DiscoverServersUsing(ElectrumClient client)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var added = await Registry.DiscoverAsync(client);
|
||||||
var selected = SelectedKnownServer;
|
var selected = SelectedKnownServer;
|
||||||
RefreshServers();
|
RefreshServers();
|
||||||
SelectedKnownServer = KnownServers.FirstOrDefault(s => s.Host == selected?.Host)
|
SelectedKnownServer = KnownServers.FirstOrDefault(s => s.Host == selected?.Host)
|
||||||
|
|||||||
@@ -314,6 +314,9 @@
|
|||||||
<TextBlock Text="{Binding UnconfirmedText}" Foreground="#FCD34D"
|
<TextBlock Text="{Binding UnconfirmedText}" Foreground="#FCD34D"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
IsVisible="{Binding UnconfirmedText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
IsVisible="{Binding UnconfirmedText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<TextBlock Text="{Binding ImmatureText}" Foreground="#FCD34D"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
IsVisible="{Binding ImmatureText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
<TextBlock Text="{Binding NetworkInfo}" Classes="on-hero" FontSize="12"
|
<TextBlock Text="{Binding NetworkInfo}" Classes="on-hero" FontSize="12"
|
||||||
Margin="0,2,0,0"/>
|
Margin="0,2,0,0"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -1234,6 +1237,8 @@
|
|||||||
<StackPanel Orientation="Horizontal" Spacing="8" IsVisible="{Binding IsDesktop}">
|
<StackPanel Orientation="Horizontal" Spacing="8" IsVisible="{Binding IsDesktop}">
|
||||||
<Button Content="{Binding Loc[wallet.connect]}" Classes="accent"
|
<Button Content="{Binding Loc[wallet.connect]}" Classes="accent"
|
||||||
Command="{Binding ConnectAndSyncCommand}"/>
|
Command="{Binding ConnectAndSyncCommand}"/>
|
||||||
|
<Button Content="{Binding Loc[wallet.discover]}"
|
||||||
|
Command="{Binding DiscoverServersCommand}"/>
|
||||||
<Button Content="{Binding Loc[wallet.resetcert]}"
|
<Button Content="{Binding Loc[wallet.resetcert]}"
|
||||||
Command="{Binding ResetCertificatesCommand}"/>
|
Command="{Binding ResetCertificatesCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -1242,6 +1247,9 @@
|
|||||||
<Button Content="{Binding Loc[wallet.connect]}" Classes="accent"
|
<Button Content="{Binding Loc[wallet.connect]}" Classes="accent"
|
||||||
Command="{Binding ConnectAndSyncCommand}"
|
Command="{Binding ConnectAndSyncCommand}"
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
|
||||||
|
<Button Content="{Binding Loc[wallet.discover]}"
|
||||||
|
Command="{Binding DiscoverServersCommand}"
|
||||||
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
|
||||||
<Button Content="{Binding Loc[wallet.resetcert]}"
|
<Button Content="{Binding Loc[wallet.resetcert]}"
|
||||||
Command="{Binding ResetCertificatesCommand}"
|
Command="{Binding ResetCertificatesCommand}"
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
|
||||||
|
|||||||
+6
-3
@@ -104,8 +104,9 @@ static int Info(string[] o)
|
|||||||
Console.WriteLine($"xpub: {doc.AccountXpub}");
|
Console.WriteLine($"xpub: {doc.AccountXpub}");
|
||||||
if (doc.Cache is { } cache)
|
if (doc.Cache is { } cache)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"saldo: {CoinAmount.Format(cache.ConfirmedSats, account.Profile.CoinUnit)} confermato"
|
Console.WriteLine($"saldo: {CoinAmount.Format(cache.ConfirmedSats - cache.ImmatureSats, account.Profile.CoinUnit)} spendibile"
|
||||||
+ (cache.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(cache.UnconfirmedSats)} in attesa (non spendibile)." : ""));
|
+ (cache.ImmatureSats != 0 ? $" + {CoinAmount.Format(cache.ImmatureSats)} in maturazione (non spendibile)" : "")
|
||||||
|
+ (cache.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(cache.UnconfirmedSats)} in attesa di conferma (non spendibile)." : ""));
|
||||||
Console.WriteLine($"sync: altezza {cache.TipHeight}, {cache.History.Count} transazioni");
|
Console.WriteLine($"sync: altezza {cache.TipHeight}, {cache.History.Count} transazioni");
|
||||||
Console.WriteLine($"ricezione: {account.GetReceiveAddress(cache.NextReceiveIndex)}");
|
Console.WriteLine($"ricezione: {account.GetReceiveAddress(cache.NextReceiveIndex)}");
|
||||||
}
|
}
|
||||||
@@ -147,6 +148,7 @@ static async Task<int> Sync(string[] o)
|
|||||||
TipHeight = result.TipHeight,
|
TipHeight = result.TipHeight,
|
||||||
ConfirmedSats = result.ConfirmedSats,
|
ConfirmedSats = result.ConfirmedSats,
|
||||||
UnconfirmedSats = result.UnconfirmedSats,
|
UnconfirmedSats = result.UnconfirmedSats,
|
||||||
|
ImmatureSats = result.ImmatureSats,
|
||||||
NextReceiveIndex = result.NextReceiveIndex,
|
NextReceiveIndex = result.NextReceiveIndex,
|
||||||
NextChangeIndex = result.NextChangeIndex,
|
NextChangeIndex = result.NextChangeIndex,
|
||||||
History = [.. result.History],
|
History = [.. result.History],
|
||||||
@@ -158,7 +160,8 @@ static async Task<int> Sync(string[] o)
|
|||||||
};
|
};
|
||||||
WalletStore.Save(doc, path, Opt(o, "--password"));
|
WalletStore.Save(doc, path, Opt(o, "--password"));
|
||||||
|
|
||||||
Console.WriteLine($"Saldo: {CoinAmount.Format(result.ConfirmedSats, account.Profile.CoinUnit)} confermato"
|
Console.WriteLine($"Saldo: {CoinAmount.Format(result.ConfirmedSats - result.ImmatureSats, account.Profile.CoinUnit)} spendibile"
|
||||||
|
+ (result.ImmatureSats != 0 ? $" + {CoinAmount.Format(result.ImmatureSats)} in maturazione (non spendibile)" : "")
|
||||||
+ (result.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(result.UnconfirmedSats)} in attesa di conferma (non spendibile)" : ""));
|
+ (result.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(result.UnconfirmedSats)} in attesa di conferma (non spendibile)" : ""));
|
||||||
Console.WriteLine($"Storico ({result.History.Count}):");
|
Console.WriteLine($"Storico ({result.History.Count}):");
|
||||||
foreach (var tx in result.History)
|
foreach (var tx in result.History)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using PalladiumWallet.Core.Chain;
|
|||||||
using PalladiumWallet.Core.Crypto;
|
using PalladiumWallet.Core.Crypto;
|
||||||
using PalladiumWallet.Core.Net;
|
using PalladiumWallet.Core.Net;
|
||||||
using PalladiumWallet.Core.Storage;
|
using PalladiumWallet.Core.Storage;
|
||||||
|
using PalladiumWallet.Core.Wallet;
|
||||||
|
|
||||||
namespace PalladiumWallet.Core.Spv;
|
namespace PalladiumWallet.Core.Spv;
|
||||||
|
|
||||||
@@ -21,6 +22,13 @@ public sealed class SyncResult
|
|||||||
public required int TipHeight { get; init; }
|
public required int TipHeight { get; init; }
|
||||||
public required long ConfirmedSats { get; init; }
|
public required long ConfirmedSats { get; init; }
|
||||||
public required long UnconfirmedSats { get; init; }
|
public required long UnconfirmedSats { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Confirmed but not yet spendable: coinbase outputs below maturity or regular
|
||||||
|
/// outputs below <see cref="Chain.ChainProfile.MinConfirmations"/>. Subset of
|
||||||
|
/// <see cref="ConfirmedSats"/>.
|
||||||
|
/// </summary>
|
||||||
|
public required long ImmatureSats { get; init; }
|
||||||
public required int NextReceiveIndex { get; init; }
|
public required int NextReceiveIndex { get; init; }
|
||||||
public required int NextChangeIndex { get; init; }
|
public required int NextChangeIndex { get; init; }
|
||||||
public required IReadOnlyList<CachedTx> History { get; init; }
|
public required IReadOnlyList<CachedTx> History { get; init; }
|
||||||
@@ -276,6 +284,9 @@ public sealed class WalletSynchronizer(IWalletAccount account, ElectrumClient cl
|
|||||||
TipHeight = tip.Height,
|
TipHeight = tip.Height,
|
||||||
ConfirmedSats = utxos.Where(u => u.Height > 0).Sum(u => u.ValueSats),
|
ConfirmedSats = utxos.Where(u => u.Height > 0).Sum(u => u.ValueSats),
|
||||||
UnconfirmedSats = utxos.Where(u => u.Height <= 0).Sum(u => u.ValueSats),
|
UnconfirmedSats = utxos.Where(u => u.Height <= 0).Sum(u => u.ValueSats),
|
||||||
|
ImmatureSats = utxos.Where(u =>
|
||||||
|
u.Height > 0 && u.Confirmations(tip.Height) < u.RequiredConfirmations(account.Profile))
|
||||||
|
.Sum(u => u.ValueSats),
|
||||||
NextReceiveIndex = nextReceive,
|
NextReceiveIndex = nextReceive,
|
||||||
NextChangeIndex = nextChange,
|
NextChangeIndex = nextChange,
|
||||||
History = history,
|
History = history,
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ public sealed class SyncCache
|
|||||||
public int TipHeight { get; set; }
|
public int TipHeight { get; set; }
|
||||||
public long ConfirmedSats { get; set; }
|
public long ConfirmedSats { get; set; }
|
||||||
public long UnconfirmedSats { get; set; }
|
public long UnconfirmedSats { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Confirmed but not yet spendable (coinbase immature or under min confirmations). Subset of ConfirmedSats.</summary>
|
||||||
|
public long ImmatureSats { get; set; }
|
||||||
public int NextReceiveIndex { get; set; }
|
public int NextReceiveIndex { get; set; }
|
||||||
public int NextChangeIndex { get; set; }
|
public int NextChangeIndex { get; set; }
|
||||||
public List<CachedTx> History { get; set; } = [];
|
public List<CachedTx> History { get; set; } = [];
|
||||||
|
|||||||
@@ -52,20 +52,8 @@ public sealed class TransactionFactory(IWalletAccount account)
|
|||||||
int tipHeight,
|
int tipHeight,
|
||||||
bool sendAll = false)
|
bool sendAll = false)
|
||||||
{
|
{
|
||||||
var coinbaseMaturity = account.Profile.CoinbaseMaturity;
|
var profile = account.Profile;
|
||||||
var minConf = account.Profile.MinConfirmations;
|
var spendable = utxos.Where(u => u.IsSpendable(profile, tipHeight)).ToList();
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
if (spendable.Count == 0)
|
if (spendable.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -77,20 +65,21 @@ public sealed class TransactionFactory(IWalletAccount account)
|
|||||||
|
|
||||||
var immature = utxos.Where(u =>
|
var immature = utxos.Where(u =>
|
||||||
!u.Frozen && u.Height > 0 && u.IsCoinbase &&
|
!u.Frozen && u.Height > 0 && u.IsCoinbase &&
|
||||||
(tipHeight - u.Height + 1) < coinbaseThreshold).ToList();
|
u.Confirmations(tipHeight) < u.RequiredConfirmations(profile)).ToList();
|
||||||
if (immature.Count > 0)
|
if (immature.Count > 0)
|
||||||
{
|
{
|
||||||
var best = immature.Max(u => tipHeight - u.Height + 1);
|
var best = immature.Max(u => u.Confirmations(tipHeight));
|
||||||
reasons.Append($"{immature.Count} coinbase output(s) not yet mature ({best}/{coinbaseThreshold} confirmations). ");
|
var threshold = profile.CoinbaseMaturity + 1;
|
||||||
|
reasons.Append($"{immature.Count} coinbase output(s) not yet mature ({best}/{threshold} confirmations). ");
|
||||||
}
|
}
|
||||||
|
|
||||||
var underConf = utxos.Where(u =>
|
var underConf = utxos.Where(u =>
|
||||||
!u.Frozen && u.Height > 0 && !u.IsCoinbase &&
|
!u.Frozen && u.Height > 0 && !u.IsCoinbase &&
|
||||||
(tipHeight - u.Height + 1) < minConf).ToList();
|
u.Confirmations(tipHeight) < u.RequiredConfirmations(profile)).ToList();
|
||||||
if (underConf.Count > 0)
|
if (underConf.Count > 0)
|
||||||
{
|
{
|
||||||
var best = underConf.Max(u => tipHeight - u.Height + 1);
|
var best = underConf.Max(u => u.Confirmations(tipHeight));
|
||||||
reasons.Append($"{underConf.Count} output(s) need {minConf} confirmations ({best} so far). ");
|
reasons.Append($"{underConf.Count} output(s) need {profile.MinConfirmations} confirmations ({best} so far). ");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new WalletSpendException(reasons.Length > 0
|
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