From 4f5a8f79530d58822d574f30c359e9d9ac875b7d Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 21 Aug 2018 15:59:57 +0900 Subject: [PATCH] Use direct provider for Kraken, update packages --- BTCPayServer.Tests/UnitTest1.cs | 2 + BTCPayServer/BTCPayServer.csproj | 13 +- BTCPayServer/Rating/ExchangeRates.cs | 10 ++ .../Rates/BTCPayRateProviderFactory.cs | 4 +- .../Rates/ExchangeSharpRateProvider.cs | 9 +- .../Rates/KrakenExchangeRateProvider.cs | 135 ++++++++++++++++++ 6 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 BTCPayServer/Services/Rates/KrakenExchangeRateProvider.cs diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 4af146835..8eca4b353 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1687,6 +1687,8 @@ namespace BTCPayServer.Tests && e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD ); } + // Kraken emit one request only after first GetRates + factory.DirectProviders["kraken"].GetRatesAsync().GetAwaiter().GetResult(); } [Fact] diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 9b89bae49..7c7361fbf 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -5,6 +5,9 @@ 1.0.2.94 NU1701,CA1816,CA1308,CA1810,CA2208 + + 7.3 + @@ -31,22 +34,22 @@ - + - + - + - + - + diff --git a/BTCPayServer/Rating/ExchangeRates.cs b/BTCPayServer/Rating/ExchangeRates.cs index a69cfe4dd..6b3642d59 100644 --- a/BTCPayServer/Rating/ExchangeRates.cs +++ b/BTCPayServer/Rating/ExchangeRates.cs @@ -221,6 +221,16 @@ namespace BTCPayServer.Rating } public class ExchangeRate { + public ExchangeRate() + { + + } + public ExchangeRate(string exchange, CurrencyPair currencyPair, BidAsk bidAsk) + { + this.Exchange = exchange; + this.CurrencyPair = currencyPair; + this.BidAsk = bidAsk; + } public string Exchange { get; set; } public CurrencyPair CurrencyPair { get; set; } public BidAsk BidAsk { get; set; } diff --git a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs index e69ffd4ad..d409000f8 100644 --- a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs +++ b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs @@ -70,7 +70,7 @@ namespace BTCPayServer.Services.Rates // 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("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)); @@ -80,7 +80,7 @@ namespace BTCPayServer.Services.Rates DirectProviders.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), 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("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())); diff --git a/BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs b/BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs index 6efdad8d3..230749db2 100644 --- a/BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs +++ b/BTCPayServer/Services/Rates/ExchangeSharpRateProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -43,17 +44,17 @@ namespace BTCPayServer.Services.Rates } // ExchangeSymbolToGlobalSymbol throws exception which would kill perf - HashSet notFoundSymbols = new HashSet(); + ConcurrentDictionary notFoundSymbols = new ConcurrentDictionary(); private ExchangeRate CreateExchangeRate(KeyValuePair ticker) { - if (notFoundSymbols.Contains(ticker.Key)) + if (notFoundSymbols.ContainsKey(ticker.Key)) return null; try { var tickerName = _ExchangeAPI.ExchangeSymbolToGlobalSymbol(ticker.Key); if (!CurrencyPair.TryParse(tickerName, out var pair)) { - notFoundSymbols.Add(ticker.Key); + notFoundSymbols.TryAdd(ticker.Key, ticker.Key); return null; } if(ReverseCurrencyPair) @@ -66,7 +67,7 @@ namespace BTCPayServer.Services.Rates } catch (ArgumentException) { - notFoundSymbols.Add(ticker.Key); + notFoundSymbols.TryAdd(ticker.Key, ticker.Key); return null; } } diff --git a/BTCPayServer/Services/Rates/KrakenExchangeRateProvider.cs b/BTCPayServer/Services/Rates/KrakenExchangeRateProvider.cs new file mode 100644 index 000000000..af95d530a --- /dev/null +++ b/BTCPayServer/Services/Rates/KrakenExchangeRateProvider.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Rating; +using ExchangeSharp; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Services.Rates +{ + // Make sure that only one request is sent to kraken in general + public class KrakenExchangeRateProvider : IRateProvider + { + public KrakenExchangeRateProvider() + { + _Helper = new ExchangeKrakenAPI(); + } + ExchangeKrakenAPI _Helper; + public HttpClient HttpClient + { + get + { + return _LocalClient ?? _Client; + } + set + { + _LocalClient = null; + } + } + HttpClient _LocalClient; + static HttpClient _Client = new HttpClient(); + + // ExchangeSymbolToGlobalSymbol throws exception which would kill perf + ConcurrentDictionary notFoundSymbols = new ConcurrentDictionary(); + string[] _Symbols = Array.Empty(); + DateTimeOffset? _LastSymbolUpdate = null; + + public async Task GetRatesAsync() + { + var result = new ExchangeRates(); + var symbols = await GetSymbolsAsync(); + var normalizedPairsList = symbols.Where(s => !notFoundSymbols.ContainsKey(s)).Select(s => _Helper.NormalizeSymbol(s)).ToList(); + var csvPairsList = string.Join(",", normalizedPairsList); + JToken apiTickers = await MakeJsonRequestAsync("/0/public/Ticker", null, new Dictionary { { "pair", csvPairsList } }); + var tickers = new List>(); + foreach (string symbol in symbols) + { + var ticker = ConvertToExchangeTicker(symbol, apiTickers[symbol]); + if (ticker != null) + { + try + { + var global = _Helper.ExchangeSymbolToGlobalSymbol(symbol); + if (CurrencyPair.TryParse(global, out var pair)) + result.Add(new ExchangeRate("kraken", pair.Inverse(), new BidAsk(ticker.Bid, ticker.Ask))); + else + notFoundSymbols.TryAdd(symbol, symbol); + } + catch (ArgumentException) + { + notFoundSymbols.TryAdd(symbol, symbol); + } + } + } + return result; + } + + private static ExchangeTicker ConvertToExchangeTicker(string symbol, JToken ticker) + { + if (ticker == null) + return null; + decimal last = ticker["c"][0].ConvertInvariant(); + return new ExchangeTicker + { + Ask = ticker["a"][0].ConvertInvariant(), + Bid = ticker["b"][0].ConvertInvariant(), + Last = last, + Volume = new ExchangeVolume + { + BaseVolume = ticker["v"][1].ConvertInvariant(), + BaseSymbol = symbol, + ConvertedVolume = ticker["v"][1].ConvertInvariant() * last, + ConvertedSymbol = symbol, + Timestamp = DateTime.UtcNow + } + }; + } + + private async Task GetSymbolsAsync() + { + if (_LastSymbolUpdate != null && DateTimeOffset.UtcNow - _LastSymbolUpdate.Value < TimeSpan.FromDays(0.5)) + { + return _Symbols; + } + else + { + JToken json = await MakeJsonRequestAsync("/0/public/AssetPairs"); + var symbols = (from prop in json.Children() where !prop.Name.Contains(".d", StringComparison.OrdinalIgnoreCase) select prop.Name).ToArray(); + _Symbols = symbols; + _LastSymbolUpdate = DateTimeOffset.UtcNow; + return symbols; + } + } + + private async Task MakeJsonRequestAsync(string url, string baseUrl = null, Dictionary payload = null, string requestMethod = null) + { + StringBuilder sb = new StringBuilder(); + sb.Append("https://api.kraken.com"); + ; + sb.Append(url); + if (payload != null) + { + sb.Append("?"); + sb.Append(String.Join('&', payload.Select(kv => $"{kv.Key}={kv.Value}").OfType().ToArray())); + } + var request = new HttpRequestMessage(HttpMethod.Get, sb.ToString()); + var response = await HttpClient.SendAsync(request); + string stringResult = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(stringResult); + if (result is JToken json) + { + if (!(json is JArray) && json["error"] is JArray error && error.Count != 0) + { + throw new APIException(error[0].ToStringInvariant()); + } + result = (T)(object)(json["result"] ?? json); + } + return result; + } + } +}