mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 13:26:47 +01:00
6049fa23a7
* Support pluginable rate providers This PR allows plugins to provide custom rate providers, that can be contextual to a store. For example, if you use the upcoming fiat offramp plugin, or the Blink plugin, you'll probably want to configure the fetch the rates from them since they are determining the actual fiat rrate to you. However, they require API keys. This PR enables these scenarios, even much more advanced ones, but for example: * Install fiat offramp plugin * Configure it * You can now use the fiat offramp rate provider (no additional config steps beyond selecting the rate source from the select, or maybe the plugin would automatically set it for you once configured) * Apply suggestions from code review * Simplify * Do not use BackgroundFetcherRateProvider for contextual rate prov --------- Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
158 lines
5.8 KiB
C#
158 lines
5.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using BTCPayServer.Logging;
|
|
using BTCPayServer.Services;
|
|
using BTCPayServer.Services.Rates;
|
|
using Microsoft.Extensions.Logging;
|
|
using NBitcoin;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace BTCPayServer.HostedServices
|
|
{
|
|
public class RatesHostedService : BaseAsyncService
|
|
{
|
|
public class ExchangeRatesCache
|
|
{
|
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
|
public DateTimeOffset Created { get; set; }
|
|
public List<BackgroundFetcherState> States { get; set; }
|
|
public override string ToString()
|
|
{
|
|
return "";
|
|
}
|
|
}
|
|
private readonly SettingsRepository _SettingsRepository;
|
|
readonly RateProviderFactory _RateProviderFactory;
|
|
|
|
public RatesHostedService(SettingsRepository repo,
|
|
RateProviderFactory rateProviderFactory,
|
|
Logs logs) : base(logs)
|
|
{
|
|
this._SettingsRepository = repo;
|
|
_RateProviderFactory = rateProviderFactory;
|
|
}
|
|
|
|
internal override Task[] InitializeTasks()
|
|
{
|
|
return new Task[]
|
|
{
|
|
CreateLoopTask(RefreshRates)
|
|
};
|
|
}
|
|
|
|
bool IsStillUsed(BackgroundFetcherRateProvider fetcher)
|
|
{
|
|
return fetcher.LastRequested is DateTimeOffset v &&
|
|
DateTimeOffset.UtcNow - v < TimeSpan.FromDays(1.0);
|
|
}
|
|
|
|
IEnumerable<(string ExchangeName, BackgroundFetcherRateProvider Fetcher)> GetStillUsedProviders()
|
|
{
|
|
foreach (var kv in _RateProviderFactory.Providers)
|
|
{
|
|
if (kv.Value is BackgroundFetcherRateProvider fetcher && IsStillUsed(fetcher))
|
|
{
|
|
yield return (kv.Key, fetcher);
|
|
}
|
|
}
|
|
}
|
|
async Task RefreshRates()
|
|
{
|
|
var usedProviders = GetStillUsedProviders().ToArray();
|
|
if (usedProviders.Length == 0)
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(30), CancellationToken);
|
|
return;
|
|
}
|
|
using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken))
|
|
{
|
|
timeout.CancelAfter(TimeSpan.FromSeconds(20.0));
|
|
try
|
|
{
|
|
await Task.WhenAll(usedProviders
|
|
.Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token).ContinueWith(t =>
|
|
{
|
|
if (t.Result.Exception != null && t.Result.Exception is not NotSupportedException)
|
|
{
|
|
Logs.PayServer.LogWarning($"Error while contacting exchange {p.ExchangeName}: {t.Result.Exception.Message}");
|
|
}
|
|
}, TaskScheduler.Default))
|
|
.ToArray()).WithCancellation(timeout.Token);
|
|
}
|
|
catch (OperationCanceledException) when (timeout.IsCancellationRequested)
|
|
{
|
|
}
|
|
if (_LastCacheDate is DateTimeOffset lastCache)
|
|
{
|
|
if (DateTimeOffset.UtcNow - lastCache > TimeSpan.FromMinutes(8.0))
|
|
{
|
|
await SaveRateCache();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
await SaveRateCache();
|
|
}
|
|
}
|
|
await Task.Delay(TimeSpan.FromSeconds(30), CancellationToken);
|
|
}
|
|
|
|
public override async Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
await TryLoadRateCache();
|
|
await base.StartAsync(cancellationToken);
|
|
}
|
|
public override async Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
await SaveRateCache();
|
|
await base.StopAsync(cancellationToken);
|
|
}
|
|
|
|
private async Task TryLoadRateCache()
|
|
{
|
|
try
|
|
{
|
|
var cache = await _SettingsRepository.GetSettingAsync<ExchangeRatesCache>();
|
|
if (cache != null)
|
|
{
|
|
_LastCacheDate = cache.Created;
|
|
var stateByExchange = cache.States.ToDictionary(o => o.ExchangeName);
|
|
foreach (var provider in _RateProviderFactory.Providers)
|
|
{
|
|
if (stateByExchange.TryGetValue(provider.Key, out var state) &&
|
|
provider.Value is BackgroundFetcherRateProvider fetcher)
|
|
{
|
|
fetcher.LoadState(state);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logs.PayServer.LogWarning(ex, "Warning: Error while trying to load rates from cache");
|
|
}
|
|
}
|
|
|
|
DateTimeOffset? _LastCacheDate;
|
|
private async Task SaveRateCache()
|
|
{
|
|
var cache = new ExchangeRatesCache();
|
|
cache.Created = DateTimeOffset.UtcNow;
|
|
_LastCacheDate = cache.Created;
|
|
|
|
var usedProviders = GetStillUsedProviders().ToArray();
|
|
cache.States = new List<BackgroundFetcherState>(usedProviders.Length);
|
|
foreach (var provider in usedProviders)
|
|
{
|
|
var state = provider.Fetcher.GetState();
|
|
state.ExchangeName = provider.ExchangeName;
|
|
cache.States.Add(state);
|
|
}
|
|
await _SettingsRepository.UpdateSetting(cache);
|
|
}
|
|
}
|
|
}
|