Implement `Start`, `StartAutomatically` and `Stop`
MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/CoinJoinStateViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/CoinJoinStateViewModel.cs
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
+using System.Threading;
using System.Windows.Input;
using Avalonia.Threading;
using NBitcoin;
@@ -160,14 +161,14 @@ public partial class CoinJoinStateViewModel : ViewModelBase
_stateMachine.Configure(State.Stopped)
.SubstateOf(State.ManualCoinJoin)
.Permit(Trigger.Play, State.ManualPlaying)
- .OnEntry(() =>
+ .OnEntry(async () =>
{
ProgressValue = 0;
StopVisible = false;
PlayVisible = true;
_wallet.AllowManualCoinJoin = false;
CurrentStatus = _stoppedMessage;
- coinJoinManager.Stop(_wallet);
+ await coinJoinManager.StopAsync(_wallet, CancellationToken.None);
})
.OnEntry(UpdateWalletMixedProgress)
.OnTrigger(Trigger.BalanceChanged, UpdateWalletMixedProgress);
@@ -176,12 +177,12 @@ public partial class CoinJoinStateViewModel : ViewModelBase
.SubstateOf(State.ManualCoinJoin)
.Permit(Trigger.Stop, State.Stopped)
.Permit(Trigger.RoundStartFailed, State.ManualFinished)
- .OnEntry(() =>
+ .OnEntry(async () =>
{
PlayVisible = false;
StopVisible = true;
CurrentStatus = _coinJoiningMessage;
- coinJoinManager.Start(_wallet);
+ await coinJoinManager.StartAsync(_wallet, CancellationToken.None);
})
.OnEntry(UpdateWalletMixedProgress)
.OnTrigger(Trigger.BalanceChanged, UpdateWalletMixedProgress);
@@ -205,7 +206,7 @@ public partial class CoinJoinStateViewModel : ViewModelBase
.Permit(Trigger.AutoCoinJoinEntered, State.AutoStarting)
.Permit(Trigger.RoundStart, State.AutoPlaying)
.Permit(Trigger.RoundStartFailed, State.AutoFinished)
- .OnEntry(() =>
+ .OnEntry(async () =>
{
IsAuto = true;
StopVisible = false;
@@ -214,7 +215,7 @@ public partial class CoinJoinStateViewModel : ViewModelBase
CurrentStatus = _initialisingMessage;
- coinJoinManager.Stop(_wallet);
+ await coinJoinManager.StopAsync(_wallet, CancellationToken.None);
_stateMachine.Fire(Trigger.AutoCoinJoinEntered);
});
@@ -250,7 +251,7 @@ public partial class CoinJoinStateViewModel : ViewModelBase
_stateMachine.Configure(State.Paused)
.SubstateOf(State.AutoCoinJoin)
.Permit(Trigger.Play, State.AutoPlaying)
- .OnEntry(() =>
+ .OnEntry(async () =>
{
IsAutoWaiting = true;
@@ -260,7 +261,7 @@ public partial class CoinJoinStateViewModel : ViewModelBase
PauseVisible = false;
PlayVisible = true;
- coinJoinManager.Stop(_wallet);
+ await coinJoinManager.StopAsync(_wallet, CancellationToken.None);
})
.OnEntry(UpdateWalletMixedProgress)
.OnTrigger(Trigger.BalanceChanged, UpdateWalletMixedProgress);
@@ -270,7 +271,7 @@ public partial class CoinJoinStateViewModel : ViewModelBase
.Permit(Trigger.Pause, State.Paused)
.Permit(Trigger.PlebStop, State.Paused)
.Permit(Trigger.RoundStartFailed, State.AutoFinished)
- .OnEntry(() =>
+ .OnEntry(async () =>
{
CurrentStatus = _coinJoiningMessage;
IsAutoWaiting = false;
@@ -278,7 +279,7 @@ public partial class CoinJoinStateViewModel : ViewModelBase
PlayVisible = false;
_autoStartTime = TimeSpan.Zero;
_countDownStarted = DateTimeOffset.Now;
- coinJoinManager.Start(_wallet);
+ await coinJoinManager.StartAsync(_wallet, CancellationToken.None);
})
.OnEntry(UpdateWalletMixedProgress)
.OnTrigger(Trigger.BalanceChanged, UpdateWalletMixedProgress);

MODIFY
WalletWasabi/WabiSabi/Client/CoinJoinManager.cs
WalletWasabi/WabiSabi/Client/CoinJoinManager.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
+using System.Threading.Channels;
using System.Threading.Tasks;
using WalletWasabi.Blockchain.Keys;
using WalletWasabi.Blockchain.TransactionOutputs;
@@ -15,14 +16,13 @@ using WalletWasabi.WebClients.Wasabi;
namespace WalletWasabi.WabiSabi.Client;
-internal enum CoinJoinCommand
-{
- Start,
- Stop,
-}
public class CoinJoinManager : BackgroundService
{
+ private record CoinJoinCommand(Wallet Wallet);
+ private record StartCoinJoinCommand(Wallet Wallet, bool RestartAutomatically) : CoinJoinCommand(Wallet);
+ private record StopCoinJoinCommand(Wallet Wallet) : CoinJoinCommand(Wallet);
+
public CoinJoinManager(WalletManager walletManager, RoundStateUpdater roundStatusUpdater, IWasabiHttpClientFactory backendHttpClientFactory, ServiceConfiguration serviceConfiguration)
{
WalletManager = walletManager;
@@ -35,15 +35,33 @@ public class CoinJoinManager : BackgroundService
public IWasabiHttpClientFactory HttpClientFactory { get; }
public RoundStateUpdater RoundStatusUpdater { get; }
public ServiceConfiguration ServiceConfiguration { get; }
- private ImmutableDictionary<string, CoinJoinTracker> TrackedCoinJoins { get; set; } = ImmutableDictionary<string, CoinJoinTracker>.Empty;
+ private ImmutableDictionary<string, CoinJoinTracker> TrackedCoinJoins { get; } = ImmutableDictionary<string, CoinJoinTracker>.Empty;
private CoinRefrigerator CoinRefrigerator { get; } = new();
public bool IsUserInSendWorkflow { get; set; }
+ public event EventHandler<StatusChangedEventArgs>? StatusChanged;
- public void Start(Wallet wallet){}
+ private readonly Channel<CoinJoinCommand> _channel = Channel.CreateUnbounded<CoinJoinCommand>();
- public void Stop(Wallet wallet){}
+ public async Task StartAsync(Wallet wallet, CancellationToken cancellationToken)
+ {
+ await _channel.Writer.WriteAsync(new StartCoinJoinCommand(wallet, false), cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task StartAutomaticallyAsync(Wallet wallet, CancellationToken cancellationToken)
+ {
+ if (wallet.NonPrivateCoins.TotalAmount() <= wallet.KeyManager.PlebStopThreshold)
+ {
+ NotifyCoinJoinStartError(wallet, CoinjoinError.NotEnoughUnprivateBalance);
+ return;
+ }
+ await _channel.Writer.WriteAsync(new StartCoinJoinCommand(wallet, true), cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task StopAsync(Wallet wallet, CancellationToken cancellationToken)
+ {
+ await _channel.Writer.WriteAsync(new StopCoinJoinCommand(wallet), cancellationToken).ConfigureAwait(false);
+ }
- public event EventHandler<StatusChangedEventArgs>? StatusChanged;
public CoinJoinClientState GetHighestCoinJoinClientState()
{
@@ -66,33 +84,84 @@ public class CoinJoinManager : BackgroundService
Logger.LogInfo("WabiSabi coinjoin client-side functionality is disabled temporarily on mainnet.");
return;
}
- CoinJoinTrackerFactory coinJoinTrackerFactory = new(HttpClientFactory, RoundStatusUpdater, stoppingToken);
-
- var trackedCoinJoins = new Dictionary<string, CoinJoinTracker>();
+ // Detects and notify about wallets that can participate in coinjoin.
+ var walletsMonitoringTask = Task.Run(async () =>
+ {
+ var trackedWallets = new Dictionary<string, Wallet>();
while (!stoppingToken.IsCancellationRequested)
{
- await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken).ConfigureAwait(false);
-
var mixableWallets = RoundStatusUpdater.AnyRound
? GetMixableWallets()
: ImmutableDictionary<string, Wallet>.Empty;
// Nofify when a wallet meets the criteria for participating in a coinjoin.
- var openedWallets = mixableWallets.Where(x => !trackedCoinJoins.ContainsKey(x.Key)).ToImmutableList();
+ var openedWallets = mixableWallets.Where(x => !trackedWallets.ContainsKey(x.Key)).ToImmutableList();
foreach (var openedWallet in openedWallets.Select(x => x.Value))
{
+ trackedWallets.Add(openedWallet.WalletName, openedWallet);
NotifyMixableWalletLoaded(openedWallet);
}
// Nofitify when a wallet is no longer meet the criteria for participating in a coinjoin.
- var closedWallets = trackedCoinJoins.Where(x => !mixableWallets.ContainsKey(x.Key)).ToImmutableList();
+ var closedWallets = trackedWallets.Where(x => !mixableWallets.ContainsKey(x.Key)).ToImmutableList();
foreach (var closedWallet in closedWallets.Select(x => x.Value))
{
- closedWallet.Cancel();
+ //closedWallet.Cancel();
NotifyMixableWalletUnloaded(closedWallet);
+ trackedWallets.Remove(closedWallet.WalletName);
+ }
+ await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken).ConfigureAwait(false);
}
+ }, stoppingToken);
+ var coinJoinsTrackerTask = Task.Run(() =>
+ {
+ var coinJoinTrackerFactory = new CoinJoinTrackerFactory(HttpClientFactory, RoundStatusUpdater, stoppingToken);
+ var trackedCoinJoins = new Dictionary<string, CoinJoinTracker>();
+
+ _ = Task.Run(async () =>
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ var command = await _channel.Reader.ReadAsync().ConfigureAwait(false);
+ switch (command)
+ {
+ case StartCoinJoinCommand startCommand:
+ var walletToStart = startCommand.Wallet;
+ if (trackedCoinJoins.ContainsKey(walletToStart.WalletName))
+ {
+ continue;
+ }
+ var coinCandidates = SelectCandidateCoins(walletToStart).ToArray();
+ if (coinCandidates.Length == 0)
+ {
+ NotifyCoinJoinStartError(walletToStart, CoinjoinError.NoCoinsToMix);
+ continue;
+ }
+
+ var coinJoinTracker = coinJoinTrackerFactory.CreateAndStart(walletToStart, coinCandidates, startCommand.RestartAutomatically);
+
+ trackedCoinJoins.Add(walletToStart.WalletName, coinJoinTracker);
+ var registrationTimeout = TimeSpan.MaxValue;
+ NotifyCoinJoinStarted(walletToStart, registrationTimeout);
+ break;
+
+ case StopCoinJoinCommand stopCommand:
+ var walletToStop = stopCommand.Wallet;
+ if (trackedCoinJoins.TryGetValue(walletToStop.WalletName, out var coinJoinTrackerToStop))
+ {
+ coinJoinTrackerToStop.Cancel();
+ trackedCoinJoins.Remove(walletToStop.WalletName);
+ }
+ break;
+ }
+ }
+ });
+ _ = Task.Run(async () =>
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
// Handles coinjoin finalization and notification.
var finishedCoinJoins = trackedCoinJoins.Where(x => x.Value.IsCompleted).Select(x => x.Value).ToImmutableArray();
foreach (var finishedCoinJoin in finishedCoinJoins)
@@ -100,9 +169,11 @@ public class CoinJoinManager : BackgroundService
NotifyCoinJoinCompletion(finishedCoinJoin);
await HandleCoinJoinFinalizationAsync(finishedCoinJoin, trackedCoinJoins, stoppingToken).ConfigureAwait(false);
}
-
- TrackedCoinJoins = trackedCoinJoins.ToImmutableDictionary();
}
+ });
+ }, stoppingToken);
+
+ await Task.WhenAll(walletsMonitoringTask, coinJoinsTrackerTask).ConfigureAwait(false);
}
private async Task HandleCoinJoinFinalizationAsync(CoinJoinTracker finishedCoinJoin, Dictionary<string, CoinJoinTracker> trackedCoinJoins, CancellationToken cancellationToken)
@@ -151,6 +222,11 @@ public class CoinJoinManager : BackgroundService
{
coins.CoinJoinInProgress = false;
}
+
+ if (finishedCoinJoin.RestartAutomatically && !finishedCoinJoin.CoinJoinTask.IsCanceled)
+ {
+ await StartAutomaticallyAsync(finishedCoinJoin.Wallet, cancellationToken).ConfigureAwait(false);
+ }
}
/// <summary>
@@ -175,8 +251,8 @@ public class CoinJoinManager : BackgroundService
private void NotifyCoinJoinStartError(Wallet openedWallet, CoinjoinError error) =>
SafeRaiseEvent(StatusChanged, new StartErrorEventArgs(openedWallet, error));
- private void NotifyMixableWalletUnloaded(CoinJoinTracker closedWallet) =>
- SafeRaiseEvent(StatusChanged, new StoppedEventArgs(closedWallet.Wallet, StopReason.WalletUnloaded));
+ private void NotifyMixableWalletUnloaded(Wallet closedWallet) =>
+ SafeRaiseEvent(StatusChanged, new StoppedEventArgs(closedWallet, StopReason.WalletUnloaded));
private void NotifyMixableWalletLoaded(Wallet openedWallet) =>
SafeRaiseEvent(StatusChanged, new LoadedEventArgs(openedWallet));

MODIFY
WalletWasabi/WabiSabi/Client/CoinJoinTracker.cs
WalletWasabi/WabiSabi/Client/CoinJoinTracker.cs
@@ -14,11 +14,13 @@ public class CoinJoinTracker : IDisposable
Wallet wallet,
CoinJoinClient coinJoinClient,
IEnumerable<SmartCoin> coinCandidates,
+ bool restartAutomatically,
CancellationToken cancellationToken)
{
Wallet = wallet;
CoinJoinClient = coinJoinClient;
CoinCandidates = coinCandidates;
+ RestartAutomatically = restartAutomatically;
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
CoinJoinTask = coinJoinClient.StartCoinJoinAsync(coinCandidates, CancellationTokenSource.Token);
}
@@ -29,6 +31,8 @@ public class CoinJoinTracker : IDisposable
public Wallet Wallet { get; }
public Task<CoinJoinResult> CoinJoinTask { get; }
public IEnumerable<SmartCoin> CoinCandidates { get; }
+ public bool RestartAutomatically { get; }
+
public bool IsCompleted => CoinJoinTask.IsCompleted;
public bool InCriticalCoinJoinState => CoinJoinClient.InCriticalCoinJoinState;

MODIFY
WalletWasabi/WabiSabi/Client/CoinJoinTrackerFactory.cs
WalletWasabi/WabiSabi/Client/CoinJoinTrackerFactory.cs
@@ -23,7 +23,7 @@ public class CoinJoinTrackerFactory
private RoundStateUpdater RoundStatusUpdater { get; }
private CancellationToken CancellationToken { get; }
- public CoinJoinTracker CreateAndStart(Wallet wallet, IEnumerable<SmartCoin> coinCandidates)
+ public CoinJoinTracker CreateAndStart(Wallet wallet, IEnumerable<SmartCoin> coinCandidates, bool restartAutomatically)
{
var coinJoinClient = new CoinJoinClient(
HttpClientFactory,
@@ -34,6 +34,6 @@ public class CoinJoinTrackerFactory
feeRateMedianTimeFrame: TimeSpan.FromHours(wallet.KeyManager.FeeRateMedianTimeFrameHours),
doNotRegisterInLastMinuteTimeLimit: TimeSpan.FromMinutes(1));
- return new CoinJoinTracker(wallet, coinJoinClient, coinCandidates, CancellationToken);
+ return new CoinJoinTracker(wallet, coinJoinClient, coinCandidates, restartAutomatically, CancellationToken);
}
}

Implement `Start`, `StartAutomatically` and `Stop`