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 States { get; set; } public override string ToString() { return ""; } } private readonly SettingsRepository _SettingsRepository; readonly RateProviderFactory _RateProviderFactory; public RatesHostedService(SettingsRepository repo, RateProviderFactory rateProviderFactory) { 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), Cancellation); return; } using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(Cancellation)) { 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) { 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), Cancellation); } 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(); 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(usedProviders.Length); foreach (var provider in usedProviders) { var state = provider.Fetcher.GetState(); state.ExchangeName = provider.ExchangeName; cache.States.Add(state); } await _SettingsRepository.UpdateSetting(cache); } } }