Add FeeRateStatStore
ADD
WalletWasabi/WabiSabi/Backend/Statistics/FeeRateStatRecord.cs
+using NBitcoin;
+
+namespace WalletWasabi.WabiSabi.Backend.Statistics;
+
+public record FeeRateStatRecord(
+ DateTimeOffset DateTimeOffset,
+ uint ConfirmationTarget,
+ FeeRate FeeRate)
+{
+ public string ToLine()
+ {
+ return $"{DateTimeOffset.ToUnixTimeSeconds()},{ConfirmationTarget},{(uint)Math.Ceiling(FeeRate.SatoshiPerByte)}";
+ }
+
+ public static FeeRateStatRecord FromLine(string line)
+ {
+ var parts = line.Split(",");
+ var date = DateTimeOffset.FromUnixTimeSeconds(long.Parse(parts[0]));
+ var confirmationTarget = uint.Parse(parts[1]);
+ var feeRate = new FeeRate((decimal)uint.Parse(parts[2]));
+ return new(date, confirmationTarget, feeRate);
+ }
+}

ADD
WalletWasabi/WabiSabi/Backend/Statistics/FeeRateStatStore.cs
+using NBitcoin;
+using NBitcoin.RPC;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Bases;
+using WalletWasabi.BitcoinCore.Rpc;
+
+namespace WalletWasabi.WabiSabi.Backend.Statistics;
+
+public class FeeRateStatStore : PeriodicRunner
+{
+ private const int MaximumDaysToStore = 30;
+ private List<FeeRateStatRecord> FeeRateStatRecords { get; }
+
+ public FeeRateStatStore(WabiSabiConfig config, IRPCClient rpc, IEnumerable<FeeRateStatRecord> feeRateStatRecords) :
+ base(TimeSpan.FromMinutes(10))
+ {
+ Config = config;
+ Rpc = rpc;
+ FeeRateStatRecords = new(feeRateStatRecords.OrderBy(x => x.DateTimeOffset));
+ }
+
+ public WabiSabiConfig Config { get; }
+ public IRPCClient Rpc { get; }
+
+ public event EventHandler<FeeRateStatRecord>? NewStat;
+
+ protected override async Task ActionAsync(CancellationToken cancel)
+ {
+ var feeRate = (await Rpc.EstimateSmartFeeAsync((int)Config.ConfirmationTarget, EstimateSmartFeeMode.Conservative, simulateIfRegTest: true, cancel).ConfigureAwait(false)).FeeRate;
+
+ FeeRateStatRecord record = new(DateTimeOffset.UtcNow, Config.ConfirmationTarget, feeRate);
+ Add(record);
+ NewStat?.Invoke(this, record);
+ }
+
+ public void Add(FeeRateStatRecord feeRateStatRecord)
+ {
+ DateTimeOffset now = DateTimeOffset.UtcNow;
+
+ FeeRateStatRecords.Add(feeRateStatRecord);
+
+ // Prune old items.
+ DateTimeOffset removeBefore = now - TimeSpan.FromDays(MaximumDaysToStore);
+ while (FeeRateStatRecords.Any() && FeeRateStatRecords[0].DateTimeOffset < removeBefore)
+ {
+ FeeRateStatRecords.RemoveAt(0);
+ }
+ }
+
+ public FeeRate GetAvarage(TimeSpan timeFrame)
+ {
+ var from = DateTimeOffset.UtcNow - timeFrame;
+ return new FeeRate(FeeRateStatRecords.Where(x => x.DateTimeOffset >= from).Average(x => x.FeeRate.SatoshiPerByte));
+ }
+
+ public static FeeRateStatStore LoadFromFile(string filePath, WabiSabiConfig config, IRPCClient rpc)
+ {
+ var now = DateTimeOffset.UtcNow;
+ var from = now - TimeSpan.FromDays(MaximumDaysToStore);
+
+ var records = !File.Exists(filePath) ?
+ Enumerable.Empty<FeeRateStatRecord>() :
+ File.ReadAllLines(filePath)
+ .Select(x => FeeRateStatRecord.FromLine(x))
+ .Where(x => x.DateTimeOffset > from);
+
+ var store = new FeeRateStatStore(config, rpc, records);
+
+ return store;
+ }
+}

MODIFY
WalletWasabi/WabiSabi/CoordinatorParameters.cs
WalletWasabi/WabiSabi/CoordinatorParameters.cs
@@ -35,6 +35,11 @@ public class CoordinatorParameters
/// </summary>
public string CoinJoinIdStoreFilePath => Path.Combine(CoordinatorDataDir, "CoinJoinIdStore.txt");
+ /// <summary>
+ /// File that is storing the feeRate statistics.
+ /// </summary>
+ public string FeeRateStatStoreFilePath => Path.Combine(CoordinatorDataDir, "FeeRateStatStore.txt");
+
/// <summary>
/// Runtime adjustable configuration of the coordinator.
/// </summary>

MODIFY
WalletWasabi/WabiSabi/WabiSabiCoordinator.cs
WalletWasabi/WabiSabi/WabiSabiCoordinator.cs
@@ -11,6 +11,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.WabiSabi;
@@ -29,6 +30,9 @@ public class WabiSabiCoordinator : BackgroundService
Arena = new(parameters.RoundProgressSteppingPeriod, rpc.Network, Config, rpc, Warden.Prison, inMemoryCoinJoinIdStore, transactionArchiver);
Arena.CoinJoinBroadcast += Arena_CoinJoinBroadcast;
+
+ FeeRateStatStore = FeeRateStatStore.LoadFromFile(parameters.FeeRateStatStoreFilePath, Config, rpc);
+ FeeRateStatStore.NewStat += FeeRateStatStore_NewStat;
}
public ConfigWatcher ConfigWatcher { get; }
@@ -37,6 +41,8 @@ public class WabiSabiCoordinator : BackgroundService
public CoordinatorParameters Parameters { get; }
public Arena Arena { get; }
+ public FeeRateStatStore FeeRateStatStore { get; }
+
public WabiSabiConfig Config => Parameters.RuntimeCoordinatorConfig;
private void Arena_CoinJoinBroadcast(object? sender, Transaction e)
@@ -49,11 +55,23 @@ public class WabiSabiCoordinator : BackgroundService
File.AppendAllLines(Parameters.CoinJoinIdStoreFilePath, new[] { e.GetHash().ToString() });
}
+ private void FeeRateStatStore_NewStat(object? sender, FeeRateStatRecord record)
+ {
+ if (!File.Exists(Parameters.FeeRateStatStoreFilePath))
+ {
+ IoHelpers.EnsureContainingDirectoryExists(Parameters.FeeRateStatStoreFilePath);
+ }
+
+ File.AppendAllLines(Parameters.FeeRateStatStoreFilePath, new[] { record.ToLine() });
+ }
+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await ConfigWatcher.StartAsync(stoppingToken).ConfigureAwait(false);
await Warden.StartAsync(stoppingToken).ConfigureAwait(false);
await Arena.StartAsync(stoppingToken).ConfigureAwait(false);
+
+ await FeeRateStatStore.StartAsync(stoppingToken).ConfigureAwait(false);
}
public override async Task StopAsync(CancellationToken cancellationToken)
@@ -63,10 +81,13 @@ public class WabiSabiCoordinator : BackgroundService
await Arena.StopAsync(cancellationToken).ConfigureAwait(false);
await ConfigWatcher.StopAsync(cancellationToken).ConfigureAwait(false);
await Warden.StopAsync(cancellationToken).ConfigureAwait(false);
+
+ await FeeRateStatStore.StopAsync(cancellationToken).ConfigureAwait(false);
}
public override void Dispose()
{
+ FeeRateStatStore.NewStat -= FeeRateStatStore_NewStat;
Arena.CoinJoinBroadcast -= Arena_CoinJoinBroadcast;
ConfigWatcher.Dispose();
Warden.Dispose();

Add FeeRateStatStore