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.
This commit is contained in:
Kukks 2020-01-10 14:50:39 +01:00 committed by nicolas.dorier
parent 1a3da096a7
commit 58d9a48787
No known key found for this signature in database
GPG Key ID: 6618763EF09186FE
17 changed files with 227 additions and 212 deletions

View File

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

View File

@ -1,7 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Rating namespace BTCPayServer.Rating
{ {

View File

@ -3,7 +3,6 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Rating namespace BTCPayServer.Rating
{ {

View File

@ -1,13 +1,9 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.ComponentModel; using System.ComponentModel;
using BTCPayServer.Rating; using BTCPayServer.Rating;

View File

@ -1,6 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
@ -20,125 +18,12 @@ namespace BTCPayServer.Services.Rates
return _Settings.AddHeader(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<string, CoinAverageExchange>
{
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 public class CoinAverageSettings : ICoinAverageAuthenticator
{ {
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public (String PublicKey, String PrivateKey)? KeyPair { get; set; } 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) public Task AddHeader(HttpRequestMessage message)
{ {
var signature = GetCoinAverageSignature(); var signature = GetCoinAverageSignature();

View File

@ -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<AvailableRateProvider> _availableExchanges;
public virtual async Task<IEnumerable<AvailableRateProvider>> 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<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
return ExchangeName == CoinGeckoName ? GetCoinGeckoRates() : GetCoinGeckoExchangeSpecificRates();
}
private async Task<ExchangeRates> 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<decimal>()))));
}
private async Task<ExchangeRates> GetCoinGeckoExchangeSpecificRates(int page = 1)
{
var resp = await Client.GetAsync($"exchanges/{Exchange}/tickers?page={page}");
resp.EnsureSuccessStatusCode();
List<ExchangeRate> result = JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("tickers")
.Select(token => new ExchangeRate(ExchangeName,
new CurrencyPair(token.Value<string>("base"), token.Value<string>("target")),
new BidAsk(token.Value<decimal>("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<Task<ExchangeRates>>();
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);
}
}
}

View File

@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;

View File

@ -57,7 +57,6 @@ namespace BTCPayServer.Services.Rates
_CacheOptions = cacheOptions; _CacheOptions = cacheOptions;
// We use 15 min because of limits with free version of bitcoinaverage // We use 15 min because of limits with free version of bitcoinaverage
CacheSpan = TimeSpan.FromMinutes(15.0); CacheSpan = TimeSpan.FromMinutes(15.0);
InitExchanges();
} }
private IOptions<MemoryCacheOptions> _CacheOptions; private IOptions<MemoryCacheOptions> _CacheOptions;
TimeSpan _CacheSpan; TimeSpan _CacheSpan;
@ -81,7 +80,7 @@ namespace BTCPayServer.Services.Rates
provider.CacheSpan = CacheSpan; provider.CacheSpan = CacheSpan;
provider.MemoryCache = cache; 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.RefreshRate = CacheSpan;
c.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0); c.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
@ -98,7 +97,7 @@ namespace BTCPayServer.Services.Rates
} }
} }
private void InitExchanges() public async Task InitExchanges()
{ {
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request // 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)); Providers.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true));
@ -112,6 +111,7 @@ namespace BTCPayServer.Services.Rates
// Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false)); // Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers // 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(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("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS"))); Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
@ -129,7 +129,7 @@ namespace BTCPayServer.Services.Rates
if (provider.Key == "cryptopia") // Shitty exchange, rate often unavailable, it spams the logs if (provider.Key == "cryptopia") // Shitty exchange, rate often unavailable, it spams the logs
continue; continue;
var prov = new BackgroundFetcherRateProvider(provider.Key, Providers[provider.Key]); var prov = new BackgroundFetcherRateProvider(provider.Key, Providers[provider.Key]);
if(provider.Key == CoinAverageRateProvider.CoinAverageName) if(provider.Key == CoinGeckoRateProvider.CoinGeckoName)
{ {
prov.RefreshRate = CacheSpan; prov.RefreshRate = CacheSpan;
prov.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0); prov.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
@ -143,40 +143,51 @@ namespace BTCPayServer.Services.Rates
} }
var cache = new MemoryCache(_CacheOptions); 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, Exchange = supportedExchange.Id
HttpClient = _httpClientFactory?.CreateClient(),
Authenticator = _CoinAverageSettings
}; };
var cached = new CachedRateProvider(supportedExchange.Key, coinAverage, cache) var cached = new CachedRateProvider(supportedExchange.Id, coinAverage, cache)
{ {
CacheSpan = CacheSpan CacheSpan = CacheSpan
}; };
Providers.Add(supportedExchange.Key, cached); Providers.Add(supportedExchange.Id, cached);
} }
} }
} }
public CoinAverageExchanges GetSupportedExchanges() public async Task<IEnumerable<AvailableRateProvider>> GetSupportedExchanges(bool reload = false)
{ {
CoinAverageExchanges exchanges = new CoinAverageExchanges(); IEnumerable<AvailableRateProvider> exchanges;
foreach (var exchange in _CoinAverageSettings.AvailableExchanges) 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 // Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average", $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short")); return new[]
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")); new AvailableRateProvider(CoinGeckoRateProvider.CoinGeckoName, "Coin Gecko",
exchanges.Add(new CoinAverageExchange("bitbank", "Bitbank", "https://public.bitbank.cc/prices")); "https://api.coingecko.com/api/v3/exchange_rates"),
new AvailableRateProvider("bylls", "Bylls",
return exchanges; "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<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken) public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)

View File

@ -202,29 +202,29 @@ namespace BTCPayServer.Tests
var coinAverageMock = new MockRateProvider(); var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{ {
Exchange = "coinaverage", Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("BTC_USD"), CurrencyPair = CurrencyPair.Parse("BTC_USD"),
BidAsk = new BidAsk(5000m) BidAsk = new BidAsk(5000m)
}); });
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{ {
Exchange = "coinaverage", Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"), CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(4500m) BidAsk = new BidAsk(4500m)
}); });
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{ {
Exchange = "coinaverage", Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"), CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
BidAsk = new BidAsk(0.001m) BidAsk = new BidAsk(0.001m)
}); });
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{ {
Exchange = "coinaverage", Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("LTC_USD"), CurrencyPair = CurrencyPair.Parse("LTC_USD"),
BidAsk = new BidAsk(500m) BidAsk = new BidAsk(500m)
}); });
rateProvider.Providers.Add("coinaverage", coinAverageMock); rateProvider.Providers.Add("coingecko", coinAverageMock);
var bitflyerMock = new MockRateProvider(); var bitflyerMock = new MockRateProvider();
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate() bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
@ -262,6 +262,15 @@ namespace BTCPayServer.Tests
BidAsk = new BidAsk(0.000136m) BidAsk = new BidAsk(0.000136m)
}); });
rateProvider.Providers.Add("bitfinex", bitfinex); 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"),
});
} }

View File

@ -8,12 +8,23 @@ using BTCPayServer.Services.Rates;
namespace BTCPayServer.Tests.Mocks namespace BTCPayServer.Tests.Mocks
{ {
public class MockRateProvider : IRateProvider public class MockRateProvider : CoinGeckoRateProvider
{ {
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates(); public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken) public List<AvailableRateProvider> AvailableRateProviders { get; set; } = new List<AvailableRateProvider>();
public MockRateProvider():base(null)
{
}
public override Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{ {
return Task.FromResult(ExchangeRates); return Task.FromResult(ExchangeRates);
} }
public override Task<IEnumerable<AvailableRateProvider>> GetAvailableExchanges(bool reload = false)
{
return Task.FromResult((IEnumerable<AvailableRateProvider>)AvailableRateProviders);
}
} }
} }

View File

@ -962,7 +962,7 @@ namespace BTCPayServer.Tests
Assert.Null(GetRatesResult?.Data); Assert.Null(GetRatesResult?.Data);
var store = acc.GetController<StoresController>(); var store = acc.GetController<StoresController>();
var ratesVM = (RatesViewModel)(Assert.IsType<ViewResult>(store.Rates()).Model); var ratesVM = (RatesViewModel)(Assert.IsType<ViewResult>(await store.Rates()).Model);
ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD"; ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD";
store.Rates(ratesVM).Wait(); store.Rates(ratesVM).Wait();
store = acc.GetController<StoresController>(); store = acc.GetController<StoresController>();
@ -1240,7 +1240,7 @@ namespace BTCPayServer.Tests
user.GrantAccess(); user.GrantAccess();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
List<decimal> rates = new List<decimal>(); List<decimal> rates = new List<decimal>();
rates.Add(CreateInvoice(tester, user, "coinaverage")); rates.Add(CreateInvoice(tester, user, "coingecko"));
var bitflyer = CreateInvoice(tester, user, "bitflyer", "JPY"); var bitflyer = CreateInvoice(tester, user, "bitflyer", "JPY");
var bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY"); var bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY");
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
@ -1256,7 +1256,7 @@ namespace BTCPayServer.Tests
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD") private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD")
{ {
var storeController = user.GetController<StoresController>(); var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; var vm = (RatesViewModel)((ViewResult)storeController.Rates().GetAwaiter().GetResult()).Model;
vm.PreferredExchange = exchange; vm.PreferredExchange = exchange;
storeController.Rates(vm).Wait(); storeController.Rates(vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice() var invoice2 = user.BitPay.CreateInvoice(new Invoice()
@ -1337,7 +1337,7 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice); Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
var storeController = user.GetController<StoresController>(); var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model; var vm = (RatesViewModel)((ViewResult)storeController.Rates().GetAwaiter().GetResult()).Model;
Assert.Equal(0.0, vm.Spread); Assert.Equal(0.0, vm.Spread);
vm.Spread = 40; vm.Spread = 40;
storeController.Rates(vm).Wait(); storeController.Rates(vm).Wait();
@ -1438,15 +1438,15 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
var store = user.GetController<StoresController>(); var store = user.GetController<StoresController>();
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model); var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>( await store.Rates()).Model);
Assert.False(rateVm.ShowScripting); Assert.False(rateVm.ShowScripting);
Assert.Equal("coinaverage", rateVm.PreferredExchange); Assert.Equal(CoinGeckoRateProvider.CoinGeckoName, rateVm.PreferredExchange);
Assert.Equal(0.0, rateVm.Spread); Assert.Equal(0.0, rateVm.Spread);
Assert.Null(rateVm.TestRateRules); Assert.Null(rateVm.TestRateRules);
rateVm.PreferredExchange = "bitflyer"; rateVm.PreferredExchange = "bitflyer";
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result); Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model); rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>( await store.Rates()).Model);
Assert.Equal("bitflyer", rateVm.PreferredExchange); Assert.Equal("bitflyer", rateVm.PreferredExchange);
rateVm.ScriptTest = "BTC_JPY,BTC_CAD"; rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
@ -1463,7 +1463,7 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(store.ShowRateRulesPost(true).Result); Assert.IsType<RedirectToActionResult>(store.ShowRateRulesPost(true).Result);
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result); Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
store = user.GetController<StoresController>(); store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model); rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>( await store.Rates()).Model);
Assert.Equal(rateVm.StoreId, user.StoreId); Assert.Equal(rateVm.StoreId, user.StoreId);
Assert.Equal(rateVm.DefaultScript, rateVm.Script); Assert.Equal(rateVm.DefaultScript, rateVm.Script);
Assert.True(rateVm.ShowScripting); Assert.True(rateVm.ShowScripting);
@ -1475,13 +1475,13 @@ namespace BTCPayServer.Tests
rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD"; rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD";
rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" + rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" +
"X_CAD = quadrigacx(X_CAD);\n" + "X_CAD = quadrigacx(X_CAD);\n" +
"X_X = coinaverage(X_X);"; "X_X = coingecko(X_X);";
rateVm.Spread = 50; rateVm.Spread = 50;
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model); rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
Assert.True(rateVm.TestRateRules.All(t => !t.Error)); Assert.True(rateVm.TestRateRules.All(t => !t.Error));
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result); Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
store = user.GetController<StoresController>(); store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model); rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>( await store.Rates()).Model);
Assert.Equal(50, rateVm.Spread); Assert.Equal(50, rateVm.Spread);
Assert.True(rateVm.ShowScripting); Assert.True(rateVm.ShowScripting);
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase); Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
@ -2687,9 +2687,12 @@ noninventoryitem:
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value)) .Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList()) .ToList())
{ {
Logs.Tester.LogInformation($"Testing {result.ExpectedName}"); Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
if (result.ExpectedName == "quadrigacx") if (result.ExpectedName == "quadrigacx")
continue; // 29 january, the exchange is down continue; // 29 january, the exchange is down
if (result.ExpectedName == "coinaverage")
continue; // no more free plan
result.Fetcher.InvalidateCache(); result.Fetcher.InvalidateCache();
var exchangeRates = result.ResultAsync.Result; var exchangeRates = result.ResultAsync.Result;
result.Fetcher.InvalidateCache(); result.Fetcher.InvalidateCache();
@ -2782,7 +2785,9 @@ noninventoryitem:
public static RateProviderFactory CreateBTCPayRateFactory() 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() private static MemoryCacheOptions CreateMemoryCache()

View File

@ -9,7 +9,6 @@ using BTCPayServer.Configuration;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Changelly; using BTCPayServer.Payments.Changelly;
@ -23,15 +22,12 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin; using NBitcoin;
using NBitcoin.DataEncoders; using NBitcoin.DataEncoders;
using Newtonsoft.Json;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
@ -197,16 +193,17 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{storeId}/rates")] [Route("{storeId}/rates")]
public IActionResult Rates() public async Task<IActionResult> Rates()
{ {
var exchanges = await GetSupportedExchanges();
var storeBlob = CurrentStore.GetStoreBlob(); var storeBlob = CurrentStore.GetStoreBlob();
var vm = new RatesViewModel(); 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.Spread = (double)(storeBlob.Spread * 100m);
vm.StoreId = CurrentStore.Id; vm.StoreId = CurrentStore.Id;
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString(); vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString(); vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
vm.AvailableExchanges = GetSupportedExchanges(); vm.AvailableExchanges = exchanges;
vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString(); vm.DefaultCurrencyPairs = storeBlob.GetDefaultCurrencyPairString();
vm.ShowScripting = storeBlob.RateScripting; vm.ShowScripting = storeBlob.RateScripting;
return View(vm); return View(vm);
@ -216,7 +213,16 @@ namespace BTCPayServer.Controllers
[Route("{storeId}/rates")] [Route("{storeId}/rates")]
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default) public async Task<IActionResult> 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; model.StoreId = storeId ?? model.StoreId;
CurrencyPair[] currencyPairs = null; CurrencyPair[] currencyPairs = null;
try try
@ -239,14 +245,14 @@ namespace BTCPayServer.Controllers
var blob = CurrentStore.GetStoreBlob(); var blob = CurrentStore.GetStoreBlob();
model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString(); model.DefaultScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
model.AvailableExchanges = GetSupportedExchanges(); model.AvailableExchanges = exchanges;
blob.PreferredExchange = model.PreferredExchange; blob.PreferredExchange = model.PreferredExchange;
blob.Spread = (decimal)model.Spread / 100.0m; blob.Spread = (decimal)model.Spread / 100.0m;
blob.DefaultCurrencyPairs = currencyPairs; blob.DefaultCurrencyPairs = currencyPairs;
if (!model.ShowScripting) 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})"); ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
return View(model); return View(model);
@ -597,13 +603,13 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
} }
private CoinAverageExchange[] GetSupportedExchanges() private async Task<IEnumerable<AvailableRateProvider>> GetSupportedExchanges()
{ {
return _RateFactory.RateProviderFactory.GetSupportedExchanges() var exchanges = await _RateFactory.RateProviderFactory.GetSupportedExchanges();
.Where(r => !string.IsNullOrWhiteSpace(r.Value.Display)) return exchanges
.Select(c => c.Value) .Where(r => !string.IsNullOrWhiteSpace(r.Name))
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase) .OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase);
.ToArray();
} }
private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network) private DerivationSchemeSettings ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)

View File

@ -10,6 +10,7 @@ using BTCPayServer.Rating;
using BTCPayServer.Services.Mails; using BTCPayServer.Services.Mails;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Text; using System.Text;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
@ -156,7 +157,7 @@ namespace BTCPayServer.Data
} }
} }
var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? "coinaverage" : PreferredExchange; var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? CoinGeckoRateProvider.CoinGeckoName : PreferredExchange;
builder.AppendLine($"X_X = {preferredExchange}(X_X);"); builder.AppendLine($"X_X = {preferredExchange}(X_X);");
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules); BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);

View File

@ -51,7 +51,7 @@ namespace BTCPayServer.Data
{ {
var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(Encoding.UTF8.GetString(storeData.StoreBlob)); var result = storeData.StoreBlob == null ? new StoreBlob() : new Serializer(null).ToObject<StoreBlob>(Encoding.UTF8.GetString(storeData.StoreBlob));
if (result.PreferredExchange == null) if (result.PreferredExchange == null)
result.PreferredExchange = CoinAverageRateProvider.CoinAverageName; result.PreferredExchange = CoinGeckoRateProvider.CoinGeckoName;
return result; return result;
} }

View File

@ -146,14 +146,7 @@ namespace BTCPayServer.HostedServices
async Task RefreshCoinAverageSupportedExchanges() async Task RefreshCoinAverageSupportedExchanges()
{ {
var exchanges = new CoinAverageExchanges(); await _RateProviderFactory.InitExchanges();
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 Task.Delay(TimeSpan.FromHours(5), Cancellation); await Task.Delay(TimeSpan.FromHours(5), Cancellation);
} }

View File

@ -1,8 +1,6 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
@ -17,19 +15,12 @@ namespace BTCPayServer.Models.StoreViewModels
public string Rule { get; set; } public string Rule { get; set; }
public bool Error { get; set; } public bool Error { get; set; }
} }
class Format public void SetExchangeRates(IEnumerable<AvailableRateProvider> supportedList, string preferredExchange)
{
public string Name { get; set; }
public string Value { get; set; }
public string Url { get; set; }
}
public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange)
{ {
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName; var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName;
var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name, Url = o.Url }).ToArray(); var chosen = supportedList.FirstOrDefault(f => f.Id == defaultStore) ?? supportedList.FirstOrDefault();
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault(); Exchanges = new SelectList(supportedList, nameof(chosen.Id), nameof(chosen.Name), chosen);
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen); PreferredExchange = chosen.Id;
PreferredExchange = chosen.Value;
RateSource = chosen.Url; RateSource = chosen.Url;
} }
@ -46,7 +37,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string ScriptTest { get; set; } public string ScriptTest { get; set; }
public string DefaultCurrencyPairs { get; set; } public string DefaultCurrencyPairs { get; set; }
public string StoreId { get; set; } public string StoreId { get; set; }
public CoinAverageExchange[] AvailableExchanges { get; set; } public IEnumerable<AvailableRateProvider> AvailableExchanges { get; set; }
[Display(Name = "Add a spread on exchange rate of ... %")] [Display(Name = "Add a spread on exchange rate of ... %")]
[Range(0.0, 100.0)] [Range(0.0, 100.0)]

View File

@ -19,11 +19,11 @@
<div class="form-group"> <div class="form-group">
<h5>Scripting</h5> <h5>Scripting</h5>
<span>Rate script allows you to express precisely how you want to calculate rates for currency pairs.</span> <span>Rate script allows you to express precisely how you want to calculate rates for currency pairs.</span>
<p class="text-muted"> <p class="text-muted overflow-auto" style="max-height: 300px">
<b>Supported exchanges are</b>: <b>Supported exchanges are</b>:
@for (int i = 0; i < Model.AvailableExchanges.Length; i++) @for (int i = 0; i < Model.AvailableExchanges.Count(); i++)
{ {
<a href="@Model.AvailableExchanges[i].Url">@Model.AvailableExchanges[i].Name</a><span>@(i == Model.AvailableExchanges.Length - 1 ? "" : ",")</span> <a href="@Model.AvailableExchanges.ElementAt(i).Url">@Model.AvailableExchanges.ElementAt(i).Name</a><span>@(i == Model.AvailableExchanges.Count() - 1 ? "" : ",")</span>
} }
</p> </p>
<p><a href="#help" data-toggle="collapse"><b>Click here for more information</b></a></p> <p><a href="#help" data-toggle="collapse"><b>Click here for more information</b></a></p>
@ -116,7 +116,7 @@
<a href="#" onclick="$('#Script').val(defaultScript); return false;">Set to default settings</a> <a href="#" onclick="$('#Script').val(defaultScript); return false;">Set to default settings</a>
</div> </div>
<div class="form-group"> <div class="form-group">
<a asp-action="ShowRateRules" asp-route-scripting="false">Turn off advanced rate rule scripting</a> <button type="submit" class="btn btn-link" value="scripting-off" name="command">Turn off advanced rate rule scripting</button>
</div> </div>
} }
else else
@ -130,7 +130,7 @@
</p> </p>
</div> </div>
<div class="form-group"> <div class="form-group">
<a asp-action="ShowRateRules" asp-route-scripting="true">Turn on advanced rate rule scripting</a> <button type="submit" class="btn btn-link" value="scripting-on" name="command">Turn on advanced rate rule scripting</button>
</div> </div>
} }
<div class="form-group"> <div class="form-group">