using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using BTCPayServer.Rating; using ExchangeSharp; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; namespace BTCPayServer.Services.Rates { public class ExchangeException { public Exception Exception { get; set; } public string ExchangeName { get; set; } } public class RateResult { public List ExchangeExceptions { get; set; } = new List(); public string Rule { get; set; } public string EvaluatedRule { get; set; } public HashSet Errors { get; set; } public decimal? Value { get; set; } public bool Cached { get; internal set; } } public class BTCPayRateProviderFactory { class QueryRateResult { public bool CachedResult { get; set; } public List Exceptions { get; set; } public ExchangeRates ExchangeRates { get; set; } } IMemoryCache _Cache; private IOptions _CacheOptions; public IMemoryCache Cache { get { return _Cache; } } CoinAverageSettings _CoinAverageSettings; public BTCPayRateProviderFactory(IOptions cacheOptions, BTCPayNetworkProvider btcpayNetworkProvider, CoinAverageSettings coinAverageSettings) { if (cacheOptions == null) throw new ArgumentNullException(nameof(cacheOptions)); _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); this.btcpayNetworkProvider = btcpayNetworkProvider; InitExchanges(); } public bool UseCoinAverageAsFallback { get; set; } = true; 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(), false)); DirectProviders.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false)); DirectProviders.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, Authenticator = _CoinAverageSettings }); // Those exchanges make multiple requests when calling GetTickers so we remove them //DirectProviders.Add("kraken", new ExchangeSharpRateProvider("kraken", new ExchangeKrakenAPI(), true)); //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())); } public CoinAverageExchanges GetSupportedExchanges() { CoinAverageExchanges exchanges = new CoinAverageExchanges(); foreach (var exchange in _CoinAverageSettings.AvailableExchanges) { exchanges.Add(exchange.Value); } // Add other exchanges supported here exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average")); exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia")); return exchanges; } private readonly Dictionary _DirectProviders = new Dictionary(); public Dictionary DirectProviders { get { return _DirectProviders; } } BTCPayNetworkProvider btcpayNetworkProvider; TimeSpan _CacheSpan; public TimeSpan CacheSpan { get { return _CacheSpan; } set { _CacheSpan = value; InvalidateCache(); } } public void InvalidateCache() { _Cache = new MemoryCache(_CacheOptions); } public async Task FetchRate(CurrencyPair pair, RateRules rules) { return await FetchRates(new HashSet(new[] { pair }), rules).First().Value; } public Dictionary> FetchRates(HashSet pairs, RateRules rules) { if (rules == null) throw new ArgumentNullException(nameof(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)))) { var dependentQueries = new List>(); foreach (var requiredExchange in i.RateRule.ExchangeRates) { if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching)) { fetching = QueryRates(requiredExchange.Exchange); fetchingExchanges.Add(requiredExchange.Exchange, fetching); } dependentQueries.Add(fetching); } fetchingRates.Add(i.Pair, GetRuleValue(dependentQueries, i.RateRule)); } return fetchingRates; } 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); foreach (var rule in query.ExchangeRates) { rateRule.ExchangeRates.Add(rule); } } rateRule.Reevaluate(); result.Value = rateRule.Value; result.Errors = rateRule.Errors; result.EvaluatedRule = rateRule.ToString(true); result.Rule = rateRule.ToString(false); return result; } private async Task QueryRates(string exchangeName) { List providers = new List(); if (DirectProviders.TryGetValue(exchangeName, out var directProvider)) providers.Add(directProvider); if (_CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName)) { providers.Add(new CoinAverageRateProvider() { Exchange = exchangeName, Authenticator = _CoinAverageSettings }); } var fallback = new FallbackRateProvider(providers.ToArray()); var cached = new CachedRateProvider(exchangeName, fallback, _Cache) { CacheSpan = CacheSpan }; var value = await cached.GetRatesAsync(); return new QueryRateResult() { CachedResult = !fallback.Used, ExchangeRates = value, Exceptions = fallback.Exceptions .Select(c => new ExchangeException() { Exception = c, ExchangeName = exchangeName }).ToList() }; } } }