Add API for CoinJoinFeeRateAvarages
MODIFY
WalletWasabi.Backend/Controllers/WabiSabiController.cs
WalletWasabi.Backend/Controllers/WabiSabiController.cs
@@ -25,7 +25,7 @@ public class WabiSabiController : ControllerBase, IWabiSabiApiRequestHandler
private Arena Arena { get; }
[HttpPost("status")]
- public Task<RoundState[]> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken)
+ public Task<RoundStateResponse> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken)
{
return Arena.GetStatusAsync(request, cancellationToken);
}

MODIFY
WalletWasabi.Tests/Helpers/ArenaBuilder.cs
WalletWasabi.Tests/Helpers/ArenaBuilder.cs
using Moq;
using NBitcoin;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.BitcoinCore.Rpc;
@@ -7,6 +9,7 @@ using WalletWasabi.WabiSabi.Backend;
using WalletWasabi.WabiSabi.Backend.Banning;
using WalletWasabi.WabiSabi.Backend.Rounds;
using WalletWasabi.WabiSabi.Backend.Rounds.CoinJoinStorage;
+using WalletWasabi.WabiSabi.Backend.Statistics;
namespace WalletWasabi.Tests.Helpers;
@@ -23,6 +26,7 @@ public class ArenaBuilder
public IRPCClient? Rpc { get; set; }
public Prison? Prison { get; set; }
public InMemoryCoinJoinIdStore? CoinJoinIdStore { get; set; }
+ public CoinJoinFeeRateStatStore? CoinJoinFeeRateStatStore { get; set; }
/// <param name="rounds">Rounds to initialize <see cref="Arena"/> with.</param>
public Arena Create(params Round[] rounds)
@@ -33,8 +37,9 @@ public class ArenaBuilder
IRPCClient rpc = Rpc ?? WabiSabiFactory.CreatePreconfiguredRpcClient().Object;
Network network = Network ?? Network.Main;
InMemoryCoinJoinIdStore coinJoinIdStore = CoinJoinIdStore ?? new();
+ CoinJoinFeeRateStatStore coinJoinFeeRateStatStore = CoinJoinFeeRateStatStore ?? new(config, rpc, Enumerable.Empty<CoinJoinFeeRateStatRecord>());
- Arena arena = new(period, network, config, rpc, prison, coinJoinIdStore);
+ Arena arena = new(period, network, config, rpc, prison, coinJoinIdStore, coinJoinFeeRateStatStore);
foreach (var round in rounds)
{

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Integration/WabiSabiApiApplicationFactory.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Integration/WabiSabiApiApplicationFactory.cs
@@ -73,7 +73,7 @@ public class WabiSabiApiApplicationFactory<TStartup> : WebApplicationFactory<TSt
public async Task<ArenaClient> CreateArenaClientAsync(WabiSabiHttpApiClient wabiSabiHttpApiClient)
{
- var rounds = await wabiSabiHttpApiClient.GetStatusAsync(RoundStateRequest.Empty, CancellationToken.None);
+ var rounds = (await wabiSabiHttpApiClient.GetStatusAsync(RoundStateRequest.Empty, CancellationToken.None)).RoundStates;
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
@@ -44,7 +44,7 @@ public class WabiSabiHttpApiIntegrationTests : IClassFixture<WabiSabiApiApplicat
var httpClient = _apiApplicationFactory.CreateClient();
var apiClient = await _apiApplicationFactory.CreateArenaClientAsync(httpClient);
- var rounds = await apiClient.GetStatusAsync(RoundStateRequest.Empty, CancellationToken.None);
+ var rounds = (await apiClient.GetStatusAsync(RoundStateRequest.Empty, CancellationToken.None)).RoundStates;
var round = rounds.First(x => x.CoinjoinState is ConstructionState);
// If an output is not in the utxo dataset then it is not unspent, this
@@ -435,7 +435,7 @@ public class WabiSabiHttpApiIntegrationTests : IClassFixture<WabiSabiApiApplicat
})).CreateClient();
ArenaClient apiClient = await _apiApplicationFactory.CreateArenaClientAsync(new StuttererHttpClient(httpClient));
- RoundState[] rounds = await apiClient.GetStatusAsync(RoundStateRequest.Empty, CancellationToken.None);
+ RoundState[] rounds = (await apiClient.GetStatusAsync(RoundStateRequest.Empty, CancellationToken.None)).RoundStates;
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
@@ -28,11 +28,11 @@ public class RoundStateUpdaterTests
// Each line represents a response for each request.
var mockApiClient = new Mock<IWabiSabiApiRequestHandler>();
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 } })
- .ReturnsAsync(() => new[] { roundState2 with { Phase = Phase.OutputRegistration } })
- .ReturnsAsync(() => Array.Empty<RoundState>());
+ .ReturnsAsync(() => new RoundStateResponse(new[] { roundState1 with { Phase = Phase.InputRegistration } }, Array.Empty<CoinJoinFeeRateAvarage>()))
+ .ReturnsAsync(() => new RoundStateResponse(new[] { roundState1 with { Phase = Phase.OutputRegistration } }, Array.Empty<CoinJoinFeeRateAvarage>()))
+ .ReturnsAsync(() => new RoundStateResponse(new[] { roundState1 with { Phase = Phase.OutputRegistration }, roundState2 with { Phase = Phase.InputRegistration } }, Array.Empty<CoinJoinFeeRateAvarage>()))
+ .ReturnsAsync(() => new RoundStateResponse(new[] { roundState2 with { Phase = Phase.OutputRegistration } }, Array.Empty<CoinJoinFeeRateAvarage>()))
+ .ReturnsAsync(() => new RoundStateResponse(Array.Empty<RoundState>(), Array.Empty<CoinJoinFeeRateAvarage>()));
using RoundStateUpdater roundStatusUpdater = new(TimeSpan.FromDays(1), mockApiClient.Object);
@@ -104,13 +104,13 @@ public class RoundStateUpdaterTests
// Exceptions, Problems, Errors everywhere!!!
var mockApiClient = new Mock<IWabiSabiApiRequestHandler>();
mockApiClient.SetupSequence(apiClient => apiClient.GetStatusAsync(It.IsAny<RoundStateRequest>(), It.IsAny<CancellationToken>()))
- .ReturnsAsync(() => new[] { roundState with { Phase = Phase.InputRegistration } })
+ .ReturnsAsync(() => new RoundStateResponse(new[] { roundState with { Phase = Phase.InputRegistration } }, Array.Empty<CoinJoinFeeRateAvarage>()))
.ThrowsAsync(new Exception())
.ThrowsAsync(new OperationCanceledException())
.ThrowsAsync(new InvalidOperationException())
.ThrowsAsync(new HttpRequestException())
- .ReturnsAsync(() => new[] { roundState with { Phase = Phase.OutputRegistration } })
- .ReturnsAsync(() => Array.Empty<RoundState>());
+ .ReturnsAsync(() => new RoundStateResponse(new[] { roundState with { Phase = Phase.OutputRegistration } }, Array.Empty<CoinJoinFeeRateAvarage>()))
+ .ReturnsAsync(() => new RoundStateResponse(Array.Empty<RoundState>(), Array.Empty<CoinJoinFeeRateAvarage>()));
using RoundStateUpdater roundStatusUpdater = new(TimeSpan.FromMilliseconds(100), mockApiClient.Object);
@@ -152,13 +152,13 @@ public class RoundStateUpdaterTests
// Exceptions, Problems, Errors everywhere!!!
var mockApiClient = new Mock<IWabiSabiApiRequestHandler>();
mockApiClient.SetupSequence(apiClient => apiClient.GetStatusAsync(It.IsAny<RoundStateRequest>(), It.IsAny<CancellationToken>()))
- .ReturnsAsync(() => new[] { roundState with { Phase = Phase.InputRegistration } })
+ .ReturnsAsync(() => new RoundStateResponse(new[] { roundState with { Phase = Phase.InputRegistration } }, Array.Empty<CoinJoinFeeRateAvarage>()))
.ThrowsAsync(new Exception())
.ThrowsAsync(new OperationCanceledException())
.ThrowsAsync(new InvalidOperationException())
.ThrowsAsync(new HttpRequestException())
- .ReturnsAsync(() => new[] { roundState with { Phase = Phase.Ended } })
- .ReturnsAsync(() => Array.Empty<RoundState>());
+ .ReturnsAsync(() => new RoundStateResponse(new[] { roundState with { Phase = Phase.Ended } }, Array.Empty<CoinJoinFeeRateAvarage>()))
+ .ReturnsAsync(() => new RoundStateResponse(Array.Empty<RoundState>(), Array.Empty<CoinJoinFeeRateAvarage>()));
using RoundStateUpdater roundStatusUpdater = new(TimeSpan.FromMilliseconds(100), mockApiClient.Object);

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(RoundStateRequest request, CancellationToken cancellationToken);
+ Task<RoundStateResponse> 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
@@ -357,14 +357,17 @@ public partial class Arena : IWabiSabiApiRequestHandler
return new Coin(input, txOutResponse.TxOut);
}
- public async Task<RoundState[]> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken)
+ public async Task<RoundStateResponse> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken)
{
using (await AsyncLock.LockAsync(cancellationToken).ConfigureAwait(false))
{
- return Rounds.Select(x => {
+ var roundStates = Rounds.Select(x =>
+ {
var checkPoint = request.RoundCheckpoints.FirstOrDefault(y => y.RoundId == x.Id);
return RoundState.FromRound(x, checkPoint == default ? 0 : checkPoint.StateId);
}).ToArray();
+
+ return new RoundStateResponse(roundStates, CoinJoinFeeRateStatStore.GetDefaultAvarages());
}
}

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
@@ -13,6 +13,7 @@ using WalletWasabi.WabiSabi.Backend.Banning;
using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Models.MultipartyTransaction;
using WalletWasabi.WabiSabi.Backend.Rounds.CoinJoinStorage;
+using WalletWasabi.WabiSabi.Backend.Statistics;
namespace WalletWasabi.WabiSabi.Backend.Rounds;
@@ -25,6 +26,7 @@ public partial class Arena : PeriodicRunner
IRPCClient rpc,
Prison prison,
InMemoryCoinJoinIdStore inMemoryCoinJoinIdStore,
+ CoinJoinFeeRateStatStore coinJoinFeeRateStatStore,
CoinJoinTransactionArchiver? archiver = null) : base(period)
{
Network = network;
@@ -34,6 +36,7 @@ public partial class Arena : PeriodicRunner
TransactionArchiver = archiver;
Random = new SecureRandom();
InMemoryCoinJoinIdStore = inMemoryCoinJoinIdStore;
+ CoinJoinFeeRateStatStore = coinJoinFeeRateStatStore;
}
public HashSet<Round> Rounds { get; } = new();
@@ -45,6 +48,7 @@ public partial class Arena : PeriodicRunner
private SecureRandom Random { get; }
private CoinJoinTransactionArchiver? TransactionArchiver { get; }
private InMemoryCoinJoinIdStore InMemoryCoinJoinIdStore { get; }
+ private CoinJoinFeeRateStatStore CoinJoinFeeRateStatStore { get; }
public event EventHandler<Transaction>? CoinJoinBroadcast;

MODIFY
WalletWasabi/WabiSabi/Backend/Statistics/CoinJoinFeeRateStatStore.cs
WalletWasabi/WabiSabi/Backend/Statistics/CoinJoinFeeRateStatStore.cs
@@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Bases;
using WalletWasabi.BitcoinCore.Rpc;
+using WalletWasabi.WabiSabi.Models;
namespace WalletWasabi.WabiSabi.Backend.Statistics;
@@ -15,6 +16,8 @@ public class CoinJoinFeeRateStatStore : PeriodicRunner
private const int MaximumDaysToStore = 30;
private List<CoinJoinFeeRateStatRecord> CoinJoinFeeRateStatRecords { get; }
+ private CoinJoinFeeRateAvarage[] DefaultAvarages { get; set; } = Array.Empty<CoinJoinFeeRateAvarage>();
+
public CoinJoinFeeRateStatStore(WabiSabiConfig config, IRPCClient rpc, IEnumerable<CoinJoinFeeRateStatRecord> feeRateStatRecords) :
base(TimeSpan.FromMinutes(10))
{
@@ -41,6 +44,15 @@ public class CoinJoinFeeRateStatStore : PeriodicRunner
{
CoinJoinFeeRateStatRecords.Add(feeRateStatRecord);
+ var timeFrames = new[]
+{
+ TimeSpan.FromDays(1),
+ TimeSpan.FromDays(7),
+ TimeSpan.FromDays(30),
+ };
+
+ DefaultAvarages = timeFrames.Select(t => new CoinJoinFeeRateAvarage((int)Math.Floor(t.TotalHours), GetAvarage(t))).ToArray();
+
// Prune old items.
DateTimeOffset removeBefore = DateTimeOffset.UtcNow - TimeSpan.FromDays(MaximumDaysToStore);
while (CoinJoinFeeRateStatRecords.Any() && CoinJoinFeeRateStatRecords[0].DateTimeOffset < removeBefore)
@@ -55,6 +67,11 @@ public class CoinJoinFeeRateStatStore : PeriodicRunner
return new FeeRate(CoinJoinFeeRateStatRecords.Where(x => x.DateTimeOffset >= from).Average(x => x.FeeRate.SatoshiPerByte));
}
+ public CoinJoinFeeRateAvarage[] GetDefaultAvarages()
+ {
+ return DefaultAvarages;
+ }
+
public static CoinJoinFeeRateStatStore LoadFromFile(string filePath, WabiSabiConfig config, IRPCClient rpc)
{
var now = DateTimeOffset.UtcNow;

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

MODIFY
WalletWasabi/WabiSabi/Client/RoundStateUpdater.cs
WalletWasabi/WabiSabi/Client/RoundStateUpdater.cs
@@ -32,12 +32,14 @@ public class RoundStateUpdater : PeriodicRunner
var request = new RoundStateRequest(
RoundStates.Select(x => new RoundStateCheckpoint(x.Key, x.Value.CoinjoinState.Events.Count)).ToImmutableList());
- RoundState[] statusResponse = await ArenaRequestHandler.GetStatusAsync(request, cancellationToken).ConfigureAwait(false);
+ var response = await ArenaRequestHandler.GetStatusAsync(request, cancellationToken).ConfigureAwait(false);
+ RoundState[] statusResponse = response.RoundStates;
var updatedRoundStates = statusResponse
.Where(rs => RoundStates.ContainsKey(rs.Id))
.Select(rs => (NewRoundState: rs, CurrentRoundState: RoundStates[rs.Id]))
- .Select(x => x.NewRoundState with {
+ .Select(x => x.NewRoundState with
+ {
CoinjoinState = x.NewRoundState.CoinjoinState.AddPreviousStates(x.CurrentRoundState.CoinjoinState)
}).ToList();

MODIFY
WalletWasabi/WabiSabi/Client/WabiSabiHttpApiClient.cs
WalletWasabi/WabiSabi/Client/WabiSabiHttpApiClient.cs
@@ -55,8 +55,8 @@ public class WabiSabiHttpApiClient : IWabiSabiApiRequestHandler
public virtual Task SignTransactionAsync(TransactionSignaturesRequest request, CancellationToken cancellationToken) =>
SendAndReceiveAsync<TransactionSignaturesRequest>(RemoteAction.SignTransaction, request, cancellationToken);
- public Task<RoundState[]> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken) =>
- SendAndReceiveAsync<RoundStateRequest, RoundState[]>(RemoteAction.GetStatus, request, cancellationToken);
+ public Task<RoundStateResponse> GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken) =>
+ SendAndReceiveAsync<RoundStateRequest, RoundStateResponse>(RemoteAction.GetStatus, request, cancellationToken);
public Task ReadyToSignAsync(ReadyToSignRequestRequest request, CancellationToken cancellationToken) =>
SendAndReceiveAsync<ReadyToSignRequestRequest>(RemoteAction.ReadyToSign, request, cancellationToken);

ADD
WalletWasabi/WabiSabi/Models/CoinJoinFeeRateAvarage.cs
+using NBitcoin;
+
+namespace WalletWasabi.WabiSabi.Models;
+
+public record CoinJoinFeeRateAvarage(int TimeFrameHours, FeeRate AvarageFeeRate);

ADD
WalletWasabi/WabiSabi/Models/RoundStateResponse.cs
+namespace WalletWasabi.WabiSabi.Models;
+
+public record RoundStateResponse(RoundState[] RoundStates, CoinJoinFeeRateAvarage[] CoinJoinFeeRateAvarages);

MODIFY
WalletWasabi/WabiSabi/WabiSabiCoordinator.cs
WalletWasabi/WabiSabi/WabiSabiCoordinator.cs
@@ -26,13 +26,13 @@ public class WabiSabiCoordinator : BackgroundService
CoinJoinTransactionArchiver transactionArchiver = new(Path.Combine(parameters.CoordinatorDataDir, "CoinJoinTransactions"));
+ CoinJoinFeeRateStatStore = CoinJoinFeeRateStatStore.LoadFromFile(parameters.CoinJoinFeeRateStatStoreFilePath, Config, rpc);
+ CoinJoinFeeRateStatStore.NewStat += FeeRateStatStore_NewStat;
+
var inMemoryCoinJoinIdStore = InMemoryCoinJoinIdStore.LoadFromFile(parameters.CoinJoinIdStoreFilePath);
- Arena = new(parameters.RoundProgressSteppingPeriod, rpc.Network, Config, rpc, Warden.Prison, inMemoryCoinJoinIdStore, transactionArchiver);
+ Arena = new(parameters.RoundProgressSteppingPeriod, rpc.Network, Config, rpc, Warden.Prison, inMemoryCoinJoinIdStore, CoinJoinFeeRateStatStore, transactionArchiver);
Arena.CoinJoinBroadcast += Arena_CoinJoinBroadcast;
-
- CoinJoinFeeRateStatStore = CoinJoinFeeRateStatStore.LoadFromFile(parameters.CoinJoinFeeRateStatStoreFilePath, Config, rpc);
- CoinJoinFeeRateStatStore.NewStat += FeeRateStatStore_NewStat;
}
public ConfigWatcher ConfigWatcher { get; }

Add API for CoinJoinFeeRateAvarages