An almost working version
MODIFY
WalletWasabi.Gui/Global.cs
WalletWasabi.Gui/Global.cs
@@ -35,6 +35,7 @@ using WalletWasabi.Services.Terminate;
using WalletWasabi.Stores;
using WalletWasabi.Tor;
using WalletWasabi.Tor.Socks5.Pool.Circuits;
+using WalletWasabi.WabiSabi.Client;
using WalletWasabi.Wallets;
using WalletWasabi.WebClients.BlockstreamInfo;
using WalletWasabi.WebClients.Wasabi;
@@ -222,6 +223,8 @@ namespace WalletWasabi.Gui
HostedServices.Register<BlockstreamInfoFeeProvider>(new BlockstreamInfoFeeProvider(TimeSpan.FromMinutes(3), new(Network, ExternalHttpClientFactory)) { IsPaused = true }, "Blockstream.info Fee Provider");
HostedServices.Register<ThirdPartyFeeProvider>(new ThirdPartyFeeProvider(TimeSpan.FromSeconds(1), Synchronizer, HostedServices.Get<BlockstreamInfoFeeProvider>()), "Third Party Fee Provider");
HostedServices.Register<HybridFeeProvider>(new HybridFeeProvider(HostedServices.Get<ThirdPartyFeeProvider>(), HostedServices.GetOrDefault<RpcFeeProvider>()), "Hybrid Fee Provider");
+ HostedServices.Register<RoundStateUpdater>(new RoundStateUpdater(TimeSpan.FromSeconds(5), new WabiSabiHttpApiClient(BackendHttpClientFactory.NewBackendHttpClient(Mode.SingleCircuitPerLifetime))), "Round infor updater");
+ HostedServices.Register<CoinJoinManager>(new CoinJoinManager(WalletManager, HostedServices.Get<RoundStateUpdater>(), BackendHttpClientFactory, Config.ServiceConfiguration), "CoinJoin Manager");
await HostedServices.StartAllAsync(cancel).ConfigureAwait(false);

ADD
WalletWasabi/WabiSabi/Client/CoinJoinManager.cs
+using Microsoft.Extensions.Hosting;
+using NBitcoin;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Models;
+using WalletWasabi.Tor.Socks5.Pool.Circuits;
+using WalletWasabi.WabiSabi.Backend.PostRequests;
+using WalletWasabi.WabiSabi.Backend.Rounds;
+using WalletWasabi.WabiSabi.Models;
+using WalletWasabi.Wallets;
+using WalletWasabi.WebClients.Wasabi;
+
+namespace WalletWasabi.WabiSabi.Client
+{
+ public class CoinJoinManager : BackgroundService
+ {
+ private const int MaxInputsRegistrableByWallet = 7; // how many
+
+ public CoinJoinManager(WalletManager walletManager, RoundStateUpdater roundStatusUpdater, HttpClientFactory backendHttpClientFactory, ServiceConfiguration serviceConfiguration)
+ {
+ WalletManager = walletManager;
+ ArenaRequestHandler = new WabiSabiHttpApiClient(backendHttpClientFactory.NewBackendHttpClient(Mode.SingleCircuitPerLifetime));
+ RoundStatusUpdater = roundStatusUpdater;
+ ServiceConfiguration = serviceConfiguration;
+ }
+
+ public WalletManager WalletManager { get; }
+ public IWabiSabiApiRequestHandler ArenaRequestHandler { get; }
+ public RoundStateUpdater RoundStatusUpdater { get; }
+ public ServiceConfiguration ServiceConfiguration { get; }
+ //public HttpClientFactory BackendHttpClientFactory { get; }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ await Task.Delay(TimeSpan.FromMinutes(4), stoppingToken).ConfigureAwait(false);
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ var currentRoundState = await RoundStatusUpdater.CreateRoundAwaiter(roundState => roundState.Phase == Phase.InputRegistration, stoppingToken).ConfigureAwait(false);
+
+ var mixableWalletsAndCoins = GetMixableWallets()
+ .Select(wallet => (Wallet: wallet, Coins: SelectCoinsForWallet(wallet, currentRoundState)));
+
+ var coinjoinClients = mixableWalletsAndCoins
+ .Select(x => new CoinJoinClient(ArenaRequestHandler, x.Coins, x.Wallet.Kitchen, x.Wallet.KeyManager, RoundStatusUpdater));
+
+ await Task.WhenAll(
+ coinjoinClients.Select(async x => await x.StartCoinJoinAsync(stoppingToken))).ConfigureAwait(false);
+ }
+ }
+
+ private IEnumerable<Coin> 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)
+ .Take(MaxInputsRegistrableByWallet)
+ .Select(x => x.Coin);
+
+ private IEnumerable<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);
+ }
+}
\ No newline at end of file

MODIFY
WalletWasabi/WabiSabi/Models/MoneyRange.cs
WalletWasabi/WabiSabi/Models/MoneyRange.cs
@@ -2,5 +2,9 @@ using NBitcoin;
namespace WalletWasabi.WabiSabi.Models
{
- public record MoneyRange(Money Min, Money Max);
+ public record MoneyRange(Money Min, Money Max)
+ {
+ public bool Contains(Money value) =>
+ value >= Min && value <= Max;
+ }
}

An almost working version