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 <nicolas.dorier@gmail.com>
This commit is contained in:
Andrew Camilleri 2024-04-30 11:31:15 +02:00 committed by GitHub
parent 4821f77304
commit 6049fa23a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 152 additions and 50 deletions

View file

@ -1,9 +1,26 @@
#nullable enable
using System; using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Rating namespace BTCPayServer.Rating
{ {
public static class Extensions public static class Extensions
{ {
public static Task<PairRate[]> 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) public static decimal RoundToSignificant(this decimal value, int divisibility)
{ {
return RoundToSignificant(value, ref divisibility); return RoundToSignificant(value, ref divisibility);

View file

@ -53,7 +53,7 @@ namespace BTCPayServer.Services.Rates
/// <summary> /// <summary>
/// This class is a decorator which handle caching and pre-emptive query to the underlying rate provider /// This class is a decorator which handle caching and pre-emptive query to the underlying rate provider
/// </summary> /// </summary>
public class BackgroundFetcherRateProvider : IRateProvider public class BackgroundFetcherRateProvider : IContextualRateProvider
{ {
public class LatestFetch public class LatestFetch
{ {
@ -63,6 +63,9 @@ namespace BTCPayServer.Services.Rates
public DateTimeOffset Updated; public DateTimeOffset Updated;
public DateTimeOffset Expiration; public DateTimeOffset Expiration;
public Exception Exception; public Exception Exception;
public IRateContext Context { get; internal set; }
internal PairRate[] GetResult() internal PairRate[] GetResult()
{ {
if (Expiration <= DateTimeOffset.UtcNow) if (Expiration <= DateTimeOffset.UtcNow)
@ -185,7 +188,7 @@ namespace BTCPayServer.Services.Rates
{ {
try try
{ {
await Fetch(cancellationToken); await Fetch(_Latest?.Context, cancellationToken);
} }
catch { } // Exception is inside _Latest catch { } // Exception is inside _Latest
return _Latest; return _Latest;
@ -194,7 +197,11 @@ namespace BTCPayServer.Services.Rates
} }
LatestFetch _Latest; LatestFetch _Latest;
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken) public Task<PairRate[]> GetRatesAsync(IRateContext context, CancellationToken cancellationToken)
{
return GetRatesAsyncCore(context, cancellationToken);
}
async Task<PairRate[]> GetRatesAsyncCore(IRateContext context, CancellationToken cancellationToken)
{ {
LastRequested = DateTimeOffset.UtcNow; LastRequested = DateTimeOffset.UtcNow;
var latest = _Latest; var latest = _Latest;
@ -202,7 +209,11 @@ namespace BTCPayServer.Services.Rates
{ {
latest = null; latest = null;
} }
return (latest ?? (await Fetch(cancellationToken))).GetResult(); return (latest ?? (await Fetch(context, cancellationToken))).GetResult();
}
public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
return GetRatesAsyncCore(null, cancellationToken);
} }
/// <summary> /// <summary>
@ -224,15 +235,16 @@ namespace BTCPayServer.Services.Rates
public RateSourceInfo RateSourceInfo => _Inner.RateSourceInfo; public RateSourceInfo RateSourceInfo => _Inner.RateSourceInfo;
private async Task<LatestFetch> Fetch(CancellationToken cancellationToken) private async Task<LatestFetch> Fetch(IRateContext context, CancellationToken cancellationToken)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var previous = _Latest; var previous = _Latest;
var fetch = new LatestFetch(); var fetch = new LatestFetch();
try try
{ {
var rates = await _Inner.GetRatesAsync(cancellationToken); var rates = await _Inner.GetRatesAsyncWithMaybeContext(context, cancellationToken);
fetch.Latest = rates; fetch.Latest = rates;
fetch.Context = context;
fetch.Updated = DateTimeOffset.UtcNow; fetch.Updated = DateTimeOffset.UtcNow;
fetch.Expiration = fetch.Updated + ValidatyTime; fetch.Expiration = fetch.Updated + ValidatyTime;
fetch.NextRefresh = fetch.Updated + RefreshRate; fetch.NextRefresh = fetch.Updated + RefreshRate;

View file

@ -1,3 +1,5 @@
#nullable enable
using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Rating; using BTCPayServer.Rating;
@ -7,6 +9,33 @@ namespace BTCPayServer.Services.Rates
public interface IRateProvider public interface IRateProvider
{ {
RateSourceInfo RateSourceInfo { get; } RateSourceInfo RateSourceInfo { get; }
/// <summary>
/// Returns rates of the provider
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException">If using this provider isn't supported (For example if a <see cref="IContextualRateProvider"/> requires a context)</exception>
Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken); Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken);
} }
public interface IRateContext { }
public interface IHasStoreIdRateContext : IRateContext
{
string StoreId { get; }
}
public record StoreIdRateContext(string StoreId) : IHasStoreIdRateContext;
/// <summary>
/// A rate provider which know additional context about the rate query.
/// </summary>
public interface IContextualRateProvider : IRateProvider
{
/// <summary>
/// Returns rates of the provider when a context is available
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException">If using this provider isn't getting an expected context</exception>
Task<PairRate[]> GetRatesAsync(IRateContext context, CancellationToken cancellationToken);
}
} }

View file

@ -22,7 +22,7 @@ namespace BTCPayServer.Services.Rates
public BidAsk BidAsk { get; set; } public BidAsk BidAsk { get; set; }
public TimeSpan Latency { get; internal set; } public TimeSpan Latency { get; internal set; }
} }
#nullable enable
public class RateFetcher public class RateFetcher
{ {
private readonly RateProviderFactory _rateProviderFactory; private readonly RateProviderFactory _rateProviderFactory;
@ -34,17 +34,18 @@ namespace BTCPayServer.Services.Rates
public RateProviderFactory RateProviderFactory => _rateProviderFactory; public RateProviderFactory RateProviderFactory => _rateProviderFactory;
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules, CancellationToken cancellationToken) public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules, IRateContext? context, CancellationToken cancellationToken)
{ {
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules, cancellationToken).First().Value; return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules, context, cancellationToken).First().Value;
} }
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules, CancellationToken cancellationToken) public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules, IRateContext? context, CancellationToken cancellationToken)
{ {
ArgumentNullException.ThrowIfNull(rules); ArgumentNullException.ThrowIfNull(rules);
var fetchingRates = new Dictionary<CurrencyPair, Task<RateResult>>(); var fetchingRates = new Dictionary<CurrencyPair, Task<RateResult>>();
var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>(); var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>();
var consolidatedRates = new ExchangeRates();
foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p)))) 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)) 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); fetchingExchanges.Add(requiredExchange.Exchange, fetching);
} }
dependentQueries.Add(fetching); dependentQueries.Add(fetching);
@ -63,7 +64,7 @@ namespace BTCPayServer.Services.Rates
return fetchingRates; return fetchingRates;
} }
public Task<RateResult> FetchRate(RateRule rateRule, CancellationToken cancellationToken) public Task<RateResult> FetchRate(RateRule rateRule, IRateContext? context, CancellationToken cancellationToken)
{ {
ArgumentNullException.ThrowIfNull(rateRule); ArgumentNullException.ThrowIfNull(rateRule);
var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>(); var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>();
@ -72,7 +73,7 @@ namespace BTCPayServer.Services.Rates
{ {
if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching)) 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); fetchingExchanges.Add(requiredExchange.Exchange, fetching);
} }
dependentQueries.Add(fetching); dependentQueries.Add(fetching);

View file

@ -1,3 +1,4 @@
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -15,22 +16,21 @@ namespace BTCPayServer.Services.Rates
{ {
public class RateProviderFactory public class RateProviderFactory
{ {
class WrapperRateProvider : IRateProvider class WrapperRateProvider
{ {
public RateSourceInfo RateSourceInfo => _inner.RateSourceInfo;
private readonly IRateProvider _inner; private readonly IRateProvider _inner;
public Exception Exception { get; private set; } public Exception? Exception { get; private set; }
public TimeSpan Latency { get; set; } public TimeSpan Latency { get; set; }
public WrapperRateProvider(IRateProvider inner) public WrapperRateProvider(IRateProvider inner)
{ {
_inner = inner; _inner = inner;
} }
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken) public async Task<PairRate[]> GetRatesAsync(IRateContext? context, CancellationToken cancellationToken)
{ {
DateTimeOffset now = DateTimeOffset.UtcNow; DateTimeOffset now = DateTimeOffset.UtcNow;
try try
{ {
return await _inner.GetRatesAsync(cancellationToken); return await _inner.GetRatesAsyncWithMaybeContext(context, cancellationToken);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -45,12 +45,19 @@ namespace BTCPayServer.Services.Rates
} }
public class QueryRateResult public class QueryRateResult
{ {
public QueryRateResult(string exchangeName, TimeSpan latency, PairRate[] pairRates)
{
Exchange = exchangeName;
Latency = latency;
PairRates = pairRates;
}
public TimeSpan Latency { get; set; } public TimeSpan Latency { get; set; }
public PairRate[] PairRates { 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 string Exchange { get; internal set; }
} }
public RateProviderFactory(IHttpClientFactory httpClientFactory, IEnumerable<IRateProvider> rateProviders) public RateProviderFactory(IHttpClientFactory httpClientFactory,IEnumerable<IRateProvider> rateProviders)
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
foreach (var prov in rateProviders) foreach (var prov in rateProviders)
@ -65,10 +72,18 @@ namespace BTCPayServer.Services.Rates
{ {
foreach (var provider in Providers.ToArray()) foreach (var provider in Providers.ToArray())
{ {
var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]); var prov = Providers[provider.Key];
prov.RefreshRate = TimeSpan.FromMinutes(1.0); if (prov is IContextualRateProvider)
prov.ValidatyTime = TimeSpan.FromMinutes(5.0); {
Providers[provider.Key] = prov; 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; var rsi = provider.Value.RateSourceInfo;
AvailableRateProviders.Add(new(rsi.Id, rsi.DisplayName, rsi.Url)); AvailableRateProviders.Add(new(rsi.Id, rsi.DisplayName, rsi.Url));
} }
@ -93,18 +108,15 @@ namespace BTCPayServer.Services.Rates
public List<RateSourceInfo> AvailableRateProviders { get; } = new List<RateSourceInfo>(); public List<RateSourceInfo> AvailableRateProviders { get; } = new List<RateSourceInfo>();
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken) public async Task<QueryRateResult> QueryRates(string exchangeName, IRateContext? context = null, CancellationToken cancellationToken = default)
{ {
Providers.TryGetValue(exchangeName, out var directProvider); Providers.TryGetValue(exchangeName, out var directProvider);
directProvider = directProvider ?? NullRateProvider.Instance; directProvider ??= NullRateProvider.Instance;
var wrapper = new WrapperRateProvider(directProvider); var wrapper = new WrapperRateProvider(directProvider);
var value = await wrapper.GetRatesAsync(cancellationToken); var value = await wrapper.GetRatesAsync(context, cancellationToken);
return new QueryRateResult() 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 Exception = wrapper.Exception != null ? new ExchangeException() { Exception = wrapper.Exception, ExchangeName = exchangeName } : null
}; };
} }

View file

@ -1110,6 +1110,19 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
Assert.True(parsers.TryParseWalletFile(electrumText, mainnet, out electrum, out _)); 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] [Fact]
public async Task CheckRatesProvider() public async Task CheckRatesProvider()
{ {
@ -1123,15 +1136,15 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
var fetch = new BackgroundFetcherRateProvider(spy); var fetch = new BackgroundFetcherRateProvider(spy);
fetch.DoNotAutoFetchIfExpired = true; fetch.DoNotAutoFetchIfExpired = true;
factory.Providers.Add("bitpay", fetch); 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(); 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(); spy.AssertNotHit();
await fetch.UpdateIfNecessary(default); await fetch.UpdateIfNecessary(default);
spy.AssertNotHit(); spy.AssertNotHit();
fetch.RefreshRate = TimeSpan.FromSeconds(1.0); fetch.RefreshRate = TimeSpan.FromSeconds(1.0);
Thread.Sleep(1020); 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(); spy.AssertNotHit();
fetch.ValidatyTime = TimeSpan.FromSeconds(1.0); fetch.ValidatyTime = TimeSpan.FromSeconds(1.0);
await fetch.UpdateIfNecessary(default); await fetch.UpdateIfNecessary(default);
@ -1141,11 +1154,27 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
await Assert.ThrowsAsync<InvalidOperationException>(() => fetch.GetRatesAsync(default)); await Assert.ThrowsAsync<InvalidOperationException>(() => 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<PairRate[]> GetRatesAsync(IRateContext context, CancellationToken cancellationToken)
{
Assert.IsAssignableFrom<IHasStoreIdRateContext>(context);
return Task.FromResult(new [] { new PairRate(new CurrencyPair("BTC", "USD"), ExpectedBidAsk) });
}
public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
public static RateProviderFactory CreateBTCPayRateFactory() public static RateProviderFactory CreateBTCPayRateFactory()
{ {
ServiceCollection services = new ServiceCollection(); ServiceCollection services = new ServiceCollection();
services.AddHttpClient(); services.AddHttpClient();
BTCPayServerServices.RegisterRateSources(services); BTCPayServerServices.RegisterRateSources(services);
services.AddRateProvider<SpyContextualRateProvider>();
var o = services.BuildServiceProvider(); var o = services.BuildServiceProvider();
return new RateProviderFactory(TestUtils.CreateHttpFactory(), o.GetService<IEnumerable<IRateProvider>>()); return new RateProviderFactory(TestUtils.CreateHttpFactory(), o.GetService<IEnumerable<IRateProvider>>());
} }

View file

@ -346,7 +346,7 @@ retry:
Assert.True(RateRules.TryParse("X_X=kraken(X_BTC) * kraken(BTC_X)", out var rule)); 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" }) 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.NotNull(result.BidAsk);
Assert.Empty(result.Errors); Assert.Empty(result.Errors);
} }
@ -365,7 +365,7 @@ retry:
b.DefaultCurrency = k.Key; b.DefaultCurrency = k.Key;
var rules = b.GetDefaultRateRules(provider); var rules = b.GetDefaultRateRules(provider);
var pairs = new[] { CurrencyPair.Parse($"BTC_{k.Key}") }.ToHashSet(); 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<RateResult> value) in result) foreach ((CurrencyPair key, Task<RateResult> value) in result)
{ {
TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}"); TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}");
@ -410,7 +410,7 @@ retry:
} }
var rules = new StoreBlob().GetDefaultRateRules(provider); 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<RateResult> value) in result) foreach ((CurrencyPair key, Task<RateResult> value) in result)
{ {
var rateResult = await value; var rateResult = await value;

View file

@ -73,7 +73,7 @@ namespace BTCPayServer.Components.WalletNav
if (defaultCurrency != network.CryptoCode) if (defaultCurrency != network.CryptoCode)
{ {
var rule = store.GetStoreBlob().GetRateRules(_networkProvider)?.GetRuleFor(new Rating.CurrencyPair(network.CryptoCode, defaultCurrency)); 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) if (bid is decimal b)
{ {
var currencyData = _currencies.GetCurrencyData(defaultCurrency, true); var currencyData = _currencies.GetCurrencyData(defaultCurrency, true);

View file

@ -137,7 +137,7 @@ namespace BTCPayServer.Controllers
pairs.Add(pair); 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()); await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
return Json(pairs return Json(pairs
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid)) .Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))

View file

@ -414,8 +414,9 @@ namespace BTCPayServer.Controllers.Greenfield
var paidCurrency = Math.Round(cryptoPaid * paymentPrompt.Rate, cdCurrency.Divisibility); var paidCurrency = Math.Round(cryptoPaid * paymentPrompt.Rate, cdCurrency.Divisibility);
var rateResult = await _rateProvider.FetchRate( var rateResult = await _rateProvider.FetchRate(
new CurrencyPair(paymentPrompt.Currency, invoice.Currency), new CurrencyPair(paymentPrompt.Currency, invoice.Currency),
store.GetStoreBlob().GetRateRules(_networkProvider), store.GetStoreBlob().GetRateRules(_networkProvider), new StoreIdRateContext(storeId),
cancellationToken
cancellationToken
); );
var paidAmount = cryptoPaid.RoundToSignificant(paymentPrompt.Divisibility); var paidAmount = cryptoPaid.RoundToSignificant(paymentPrompt.Divisibility);
var createPullPayment = new CreatePullPayment var createPullPayment = new CreatePullPayment

View file

@ -120,7 +120,7 @@ namespace BTCPayServer.Controllers.GreenField
var rules = blob.GetRateRules(_btcPayNetworkProvider); 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); await Task.WhenAll(rateTasks.Values);
var result = new List<StoreRateResult>(); var result = new List<StoreRateResult>();
foreach (var rateTask in rateTasks) foreach (var rateTask in rateTasks)

View file

@ -64,7 +64,7 @@ namespace BTCPayServer.Controllers.GreenField
var rules = blob.GetRateRules(_btcPayNetworkProvider); 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); await Task.WhenAll(rateTasks.Values);
var result = new List<StoreRateResult>(); var result = new List<StoreRateResult>();
foreach (var rateTask in rateTasks) foreach (var rateTask in rateTasks)

View file

@ -391,7 +391,7 @@ namespace BTCPayServer.Controllers
model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodCurrency); model.RateThenText = _displayFormatter.Currency(model.CryptoAmountThen, paymentMethodCurrency);
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider); rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
rateResult = await _RateProvider.FetchRate( rateResult = await _RateProvider.FetchRate(
new CurrencyPair(paymentMethodCurrency, invoice.Currency), rules, new CurrencyPair(paymentMethodCurrency, invoice.Currency), rules, new StoreIdRateContext(store.Id),
cancellationToken); cancellationToken);
//TODO: What if fetching rate failed? //TODO: What if fetching rate failed?
if (rateResult.BidAsk is null) if (rateResult.BidAsk is null)
@ -500,7 +500,7 @@ namespace BTCPayServer.Controllers
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider); rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
rateResult = await _RateProvider.FetchRate( rateResult = await _RateProvider.FetchRate(
new CurrencyPair(paymentMethodCurrency, model.CustomCurrency), rules, new CurrencyPair(paymentMethodCurrency, model.CustomCurrency), rules, new StoreIdRateContext(store.Id),
cancellationToken); cancellationToken);
//TODO: What if fetching rate failed? //TODO: What if fetching rate failed?

View file

@ -11,6 +11,7 @@ using BTCPayServer.Client;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -124,7 +125,7 @@ public partial class UIStoresController
pairs.Add(currencyPair); 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<RatesViewModel.TestResultViewModel>(); var testResults = new List<RatesViewModel.TestResultViewModel>();
foreach (var fetch in fetchs) foreach (var fetch in fetchs)
{ {

View file

@ -531,7 +531,7 @@ namespace BTCPayServer.Controllers
try try
{ {
cts.CancelAfter(TimeSpan.FromSeconds(5)); 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); .WithCancellation(cts.Token);
if (result.BidAsk != null) if (result.BidAsk != null)
{ {

View file

@ -412,7 +412,7 @@ namespace BTCPayServer.HostedServices
throw new FormatException("Invalid RateRule"); throw new FormatException("Invalid RateRule");
} }
return _rateFetcher.FetchRate(rule, cancellationToken); return _rateFetcher.FetchRate(rule, new StoreIdRateContext(payout.StoreDataId), cancellationToken);
} }
public Task<PayoutApproval.ApprovalResult> Approve(PayoutApproval approval) public Task<PayoutApproval.ApprovalResult> Approve(PayoutApproval approval)

View file

@ -75,7 +75,7 @@ namespace BTCPayServer.HostedServices
await Task.WhenAll(usedProviders await Task.WhenAll(usedProviders
.Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token).ContinueWith(t => .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}"); Logs.PayServer.LogWarning($"Error while contacting exchange {p.ExchangeName}: {t.Result.Exception.Message}");
} }

View file

@ -163,7 +163,7 @@ namespace BTCPayServer.Payments
public async Task FetchingRates(RateFetcher rateFetcher, RateRules rateRules, CancellationToken cancellationToken) public async Task FetchingRates(RateFetcher rateFetcher, RateRules rateRules, CancellationToken cancellationToken)
{ {
var currencyPairsToFetch = GetCurrenciesToFetch(); var currencyPairsToFetch = GetCurrenciesToFetch();
var fetchingRates = rateFetcher.FetchRates(currencyPairsToFetch, rateRules, cancellationToken); var fetchingRates = rateFetcher.FetchRates(currencyPairsToFetch, rateRules, new StoreIdRateContext(InvoiceEntity.StoreId), cancellationToken);
HashSet<CurrencyPair> failedRates = new HashSet<CurrencyPair>(); HashSet<CurrencyPair> failedRates = new HashSet<CurrencyPair>();
foreach (var fetching in fetchingRates) foreach (var fetching in fetchingRates)
{ {