WIP Adding BlockstreamInfoFeeProvider
ADD
WalletWasabi.Tests/IntegrationTests/BlockstreamInfoTests.cs
+using NBitcoin;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Blockchain.Analysis.FeesEstimation;
+using WalletWasabi.Tests.Helpers;
+using WalletWasabi.Tor;
+using WalletWasabi.Tor.Http;
+using WalletWasabi.Tor.Socks5;
+using WalletWasabi.Tor.Socks5.Pool;
+using WalletWasabi.WebClients.BlockstreamInfo;
+using Xunit;
+
+namespace WalletWasabi.Tests.IntegrationTests
+{
+ public class BlockstreamInfoTests : IAsyncLifetime
+ {
+ public BlockstreamInfoTests()
+ {
+ TorHttpPool = new(new TorTcpConnectionFactory(Common.TorSocks5Endpoint));
+ TorManager = new(Common.TorSettings, Common.TorSocks5Endpoint);
+ }
+
+ private TorHttpPool TorHttpPool { get; }
+ private TorProcessManager TorManager { get; }
+
+ public async Task InitializeAsync()
+ {
+ bool started = await TorManager.StartAsync(ensureRunning: true);
+ Assert.True(started, "Tor failed to start.");
+ }
+
+ public Task DisposeAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ [Fact]
+ public async Task GetFeeEstimatesClearnetMainnetAsync()
+ {
+ using var client = new BlockstreamInfoClient(Network.Main);
+ var estimates = await client.GetFeeEstimatesAsync(CancellationToken.None);
+ Assert.NotNull(estimates);
+ Assert.NotEmpty(estimates.Estimations);
+ }
+
+ [Fact]
+ public async Task GetFeeEstimatesTorMainnetAsync()
+ {
+ using var client = new BlockstreamInfoClient(Network.Main, TorHttpPool);
+ var estimates = await client.GetFeeEstimatesAsync(CancellationToken.None);
+ Assert.NotNull(estimates);
+ Assert.NotEmpty(estimates.Estimations);
+ }
+
+ [Fact]
+ public async Task GetFeeEstimatesClearnetTestnetAsync()
+ {
+ using var client = new BlockstreamInfoClient(Network.TestNet);
+ var estimates = await client.GetFeeEstimatesAsync(CancellationToken.None);
+ Assert.NotNull(estimates);
+ Assert.NotEmpty(estimates.Estimations);
+ }
+
+ [Fact]
+ public async Task GetFeeEstimatesTorTestnetAsync()
+ {
+ using var client = new BlockstreamInfoClient(Network.TestNet, TorHttpPool);
+ var estimates = await client.GetFeeEstimatesAsync(CancellationToken.None);
+ Assert.NotNull(estimates);
+ Assert.NotEmpty(estimates.Estimations);
+ }
+
+ [Fact]
+ public async Task SimulatesFeeEstimatesClearnetRegtestAsync()
+ {
+ using var client = new BlockstreamInfoClient(Network.RegTest);
+ var estimates = await client.GetFeeEstimatesAsync(CancellationToken.None);
+ Assert.NotNull(estimates);
+ Assert.NotEmpty(estimates.Estimations);
+ }
+
+ [Fact]
+ public async Task SimulatesFeeEstimatesTorRegtestAsync()
+ {
+ using var client = new BlockstreamInfoClient(Network.RegTest, TorHttpPool);
+ var estimates = await client.GetFeeEstimatesAsync(CancellationToken.None);
+ Assert.NotNull(estimates);
+ Assert.NotEmpty(estimates.Estimations);
+ }
+ }
+}

MODIFY
WalletWasabi/Blockchain/Analysis/FeesEstimation/HybridFeeProvider.cs
WalletWasabi/Blockchain/Analysis/FeesEstimation/HybridFeeProvider.cs
@@ -10,6 +10,7 @@ using WalletWasabi.Logging;
using WalletWasabi.Models;
using WalletWasabi.Nito.AsyncEx;
using WalletWasabi.Services;
+using WalletWasabi.WebClients.BlockstreamInfo;
namespace WalletWasabi.Blockchain.Analysis.FeesEstimation
{
@@ -19,16 +20,18 @@ namespace WalletWasabi.Blockchain.Analysis.FeesEstimation
/// </summary>
public class HybridFeeProvider : IHostedService
{
- public HybridFeeProvider(WasabiSynchronizer synchronizer, RpcFeeProvider? rpcFeeProvider)
+ public HybridFeeProvider(WasabiSynchronizer synchronizer, RpcFeeProvider? rpcFeeProvider, BlockstreamInfoFreeProvider blockstreamProvider)
{
Synchronizer = synchronizer;
RpcFeeProvider = rpcFeeProvider;
+ BlockstreamProvider = blockstreamProvider;
}
public event EventHandler<AllFeeEstimate>? AllFeeEstimateChanged;
public WasabiSynchronizer Synchronizer { get; }
public RpcFeeProvider? RpcFeeProvider { get; }
+ public BlockstreamInfoFreeProvider BlockstreamProvider { get; }
private object Lock { get; } = new object();
public AllFeeEstimate? AllFeeEstimate { get; private set; }
private AbandonedTasks ProcessingEvents { get; } = new();
@@ -37,8 +40,10 @@ namespace WalletWasabi.Blockchain.Analysis.FeesEstimation
{
SetAllFeeEstimateIfLooksBetter(RpcFeeProvider?.LastAllFeeEstimate);
SetAllFeeEstimateIfLooksBetter(Synchronizer.LastResponse?.AllFeeEstimate);
+ SetAllFeeEstimateIfLooksBetter(BlockstreamProvider.LastAllFeeEstimate);
Synchronizer.ResponseArrived += Synchronizer_ResponseArrived;
+ BlockstreamProvider.AllFeeEstimateArrived += OnAllFeeEstimateArrived;
if (RpcFeeProvider is not null)
{
RpcFeeProvider.AllFeeEstimateArrived += OnAllFeeEstimateArrived;
@@ -50,6 +55,7 @@ namespace WalletWasabi.Blockchain.Analysis.FeesEstimation
public async Task StopAsync(CancellationToken cancellationToken)
{
Synchronizer.ResponseArrived -= Synchronizer_ResponseArrived;
+ BlockstreamProvider.AllFeeEstimateArrived -= OnAllFeeEstimateArrived;
if (RpcFeeProvider is not null)
{
RpcFeeProvider.AllFeeEstimateArrived -= OnAllFeeEstimateArrived;
@@ -106,6 +112,7 @@ namespace WalletWasabi.Blockchain.Analysis.FeesEstimation
else
{
// If neither user's full node, nor backend is ready, then let's try our best effort figuring out which data looks better:
+
notify = SetAllFeeEstimateIfLooksBetter(fees);
}
}
@@ -132,6 +139,26 @@ namespace WalletWasabi.Blockchain.Analysis.FeesEstimation
}
}
}
+ else if (sender is BlockstreamInfoFreeProvider blockstreamProvider)
+ {
+ var rpcProvider = RpcFeeProvider;
+ if (rpcProvider is null)
+ {
+ // If user doesn't use full node, then set it, this is the best we got.
+ notify = SetAllFeeEstimate(fees);
+ }
+ if (rpcProvider.LastAllFeeEstimate?.IsAccurate is true && !rpcProvider.InError)
+ {
+ // If user's full node is properly serving data, then we don't care about the backend.
+ return;
+ }
+ if (fees.IsAccurate && !blockstreamProvider.InError)
+ {
+ }
+ else
+ {
+ }
+ }
}
if (notify)

ADD
WalletWasabi/WebClients/BlockstreamInfo/BlockstreamInfoClient.cs
+using NBitcoin;
+using NBitcoin.RPC;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Blockchain.Analysis.FeesEstimation;
+using WalletWasabi.Tor.Http;
+using WalletWasabi.Tor.Http.Extensions;
+using WalletWasabi.Tor.Socks5.Pool;
+
+namespace WalletWasabi.WebClients.BlockstreamInfo
+{
+ public class BlockstreamInfoClient : IDisposable
+ {
+ private bool _disposedValue;
+
+ public BlockstreamInfoClient(Network network, TorHttpPool? pool = null)
+ {
+ if (pool is not null)
+ {
+ TorHttpClient = new TorHttpClient(new Uri(network == Network.TestNet ? "http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/testnet" : "http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion"), pool);
+ }
+ else
+ {
+ HttpClient = new HttpClient();
+ HttpClient.BaseAddress = new Uri(network == Network.TestNet ? "https://blockstream.info/testnet" : "https://blockstream.info");
+ }
+ }
+
+ public TorHttpClient? TorHttpClient { get; }
+ public HttpClient? HttpClient { get; }
+
+ public async Task<AllFeeEstimate> GetFeeEstimatesAsync(CancellationToken cancel)
+ {
+ using HttpResponseMessage response = TorHttpClient is not null
+ ? await TorHttpClient.SendAsync(HttpMethod.Get, "api/fee-estimates", null, cancel).ConfigureAwait(false)
+ : await HttpClient!.GetAsync("api/fee-estimates", cancel).ConfigureAwait(false);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ await response.ThrowRequestExceptionFromContentAsync().ConfigureAwait(false);
+ }
+
+ var responseString = await response.Content.ReadAsStringAsync(cancel).ConfigureAwait(false);
+ var parsed = JsonDocument.Parse(responseString).RootElement;
+ var myDic = new Dictionary<int, int>();
+ foreach (var elem in parsed.EnumerateObject())
+ {
+ myDic.Add(int.Parse(elem.Name), (int)Math.Ceiling(elem.Value.GetDouble()));
+ }
+
+ return new AllFeeEstimate(EstimateSmartFeeMode.Conservative, myDic, isAccurate: true);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ HttpClient?.Dispose();
+ }
+
+ _disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}

ADD
WalletWasabi/WebClients/BlockstreamInfo/BlockstreamInfoFreeProvider.cs
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Bases;
+using WalletWasabi.Blockchain.Analysis.FeesEstimation;
+
+namespace WalletWasabi.WebClients.BlockstreamInfo
+{
+ public class BlockstreamInfoFreeProvider : PeriodicRunner
+ {
+ public BlockstreamInfoFreeProvider(TimeSpan period, BlockstreamInfoClient blockstreamInfoClient) : base(period)
+ {
+ BlockstreamInfoClient = blockstreamInfoClient;
+ }
+
+ public event EventHandler<AllFeeEstimate>? AllFeeEstimateArrived;
+
+ public BlockstreamInfoClient BlockstreamInfoClient { get; set; }
+ public AllFeeEstimate? LastAllFeeEstimate { get; private set; }
+ public bool InError { get; private set; } = false;
+ public bool IsPaused { get; set; } = false;
+
+ protected override async Task ActionAsync(CancellationToken cancel)
+ {
+ if (IsPaused)
+ {
+ return;
+ }
+ try
+ {
+ var allFeeEstimate = await BlockstreamInfoClient.GetFeeEstimatesAsync(cancel).ConfigureAwait(false);
+
+ LastAllFeeEstimate = allFeeEstimate;
+ if (allFeeEstimate?.Estimations?.Any() is true)
+ {
+ AllFeeEstimateArrived?.Invoke(this, allFeeEstimate);
+ }
+ InError = false;
+ }
+ catch
+ {
+ InError = true;
+ throw;
+ }
+ }
+ }
+}

WIP Adding BlockstreamInfoFeeProvider