diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 49d557920..9844dc1c8 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -120,36 +120,67 @@ namespace BTCPayServer.Tests .Build(); _Host.Start(); InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository)); - StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository)); - var rateProvider = (RateProviderFactory)_Host.Services.GetService(typeof(RateProviderFactory)); - rateProvider.DirectProviders.Clear(); + StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository)); - var coinAverageMock = new MockRateProvider(); - coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() + if (MockRates) { - Exchange = "coinaverage", - CurrencyPair = CurrencyPair.Parse("BTC_USD"), - BidAsk = new BidAsk(5000m) - }); - coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() - { - Exchange = "coinaverage", - CurrencyPair = CurrencyPair.Parse("BTC_CAD"), - BidAsk = new BidAsk(4500m) - }); - coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() - { - Exchange = "coinaverage", - CurrencyPair = CurrencyPair.Parse("LTC_BTC"), - BidAsk = new BidAsk(0.001m) - }); - coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() - { - Exchange = "coinaverage", - CurrencyPair = CurrencyPair.Parse("LTC_USD"), - BidAsk = new BidAsk(500m) - }); - rateProvider.DirectProviders.Add("coinaverage", coinAverageMock); + var rateProvider = (RateProviderFactory)_Host.Services.GetService(typeof(RateProviderFactory)); + rateProvider.Providers.Clear(); + + var coinAverageMock = new MockRateProvider(); + coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "coinaverage", + CurrencyPair = CurrencyPair.Parse("BTC_USD"), + BidAsk = new BidAsk(5000m) + }); + coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "coinaverage", + CurrencyPair = CurrencyPair.Parse("BTC_CAD"), + BidAsk = new BidAsk(4500m) + }); + coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "coinaverage", + CurrencyPair = CurrencyPair.Parse("LTC_BTC"), + BidAsk = new BidAsk(0.001m) + }); + coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "coinaverage", + CurrencyPair = CurrencyPair.Parse("LTC_USD"), + BidAsk = new BidAsk(500m) + }); + rateProvider.Providers.Add("coinaverage", coinAverageMock); + + var bitflyerMock = new MockRateProvider(); + bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "bitflyer", + CurrencyPair = CurrencyPair.Parse("BTC_JPY"), + BidAsk = new BidAsk(700000m) + }); + rateProvider.Providers.Add("bitflyer", bitflyerMock); + + var quadrigacx = new MockRateProvider(); + quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "quadrigacx", + CurrencyPair = CurrencyPair.Parse("BTC_CAD"), + BidAsk = new BidAsk(6000m) + }); + rateProvider.Providers.Add("quadrigacx", quadrigacx); + + var bittrex = new MockRateProvider(); + bittrex.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "bittrex", + CurrencyPair = CurrencyPair.Parse("DOGE_BTC"), + BidAsk = new BidAsk(0.004m) + }); + rateProvider.Providers.Add("bittrex", bittrex); + } } public string HostName @@ -177,7 +208,7 @@ namespace BTCPayServer.Tests { context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication)); } - if(storeId != null) + if (storeId != null) { context.SetStoreData(GetService().FindStore(storeId, userId).GetAwaiter().GetResult()); } diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index ca156ae73..7008283f7 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -14,6 +14,7 @@ using Xunit; using NBXplorer.DerivationStrategy; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; +using BTCPayServer.Tests.Logging; namespace BTCPayServer.Tests { diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 2ddd0c295..3f388a81d 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -398,6 +398,7 @@ namespace BTCPayServer.Tests } [Fact] + [Trait("Flaky", "Flaky")] public void CanSetLightningServer() { using (var tester = ServerTester.Create()) @@ -546,18 +547,21 @@ namespace BTCPayServer.Tests } [Fact] + [Trait("Flaky", "Flaky")] public void CanSendLightningPaymentCLightning() { ProcessLightningPayment(LightningConnectionType.CLightning); } [Fact] + [Trait("Flaky", "Flaky")] public void CanSendLightningPaymentCharge() { ProcessLightningPayment(LightningConnectionType.Charge); } [Fact] + [Trait("Flaky", "Flaky")] public void CanSendLightningPaymentLnd() { ProcessLightningPayment(LightningConnectionType.LndREST); @@ -943,8 +947,8 @@ namespace BTCPayServer.Tests user.RegisterDerivationScheme("BTC"); List rates = new List(); rates.Add(CreateInvoice(tester, user, "coinaverage")); - var bitflyer = CreateInvoice(tester, user, "bitflyer"); - var bitflyer2 = CreateInvoice(tester, user, "bitflyer"); + var bitflyer = CreateInvoice(tester, user, "bitflyer", "JPY"); + var bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY"); Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache rates.Add(bitflyer); @@ -955,7 +959,7 @@ namespace BTCPayServer.Tests } } - private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange) + private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD") { var storeController = user.GetController(); var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; @@ -964,7 +968,7 @@ namespace BTCPayServer.Tests var invoice2 = user.BitPay.CreateInvoice(new Invoice() { Price = 5000.0m, - Currency = "USD", + Currency = currency, PosData = "posData", OrderId = "orderId", ItemDesc = "Some description", @@ -978,12 +982,10 @@ namespace BTCPayServer.Tests { using (var tester = ServerTester.Create()) { - tester.PayTester.MockRates = false; tester.Start(); var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); - // First we try payment with a merchant having only BTC var invoice1 = user.BitPay.CreateInvoice(new Invoice() { @@ -994,7 +996,7 @@ namespace BTCPayServer.Tests ItemDesc = "Some description", FullNotifications = true }, Facade.Merchant); - + Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice); var storeController = user.GetController(); var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; @@ -1013,11 +1015,9 @@ namespace BTCPayServer.Tests FullNotifications = true }, Facade.Merchant); - // The rate was 5000 USD per BTC - // Now it should be 3000 USD per BTC - // So the expected price should be - var expected = Money.Coins(5000m / 3000m); - Assert.True(invoice2.BtcPrice.Almost(expected, 0.00001m)); + var expectedRate = 5000.0m * 0.6m; + var expectedCoins = invoice2.Price / expectedRate; + Assert.True(invoice2.BtcPrice.Almost(Money.Coins(expectedCoins), 0.00001m)); } } @@ -1132,7 +1132,7 @@ namespace BTCPayServer.Tests rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD"; rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" + "X_CAD = quadrigacx(X_CAD);\n" + - "X_X = gdax(X_X);"; + "X_X = coinaverage(X_X);"; rateVm.Spread = 50; rateVm = Assert.IsType(Assert.IsType(store.Rates(rateVm, "Test").Result).Model); Assert.True(rateVm.TestRateRules.All(t => !t.Error)); @@ -1669,11 +1669,14 @@ namespace BTCPayServer.Tests var factory = CreateBTCPayRateFactory(); foreach (var result in factory - .DirectProviders - .Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync())) + .Providers + .Where(p => p.Value is BackgroundFetcherRateProvider) + .Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value)) .ToList()) { + result.Fetcher.InvalidateCache(); var exchangeRates = result.ResultAsync.Result; + result.Fetcher.InvalidateCache(); Assert.NotNull(exchangeRates); Assert.NotEmpty(exchangeRates); Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]); @@ -1687,7 +1690,7 @@ namespace BTCPayServer.Tests ); } // Kraken emit one request only after first GetRates - factory.DirectProviders["kraken"].GetRatesAsync().GetAwaiter().GetResult(); + factory.Providers["kraken"].GetRatesAsync().GetAwaiter().GetResult(); } [Fact] @@ -1712,44 +1715,87 @@ namespace BTCPayServer.Tests private static RateProviderFactory CreateBTCPayRateFactory() { - return new RateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, null, new CoinAverageSettings()); + return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings()); + } + + private static MemoryCacheOptions CreateMemoryCache() + { + return new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }; + } + + class SpyRateProvider : IRateProvider + { + public bool Hit { get; set; } + public Task GetRatesAsync() + { + Hit = true; + var rates = new ExchangeRates(); + rates.Add(new ExchangeRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(5000))); + return Task.FromResult(rates); + } + + public void AssertHit() + { + Assert.True(Hit, "Should have hit the provider"); + Hit = false; + } + public void AssertNotHit() + { + Assert.False(Hit, "Should have not hit the provider"); + Hit = false; + } } [Fact] public void CheckRatesProvider() { - var coinAverage = new CoinAverageRateProvider(); - var rates = coinAverage.GetRatesAsync().GetAwaiter().GetResult(); - Assert.NotNull(rates.GetRate("coinaverage", new CurrencyPair("BTC", "JPY"))); - var ratesBitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRatesAsync().GetAwaiter().GetResult(); - Assert.NotNull(ratesBitpay.GetRate("bitpay", new CurrencyPair("BTC", "JPY"))); - + var spy = new SpyRateProvider(); RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules); var factory = CreateBTCPayRateFactory(); - var fetcher = new RateFetcher(CreateBTCPayRateFactory()); - factory.CacheSpan = TimeSpan.FromSeconds(10); + factory.Providers.Clear(); + factory.Providers.Add("coinaverage", new CachedRateProvider("coinaverage", spy, new MemoryCache(CreateMemoryCache()))); + factory.Providers.Add("bittrex", new CachedRateProvider("bittrex", spy, new MemoryCache(CreateMemoryCache()))); + factory.CacheSpan = TimeSpan.FromSeconds(1); + + var fetcher = new RateFetcher(factory); var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); - Assert.False(fetchedRate.Cached); + spy.AssertHit(); fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); - Assert.True(fetchedRate.Cached); + spy.AssertNotHit(); - Thread.Sleep(11000); + Thread.Sleep(3000); fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); - Assert.False(fetchedRate.Cached); + spy.AssertHit(); fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); - Assert.True(fetchedRate.Cached); + spy.AssertNotHit(); // Should cache at exchange level so this should hit the cache var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult(); - Assert.True(fetchedRate.Cached); - Assert.NotEqual(fetchedRate.BidAsk.Bid, fetchedRate2.BidAsk.Bid); + spy.AssertNotHit(); + Assert.Null(fetchedRate2.BidAsk); + Assert.Equal(RateRulesErrors.RateUnavailable, fetchedRate2.Errors.First()); // Should cache at exchange level this should not hit the cache as it is different exchange RateRules.TryParse("X_X = bittrex(X_X);", out rateRules); fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); - Assert.False(fetchedRate.Cached); + spy.AssertHit(); + factory.Providers.Clear(); + var fetch = new BackgroundFetcherRateProvider(spy); + factory.Providers.Add("bittrex", fetch); + fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + spy.AssertHit(); + fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + spy.AssertNotHit(); + fetch.UpdateIfNecessary().GetAwaiter().GetResult(); + spy.AssertNotHit(); + fetch.RefreshRate = TimeSpan.FromSeconds(1.0); + Thread.Sleep(1020); + fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + spy.AssertNotHit(); + fetch.UpdateIfNecessary().GetAwaiter().GetResult(); + spy.AssertHit(); } private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx) diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 2c9d3985a..ffbd364ae 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -195,12 +195,9 @@ namespace BTCPayServer.Controllers var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray()); logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})"); } - if (rateResult.ExchangeExceptions.Count != 0) + foreach (var ex in rateResult.ExchangeExceptions) { - foreach (var ex in rateResult.ExchangeExceptions) - { - logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})"); - } + logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})"); } }).ToArray()); } diff --git a/BTCPayServer/HostedServices/RatesHostedService.cs b/BTCPayServer/HostedServices/RatesHostedService.cs index 3fedcbe4d..f43b14c0f 100644 --- a/BTCPayServer/HostedServices/RatesHostedService.cs +++ b/BTCPayServer/HostedServices/RatesHostedService.cs @@ -33,16 +33,41 @@ namespace BTCPayServer.HostedServices return new[] { CreateLoopTask(RefreshCoinAverageSupportedExchanges), - CreateLoopTask(RefreshCoinAverageSettings) + CreateLoopTask(RefreshCoinAverageSettings), + CreateLoopTask(RefreshRates) }; } + async Task RefreshRates() + { + + using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(Cancellation)) + { + timeout.CancelAfter(TimeSpan.FromSeconds(20.0)); + try + { + await Task.WhenAll(_RateProviderFactory.Providers + .Select(p => (Fetcher: p.Value as BackgroundFetcherRateProvider, ExchangeName: p.Key)).Where(p => p.Fetcher != null) + .Select(p => p.Fetcher.UpdateIfNecessary().ContinueWith(t => + { + if (t.Result.Exception != null) + { + Logs.PayServer.LogWarning($"Error while contacting {p.ExchangeName}: {t.Result.Exception.Message}"); + } + }, TaskScheduler.Default)) + .ToArray()).WithCancellation(timeout.Token); + } + catch (OperationCanceledException) when (timeout.IsCancellationRequested) + { + } + } + await Task.Delay(TimeSpan.FromSeconds(30), Cancellation); + } async Task RefreshCoinAverageSupportedExchanges() { - await new SynchronizationContextRemover(); var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync(); var exchanges = new CoinAverageExchanges(); - foreach(var item in tickers + foreach (var item in tickers .Exchanges .Select(c => new CoinAverageExchange(c.Name, c.DisplayName))) { @@ -54,7 +79,6 @@ namespace BTCPayServer.HostedServices async Task RefreshCoinAverageSettings() { - await new SynchronizationContextRemover(); var rates = (await _SettingsRepository.GetSettingAsync()) ?? new RatesSetting(); _RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes); if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey)) diff --git a/BTCPayServer/Services/Rates/BackgroundFetcherRateProvider.cs b/BTCPayServer/Services/Rates/BackgroundFetcherRateProvider.cs index f54985173..674ef4f33 100644 --- a/BTCPayServer/Services/Rates/BackgroundFetcherRateProvider.cs +++ b/BTCPayServer/Services/Rates/BackgroundFetcherRateProvider.cs @@ -10,7 +10,7 @@ namespace BTCPayServer.Services.Rates { public class BackgroundFetcherRateProvider : IRateProvider { - class LatestFetch + public class LatestFetch { public ExchangeRates Latest; public DateTimeOffset Timestamp; @@ -41,20 +41,24 @@ namespace BTCPayServer.Services.Rates get { var latest = _Latest; - if (latest == null) + if (latest == null || latest.Exception != null) return DateTimeOffset.UtcNow; return latest.Timestamp + RefreshRate; } } - public async Task UpdateIfNecessary() + public async Task UpdateIfNecessary() { if (NextUpdate <= DateTimeOffset.UtcNow) { - await Fetch(); - return true; + try + { + await Fetch(); + } + catch { } // Exception is inside _Latest + return _Latest; } - return false; + return _Latest; } LatestFetch _Latest; @@ -77,7 +81,14 @@ namespace BTCPayServer.Services.Rates } fetch.Timestamp = DateTimeOffset.UtcNow; _Latest = fetch; + if (fetch.Exception != null) + ExceptionDispatchInfo.Capture(fetch.Exception).Throw(); return fetch; } + + public void InvalidateCache() + { + _Latest = null; + } } } diff --git a/BTCPayServer/Services/Rates/CachedRateProvider.cs b/BTCPayServer/Services/Rates/CachedRateProvider.cs index 6f1e6bbe4..416e34136 100644 --- a/BTCPayServer/Services/Rates/CachedRateProvider.cs +++ b/BTCPayServer/Services/Rates/CachedRateProvider.cs @@ -38,7 +38,7 @@ namespace BTCPayServer.Services.Rates get; set; } = TimeSpan.FromMinutes(1.0); - public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; } + public IMemoryCache MemoryCache { get => _MemoryCache; set => _MemoryCache = value; } public Task GetRatesAsync() { diff --git a/BTCPayServer/Services/Rates/FallbackRateProvider.cs b/BTCPayServer/Services/Rates/FallbackRateProvider.cs index 2e618cb3a..dd7ebc2ae 100644 --- a/BTCPayServer/Services/Rates/FallbackRateProvider.cs +++ b/BTCPayServer/Services/Rates/FallbackRateProvider.cs @@ -9,7 +9,6 @@ namespace BTCPayServer.Services.Rates public class FallbackRateProvider : IRateProvider { IRateProvider[] _Providers; - public bool Used { get; set; } public FallbackRateProvider(IRateProvider[] providers) { if (providers == null) @@ -19,7 +18,6 @@ namespace BTCPayServer.Services.Rates public async Task GetRatesAsync() { - Used = true; foreach (var p in _Providers) { try diff --git a/BTCPayServer/Services/Rates/NullRateProvider.cs b/BTCPayServer/Services/Rates/NullRateProvider.cs new file mode 100644 index 000000000..5cc530203 --- /dev/null +++ b/BTCPayServer/Services/Rates/NullRateProvider.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Rating; + +namespace BTCPayServer.Services.Rates +{ + public class NullRateProvider : IRateProvider + { + private NullRateProvider() + { + + } + private static readonly NullRateProvider _Instance = new NullRateProvider(); + public static NullRateProvider Instance + { + get + { + return _Instance; + } + } + public Task GetRatesAsync() + { + return Task.FromResult(new ExchangeRates()); + } + } +} diff --git a/BTCPayServer/Services/Rates/RateFetcher.cs b/BTCPayServer/Services/Rates/RateFetcher.cs index d224870d9..06f961d9d 100644 --- a/BTCPayServer/Services/Rates/RateFetcher.cs +++ b/BTCPayServer/Services/Rates/RateFetcher.cs @@ -24,7 +24,7 @@ namespace BTCPayServer.Services.Rates public string EvaluatedRule { get; set; } public HashSet Errors { get; set; } public BidAsk BidAsk { get; set; } - public bool Cached { get; internal set; } + public TimeSpan Latency { get; internal set; } } public class RateFetcher @@ -72,13 +72,12 @@ namespace BTCPayServer.Services.Rates private async Task GetRuleValue(List> dependentQueries, RateRule rateRule) { var result = new RateResult(); - result.Cached = true; foreach (var queryAsync in dependentQueries) { var query = await queryAsync; - if (!query.CachedResult) - result.Cached = false; - result.ExchangeExceptions.AddRange(query.Exceptions); + result.Latency = query.Latency; + if (query.Exception != null) + result.ExchangeExceptions.Add(query.Exception); foreach (var rule in query.ExchangeRates) { rateRule.ExchangeRates.SetRate(rule.Exchange, rule.CurrencyPair, rule.BidAsk); diff --git a/BTCPayServer/Services/Rates/RateProviderFactory.cs b/BTCPayServer/Services/Rates/RateProviderFactory.cs index a50ad12a1..b09d9a91a 100644 --- a/BTCPayServer/Services/Rates/RateProviderFactory.cs +++ b/BTCPayServer/Services/Rates/RateProviderFactory.cs @@ -12,11 +12,38 @@ namespace BTCPayServer.Services.Rates { public class RateProviderFactory { + class WrapperRateProvider : IRateProvider + { + private readonly IRateProvider _inner; + public Exception Exception { get; private set; } + public TimeSpan Latency { get; set; } + public WrapperRateProvider(IRateProvider inner) + { + _inner = inner; + } + public Task GetRatesAsync() + { + DateTimeOffset now = DateTimeOffset.UtcNow; + try + { + return _inner.GetRatesAsync(); + } + catch (Exception ex) + { + Exception = ex; + return Task.FromResult(new ExchangeRates()); + } + finally + { + Latency = DateTimeOffset.UtcNow - now; + } + } + } public class QueryRateResult { - public bool CachedResult { get; set; } - public List Exceptions { get; set; } + public TimeSpan Latency { get; set; } public ExchangeRates ExchangeRates { get; set; } + public ExchangeException Exception { get; internal set; } } public RateProviderFactory(IOptions cacheOptions, IHttpClientFactory httpClientFactory, @@ -24,22 +51,12 @@ namespace BTCPayServer.Services.Rates { _httpClientFactory = httpClientFactory; _CoinAverageSettings = coinAverageSettings; - _Cache = new MemoryCache(cacheOptions); _CacheOptions = cacheOptions; // We use 15 min because of limits with free version of bitcoinaverage CacheSpan = TimeSpan.FromMinutes(15.0); InitExchanges(); } - IMemoryCache _Cache; private IOptions _CacheOptions; - - public IMemoryCache Cache - { - get - { - return _Cache; - } - } TimeSpan _CacheSpan; public TimeSpan CacheSpan { @@ -55,12 +72,19 @@ namespace BTCPayServer.Services.Rates } public void InvalidateCache() { - _Cache = new MemoryCache(_CacheOptions); + var cache = new MemoryCache(_CacheOptions); + foreach (var provider in Providers.Select(p => p.Value as CachedRateProvider).Where(p => p != null)) + { + provider.CacheSpan = CacheSpan; + provider.MemoryCache = cache; + } + if (Providers.TryGetValue(CoinAverageRateProvider.CoinAverageName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c) + c.RefreshRate = CacheSpan; } CoinAverageSettings _CoinAverageSettings; private readonly IHttpClientFactory _httpClientFactory; private readonly Dictionary _DirectProviders = new Dictionary(); - public Dictionary DirectProviders + public Dictionary Providers { get { @@ -71,24 +95,50 @@ namespace BTCPayServer.Services.Rates private void InitExchanges() { // We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request - DirectProviders.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true)); - DirectProviders.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true)); - DirectProviders.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), true)); - DirectProviders.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false)); - DirectProviders.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false)); + Providers.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true)); + Providers.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true)); + Providers.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), true)); + Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false)); + Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false)); // Handmade providers - DirectProviders.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/")))); - DirectProviders.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider()); - DirectProviders.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), Authenticator = _CoinAverageSettings }); + Providers.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/")))); + Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider()); + Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), Authenticator = _CoinAverageSettings }); + Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient() }); // Those exchanges make multiple requests when calling GetTickers so we remove them - DirectProviders.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient() }); //DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI())); //DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI())); //DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI())); //DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI())); //DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI())); + + foreach (var provider in Providers.ToArray()) + { + var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]); + prov.RefreshRate = provider.Key == CoinAverageRateProvider.CoinAverageName ? CacheSpan : TimeSpan.FromMinutes(1.0); + Providers[provider.Key] = prov; + } + + var cache = new MemoryCache(_CacheOptions); + foreach (var supportedExchange in GetSupportedExchanges()) + { + if (!Providers.ContainsKey(supportedExchange.Key)) + { + var coinAverage = new CoinAverageRateProvider() + { + Exchange = supportedExchange.Key, + HttpClient = _httpClientFactory?.CreateClient(), + Authenticator = _CoinAverageSettings + }; + var cached = new CachedRateProvider(supportedExchange.Key, coinAverage, cache) + { + CacheSpan = CacheSpan + }; + Providers.Add(supportedExchange.Key, cached); + } + } } public CoinAverageExchanges GetSupportedExchanges() @@ -106,33 +156,18 @@ namespace BTCPayServer.Services.Rates return exchanges; } - public bool UseCoinAverageAsFallback { get; set; } = true; public async Task QueryRates(string exchangeName) { - List providers = new List(); - if (DirectProviders.TryGetValue(exchangeName, out var directProvider)) - providers.Add(directProvider); - if (UseCoinAverageAsFallback && _CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName)) - { - providers.Add(new CoinAverageRateProvider() - { - Exchange = exchangeName, - HttpClient = _httpClientFactory?.CreateClient(), - Authenticator = _CoinAverageSettings - }); - } - var fallback = new FallbackRateProvider(providers.ToArray()); - var cached = new CachedRateProvider(exchangeName, fallback, _Cache) - { - CacheSpan = CacheSpan - }; - var value = await cached.GetRatesAsync(); + Providers.TryGetValue(exchangeName, out var directProvider); + directProvider = directProvider ?? NullRateProvider.Instance; + + var wrapper = new WrapperRateProvider(directProvider); + var value = await wrapper.GetRatesAsync(); return new QueryRateResult() { - CachedResult = !fallback.Used, + Latency = wrapper.Latency, ExchangeRates = value, - Exceptions = fallback.Exceptions - .Select(c => new ExchangeException() { Exception = c, ExchangeName = exchangeName }).ToList() + Exception = wrapper.Exception != null ? new ExchangeException() { Exception = wrapper.Exception, ExchangeName = exchangeName } : null }; } } diff --git a/BTCPayServer/Services/SettingsRepository.cs b/BTCPayServer/Services/SettingsRepository.cs index a6911abc2..4c9b32e58 100644 --- a/BTCPayServer/Services/SettingsRepository.cs +++ b/BTCPayServer/Services/SettingsRepository.cs @@ -83,7 +83,7 @@ namespace BTCPayServer.Services MultiValueDictionary> _Subscriptions = new MultiValueDictionary>(); public async Task WaitSettingsChanged(CancellationToken cancellation) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using (cancellation.Register(() => { try