Add RoundStatusUpdater
MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Client/CoinJoinClientTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Client/CoinJoinClientTests.cs
@@ -53,7 +53,10 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Client
var wabiSabiApi = new WabiSabiController(coordinator);
- using CoinJoinClient coinJoinClient = new(roundState.Id, wabiSabiApi, new[] { smartCoin.Coin }, kitchen, km);
+ using var roundStateUpdater = new RoundStatusUpdater(TimeSpan.FromSeconds(1), wabiSabiApi);
+ await roundStateUpdater.StartAsync(CancellationToken.None);
+
+ using CoinJoinClient coinJoinClient = new(roundState.Id, wabiSabiApi, new[] { smartCoin.Coin }, kitchen, km, roundStateUpdater);
await coinJoinClient.StartAsync(CancellationToken.None);
}
}

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Integration/WabiSabiHttpApiIntegrationTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Integration/WabiSabiHttpApiIntegrationTests.cs
+using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -103,11 +104,16 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Integration
// Create the coinjoin client
var apiClient = _apiApplicationFactory.CreateWabiSabiHttpApiClient(httpClient);
+
var rounds = await apiClient.GetStatusAsync(CancellationToken.None);
var roundState = rounds.First(x => x.CoinjoinState is ConstructionState);
var kitchen = new Kitchen();
kitchen.Cook("");
- using var coinJoinClient = new CoinJoinClient(roundState.Id, apiClient, coins, kitchen, keyManager);
+
+ using var roundStateUpdater = new RoundStatusUpdater(TimeSpan.FromSeconds(1), apiClient);
+ await roundStateUpdater.StartAsync(CancellationToken.None);
+
+ using var coinJoinClient = new CoinJoinClient(roundState.Id, apiClient, coins, kitchen, keyManager, roundStateUpdater);
// Run the coinjoin client task.
await coinJoinClient.StartAsync(CancellationToken.None);

MODIFY
WalletWasabi/WabiSabi/Client/CoinJoinClient.cs
WalletWasabi/WabiSabi/Client/CoinJoinClient.cs
@@ -27,12 +27,14 @@ namespace WalletWasabi.WabiSabi.Client
IWabiSabiApiRequestHandler arenaRequestHandler,
IEnumerable<Coin> coins,
Kitchen kitchen,
- KeyManager keymanager)
+ KeyManager keymanager,
+ RoundStatusUpdater roundStatusUpdater)
{
RoundId = roundId;
ArenaRequestHandler = arenaRequestHandler;
Kitchen = kitchen;
Keymanager = keymanager;
+ RoundStatusUpdater = roundStatusUpdater;
SecureRandom = new SecureRandom();
Coins = coins;
}
@@ -48,6 +50,7 @@ namespace WalletWasabi.WabiSabi.Client
public IWabiSabiApiRequestHandler ArenaRequestHandler { get; }
public Kitchen Kitchen { get; }
public KeyManager Keymanager { get; }
+ private RoundStatusUpdater RoundStatusUpdater { get; }
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
@@ -78,13 +81,12 @@ namespace WalletWasabi.WabiSabi.Client
aliceClients = await ConfirmConnectionsAsync(aliceClients, cancellationToken).ConfigureAwait(false);
// Output registration.
- // Here we should have something like:
- // RoundState roundState = await OutputRegistrationPhase.ConfigureAwait(false);
- await WaitFor(Phase.OutputRegistration, cancellationToken).ConfigureAwait(false);
+ RoundState = await RoundStatusUpdater.CreateRoundAwaiter(RoundId, rs => rs.Phase == Phase.OutputRegistration).ConfigureAwait(false);
var outputsWithCredentials = outputs.Zip(aliceClients, (output, alice) => (output, alice.RealAmountCredentials, alice.RealVsizeCredentials));
await RegisterOutputsAsync(outputsWithCredentials, cancellationToken).ConfigureAwait(false);
- await WaitFor(Phase.TransactionSigning, cancellationToken).ConfigureAwait(false);
+ // Signing.
+ RoundState = await RoundStatusUpdater.CreateRoundAwaiter(RoundId, rs => rs.Phase == Phase.TransactionSigning).ConfigureAwait(false);
var signingState = RoundState.Assert<SigningState>();
var unsignedCoinJoin = signingState.CreateUnsignedTransaction();
@@ -100,17 +102,6 @@ namespace WalletWasabi.WabiSabi.Client
}
}
- private async Task WaitFor(Phase expectedPhase, CancellationToken cancellationToken)
- {
- // ideally this should await for a CompletionTask<RoundState> instead of
- // iterate in this absurd way.
- while (RoundState.Phase < expectedPhase)
- {
- await RefreshRoundAsync(cancellationToken).ConfigureAwait(false);
- await Task.Delay(500).ConfigureAwait(false);
- }
- }
-
private async Task RefreshRoundAsync(CancellationToken cancellationToken)
{
// this code is part of a `RoundUpdater` background service that fetches this information

ADD
WalletWasabi/WabiSabi/Client/RoundStatusUpdater.cs
+using NBitcoin;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Bases;
+using WalletWasabi.WabiSabi.Backend.PostRequests;
+using WalletWasabi.WabiSabi.Backend.Rounds;
+using WalletWasabi.WabiSabi.Models;
+
+namespace WalletWasabi.WabiSabi.Client
+{
+ public class RoundStatusUpdater : PeriodicRunner
+ {
+ private IWabiSabiApiRequestHandler ArenaRequestHandler { get; }
+ private ImmutableDictionary<uint256, RoundState> RoundStates { get; set; } = ImmutableDictionary.Create<uint256, RoundState>();
+
+ private Dictionary<uint256, List<(TaskCompletionSource<RoundState> Task, Predicate<RoundState> Predicate)>> Awaiters { get; set; } = new();
+
+ public RoundStatusUpdater(TimeSpan requestInterval, IWabiSabiApiRequestHandler arenaRequestHandler) : base(requestInterval)
+ {
+ ArenaRequestHandler = arenaRequestHandler;
+ }
+
+ protected override async Task ActionAsync(CancellationToken cancellationToken)
+ {
+ var responseRoundStates = (await ArenaRequestHandler.GetStatusAsync(cancellationToken).ConfigureAwait(false))
+ .ToDictionary(round => round.Id);
+
+ var updatedRoundStates = responseRoundStates.Where(round => RoundStates.ContainsKey(round.Key));
+ var newRoundStates = responseRoundStates.Where(round => !RoundStates.ContainsKey(round.Key));
+ var removedRoundStates = RoundStates.Where(round => !responseRoundStates.ContainsKey(round.Key));
+
+ List<uint256> roundsToUpdate = new();
+
+ foreach (var updatedRound in updatedRoundStates.Select(round => round.Value))
+ {
+ var oldRound = RoundStates[updatedRound.Id];
+ if (oldRound != updatedRound)
+ {
+ roundsToUpdate.Add(updatedRound.Id);
+ }
+ }
+
+ foreach (var round in newRoundStates.Select(round => round.Value))
+ {
+ roundsToUpdate.Add(round.Id);
+ }
+
+ foreach (var round in removedRoundStates.Select(round => round.Value))
+ {
+ // TODO: set all associated tasks to failed.
+ }
+
+ RoundStates = updatedRoundStates.Union(newRoundStates).ToImmutableDictionary();
+
+ ExecuteAwaiters(roundsToUpdate, RoundStates);
+ }
+
+ private void ExecuteAwaiters(
+ IEnumerable<uint256> roundsToUpdate,
+ ImmutableDictionary<uint256, RoundState> roundStates)
+ {
+ foreach (var roundId in roundsToUpdate.Where(rid => roundStates.ContainsKey(rid) && Awaiters.ContainsKey(rid)))
+ {
+ var roundState = roundStates[roundId];
+ var taskAndPredicateList = Awaiters[roundId];
+ foreach (var taskAndPredicate in taskAndPredicateList.ToArray())
+ {
+ if (taskAndPredicate.Predicate(roundState))
+ {
+ var task = taskAndPredicate.Task;
+ task.TrySetResult(roundState);
+ taskAndPredicateList.Remove(taskAndPredicate);
+ }
+ }
+ }
+ }
+
+ public Task<RoundState> CreateRoundAwaiter(uint256 roundId, Predicate<RoundState> predicate)
+ {
+ // TODO: Add cancellationToken
+ var tcs = new TaskCompletionSource<RoundState>();
+ if (!Awaiters.ContainsKey(roundId))
+ {
+ Awaiters.Add(roundId, new List<(TaskCompletionSource<RoundState>, Predicate<RoundState>)>());
+ }
+ var predicateList = Awaiters[roundId];
+ predicateList.Add((tcs, predicate));
+ return tcs.Task;
+ }
+ }
+}

Add RoundStatusUpdater