Poll and cache rates in parallel

This commit is contained in:
nicolas.dorier
2018-08-23 00:24:33 +09:00
parent 87d384dba5
commit f12114f9aa
12 changed files with 301 additions and 131 deletions

View File

@@ -121,35 +121,66 @@ namespace BTCPayServer.Tests
_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();
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<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
}

View File

@@ -14,6 +14,7 @@ using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Logging;
namespace BTCPayServer.Tests
{

View File

@@ -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<decimal> rates = new List<decimal>();
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<StoresController>();
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<StoresController>();
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<RatesViewModel>(Assert.IsType<ViewResult>(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<ExchangeRates> 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)

View File

@@ -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());
}

View File

@@ -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<RatesSetting>()) ?? new RatesSetting();
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))

View File

@@ -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<bool> UpdateIfNecessary()
public async Task<LatestFetch> 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;
}
}
}

View File

@@ -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<ExchangeRates> GetRatesAsync()
{

View File

@@ -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<ExchangeRates> GetRatesAsync()
{
Used = true;
foreach (var p in _Providers)
{
try

View File

@@ -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<ExchangeRates> GetRatesAsync()
{
return Task.FromResult(new ExchangeRates());
}
}
}

View File

@@ -24,7 +24,7 @@ namespace BTCPayServer.Services.Rates
public string EvaluatedRule { get; set; }
public HashSet<RateRulesErrors> 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<RateResult> GetRuleValue(List<Task<QueryRateResult>> 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);

View File

@@ -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<ExchangeRates> 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<ExchangeException> Exceptions { get; set; }
public TimeSpan Latency { get; set; }
public ExchangeRates ExchangeRates { get; set; }
public ExchangeException Exception { get; internal set; }
}
public RateProviderFactory(IOptions<MemoryCacheOptions> 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<MemoryCacheOptions> _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<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
public Dictionary<string, IRateProvider> DirectProviders
public Dictionary<string, IRateProvider> 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<QueryRateResult> QueryRates(string exchangeName)
{
List<IRateProvider> providers = new List<IRateProvider>();
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
};
}
}

View File

@@ -83,7 +83,7 @@ namespace BTCPayServer.Services
MultiValueDictionary<Type, TaskCompletionSource<bool>> _Subscriptions = new MultiValueDictionary<Type, TaskCompletionSource<bool>>();
public async Task WaitSettingsChanged<T>(CancellationToken cancellation)
{
var tcs = new TaskCompletionSource<bool>();
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using (cancellation.Register(() =>
{
try