Keep coinaverage compatibility, improve UX, hardcode feed provider supported exchanges

This commit is contained in:
nicolas.dorier
2020-01-13 22:20:45 +09:00
parent 58d9a48787
commit 5dbdb4b399
16 changed files with 280 additions and 167 deletions

View File

@@ -21,6 +21,11 @@ namespace BTCPayServer
: "http://explorer.litecointools.com/tx/{0}", : "http://explorer.litecointools.com/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork, NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "litecoin", UriScheme = "litecoin",
DefaultRateRules = new[]
{
"LTC_X = LTC_BTC * BTC_X",
"LTC_BTC = coingecko(LTC_BTC)"
},
CryptoImagePath = "imlegacy/litecoin.svg", CryptoImagePath = "imlegacy/litecoin.svg",
LightningImagePath = "imlegacy/litecoin-lightning.svg", LightningImagePath = "imlegacy/litecoin-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),

View File

@@ -14,6 +14,11 @@ namespace BTCPayServer
NetworkType == NetworkType.Mainnet NetworkType == NetworkType.Mainnet
? "https://www.exploremonero.com/transaction/{0}" ? "https://www.exploremonero.com/transaction/{0}"
: "https://testnet.xmrchain.net/tx/{0}", : "https://testnet.xmrchain.net/tx/{0}",
DefaultRateRules = new[]
{
"XMR_X = XMR_BTC * BTC_X",
"XMR_BTC = kraken(XMR_BTC)"
},
CryptoImagePath = "/imlegacy/monero.svg" CryptoImagePath = "/imlegacy/monero.svg"
}); });
} }

View File

@@ -1,16 +1,24 @@
namespace BTCPayServer.Rating namespace BTCPayServer.Rating
{ {
public enum RateSource
{
Coingecko,
CoinAverage,
Direct
}
public class AvailableRateProvider public class AvailableRateProvider
{ {
public string Name { get; set; } public string Name { get; }
public string Url { get; set; } public string Url { get; }
public string Id { get; set; } public string Id { get; }
public RateSource Source { get; }
public AvailableRateProvider(string id, string name, string url) public AvailableRateProvider(string id, string name, string url, RateSource source)
{ {
Id = id; Id = id;
Name = name; Name = name;
Url = url; Url = url;
Source = source;
} }
} }
} }

View File

@@ -19,19 +19,6 @@ namespace BTCPayServer.Services.Rates
} }
} }
public class GetExchangeTickersResponse
{
public class Exchange
{
public string Name { get; set; }
[JsonProperty("display_name")]
public string DisplayName { get; set; }
public string[] Symbols { get; set; }
}
public bool Success { get; set; }
public Exchange[] Exchanges { get; set; }
}
public class RatesSetting public class RatesSetting
{ {
public string PublicKey { get; set; } public string PublicKey { get; set; }
@@ -196,32 +183,6 @@ namespace BTCPayServer.Services.Rates
response.RequestsPerPeriod = jobj["requests_per_period"].Value<int>(); response.RequestsPerPeriod = jobj["requests_per_period"].Value<int>();
return response; return response;
} }
public async Task<GetExchangeTickersResponse> GetExchangeTickersAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/symbols/exchanges/ticker");
var auth = Authenticator;
if (auth != null)
{
await auth.AddHeader(request);
}
var resp = await HttpClient.SendAsync(request);
resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
var response = new GetExchangeTickersResponse();
response.Success = jobj["success"].Value<bool>();
var exchanges = (JObject)jobj["exchanges"];
response.Exchanges = exchanges
.Properties()
.Select(p =>
{
var exchange = JsonConvert.DeserializeObject<GetExchangeTickersResponse.Exchange>(p.Value.ToString());
exchange.Name = p.Name;
return exchange;
})
.ToArray();
return response;
}
} }
public class GetRateLimitsResponse public class GetRateLimitsResponse

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,7 @@ using ExchangeSharp;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache; using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
@@ -57,6 +58,7 @@ 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;
@@ -96,8 +98,22 @@ namespace BTCPayServer.Services.Rates
return _DirectProviders; return _DirectProviders;
} }
} }
internal IEnumerable<AvailableRateProvider> GetDirectlySupportedExchanges()
{
yield return new AvailableRateProvider("binance", "Binance", "https://api.binance.com/api/v1/ticker/24hr", RateSource.Direct);
yield return new AvailableRateProvider("bittrex", "Bittrex", "https://bittrex.com/api/v1.1/public/getmarketsummaries", RateSource.Direct);
yield return new AvailableRateProvider("poloniex", "Poloniex", "https://poloniex.com/public?command=returnTicker", RateSource.Direct);
yield return new AvailableRateProvider("hitbtc", "HitBTC", "https://api.hitbtc.com/api/2/public/ticker", RateSource.Direct);
yield return new AvailableRateProvider("ndax", "NDAX", "https://ndax.io/api/returnTicker", RateSource.Direct);
public async Task InitExchanges() yield return new AvailableRateProvider(CoinGeckoRateProvider.CoinGeckoName, "Coin Gecko", "https://api.coingecko.com/api/v3/exchange_rates", RateSource.Direct);
yield return new AvailableRateProvider(CoinAverageRateProvider.CoinAverageName, "Coin Average", "https://apiv2.bitcoinaverage.com/indices/global/ticker/short", RateSource.Direct);
yield return new AvailableRateProvider("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD", RateSource.Direct);
yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", RateSource.Direct);
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices", RateSource.Direct);
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates", RateSource.Direct);
}
void 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));
@@ -106,10 +122,6 @@ namespace BTCPayServer.Services.Rates
Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitBTCAPI(), true)); Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitBTCAPI(), true));
Providers.Add("ndax", new ExchangeSharpRateProvider("ndax", new ExchangeNDAXAPI(), true)); Providers.Add("ndax", new ExchangeSharpRateProvider("ndax", new ExchangeNDAXAPI(), true));
// Cryptopia is often not available
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
// Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers // Handmade providers
Providers.Add(CoinGeckoRateProvider.CoinGeckoName, new CoinGeckoRateProvider(_httpClientFactory)); 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 });
@@ -129,7 +141,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 == CoinGeckoRateProvider.CoinGeckoName) 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,7 +155,22 @@ namespace BTCPayServer.Services.Rates
} }
var cache = new MemoryCache(_CacheOptions); var cache = new MemoryCache(_CacheOptions);
foreach (var supportedExchange in await GetSupportedExchanges(true)) foreach (var supportedExchange in GetCoinGeckoSupportedExchanges())
{
if (!Providers.ContainsKey(supportedExchange.Id))
{
var coinAverage = new CoinGeckoRateProvider(_httpClientFactory)
{
Exchange = supportedExchange.Id
};
var cached = new CachedRateProvider(supportedExchange.Id, coinAverage, cache)
{
CacheSpan = CacheSpan
};
Providers.Add(supportedExchange.Id, cached);
}
}
foreach (var supportedExchange in GetCoinAverageSupportedExchanges())
{ {
if (!Providers.ContainsKey(supportedExchange.Id)) if (!Providers.ContainsKey(supportedExchange.Id))
{ {
@@ -160,34 +187,114 @@ namespace BTCPayServer.Services.Rates
} }
} }
public async Task<IEnumerable<AvailableRateProvider>> GetSupportedExchanges(bool reload = false) IEnumerable<AvailableRateProvider> _AvailableRateProviders = null;
public IEnumerable<AvailableRateProvider> GetSupportedExchanges()
{ {
IEnumerable<AvailableRateProvider> exchanges; if (_AvailableRateProviders == null)
switch (Providers[CoinGeckoRateProvider.CoinGeckoName])
{ {
case BackgroundFetcherRateProvider backgroundFetcherRateProvider: var availableProviders = new Dictionary<string, AvailableRateProvider>();
exchanges = await ((CoinGeckoRateProvider)((BackgroundFetcherRateProvider)Providers[ foreach (var exchange in GetDirectlySupportedExchanges())
CoinGeckoRateProvider.CoinGeckoName]).Inner).GetAvailableExchanges(reload); {
break; availableProviders.Add(exchange.Id, exchange);
case CoinGeckoRateProvider coinGeckoRateProvider: }
exchanges = await coinGeckoRateProvider.GetAvailableExchanges(reload); foreach (var exchange in GetCoinGeckoSupportedExchanges())
break; {
default: availableProviders.TryAdd(exchange.Id, exchange);
exchanges = new AvailableRateProvider[0]; }
break; foreach (var exchange in GetCoinAverageSupportedExchanges())
{
availableProviders.TryAdd(exchange.Id, exchange);
}
_AvailableRateProviders = availableProviders.Values.OrderBy(o => o.Name).ToArray();
} }
// Add other exchanges supported here return _AvailableRateProviders;
return new[] }
internal IEnumerable<AvailableRateProvider> GetCoinAverageSupportedExchanges()
{
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"),
})
{ {
new AvailableRateProvider(CoinGeckoRateProvider.CoinGeckoName, "Coin Gecko", yield return new AvailableRateProvider(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}", RateSource.CoinAverage);
"https://api.coingecko.com/api/v3/exchange_rates"), }
new AvailableRateProvider("bylls", "Bylls", yield return new AvailableRateProvider("gdax", string.Empty, $"https://apiv2.bitcoinaverage.com/exchanges/gdax", RateSource.CoinAverage);
"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"), internal IEnumerable<AvailableRateProvider> GetCoinGeckoSupportedExchanges()
new AvailableRateProvider(CoinAverageRateProvider.CoinAverageName, "Coin Average", {
"https://apiv2.bitcoinaverage.com/indices/global/ticker/short") return JArray.Parse(CoinGeckoRateProvider.SupportedExchanges).Select(token =>
}.Concat(exchanges); new AvailableRateProvider(Normalize(token["id"].ToString().ToLowerInvariant()), token["name"].ToString(),
$"https://api.coingecko.com/api/v3/exchanges/{token["id"]}/tickers", RateSource.Coingecko))
.Concat(new[] { new AvailableRateProvider("gdax", string.Empty, $"https://api.coingecko.com/api/v3/exchanges/gdax", RateSource.Coingecko) });
}
private string Normalize(string name)
{
if (name == "oasis_trade")
return "oasisdex";
if (name == "gdax")
return "coinbasepro";
return name;
} }
public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken) public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)

View File

@@ -215,8 +215,8 @@ namespace BTCPayServer.Tests
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{ {
Exchange = "coingecko", Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"), CurrencyPair = CurrencyPair.Parse("BTC_LTC"),
BidAsk = new BidAsk(0.001m) BidAsk = new BidAsk(162m)
}); });
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{ {
@@ -262,15 +262,6 @@ 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,23 +8,17 @@ using BTCPayServer.Services.Rates;
namespace BTCPayServer.Tests.Mocks namespace BTCPayServer.Tests.Mocks
{ {
public class MockRateProvider : CoinGeckoRateProvider public class MockRateProvider : IRateProvider
{ {
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates(); public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
public List<AvailableRateProvider> AvailableRateProviders { get; set; } = new List<AvailableRateProvider>();
public MockRateProvider():base(null) public MockRateProvider()
{ {
} }
public override Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken) public 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,9 +962,9 @@ 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>(await store.Rates()).Model); var ratesVM = (RatesViewModel)(Assert.IsType<ViewResult>(store.Rates()).Model);
ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD"; ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD";
store.Rates(ratesVM).Wait(); await store.Rates(ratesVM);
store = acc.GetController<StoresController>(); store = acc.GetController<StoresController>();
rateController = acc.GetController<RateController>(); rateController = acc.GetController<RateController>();
GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, default) GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, default)
@@ -1240,9 +1240,9 @@ 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, "coingecko")); rates.Add(await CreateInvoice(tester, user, "coingecko"));
var bitflyer = CreateInvoice(tester, user, "bitflyer", "JPY"); var bitflyer = await CreateInvoice(tester, user, "bitflyer", "JPY");
var bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY"); var bitflyer2 = await CreateInvoice(tester, user, "bitflyer", "JPY");
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
rates.Add(bitflyer); rates.Add(bitflyer);
@@ -1253,13 +1253,13 @@ namespace BTCPayServer.Tests
} }
} }
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD") private static async Task<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().GetAwaiter().GetResult()).Model; var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
vm.PreferredExchange = exchange; vm.PreferredExchange = exchange;
storeController.Rates(vm).Wait(); await storeController.Rates(vm);
var invoice2 = user.BitPay.CreateInvoice(new Invoice() var invoice2 = await user.BitPay.CreateInvoiceAsync(new Invoice()
{ {
Price = 5000.0m, Price = 5000.0m,
Currency = currency, Currency = currency,
@@ -1337,10 +1337,10 @@ 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().GetAwaiter().GetResult()).Model; var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
Assert.Equal(0.0, vm.Spread); Assert.Equal(0.0, vm.Spread);
vm.Spread = 40; vm.Spread = 40;
storeController.Rates(vm).Wait(); await storeController.Rates(vm);
var invoice2 = user.BitPay.CreateInvoice(new Invoice() var invoice2 = user.BitPay.CreateInvoice(new Invoice()
@@ -1438,37 +1438,37 @@ 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>( await store.Rates()).Model); var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.False(rateVm.ShowScripting); Assert.False(rateVm.ShowScripting);
Assert.Equal(CoinGeckoRateProvider.CoinGeckoName, 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>(await store.Rates(rateVm, "Save"));
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>( await store.Rates()).Model); rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(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";
rateVm.Spread = 10; rateVm.Spread = 10;
store = user.GetController<StoresController>(); store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model); rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(await store.Rates(rateVm, "Test")).Model);
Assert.NotNull(rateVm.TestRateRules); Assert.NotNull(rateVm.TestRateRules);
Assert.Equal(2, rateVm.TestRateRules.Count); Assert.Equal(2, rateVm.TestRateRules.Count);
Assert.False(rateVm.TestRateRules[0].Error); Assert.False(rateVm.TestRateRules[0].Error);
Assert.StartsWith("(bitflyer(BTC_JPY)) * (0.9, 1.1) =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase); Assert.StartsWith("(bitflyer(BTC_JPY)) * (0.9, 1.1) =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
Assert.True(rateVm.TestRateRules[1].Error); Assert.True(rateVm.TestRateRules[1].Error);
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result); Assert.IsType<RedirectToActionResult>(await store.Rates(rateVm, "Save"));
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>(await store.Rates(rateVm, "Save"));
store = user.GetController<StoresController>(); store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>( await store.Rates()).Model); rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(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);
rateVm.ScriptTest = "BTC_JPY"; rateVm.ScriptTest = "BTC_JPY";
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model); rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(await store.Rates(rateVm, "Test")).Model);
Assert.True(rateVm.ShowScripting); Assert.True(rateVm.ShowScripting);
Assert.Contains("(bitflyer(BTC_JPY)) * (0.9, 1.1) = ", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase); Assert.Contains("(bitflyer(BTC_JPY)) * (0.9, 1.1) = ", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
@@ -1477,11 +1477,11 @@ namespace BTCPayServer.Tests
"X_CAD = quadrigacx(X_CAD);\n" + "X_CAD = quadrigacx(X_CAD);\n" +
"X_X = coingecko(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>(await store.Rates(rateVm, "Test")).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>(await store.Rates(rateVm, "Save"));
store = user.GetController<StoresController>(); store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>( await store.Rates()).Model); rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(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);
@@ -1569,7 +1569,7 @@ namespace BTCPayServer.Tests
Assert.NotNull(ltcCryptoInfo); Assert.NotNull(ltcCryptoInfo);
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network); invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture)); var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money... cashCow.Generate(4); // LTC is not worth a lot, so just to make sure we have money...
cashCow.SendToAddress(invoiceAddress, secondPayment); cashCow.SendToAddress(invoiceAddress, secondPayment);
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress); Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
TestUtils.Eventually(() => TestUtils.Eventually(() =>
@@ -2680,7 +2680,7 @@ noninventoryitem:
public void CanQueryDirectProviders() public void CanQueryDirectProviders()
{ {
var factory = CreateBTCPayRateFactory(); var factory = CreateBTCPayRateFactory();
var all = string.Join("\r\n", factory.GetSupportedExchanges().Select(e => e.Id).ToArray());
foreach (var result in factory foreach (var result in factory
.Providers .Providers
.Where(p => p.Value is BackgroundFetcherRateProvider) .Where(p => p.Value is BackgroundFetcherRateProvider)
@@ -2785,9 +2785,7 @@ noninventoryitem:
public static RateProviderFactory CreateBTCPayRateFactory() public static RateProviderFactory CreateBTCPayRateFactory()
{ {
var result = new RateProviderFactory(CreateMemoryCache(), new MockHttpClientFactory(), new CoinAverageSettings()); return new RateProviderFactory(CreateMemoryCache(), new MockHttpClientFactory(), new CoinAverageSettings());
result.InitExchanges().GetAwaiter().GetResult();
return result;
} }
private static MemoryCacheOptions CreateMemoryCache() private static MemoryCacheOptions CreateMemoryCache()

View File

@@ -86,14 +86,14 @@ namespace BTCPayServer.Configuration
var supportedChains = conf.GetOrDefault<string>("chains", "btc") var supportedChains = conf.GetOrDefault<string>("chains", "btc")
.Split(',', StringSplitOptions.RemoveEmptyEntries) .Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.ToUpperInvariant()).ToList(); .Select(t => t.ToUpperInvariant()).ToHashSet();
var networkProvider = new BTCPayNetworkProvider(NetworkType); var networkProvider = new BTCPayNetworkProvider(NetworkType);
var filtered = networkProvider.Filter(supportedChains.ToArray()); var filtered = networkProvider.Filter(supportedChains.ToArray());
var elementsBased = filtered.GetAll().OfType<ElementsBTCPayNetwork>(); var elementsBased = filtered.GetAll().OfType<ElementsBTCPayNetwork>();
var parentChains = elementsBased.Select(network => network.NetworkCryptoCode.ToUpperInvariant()).Distinct(); var parentChains = elementsBased.Select(network => network.NetworkCryptoCode.ToUpperInvariant()).Distinct();
var allSubChains = networkProvider.GetAll().OfType<ElementsBTCPayNetwork>() var allSubChains = networkProvider.GetAll().OfType<ElementsBTCPayNetwork>()
.Where(network => parentChains.Contains(network.NetworkCryptoCode)).Select(network => network.CryptoCode); .Where(network => parentChains.Contains(network.NetworkCryptoCode)).Select(network => network.CryptoCode.ToUpperInvariant());
supportedChains.AddRange(allSubChains); supportedChains.AddRange(allSubChains);
NetworkProvider = networkProvider.Filter(supportedChains.ToArray()); NetworkProvider = networkProvider.Filter(supportedChains.ToArray());
foreach (var chain in supportedChains) foreach (var chain in supportedChains)

View File

@@ -193,9 +193,9 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("{storeId}/rates")] [Route("{storeId}/rates")]
public async Task<IActionResult> Rates() public IActionResult Rates()
{ {
var exchanges = await GetSupportedExchanges(); var exchanges = GetSupportedExchanges();
var storeBlob = CurrentStore.GetStoreBlob(); var storeBlob = CurrentStore.GetStoreBlob();
var vm = new RatesViewModel(); var vm = new RatesViewModel();
vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? CoinGeckoRateProvider.CoinGeckoName); vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? CoinGeckoRateProvider.CoinGeckoName);
@@ -221,7 +221,7 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ShowRateRules), new {scripting = false, storeId = model.StoreId}); return RedirectToAction(nameof(ShowRateRules), new {scripting = false, storeId = model.StoreId});
} }
var exchanges = await GetSupportedExchanges(); var exchanges = GetSupportedExchanges();
model.SetExchangeRates(exchanges, model.PreferredExchange); model.SetExchangeRates(exchanges, model.PreferredExchange);
model.StoreId = storeId ?? model.StoreId; model.StoreId = storeId ?? model.StoreId;
CurrencyPair[] currencyPairs = null; CurrencyPair[] currencyPairs = null;
@@ -338,7 +338,7 @@ namespace BTCPayServer.Controllers
Description = scripting ? Description = scripting ?
"This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)" "This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?", : "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
ButtonClass = "btn-primary" ButtonClass = scripting ? "btn-primary" : "btn-danger"
}); });
} }
@@ -603,9 +603,9 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores"); return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
} }
private async Task<IEnumerable<AvailableRateProvider>> GetSupportedExchanges() private IEnumerable<AvailableRateProvider> GetSupportedExchanges()
{ {
var exchanges = await _RateFactory.RateProviderFactory.GetSupportedExchanges(); var exchanges = _RateFactory.RateProviderFactory.GetSupportedExchanges();
return exchanges return exchanges
.Where(r => !string.IsNullOrWhiteSpace(r.Name)) .Where(r => !string.IsNullOrWhiteSpace(r.Name))
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase); .OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase);

View File

@@ -44,7 +44,6 @@ namespace BTCPayServer.HostedServices
{ {
return new Task[] return new Task[]
{ {
CreateLoopTask(RefreshCoinAverageSupportedExchanges),
CreateLoopTask(RefreshCoinAverageSettings), CreateLoopTask(RefreshCoinAverageSettings),
CreateLoopTask(RefreshRates) CreateLoopTask(RefreshRates)
}; };
@@ -144,12 +143,6 @@ namespace BTCPayServer.HostedServices
await _SettingsRepository.UpdateSetting(cache); await _SettingsRepository.UpdateSetting(cache);
} }
async Task RefreshCoinAverageSupportedExchanges()
{
await _RateProviderFactory.InitExchanges();
await Task.Delay(TimeSpan.FromHours(5), Cancellation);
}
async Task RefreshCoinAverageSettings() async Task RefreshCoinAverageSettings()
{ {
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting(); var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using BTCPayServer.Rating; using BTCPayServer.Rating;
@@ -17,13 +18,29 @@ namespace BTCPayServer.Models.StoreViewModels
} }
public void SetExchangeRates(IEnumerable<AvailableRateProvider> supportedList, string preferredExchange) public void SetExchangeRates(IEnumerable<AvailableRateProvider> supportedList, string preferredExchange)
{ {
var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName; var defaultStore = preferredExchange ?? CoinGeckoRateProvider.CoinGeckoName;
supportedList = supportedList.Select(a => new AvailableRateProvider(a.Id, GetName(a), a.Url, a.Source)).ToArray();
var chosen = supportedList.FirstOrDefault(f => f.Id == defaultStore) ?? supportedList.FirstOrDefault(); var chosen = supportedList.FirstOrDefault(f => f.Id == defaultStore) ?? supportedList.FirstOrDefault();
Exchanges = new SelectList(supportedList, nameof(chosen.Id), nameof(chosen.Name), chosen); Exchanges = new SelectList(supportedList, nameof(chosen.Id), nameof(chosen.Name), chosen);
PreferredExchange = chosen.Id; PreferredExchange = chosen.Id;
RateSource = chosen.Url; RateSource = chosen.Url;
} }
private string GetName(AvailableRateProvider a)
{
switch (a.Source)
{
case Rating.RateSource.Direct:
return a.Name;
case Rating.RateSource.Coingecko:
return $"{a.Name} (via CoinGecko, free)";
case Rating.RateSource.CoinAverage:
return $"{a.Name} (via BitcoinAverage, commercial)";
default:
throw new NotSupportedException(a.Source.ToString());
}
}
public List<TestResultViewModel> TestRateRules { get; set; } public List<TestResultViewModel> TestRateRules { get; set; }
public SelectList Exchanges { get; set; } public SelectList Exchanges { get; set; }

View File

@@ -44,7 +44,7 @@
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true", "BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
"BTCPAY_DISABLE-REGISTRATION": "false", "BTCPAY_DISABLE-REGISTRATION": "false",
"ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,lbtc", "BTCPAY_CHAINS": "btc,ltc,lbtc",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver", "BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
"BTCPAY_EXTERNALSERVICES": "totoservice:totolink;", "BTCPAY_EXTERNALSERVICES": "totoservice:totolink;",
"BTCPAY_SSHCONNECTION": "root@127.0.0.1:21622", "BTCPAY_SSHCONNECTION": "root@127.0.0.1:21622",

View File

@@ -27,7 +27,7 @@
<div class="row"> <div class="row">
<div class="col-lg-12 text-center"> <div class="col-lg-12 text-center">
<form method="post"> <form method="post">
<button id="continue" type="submit" class="btn btn-secondary @Model.ButtonClass w-25">@Model.Action</button> <button id="continue" type="submit" class="btn @Model.ButtonClass w-25">@Model.Action</button>
<button type="submit" class="btn btn-secondary w-25" onclick="history.back(); return false;">Go back</button> <button type="submit" class="btn btn-secondary w-25" onclick="history.back(); return false;">Go back</button>
</form> </form>
</div> </div>

View File

@@ -18,15 +18,61 @@
{ {
<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> <p>Rate script allows you to express precisely how you want to calculate rates for currency pairs.</p>
<p class="text-muted overflow-auto" style="max-height: 300px"> <p>We are retrieving the rate of each exchange either directly, via <a href="https://www.coingecko.com/" target="_blank">CoinGecko (free)</a> or <a href="https://bitcoinaverage.com/" target="_blank">BitcoinAverage (commercial)</a></p>
<b>Supported exchanges are</b>: <div class="accordion" id="accordion-info">
@for (int i = 0; i < Model.AvailableExchanges.Count(); i++) <div class="card">
{ <div class="card-header" id="direct-header">
<a href="@Model.AvailableExchanges.ElementAt(i).Url">@Model.AvailableExchanges.ElementAt(i).Name</a><span>@(i == Model.AvailableExchanges.Count() - 1 ? "" : ",")</span> <h2 class="mb-0">
} <button class="btn btn-link" type="button" data-toggle="collapse" data-target="#direct-content" aria-expanded="true">
</p> Direct integration
<p><a href="#help" data-toggle="collapse"><b>Click here for more information</b></a></p> </button>
</h2>
</div>
<div class="collapse" id="direct-content">
<div class="card-body text-muted overflow-auto">
@foreach (var exchange in Model.AvailableExchanges.Where(a => a.Source == BTCPayServer.Rating.RateSource.Direct))
{
<a href="@exchange.Url">@exchange.Id</a><span>&nbsp;</span>
}
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="coingecko-header">
<h2 class="mb-0">
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#coingecko-content" aria-expanded="true">
Coingecko integration
</button>
</h2>
</div>
<div id="coingecko-content" class="collapse">
<div class="card-body text-muted overflow-auto">
@foreach (var exchange in Model.AvailableExchanges.Where(a => a.Source == BTCPayServer.Rating.RateSource.Coingecko))
{
<a href="@exchange.Url">@exchange.Id</a><span>&nbsp;</span>
}
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="coinaverage-header">
<h2 class="mb-0">
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#coinaverage-content" aria-expanded="true">
CoinAverage integration (commercial API)
</button>
</h2>
</div>
<div id="coinaverage-content" class="collapse">
<div class="card-body text-muted overflow-auto">
@foreach (var exchange in Model.AvailableExchanges.Where(a => a.Source == BTCPayServer.Rating.RateSource.CoinAverage))
{
<a href="@exchange.Url">@exchange.Id</a><span>&nbsp;</span>
}
</div>
</div>
</div>
</div>
</div> </div>
} }
@if (Model.TestRateRules != null) @if (Model.TestRateRules != null)
@@ -110,7 +156,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="Script"></label> <label asp-for="Script"></label> <a href="#help" data-toggle="collapse"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<textarea asp-for="Script" rows="20" cols="80" class="form-control"></textarea> <textarea asp-for="Script" rows="20" cols="80" class="form-control"></textarea>
<span asp-validation-for="Script" class="text-danger"></span> <span asp-validation-for="Script" class="text-danger"></span>
<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>