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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Rating
{

View File

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

View File

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

View File

@ -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,124 +18,11 @@ namespace BTCPayServer.Services.Rates
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
{
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)
{

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.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

View File

@ -57,7 +57,6 @@ namespace BTCPayServer.Services.Rates
_CacheOptions = cacheOptions;
// We use 15 min because of limits with free version of bitcoinaverage
CacheSpan = TimeSpan.FromMinutes(15.0);
InitExchanges();
}
private IOptions<MemoryCacheOptions> _CacheOptions;
TimeSpan _CacheSpan;
@ -81,7 +80,7 @@ namespace BTCPayServer.Services.Rates
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 @@ 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
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));
// 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 @@ namespace BTCPayServer.Services.Rates
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 @@ namespace BTCPayServer.Services.Rates
}
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<IEnumerable<AvailableRateProvider>> GetSupportedExchanges(bool reload = false)
{
CoinAverageExchanges exchanges = new CoinAverageExchanges();
foreach (var exchange in _CoinAverageSettings.AvailableExchanges)
IEnumerable<AvailableRateProvider> 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<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)

View File

@ -202,29 +202,29 @@ namespace BTCPayServer.Tests
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 @@ namespace BTCPayServer.Tests
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"),
});
}

View File

@ -8,12 +8,23 @@ using BTCPayServer.Services.Rates;
namespace BTCPayServer.Tests.Mocks
{
public class MockRateProvider : IRateProvider
public class MockRateProvider : CoinGeckoRateProvider
{
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);
}
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);
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";
store.Rates(ratesVM).Wait();
store = acc.GetController<StoresController>();
@ -1240,7 +1240,7 @@ namespace BTCPayServer.Tests
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
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 bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY");
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")
{
var storeController = user.GetController<StoresController>();
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 @@ namespace BTCPayServer.Tests
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
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);
vm.Spread = 40;
storeController.Rates(vm).Wait();
@ -1438,15 +1438,15 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
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.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<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);
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
@ -1463,7 +1463,7 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(store.ShowRateRulesPost(true).Result);
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
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.DefaultScript, rateVm.Script);
Assert.True(rateVm.ShowScripting);
@ -1475,13 +1475,13 @@ namespace BTCPayServer.Tests
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<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
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.True(rateVm.ShowScripting);
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))
.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 @@ noninventoryitem:
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()

View File

@ -9,7 +9,6 @@ using BTCPayServer.Configuration;
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.Rates;
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 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("{storeId}/rates")]
public IActionResult Rates()
public async Task<IActionResult> 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 @@ namespace BTCPayServer.Controllers
[Route("{storeId}/rates")]
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;
CurrencyPair[] currencyPairs = null;
try
@ -239,14 +245,14 @@ namespace BTCPayServer.Controllers
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 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
private CoinAverageExchange[] GetSupportedExchanges()
private async Task<IEnumerable<AvailableRateProvider>> 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)

View File

@ -10,6 +10,7 @@ using BTCPayServer.Rating;
using BTCPayServer.Services.Mails;
using Newtonsoft.Json;
using System.Text;
using BTCPayServer.Services.Rates;
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);");
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));
if (result.PreferredExchange == null)
result.PreferredExchange = CoinAverageRateProvider.CoinAverageName;
result.PreferredExchange = CoinGeckoRateProvider.CoinGeckoName;
return result;
}

View File

@ -146,14 +146,7 @@ namespace BTCPayServer.HostedServices
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);
}

View File

@ -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 @@ namespace BTCPayServer.Models.StoreViewModels
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<AvailableRateProvider> 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 @@ namespace BTCPayServer.Models.StoreViewModels
public string ScriptTest { get; set; }
public string DefaultCurrencyPairs { 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 ... %")]
[Range(0.0, 100.0)]

View File

@ -19,11 +19,11 @@
<div class="form-group">
<h5>Scripting</h5>
<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>:
@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><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>
</div>
<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>
}
else
@ -130,7 +130,7 @@
</p>
</div>
<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 class="form-group">