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
18 changed files with 152 additions and 50 deletions

View File

@@ -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<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()
{
ServiceCollection services = new ServiceCollection();
services.AddHttpClient();
BTCPayServerServices.RegisterRateSources(services);
services.AddRateProvider<SpyContextualRateProvider>();
var o = services.BuildServiceProvider();
return new RateProviderFactory(TestUtils.CreateHttpFactory(), o.GetService<IEnumerable<IRateProvider>>());
}