Skip to content

Commit

Permalink
Simplify background service construction (WalletWasabi#13241)
Browse files Browse the repository at this point in the history
* Simplify background service construction

The idea is to make it easier for the IoC to build those background services.

* Remove `UserAgent` for P2pNode
  • Loading branch information
lontivero authored Jul 25, 2024
1 parent 3e447df commit cb570da
Show file tree
Hide file tree
Showing 21 changed files with 53 additions and 66 deletions.
8 changes: 4 additions & 4 deletions WalletWasabi.Backend/Global.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ public Global(string dataDir, IRPCClient rpcClient, Config config)
}

// We have to find it, because it's cloned by the node and not perfectly cloned (event handlers cannot be cloned.)
P2pNode = new(config.Network, config.GetBitcoinP2pEndPoint(), new(), $"/WasabiCoordinator:{Constants.BackendMajorVersion}/");
HostedServices.Register<BlockNotifier>(() => new BlockNotifier(TimeSpan.FromSeconds(7), rpcClient, P2pNode), "Block Notifier");
P2pNode = new(config.Network, config.GetBitcoinP2pEndPoint(), new());
HostedServices.Register<BlockNotifier>(() => new BlockNotifier(rpcClient, P2pNode), "Block Notifier");

// Initialize index building
var indexBuilderServiceDir = Path.Combine(DataDir, "IndexBuilderService");
var indexFilePath = Path.Combine(indexBuilderServiceDir, $"Index{RpcClient.Network}.dat");
IndexBuilderService = new(RpcClient, HostedServices.Get<BlockNotifier>(), indexFilePath);
IndexBuilderService = new(RpcClient, HostedServices.Get<BlockNotifier>());

MempoolMirror = new MempoolMirror(TimeSpan.FromSeconds(21), RpcClient, P2pNode);
MempoolMirror = new MempoolMirror(RpcClient, P2pNode);
}

public string DataDir { get; }
Expand Down
4 changes: 2 additions & 2 deletions WalletWasabi.Daemon/Global.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,13 +377,13 @@ private async Task StartLocalBitcoinNodeAsync(CancellationToken cancel)

private void RegisterLocalNodeDependentComponents(CoreNode coreNode)
{
HostedServices.Register<BlockNotifier>(() => new BlockNotifier(TimeSpan.FromSeconds(7), coreNode.RpcClient, coreNode.P2pNode), "Block Notifier");
HostedServices.Register<BlockNotifier>(() => new BlockNotifier(coreNode.RpcClient, coreNode.P2pNode), "Block Notifier");
HostedServices.Register<RpcMonitor>(() => new RpcMonitor(TimeSpan.FromSeconds(7), coreNode.RpcClient), "RPC Monitor");
HostedServices.Register<RpcFeeProvider>(() => new RpcFeeProvider(TimeSpan.FromMinutes(1), coreNode.RpcClient, HostedServices.Get<RpcMonitor>()), "RPC Fee Provider");
if (!Config.BlockOnlyMode)
{
HostedServices.Register<MempoolMirror>(
() => new MempoolMirror(TimeSpan.FromSeconds(21), coreNode.RpcClient, coreNode.P2pNode),
() => new MempoolMirror(coreNode.RpcClient, coreNode.P2pNode),
"Full Node Mempool Mirror");
}
}
Expand Down
2 changes: 1 addition & 1 deletion WalletWasabi.Tests/Helpers/ArenaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public Arena Create(params Round[] rounds)
Network network = Network ?? Network.Main;
RoundParameterFactory roundParameterFactory = RoundParameterFactory ?? CreateRoundParameterFactory(config, network);

Arena arena = new(period, config, rpc, prison, roundParameterFactory);
Arena arena = new(config, rpc, prison, roundParameterFactory, period:period);

foreach (var round in rounds)
{
Expand Down
2 changes: 1 addition & 1 deletion WalletWasabi.Tests/RegressionTests/BackendTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ public async Task FilterBuilderTestAsync()
var indexBuilderServiceDir = Helpers.Common.GetWorkDir();
var indexFilePath = Path.Combine(indexBuilderServiceDir, $"Index{rpc.Network}.dat");

IndexBuilderService indexBuilderService = new(rpc, global.HostedServices.Get<BlockNotifier>(), indexFilePath);
IndexBuilderService indexBuilderService = new(rpc, global.HostedServices.Get<BlockNotifier>());
try
{
indexBuilderService.Synchronize();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public async Task SegwitTaprootUnsynchronizedBitcoinNodeAsync()
InitialBlockDownload = false
}),
};
using var blockNotifier = new BlockNotifier(TimeSpan.MaxValue, rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier, "filters.txt");
using var blockNotifier = new BlockNotifier(rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier);

indexer.Synchronize();

Expand All @@ -54,8 +54,8 @@ public async Task SegwitTaprootStalledBitcoinNodeAsync()
});
}
};
using var blockNotifier = new BlockNotifier(TimeSpan.MaxValue, rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier, "filters.txt");
using var blockNotifier = new BlockNotifier(rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier);

indexer.Synchronize();

Expand Down Expand Up @@ -85,8 +85,8 @@ public async Task SegwitTaprootSynchronizingBitcoinNodeAsync()
OnGetBlockHashAsync = (height) => Task.FromResult(blockchain[height].Hash),
OnGetVerboseBlockAsync = (hash) => Task.FromResult(blockchain.Single(x => x.Hash == hash))
};
using var blockNotifier = new BlockNotifier(TimeSpan.MaxValue, rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier, "filters.txt");
using var blockNotifier = new BlockNotifier(rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier);

indexer.Synchronize();

Expand Down Expand Up @@ -155,8 +155,8 @@ public async Task SegwitTaprootSynchronizedBitcoinNodeAsync()
OnGetBlockHashAsync = (height) => Task.FromResult(blockchain[height].Hash),
OnGetVerboseBlockAsync = (hash) => Task.FromResult(blockchain.Single(x => x.Hash == hash))
};
using var blockNotifier = new BlockNotifier(TimeSpan.MaxValue, rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier, "filters.txt");
using var blockNotifier = new BlockNotifier(rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier);

indexer.Synchronize();

Expand All @@ -181,8 +181,8 @@ public async Task TaprootUnsynchronizedBitcoinNodeAsync()
InitialBlockDownload = false
}),
};
using var blockNotifier = new BlockNotifier(TimeSpan.MaxValue, rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier, "filters.txt");
using var blockNotifier = new BlockNotifier(rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier);

indexer.Synchronize();

Expand All @@ -208,8 +208,8 @@ public async Task TaprootStalledBitcoinNodeAsync()
});
}
};
using var blockNotifier = new BlockNotifier(TimeSpan.MaxValue, rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier, "filters.txt");
using var blockNotifier = new BlockNotifier(rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier);

indexer.Synchronize();

Expand Down Expand Up @@ -239,8 +239,8 @@ public async Task TaprootSynchronizingBitcoinNodeAsync()
OnGetBlockHashAsync = (height) => Task.FromResult(blockchain[height].Hash),
OnGetVerboseBlockAsync = (hash) => Task.FromResult(blockchain.Single(x => x.Hash == hash))
};
using var blockNotifier = new BlockNotifier(TimeSpan.MaxValue, rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier, "filters.txt");
using var blockNotifier = new BlockNotifier(rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier);

indexer.Synchronize();

Expand All @@ -267,8 +267,8 @@ public async Task TaprootSynchronizedBitcoinNodeAsync()
OnGetBlockHashAsync = (height) => Task.FromResult(blockchain[height].Hash),
OnGetVerboseBlockAsync = (hash) => Task.FromResult(blockchain.Single(x => x.Hash == hash))
};
using var blockNotifier = new BlockNotifier(TimeSpan.MaxValue, rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier, "filters.txt");
using var blockNotifier = new BlockNotifier(rpc);
var indexer = new IndexBuilderService(rpc, blockNotifier);

indexer.Synchronize();

Expand Down
10 changes: 5 additions & 5 deletions WalletWasabi.Tests/UnitTests/BitcoinCore/MempoolMirrorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public async Task CanCopyMempoolFromRpcAsync()
{
var coreNode = await TestNodeBuilder.CreateAsync();
using HostedServices services = new();
services.Register<MempoolMirror>(() => new MempoolMirror(TimeSpan.FromSeconds(2), coreNode.RpcClient, coreNode.P2pNode), "Mempool Mirror");
services.Register<MempoolMirror>(() => new MempoolMirror(coreNode.RpcClient, coreNode.P2pNode, TimeSpan.FromSeconds(2)), "Mempool Mirror");
try
{
var rpc = coreNode.RpcClient;
Expand Down Expand Up @@ -59,7 +59,7 @@ public async Task CanHandleArrivingTxAsync()
{
var coreNode = await TestNodeBuilder.CreateAsync();
using HostedServices services = new();
services.Register<MempoolMirror>(() => new MempoolMirror(TimeSpan.FromSeconds(7), coreNode.RpcClient, coreNode.P2pNode), "Mempool Mirror");
services.Register<MempoolMirror>(() => new MempoolMirror(coreNode.RpcClient, coreNode.P2pNode, TimeSpan.FromSeconds(7)), "Mempool Mirror");
try
{
var rpc = coreNode.RpcClient;
Expand Down Expand Up @@ -97,7 +97,7 @@ public async Task CanHandleTheSameTxSentManyTimesAsync()
{
var coreNode = await TestNodeBuilder.CreateAsync();
using HostedServices services = new();
services.Register<MempoolMirror>(() => new MempoolMirror(TimeSpan.FromSeconds(2), coreNode.RpcClient, coreNode.P2pNode), "Mempool Mirror");
services.Register<MempoolMirror>(() => new MempoolMirror(coreNode.RpcClient, coreNode.P2pNode, TimeSpan.FromSeconds(2)), "Mempool Mirror");
try
{
var rpc = coreNode.RpcClient;
Expand Down Expand Up @@ -150,7 +150,7 @@ public async Task CanHandleManyTxsAsync()
{
var coreNode = await TestNodeBuilder.CreateAsync();
using HostedServices services = new();
services.Register<MempoolMirror>(() => new MempoolMirror(TimeSpan.FromSeconds(2), coreNode.RpcClient, coreNode.P2pNode), "Mempool Mirror");
services.Register<MempoolMirror>(() => new MempoolMirror(coreNode.RpcClient, coreNode.P2pNode, TimeSpan.FromSeconds(2)), "Mempool Mirror");
try
{
var rpc = coreNode.RpcClient;
Expand Down Expand Up @@ -196,7 +196,7 @@ public async Task CanHandleConfirmationAsync()
{
var coreNode = await TestNodeBuilder.CreateAsync();
using HostedServices services = new();
services.Register<MempoolMirror>(() => new MempoolMirror(TimeSpan.FromSeconds(2), coreNode.RpcClient, coreNode.P2pNode), "Mempool Mirror");
services.Register<MempoolMirror>(() => new MempoolMirror(coreNode.RpcClient, coreNode.P2pNode, TimeSpan.FromSeconds(2)), "Mempool Mirror");
try
{
var rpc = coreNode.RpcClient;
Expand Down
2 changes: 1 addition & 1 deletion WalletWasabi.Tests/UnitTests/BitcoinCore/P2pBasedTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public async Task BlockNotifierTestsAsync()
{
var coreNode = await TestNodeBuilder.CreateAsync();
using HostedServices services = new();
services.Register<BlockNotifier>(() => new BlockNotifier(TimeSpan.FromSeconds(7), coreNode.RpcClient, coreNode.P2pNode), "Block Notifier");
services.Register<BlockNotifier>(() => new BlockNotifier(coreNode.RpcClient, coreNode.P2pNode,TimeSpan.FromSeconds(7) ), "Block Notifier");

await services.StartAllAsync();
try
Expand Down
2 changes: 1 addition & 1 deletion WalletWasabi.Tests/UnitTests/BlockNotifierTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ private BlockNotifier CreateNotifier(ConcurrentChain chain)

rpc.OnGetBlockHeaderAsync = (blockHash) => Task.FromResult(chain.GetBlock(blockHash).Header);

var notifier = new BlockNotifier(TimeSpan.FromMilliseconds(100), rpc);
var notifier = new BlockNotifier(rpc, period: TimeSpan.FromMilliseconds(100));
return notifier;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using WalletWasabi.Helpers;
using WalletWasabi.Tests.Helpers;
using WalletWasabi.WabiSabi;
using WalletWasabi.WabiSabi.Backend;
using WalletWasabi.WabiSabi.Backend.DoSPrevention;
using Xunit;

Expand All @@ -16,10 +17,7 @@ public async Task CanStartAndStopAsync()
{
var workDir = Common.GetWorkDir();
await IoHelpers.TryDeleteDirectoryAsync(workDir);
CoordinatorParameters coordinatorParameters = new(workDir);
using var w = new Warden(
coordinatorParameters.PrisonFilePath,
coordinatorParameters.RuntimeCoordinatorConfig);
using var w = new Warden(new WabiSabiConfig());
await w.StartAsync(CancellationToken.None);
await w.StopAsync(CancellationToken.None);
}
Expand All @@ -31,10 +29,7 @@ public async Task PrisonSerializationAsync()
await IoHelpers.TryDeleteDirectoryAsync(workDir);

// Create prison.
CoordinatorParameters coordinatorParameters = new(workDir);
using var w = new Warden(
coordinatorParameters.PrisonFilePath,
coordinatorParameters.RuntimeCoordinatorConfig);
using var w = new Warden(new WabiSabiConfig());
await w.StartAsync(CancellationToken.None);
var now = DateTimeOffset.FromUnixTimeSeconds(DateTimeOffset.UtcNow.ToUnixTimeSeconds());
var i1 = BitcoinFactory.CreateOutPoint();
Expand All @@ -52,11 +47,11 @@ public async Task PrisonSerializationAsync()
await w.StopAsync(CancellationToken.None);

// See if prev UTXOs are loaded.
CoordinatorParameters coordinatorParameters2 = new(workDir);
using var w2 = new Warden(coordinatorParameters2.PrisonFilePath, coordinatorParameters2.RuntimeCoordinatorConfig);
var cfg = new WabiSabiConfig();
using var w2 = new Warden(cfg);
await w2.StartAsync(CancellationToken.None);

var dosConfig = coordinatorParameters2.RuntimeCoordinatorConfig.GetDoSConfiguration();
var dosConfig = cfg.GetDoSConfiguration();
Assert.True(w2.Prison.IsBanned(i1, dosConfig, DateTimeOffset.UtcNow));
Assert.True(w2.Prison.IsBanned(i2, dosConfig, DateTimeOffset.UtcNow));
Assert.True(w2.Prison.IsBanned(i3, dosConfig, DateTimeOffset.UtcNow));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Client;

public class ArenaClientTests
{
public MempoolMirror DummyMempoolMirror { get; } = new(TimeSpan.Zero, null!, null!);

[Fact]
public async Task FullP2wpkhCoinjoinTestAsync()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public async Task RegisterOutputTestAsync()
var idempotencyRequestCache = new IdempotencyRequestCache(memoryCache);

using CoinJoinFeeRateStatStore coinJoinFeeRateStatStore = new(config, arena.Rpc);
using var mempoolMirror = new MempoolMirror(TimeSpan.Zero, null!, null!);
using var mempoolMirror = new MempoolMirror(null!, null!, TimeSpan.Zero);
var wabiSabiApi = new WabiSabiController(idempotencyRequestCache, arena, coinJoinFeeRateStatStore);

InsecureRandom insecureRandom = InsecureRandom.Instance;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
services.AddScoped(s => new CoinJoinScriptStore());
services.AddSingleton<CoinJoinFeeRateStatStore>();
services.AddHttpClient();
services.AddSingleton(s => new MempoolMirror(TimeSpan.Zero, null!, null!));
services.AddSingleton(s => new MempoolMirror(null!, null!, TimeSpan.Zero));
});
builder.ConfigureLogging(o => o.SetMinimumLevel(LogLevel.Warning));
}
Expand Down
2 changes: 1 addition & 1 deletion WalletWasabi/BitcoinCore/CoreNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ public static async Task<CoreNode> CreateAsync(CoreNodeParams coreNodeParams, Ca
Logger.LogInfo($"Started {Constants.BuiltinBitcoinNodeName}.");
}

coreNode.P2pNode = new P2pNode(coreNode.Network, coreNode.P2pEndPoint, coreNode.MempoolService, coreNodeParams.UserAgent);
coreNode.P2pNode = new P2pNode(coreNode.Network, coreNode.P2pEndPoint, coreNode.MempoolService);
await coreNode.P2pNode.ConnectAsync(cancel).ConfigureAwait(false);

return coreNode;
Expand Down
2 changes: 1 addition & 1 deletion WalletWasabi/BitcoinCore/Mempool/MempoolMirror.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace WalletWasabi.BitcoinCore.Mempool;
public class MempoolMirror : PeriodicRunner
{
/// <param name="period">How often to mirror the mempool.</param>
public MempoolMirror(TimeSpan period, IRPCClient rpc, P2pNode node) : base(period)
public MempoolMirror( IRPCClient rpc, P2pNode node, TimeSpan? period = null) : base(period ?? TimeSpan.FromSeconds(21))
{
Rpc = rpc;
Node = node;
Expand Down
5 changes: 1 addition & 4 deletions WalletWasabi/BitcoinCore/P2pNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@ public class P2pNode
{
private bool _disposed = false;

public P2pNode(Network network, EndPoint endPoint, MempoolService mempoolService, string userAgent)
public P2pNode(Network network, EndPoint endPoint, MempoolService mempoolService)
{
Network = Guard.NotNull(nameof(network), network);
EndPoint = Guard.NotNull(nameof(endPoint), endPoint);
MempoolService = Guard.NotNull(nameof(mempoolService), mempoolService);
UserAgent = Guard.NotNullOrEmptyOrWhitespace(nameof(userAgent), userAgent, trim: true);

Stop = new CancellationTokenSource();
NodeEventsSubscribed = false;
Expand All @@ -38,7 +37,6 @@ public P2pNode(Network network, EndPoint endPoint, MempoolService mempoolService
private Network Network { get; }
private EndPoint EndPoint { get; }
public MempoolService MempoolService { get; }
private string UserAgent { get; }

private bool NodeEventsSubscribed { get; set; }
private object SubscriptionLock { get; }
Expand All @@ -52,7 +50,6 @@ public async Task ConnectAsync(CancellationToken cancel)
handshakeTimeout.CancelAfter(TimeSpan.FromSeconds(21));
var parameters = new NodeConnectionParameters()
{
UserAgent = UserAgent,
ConnectCancellation = linked.Token,
IsRelay = true
};
Expand Down
6 changes: 4 additions & 2 deletions WalletWasabi/Blockchain/BlockFilters/IndexBuilderService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ public class IndexBuilderService

private long _workerCount;

public IndexBuilderService(IRPCClient rpc, BlockNotifier blockNotifier, string indexFilePath)
public IndexBuilderService(IRPCClient rpc, BlockNotifier blockNotifier)
{
RpcClient = Guard.NotNull(nameof(rpc), rpc);
BlockNotifier = Guard.NotNull(nameof(blockNotifier), blockNotifier);
IndexFilePath = Guard.NotNullOrEmptyOrWhitespace(nameof(indexFilePath), indexFilePath);
var indexBuilderServiceDir = Path.Combine(".", "IndexBuilderService");
var indexFilePath = Path.Combine(indexBuilderServiceDir, $"Index{rpc.Network}.dat");

IndexFilePath = Guard.NotNullOrEmptyOrWhitespace(nameof(indexFilePath), indexFilePath);
StartingHeight = SmartHeader.GetStartingHeader(RpcClient.Network).Height;

_serviceStatus = NotStarted;
Expand Down
2 changes: 1 addition & 1 deletion WalletWasabi/Blockchain/Blocks/BlockNotifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace WalletWasabi.Blockchain.Blocks;

public class BlockNotifier : PeriodicRunner
{
public BlockNotifier(TimeSpan period, IRPCClient rpcClient, P2pNode? p2pNode = null) : base(period)
public BlockNotifier(IRPCClient rpcClient, P2pNode? p2pNode = null, TimeSpan? period = null) : base(period ?? TimeSpan.FromSeconds(7))
{
RpcClient = Guard.NotNull(nameof(rpcClient), rpcClient);
P2pNode = p2pNode;
Expand Down
4 changes: 2 additions & 2 deletions WalletWasabi/WabiSabi/Backend/DoSPrevention/Warden.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ namespace WalletWasabi.WabiSabi.Backend.DoSPrevention;

public class Warden : BackgroundService
{
public Warden(string prisonFilePath, WabiSabiConfig config)
public Warden(WabiSabiConfig config)
{
PrisonFilePath = prisonFilePath;
PrisonFilePath = "Prison.txt";
Config = config;
OffendersToSaveChannel = Channel.CreateUnbounded<Offender>();

Expand Down
5 changes: 3 additions & 2 deletions WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ namespace WalletWasabi.WabiSabi.Backend.Rounds;
public partial class Arena : PeriodicRunner
{
public Arena(
TimeSpan period,
WabiSabiConfig config,
IRPCClient rpc,
Prison prison,
RoundParameterFactory roundParameterFactory,
CoinJoinScriptStore? coinJoinScriptStore = null ) : base(period)
CoinJoinScriptStore? coinJoinScriptStore = null,
TimeSpan? period = null
) : base(period ?? TimeSpan.FromSeconds(2))
{
Config = config;
Rpc = rpc;
Expand Down
5 changes: 0 additions & 5 deletions WalletWasabi/WabiSabi/CoordinatorParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,4 @@ public CoordinatorParameters(string dataDir)
/// Set how often changes in the configuration file should be monitored.
/// </summary>
public TimeSpan ConfigChangeMonitoringPeriod { get; init; } = TimeSpan.FromSeconds(7);

/// <summary>
/// How often should rounds be stepped.
/// </summary>
public TimeSpan RoundProgressSteppingPeriod { get; init; } = TimeSpan.FromSeconds(1);
}
Loading

0 comments on commit cb570da

Please sign in to comment.