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 @@
}
else
@@ -130,7 +130,7 @@
}