RPC EstimateAllFee cancellable + tests fixed
MODIFY
WalletWasabi.Tests/UnitTests/AllFeeEstimateTests.cs
WalletWasabi.Tests/UnitTests/AllFeeEstimateTests.cs
@@ -13,6 +13,7 @@ using WalletWasabi.Helpers;
using WalletWasabi.Tests.Helpers;
using Xunit;
using Moq;
+using System.Threading;
namespace WalletWasabi.Tests.UnitTests
{
@@ -146,7 +147,7 @@ namespace WalletWasabi.Tests.UnitTests
});
mockRpc.Setup(rpc => rpc.GetPeersInfoAsync())
.ReturnsAsync(Array.Empty<PeerInfo>());
- mockRpc.Setup(rpc => rpc.GetMempoolInfoAsync())
+ mockRpc.Setup(rpc => rpc.GetMempoolInfoAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new MemPoolInfo
{
MemPoolMinFee = 0.00001000 // 1 s/b (default value)
@@ -168,7 +169,7 @@ namespace WalletWasabi.Tests.UnitTests
mockRpc.Setup(rpc => rpc.EstimateSmartFeeAsync(It.IsAny<int>(), It.IsAny<EstimateSmartFeeMode>()))
.ThrowsAsync(new RPCException(RPCErrorCode.RPC_CLIENT_NOT_CONNECTED, "Error-EstimateSmartFee", null));
- mockRpc.Setup(rpc => rpc.GetMempoolInfoAsync())
+ mockRpc.Setup(rpc => rpc.GetMempoolInfoAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new MemPoolInfo
{
MemPoolMinFee = 0.00001000 // 1 s/b (default value)
@@ -231,7 +232,7 @@ namespace WalletWasabi.Tests.UnitTests
var mockRpc = CreateAndConfigureRpcClient(hasPeersInfo: true);
var any = EstimateSmartFeeMode.Conservative;
- mockRpc.Setup(rpc => rpc.GetMempoolInfoAsync()).ReturnsAsync(
+ mockRpc.Setup(rpc => rpc.GetMempoolInfoAsync(It.IsAny<CancellationToken>())).ReturnsAsync(
new MemPoolInfo
{
MemPoolMinFee = 0.00001000, // 1 s/b (default value)
@@ -279,7 +280,7 @@ namespace WalletWasabi.Tests.UnitTests
{
var mockRpc = CreateAndConfigureRpcClient(hasPeersInfo: true);
var mempoolInfo = MempoolInfoGenerator.GenerateMempoolInfo();
- mockRpc.Setup(rpc => rpc.GetMempoolInfoAsync()).ReturnsAsync(mempoolInfo);
+ mockRpc.Setup(rpc => rpc.GetMempoolInfoAsync(It.IsAny<CancellationToken>())).ReturnsAsync(mempoolInfo);
mockRpc.Setup(rpc => rpc.EstimateSmartFeeAsync(It.IsAny<int>(), EstimateSmartFeeMode.Conservative)).ReturnsAsync(FeeRateResponse(2, 120m));
var feeRates = await mockRpc.Object.EstimateAllFeeAsync(EstimateSmartFeeMode.Conservative);
var estimations = feeRates.Estimations;
@@ -304,7 +305,7 @@ namespace WalletWasabi.Tests.UnitTests
hasPeersInfo
? new[] { new PeerInfo() }
: Array.Empty<PeerInfo>());
- mockRpc.Setup(rpc => rpc.GetMempoolInfoAsync()).ReturnsAsync(
+ mockRpc.Setup(rpc => rpc.GetMempoolInfoAsync(It.IsAny<CancellationToken>())).ReturnsAsync(
new MemPoolInfo
{
MemPoolMinFee = memPoolMinFee, // 1 s/b (default value)

MODIFY
WalletWasabi.Tests/UnitTests/BitcoinCore/RpcBasedTests.cs
WalletWasabi.Tests/UnitTests/BitcoinCore/RpcBasedTests.cs
using NBitcoin;
using NBitcoin.RPC;
+using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -173,6 +174,23 @@ namespace WalletWasabi.Tests.UnitTests.BitcoinCore
}
}
+ [Fact]
+ public async Task FeeEstimationCanCancelAsync()
+ {
+ var coreNode = await TestNodeBuilder.CreateAsync();
+ try
+ {
+ var rpc = coreNode.RpcClient;
+ using CancellationTokenSource cts = new();
+ cts.Cancel();
+ await Assert.ThrowsAsync<OperationCanceledException>(async () => await rpc.EstimateAllFeeAsync(EstimateSmartFeeMode.Conservative, true, cts.Token));
+ }
+ finally
+ {
+ await coreNode.TryStopAsync();
+ }
+ }
+
[Fact]
public async Task CantDoubleSpendAsync()
{

MODIFY
WalletWasabi.Tests/UnitTests/MockRpcClient.cs
WalletWasabi.Tests/UnitTests/MockRpcClient.cs
using System;
+using System.Threading;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.RPC;
@@ -64,7 +65,7 @@ namespace WalletWasabi.Tests.UnitTests
throw new NotImplementedException();
}
- public Task<MemPoolInfo> GetMempoolInfoAsync()
+ public Task<MemPoolInfo> GetMempoolInfoAsync(CancellationToken cancel = default)
{
return OnGetMempoolInfoAsync();
}

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/ConfigTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/ConfigTests.cs
@@ -132,7 +132,7 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
rpcMock.SetupGet(rpc => rpc.Network).Returns(Network.Main);
rpcMock.Setup(rpc => rpc.EstimateSmartFeeAsync(It.IsAny<int>(), It.IsAny<EstimateSmartFeeMode>()))
.ReturnsAsync(new EstimateSmartFeeResponse { FeeRate = new FeeRate(10m) });
- rpcMock.Setup(rpc => rpc.GetMempoolInfoAsync())
+ rpcMock.Setup(rpc => rpc.GetMempoolInfoAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new MemPoolInfo { MemPoolMinFee = 0.00001000 });
return rpcMock.Object;
}

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/CoordinatorTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/CoordinatorTests.cs
@@ -75,7 +75,7 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Backend
mockRpcClient.Setup(rpc => rpc.Network).Returns(Network.Main);
mockRpcClient.Setup(rpc => rpc.EstimateSmartFeeAsync(It.IsAny<int>(), It.IsAny<EstimateSmartFeeMode>()))
.ReturnsAsync(new EstimateSmartFeeResponse { Blocks = 5, FeeRate = new FeeRate(100m) });
- mockRpcClient.Setup(rpc => rpc.GetMempoolInfoAsync())
+ mockRpcClient.Setup(rpc => rpc.GetMempoolInfoAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new MemPoolInfo { MemPoolMinFee = 0.00001000 });
return mockRpcClient.Object;
}

MODIFY
WalletWasabi/BitcoinCore/Rpc/CachedRpcClient.cs
WalletWasabi/BitcoinCore/Rpc/CachedRpcClient.cs
@@ -145,7 +145,7 @@ namespace WalletWasabi.BitcoinCore.Rpc
() => base.GetMempoolEntryAsync(txid, throwIfNotFound)).ConfigureAwait(false);
}
- public override async Task<MemPoolInfo> GetMempoolInfoAsync()
+ public override async Task<MemPoolInfo> GetMempoolInfoAsync(CancellationToken cancel = default)
{
string cacheKey = nameof(GetMempoolInfoAsync);
var cacheOptions = new MemoryCacheEntryOptions
@@ -157,7 +157,7 @@ namespace WalletWasabi.BitcoinCore.Rpc
return await Cache.AtomicGetOrCreateAsync(
cacheKey,
cacheOptions,
- () => base.GetMempoolInfoAsync()).ConfigureAwait(false);
+ () => base.GetMempoolInfoAsync(cancel)).ConfigureAwait(false);
}
public override async Task<EstimateSmartFeeResponse> EstimateSmartFeeAsync(int confirmationTarget, EstimateSmartFeeMode estimateMode = EstimateSmartFeeMode.Conservative)

MODIFY
WalletWasabi/BitcoinCore/Rpc/IRPCClient.cs
WalletWasabi/BitcoinCore/Rpc/IRPCClient.cs
using NBitcoin;
using NBitcoin.RPC;
using System;
+using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.BitcoinCore.Rpc.Models;
@@ -35,7 +36,7 @@ namespace WalletWasabi.BitcoinCore.Rpc
Task<uint256[]> GetRawMempoolAsync();
- Task<MemPoolInfo> GetMempoolInfoAsync();
+ Task<MemPoolInfo> GetMempoolInfoAsync(CancellationToken cancel = default);
Task<MempoolAcceptResult> TestMempoolAcceptAsync(Transaction transaction, bool allowHighFees = false);

MODIFY
WalletWasabi/BitcoinCore/Rpc/RpcClientBase.cs
WalletWasabi/BitcoinCore/Rpc/RpcClientBase.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.BitcoinCore.Rpc.Models;
using WalletWasabi.Helpers;
@@ -59,12 +60,11 @@ namespace WalletWasabi.BitcoinCore.Rpc
return await Rpc.GetMempoolEntryAsync(txid, throwIfNotFound).ConfigureAwait(false);
}
- public virtual async Task<MemPoolInfo> GetMempoolInfoAsync()
+ public virtual async Task<MemPoolInfo> GetMempoolInfoAsync(CancellationToken cancel = default)
{
try
{
var response = await Rpc.SendCommandAsync(RPCOperations.getmempoolinfo, true).ConfigureAwait(false);
-
static IEnumerable<FeeRateGroup> ExtractFeeRateGroups(JToken jt) =>
jt switch
{
@@ -95,6 +95,8 @@ namespace WalletWasabi.BitcoinCore.Rpc
}
catch (RPCException ex) when (ex.RPCCode == RPCErrorCode.RPC_MISC_ERROR)
{
+ cancel.ThrowIfCancellationRequested();
+
return await Rpc.GetMemPoolAsync().ConfigureAwait(false);
}
}

MODIFY
WalletWasabi/Extensions/RPCClientExtensions.cs
WalletWasabi/Extensions/RPCClientExtensions.cs
@@ -72,12 +72,12 @@ namespace NBitcoin.RPC
{
var smartEstimations = (simulateIfRegTest && rpc.Network == Network.RegTest)
? SimulateRegTestFeeEstimation(estimateMode)
- : await GetFeeEstimationsAsync(rpc, estimateMode).ConfigureAwait(false);
+ : await GetFeeEstimationsAsync(rpc, estimateMode, cancel).ConfigureAwait(false);
- var mempoolInfo = await rpc.GetMempoolInfoAsync().ConfigureAwait(false);
+ var mempoolInfo = await rpc.GetMempoolInfoAsync(cancel).ConfigureAwait(false);
var sanityFeeRate = mempoolInfo.GetSanityFeeRate();
- var rpcStatus = await rpc.GetRpcStatusAsync(CancellationToken.None).ConfigureAwait(false);
+ var rpcStatus = await rpc.GetRpcStatusAsync(cancel).ConfigureAwait(false);
var minEstimations = GetFeeEstimationsFromMempoolInfo(mempoolInfo);
@@ -100,7 +100,7 @@ namespace NBitcoin.RPC
rpcStatus.Synchronized);
}
- private static async Task<Dictionary<int, int>> GetFeeEstimationsAsync(IRPCClient rpc, EstimateSmartFeeMode estimateMode)
+ private static async Task<Dictionary<int, int>> GetFeeEstimationsAsync(IRPCClient rpc, EstimateSmartFeeMode estimateMode, CancellationToken cancel)
{
var batchClient = rpc.PrepareBatch();
@@ -113,6 +113,7 @@ namespace NBitcoin.RPC
try
{
await Task.WhenAll(rpcFeeEstimationTasks).ConfigureAwait(false);
+ cancel.ThrowIfCancellationRequested();
}
catch
{
@@ -121,6 +122,11 @@ namespace NBitcoin.RPC
throw rpcFeeEstimationTasks[0].Exception?.InnerExceptions[0]
?? new Exception($"{nameof(GetFeeEstimationsAsync)} failed to fetch fee estimations.");
}
+ if (cancel.IsCancellationRequested)
+ {
+ Logger.LogInfo($"{nameof(GetFeeEstimationsAsync)} has been cancelled.");
+ throw new OperationCanceledException();
+ }
}
// EstimateSmartFeeAsync returns the block number where estimate was found - not always what the requested one.

RPC EstimateAllFee cancellable + tests fixed