mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 21:32:27 +01:00
Keep coinaverage compatibility, improve UX, hardcode feed provider supported exchanges
This commit is contained in:
parent
58d9a48787
commit
5dbdb4b399
@ -21,6 +21,11 @@ namespace BTCPayServer
|
||||
: "http://explorer.litecointools.com/tx/{0}",
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"LTC_X = LTC_BTC * BTC_X",
|
||||
"LTC_BTC = coingecko(LTC_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/litecoin.svg",
|
||||
LightningImagePath = "imlegacy/litecoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
|
@ -14,6 +14,11 @@ namespace BTCPayServer
|
||||
NetworkType == NetworkType.Mainnet
|
||||
? "https://www.exploremonero.com/transaction/{0}"
|
||||
: "https://testnet.xmrchain.net/tx/{0}",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"XMR_X = XMR_BTC * BTC_X",
|
||||
"XMR_BTC = kraken(XMR_BTC)"
|
||||
},
|
||||
CryptoImagePath = "/imlegacy/monero.svg"
|
||||
});
|
||||
}
|
||||
|
@ -1,16 +1,24 @@
|
||||
namespace BTCPayServer.Rating
|
||||
{
|
||||
public enum RateSource
|
||||
{
|
||||
Coingecko,
|
||||
CoinAverage,
|
||||
Direct
|
||||
}
|
||||
public class AvailableRateProvider
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Name { get; }
|
||||
public string Url { get; }
|
||||
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;
|
||||
Name = name;
|
||||
Url = url;
|
||||
Source = source;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 string PublicKey { get; set; }
|
||||
@ -196,32 +183,6 @@ namespace BTCPayServer.Services.Rates
|
||||
response.RequestsPerPeriod = jobj["requests_per_period"].Value<int>();
|
||||
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
|
||||
|
File diff suppressed because one or more lines are too long
@ -9,6 +9,7 @@ using ExchangeSharp;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
@ -57,6 +58,7 @@ 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;
|
||||
@ -96,8 +98,22 @@ namespace BTCPayServer.Services.Rates
|
||||
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
|
||||
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("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
|
||||
Providers.Add(CoinGeckoRateProvider.CoinGeckoName, new CoinGeckoRateProvider(_httpClientFactory));
|
||||
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
|
||||
continue;
|
||||
var prov = new BackgroundFetcherRateProvider(provider.Key, Providers[provider.Key]);
|
||||
if(provider.Key == CoinGeckoRateProvider.CoinGeckoName)
|
||||
if (provider.Key == CoinGeckoRateProvider.CoinGeckoName)
|
||||
{
|
||||
prov.RefreshRate = CacheSpan;
|
||||
prov.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
|
||||
@ -143,7 +155,22 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
@ -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;
|
||||
switch (Providers[CoinGeckoRateProvider.CoinGeckoName])
|
||||
if (_AvailableRateProviders == null)
|
||||
{
|
||||
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;
|
||||
var availableProviders = new Dictionary<string, AvailableRateProvider>();
|
||||
foreach (var exchange in GetDirectlySupportedExchanges())
|
||||
{
|
||||
availableProviders.Add(exchange.Id, exchange);
|
||||
}
|
||||
foreach (var exchange in GetCoinGeckoSupportedExchanges())
|
||||
{
|
||||
availableProviders.TryAdd(exchange.Id, exchange);
|
||||
}
|
||||
foreach (var exchange in GetCoinAverageSupportedExchanges())
|
||||
{
|
||||
availableProviders.TryAdd(exchange.Id, exchange);
|
||||
}
|
||||
_AvailableRateProviders = availableProviders.Values.OrderBy(o => o.Name).ToArray();
|
||||
}
|
||||
// Add other exchanges supported here
|
||||
return new[]
|
||||
return _AvailableRateProviders;
|
||||
}
|
||||
|
||||
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",
|
||||
"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);
|
||||
yield return new AvailableRateProvider(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}", RateSource.CoinAverage);
|
||||
}
|
||||
yield return new AvailableRateProvider("gdax", string.Empty, $"https://apiv2.bitcoinaverage.com/exchanges/gdax", RateSource.CoinAverage);
|
||||
}
|
||||
|
||||
internal IEnumerable<AvailableRateProvider> GetCoinGeckoSupportedExchanges()
|
||||
{
|
||||
return JArray.Parse(CoinGeckoRateProvider.SupportedExchanges).Select(token =>
|
||||
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)
|
||||
|
@ -215,8 +215,8 @@ namespace BTCPayServer.Tests
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coingecko",
|
||||
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
|
||||
BidAsk = new BidAsk(0.001m)
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_LTC"),
|
||||
BidAsk = new BidAsk(162m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
@ -262,15 +262,6 @@ 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,23 +8,17 @@ using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Tests.Mocks
|
||||
{
|
||||
public class MockRateProvider : CoinGeckoRateProvider
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
public override Task<IEnumerable<AvailableRateProvider>> GetAvailableExchanges(bool reload = false)
|
||||
{
|
||||
return Task.FromResult((IEnumerable<AvailableRateProvider>)AvailableRateProviders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -962,9 +962,9 @@ namespace BTCPayServer.Tests
|
||||
Assert.Null(GetRatesResult?.Data);
|
||||
|
||||
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";
|
||||
store.Rates(ratesVM).Wait();
|
||||
await store.Rates(ratesVM);
|
||||
store = acc.GetController<StoresController>();
|
||||
rateController = acc.GetController<RateController>();
|
||||
GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, default)
|
||||
@ -1240,9 +1240,9 @@ namespace BTCPayServer.Tests
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
List<decimal> rates = new List<decimal>();
|
||||
rates.Add(CreateInvoice(tester, user, "coingecko"));
|
||||
var bitflyer = CreateInvoice(tester, user, "bitflyer", "JPY");
|
||||
var bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY");
|
||||
rates.Add(await CreateInvoice(tester, user, "coingecko"));
|
||||
var bitflyer = await CreateInvoice(tester, user, "bitflyer", "JPY");
|
||||
var bitflyer2 = await CreateInvoice(tester, user, "bitflyer", "JPY");
|
||||
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
|
||||
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 vm = (RatesViewModel)((ViewResult)storeController.Rates().GetAwaiter().GetResult()).Model;
|
||||
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
|
||||
vm.PreferredExchange = exchange;
|
||||
storeController.Rates(vm).Wait();
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
await storeController.Rates(vm);
|
||||
var invoice2 = await user.BitPay.CreateInvoiceAsync(new Invoice()
|
||||
{
|
||||
Price = 5000.0m,
|
||||
Currency = currency,
|
||||
@ -1337,10 +1337,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
|
||||
|
||||
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);
|
||||
vm.Spread = 40;
|
||||
storeController.Rates(vm).Wait();
|
||||
await storeController.Rates(vm);
|
||||
|
||||
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
@ -1438,37 +1438,37 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
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.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>( await store.Rates()).Model);
|
||||
Assert.IsType<RedirectToActionResult>(await store.Rates(rateVm, "Save"));
|
||||
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
|
||||
Assert.Equal("bitflyer", rateVm.PreferredExchange);
|
||||
|
||||
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
|
||||
rateVm.Spread = 10;
|
||||
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.Equal(2, rateVm.TestRateRules.Count);
|
||||
Assert.False(rateVm.TestRateRules[0].Error);
|
||||
Assert.StartsWith("(bitflyer(BTC_JPY)) * (0.9, 1.1) =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
|
||||
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.Rates(rateVm, "Save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(await store.Rates(rateVm, "Save"));
|
||||
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.DefaultScript, rateVm.Script);
|
||||
Assert.True(rateVm.ShowScripting);
|
||||
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.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_X = coingecko(X_X);";
|
||||
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.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
|
||||
Assert.IsType<RedirectToActionResult>(await store.Rates(rateVm, "Save"));
|
||||
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.True(rateVm.ShowScripting);
|
||||
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
|
||||
@ -1569,7 +1569,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(ltcCryptoInfo);
|
||||
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
|
||||
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);
|
||||
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
|
||||
TestUtils.Eventually(() =>
|
||||
@ -2680,7 +2680,7 @@ noninventoryitem:
|
||||
public void CanQueryDirectProviders()
|
||||
{
|
||||
var factory = CreateBTCPayRateFactory();
|
||||
|
||||
var all = string.Join("\r\n", factory.GetSupportedExchanges().Select(e => e.Id).ToArray());
|
||||
foreach (var result in factory
|
||||
.Providers
|
||||
.Where(p => p.Value is BackgroundFetcherRateProvider)
|
||||
@ -2785,9 +2785,7 @@ noninventoryitem:
|
||||
|
||||
public static RateProviderFactory CreateBTCPayRateFactory()
|
||||
{
|
||||
var result = new RateProviderFactory(CreateMemoryCache(), new MockHttpClientFactory(), new CoinAverageSettings());
|
||||
result.InitExchanges().GetAwaiter().GetResult();
|
||||
return result;
|
||||
return new RateProviderFactory(CreateMemoryCache(), new MockHttpClientFactory(), new CoinAverageSettings());
|
||||
}
|
||||
|
||||
private static MemoryCacheOptions CreateMemoryCache()
|
||||
|
@ -86,14 +86,14 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.ToUpperInvariant()).ToList();
|
||||
.Select(t => t.ToUpperInvariant()).ToHashSet();
|
||||
|
||||
var networkProvider = new BTCPayNetworkProvider(NetworkType);
|
||||
var filtered = networkProvider.Filter(supportedChains.ToArray());
|
||||
var elementsBased = filtered.GetAll().OfType<ElementsBTCPayNetwork>();
|
||||
var parentChains = elementsBased.Select(network => network.NetworkCryptoCode.ToUpperInvariant()).Distinct();
|
||||
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);
|
||||
NetworkProvider = networkProvider.Filter(supportedChains.ToArray());
|
||||
foreach (var chain in supportedChains)
|
||||
|
@ -193,9 +193,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/rates")]
|
||||
public async Task<IActionResult> Rates()
|
||||
public IActionResult Rates()
|
||||
{
|
||||
var exchanges = await GetSupportedExchanges();
|
||||
var exchanges = GetSupportedExchanges();
|
||||
var storeBlob = CurrentStore.GetStoreBlob();
|
||||
var vm = new RatesViewModel();
|
||||
vm.SetExchangeRates(exchanges, storeBlob.PreferredExchange ?? CoinGeckoRateProvider.CoinGeckoName);
|
||||
@ -221,7 +221,7 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ShowRateRules), new {scripting = false, storeId = model.StoreId});
|
||||
}
|
||||
|
||||
var exchanges = await GetSupportedExchanges();
|
||||
var exchanges = GetSupportedExchanges();
|
||||
model.SetExchangeRates(exchanges, model.PreferredExchange);
|
||||
model.StoreId = storeId ?? model.StoreId;
|
||||
CurrencyPair[] currencyPairs = null;
|
||||
@ -338,7 +338,7 @@ namespace BTCPayServer.Controllers
|
||||
Description = scripting ?
|
||||
"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?",
|
||||
ButtonClass = "btn-primary"
|
||||
ButtonClass = scripting ? "btn-primary" : "btn-danger"
|
||||
});
|
||||
}
|
||||
|
||||
@ -603,9 +603,9 @@ namespace BTCPayServer.Controllers
|
||||
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
|
||||
.Where(r => !string.IsNullOrWhiteSpace(r.Name))
|
||||
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase);
|
||||
|
@ -44,7 +44,6 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
return new Task[]
|
||||
{
|
||||
CreateLoopTask(RefreshCoinAverageSupportedExchanges),
|
||||
CreateLoopTask(RefreshCoinAverageSettings),
|
||||
CreateLoopTask(RefreshRates)
|
||||
};
|
||||
@ -144,12 +143,6 @@ namespace BTCPayServer.HostedServices
|
||||
await _SettingsRepository.UpdateSetting(cache);
|
||||
}
|
||||
|
||||
async Task RefreshCoinAverageSupportedExchanges()
|
||||
{
|
||||
await _RateProviderFactory.InitExchanges();
|
||||
await Task.Delay(TimeSpan.FromHours(5), Cancellation);
|
||||
}
|
||||
|
||||
async Task RefreshCoinAverageSettings()
|
||||
{
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Rating;
|
||||
@ -17,13 +18,29 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
}
|
||||
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();
|
||||
Exchanges = new SelectList(supportedList, nameof(chosen.Id), nameof(chosen.Name), chosen);
|
||||
PreferredExchange = chosen.Id;
|
||||
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 SelectList Exchanges { get; set; }
|
||||
|
@ -44,7 +44,7 @@
|
||||
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"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_EXTERNALSERVICES": "totoservice:totolink;",
|
||||
"BTCPAY_SSHCONNECTION": "root@127.0.0.1:21622",
|
||||
|
@ -27,7 +27,7 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -18,15 +18,61 @@
|
||||
{
|
||||
<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 overflow-auto" style="max-height: 300px">
|
||||
<b>Supported exchanges are</b>:
|
||||
@for (int i = 0; i < Model.AvailableExchanges.Count(); i++)
|
||||
{
|
||||
<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>
|
||||
<p>Rate script allows you to express precisely how you want to calculate rates for currency pairs.</p>
|
||||
<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>
|
||||
<div class="accordion" id="accordion-info">
|
||||
<div class="card">
|
||||
<div class="card-header" id="direct-header">
|
||||
<h2 class="mb-0">
|
||||
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#direct-content" aria-expanded="true">
|
||||
Direct integration
|
||||
</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> </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> </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> </span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (Model.TestRateRules != null)
|
||||
@ -110,7 +156,7 @@
|
||||
|
||||
</div>
|
||||
<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>
|
||||
<span asp-validation-for="Script" class="text-danger"></span>
|
||||
<a href="#" onclick="$('#Script').val(defaultScript); return false;">Set to default settings</a>
|
||||
|
Loading…
Reference in New Issue
Block a user