Refactor rate handling to prevent error of exchange name

This commit is contained in:
nicolas.dorier 2020-01-17 18:11:05 +09:00
parent 605a0fd3c9
commit a8ac01cd8b
No known key found for this signature in database
GPG key ID: 6618763EF09186FE
17 changed files with 115 additions and 184 deletions

View file

@ -11,7 +11,15 @@ namespace BTCPayServer.Rating
Dictionary<string, ExchangeRate> _AllRates = new Dictionary<string, ExchangeRate>(); Dictionary<string, ExchangeRate> _AllRates = new Dictionary<string, ExchangeRate>();
public ExchangeRates() public ExchangeRates()
{ {
}
public ExchangeRates(string exchangeName, IEnumerable<PairRate> rates)
{
foreach (var rate in rates)
{
Add(new ExchangeRate(exchangeName, rate.CurrencyPair, rate.BidAsk));
}
} }
public ExchangeRates(IEnumerable<ExchangeRate> rates) public ExchangeRates(IEnumerable<ExchangeRate> rates)
{ {
@ -218,6 +226,26 @@ namespace BTCPayServer.Rating
return $"({Bid.ToString(CultureInfo.InvariantCulture)} , {Ask.ToString(CultureInfo.InvariantCulture)})"; return $"({Bid.ToString(CultureInfo.InvariantCulture)} , {Ask.ToString(CultureInfo.InvariantCulture)})";
} }
} }
public class PairRate
{
public PairRate(CurrencyPair currencyPair, BidAsk bidAsk)
{
if (currencyPair == null)
throw new ArgumentNullException(nameof(currencyPair));
if (bidAsk == null)
throw new ArgumentNullException(nameof(bidAsk));
this.CurrencyPair = currencyPair;
this.BidAsk = bidAsk;
}
public CurrencyPair CurrencyPair { get; }
public BidAsk BidAsk { get; }
public override string ToString()
{
return $"{CurrencyPair} == {BidAsk}";
}
}
public class ExchangeRate public class ExchangeRate
{ {
public ExchangeRate() public ExchangeRate()

View file

@ -60,14 +60,13 @@ namespace BTCPayServer.Services.Rates
{ {
public class LatestFetch public class LatestFetch
{ {
public ExchangeRates Latest; public PairRate[] Latest;
public DateTimeOffset NextRefresh; public DateTimeOffset NextRefresh;
public TimeSpan Backoff = TimeSpan.FromSeconds(5.0); public TimeSpan Backoff = TimeSpan.FromSeconds(5.0);
public DateTimeOffset Updated; public DateTimeOffset Updated;
public DateTimeOffset Expiration; public DateTimeOffset Expiration;
public Exception Exception; public Exception Exception;
public string ExchangeName; internal PairRate[] GetResult()
internal ExchangeRates GetResult()
{ {
if (Expiration <= DateTimeOffset.UtcNow) if (Expiration <= DateTimeOffset.UtcNow)
{ {
@ -77,7 +76,7 @@ namespace BTCPayServer.Services.Rates
} }
else else
{ {
throw new InvalidOperationException($"The rate has expired ({ExchangeName})"); throw new InvalidOperationException($"The rate has expired");
} }
} }
return Latest; return Latest;
@ -108,7 +107,6 @@ namespace BTCPayServer.Services.Rates
{ {
state.LastUpdated = fetch.Updated; state.LastUpdated = fetch.Updated;
state.Rates = fetch.Latest state.Rates = fetch.Latest
.Where(e => e.Exchange == ExchangeName)
.Select(r => new BackgroundFetcherRate() .Select(r => new BackgroundFetcherRate()
{ {
Pair = r.CurrencyPair, Pair = r.CurrencyPair,
@ -128,8 +126,7 @@ namespace BTCPayServer.Services.Rates
{ {
var fetch = new LatestFetch() var fetch = new LatestFetch()
{ {
ExchangeName = state.ExchangeName, Latest = rates.Select(r => new PairRate(r.Pair, r.BidAsk)).ToArray(),
Latest = new ExchangeRates(rates.Select(r => new ExchangeRate(state.ExchangeName, r.Pair, r.BidAsk))),
Updated = updated, Updated = updated,
NextRefresh = updated + RefreshRate, NextRefresh = updated + RefreshRate,
Expiration = updated + ValidatyTime Expiration = updated + ValidatyTime
@ -207,7 +204,7 @@ namespace BTCPayServer.Services.Rates
} }
LatestFetch _Latest; LatestFetch _Latest;
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken) public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{ {
LastRequested = DateTimeOffset.UtcNow; LastRequested = DateTimeOffset.UtcNow;
var latest = _Latest; var latest = _Latest;
@ -241,7 +238,6 @@ namespace BTCPayServer.Services.Rates
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var previous = _Latest; var previous = _Latest;
var fetch = new LatestFetch(); var fetch = new LatestFetch();
fetch.ExchangeName = ExchangeName;
try try
{ {
var rates = await _Inner.GetRatesAsync(cancellationToken); var rates = await _Inner.GetRatesAsync(cancellationToken);

View file

@ -9,24 +9,22 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
public class BitbankRateProvider : IRateProvider, IHasExchangeName public class BitbankRateProvider : IRateProvider
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
public BitbankRateProvider(HttpClient httpClient) public BitbankRateProvider(HttpClient httpClient)
{ {
_httpClient = httpClient ?? new HttpClient(); _httpClient = httpClient ?? new HttpClient();
} }
public string ExchangeName => "bitbank";
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken) public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{ {
var response = await _httpClient.GetAsync("https://public.bitbank.cc/prices", cancellationToken); var response = await _httpClient.GetAsync("https://public.bitbank.cc/prices", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken); var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
return new ExchangeRates(((jobj["data"] as JObject) ?? new JObject()) return ((jobj["data"] as JObject) ?? new JObject())
.Properties() .Properties()
.Select(p => new ExchangeRate(ExchangeName, CurrencyPair.Parse(p.Name), CreateBidAsk(p))) .Select(p => new PairRate(CurrencyPair.Parse(p.Name), CreateBidAsk(p)))
.ToArray()); .ToArray();
} }
private static BidAsk CreateBidAsk(JProperty p) private static BidAsk CreateBidAsk(JProperty p)

View file

@ -10,26 +10,23 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
public class BitpayRateProvider : IRateProvider, IHasExchangeName public class BitpayRateProvider : IRateProvider
{ {
public const string BitpayName = "bitpay";
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
public BitpayRateProvider(HttpClient httpClient) public BitpayRateProvider(HttpClient httpClient)
{ {
_httpClient = httpClient ?? new HttpClient(); _httpClient = httpClient ?? new HttpClient();
} }
public string ExchangeName => BitpayName; public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{ {
var response = await _httpClient.GetAsync("https://bitpay.com/rates", cancellationToken); var response = await _httpClient.GetAsync("https://bitpay.com/rates", cancellationToken);
var jarray = (JArray)(await response.Content.ReadAsAsync<JObject>(cancellationToken))["data"]; var jarray = (JArray)(await response.Content.ReadAsAsync<JObject>(cancellationToken))["data"];
return new ExchangeRates(jarray return jarray
.Children<JObject>() .Children<JObject>()
.Select(jobj => new ExchangeRate(ExchangeName, new CurrencyPair("BTC", jobj["code"].Value<string>()), new BidAsk(jobj["rate"].Value<decimal>()))) .Select(jobj => new PairRate(new CurrencyPair("BTC", jobj["code"].Value<string>()), new BidAsk(jobj["rate"].Value<decimal>())))
.Where(o => o.CurrencyPair.Right != "BTC") .Where(o => o.CurrencyPair.Right != "BTC")
.ToArray()); .ToArray();
} }
} }
} }

View file

@ -7,21 +7,20 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
public class ByllsRateProvider : IRateProvider, IHasExchangeName public class ByllsRateProvider : IRateProvider
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
public ByllsRateProvider(HttpClient httpClient) public ByllsRateProvider(HttpClient httpClient)
{ {
_httpClient = httpClient ?? new HttpClient(); _httpClient = httpClient ?? new HttpClient();
} }
public string ExchangeName => "bylls";
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken) public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{ {
var response = await _httpClient.GetAsync("https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", cancellationToken); var response = await _httpClient.GetAsync("https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken); var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var value = jobj["public_price"]["to_price"].Value<decimal>(); var value = jobj["public_price"]["to_price"].Value<decimal>();
return new ExchangeRates(new[] { new ExchangeRate(ExchangeName, new CurrencyPair("BTC", "CAD"), new BidAsk(value)) }); return new[] { new PairRate(new CurrencyPair("BTC", "CAD"), new BidAsk(value)) };
} }
} }
} }

File diff suppressed because one or more lines are too long

View file

@ -11,17 +11,15 @@ using ExchangeSharp;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
public class ExchangeSharpRateProvider : IRateProvider, IHasExchangeName public class ExchangeSharpRateProvider : IRateProvider
{ {
readonly ExchangeAPI _ExchangeAPI; readonly ExchangeAPI _ExchangeAPI;
readonly string _ExchangeName; public ExchangeSharpRateProvider(ExchangeAPI exchangeAPI, bool reverseCurrencyPair = false)
public ExchangeSharpRateProvider(string exchangeName, ExchangeAPI exchangeAPI, bool reverseCurrencyPair = false)
{ {
if (exchangeAPI == null) if (exchangeAPI == null)
throw new ArgumentNullException(nameof(exchangeAPI)); throw new ArgumentNullException(nameof(exchangeAPI));
exchangeAPI.RequestTimeout = TimeSpan.FromSeconds(5.0); exchangeAPI.RequestTimeout = TimeSpan.FromSeconds(5.0);
_ExchangeAPI = exchangeAPI; _ExchangeAPI = exchangeAPI;
_ExchangeName = exchangeName;
ReverseCurrencyPair = reverseCurrencyPair; ReverseCurrencyPair = reverseCurrencyPair;
} }
@ -30,9 +28,7 @@ namespace BTCPayServer.Services.Rates
get; set; get; set;
} }
public string ExchangeName => _ExchangeName; public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{ {
await new SynchronizationContextRemover(); await new SynchronizationContextRemover();
var rates = await _ExchangeAPI.GetTickersAsync(); var rates = await _ExchangeAPI.GetTickersAsync();
@ -43,14 +39,14 @@ namespace BTCPayServer.Services.Rates
var exchangeRates = await Task.WhenAll(exchangeRateTasks); var exchangeRates = await Task.WhenAll(exchangeRateTasks);
return new ExchangeRates(exchangeRates return exchangeRates
.Where(t => t != null) .Where(t => t != null)
.ToArray()); .ToArray();
} }
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf // ExchangeSymbolToGlobalSymbol throws exception which would kill perf
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>(); ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>();
private async Task<ExchangeRate> CreateExchangeRate(KeyValuePair<string, ExchangeTicker> ticker) private async Task<PairRate> CreateExchangeRate(KeyValuePair<string, ExchangeTicker> ticker)
{ {
if (notFoundSymbols.TryGetValue(ticker.Key, out _)) if (notFoundSymbols.TryGetValue(ticker.Key, out _))
return null; return null;
@ -64,11 +60,7 @@ namespace BTCPayServer.Services.Rates
} }
if(ReverseCurrencyPair) if(ReverseCurrencyPair)
pair = new CurrencyPair(pair.Right, pair.Left); pair = new CurrencyPair(pair.Right, pair.Left);
var rate = new ExchangeRate(); return new PairRate(pair, new BidAsk(ticker.Value.Bid, ticker.Value.Ask));
rate.CurrencyPair = pair;
rate.Exchange = _ExchangeName;
rate.BidAsk = new BidAsk(ticker.Value.Bid, ticker.Value.Ask);
return rate;
} }
catch (ArgumentException) catch (ArgumentException)
{ {

View file

@ -17,7 +17,7 @@ namespace BTCPayServer.Services.Rates
_Providers = providers; _Providers = providers;
} }
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken) public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{ {
foreach (var p in _Providers) foreach (var p in _Providers)
{ {
@ -31,7 +31,7 @@ namespace BTCPayServer.Services.Rates
} }
catch(Exception ex) { Exceptions.Add(ex); } catch(Exception ex) { Exceptions.Add(ex); }
} }
return new ExchangeRates(); return Array.Empty<PairRate>();
} }
public List<Exception> Exceptions { get; set; } = new List<Exception>(); public List<Exception> Exceptions { get; set; } = new List<Exception>();

View file

@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public interface IHasExchangeName
{
string ExchangeName { get; }
}
}

View file

@ -9,6 +9,6 @@ namespace BTCPayServer.Services.Rates
{ {
public interface IRateProvider public interface IRateProvider
{ {
Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken); Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken);
} }
} }

View file

@ -14,7 +14,7 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates namespace BTCPayServer.Services.Rates
{ {
// Make sure that only one request is sent to kraken in general // Make sure that only one request is sent to kraken in general
public class KrakenExchangeRateProvider : IRateProvider, IHasExchangeName public class KrakenExchangeRateProvider : IRateProvider
{ {
public KrakenExchangeRateProvider() public KrakenExchangeRateProvider()
{ {
@ -87,9 +87,9 @@ namespace BTCPayServer.Services.Rates
{ "ZGBP", "GBP" } { "ZGBP", "GBP" }
}; };
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken) public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{ {
var result = new ExchangeRates(); var result = new List<PairRate>();
var symbols = await GetSymbolsAsync(cancellationToken); var symbols = await GetSymbolsAsync(cancellationToken);
var normalizedPairsList = symbols.Where(s => !notFoundSymbols.ContainsKey(s)).Select(s => _Helper.NormalizeMarketSymbol(s)).ToList(); var normalizedPairsList = symbols.Where(s => !notFoundSymbols.ContainsKey(s)).Select(s => _Helper.NormalizeMarketSymbol(s)).ToList();
var csvPairsList = string.Join(",", normalizedPairsList); var csvPairsList = string.Join(",", normalizedPairsList);
@ -117,7 +117,7 @@ namespace BTCPayServer.Services.Rates
global = await _Helper.ExchangeMarketSymbolToGlobalMarketSymbolAsync(symbol); global = await _Helper.ExchangeMarketSymbolToGlobalMarketSymbolAsync(symbol);
} }
if (CurrencyPair.TryParse(global, out var pair)) if (CurrencyPair.TryParse(global, out var pair))
result.Add(new ExchangeRate("kraken", pair.Inverse(), new BidAsk(ticker.Bid, ticker.Ask))); result.Add(new PairRate(pair.Inverse(), new BidAsk(ticker.Bid, ticker.Ask)));
else else
notFoundSymbols.TryAdd(symbol, symbol); notFoundSymbols.TryAdd(symbol, symbol);
} }
@ -127,7 +127,7 @@ namespace BTCPayServer.Services.Rates
} }
} }
} }
return result; return result.ToArray();
} }
private static ExchangeTicker ConvertToExchangeTicker(string symbol, JToken ticker) private static ExchangeTicker ConvertToExchangeTicker(string symbol, JToken ticker)

View file

@ -21,9 +21,9 @@ namespace BTCPayServer.Services.Rates
return _Instance; return _Instance;
} }
} }
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken) public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{ {
return Task.FromResult(new ExchangeRates()); return Task.FromResult(Array.Empty<PairRate>());
} }
} }
} }

View file

@ -79,9 +79,9 @@ namespace BTCPayServer.Services.Rates
result.Latency = query.Latency; result.Latency = query.Latency;
if (query.Exception != null) if (query.Exception != null)
result.ExchangeExceptions.Add(query.Exception); result.ExchangeExceptions.Add(query.Exception);
foreach (var rule in query.ExchangeRates) foreach (var rule in query.PairRates)
{ {
rateRule.ExchangeRates.SetRate(rule.Exchange, rule.CurrencyPair, rule.BidAsk); rateRule.ExchangeRates.SetRate(query.Exchange, rule.CurrencyPair, rule.BidAsk);
} }
} }
rateRule.Reevaluate(); rateRule.Reevaluate();

View file

@ -25,7 +25,7 @@ namespace BTCPayServer.Services.Rates
{ {
_inner = inner; _inner = inner;
} }
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken) public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{ {
DateTimeOffset now = DateTimeOffset.UtcNow; DateTimeOffset now = DateTimeOffset.UtcNow;
try try
@ -35,7 +35,7 @@ namespace BTCPayServer.Services.Rates
catch (Exception ex) catch (Exception ex)
{ {
Exception = ex; Exception = ex;
return new ExchangeRates(); return Array.Empty<PairRate>();
} }
finally finally
{ {
@ -46,37 +46,15 @@ namespace BTCPayServer.Services.Rates
public class QueryRateResult public class QueryRateResult
{ {
public TimeSpan Latency { get; set; } public TimeSpan Latency { get; set; }
public ExchangeRates ExchangeRates { get; set; } public PairRate[] PairRates { get; set; }
public ExchangeException Exception { get; internal set; } public ExchangeException Exception { get; internal set; }
public string Exchange { get; internal set; }
} }
public RateProviderFactory(IHttpClientFactory httpClientFactory) public RateProviderFactory(IHttpClientFactory httpClientFactory)
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
// We use 15 min because of limits with free version of bitcoinaverage
CacheSpan = TimeSpan.FromMinutes(15.0);
InitExchanges(); InitExchanges();
} }
TimeSpan _CacheSpan;
public TimeSpan CacheSpan
{
get
{
return _CacheSpan;
}
set
{
_CacheSpan = value;
InvalidateCache();
}
}
public void InvalidateCache()
{
if (Providers.TryGetValue(CoinGeckoRateProvider.CoinGeckoName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c)
{
c.RefreshRate = CacheSpan;
c.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
}
}
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>(); private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
public Dictionary<string, IRateProvider> Providers public Dictionary<string, IRateProvider> Providers
@ -94,7 +72,7 @@ namespace BTCPayServer.Services.Rates
yield return new AvailableRateProvider("hitbtc", "HitBTC", "https://api.hitbtc.com/api/2/public/ticker"); yield return new AvailableRateProvider("hitbtc", "HitBTC", "https://api.hitbtc.com/api/2/public/ticker");
yield return new AvailableRateProvider("ndax", "NDAX", "https://ndax.io/api/returnTicker"); yield return new AvailableRateProvider("ndax", "NDAX", "https://ndax.io/api/returnTicker");
yield return new AvailableRateProvider(CoinGeckoRateProvider.CoinGeckoName, "Coin Gecko", "https://api.coingecko.com/api/v3/exchange_rates"); yield return new AvailableRateProvider("coingecko", "CoinGecko", "https://api.coingecko.com/api/v3/exchange_rates");
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"); 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");
yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD"); yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD");
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices"); yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices");
@ -103,14 +81,14 @@ namespace BTCPayServer.Services.Rates
void InitExchanges() 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(new ExchangeBinanceAPI(), true));
Providers.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true)); Providers.Add("bittrex", new ExchangeSharpRateProvider(new ExchangeBittrexAPI(), true));
Providers.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), true)); Providers.Add("poloniex", new ExchangeSharpRateProvider(new ExchangePoloniexAPI(), true));
Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitBTCAPI(), true)); Providers.Add("hitbtc", new ExchangeSharpRateProvider(new ExchangeHitBTCAPI(), true));
Providers.Add("ndax", new ExchangeSharpRateProvider("ndax", new ExchangeNDAXAPI(), true)); Providers.Add("ndax", new ExchangeSharpRateProvider(new ExchangeNDAXAPI(), true));
// Handmade providers // Handmade providers
Providers.Add(CoinGeckoRateProvider.CoinGeckoName, new CoinGeckoRateProvider(_httpClientFactory)); Providers.Add("coingecko", new CoinGeckoRateProvider(_httpClientFactory));
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") }); Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS"))); Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK"))); Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
@ -132,11 +110,11 @@ namespace BTCPayServer.Services.Rates
foreach (var supportedExchange in GetCoinGeckoSupportedExchanges()) foreach (var supportedExchange in GetCoinGeckoSupportedExchanges())
{ {
if (!Providers.ContainsKey(supportedExchange.Id)) if (!Providers.ContainsKey(supportedExchange.Id) && supportedExchange.Id != "coingecko")
{ {
var coingecko = new CoinGeckoRateProvider(_httpClientFactory) var coingecko = new CoinGeckoRateProvider(_httpClientFactory)
{ {
Exchange = supportedExchange.Id UnderlyingExchange = supportedExchange.Id
}; };
var bgFetcher = new BackgroundFetcherRateProvider(supportedExchange.Id, coingecko); var bgFetcher = new BackgroundFetcherRateProvider(supportedExchange.Id, coingecko);
bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0); bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0);
@ -191,8 +169,9 @@ namespace BTCPayServer.Services.Rates
var value = await wrapper.GetRatesAsync(cancellationToken); var value = await wrapper.GetRatesAsync(cancellationToken);
return new QueryRateResult() return new QueryRateResult()
{ {
Exchange = exchangeName,
Latency = wrapper.Latency, Latency = wrapper.Latency,
ExchangeRates = value, PairRates = value,
Exception = wrapper.Exception != null ? new ExchangeException() { Exception = wrapper.Exception, ExchangeName = exchangeName } : null Exception = wrapper.Exception != null ? new ExchangeException() { Exception = wrapper.Exception, ExchangeName = exchangeName } : null
}; };
} }

View file

@ -200,67 +200,27 @@ namespace BTCPayServer.Tests
rateProvider.Providers.Clear(); rateProvider.Providers.Clear();
var coinAverageMock = new MockRateProvider(); var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_USD"), new BidAsk(5000m)));
{ coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_CAD"), new BidAsk(4500m)));
Exchange = "coingecko", coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_LTC"), new BidAsk(162m)));
CurrencyPair = CurrencyPair.Parse("BTC_USD"), coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("LTC_USD"), new BidAsk(500m)));
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("BTC_LTC"),
BidAsk = new BidAsk(162m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
BidAsk = new BidAsk(500m)
});
rateProvider.Providers.Add("coingecko", coinAverageMock); rateProvider.Providers.Add("coingecko", coinAverageMock);
var bitflyerMock = new MockRateProvider(); var bitflyerMock = new MockRateProvider();
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate() bitflyerMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_JPY"), new BidAsk(700000m)));
{
Exchange = "bitflyer",
CurrencyPair = CurrencyPair.Parse("BTC_JPY"),
BidAsk = new BidAsk(700000m)
});
rateProvider.Providers.Add("bitflyer", bitflyerMock); rateProvider.Providers.Add("bitflyer", bitflyerMock);
var quadrigacx = new MockRateProvider(); var quadrigacx = new MockRateProvider();
quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate() quadrigacx.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_CAD"), new BidAsk(6000m)));
{
Exchange = "quadrigacx",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(6000m)
});
rateProvider.Providers.Add("quadrigacx", quadrigacx); rateProvider.Providers.Add("quadrigacx", quadrigacx);
var bittrex = new MockRateProvider(); var bittrex = new MockRateProvider();
bittrex.ExchangeRates.Add(new Rating.ExchangeRate() bittrex.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("DOGE_BTC"), new BidAsk(0.004m)));
{
Exchange = "bittrex",
CurrencyPair = CurrencyPair.Parse("DOGE_BTC"),
BidAsk = new BidAsk(0.004m)
});
rateProvider.Providers.Add("bittrex", bittrex); rateProvider.Providers.Add("bittrex", bittrex);
var bitfinex = new MockRateProvider(); var bitfinex = new MockRateProvider();
bitfinex.ExchangeRates.Add(new Rating.ExchangeRate() bitfinex.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("UST_BTC"), new BidAsk(0.000136m)));
{
Exchange = "bitfinex",
CurrencyPair = CurrencyPair.Parse("UST_BTC"),
BidAsk = new BidAsk(0.000136m)
});
rateProvider.Providers.Add("bitfinex", bitfinex); rateProvider.Providers.Add("bitfinex", bitfinex);
} }

View file

@ -10,15 +10,15 @@ namespace BTCPayServer.Tests.Mocks
{ {
public class MockRateProvider : IRateProvider public class MockRateProvider : IRateProvider
{ {
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates(); public List<PairRate> ExchangeRates { get; set; } = new List<PairRate>();
public MockRateProvider() public MockRateProvider()
{ {
} }
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken) public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{ {
return Task.FromResult(ExchangeRates); return Task.FromResult(ExchangeRates.ToArray());
} }
} }
} }

View file

@ -2684,14 +2684,14 @@ noninventoryitem:
var all = string.Join("\r\n", factory.GetSupportedExchanges().Select(e => e.Id).ToArray()); 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 bf && !(bf.Inner is CoinGeckoRateProvider cg && !cg.CoinGeckoRate)) .Where(p => p.Value is BackgroundFetcherRateProvider bf && !(bf.Inner is CoinGeckoRateProvider cg && cg.UnderlyingExchange != null))
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value)) .Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList()) .ToList())
{ {
Logs.Tester.LogInformation($"Testing {result.ExpectedName}"); Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
result.Fetcher.InvalidateCache(); result.Fetcher.InvalidateCache();
var exchangeRates = result.ResultAsync.Result; var exchangeRates = new ExchangeRates(result.ExpectedName, result.ResultAsync.Result);
result.Fetcher.InvalidateCache(); result.Fetcher.InvalidateCache();
Assert.NotNull(exchangeRates); Assert.NotNull(exchangeRates);
Assert.NotEmpty(exchangeRates); Assert.NotEmpty(exchangeRates);
@ -2788,12 +2788,12 @@ noninventoryitem:
class SpyRateProvider : IRateProvider class SpyRateProvider : IRateProvider
{ {
public bool Hit { get; set; } public bool Hit { get; set; }
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken) public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{ {
Hit = true; Hit = true;
var rates = new ExchangeRates(); var rates = new List<PairRate>();
rates.Add(new ExchangeRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(5000))); rates.Add(new PairRate(CurrencyPair.Parse("BTC_USD"), new BidAsk(5000)));
return Task.FromResult(rates); return Task.FromResult(rates.ToArray());
} }
public void AssertHit() public void AssertHit()
@ -2890,7 +2890,6 @@ noninventoryitem:
var factory = CreateBTCPayRateFactory(); var factory = CreateBTCPayRateFactory();
factory.Providers.Clear(); factory.Providers.Clear();
factory.CacheSpan = TimeSpan.FromSeconds(1);
var fetcher = new RateFetcher(factory); var fetcher = new RateFetcher(factory);
factory.Providers.Clear(); factory.Providers.Clear();
var fetch = new BackgroundFetcherRateProvider("spy", spy); var fetch = new BackgroundFetcherRateProvider("spy", spy);