From 58d9a48787a804f933a4d297cf160acd9eb12238 Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 10 Jan 2020 14:50:39 +0100 Subject: [PATCH] CoinGecko Rate Provider The CoinGecko rate provider is similar to the bitcoin average one, in that you can ask it for a rate from its aggregated sourcing or you can get rates from specific exchanges. I've added support for both. I haven't integrated it or replaced coinaverage just to see if we should use it as the default and switch everyone to it or what other action to take. --- BTCPayServer.Rating/AvailableRateProvider.cs | 16 +++ BTCPayServer.Rating/CurrencyPair.cs | 3 - BTCPayServer.Rating/ExchangeRates.cs | 1 - .../Providers/CoinAverageRateProvider.cs | 4 - .../Providers/CoinAverageSettings.cs | 117 +----------------- .../Providers/CoinGeckoRateProvider.cs | 96 ++++++++++++++ BTCPayServer.Rating/RateRules.cs | 1 - .../Services/RateProviderFactory.cs | 57 +++++---- BTCPayServer.Tests/BTCPayServerTester.cs | 19 ++- BTCPayServer.Tests/Mocks/MockRateProvider.cs | 15 ++- BTCPayServer.Tests/UnitTest1.cs | 27 ++-- BTCPayServer/Controllers/StoresController.cs | 38 +++--- BTCPayServer/Data/StoreBlob.cs | 3 +- BTCPayServer/Data/StoreDataExtensions.cs | 2 +- .../HostedServices/RatesHostedService.cs | 9 +- .../Models/StoreViewModels/RatesViewModel.cs | 21 +--- BTCPayServer/Views/Stores/Rates.cshtml | 10 +- 17 files changed, 227 insertions(+), 212 deletions(-) create mode 100644 BTCPayServer.Rating/AvailableRateProvider.cs create mode 100644 BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs diff --git a/BTCPayServer.Rating/AvailableRateProvider.cs b/BTCPayServer.Rating/AvailableRateProvider.cs new file mode 100644 index 0000000000..2265d06c43 --- /dev/null +++ b/BTCPayServer.Rating/AvailableRateProvider.cs @@ -0,0 +1,16 @@ +namespace BTCPayServer.Rating +{ + public class AvailableRateProvider + { + public string Name { get; set; } + public string Url { get; set; } + public string Id { get; set; } + + public AvailableRateProvider(string id, string name, string url) + { + Id = id; + Name = name; + Url = url; + } + } +} diff --git a/BTCPayServer.Rating/CurrencyPair.cs b/BTCPayServer.Rating/CurrencyPair.cs index af4d3ab969..5307331e12 100644 --- a/BTCPayServer.Rating/CurrencyPair.cs +++ b/BTCPayServer.Rating/CurrencyPair.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace BTCPayServer.Rating { diff --git a/BTCPayServer.Rating/ExchangeRates.cs b/BTCPayServer.Rating/ExchangeRates.cs index 6b3642d595..c9142e2b23 100644 --- a/BTCPayServer.Rating/ExchangeRates.cs +++ b/BTCPayServer.Rating/ExchangeRates.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Threading.Tasks; namespace BTCPayServer.Rating { diff --git a/BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs b/BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs index 203e7105d8..c7bca5e709 100644 --- a/BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs +++ b/BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs @@ -1,13 +1,9 @@ using Newtonsoft.Json; -using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json.Linq; using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; -using System.Security.Cryptography; -using System.Text; using System.Threading.Tasks; using System.ComponentModel; using BTCPayServer.Rating; diff --git a/BTCPayServer.Rating/Providers/CoinAverageSettings.cs b/BTCPayServer.Rating/Providers/CoinAverageSettings.cs index 92769535a6..29d19004fb 100644 --- a/BTCPayServer.Rating/Providers/CoinAverageSettings.cs +++ b/BTCPayServer.Rating/Providers/CoinAverageSettings.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Security.Cryptography; using System.Text; @@ -20,125 +18,12 @@ public Task AddHeader(HttpRequestMessage message) return _Settings.AddHeader(message); } } - - public class CoinAverageExchange - { - public CoinAverageExchange(string name, string display, string url) - { - Name = name; - Display = display; - Url = url; - } - public string Name { get; set; } - public string Display { get; set; } - public string Url - { - get; - set; - } - } - public class CoinAverageExchanges : Dictionary - { - public CoinAverageExchanges() - { - } - - public void Add(CoinAverageExchange exchange) - { - if (!TryAdd(exchange.Name, exchange)) - { - this.Remove(exchange.Name); - this.Add(exchange.Name, exchange); - } - } - } public class CoinAverageSettings : ICoinAverageAuthenticator { private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); public (String PublicKey, String PrivateKey)? KeyPair { get; set; } - public CoinAverageExchanges AvailableExchanges { get; set; } = new CoinAverageExchanges(); - - public CoinAverageSettings() - { - //GENERATED BY: - //StringBuilder b = new StringBuilder(); - //b.AppendLine("_coinAverageSettings.AvailableExchanges = new[] {"); - //foreach (var availableExchange in _coinAverageSettings.AvailableExchanges) - //{ - // b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),"); - //} - //b.AppendLine("}.ToArray()"); - AvailableExchanges = new CoinAverageExchanges(); - foreach (var item in - new[] { - (DisplayName: "Idex", Name: "idex"), - (DisplayName: "Coinfloor", Name: "coinfloor"), - (DisplayName: "Okex", Name: "okex"), - (DisplayName: "Bitfinex", Name: "bitfinex"), - (DisplayName: "Bittylicious", Name: "bittylicious"), - (DisplayName: "BTC Markets", Name: "btcmarkets"), - (DisplayName: "Kucoin", Name: "kucoin"), - (DisplayName: "IDAX", Name: "idax"), - (DisplayName: "Kraken", Name: "kraken"), - (DisplayName: "Bit2C", Name: "bit2c"), - (DisplayName: "Mercado Bitcoin", Name: "mercado"), - (DisplayName: "CEX.IO", Name: "cex"), - (DisplayName: "Bitex.la", Name: "bitex"), - (DisplayName: "Quoine", Name: "quoine"), - (DisplayName: "Stex", Name: "stex"), - (DisplayName: "CoinTiger", Name: "cointiger"), - (DisplayName: "Poloniex", Name: "poloniex"), - (DisplayName: "Zaif", Name: "zaif"), - (DisplayName: "Huobi", Name: "huobi"), - (DisplayName: "QuickBitcoin", Name: "quickbitcoin"), - (DisplayName: "Tidex", Name: "tidex"), - (DisplayName: "Tokenomy", Name: "tokenomy"), - (DisplayName: "Bitcoin.co.id", Name: "bitcoin_co_id"), - (DisplayName: "Kryptono", Name: "kryptono"), - (DisplayName: "Bitso", Name: "bitso"), - (DisplayName: "Korbit", Name: "korbit"), - (DisplayName: "Yobit", Name: "yobit"), - (DisplayName: "BitBargain", Name: "bitbargain"), - (DisplayName: "Livecoin", Name: "livecoin"), - (DisplayName: "Hotbit", Name: "hotbit"), - (DisplayName: "Coincheck", Name: "coincheck"), - (DisplayName: "Binance", Name: "binance"), - (DisplayName: "Bit-Z", Name: "bitz"), - (DisplayName: "Coinbase Pro", Name: "coinbasepro"), - (DisplayName: "Rock Trading", Name: "rocktrading"), - (DisplayName: "Bittrex", Name: "bittrex"), - (DisplayName: "BitBay", Name: "bitbay"), - (DisplayName: "Tokenize", Name: "tokenize"), - (DisplayName: "Hitbtc", Name: "hitbtc"), - (DisplayName: "Upbit", Name: "upbit"), - (DisplayName: "Bitstamp", Name: "bitstamp"), - (DisplayName: "Luno", Name: "luno"), - (DisplayName: "Trade.io", Name: "tradeio"), - (DisplayName: "LocalBitcoins", Name: "localbitcoins"), - (DisplayName: "Independent Reserve", Name: "independentreserve"), - (DisplayName: "Coinsquare", Name: "coinsquare"), - (DisplayName: "Exmoney", Name: "exmoney"), - (DisplayName: "Coinegg", Name: "coinegg"), - (DisplayName: "FYB-SG", Name: "fybsg"), - (DisplayName: "Cryptonit", Name: "cryptonit"), - (DisplayName: "BTCTurk", Name: "btcturk"), - (DisplayName: "bitFlyer", Name: "bitflyer"), - (DisplayName: "Negocie Coins", Name: "negociecoins"), - (DisplayName: "OasisDEX", Name: "oasisdex"), - (DisplayName: "CoinMate", Name: "coinmate"), - (DisplayName: "BitForex", Name: "bitforex"), - (DisplayName: "Bitsquare", Name: "bitsquare"), - (DisplayName: "FYB-SE", Name: "fybse"), - (DisplayName: "itBit", Name: "itbit"), - }) - { - AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}")); - } - // Keep back-compat - AvailableExchanges.Add(new CoinAverageExchange("gdax", string.Empty, $"https://apiv2.bitcoinaverage.com/exchanges/coinbasepro")); - } - + public Task AddHeader(HttpRequestMessage message) { var signature = GetCoinAverageSignature(); diff --git a/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs b/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs new file mode 100644 index 0000000000..e08a43c74e --- /dev/null +++ b/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Rating; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Services.Rates +{ + public class CoinGeckoRateProvider : IRateProvider, IHasExchangeName + { + private readonly HttpClient Client; + public static string CoinGeckoName { get; } = "coingecko"; + public string Exchange { get; set; } + public string ExchangeName => Exchange ?? CoinGeckoName; + + public CoinGeckoRateProvider(IHttpClientFactory httpClientFactory) + { + if (httpClientFactory == null) + { + return;; + } + Client = httpClientFactory.CreateClient(); + Client.BaseAddress = new Uri("https://api.coingecko.com/api/v3/"); + Client.DefaultRequestHeaders.Add("Accept", "application/json"); + } + + private IEnumerable _availableExchanges; + + public virtual async Task> GetAvailableExchanges(bool reload = false) + { + if (_availableExchanges != null && !reload) return _availableExchanges; + var resp = await Client.GetAsync("exchanges/list"); + resp.EnsureSuccessStatusCode(); + _availableExchanges = JArray.Parse(await resp.Content.ReadAsStringAsync()) + .Select(token => + new AvailableRateProvider(token["id"].ToString().ToLowerInvariant(), token["name"].ToString(), + $"{Client.BaseAddress}exchanges/{token["id"]}/tickers")); + + return _availableExchanges; + } + + public virtual Task GetRatesAsync(CancellationToken cancellationToken) + { + return ExchangeName == CoinGeckoName ? GetCoinGeckoRates() : GetCoinGeckoExchangeSpecificRates(); + } + + private async Task GetCoinGeckoRates() + { + var resp = await Client.GetAsync("exchange_rates"); + resp.EnsureSuccessStatusCode(); + return new ExchangeRates(JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("rates").Children() + .Select(token => new ExchangeRate(CoinGeckoName, + new CurrencyPair("BTC", ((JProperty)token).Name.ToString()), + new BidAsk(((JProperty)token).Value["value"].Value())))); + } + + private async Task GetCoinGeckoExchangeSpecificRates(int page = 1) + { + var resp = await Client.GetAsync($"exchanges/{Exchange}/tickers?page={page}"); + + resp.EnsureSuccessStatusCode(); + List result = JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("tickers") + .Select(token => new ExchangeRate(ExchangeName, + new CurrencyPair(token.Value("base"), token.Value("target")), + new BidAsk(token.Value("last")))).ToList(); + if (page == 1 && resp.Headers.TryGetValues("total", out var total) && + resp.Headers.TryGetValues("per-page", out var perPage)) + { + var totalItems = int.Parse(total.First()); + var perPageItems = int.Parse(perPage.First()); + + var totalPages = totalItems / perPageItems; + if (totalItems % perPageItems != 0) + { + totalPages++; + } + + var tasks = new List>(); + for (int i = 2; i <= totalPages; i++) + { + tasks.Add(GetCoinGeckoExchangeSpecificRates(i)); + } + + foreach (var t in (await Task.WhenAll(tasks))) + { + result.AddRange(t); + } + } + + return new ExchangeRates(result); + } + } +} diff --git a/BTCPayServer.Rating/RateRules.cs b/BTCPayServer.Rating/RateRules.cs index 9dfbfedeaa..2b47b34fad 100644 --- a/BTCPayServer.Rating/RateRules.cs +++ b/BTCPayServer.Rating/RateRules.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; diff --git a/BTCPayServer.Rating/Services/RateProviderFactory.cs b/BTCPayServer.Rating/Services/RateProviderFactory.cs index 154b871d98..8911f7d5ed 100644 --- a/BTCPayServer.Rating/Services/RateProviderFactory.cs +++ b/BTCPayServer.Rating/Services/RateProviderFactory.cs @@ -57,7 +57,6 @@ public RateProviderFactory(IOptions cacheOptions, _CacheOptions = cacheOptions; // We use 15 min because of limits with free version of bitcoinaverage CacheSpan = TimeSpan.FromMinutes(15.0); - InitExchanges(); } private IOptions _CacheOptions; TimeSpan _CacheSpan; @@ -81,7 +80,7 @@ public void InvalidateCache() provider.CacheSpan = CacheSpan; provider.MemoryCache = cache; } - if (Providers.TryGetValue(CoinAverageRateProvider.CoinAverageName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c) + if (Providers.TryGetValue(CoinGeckoRateProvider.CoinGeckoName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c) { c.RefreshRate = CacheSpan; c.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0); @@ -98,7 +97,7 @@ public Dictionary Providers } } - private void InitExchanges() + public async Task InitExchanges() { // We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request Providers.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true)); @@ -112,6 +111,7 @@ private void InitExchanges() // Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false)); // Handmade providers + Providers.Add(CoinGeckoRateProvider.CoinGeckoName, new CoinGeckoRateProvider(_httpClientFactory)); Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_COINAVERAGE"), Authenticator = _CoinAverageSettings }); Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") }); Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS"))); @@ -129,7 +129,7 @@ private void InitExchanges() if (provider.Key == "cryptopia") // Shitty exchange, rate often unavailable, it spams the logs continue; var prov = new BackgroundFetcherRateProvider(provider.Key, Providers[provider.Key]); - if(provider.Key == CoinAverageRateProvider.CoinAverageName) + if(provider.Key == CoinGeckoRateProvider.CoinGeckoName) { prov.RefreshRate = CacheSpan; prov.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0); @@ -143,40 +143,51 @@ private void InitExchanges() } var cache = new MemoryCache(_CacheOptions); - foreach (var supportedExchange in GetSupportedExchanges()) + foreach (var supportedExchange in await GetSupportedExchanges(true)) { - if (!Providers.ContainsKey(supportedExchange.Key)) + if (!Providers.ContainsKey(supportedExchange.Id)) { - var coinAverage = new CoinAverageRateProvider() + var coinAverage = new CoinGeckoRateProvider(_httpClientFactory) { - Exchange = supportedExchange.Key, - HttpClient = _httpClientFactory?.CreateClient(), - Authenticator = _CoinAverageSettings + Exchange = supportedExchange.Id }; - var cached = new CachedRateProvider(supportedExchange.Key, coinAverage, cache) + var cached = new CachedRateProvider(supportedExchange.Id, coinAverage, cache) { CacheSpan = CacheSpan }; - Providers.Add(supportedExchange.Key, cached); + Providers.Add(supportedExchange.Id, cached); } } } - public CoinAverageExchanges GetSupportedExchanges() + public async Task> GetSupportedExchanges(bool reload = false) { - CoinAverageExchanges exchanges = new CoinAverageExchanges(); - foreach (var exchange in _CoinAverageSettings.AvailableExchanges) + IEnumerable exchanges; + switch (Providers[CoinGeckoRateProvider.CoinGeckoName]) { - exchanges.Add(exchange.Value); + case BackgroundFetcherRateProvider backgroundFetcherRateProvider: + exchanges = await ((CoinGeckoRateProvider)((BackgroundFetcherRateProvider)Providers[ + CoinGeckoRateProvider.CoinGeckoName]).Inner).GetAvailableExchanges(reload); + break; + case CoinGeckoRateProvider coinGeckoRateProvider: + exchanges = await coinGeckoRateProvider.GetAvailableExchanges(reload); + break; + default: + exchanges = new AvailableRateProvider[0]; + break; } - // Add other exchanges supported here - exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average", $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short")); - exchanges.Add(new CoinAverageExchange("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD")); - exchanges.Add(new CoinAverageExchange("ndax", "NDAX", "https://ndax.io/api/returnTicker")); - exchanges.Add(new CoinAverageExchange("bitbank", "Bitbank", "https://public.bitbank.cc/prices")); - - return exchanges; + return new[] + { + new AvailableRateProvider(CoinGeckoRateProvider.CoinGeckoName, "Coin Gecko", + "https://api.coingecko.com/api/v3/exchange_rates"), + new AvailableRateProvider("bylls", "Bylls", + "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD"), + new AvailableRateProvider("ndax", "NDAX", "https://ndax.io/api/returnTicker"), + new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices"), + new AvailableRateProvider(CoinAverageRateProvider.CoinAverageName, "Coin Average", + "https://apiv2.bitcoinaverage.com/indices/global/ticker/short") + }.Concat(exchanges); } public async Task QueryRates(string exchangeName, CancellationToken cancellationToken) diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 95eea7a023..9df171299a 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -202,29 +202,29 @@ public async Task StartAsync() var coinAverageMock = new MockRateProvider(); coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() { - Exchange = "coinaverage", + Exchange = "coingecko", CurrencyPair = CurrencyPair.Parse("BTC_USD"), BidAsk = new BidAsk(5000m) }); coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() { - Exchange = "coinaverage", + Exchange = "coingecko", CurrencyPair = CurrencyPair.Parse("BTC_CAD"), BidAsk = new BidAsk(4500m) }); coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() { - Exchange = "coinaverage", + Exchange = "coingecko", CurrencyPair = CurrencyPair.Parse("LTC_BTC"), BidAsk = new BidAsk(0.001m) }); coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() { - Exchange = "coinaverage", + Exchange = "coingecko", CurrencyPair = CurrencyPair.Parse("LTC_USD"), BidAsk = new BidAsk(500m) }); - rateProvider.Providers.Add("coinaverage", coinAverageMock); + rateProvider.Providers.Add("coingecko", coinAverageMock); var bitflyerMock = new MockRateProvider(); bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate() @@ -262,6 +262,15 @@ public async Task StartAsync() BidAsk = new BidAsk(0.000136m) }); rateProvider.Providers.Add("bitfinex", bitfinex); + + + coinAverageMock.AvailableRateProviders.AddRange(new [] + { + new AvailableRateProvider("bitflyer", "bitflyer", "bitflyer"), + new AvailableRateProvider("quadrigacx", "quadrigacx", "quadrigacx"), + new AvailableRateProvider("bittrex", "bittrex", "bittrex"), + new AvailableRateProvider("bitfinex", "bitfinex", "bitfinex"), + }); } diff --git a/BTCPayServer.Tests/Mocks/MockRateProvider.cs b/BTCPayServer.Tests/Mocks/MockRateProvider.cs index 3397629921..b84e953772 100644 --- a/BTCPayServer.Tests/Mocks/MockRateProvider.cs +++ b/BTCPayServer.Tests/Mocks/MockRateProvider.cs @@ -8,12 +8,23 @@ namespace BTCPayServer.Tests.Mocks { - public class MockRateProvider : IRateProvider + public class MockRateProvider : CoinGeckoRateProvider { public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates(); - public Task GetRatesAsync(CancellationToken cancellationToken) + public List AvailableRateProviders { get; set; } = new List(); + + public MockRateProvider():base(null) + { + + } + public override Task GetRatesAsync(CancellationToken cancellationToken) { return Task.FromResult(ExchangeRates); } + + public override Task> GetAvailableExchanges(bool reload = false) + { + return Task.FromResult((IEnumerable)AvailableRateProviders); + } } } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 86e44a32c1..09f7fcb4a4 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -962,7 +962,7 @@ public async Task CanGetRates() Assert.Null(GetRatesResult?.Data); var store = acc.GetController(); - var ratesVM = (RatesViewModel)(Assert.IsType(store.Rates()).Model); + var ratesVM = (RatesViewModel)(Assert.IsType(await store.Rates()).Model); ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD"; store.Rates(ratesVM).Wait(); store = acc.GetController(); @@ -1240,7 +1240,7 @@ public async Task CanUseExchangeSpecificRate() user.GrantAccess(); user.RegisterDerivationScheme("BTC"); List rates = new List(); - rates.Add(CreateInvoice(tester, user, "coinaverage")); + rates.Add(CreateInvoice(tester, user, "coingecko")); var bitflyer = CreateInvoice(tester, user, "bitflyer", "JPY"); var bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY"); Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache @@ -1256,7 +1256,7 @@ public async Task CanUseExchangeSpecificRate() private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD") { var storeController = user.GetController(); - var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; + var vm = (RatesViewModel)((ViewResult)storeController.Rates().GetAwaiter().GetResult()).Model; vm.PreferredExchange = exchange; storeController.Rates(vm).Wait(); var invoice2 = user.BitPay.CreateInvoice(new Invoice() @@ -1337,7 +1337,7 @@ public async Task CanTweakRate() Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice); var storeController = user.GetController(); - var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; + var vm = (RatesViewModel)((ViewResult)storeController.Rates().GetAwaiter().GetResult()).Model; Assert.Equal(0.0, vm.Spread); vm.Spread = 40; storeController.Rates(vm).Wait(); @@ -1438,15 +1438,15 @@ public async Task CanModifyRates() user.RegisterDerivationScheme("BTC"); var store = user.GetController(); - var rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); + var rateVm = Assert.IsType(Assert.IsType( await store.Rates()).Model); Assert.False(rateVm.ShowScripting); - Assert.Equal("coinaverage", rateVm.PreferredExchange); + Assert.Equal(CoinGeckoRateProvider.CoinGeckoName, rateVm.PreferredExchange); Assert.Equal(0.0, rateVm.Spread); Assert.Null(rateVm.TestRateRules); rateVm.PreferredExchange = "bitflyer"; Assert.IsType(store.Rates(rateVm, "Save").Result); - rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); + rateVm = Assert.IsType(Assert.IsType( await store.Rates()).Model); Assert.Equal("bitflyer", rateVm.PreferredExchange); rateVm.ScriptTest = "BTC_JPY,BTC_CAD"; @@ -1463,7 +1463,7 @@ public async Task CanModifyRates() Assert.IsType(store.ShowRateRulesPost(true).Result); Assert.IsType(store.Rates(rateVm, "Save").Result); store = user.GetController(); - rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); + rateVm = Assert.IsType(Assert.IsType( await store.Rates()).Model); Assert.Equal(rateVm.StoreId, user.StoreId); Assert.Equal(rateVm.DefaultScript, rateVm.Script); Assert.True(rateVm.ShowScripting); @@ -1475,13 +1475,13 @@ public async Task CanModifyRates() 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 = coinaverage(X_X);"; + "X_X = coingecko(X_X);"; rateVm.Spread = 50; rateVm = Assert.IsType(Assert.IsType(store.Rates(rateVm, "Test").Result).Model); Assert.True(rateVm.TestRateRules.All(t => !t.Error)); Assert.IsType(store.Rates(rateVm, "Save").Result); store = user.GetController(); - rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); + rateVm = Assert.IsType(Assert.IsType( await store.Rates()).Model); Assert.Equal(50, rateVm.Spread); Assert.True(rateVm.ShowScripting); Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase); @@ -2687,9 +2687,12 @@ public void CanQueryDirectProviders() .Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value)) .ToList()) { + Logs.Tester.LogInformation($"Testing {result.ExpectedName}"); if (result.ExpectedName == "quadrigacx") continue; // 29 january, the exchange is down + if (result.ExpectedName == "coinaverage") + continue; // no more free plan result.Fetcher.InvalidateCache(); var exchangeRates = result.ResultAsync.Result; result.Fetcher.InvalidateCache(); @@ -2782,7 +2785,9 @@ public void CanGetRateCryptoCurrenciesByDefault() public static RateProviderFactory CreateBTCPayRateFactory() { - return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings()); + var result = new RateProviderFactory(CreateMemoryCache(), new MockHttpClientFactory(), new CoinAverageSettings()); + result.InitExchanges().GetAwaiter().GetResult(); + return result; } private static MemoryCacheOptions CreateMemoryCache() diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 11d89f851b..877de6362b 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -9,7 +9,6 @@ using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.Models; -using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Changelly; @@ -23,15 +22,12 @@ using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.Extensions.Options; using NBitcoin; using NBitcoin.DataEncoders; -using Newtonsoft.Json; namespace BTCPayServer.Controllers { @@ -197,16 +193,17 @@ public async Task DeleteStoreUserPost(string storeId, string user [HttpGet] [Route("{storeId}/rates")] - public IActionResult Rates() + public async Task Rates() { + var exchanges = await GetSupportedExchanges(); var storeBlob = CurrentStore.GetStoreBlob(); var vm = new RatesViewModel(); - vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName); + vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? CoinGeckoRateProvider.CoinGeckoName); vm.Spread = (double)(storeBlob.Spread * 100m); vm.StoreId = CurrentStore.Id; vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString(); vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString(); - vm.AvailableExchanges = GetSupportedExchanges(); + vm.AvailableExchanges = exchanges; vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString(); vm.ShowScripting = storeBlob.RateScripting; return View(vm); @@ -216,7 +213,16 @@ public IActionResult Rates() [Route("{storeId}/rates")] public async Task Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default) { - model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange); + if (command == "scripting-on") + { + return RedirectToAction(nameof(ShowRateRules), new {scripting = true,storeId = model.StoreId}); + }else if (command == "scripting-off") + { + return RedirectToAction(nameof(ShowRateRules), new {scripting = false, storeId = model.StoreId}); + } + + var exchanges = await GetSupportedExchanges(); + model.SetExchangeRates(exchanges, model.PreferredExchange); model.StoreId = storeId ?? model.StoreId; CurrencyPair[] currencyPairs = null; try @@ -239,14 +245,14 @@ public async Task Rates(RatesViewModel model, string command = nu var blob = CurrentStore.GetStoreBlob(); model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString(); - model.AvailableExchanges = GetSupportedExchanges(); + model.AvailableExchanges = exchanges; blob.PreferredExchange = model.PreferredExchange; blob.Spread = (decimal)model.Spread / 100.0m; blob.DefaultCurrencyPairs = currencyPairs; if (!model.ShowScripting) { - if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase)) + if (!exchanges.Any(provider => provider.Id.Equals(model.PreferredExchange, StringComparison.InvariantCultureIgnoreCase))) { ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})"); return View(model); @@ -597,13 +603,13 @@ public async Task DeleteStorePost(string storeId) return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); } - private CoinAverageExchange[] GetSupportedExchanges() + private async Task> GetSupportedExchanges() { - return _RateFactory.RateProviderFactory.GetSupportedExchanges() - .Where(r => !string.IsNullOrWhiteSpace(r.Value.Display)) - .Select(c => c.Value) - .OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase) - .ToArray(); + var exchanges = await _RateFactory.RateProviderFactory.GetSupportedExchanges(); + return exchanges + .Where(r => !string.IsNullOrWhiteSpace(r.Name)) + .OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase); + } private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network) diff --git a/BTCPayServer/Data/StoreBlob.cs b/BTCPayServer/Data/StoreBlob.cs index 8dc12d6275..d7782d2642 100644 --- a/BTCPayServer/Data/StoreBlob.cs +++ b/BTCPayServer/Data/StoreBlob.cs @@ -10,6 +10,7 @@ using BTCPayServer.Services.Mails; using Newtonsoft.Json; using System.Text; +using BTCPayServer.Services.Rates; namespace BTCPayServer.Data { @@ -156,7 +157,7 @@ public RateRules GetDefaultRateRules(BTCPayNetworkProvider networkProvider) } } - var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? "coinaverage" : PreferredExchange; + var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? CoinGeckoRateProvider.CoinGeckoName : PreferredExchange; builder.AppendLine($"X_X = {preferredExchange}(X_X);"); BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules); diff --git a/BTCPayServer/Data/StoreDataExtensions.cs b/BTCPayServer/Data/StoreDataExtensions.cs index 1df7fd0a08..59f7f00efb 100644 --- a/BTCPayServer/Data/StoreDataExtensions.cs +++ b/BTCPayServer/Data/StoreDataExtensions.cs @@ -51,7 +51,7 @@ public static StoreBlob GetStoreBlob(this StoreData storeData) { var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject(Encoding.UTF8.GetString(storeData.StoreBlob)); if (result.PreferredExchange == null) - result.PreferredExchange = CoinAverageRateProvider.CoinAverageName; + result.PreferredExchange = CoinGeckoRateProvider.CoinGeckoName; return result; } diff --git a/BTCPayServer/HostedServices/RatesHostedService.cs b/BTCPayServer/HostedServices/RatesHostedService.cs index 9e7776e67f..e9408c46e0 100644 --- a/BTCPayServer/HostedServices/RatesHostedService.cs +++ b/BTCPayServer/HostedServices/RatesHostedService.cs @@ -146,14 +146,7 @@ private async Task SaveRateCache() async Task RefreshCoinAverageSupportedExchanges() { - var exchanges = new CoinAverageExchanges(); - foreach (var item in (await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync()) - .Exchanges - .Select(c => new CoinAverageExchange(c.Name, c.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{c.Name}"))) - { - exchanges.Add(item); - } - _coinAverageSettings.AvailableExchanges = exchanges; + await _RateProviderFactory.InitExchanges(); await Task.Delay(TimeSpan.FromHours(5), Cancellation); } diff --git a/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs b/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs index b26d1e10e5..d2114adccd 100644 --- a/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Threading.Tasks; using BTCPayServer.Rating; using BTCPayServer.Services.Rates; using Microsoft.AspNetCore.Mvc.Rendering; @@ -17,19 +15,12 @@ public class TestResultViewModel public string Rule { get; set; } public bool Error { get; set; } } - class Format - { - public string Name { get; set; } - public string Value { get; set; } - public string Url { get; set; } - } - public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange) + public void SetExchangeRates(IEnumerable supportedList, string preferredExchange) { var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName; - var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name, Url = o.Url }).ToArray(); - var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault(); - Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen); - PreferredExchange = chosen.Value; + var chosen = supportedList.FirstOrDefault(f => f.Id == defaultStore) ?? supportedList.FirstOrDefault(); + Exchanges = new SelectList(supportedList, nameof(chosen.Id), nameof(chosen.Name), chosen); + PreferredExchange = chosen.Id; RateSource = chosen.Url; } @@ -46,7 +37,7 @@ public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferr public string ScriptTest { get; set; } public string DefaultCurrencyPairs { get; set; } public string StoreId { get; set; } - public CoinAverageExchange[] AvailableExchanges { get; set; } + public IEnumerable AvailableExchanges { get; set; } [Display(Name = "Add a spread on exchange rate of ... %")] [Range(0.0, 100.0)] diff --git a/BTCPayServer/Views/Stores/Rates.cshtml b/BTCPayServer/Views/Stores/Rates.cshtml index 6c0fa15180..2970584be3 100644 --- a/BTCPayServer/Views/Stores/Rates.cshtml +++ b/BTCPayServer/Views/Stores/Rates.cshtml @@ -19,11 +19,11 @@
Scripting
Rate script allows you to express precisely how you want to calculate rates for currency pairs. -

+

Supported exchanges are: - @for (int i = 0; i < Model.AvailableExchanges.Length; i++) + @for (int i = 0; i < Model.AvailableExchanges.Count(); i++) { - @Model.AvailableExchanges[i].Name@(i == Model.AvailableExchanges.Length - 1 ? "" : ",") + @Model.AvailableExchanges.ElementAt(i).Name@(i == Model.AvailableExchanges.Count() - 1 ? "" : ",") }

Click here for more information

@@ -116,7 +116,7 @@ Set to default settings
- Turn off advanced rate rule scripting +
} else @@ -130,7 +130,7 @@

- Turn on advanced rate rule scripting +
}