Use CoinRegistry instead of a ConcurrentHashSet
MODIFY
WalletWasabi.Gui/CommandLine/Daemon.cs
WalletWasabi.Gui/CommandLine/Daemon.cs
@@ -176,14 +176,13 @@ namespace WalletWasabi.Gui.CommandLine
{
try
{
- if (mixAll)
+ var coinsView = Global.WalletService.Coins;
+ var coinsToMix = coinsView.Available();
+ if (!mixAll)
{
- await Global.ChaumianClient.QueueCoinsToMixAsync(password, Global.WalletService.Coins.Where(x => !x.Unavailable).ToArray());
- }
- else
- {
- await Global.ChaumianClient.QueueCoinsToMixAsync(password, Global.WalletService.Coins.Where(x => !x.Unavailable && x.AnonymitySet < Global.WalletService.ServiceConfiguration.MixUntilAnonymitySet).ToArray());
+ coinsToMix = coinsToMix.FilterBy(x => x.AnonymitySet < Global.WalletService.ServiceConfiguration.MixUntilAnonymitySet);
}
+ await Global.ChaumianClient.QueueCoinsToMixAsync(password, coinsToMix.ToArray());
}
catch (Exception ex)
{

MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/CoinJoinTabViewModel.cs
WalletWasabi.Gui/Controls/WalletExplorer/CoinJoinTabViewModel.cs
@@ -309,14 +309,15 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
}
else
{
- var queued = Global.WalletService.Coins.Where(x => x.CoinJoinInProgress);
+ var coins = Global.WalletService.Coins;
+ var queued = coins.CoinJoinInProcess();
if (queued.Any())
{
RequiredBTC = registrableRound.State.CalculateRequiredAmount(Global.ChaumianClient.State.GetAllQueuedCoinAmounts().ToArray());
}
else
{
- var available = Global.WalletService.Coins.Where(x => x.Confirmed && !x.Unavailable);
+ var available = coins.Confirmed().Available();
RequiredBTC = available.Any()
? registrableRound.State.CalculateRequiredAmount(available.Where(x => x.AnonymitySet < Global.Config.PrivacyLevelStrong).Select(x => x.Amount).ToArray())
: registrableRound.State.CalculateRequiredAmount();

MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/CoinListViewModel.cs
WalletWasabi.Gui/Controls/WalletExplorer/CoinListViewModel.cs
@@ -382,7 +382,7 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
{
Disposables = Disposables is null ? new CompositeDisposable() : throw new NotSupportedException($"Cannot open {GetType().Name} before closing it.");
- foreach (var sc in Global.WalletService.Coins.Where(sc => sc.Unspent))
+ foreach (var sc in Global.WalletService.Coins.UnSpent())
{
var newCoinVm = new CoinViewModel(this, sc);
newCoinVm.SubscribeEvents();
@@ -408,7 +408,8 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
- foreach (SmartCoin c in e.NewItems.Cast<SmartCoin>().Where(sc => sc.Unspent))
+ var added = new CoinsView(e.NewItems.Cast<SmartCoin>());
+ foreach (SmartCoin c in added.UnSpent())
{
var newCoinVm = new CoinViewModel(this, c);
newCoinVm.SubscribeEvents();
@@ -417,7 +418,8 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
break;
case NotifyCollectionChangedAction.Remove:
- foreach (var c in e.OldItems.Cast<SmartCoin>())
+ var removed = new CoinsView(e.OldItems.Cast<SmartCoin>());
+ foreach (var c in removed)
{
CoinViewModel toRemove = RootList.Items.FirstOrDefault(cvm => cvm.Model == c);
if (toRemove != default)

MODIFY
WalletWasabi.Gui/Controls/WalletExplorer/WalletViewModel.cs
WalletWasabi.Gui/Controls/WalletExplorer/WalletViewModel.cs
@@ -102,7 +102,7 @@ namespace WalletWasabi.Gui.Controls.WalletExplorer
observed.Subscribe(_ =>
{
- Money balance = Enumerable.Where(WalletService.Coins, c => c.Unspent && !c.SpentAccordingToBackend).Sum(c => (long?)c.Amount) ?? 0;
+ Money balance = WalletService.Coins.UnSpent().TotalAmount();
Title = $"{Name} ({(Global.UiConfig.LurkingWifeMode ? "#########" : balance.ToString(false, true))} BTC)";
})

MODIFY
WalletWasabi.Gui/Dialogs/CannotCloseDialogViewModel.cs
WalletWasabi.Gui/Dialogs/CannotCloseDialogViewModel.cs
@@ -136,7 +136,7 @@ namespace WalletWasabi.Gui.Dialogs
return;
}
- SmartCoin[] enqueuedCoins = Global.WalletService.Coins.Where(x => x.CoinJoinInProgress).ToArray();
+ SmartCoin[] enqueuedCoins = Global.WalletService.Coins.CoinJoinInProcess().ToArray();
Exception latestException = null;
foreach (var coin in enqueuedCoins)
{

MODIFY
WalletWasabi.Gui/Global.cs
WalletWasabi.Gui/Global.cs
@@ -118,7 +118,7 @@ namespace WalletWasabi.Gui
return;
}
- SmartCoin[] enqueuedCoins = WalletService.Coins.Where(x => x.CoinJoinInProgress).ToArray();
+ SmartCoin[] enqueuedCoins = WalletService.Coins.CoinJoinInProcess().ToArray();
if (enqueuedCoins.Any())
{
Logger.LogWarning("Unregistering coins in CoinJoin process.");

MODIFY
WalletWasabi.Tests/IntegrationTests/RegTests.cs
WalletWasabi.Tests/IntegrationTests/RegTests.cs
@@ -593,7 +593,7 @@ namespace WalletWasabi.Tests.IntegrationTests
await WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), 2);
Assert.Equal(3, await wallet.CountBlocksAsync());
- Assert.Equal(3, wallet.Coins.Count);
+ Assert.Equal(3, wallet.Coins.Count());
firstCoin = wallet.Coins.OrderBy(x => x.Height).First();
var secondCoin = wallet.Coins.OrderBy(x => x.Height).Take(2).Last();
var thirdCoin = wallet.Coins.OrderBy(x => x.Height).Last();
@@ -652,7 +652,7 @@ namespace WalletWasabi.Tests.IntegrationTests
Assert.Equal(4, await wallet.CountBlocksAsync());
- Assert.Equal(4, wallet.Coins.Count);
+ Assert.Equal(4, wallet.Coins.Count());
Assert.Empty(wallet.Coins.Where(x => x.TransactionId == txId4));
Assert.NotEmpty(wallet.Coins.Where(x => x.TransactionId == tx4bumpRes.TransactionId));
var rbfCoin = wallet.Coins.Single(x => x.TransactionId == tx4bumpRes.TransactionId);
@@ -1612,7 +1612,7 @@ namespace WalletWasabi.Tests.IntegrationTests
// this is necesary because we are in a fork now.
var tx0Id = await rpc.SendToAddressAsync(key.GetP2wpkhAddress(network), Money.Coins(1m),
replaceable: true);
- while (wallet.Coins.Count == 0)
+ while (wallet.Coins.Count() == 0)
{
await Task.Delay(500); // Waits for the funding transaction get to the mempool.
}
@@ -1632,7 +1632,7 @@ namespace WalletWasabi.Tests.IntegrationTests
tx1Res = wallet.BuildTransaction(password, operations, FeeStrategy.TwentyMinutesConfirmationTargetStrategy, allowUnconfirmed: true);
await wallet.SendTransactionAsync(tx1Res.Transaction);
- while (wallet.Coins.Count != 3)
+ while (wallet.Coins.Count() != 3)
{
await Task.Delay(500); // Waits for the funding transaction get to the mempool.
}
@@ -1654,7 +1654,7 @@ namespace WalletWasabi.Tests.IntegrationTests
var tx2Res = wallet.BuildTransaction(password, operations, FeeStrategy.TwentyMinutesConfirmationTargetStrategy, allowUnconfirmed: true);
await wallet.SendTransactionAsync(tx2Res.Transaction);
- while (wallet.Coins.Count != 4)
+ while (wallet.Coins.Count() != 4)
{
await Task.Delay(500); // Waits for the transaction get to the mempool.
}
@@ -1692,16 +1692,16 @@ namespace WalletWasabi.Tests.IntegrationTests
Assert.True(wallet.Coins.All(x => x.Confirmed));
// Test coin basic count.
- var coinCount = wallet.Coins.Count;
+ var coinCount = wallet.Coins.Count();
var to = wallet.GetReceiveKey(new SmartLabel("foo"));
var res = wallet.BuildTransaction(password, new PaymentIntent(to.P2wpkhScript, Money.Coins(0.2345m), label: new SmartLabel("bar")), FeeStrategy.TwentyMinutesConfirmationTargetStrategy, allowUnconfirmed: true);
await wallet.SendTransactionAsync(res.Transaction);
- Assert.Equal(coinCount + 2, wallet.Coins.Count);
+ Assert.Equal(coinCount + 2, wallet.Coins.Count());
Assert.Equal(2, wallet.Coins.Count(x => !x.Confirmed));
Interlocked.Exchange(ref _filtersProcessedByWalletCount, 0);
await rpc.GenerateAsync(1);
await WaitForFiltersToBeProcessedAsync(TimeSpan.FromSeconds(120), 1);
- Assert.Equal(coinCount + 2, wallet.Coins.Count);
+ Assert.Equal(coinCount + 2, wallet.Coins.Count());
Assert.Equal(0, wallet.Coins.Count(x => !x.Confirmed));
}
finally
@@ -1777,7 +1777,7 @@ namespace WalletWasabi.Tests.IntegrationTests
Assert.Empty(wallet.Coins);
var tx0Id = await rpc.SendToAddressAsync(key.GetP2wpkhAddress(network), Money.Coins(1m), replaceable: true);
- while (wallet.Coins.Count == 0)
+ while (wallet.Coins.Count() == 0)
{
await Task.Delay(500); // Waits for the funding transaction get to the mempool.
}
@@ -3416,7 +3416,7 @@ namespace WalletWasabi.Tests.IntegrationTests
Assert.True(3 == (await chaumianClient2.QueueCoinsToMixAsync(password, wallet2.Coins.ToArray())).Count());
Task timeout = Task.Delay(TimeSpan.FromSeconds(2 * (1 + 11 + 7 + 3 * (3 + 7))));
- while (wallet.Coins.Count != 7)
+ while (wallet.Coins.Count() != 7)
{
if (timeout.IsCompletedSuccessfully)
{

MODIFY
WalletWasabi.Tests/UnitTests/TransactionProcessorTests.cs
WalletWasabi.Tests/UnitTests/TransactionProcessorTests.cs
@@ -313,7 +313,6 @@ namespace WalletWasabi.Tests.UnitTests
return new TransactionProcessor(
txStore,
keyManager,
- new ObservableConcurrentHashSet<SmartCoin>(),
Money.Coins(0.0001m));
}

MODIFY
WalletWasabi/Models/CoinsRegistry.cs
WalletWasabi/Models/CoinsRegistry.cs
@@ -4,40 +4,53 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using NBitcoin;
-using WalletWasabi.Models;
namespace WalletWasabi.Models
{
public class CoinsRegistry : ICoinsView
{
- private HashSet<SmartCoin> _coins;
- private object _lock;
+ private HashSet<SmartCoin> Coins { get; }
+ private HashSet<SmartCoin> LatestCoinsSnapshot { get; set; }
+ private bool InvalidateSnapshot { get; set; }
+ private object Lock { get; set; }
+
public event NotifyCollectionChangedEventHandler CollectionChanged;
public CoinsRegistry()
{
- _coins = new HashSet<SmartCoin>();
- _lock = new object();
+ Coins = new HashSet<SmartCoin>();
+ LatestCoinsSnapshot = new HashSet<SmartCoin>();
+ InvalidateSnapshot = false;
+ Lock = new object();
}
private CoinsView AsCoinsView()
{
- return new CoinsView(_coins);
+ lock (Lock)
+ {
+ if (InvalidateSnapshot)
+ {
+ LatestCoinsSnapshot = Coins.ToHashSet(); // Creates a clone
+ InvalidateSnapshot = false;
+ }
+ }
+ return new CoinsView(LatestCoinsSnapshot);
}
- public bool IsEmpty => !_coins.Any();
+ public bool IsEmpty => !AsCoinsView().Any();
public SmartCoin GetByOutPoint(OutPoint outpoint)
{
- return _coins.FirstOrDefault(x => x.GetOutPoint() == outpoint);
+ return AsCoinsView().FirstOrDefault(x => x.GetOutPoint() == outpoint);
}
public bool TryAdd(SmartCoin coin)
{
var added = false;
- lock (_lock)
+ lock (Lock)
{
- added = _coins.Add(coin);
+ added = Coins.Add(coin);
+ InvalidateSnapshot |= added;
}
if (added)
@@ -51,21 +64,27 @@ namespace WalletWasabi.Models
{
var coinsToRemove = AsCoinsView().DescendantOf(coin).ToList();
coinsToRemove.Add(coin);
- lock (_lock)
+ var removedCoins = new List<SmartCoin>();
+ lock (Lock)
{
foreach (var toRemove in coinsToRemove)
{
- _coins.Remove(coin);
+ if (Coins.Remove(toRemove))
+ {
+ removedCoins.Add(toRemove);
+ }
}
+ InvalidateSnapshot = true;
}
- CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, coinsToRemove));
+ CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedCoins));
}
public void Clear()
{
- lock (_lock)
+ lock (Lock)
{
- _coins.Clear();
+ Coins.Clear();
+ InvalidateSnapshot = true;
}
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}

MODIFY
WalletWasabi/Services/TransactionProcessor.cs
WalletWasabi/Services/TransactionProcessor.cs
@@ -15,7 +15,7 @@ namespace WalletWasabi.Services
public KeyManager KeyManager { get; }
- public ObservableConcurrentHashSet<SmartCoin> Coins { get; }
+ public CoinsRegistry Coins { get; }
public Money DustThreshold { get; }
public event EventHandler<SmartCoin> CoinSpent;
@@ -27,13 +27,12 @@ namespace WalletWasabi.Services
public TransactionProcessor(
AllTransactionStore transactionStore,
KeyManager keyManager,
- ObservableConcurrentHashSet<SmartCoin> coins,
Money dustThreshold)
{
TransactionStore = Guard.NotNull(nameof(transactionStore), transactionStore);
KeyManager = Guard.NotNull(nameof(keyManager), keyManager);
- Coins = Guard.NotNull(nameof(coins), coins);
DustThreshold = Guard.NotNull(nameof(dustThreshold), dustThreshold);
+ Coins = new CoinsRegistry();
}
public bool Process(SmartTransaction tx)
@@ -93,7 +92,7 @@ namespace WalletWasabi.Services
// will add later if they came to our keys
foreach (SmartCoin doubleSpentCoin in doubleSpends.Where(x => !x.Confirmed))
{
- Coins.TryRemove(doubleSpentCoin);
+ Coins.Remove(doubleSpentCoin);
}
tx.SetReplacement();
walletRelevant = true;
@@ -108,7 +107,7 @@ namespace WalletWasabi.Services
// remove double spent coins recursively (if other coin spends it, remove that too and so on), will add later if they came to our keys
foreach (SmartCoin doubleSpentCoin in doubleSpends)
{
- Coins.TryRemove(doubleSpentCoin);
+ Coins.Remove(doubleSpentCoin);
}
walletRelevant = true;
}
@@ -124,7 +123,7 @@ namespace WalletWasabi.Services
if (allReceivedInternal)
{
// It is likely a coinjoin if the diff between receive and sent amount is small and have at least 2 equal outputs.
- Money spentAmount = Coins.Where(x => tx.Transaction.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).Sum(x => x.Amount);
+ Money spentAmount = Coins.OutPoints(tx.Transaction.Inputs.ToTxoRefs()).TotalAmount();
Money receivedAmount = tx.Transaction.Outputs.Where(x => receiveKeys.Any(y => y.P2wpkhScript == x.ScriptPubKey)).Sum(x => x.Value);
bool receivedAlmostAsMuchAsSpent = spentAmount.Almost(receivedAmount, Money.Coins(0.005m));
@@ -151,7 +150,7 @@ namespace WalletWasabi.Services
}
foundKey.SetKeyState(KeyState.Used, KeyManager);
- spentOwnCoins ??= Coins.Where(x => tx.Transaction.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).ToList();
+ spentOwnCoins ??= Coins.OutPoints(tx.Transaction.Inputs.ToTxoRefs()).ToList();
var anonset = tx.Transaction.GetAnonymitySet(i);
if (spentOwnCoins.Count != 0)
{
@@ -177,7 +176,7 @@ namespace WalletWasabi.Services
{
if (newCoin.Height != Height.Mempool) // Update the height of this old coin we already had.
{
- SmartCoin oldCoin = Coins.FirstOrDefault(x => x.TransactionId == txId && x.Index == i);
+ SmartCoin oldCoin = Coins.GetByOutPoint(new OutPoint(txId, i));
if (oldCoin != null) // Just to be sure, it is a concurrent collection.
{
oldCoin.Height = newCoin.Height;
@@ -192,7 +191,7 @@ namespace WalletWasabi.Services
{
var input = tx.Transaction.Inputs[i];
- var foundCoin = Coins.FirstOrDefault(x => x.TransactionId == input.PrevOut.Hash && x.Index == input.PrevOut.N);
+ var foundCoin = Coins.GetByOutPoint(input.PrevOut);
if (foundCoin != null)
{
walletRelevant = true;

MODIFY
WalletWasabi/Services/WalletService.cs
WalletWasabi/Services/WalletService.cs
@@ -71,7 +71,7 @@ namespace WalletWasabi.Services
public ConcurrentDictionary<uint256, (Height height, DateTimeOffset dateTime)> ProcessedBlocks { get; }
- public ObservableConcurrentHashSet<SmartCoin> Coins { get; }
+ public CoinsRegistry Coins { get; }
public event EventHandler<FilterModel> NewFilterProcessed;
@@ -100,8 +100,6 @@ namespace WalletWasabi.Services
ProcessedBlocks = new ConcurrentDictionary<uint256, (Height height, DateTimeOffset dateTime)>();
HandleFiltersLock = new AsyncLock();
- Coins = new ObservableConcurrentHashSet<SmartCoin>();
-
BlocksFolderPath = Path.Combine(workFolderDir, "Blocks", Network.ToString());
RuntimeParams.SetDataDir(workFolderDir);
@@ -110,7 +108,8 @@ namespace WalletWasabi.Services
KeyManager.AssertCleanKeysIndexed();
KeyManager.AssertLockedInternalKeysIndexed(14);
- TransactionProcessor = new TransactionProcessor(BitcoinStore.TransactionStore, KeyManager, Coins, ServiceConfiguration.DustThreshold);
+ TransactionProcessor = new TransactionProcessor(BitcoinStore.TransactionStore, KeyManager, ServiceConfiguration.DustThreshold);
+ Coins = TransactionProcessor.Coins;
TransactionProcessor.CoinSpent += TransactionProcessor_CoinSpent;
TransactionProcessor.CoinReceived += TransactionProcessor_CoinReceivedAsync;
@@ -171,7 +170,7 @@ namespace WalletWasabi.Services
{
foreach (var toAlsoRemove in Coins.Where(x => x.TransactionId == toRemove.SpenderTransactionId).Distinct().ToList())
{
- Coins.TryRemove(toAlsoRemove);
+ Coins.Remove(toAlsoRemove);
}
}
}
@@ -209,9 +208,9 @@ namespace WalletWasabi.Services
ProcessedBlocks.TryRemove(invalidBlockHash, out _);
if (blockState != null && blockState.BlockHeight != default(Height))
{
- foreach (var toRemove in Coins.Where(x => x.Height == blockState.BlockHeight).Distinct().ToList())
+ foreach (var toRemove in Coins.AtBlockHeight(blockState.BlockHeight))
{
- Coins.TryRemove(toRemove);
+ Coins.Remove(toRemove);
}
}
}
@@ -919,7 +918,7 @@ namespace WalletWasabi.Services
if (transaction.Transaction.Inputs.Count == 1) // If we tried to only spend one coin, then we can mark it as spent. If there were more coins, then we do not know.
{
OutPoint input = transaction.Transaction.Inputs.First().PrevOut;
- SmartCoin coin = Coins.FirstOrDefault(x => x.TransactionId == input.Hash && x.Index == input.N);
+ SmartCoin coin = Coins.GetByOutPoint(input);
if (coin != default)
{
coin.SpentAccordingToBackend = true;
@@ -999,7 +998,7 @@ namespace WalletWasabi.Services
try
{
- var unspentCoins = Coins.Where(c => c.Unspent); //refreshing unspent coins clusters only
+ var unspentCoins = Coins.UnSpent(); //refreshing unspent coins clusters only
if (unspentCoins.Any())
{
ILookup<Script, SmartCoin> lookupScriptPubKey = Coins.ToLookup(c => c.ScriptPubKey, c => c);

Use CoinRegistry instead of a ConcurrentHashSet