From 6049fa23a7adc0a19edef14611656bec6df5369c Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Tue, 30 Apr 2024 11:31:15 +0200 Subject: [PATCH] Support pluginable rate providers (#5777) * 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 --- BTCPayServer.Rating/Extensions.cs | 17 +++++++ .../BackgroundFetcherRateProvider.cs | 24 +++++++--- .../Providers/IRateProvider.cs | 29 +++++++++++ BTCPayServer.Rating/Services/RateFetcher.cs | 15 +++--- .../Services/RateProviderFactory.cs | 48 ++++++++++++------- BTCPayServer.Tests/FastTests.cs | 35 ++++++++++++-- BTCPayServer.Tests/ThirdPartyTests.cs | 6 +-- .../Components/WalletNav/WalletNav.cs | 2 +- .../Controllers/BitpayRateController.cs | 2 +- .../GreenField/GreenfieldInvoiceController.cs | 5 +- ...nfieldStoreRatesConfigurationController.cs | 2 +- .../GreenfieldStoreRatesController.cs | 2 +- .../Controllers/UIInvoiceController.UI.cs | 4 +- .../Controllers/UIStoresController.Rates.cs | 3 +- .../Controllers/UIWalletsController.cs | 2 +- .../PullPaymentHostedService.cs | 2 +- .../HostedServices/RatesHostedService.cs | 2 +- .../Payments/IPaymentMethodHandler.cs | 2 +- 18 files changed, 152 insertions(+), 50 deletions(-) diff --git a/BTCPayServer.Rating/Extensions.cs b/BTCPayServer.Rating/Extensions.cs index 9d1af928a..727b975ac 100644 --- a/BTCPayServer.Rating/Extensions.cs +++ b/BTCPayServer.Rating/Extensions.cs @@ -1,9 +1,26 @@ +#nullable enable using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Services.Rates; namespace BTCPayServer.Rating { public static class Extensions { + public static Task GetRatesAsyncWithMaybeContext(this IRateProvider rateProvider, IRateContext? context, CancellationToken cancellationToken) + { + if (rateProvider is IContextualRateProvider contextualRateProvider && context is { }) + { + return contextualRateProvider.GetRatesAsync(context, cancellationToken); + } + else + { + return rateProvider.GetRatesAsync(cancellationToken); + } + } public static decimal RoundToSignificant(this decimal value, int divisibility) { return RoundToSignificant(value, ref divisibility); diff --git a/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs b/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs index cba9865b2..91058d9ad 100644 --- a/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs +++ b/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs @@ -53,7 +53,7 @@ namespace BTCPayServer.Services.Rates /// /// This class is a decorator which handle caching and pre-emptive query to the underlying rate provider /// - public class BackgroundFetcherRateProvider : IRateProvider + public class BackgroundFetcherRateProvider : IContextualRateProvider { public class LatestFetch { @@ -63,6 +63,9 @@ namespace BTCPayServer.Services.Rates public DateTimeOffset Updated; public DateTimeOffset Expiration; public Exception Exception; + + public IRateContext Context { get; internal set; } + internal PairRate[] GetResult() { if (Expiration <= DateTimeOffset.UtcNow) @@ -185,7 +188,7 @@ namespace BTCPayServer.Services.Rates { try { - await Fetch(cancellationToken); + await Fetch(_Latest?.Context, cancellationToken); } catch { } // Exception is inside _Latest return _Latest; @@ -194,7 +197,11 @@ namespace BTCPayServer.Services.Rates } LatestFetch _Latest; - public async Task GetRatesAsync(CancellationToken cancellationToken) + public Task GetRatesAsync(IRateContext context, CancellationToken cancellationToken) + { + return GetRatesAsyncCore(context, cancellationToken); + } + async Task GetRatesAsyncCore(IRateContext context, CancellationToken cancellationToken) { LastRequested = DateTimeOffset.UtcNow; var latest = _Latest; @@ -202,7 +209,11 @@ namespace BTCPayServer.Services.Rates { latest = null; } - return (latest ?? (await Fetch(cancellationToken))).GetResult(); + return (latest ?? (await Fetch(context, cancellationToken))).GetResult(); + } + public Task GetRatesAsync(CancellationToken cancellationToken) + { + return GetRatesAsyncCore(null, cancellationToken); } /// @@ -224,15 +235,16 @@ namespace BTCPayServer.Services.Rates public RateSourceInfo RateSourceInfo => _Inner.RateSourceInfo; - private async Task Fetch(CancellationToken cancellationToken) + private async Task Fetch(IRateContext context, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var previous = _Latest; var fetch = new LatestFetch(); try { - var rates = await _Inner.GetRatesAsync(cancellationToken); + var rates = await _Inner.GetRatesAsyncWithMaybeContext(context, cancellationToken); fetch.Latest = rates; + fetch.Context = context; fetch.Updated = DateTimeOffset.UtcNow; fetch.Expiration = fetch.Updated + ValidatyTime; fetch.NextRefresh = fetch.Updated + RefreshRate; diff --git a/BTCPayServer.Rating/Providers/IRateProvider.cs b/BTCPayServer.Rating/Providers/IRateProvider.cs index 670cd2a34..f93f8d1bd 100644 --- a/BTCPayServer.Rating/Providers/IRateProvider.cs +++ b/BTCPayServer.Rating/Providers/IRateProvider.cs @@ -1,3 +1,5 @@ +#nullable enable +using System; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Rating; @@ -7,6 +9,33 @@ namespace BTCPayServer.Services.Rates public interface IRateProvider { RateSourceInfo RateSourceInfo { get; } + /// + /// Returns rates of the provider + /// + /// + /// + /// If using this provider isn't supported (For example if a requires a context) Task GetRatesAsync(CancellationToken cancellationToken); } + + public interface IRateContext { } + public interface IHasStoreIdRateContext : IRateContext + { + string StoreId { get; } + } + public record StoreIdRateContext(string StoreId) : IHasStoreIdRateContext; + + /// + /// A rate provider which know additional context about the rate query. + /// + public interface IContextualRateProvider : IRateProvider + { + /// + /// Returns rates of the provider when a context is available + /// + /// + /// + /// If using this provider isn't getting an expected context + Task GetRatesAsync(IRateContext context, CancellationToken cancellationToken); + } } diff --git a/BTCPayServer.Rating/Services/RateFetcher.cs b/BTCPayServer.Rating/Services/RateFetcher.cs index df57cba08..19aba5a91 100644 --- a/BTCPayServer.Rating/Services/RateFetcher.cs +++ b/BTCPayServer.Rating/Services/RateFetcher.cs @@ -22,7 +22,7 @@ namespace BTCPayServer.Services.Rates public BidAsk BidAsk { get; set; } public TimeSpan Latency { get; internal set; } } - +#nullable enable public class RateFetcher { private readonly RateProviderFactory _rateProviderFactory; @@ -34,17 +34,18 @@ namespace BTCPayServer.Services.Rates public RateProviderFactory RateProviderFactory => _rateProviderFactory; - public async Task FetchRate(CurrencyPair pair, RateRules rules, CancellationToken cancellationToken) + public async Task FetchRate(CurrencyPair pair, RateRules rules, IRateContext? context, CancellationToken cancellationToken) { - return await FetchRates(new HashSet(new[] { pair }), rules, cancellationToken).First().Value; + return await FetchRates(new HashSet(new[] { pair }), rules, context, cancellationToken).First().Value; } - public Dictionary> FetchRates(HashSet pairs, RateRules rules, CancellationToken cancellationToken) + public Dictionary> FetchRates(HashSet pairs, RateRules rules, IRateContext? context, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(rules); var fetchingRates = new Dictionary>(); var fetchingExchanges = new Dictionary>(); + var consolidatedRates = new ExchangeRates(); foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p)))) { @@ -53,7 +54,7 @@ namespace BTCPayServer.Services.Rates { if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching)) { - fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange, cancellationToken); + fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange, context, cancellationToken); fetchingExchanges.Add(requiredExchange.Exchange, fetching); } dependentQueries.Add(fetching); @@ -63,7 +64,7 @@ namespace BTCPayServer.Services.Rates return fetchingRates; } - public Task FetchRate(RateRule rateRule, CancellationToken cancellationToken) + public Task FetchRate(RateRule rateRule, IRateContext? context, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(rateRule); var fetchingExchanges = new Dictionary>(); @@ -72,7 +73,7 @@ namespace BTCPayServer.Services.Rates { if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching)) { - fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange, cancellationToken); + fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange, context, cancellationToken); fetchingExchanges.Add(requiredExchange.Exchange, fetching); } dependentQueries.Add(fetching); diff --git a/BTCPayServer.Rating/Services/RateProviderFactory.cs b/BTCPayServer.Rating/Services/RateProviderFactory.cs index e69089c9a..736e95bc8 100644 --- a/BTCPayServer.Rating/Services/RateProviderFactory.cs +++ b/BTCPayServer.Rating/Services/RateProviderFactory.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Linq; @@ -15,22 +16,21 @@ namespace BTCPayServer.Services.Rates { public class RateProviderFactory { - class WrapperRateProvider : IRateProvider + class WrapperRateProvider { - public RateSourceInfo RateSourceInfo => _inner.RateSourceInfo; private readonly IRateProvider _inner; - public Exception Exception { get; private set; } + public Exception? Exception { get; private set; } public TimeSpan Latency { get; set; } public WrapperRateProvider(IRateProvider inner) { _inner = inner; } - public async Task GetRatesAsync(CancellationToken cancellationToken) + public async Task GetRatesAsync(IRateContext? context, CancellationToken cancellationToken) { DateTimeOffset now = DateTimeOffset.UtcNow; try { - return await _inner.GetRatesAsync(cancellationToken); + return await _inner.GetRatesAsyncWithMaybeContext(context, cancellationToken); } catch (Exception ex) { @@ -45,12 +45,19 @@ namespace BTCPayServer.Services.Rates } public class QueryRateResult { + public QueryRateResult(string exchangeName, TimeSpan latency, PairRate[] pairRates) + { + Exchange = exchangeName; + Latency = latency; + PairRates = pairRates; + } + public TimeSpan Latency { get; set; } public PairRate[] PairRates { get; set; } - public ExchangeException Exception { get; internal set; } + public ExchangeException? Exception { get; internal set; } public string Exchange { get; internal set; } } - public RateProviderFactory(IHttpClientFactory httpClientFactory, IEnumerable rateProviders) + public RateProviderFactory(IHttpClientFactory httpClientFactory,IEnumerable rateProviders) { _httpClientFactory = httpClientFactory; foreach (var prov in rateProviders) @@ -65,10 +72,18 @@ namespace BTCPayServer.Services.Rates { foreach (var provider in Providers.ToArray()) { - var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]); - prov.RefreshRate = TimeSpan.FromMinutes(1.0); - prov.ValidatyTime = TimeSpan.FromMinutes(5.0); - Providers[provider.Key] = prov; + var prov = Providers[provider.Key]; + if (prov is IContextualRateProvider) + { + Providers[provider.Key] = prov; + } + else + { + var prov2 = new BackgroundFetcherRateProvider(prov); + prov2.RefreshRate = TimeSpan.FromMinutes(1.0); + prov2.ValidatyTime = TimeSpan.FromMinutes(5.0); + Providers[provider.Key] = prov2; + } var rsi = provider.Value.RateSourceInfo; AvailableRateProviders.Add(new(rsi.Id, rsi.DisplayName, rsi.Url)); } @@ -93,18 +108,15 @@ namespace BTCPayServer.Services.Rates public List AvailableRateProviders { get; } = new List(); - public async Task QueryRates(string exchangeName, CancellationToken cancellationToken) + public async Task QueryRates(string exchangeName, IRateContext? context = null, CancellationToken cancellationToken = default) { Providers.TryGetValue(exchangeName, out var directProvider); - directProvider = directProvider ?? NullRateProvider.Instance; + directProvider ??= NullRateProvider.Instance; var wrapper = new WrapperRateProvider(directProvider); - var value = await wrapper.GetRatesAsync(cancellationToken); - return new QueryRateResult() + var value = await wrapper.GetRatesAsync(context, cancellationToken); + return new QueryRateResult(exchangeName, wrapper.Latency, value) { - Exchange = exchangeName, - Latency = wrapper.Latency, - PairRates = value, Exception = wrapper.Exception != null ? new ExchangeException() { Exception = wrapper.Exception, ExchangeName = exchangeName } : null }; } diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index 4536d270d..4ee95fc44 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -1110,6 +1110,19 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku Assert.True(parsers.TryParseWalletFile(electrumText, mainnet, out electrum, out _)); } + [Fact] + public async Task CanPassContextToRateProviders() + { + var factory = CreateBTCPayRateFactory(); + var fetcher = new RateFetcher(factory); + Assert.True(RateRules.TryParse("X_X=spy(X_X)", out var rule)); + var result = await fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rule, null, default); + Assert.Single(result.Errors); + result = await fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rule, new StoreIdRateContext("hello"), default); + Assert.Empty(result.Errors); + Assert.Equal(SpyContextualRateProvider.ExpectedBidAsk, result.BidAsk); + } + [Fact] public async Task CheckRatesProvider() { @@ -1123,15 +1136,15 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku var fetch = new BackgroundFetcherRateProvider(spy); fetch.DoNotAutoFetchIfExpired = true; factory.Providers.Add("bitpay", fetch); - var fetchedRate = await fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default); + var fetchedRate = await fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, null, default); spy.AssertHit(); - fetchedRate = await fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default); + fetchedRate = await fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, null, default); spy.AssertNotHit(); await fetch.UpdateIfNecessary(default); spy.AssertNotHit(); fetch.RefreshRate = TimeSpan.FromSeconds(1.0); Thread.Sleep(1020); - fetchedRate = await fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default); + fetchedRate = await fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, null, default); spy.AssertNotHit(); fetch.ValidatyTime = TimeSpan.FromSeconds(1.0); await fetch.UpdateIfNecessary(default); @@ -1141,11 +1154,27 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku await Assert.ThrowsAsync(() => fetch.GetRatesAsync(default)); } + class SpyContextualRateProvider : IContextualRateProvider + { + public static BidAsk ExpectedBidAsk = new BidAsk(1.12345m); + public RateSourceInfo RateSourceInfo => new RateSourceInfo("spy", "hello world", "abc..."); + public Task GetRatesAsync(IRateContext context, CancellationToken cancellationToken) + { + Assert.IsAssignableFrom(context); + return Task.FromResult(new [] { new PairRate(new CurrencyPair("BTC", "USD"), ExpectedBidAsk) }); + } + + public Task GetRatesAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } public static RateProviderFactory CreateBTCPayRateFactory() { ServiceCollection services = new ServiceCollection(); services.AddHttpClient(); BTCPayServerServices.RegisterRateSources(services); + services.AddRateProvider(); var o = services.BuildServiceProvider(); return new RateProviderFactory(TestUtils.CreateHttpFactory(), o.GetService>()); } diff --git a/BTCPayServer.Tests/ThirdPartyTests.cs b/BTCPayServer.Tests/ThirdPartyTests.cs index a1ce68c9d..aa632276b 100644 --- a/BTCPayServer.Tests/ThirdPartyTests.cs +++ b/BTCPayServer.Tests/ThirdPartyTests.cs @@ -346,7 +346,7 @@ retry: Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule)); foreach (var pair in new[] { "DOGE_USD", "DOGE_CAD", "DASH_CAD", "DASH_USD", "DASH_EUR" }) { - var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, default).GetAwaiter().GetResult(); + var result = fetcher.FetchRate(CurrencyPair.Parse(pair), rule, null, default).GetAwaiter().GetResult(); Assert.NotNull(result.BidAsk); Assert.Empty(result.Errors); } @@ -365,7 +365,7 @@ retry: b.DefaultCurrency = k.Key; var rules = b.GetDefaultRateRules(provider); var pairs = new[] { CurrencyPair.Parse($"BTC_{k.Key}") }.ToHashSet(); - var result = fetcher.FetchRates(pairs, rules, default); + var result = fetcher.FetchRates(pairs, rules, null, default); foreach ((CurrencyPair key, Task value) in result) { TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}"); @@ -410,7 +410,7 @@ retry: } var rules = new StoreBlob().GetDefaultRateRules(provider); - var result = fetcher.FetchRates(pairs, rules, cts.Token); + var result = fetcher.FetchRates(pairs, rules, null, cts.Token); foreach ((CurrencyPair key, Task value) in result) { var rateResult = await value; diff --git a/BTCPayServer/Components/WalletNav/WalletNav.cs b/BTCPayServer/Components/WalletNav/WalletNav.cs index 62d5e3077..1eb61e8ef 100644 --- a/BTCPayServer/Components/WalletNav/WalletNav.cs +++ b/BTCPayServer/Components/WalletNav/WalletNav.cs @@ -73,7 +73,7 @@ namespace BTCPayServer.Components.WalletNav if (defaultCurrency != network.CryptoCode) { var rule = store.GetStoreBlob().GetRateRules(_networkProvider)?.GetRuleFor(new Rating.CurrencyPair(network.CryptoCode, defaultCurrency)); - var bid = rule is null ? null : (await _rateFetcher.FetchRate(rule, HttpContext.RequestAborted)).BidAsk?.Bid; + var bid = rule is null ? null : (await _rateFetcher.FetchRate(rule, new StoreIdRateContext(walletId.StoreId), HttpContext.RequestAborted)).BidAsk?.Bid; if (bid is decimal b) { var currencyData = _currencies.GetCurrencyData(defaultCurrency, true); diff --git a/BTCPayServer/Controllers/BitpayRateController.cs b/BTCPayServer/Controllers/BitpayRateController.cs index 4e2c3958e..7a25c081a 100644 --- a/BTCPayServer/Controllers/BitpayRateController.cs +++ b/BTCPayServer/Controllers/BitpayRateController.cs @@ -137,7 +137,7 @@ namespace BTCPayServer.Controllers pairs.Add(pair); } - var fetching = _rateProviderFactory.FetchRates(pairs, rules, cancellationToken); + var fetching = _rateProviderFactory.FetchRates(pairs, rules, new StoreIdRateContext(storeId), cancellationToken); await Task.WhenAll(fetching.Select(f => f.Value).ToArray()); return Json(pairs .Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid)) diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs index eaba47900..496366b03 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs @@ -414,8 +414,9 @@ namespace BTCPayServer.Controllers.Greenfield var paidCurrency = Math.Round(cryptoPaid * paymentPrompt.Rate, cdCurrency.Divisibility); var rateResult = await _rateProvider.FetchRate( new CurrencyPair(paymentPrompt.Currency, invoice.Currency), - store.GetStoreBlob().GetRateRules(_networkProvider), - cancellationToken + store.GetStoreBlob().GetRateRules(_networkProvider), new StoreIdRateContext(storeId), + + cancellationToken ); var paidAmount = cryptoPaid.RoundToSignificant(paymentPrompt.Divisibility); var createPullPayment = new CreatePullPayment diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.cs index b4b6ec887..9fac97550 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.cs @@ -120,7 +120,7 @@ namespace BTCPayServer.Controllers.GreenField var rules = blob.GetRateRules(_btcPayNetworkProvider); - var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, CancellationToken.None); + var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, new StoreIdRateContext(data.Id), CancellationToken.None); await Task.WhenAll(rateTasks.Values); var result = new List(); foreach (var rateTask in rateTasks) diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesController.cs index 734f3f436..b0201101c 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesController.cs @@ -64,7 +64,7 @@ namespace BTCPayServer.Controllers.GreenField var rules = blob.GetRateRules(_btcPayNetworkProvider); - var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, CancellationToken.None); + var rateTasks = _rateProviderFactory.FetchRates(parsedCurrencyPairs, rules, new StoreIdRateContext(data.Id), CancellationToken.None); await Task.WhenAll(rateTasks.Values); var result = new List(); foreach (var rateTask in rateTasks) diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs index ad2b29afe..5c365b2c7 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs @@ -391,7 +391,7 @@ namespace BTCPayServer.Controllers model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodCurrency); rules = store.GetStoreBlob().GetRateRules(_NetworkProvider); rateResult = await _RateProvider.FetchRate( - new CurrencyPair(paymentMethodCurrency, invoice.Currency), rules, + new CurrencyPair(paymentMethodCurrency, invoice.Currency), rules, new StoreIdRateContext(store.Id), cancellationToken); //TODO: What if fetching rate failed? if (rateResult.BidAsk is null) @@ -500,7 +500,7 @@ namespace BTCPayServer.Controllers rules = store.GetStoreBlob().GetRateRules(_NetworkProvider); rateResult = await _RateProvider.FetchRate( - new CurrencyPair(paymentMethodCurrency, model.CustomCurrency), rules, + new CurrencyPair(paymentMethodCurrency, model.CustomCurrency), rules, new StoreIdRateContext(store.Id), cancellationToken); //TODO: What if fetching rate failed? diff --git a/BTCPayServer/Controllers/UIStoresController.Rates.cs b/BTCPayServer/Controllers/UIStoresController.Rates.cs index c1b12cbf4..21074acba 100644 --- a/BTCPayServer/Controllers/UIStoresController.Rates.cs +++ b/BTCPayServer/Controllers/UIStoresController.Rates.cs @@ -11,6 +11,7 @@ using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Rating; +using BTCPayServer.Services.Rates; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -124,7 +125,7 @@ public partial class UIStoresController pairs.Add(currencyPair); } - var fetchs = _rateFactory.FetchRates(pairs.ToHashSet(), rules, cancellationToken); + var fetchs = _rateFactory.FetchRates(pairs.ToHashSet(), rules, new StoreIdRateContext(model.StoreId), cancellationToken); var testResults = new List(); foreach (var fetch in fetchs) { diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs index 55769a03a..4bb60a3a0 100644 --- a/BTCPayServer/Controllers/UIWalletsController.cs +++ b/BTCPayServer/Controllers/UIWalletsController.cs @@ -531,7 +531,7 @@ namespace BTCPayServer.Controllers try { cts.CancelAfter(TimeSpan.FromSeconds(5)); - var result = await RateFetcher.FetchRate(currencyPair, rateRules, cts.Token) + var result = await RateFetcher.FetchRate(currencyPair, rateRules, new StoreIdRateContext(walletId.StoreId), cts.Token) .WithCancellation(cts.Token); if (result.BidAsk != null) { diff --git a/BTCPayServer/HostedServices/PullPaymentHostedService.cs b/BTCPayServer/HostedServices/PullPaymentHostedService.cs index cd954abcc..5fd6ff8d8 100644 --- a/BTCPayServer/HostedServices/PullPaymentHostedService.cs +++ b/BTCPayServer/HostedServices/PullPaymentHostedService.cs @@ -412,7 +412,7 @@ namespace BTCPayServer.HostedServices throw new FormatException("Invalid RateRule"); } - return _rateFetcher.FetchRate(rule, cancellationToken); + return _rateFetcher.FetchRate(rule, new StoreIdRateContext(payout.StoreDataId), cancellationToken); } public Task Approve(PayoutApproval approval) diff --git a/BTCPayServer/HostedServices/RatesHostedService.cs b/BTCPayServer/HostedServices/RatesHostedService.cs index 70ad24e5a..8fba4ecc7 100644 --- a/BTCPayServer/HostedServices/RatesHostedService.cs +++ b/BTCPayServer/HostedServices/RatesHostedService.cs @@ -75,7 +75,7 @@ namespace BTCPayServer.HostedServices await Task.WhenAll(usedProviders .Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token).ContinueWith(t => { - if (t.Result.Exception != null) + if (t.Result.Exception != null && t.Result.Exception is not NotSupportedException) { Logs.PayServer.LogWarning($"Error while contacting exchange {p.ExchangeName}: {t.Result.Exception.Message}"); } diff --git a/BTCPayServer/Payments/IPaymentMethodHandler.cs b/BTCPayServer/Payments/IPaymentMethodHandler.cs index b4b929007..c5baaf3ca 100644 --- a/BTCPayServer/Payments/IPaymentMethodHandler.cs +++ b/BTCPayServer/Payments/IPaymentMethodHandler.cs @@ -163,7 +163,7 @@ namespace BTCPayServer.Payments public async Task FetchingRates(RateFetcher rateFetcher, RateRules rateRules, CancellationToken cancellationToken) { var currencyPairsToFetch = GetCurrenciesToFetch(); - var fetchingRates = rateFetcher.FetchRates(currencyPairsToFetch, rateRules, cancellationToken); + var fetchingRates = rateFetcher.FetchRates(currencyPairsToFetch, rateRules, new StoreIdRateContext(InvoiceEntity.StoreId), cancellationToken); HashSet failedRates = new HashSet(); foreach (var fetching in fetchingRates) {