mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 21:32:27 +01:00
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:
parent
1a3da096a7
commit
58d9a48787
16
BTCPayServer.Rating/AvailableRateProvider.cs
Normal file
16
BTCPayServer.Rating/AvailableRateProvider.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
{
|
||||
|
@ -3,7 +3,6 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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 @@ 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)
|
||||
{
|
||||
var signature = GetCoinAverageSignature();
|
||||
|
96
BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs
Normal file
96
BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)]
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user