Implement network message/serialization/client
MODIFY
WalletWasabi.Backend/Controllers/WabiSabiController.cs
WalletWasabi.Backend/Controllers/WabiSabiController.cs
@@ -24,10 +24,10 @@ public class WabiSabiController : ControllerBase, IWabiSabiApiRequestHandler
private IdempotencyRequestCache IdempotencyRequestCache { get; }
private Arena Arena { get; }
- [HttpGet("status")]
- public Task<RoundState[]> GetStatusAsync(CancellationToken cancellationToken)
+ [HttpPost("status")]
+ public Task<RoundState[]> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken)
{
- return Arena.GetStatusAsync(cancellationToken);
+ return Arena.GetStatusAsync(request, cancellationToken);
}
[HttpPost("connection-confirmation")]

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Integration/WabiSabiApiApplicationFactory.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Integration/WabiSabiApiApplicationFactory.cs
@@ -18,6 +18,7 @@ using WalletWasabi.WabiSabi.Backend.Banning;
using WalletWasabi.WabiSabi.Backend.Rounds;
using WalletWasabi.WabiSabi.Backend.Rounds.CoinJoinStorage;
using WalletWasabi.WabiSabi.Client;
+using WalletWasabi.WabiSabi.Models;
using WalletWasabi.WabiSabi.Models.MultipartyTransaction;
namespace WalletWasabi.Tests.UnitTests.WabiSabi.Integration;
@@ -72,7 +73,7 @@ public class WabiSabiApiApplicationFactory<TStartup> : WebApplicationFactory<TSt
public async Task<ArenaClient> CreateArenaClientAsync(WabiSabiHttpApiClient wabiSabiHttpApiClient)
{
- var rounds = await wabiSabiHttpApiClient.GetStatusAsync(CancellationToken.None);
+ var rounds = await wabiSabiHttpApiClient.GetStatusAsync(RoundStateRequest.Empty, CancellationToken.None);
var round = rounds.First(x => x.CoinjoinState is ConstructionState);
var insecureRandom = new InsecureRandom();
var arenaClient = new ArenaClient(

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Integration/WabiSabiHttpApiIntegrationTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Integration/WabiSabiHttpApiIntegrationTests.cs
@@ -41,7 +41,7 @@ public class WabiSabiHttpApiIntegrationTests : IClassFixture<WabiSabiApiApplicat
var httpClient = _apiApplicationFactory.CreateClient();
var apiClient = await _apiApplicationFactory.CreateArenaClientAsync(httpClient);
- var rounds = await apiClient.GetStatusAsync(CancellationToken.None);
+ var rounds = await apiClient.GetStatusAsync(RoundStateRequest.Empty, CancellationToken.None);
var round = rounds.First(x => x.CoinjoinState is ConstructionState);
// If an output is not in the utxo dataset then it is not unspent, this
@@ -430,7 +430,7 @@ public class WabiSabiHttpApiIntegrationTests : IClassFixture<WabiSabiApiApplicat
})).CreateClient();
ArenaClient apiClient = await _apiApplicationFactory.CreateArenaClientAsync(new StuttererHttpClient(httpClient));
- RoundState[] rounds = await apiClient.GetStatusAsync(CancellationToken.None);
+ RoundState[] rounds = await apiClient.GetStatusAsync(RoundStateRequest.Empty, CancellationToken.None);
RoundState round = rounds.First(x => x.CoinjoinState is ConstructionState);
var ownershipProof = WabiSabiFactory.CreateOwnershipProof(signingKey, round.Id);

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Models/RoundStateUpdaterTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Models/RoundStateUpdaterTests.cs
@@ -27,7 +27,7 @@ public class RoundStateUpdaterTests
// The coordinator creates two rounds.
// Each line represents a response for each request.
var mockApiClient = new Mock<IWabiSabiApiRequestHandler>();
- mockApiClient.SetupSequence(apiClient => apiClient.GetStatusAsync(It.IsAny<CancellationToken>()))
+ mockApiClient.SetupSequence(apiClient => apiClient.GetStatusAsync(It.IsAny<RoundStateRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(() => new[] { roundState1 with { Phase = Phase.InputRegistration } })
.ReturnsAsync(() => new[] { roundState1 with { Phase = Phase.OutputRegistration } })
.ReturnsAsync(() => new[] { roundState1 with { Phase = Phase.OutputRegistration }, roundState2 with { Phase = Phase.InputRegistration } })
@@ -104,7 +104,7 @@ public class RoundStateUpdaterTests
// Each line represents a response for each request.
// Exceptions, Problems, Errors everywhere!!!
var mockApiClient = new Mock<IWabiSabiApiRequestHandler>();
- mockApiClient.SetupSequence(apiClient => apiClient.GetStatusAsync(It.IsAny<CancellationToken>()))
+ mockApiClient.SetupSequence(apiClient => apiClient.GetStatusAsync(It.IsAny<RoundStateRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(() => new[] { roundState with { Phase = Phase.InputRegistration } })
.ThrowsAsync(new Exception())
.ThrowsAsync(new OperationCanceledException())

MODIFY
WalletWasabi/WabiSabi/Backend/PostRequests/IWabiSabiApiRequestHandler.cs
WalletWasabi/WabiSabi/Backend/PostRequests/IWabiSabiApiRequestHandler.cs
@@ -18,7 +18,7 @@ public interface IWabiSabiApiRequestHandler
Task<ReissueCredentialResponse> ReissuanceAsync(ReissueCredentialRequest request, CancellationToken cancellationToken);
- Task<RoundState[]> GetStatusAsync(CancellationToken cancellationToken);
+ Task<RoundState[]> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken);
Task ReadyToSignAsync(ReadyToSignRequestRequest request, CancellationToken cancellationToken);
}

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.Partial.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.Partial.cs
using NBitcoin;
using Nito.AsyncEx;
-using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -358,11 +357,14 @@ public partial class Arena : IWabiSabiApiRequestHandler
return new Coin(input, txOutResponse.TxOut);
}
- public async Task<RoundState[]> GetStatusAsync(CancellationToken cancellationToken)
+ public async Task<RoundState[]> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken)
{
using (await AsyncLock.LockAsync(cancellationToken).ConfigureAwait(false))
{
- return Rounds.Select(x => RoundState.FromRound(x)).ToArray();
+ return Rounds.Select(x => {
+ var checkPoint = request.RoundCheckpoints.FirstOrDefault(y => y.RoundId == x.Id);
+ return RoundState.FromRound(x, checkPoint == default ? 0 : checkPoint.StateId);
+ }).ToArray();
}
}

MODIFY
WalletWasabi/WabiSabi/Client/ArenaClient.cs
WalletWasabi/WabiSabi/Client/ArenaClient.cs
@@ -195,9 +195,9 @@ public class ArenaClient
await RequestHandler.SignTransactionAsync(new TransactionSignaturesRequest(roundId, txInput.Index, txInput.WitScript), cancellationToken).ConfigureAwait(false);
}
- public async Task<RoundState[]> GetStatusAsync(CancellationToken cancellationToken)
+ public async Task<RoundState[]> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken)
{
- return await RequestHandler.GetStatusAsync(cancellationToken).ConfigureAwait(false);
+ return await RequestHandler.GetStatusAsync(request, cancellationToken).ConfigureAwait(false);
}
public async Task ReadyToSignAsync(

MODIFY
WalletWasabi/WabiSabi/Client/RoundStateUpdater.cs
WalletWasabi/WabiSabi/Client/RoundStateUpdater.cs
using NBitcoin;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -26,8 +27,20 @@ public class RoundStateUpdater : PeriodicRunner
protected override async Task ActionAsync(CancellationToken cancellationToken)
{
- var statusResponse = await ArenaRequestHandler.GetStatusAsync(cancellationToken).ConfigureAwait(false);
- RoundStates = statusResponse.ToDictionary(round => round.Id);
+ var request = new RoundStateRequest(RoundStates.Select(x => (x.Key, x.Value.CoinjoinState.Events.Count)).ToImmutableList());
+ RoundState[] statusResponse = await ArenaRequestHandler.GetStatusAsync(request, cancellationToken).ConfigureAwait(false);
+
+ var updatedRoundStates = statusResponse
+ .Where(rs => RoundStates.ContainsKey(rs.Id))
+ .Select(rs => (Update: rs, CurrentRoundState: RoundStates[rs.Id]))
+ .Select(x => x.Update with {
+ CoinjoinState = x.Update.CoinjoinState.MergeBack(x.CurrentRoundState.CoinjoinState)
+ }).ToList();
+
+ var newRoundStates = statusResponse
+ .Where(rs => !RoundStates.ContainsKey(rs.Id));
+
+ RoundStates = newRoundStates.Concat(updatedRoundStates).ToDictionary(x => x.Id, x => x);
lock (AwaitersLock)
{

MODIFY
WalletWasabi/WabiSabi/Client/WabiSabiHttpApiClient.cs
WalletWasabi/WabiSabi/Client/WabiSabiHttpApiClient.cs
@@ -55,17 +55,8 @@ public class WabiSabiHttpApiClient : IWabiSabiApiRequestHandler
public virtual Task SignTransactionAsync(TransactionSignaturesRequest request, CancellationToken cancellationToken) =>
SendAndReceiveAsync<TransactionSignaturesRequest>(RemoteAction.SignTransaction, request, cancellationToken);
- public async Task<RoundState[]> GetStatusAsync(CancellationToken cancellationToken)
- {
- using var response = await _client.SendAsync(HttpMethod.Get, GetUriEndPoint(RemoteAction.GetStatus), cancel: cancellationToken).ConfigureAwait(false);
-
- if (!response.IsSuccessStatusCode)
- {
- await response.ThrowRequestExceptionFromContentAsync(cancellationToken).ConfigureAwait(false);
- }
- var jsonString = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
- return Deserialize<RoundState[]>(jsonString);
- }
+ public Task<RoundState[]> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken) =>
+ SendAndReceiveAsync<RoundStateRequest, RoundState[]>(RemoteAction.GetStatus, request, cancellationToken);
public Task ReadyToSignAsync(ReadyToSignRequestRequest request, CancellationToken cancellationToken) =>
SendAndReceiveAsync<ReadyToSignRequestRequest>(RemoteAction.ReadyToSign, request, cancellationToken);

MODIFY
WalletWasabi/WabiSabi/Models/MultipartyTransaction/MultipartyTransactionState.cs
WalletWasabi/WabiSabi/Models/MultipartyTransaction/MultipartyTransactionState.cs
@@ -25,17 +25,23 @@ public abstract record MultipartyTransactionState
[JsonIgnore]
public IEnumerable<TxOut> Outputs => Events.OfType<OutputAdded>().Select(x => x.Output);
+ [JsonIgnore]
public Money Balance => Inputs.Sum(x => x.Amount) - Outputs.Sum(x => x.Value);
+ [JsonIgnore]
public int EstimatedInputsVsize => Inputs.Sum(x => x.TxOut.ScriptPubKey.EstimateInputVsize());
+ [JsonIgnore]
public int OutputsVsize => Outputs.Sum(x => x.ScriptPubKey.EstimateOutputVsize());
+ [JsonIgnore]
public int EstimatedVsize => MultipartyTransactionParameters.SharedOverhead + EstimatedInputsVsize + OutputsVsize;
+ [JsonIgnore]
public int MaxTransactionSize => Parameters.MaxTransactionSize;
// With no coordinator fees we can't ensure that the shared overhead
// of the transaction also pays at the nominal feerate so this will have
// to do for now, but in the future EstimatedVsize should be used
// including the shared overhead
+ [JsonIgnore]
public FeeRate EffectiveFeeRate => new(Balance, EstimatedInputsVsize + OutputsVsize);
public ImmutableList<IEvent> Events { get; init; } = ImmutableList<IEvent>.Empty;

MODIFY
WalletWasabi/WabiSabi/Models/MultipartyTransaction/SigningState.cs
WalletWasabi/WabiSabi/Models/MultipartyTransaction/SigningState.cs
@@ -14,17 +14,6 @@ public record SigningState : MultipartyTransactionState
: base(parameters)
{
Events = events.ToImmutableList();
- SortedInputs = Inputs
- .OrderByDescending(x => x.Amount)
- .ThenBy(x => x.Outpoint.ToBytes(), ByteArrayComparer.Comparer)
- .ToList();
-
- SortedOutputs = Outputs
- .GroupBy(x => x.ScriptPubKey)
- .Select(x => new TxOut(x.Sum(y => y.Value), x.Key))
- .OrderByDescending(x => x.Value)
- .ThenBy(x => x.ScriptPubKey.ToBytes(true), ByteArrayComparer.Comparer)
- .ToList();
}
public ImmutableDictionary<int, WitScript> Witnesses { get; init; } = ImmutableDictionary<int, WitScript>.Empty;
@@ -35,9 +24,18 @@ public record SigningState : MultipartyTransactionState
public IEnumerable<Coin> UnsignedInputs => SortedInputs.Where((_, i) => !IsInputSigned(i));
[JsonIgnore]
- public List<Coin> SortedInputs { get; }
+ public List<Coin> SortedInputs => Inputs
+ .OrderByDescending(x => x.Amount)
+ .ThenBy(x => x.Outpoint.ToBytes(), ByteArrayComparer.Comparer)
+ .ToList();
+
[JsonIgnore]
- public List<TxOut> SortedOutputs { get; }
+ public List<TxOut> SortedOutputs => Outputs
+ .GroupBy(x => x.ScriptPubKey)
+ .Select(x => new TxOut(x.Sum(y => y.Value), x.Key))
+ .OrderByDescending(x => x.Value)
+ .ThenBy(x => x.ScriptPubKey.ToBytes(true), ByteArrayComparer.Comparer)
+ .ToList();
public bool IsInputSigned(int index) => Witnesses.ContainsKey(index);

MODIFY
WalletWasabi/WabiSabi/Models/RoundState.cs
WalletWasabi/WabiSabi/Models/RoundState.cs
@@ -31,7 +31,7 @@ public record RoundState(
public DateTimeOffset InputRegistrationEnd => InputRegistrationStart + InputRegistrationTimeout;
- public static RoundState FromRound(Round round, int order = -1) =>
+ public static RoundState FromRound(Round round, int order = 0) =>
new(
round is BlameRound blameRound ? blameRound.BlameOf.Id : uint256.Zero,
round.AmountCredentialIssuerParameters,

ADD
WalletWasabi/WabiSabi/Models/RoundStateRequest.cs
+using System.Collections.Immutable;
+using NBitcoin;
+
+namespace WalletWasabi.WabiSabi.Models;
+
+public record RoundStateRequest(ImmutableList<(uint256 RoundId, int StateId)> RoundCheckpoints)
+{
+ public static readonly RoundStateRequest Empty = new (ImmutableList<(uint256 RoundId, int StateId)>.Empty);
+}

Implement network message/serialization/client