Finalize Dust protection.
Closes
MODIFY
WalletWasabi.Gui/CommandLine/Daemon.cs
WalletWasabi.Gui/CommandLine/Daemon.cs
@@ -173,7 +173,7 @@ namespace WalletWasabi.Gui.CommandLine
await Global.InitializeNoUiAsync();
await Global.InitializeWalletServiceAsync(keyManager);
- await Global.ChaumianClient.QueueCoinsToMixAsync(password, Global.WalletService.Coins.Where(x => x.Unspent).ToArray());
+ await Global.ChaumianClient.QueueCoinsToMixAsync(password, Global.WalletService.Coins.Where(x => !x.Unavailable).ToArray());
while (Global.ChaumianClient.State.AnyCoinsQueued())
{

MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/CoinJoinTabViewModel.cs
WalletWasabi.Gui/Controls/WalletExplorer/CoinJoinTabViewModel.cs
@@ -330,7 +330,7 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
}
else
{
- var available = Global.WalletService.Coins.Where(x => x.Confirmed && !x.SpentOrCoinJoinInProgress);
+ var available = Global.WalletService.Coins.Where(x => x.Confirmed && !x.Unavailable);
if (available.Any())
{
RequiredBTC = registrableRound.State.CalculateRequiredAmount(available.Where(x => x.AnonymitySet < Global.Config.PrivacyLevelStrong).Select(x => x.Amount).ToArray());

MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/CoinListViewModel.cs
WalletWasabi.Gui/Controls/WalletExplorer/CoinListViewModel.cs
@@ -201,7 +201,7 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe().DisposeWith(Disposables);
- foreach (var sc in Global.WalletService.Coins.Where(sc => sc.Unspent))
+ foreach (var sc in Global.WalletService.Coins.Where(sc => sc.Unspent && !sc.IsDust))
{
RootList.Add(new CoinViewModel(sc).DisposeWith(Disposables));
}
@@ -331,7 +331,7 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
- foreach (SmartCoin c in e.NewItems.Cast<SmartCoin>().Where(sc => sc.Unspent))
+ foreach (SmartCoin c in e.NewItems.Cast<SmartCoin>().Where(sc => sc.Unspent && !sc.IsDust))
{
RootList.Add(new CoinViewModel(c).DisposeWith(Disposables));
}

MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/CoinViewModel.cs
WalletWasabi.Gui/Controls/WalletExplorer/CoinViewModel.cs
@@ -34,9 +34,9 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
this.RaisePropertyChanged(nameof(Confirmed));
}).DisposeWith(Disposables);
- model.WhenAnyValue(x => x.SpentOrCoinJoinInProgress).ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ =>
+ model.WhenAnyValue(x => x.Unavailable).ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ =>
{
- this.RaisePropertyChanged(nameof(SpentOrCoinJoinInProgress));
+ this.RaisePropertyChanged(nameof(Unavailable));
}).DisposeWith(Disposables);
model.WhenAnyValue(x => x.CoinJoinInProgress).ObserveOn(RxApp.MainThreadScheduler).Subscribe(_ =>
@@ -90,7 +90,7 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
public bool CoinJoinInProgress => Model.CoinJoinInProgress;
- public bool SpentOrCoinJoinInProgress => Model.SpentOrCoinJoinInProgress;
+ public bool Unavailable => Model.Unavailable;
public bool Unspent => Model.Unspent;

MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/WalletViewModel.cs
WalletWasabi.Gui/Controls/WalletExplorer/WalletViewModel.cs
@@ -85,7 +85,7 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
private void SetBalance(string walletName)
{
- Money balance = Enumerable.Where(WalletService.Coins, c => c.Unspent).Sum(c => (long?)c.Amount) ?? 0;
+ Money balance = Enumerable.Where(WalletService.Coins, c => c.Unspent && !c.IsDust && !c.SpentAccordingToBackend).Sum(c => (long?)c.Amount) ?? 0;
Title = $"{walletName} ({balance.ToString(false, true)} BTC)";
}

MODIFY
WalletWasabi.Tests/ModelTests.cs
WalletWasabi.Tests/ModelTests.cs
@@ -170,7 +170,7 @@ namespace WalletWasabi.Tests
Assert.Equal(coin.Height, deserialized.Height);
Assert.Equal(coin.Amount, deserialized.Amount);
Assert.Equal(coin.Index, deserialized.Index);
- Assert.Equal(coin.SpentOrCoinJoinInProgress, deserialized.SpentOrCoinJoinInProgress);
+ Assert.Equal(coin.Unavailable, deserialized.Unavailable);
Assert.Equal(coin.Label, deserialized.Label);
Assert.Equal(coin.ScriptPubKey, deserialized.ScriptPubKey);
Assert.Equal(coin.SpenderTransactionId, deserialized.SpenderTransactionId);

MODIFY
WalletWasabi.Tests/RegTests.cs
WalletWasabi.Tests/RegTests.cs
@@ -545,7 +545,7 @@ namespace WalletWasabi.Tests
Assert.Equal(Money.Coins(0.1m), firstCoin.Amount);
Assert.Equal(synchronizer.BestKnownFilter.BlockHeight, firstCoin.Height);
Assert.InRange(firstCoin.Index, 0U, 1U);
- Assert.False(firstCoin.SpentOrCoinJoinInProgress);
+ Assert.False(firstCoin.Unavailable);
Assert.Equal("foo label", firstCoin.Label);
Assert.Equal(key.P2wpkhScript, firstCoin.ScriptPubKey);
Assert.Null(firstCoin.SpenderTransactionId);
@@ -575,7 +575,7 @@ namespace WalletWasabi.Tests
Assert.Equal(synchronizer.BestKnownFilter.BlockHeight.Value - 2, firstCoin.Height.Value);
Assert.Equal(synchronizer.BestKnownFilter.BlockHeight.Value - 1, secondCoin.Height.Value);
Assert.Equal(synchronizer.BestKnownFilter.BlockHeight, thirdCoin.Height);
- Assert.False(thirdCoin.SpentOrCoinJoinInProgress);
+ Assert.False(thirdCoin.Unavailable);
Assert.Equal("foo label", firstCoin.Label);
Assert.Equal("bar label", secondCoin.Label);
Assert.Equal("bar label", thirdCoin.Label);
@@ -632,7 +632,7 @@ namespace WalletWasabi.Tests
Assert.Equal(Money.Coins(0.03m), rbfCoin.Amount);
Assert.Equal(synchronizer.BestKnownFilter.BlockHeight.Value - 2, rbfCoin.Height.Value);
- Assert.False(rbfCoin.SpentOrCoinJoinInProgress);
+ Assert.False(rbfCoin.Unavailable);
Assert.Equal("bar label", rbfCoin.Label);
Assert.Equal(key2.P2wpkhScript, rbfCoin.ScriptPubKey);
Assert.Null(rbfCoin.SpenderTransactionId);
@@ -805,7 +805,7 @@ namespace WalletWasabi.Tests
#region Basic
Script receive = wallet.GetReceiveKey("Basic").P2wpkhScript;
- Money amountToSend = wallet.Coins.Where(x => !x.SpentOrCoinJoinInProgress).Sum(x => x.Amount) / 2;
+ Money amountToSend = wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) / 2;
var res = wallet.BuildTransaction(password, new[] { new WalletService.Operation(receive, amountToSend, "foo") }, 1008, allowUnconfirmed: true);
foreach (SmartCoin coin in res.SpentCoins)
@@ -853,7 +853,7 @@ namespace WalletWasabi.Tests
#region SubtractFeeFromAmount
receive = wallet.GetReceiveKey("SubtractFeeFromAmount").P2wpkhScript;
- amountToSend = wallet.Coins.Where(x => !x.SpentOrCoinJoinInProgress).Sum(x => x.Amount) / 3;
+ amountToSend = wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) / 3;
res = wallet.BuildTransaction(password, new[] { new WalletService.Operation(receive, amountToSend, "foo") }, 1008, allowUnconfirmed: true, subtractFeeFromAmountIndex: 0);
Assert.Equal(2, res.InnerWalletOutputs.Count());
@@ -1009,7 +1009,7 @@ namespace WalletWasabi.Tests
Assert.Single(res.Transaction.Transaction.Outputs);
var maxBuiltTxOutput = res.Transaction.Transaction.Outputs.Single();
Assert.Equal(receive, maxBuiltTxOutput.ScriptPubKey);
- Assert.Equal(wallet.Coins.Where(x => !x.SpentOrCoinJoinInProgress).Sum(x => x.Amount) - res.Fee, maxBuiltTxOutput.Value);
+ Assert.Equal(wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) - res.Fee, maxBuiltTxOutput.Value);
await wallet.SendTransactionAsync(res.Transaction);
@@ -1022,7 +1022,7 @@ namespace WalletWasabi.Tests
var inputCountBefore = res.SpentCoins.Count();
res = wallet.BuildTransaction(password, new[] { new WalletService.Operation(receive, Money.Zero, "foo") }, 1008,
allowUnconfirmed: true,
- allowedInputs: wallet.Coins.Where(x => !x.SpentOrCoinJoinInProgress).Select(x => new TxoRef(x.TransactionId, x.Index)).Take(1));
+ allowedInputs: wallet.Coins.Where(x => !x.Unavailable).Select(x => new TxoRef(x.TransactionId, x.Index)).Take(1));
Assert.Single(res.InnerWalletOutputs);
Assert.Empty(res.OuterWalletOutputs);
@@ -1062,7 +1062,7 @@ namespace WalletWasabi.Tests
Assert.Single(res.InnerWalletOutputs);
Assert.Equal($"{Constants.ChangeOfSpecialLabelStart}my label{Constants.ChangeOfSpecialLabelEnd}", res.InnerWalletOutputs.Single().Label);
- amountToSend = wallet.Coins.Where(x => !x.SpentOrCoinJoinInProgress).Sum(x => x.Amount) / 3;
+ amountToSend = wallet.Coins.Where(x => !x.Unavailable).Sum(x => x.Amount) / 3;
res = wallet.BuildTransaction(password, new[] {
new WalletService.Operation(new Key().ScriptPubKey, amountToSend, "outgoing"),
new WalletService.Operation(new Key().ScriptPubKey, amountToSend, "outgoing2")
@@ -1094,7 +1094,7 @@ namespace WalletWasabi.Tests
receive = wallet.GetReceiveKey("AllowedInputsDisallowUnconfirmed").P2wpkhScript;
- var allowedInputs = wallet.Coins.Where(x => !x.SpentOrCoinJoinInProgress).Select(x => new TxoRef(x.TransactionId, x.Index)).Take(1);
+ var allowedInputs = wallet.Coins.Where(x => !x.Unavailable).Select(x => new TxoRef(x.TransactionId, x.Index)).Take(1);
var toSend = new[] { new WalletService.Operation(receive, Money.Zero, "fizz") };
// covers:
@@ -1573,12 +1573,12 @@ namespace WalletWasabi.Tests
Assert.Contains(wallet.Coins, x => x.TransactionId == tx1Res.Transaction.GetHash());
// There is a coin destroyed
- Assert.Equal(1, wallet.Coins.Count(x => x.SpentOrCoinJoinInProgress && x.SpenderTransactionId == tx1Res.Transaction.GetHash()));
+ Assert.Equal(1, wallet.Coins.Count(x => x.Unavailable && x.SpenderTransactionId == tx1Res.Transaction.GetHash()));
// There is at least one coin created from the destruction of the first coin
Assert.Contains(wallet.Coins, x => x.SpentOutputs.Any(o => o.TransactionId == tx0Id));
- var totalWallet = wallet.Coins.Where(c => !c.SpentOrCoinJoinInProgress).Sum(c => c.Amount);
+ var totalWallet = wallet.Coins.Where(c => !c.Unavailable).Sum(c => c.Amount);
Assert.Equal((1 * Money.COIN) - tx1Res.Fee.Satoshi, totalWallet);
// Spend the unconfirmed and unspent coin (send it to ourself)
@@ -1593,12 +1593,12 @@ namespace WalletWasabi.Tests
Assert.Contains(wallet.Coins, x => x.TransactionId == tx2Res.Transaction.GetHash());
// There is a coin destroyed
- Assert.Equal(1, wallet.Coins.Count(x => x.SpentOrCoinJoinInProgress && x.SpenderTransactionId == tx2Res.Transaction.GetHash()));
+ Assert.Equal(1, wallet.Coins.Count(x => x.Unavailable && x.SpenderTransactionId == tx2Res.Transaction.GetHash()));
// There is at least one coin created from the destruction of the first coin
Assert.Contains(wallet.Coins, x => x.SpentOutputs.Any(o => o.TransactionId == tx1Res.Transaction.GetHash()));
- totalWallet = wallet.Coins.Where(c => !c.SpentOrCoinJoinInProgress).Sum(c => c.Amount);
+ totalWallet = wallet.Coins.Where(c => !c.Unavailable).Sum(c => c.Amount);
Assert.Equal((1 * Money.COIN) - tx1Res.Fee.Satoshi - tx2Res.Fee.Satoshi, totalWallet);
Interlocked.Exchange(ref _filtersProcessedByWalletCount, 0);
@@ -3291,15 +3291,15 @@ namespace WalletWasabi.Tests
SmartCoin[] unspentChanges = wallet.Coins.Where(x => x.Label == "ZeroLink Change" && x.Unspent).ToArray();
await wallet.ChaumianClient.DequeueCoinsFromMixAsync(unspentChanges);
- Assert.Equal(3, wallet.Coins.Count(x => x.Label == "ZeroLink Mixed Coin" && !x.SpentOrCoinJoinInProgress));
- Assert.Equal(3, wallet2.Coins.Count(x => x.Label == "ZeroLink Mixed Coin" && !x.SpentOrCoinJoinInProgress));
+ Assert.Equal(3, wallet.Coins.Count(x => x.Label == "ZeroLink Mixed Coin" && !x.Unavailable));
+ Assert.Equal(3, wallet2.Coins.Count(x => x.Label == "ZeroLink Mixed Coin" && !x.Unavailable));
Assert.Equal(0, wallet.Coins.Count(x => x.Label == "ZeroLink Mixed Coin" && !x.Unspent));
Assert.Equal(0, wallet2.Coins.Count(x => x.Label == "ZeroLink Mixed Coin" && !x.Unspent));
Assert.Equal(2, wallet.Coins.Count(x => x.Label == "ZeroLink Change" && !x.Unspent));
Assert.Equal(0, wallet2.Coins.Count(x => x.Label == "ZeroLink Change"));
Assert.Equal(0, wallet.Coins.Count(x => x.Label == "ZeroLink Change" && x.Unspent));
Assert.Equal(0, wallet.Coins.Count(x => x.Label == "ZeroLink Dequeued Change" && !x.Unspent));
- Assert.Equal(1, wallet.Coins.Count(x => x.Label == "ZeroLink Dequeued Change" && !x.SpentOrCoinJoinInProgress));
+ Assert.Equal(1, wallet.Coins.Count(x => x.Label == "ZeroLink Dequeued Change" && !x.Unavailable));
}
finally
{

MODIFY
WalletWasabi/Helpers/Constants.cs
WalletWasabi/Helpers/Constants.cs
@@ -62,6 +62,6 @@ namespace WalletWasabi.Helpers
public const string ChangeOfSpecialLabelStart = "change of (";
public const string ChangeOfSpecialLabelEnd = ")";
- public static Money Dust = Money.Coins(0.0001m);
+ public static Money DustThreshold = Money.Coins(0.0001m);
}
}

MODIFY
WalletWasabi/Models/SmartCoin.cs
WalletWasabi/Models/SmartCoin.cs
@@ -43,12 +43,13 @@ namespace WalletWasabi.Models
private HdPubKey _hdPubKey;
private ISecret _secret;
+ private bool _isDust;
+ private string _clusters;
private bool _confirmed;
- private bool _spentOrCoinJoinInProgress;
+ private bool _unavailable;
private bool _unspent;
private bool _isBanned;
- private string _clusters;
#endregion Fields
@@ -141,7 +142,6 @@ namespace WalletWasabi.Models
set
{
value = Guard.Correct(value);
- value = Amount <= Constants.Dust ? "Dust" : value;
if (value != _label)
{
_label = value;
@@ -222,7 +222,8 @@ namespace WalletWasabi.Models
{
_coinJoinInProgress = value;
OnPropertyChanged(nameof(CoinJoinInProgress));
- SetSpentOrCoinJoinInProgress();
+
+ SetUnavailable();
}
}
}
@@ -249,13 +250,15 @@ namespace WalletWasabi.Models
[JsonProperty]
public bool SpentAccordingToBackend
{
- get { return _spentAccordingToBackend; }
+ get => _spentAccordingToBackend;
set
{
if (value != _spentAccordingToBackend)
{
_spentAccordingToBackend = value;
OnPropertyChanged(nameof(SpentAccordingToBackend));
+
+ SetUnavailable();
}
}
}
@@ -295,6 +298,22 @@ namespace WalletWasabi.Models
}
}
+ [JsonProperty]
+ public bool IsDust
+ {
+ get => _isDust;
+ private set
+ {
+ if (value != _isDust)
+ {
+ _isDust = value;
+ OnPropertyChanged(nameof(IsDust));
+
+ SetUnavailable();
+ }
+ }
+ }
+
public string Clusters
{
get => _clusters;
@@ -325,15 +344,18 @@ namespace WalletWasabi.Models
}
}
- public bool SpentOrCoinJoinInProgress
+ /// <summary>
+ /// Spent || SpentAccordingToBackend || CoinJoinInProgress || IsDust;
+ /// </summary>
+ public bool Unavailable
{
- get => _spentOrCoinJoinInProgress;
+ get => _unavailable;
private set
{
- if (value != _spentOrCoinJoinInProgress)
+ if (value != _unavailable)
{
- _spentOrCoinJoinInProgress = value;
- OnPropertyChanged(nameof(SpentOrCoinJoinInProgress));
+ _unavailable = value;
+ OnPropertyChanged(nameof(Unavailable));
}
}
}
@@ -348,7 +370,7 @@ namespace WalletWasabi.Models
_unspent = value;
OnPropertyChanged(nameof(Unspent));
- SetSpentOrCoinJoinInProgress();
+ SetUnavailable();
}
}
}
@@ -385,9 +407,14 @@ namespace WalletWasabi.Models
IsBanned = BannedUntilUtc != null && BannedUntilUtc > DateTimeOffset.UtcNow;
}
- private void SetSpentOrCoinJoinInProgress()
+ private void SetUnavailable()
+ {
+ Unavailable = !Unspent || SpentAccordingToBackend || CoinJoinInProgress || IsDust;
+ }
+
+ private void SetIsDust(Money dustThreshold)
{
- SpentOrCoinJoinInProgress = !Unspent || CoinJoinInProgress;
+ IsDust = Amount <= dustThreshold;
}
#endregion PropertySetters
@@ -436,7 +463,8 @@ namespace WalletWasabi.Models
SetConfirmed();
SetUnspent();
SetIsBanned();
- SetSpentOrCoinJoinInProgress();
+ SetUnavailable();
+ SetIsDust(Constants.DustThreshold);
}
#endregion Constructors

MODIFY
WalletWasabi/Services/CcjClient.cs
WalletWasabi/Services/CcjClient.cs
@@ -780,7 +780,7 @@ namespace WalletWasabi.Services
continue;
}
- if (coin.SpentOrCoinJoinInProgress)
+ if (coin.Unavailable)
{
except.Add(coin);
continue;

MODIFY
WalletWasabi/Services/WalletService.cs
WalletWasabi/Services/WalletService.cs
@@ -545,7 +545,7 @@ namespace WalletWasabi.Services
TransactionCache.TryAdd(tx);
// If it's being mixed and anonset is not sufficient, then queue it.
- if (newCoin.Unspent && ChaumianClient.HasIngredients && newCoin.Label.StartsWith("ZeroLink", StringComparison.Ordinal) && newCoin.AnonymitySet < ServiceConfiguration.MixUntilAnonymitySet)
+ if (newCoin.Unspent && !newCoin.IsDust && ChaumianClient.HasIngredients && newCoin.Label.StartsWith("ZeroLink", StringComparison.Ordinal) && newCoin.AnonymitySet < ServiceConfiguration.MixUntilAnonymitySet)
{
Task.Run(async () =>
{
@@ -974,22 +974,22 @@ namespace WalletWasabi.Services
if (allowUnconfirmed)
{
- allowedSmartCoinInputs = Coins.Where(x => !x.SpentOrCoinJoinInProgress && allowedInputs.Any(y => y.TransactionId == x.TransactionId && y.Index == x.Index)).ToList();
+ allowedSmartCoinInputs = Coins.Where(x => !x.Unavailable && allowedInputs.Any(y => y.TransactionId == x.TransactionId && y.Index == x.Index)).ToList();
}
else
{
- allowedSmartCoinInputs = Coins.Where(x => !x.SpentOrCoinJoinInProgress && x.Confirmed && allowedInputs.Any(y => y.TransactionId == x.TransactionId && y.Index == x.Index)).ToList();
+ allowedSmartCoinInputs = Coins.Where(x => !x.Unavailable && x.Confirmed && allowedInputs.Any(y => y.TransactionId == x.TransactionId && y.Index == x.Index)).ToList();
}
}
else
{
if (allowUnconfirmed)
{
- allowedSmartCoinInputs = Coins.Where(x => !x.SpentOrCoinJoinInProgress).ToList();
+ allowedSmartCoinInputs = Coins.Where(x => !x.Unavailable).ToList();
}
else
{
- allowedSmartCoinInputs = Coins.Where(x => !x.SpentOrCoinJoinInProgress && x.Confirmed).ToList();
+ allowedSmartCoinInputs = Coins.Where(x => !x.Unavailable && x.Confirmed).ToList();
}
}
@@ -1284,7 +1284,7 @@ namespace WalletWasabi.Services
{
Interlocked.Increment(ref _refreshCoinCalls);
}
- var unspentCoins = Coins.Where(c => c.Unspent); //refreshing unspent coins clusters only
+ var unspentCoins = Coins.Where(c => c.Unspent && !c.IsDust); //refreshing unspent coins clusters only
if (unspentCoins.Any())
{
ILookup<Script, SmartCoin> lookupScriptPubKey = Coins.ToLookup(c => c.ScriptPubKey, c => c);

Finalize Dust protection.
Closes https://github.com/zkSNACKs/WalletWasabi/issues/1224