Track wallets
MODIFY
WalletWasabi/WabiSabi/Client/CoinJoinClient.cs
WalletWasabi/WabiSabi/Client/CoinJoinClient.cs
@@ -5,8 +5,10 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Blockchain.Keys;
+using WalletWasabi.Blockchain.TransactionOutputs;
using WalletWasabi.Crypto.Randomness;
using WalletWasabi.Logging;
+using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Backend.PostRequests;
using WalletWasabi.WabiSabi.Backend.Rounds;
using WalletWasabi.WabiSabi.Client.CredentialDependencies;
@@ -22,7 +24,7 @@ namespace WalletWasabi.WabiSabi.Client
{
public CoinJoinClient(
IWabiSabiApiRequestHandler arenaRequestHandler,
- IEnumerable<Coin> coins,
+ IEnumerable<SmartCoin> coins,
Kitchen kitchen,
KeyManager keymanager,
RoundStateUpdater roundStatusUpdater)
@@ -35,7 +37,7 @@ namespace WalletWasabi.WabiSabi.Client
Coins = coins;
}
- private IEnumerable<Coin> Coins { get; set; }
+ private IEnumerable<SmartCoin> Coins { get; set; }
private SecureRandom SecureRandom { get; } = new SecureRandom();
public IWabiSabiApiRequestHandler ArenaRequestHandler { get; }
public Kitchen Kitchen { get; }
@@ -127,7 +129,7 @@ namespace WalletWasabi.WabiSabi.Client
private List<AliceClient> CreateAliceClients(RoundState roundState)
{
List<AliceClient> aliceClients = new();
- foreach (var coin in Coins)
+ foreach (var coin in Coins.Select(x => x.Coin))
{
var aliceArenaClient = new ArenaClient(
roundState.CreateAmountCredentialClient(SecureRandom),
@@ -144,8 +146,33 @@ namespace WalletWasabi.WabiSabi.Client
private async Task RegisterCoinsAsync(IEnumerable<AliceClient> aliceClients, CancellationToken cancellationToken)
{
async Task RegisterInputTask(AliceClient aliceClient)
+ {
+ var smartCoin = Coins.Single(x => x.Coin == aliceClient.Coin);
+ try
{
await aliceClient.RegisterInputAsync(cancellationToken).ConfigureAwait(false);
+ smartCoin.CoinJoinInProgress = true;
+ }
+ catch (System.Net.Http.HttpRequestException ex)
+ {
+ if (ex.InnerException is WabiSabiProtocolException wpe)
+ {
+ switch (wpe.ErrorCode)
+ {
+ case WabiSabiProtocolErrorCode.InputSpent:
+ smartCoin.SpentAccordingToBackend = true;
+ break;
+ case WabiSabiProtocolErrorCode.InputBanned:
+ smartCoin.BannedUntilUtc = DateTimeOffset.UtcNow.AddDays(1);
+ smartCoin.SetIsBanned();
+ break;
+ default:
+ return;
+ }
+
+ }
+ throw;
+ }
}
var registerRequests = aliceClients.Select(RegisterInputTask);

MODIFY
WalletWasabi/WabiSabi/Client/CoinJoinManager.cs
WalletWasabi/WabiSabi/Client/CoinJoinManager.cs
@@ -2,9 +2,12 @@ using Microsoft.Extensions.Hosting;
using NBitcoin;
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using WalletWasabi.Blockchain.TransactionOutputs;
+using WalletWasabi.Logging;
using WalletWasabi.Models;
using WalletWasabi.Tor.Socks5.Pool.Circuits;
using WalletWasabi.WabiSabi.Backend.PostRequests;
@@ -35,36 +38,89 @@ namespace WalletWasabi.WabiSabi.Client
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
- await Task.Delay(TimeSpan.FromMinutes(4), stoppingToken).ConfigureAwait(false);
+ var trackedWallets = new Dictionary<string, WalletTrackingData>();
+ Task<RoundState> roundUpdateTask = RoundStatusUpdater.CreateRoundAwaiter(roundState => roundState.Phase == Phase.InputRegistration, stoppingToken);
+ var currentRoundState = await roundUpdateTask.ConfigureAwait(false);
+
while (!stoppingToken.IsCancellationRequested)
{
- var currentRoundState = await RoundStatusUpdater.CreateRoundAwaiter(roundState => roundState.Phase == Phase.InputRegistration, stoppingToken).ConfigureAwait(false);
+ var delayTask = Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
+ var completedTask = await Task.WhenAny(
+ roundUpdateTask,
+ delayTask
+ ).ConfigureAwait(false);
+
+ if (completedTask == roundUpdateTask)
+ {
+ currentRoundState = await roundUpdateTask.ConfigureAwait(false);
+ roundUpdateTask = RoundStatusUpdater.CreateRoundAwaiter(roundState => roundState.Phase == Phase.InputRegistration, stoppingToken);
+ }
+ Logger.LogInfo($"Current round: {currentRoundState.Id} - ({currentRoundState.Phase} ({currentRoundState.BlameOf} )");
+ var mixableWallets = GetMixableWallets();
+ var openedWallets = mixableWallets.Where(x => !trackedWallets.ContainsKey(x.Key));
+ var closedWallets = trackedWallets.Where(x => !mixableWallets.ContainsKey(x.Key));
+
+ foreach (var openedWallet in openedWallets.Select(x => x.Value))
+ {
+ var coins = SelectCoinsForWallet(openedWallet, currentRoundState);
+ var coinjoinClient = new CoinJoinClient(ArenaRequestHandler, coins, openedWallet.Kitchen, openedWallet.KeyManager, RoundStatusUpdater);
+ var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
+ var coinjoinTask = coinjoinClient.StartCoinJoinAsync(cts.Token);
+
+ trackedWallets.Add(openedWallet.WalletName, new WalletTrackingData(openedWallet, coinjoinTask, cts));
+ }
+
+ foreach (var closedWallet in closedWallets.Select(x => x.Value))
+ {
+ closedWallet.CancellationTokenSource.Cancel();
+ closedWallet.CancellationTokenSource.Dispose();
+ }
- var mixableWalletsAndCoins = GetMixableWallets()
- .Select(wallet => (Wallet: wallet, Coins: SelectCoinsForWallet(wallet, currentRoundState)));
+ var finishedCoinJoins = trackedWallets
+ .Where(x => x.Value.CoinJoinTask.IsCompleted)
+ .Select(x => x.Value)
+ .ToImmutableArray();
- var coinjoinClients = mixableWalletsAndCoins
- .Select(x => new CoinJoinClient(ArenaRequestHandler, x.Coins, x.Wallet.Kitchen, x.Wallet.KeyManager, RoundStatusUpdater));
+ foreach (var finishedCoinJoin in finishedCoinJoins)
+ {
+ trackedWallets.Remove(finishedCoinJoin.Wallet.WalletName);
+ finishedCoinJoin.CancellationTokenSource.Dispose();
- await Task.WhenAll(
- coinjoinClients.Select(async x => await x.StartCoinJoinAsync(stoppingToken))).ConfigureAwait(false);
+ var finishedCoinJoinTask = finishedCoinJoin.CoinJoinTask;
+ if (finishedCoinJoinTask.IsCompletedSuccessfully)
+ {
+ Logger.LogInfo("Coinjoin client finished successfully!");
+ }
+ else if (finishedCoinJoinTask.IsFaulted)
+ {
+ Logger.LogError(finishedCoinJoinTask.Exception!);
+ }
+ else if (finishedCoinJoinTask.IsCanceled)
+ {
+ Logger.LogInfo("Coinjoin client was cancelled.");
+ }
+ }
}
}
- private IEnumerable<Coin> SelectCoinsForWallet(Wallet wallet, RoundState roundState) =>
+ private ImmutableList<SmartCoin> SelectCoinsForWallet(Wallet wallet, RoundState roundState) =>
wallet.Coins.Available().Confirmed()
.Where(x => x.HdPubKey.AnonymitySet < ServiceConfiguration.GetMixUntilAnonymitySetValue())
.Where(x => roundState.CoinjoinState.Parameters.AllowedInputAmounts.Contains(x.Amount) )
.Where(x => roundState.CoinjoinState.Parameters.AllowedInputTypes.Any(t => x.ScriptPubKey.IsScriptType(t)))
- .OrderByDescending(x => x.Amount)
+ .OrderBy(x => x.HdPubKey.AnonymitySet)
+ .ThenByDescending(x => x.Amount)
.Take(MaxInputsRegistrableByWallet)
- .Select(x => x.Coin);
+ .ToImmutableList();
- private IEnumerable<Wallet> GetMixableWallets() =>
+ private ImmutableDictionary<string, Wallet> GetMixableWallets() =>
WalletManager.GetWallets()
.Where(x => x.State == WalletState.Started) // Only running wallets
.Where(x => x.KeyManager.AutoCoinJoin) // configured to be mixed automatically
.Where(x => !x.KeyManager.IsWatchOnly) // that are not watch-only wallets
- .Where(x => x.Kitchen.HasIngredients);
+ .Where(x => x.Kitchen.HasIngredients)
+ .ToImmutableDictionary(x => x.WalletName, x => x);
+
+ record WalletTrackingData(Wallet Wallet, Task<bool> CoinJoinTask, CancellationTokenSource CancellationTokenSource);
}
}
\ No newline at end of file

Track wallets