From a8ac01cd8b6d01a7fac5e5365bbc03a04bb3bfc1 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Fri, 17 Jan 2020 18:11:05 +0900 Subject: [PATCH] Refactor rate handling to prevent error of exchange name --- BTCPayServer.Rating/ExchangeRates.cs | 28 ++++++++++ .../BackgroundFetcherRateProvider.cs | 14 ++--- .../Providers/BitbankRateProvider.cs | 12 ++-- .../Providers/BitpayRateProvider.cs | 13 ++--- .../Providers/ByllsRateProvider.cs | 7 +-- .../Providers/CoinGeckoRateProvider.cs | 41 ++++++-------- .../Providers/ExchangeSharpRateProvider.cs | 22 +++----- .../Providers/FallbackRateProvider.cs | 4 +- .../Providers/IHasExchangeName.cs | 12 ---- .../Providers/IRateProvider.cs | 2 +- .../Providers/KrakenExchangeRateProvider.cs | 10 ++-- .../Providers/NullRateProvider.cs | 4 +- BTCPayServer.Rating/Services/RateFetcher.cs | 4 +- .../Services/RateProviderFactory.cs | 51 +++++------------ BTCPayServer.Tests/BTCPayServerTester.cs | 56 +++---------------- BTCPayServer.Tests/Mocks/MockRateProvider.cs | 6 +- BTCPayServer.Tests/UnitTest1.cs | 13 ++--- 17 files changed, 115 insertions(+), 184 deletions(-) delete mode 100644 BTCPayServer.Rating/Providers/IHasExchangeName.cs diff --git a/BTCPayServer.Rating/ExchangeRates.cs b/BTCPayServer.Rating/ExchangeRates.cs index c9142e2b2..5bd310534 100644 --- a/BTCPayServer.Rating/ExchangeRates.cs +++ b/BTCPayServer.Rating/ExchangeRates.cs @@ -11,7 +11,15 @@ namespace BTCPayServer.Rating Dictionary _AllRates = new Dictionary(); public ExchangeRates() { + + } + public ExchangeRates(string exchangeName, IEnumerable rates) + { + foreach (var rate in rates) + { + Add(new ExchangeRate(exchangeName, rate.CurrencyPair, rate.BidAsk)); + } } public ExchangeRates(IEnumerable rates) { @@ -218,6 +226,26 @@ namespace BTCPayServer.Rating 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 ExchangeRate() diff --git a/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs b/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs index 92f9874d2..b74756e0d 100644 --- a/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs +++ b/BTCPayServer.Rating/Providers/BackgroundFetcherRateProvider.cs @@ -60,14 +60,13 @@ namespace BTCPayServer.Services.Rates { public class LatestFetch { - public ExchangeRates Latest; + public PairRate[] Latest; public DateTimeOffset NextRefresh; public TimeSpan Backoff = TimeSpan.FromSeconds(5.0); public DateTimeOffset Updated; public DateTimeOffset Expiration; public Exception Exception; - public string ExchangeName; - internal ExchangeRates GetResult() + internal PairRate[] GetResult() { if (Expiration <= DateTimeOffset.UtcNow) { @@ -77,7 +76,7 @@ namespace BTCPayServer.Services.Rates } else { - throw new InvalidOperationException($"The rate has expired ({ExchangeName})"); + throw new InvalidOperationException($"The rate has expired"); } } return Latest; @@ -108,7 +107,6 @@ namespace BTCPayServer.Services.Rates { state.LastUpdated = fetch.Updated; state.Rates = fetch.Latest - .Where(e => e.Exchange == ExchangeName) .Select(r => new BackgroundFetcherRate() { Pair = r.CurrencyPair, @@ -128,8 +126,7 @@ namespace BTCPayServer.Services.Rates { var fetch = new LatestFetch() { - ExchangeName = state.ExchangeName, - Latest = new ExchangeRates(rates.Select(r => new ExchangeRate(state.ExchangeName, r.Pair, r.BidAsk))), + Latest = rates.Select(r => new PairRate(r.Pair, r.BidAsk)).ToArray(), Updated = updated, NextRefresh = updated + RefreshRate, Expiration = updated + ValidatyTime @@ -207,7 +204,7 @@ namespace BTCPayServer.Services.Rates } LatestFetch _Latest; - public async Task GetRatesAsync(CancellationToken cancellationToken) + public async Task GetRatesAsync(CancellationToken cancellationToken) { LastRequested = DateTimeOffset.UtcNow; var latest = _Latest; @@ -241,7 +238,6 @@ namespace BTCPayServer.Services.Rates cancellationToken.ThrowIfCancellationRequested(); var previous = _Latest; var fetch = new LatestFetch(); - fetch.ExchangeName = ExchangeName; try { var rates = await _Inner.GetRatesAsync(cancellationToken); diff --git a/BTCPayServer.Rating/Providers/BitbankRateProvider.cs b/BTCPayServer.Rating/Providers/BitbankRateProvider.cs index 00f970405..d38df23ce 100644 --- a/BTCPayServer.Rating/Providers/BitbankRateProvider.cs +++ b/BTCPayServer.Rating/Providers/BitbankRateProvider.cs @@ -9,24 +9,22 @@ using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.Rates { - public class BitbankRateProvider : IRateProvider, IHasExchangeName + public class BitbankRateProvider : IRateProvider { private readonly HttpClient _httpClient; public BitbankRateProvider(HttpClient httpClient) { _httpClient = httpClient ?? new HttpClient(); } - public string ExchangeName => "bitbank"; - public async Task GetRatesAsync(CancellationToken cancellationToken) + public async Task GetRatesAsync(CancellationToken cancellationToken) { var response = await _httpClient.GetAsync("https://public.bitbank.cc/prices", cancellationToken); var jobj = await response.Content.ReadAsAsync(cancellationToken); - return new ExchangeRates(((jobj["data"] as JObject) ?? new JObject()) + return ((jobj["data"] as JObject) ?? new JObject()) .Properties() - .Select(p => new ExchangeRate(ExchangeName, CurrencyPair.Parse(p.Name), CreateBidAsk(p))) - .ToArray()); - + .Select(p => new PairRate(CurrencyPair.Parse(p.Name), CreateBidAsk(p))) + .ToArray(); } private static BidAsk CreateBidAsk(JProperty p) diff --git a/BTCPayServer.Rating/Providers/BitpayRateProvider.cs b/BTCPayServer.Rating/Providers/BitpayRateProvider.cs index 623ae32e0..fd45a8d80 100644 --- a/BTCPayServer.Rating/Providers/BitpayRateProvider.cs +++ b/BTCPayServer.Rating/Providers/BitpayRateProvider.cs @@ -10,26 +10,23 @@ using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.Rates { - public class BitpayRateProvider : IRateProvider, IHasExchangeName + public class BitpayRateProvider : IRateProvider { - public const string BitpayName = "bitpay"; private readonly HttpClient _httpClient; public BitpayRateProvider(HttpClient httpClient) { _httpClient = httpClient ?? new HttpClient(); } - public string ExchangeName => BitpayName; - - public async Task GetRatesAsync(CancellationToken cancellationToken) + public async Task GetRatesAsync(CancellationToken cancellationToken) { var response = await _httpClient.GetAsync("https://bitpay.com/rates", cancellationToken); var jarray = (JArray)(await response.Content.ReadAsAsync(cancellationToken))["data"]; - return new ExchangeRates(jarray + return jarray .Children() - .Select(jobj => new ExchangeRate(ExchangeName, new CurrencyPair("BTC", jobj["code"].Value()), new BidAsk(jobj["rate"].Value()))) + .Select(jobj => new PairRate(new CurrencyPair("BTC", jobj["code"].Value()), new BidAsk(jobj["rate"].Value()))) .Where(o => o.CurrencyPair.Right != "BTC") - .ToArray()); + .ToArray(); } } } diff --git a/BTCPayServer.Rating/Providers/ByllsRateProvider.cs b/BTCPayServer.Rating/Providers/ByllsRateProvider.cs index 23a822ed2..8f5e0a1ea 100644 --- a/BTCPayServer.Rating/Providers/ByllsRateProvider.cs +++ b/BTCPayServer.Rating/Providers/ByllsRateProvider.cs @@ -7,21 +7,20 @@ using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.Rates { - public class ByllsRateProvider : IRateProvider, IHasExchangeName + public class ByllsRateProvider : IRateProvider { private readonly HttpClient _httpClient; public ByllsRateProvider(HttpClient httpClient) { _httpClient = httpClient ?? new HttpClient(); } - public string ExchangeName => "bylls"; - public async Task GetRatesAsync(CancellationToken cancellationToken) + public async Task GetRatesAsync(CancellationToken cancellationToken) { var response = await _httpClient.GetAsync("https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", cancellationToken); var jobj = await response.Content.ReadAsAsync(cancellationToken); var value = jobj["public_price"]["to_price"].Value(); - 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)) }; } } } diff --git a/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs b/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs index 115c2e037..3df091b72 100644 --- a/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs +++ b/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs @@ -9,21 +9,18 @@ using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.Rates { - public class CoinGeckoRateProvider : IRateProvider, IHasExchangeName + public class CoinGeckoRateProvider : IRateProvider { + public const string CoinGeckoName = "coingecko"; // https://api.coingecko.com/api/v3/exchanges/list internal static readonly string SupportedExchanges = "[{\"id\":\"abcc\",\"name\":\"ABCC\"},{\"id\":\"acx\",\"name\":\"ACX\"},{\"id\":\"aex\",\"name\":\"AEX\"},{\"id\":\"airswap\",\"name\":\"AirSwap\"},{\"id\":\"allbit\",\"name\":\"Allbit\"},{\"id\":\"allcoin\",\"name\":\"Allcoin\"},{\"id\":\"alterdice\",\"name\":\"AlterDice\"},{\"id\":\"altilly\",\"name\":\"Altilly\"},{\"id\":\"altmarkets\",\"name\":\"Altmarkets\"},{\"id\":\"anx\",\"name\":\"ANX\"},{\"id\":\"aphelion\",\"name\":\"Aphelion\"},{\"id\":\"atomars\",\"name\":\"Atomars\"},{\"id\":\"axnet\",\"name\":\"AXNET\"},{\"id\":\"b2bx\",\"name\":\"B2BX\"},{\"id\":\"bakkt\",\"name\":\"Bakkt\"},{\"id\":\"bamboo_relay\",\"name\":\"Bamboo Relay\"},{\"id\":\"bancor\",\"name\":\"Bancor Network\"},{\"id\":\"bankera\",\"name\":\"Bankera\"},{\"id\":\"basefex\",\"name\":\"BaseFEX\"},{\"id\":\"bcex\",\"name\":\"BCEX\"},{\"id\":\"beaxy\",\"name\":\"Beaxy\"},{\"id\":\"bgogo\",\"name\":\"Bgogo\"},{\"id\":\"bhex\",\"name\":\"BHEX\"},{\"id\":\"bibox\",\"name\":\"Bibox\"},{\"id\":\"bibox_futures\",\"name\":\"Bibox (Futures)\"},{\"id\":\"bigmarkets\",\"name\":\"BIG markets\"},{\"id\":\"bigone\",\"name\":\"BigONE\"},{\"id\":\"bihodl\",\"name\":\"BiHODL \"},{\"id\":\"biki\",\"name\":\"Biki\"},{\"id\":\"bilaxy\",\"name\":\"Bilaxy\"},{\"id\":\"binance\",\"name\":\"Binance\"},{\"id\":\"binance_dex\",\"name\":\"Binance DEX\"},{\"id\":\"binance_futures\",\"name\":\"Binance (Futures)\"},{\"id\":\"binance_jersey\",\"name\":\"Binance Jersey\"},{\"id\":\"binance_us\",\"name\":\"Binance US\"},{\"id\":\"bione\",\"name\":\"Bione\"},{\"id\":\"birake\",\"name\":\"Birake\"},{\"id\":\"bisq\",\"name\":\"Bisq\"},{\"id\":\"bit2c\",\"name\":\"Bit2c\"},{\"id\":\"bitalong\",\"name\":\"Bitalong\"},{\"id\":\"bitasset\",\"name\":\"BitAsset\"},{\"id\":\"bitbank\",\"name\":\"Bitbank\"},{\"id\":\"bitbay\",\"name\":\"BitBay\"},{\"id\":\"bitbegin\",\"name\":\"Bitbegin\"},{\"id\":\"bitbox\",\"name\":\"BITBOX\"},{\"id\":\"bitc3\",\"name\":\"Bitc3\"},{\"id\":\"bitci\",\"name\":\"Bitci\"},{\"id\":\"bitcoin_com\",\"name\":\"Bitcoin.com\"},{\"id\":\"bitcratic\",\"name\":\"Bitcratic\"},{\"id\":\"bitex\",\"name\":\"Bitex.la\"},{\"id\":\"bitexbook\",\"name\":\"BITEXBOOK\"},{\"id\":\"bitexlive\",\"name\":\"Bitexlive\"},{\"id\":\"bitfex\",\"name\":\"Bitfex\"},{\"id\":\"bitfinex\",\"name\":\"Bitfinex\"},{\"id\":\"bitfinex_futures\",\"name\":\"Bitfinex (Futures)\"},{\"id\":\"bitflyer\",\"name\":\"bitFlyer\"},{\"id\":\"bitflyer_futures\",\"name\":\"Bitflyer (Futures)\"},{\"id\":\"bitforex\",\"name\":\"Bitforex\"},{\"id\":\"bitforex_futures\",\"name\":\"Bitforex (Futures)\"},{\"id\":\"bithash\",\"name\":\"BitHash\"},{\"id\":\"bitholic\",\"name\":\"Bithumb Singapore\"},{\"id\":\"bithumb\",\"name\":\"Bithumb\"},{\"id\":\"bithumb_global\",\"name\":\"Bithumb Global\"},{\"id\":\"bitinfi\",\"name\":\"Bitinfi\"},{\"id\":\"bitker\",\"name\":\"BITKER\"},{\"id\":\"bitkonan\",\"name\":\"BitKonan\"},{\"id\":\"bitkub\",\"name\":\"Bitkub\"},{\"id\":\"bitlish\",\"name\":\"Bitlish\"},{\"id\":\"bitmart\",\"name\":\"BitMart\"},{\"id\":\"bitmax\",\"name\":\"BitMax\"},{\"id\":\"bitmesh\",\"name\":\"Bitmesh\"},{\"id\":\"bitmex\",\"name\":\"Bitmex\"},{\"id\":\"bitoffer\",\"name\":\"Bitoffer\"},{\"id\":\"bitonbay\",\"name\":\"BitOnBay\"},{\"id\":\"bitopro\",\"name\":\"BitoPro\"},{\"id\":\"bitpanda\",\"name\":\"Bitpanda Global Exchange\"},{\"id\":\"bitrabbit\",\"name\":\"BitRabbit\"},{\"id\":\"bitrue\",\"name\":\"Bitrue\"},{\"id\":\"bits_blockchain\",\"name\":\"Bits Blockchain\"},{\"id\":\"bitsdaq\",\"name\":\"Bitsdaq\"},{\"id\":\"bitshares_assets\",\"name\":\"Bitshares Assets\"},{\"id\":\"bitso\",\"name\":\"Bitso\"},{\"id\":\"bitsonic\",\"name\":\"Bitsonic\"},{\"id\":\"bitstamp\",\"name\":\"Bitstamp\"},{\"id\":\"bitsten\",\"name\":\"Bitsten\"},{\"id\":\"bitstorage\",\"name\":\"BitStorage\"},{\"id\":\"bittrex\",\"name\":\"Bittrex\"},{\"id\":\"bit_z\",\"name\":\"Bit-Z\"},{\"id\":\"bitz_futures\",\"name\":\"Bitz (Futures)\"},{\"id\":\"bkex\",\"name\":\"BKEX\"},{\"id\":\"bleutrade\",\"name\":\"bleutrade\"},{\"id\":\"blockonix\",\"name\":\"Blockonix\"},{\"id\":\"boa\",\"name\":\"BOA Exchange\"},{\"id\":\"braziliex\",\"name\":\"Braziliex\"},{\"id\":\"btc_alpha\",\"name\":\"BTC-Alpha\"},{\"id\":\"btcbox\",\"name\":\"BTCBOX\"},{\"id\":\"btcc\",\"name\":\"BTCC\"},{\"id\":\"btcexa\",\"name\":\"BTCEXA\"},{\"id\":\"btc_exchange\",\"name\":\"Btc Exchange\"},{\"id\":\"btcmarkets\",\"name\":\"BTCMarkets\"},{\"id\":\"btcnext\",\"name\":\"BTCNEXT\"},{\"id\":\"btcsquare\",\"name\":\"BTCSquare\"},{\"id\":\"btc_trade_ua\",\"name\":\"BTC Trade UA\"},{\"id\":\"btcturk\",\"name\":\"BTCTurk\"},{\"id\":\"btse\",\"name\":\"BTSE\"},{\"id\":\"btse_futures\",\"name\":\"BTSE (Futures)\"},{\"id\":\"buyucoin\",\"name\":\"BuyUcoin\"},{\"id\":\"bvnex\",\"name\":\"Bvnex\"},{\"id\":\"bw\",\"name\":\"BW.com\"},{\"id\":\"bx_thailand\",\"name\":\"BX Thailand\"},{\"id\":\"bybit\",\"name\":\"Bybit\"},{\"id\":\"c2cx\",\"name\":\"C2CX\"},{\"id\":\"cashierest\",\"name\":\"Cashierest\"},{\"id\":\"cashpayz\",\"name\":\"Cashpayz\"},{\"id\":\"catex\",\"name\":\"Catex\"},{\"id\":\"cbx\",\"name\":\"CBX\"},{\"id\":\"ccex\",\"name\":\"C-CEX\"},{\"id\":\"ccx\",\"name\":\"CCXCanada\"},{\"id\":\"cex\",\"name\":\"CEX.IO\"},{\"id\":\"cezex\",\"name\":\"Cezex\"},{\"id\":\"chainex\",\"name\":\"ChainEX\"},{\"id\":\"chainrift\",\"name\":\"Chainrift\"},{\"id\":\"chaoex\",\"name\":\"CHAOEX\"},{\"id\":\"citex\",\"name\":\"CITEX\"},{\"id\":\"cme_futures\",\"name\":\"CME Bitcoin Futures\"},{\"id\":\"codex\",\"name\":\"CODEX\"},{\"id\":\"coinall\",\"name\":\"CoinAll\"},{\"id\":\"coinasset\",\"name\":\"CoinAsset\"},{\"id\":\"coinbe\",\"name\":\"Coinbe\"},{\"id\":\"coinbene\",\"name\":\"CoinBene\"},{\"id\":\"coinbig\",\"name\":\"COINBIG\"},{\"id\":\"coinbit\",\"name\":\"Coinbit\"},{\"id\":\"coinchangex\",\"name\":\"Coinchangex\"},{\"id\":\"coincheck\",\"name\":\"Coincheck\"},{\"id\":\"coindeal\",\"name\":\"Coindeal\"},{\"id\":\"coindirect\",\"name\":\"CoinDirect\"},{\"id\":\"coineal\",\"name\":\"Coineal\"},{\"id\":\"coin_egg\",\"name\":\"CoinEgg\"},{\"id\":\"coinex\",\"name\":\"CoinEx\"},{\"id\":\"coinfalcon\",\"name\":\"Coinfalcon\"},{\"id\":\"coinfield\",\"name\":\"Coinfield\"},{\"id\":\"coinfinit\",\"name\":\"Coinfinit\"},{\"id\":\"coinflex\",\"name\":\"CoinFLEX\"},{\"id\":\"coinflex_futures\",\"name\":\"CoinFLEX (Futures)\"},{\"id\":\"coinfloor\",\"name\":\"Coinfloor\"},{\"id\":\"coingi\",\"name\":\"Coingi\"},{\"id\":\"coinhe\",\"name\":\"CoinHe\"},{\"id\":\"coinhub\",\"name\":\"Coinhub\"},{\"id\":\"coinjar\",\"name\":\"CoinJar Exchange\"},{\"id\":\"coinlim\",\"name\":\"Coinlim\"},{\"id\":\"coin_metro\",\"name\":\"Coinmetro\"},{\"id\":\"coinmex\",\"name\":\"CoinMex\"},{\"id\":\"coinnest\",\"name\":\"CoinNest\"},{\"id\":\"coinone\",\"name\":\"Coinone\"},{\"id\":\"coinpark\",\"name\":\"Coinpark\"},{\"id\":\"coinplace\",\"name\":\"Coinplace\"},{\"id\":\"coinsbank\",\"name\":\"Coinsbank\"},{\"id\":\"coinsbit\",\"name\":\"Coinsbit\"},{\"id\":\"coinsuper\",\"name\":\"Coinsuper\"},{\"id\":\"cointiger\",\"name\":\"CoinTiger\"},{\"id\":\"coinxpro\",\"name\":\"COINX.PRO\"},{\"id\":\"coinzest\",\"name\":\"Coinzest\"},{\"id\":\"coinzo\",\"name\":\"Coinzo\"},{\"id\":\"c_patex\",\"name\":\"C-Patex\"},{\"id\":\"cpdax\",\"name\":\"CPDAX\"},{\"id\":\"credoex\",\"name\":\"CredoEx\"},{\"id\":\"crex24\",\"name\":\"CREX24\"},{\"id\":\"crxzone\",\"name\":\"CRXzone\"},{\"id\":\"cryptaldash\",\"name\":\"CryptalDash\"},{\"id\":\"cryptex\",\"name\":\"Cryptex\"},{\"id\":\"crypto_bridge\",\"name\":\"CryptoBridge\"},{\"id\":\"cryptology\",\"name\":\"Cryptology\"},{\"id\":\"cryptonit\",\"name\":\"Cryptonit\"},{\"id\":\"crytrex\",\"name\":\"CryTrEx\"},{\"id\":\"cybex\",\"name\":\"Cybex DEX\"},{\"id\":\"dach_exchange\",\"name\":\"Dach Exchange\"},{\"id\":\"dakuce\",\"name\":\"Dakuce\"},{\"id\":\"darb_finance\",\"name\":\"Darb Finance\"},{\"id\":\"daybit\",\"name\":\"Daybit\"},{\"id\":\"dcoin\",\"name\":\"Dcoin\"},{\"id\":\"ddex\",\"name\":\"DDEX\"},{\"id\":\"decoin\",\"name\":\"Decoin\"},{\"id\":\"delta_futures\",\"name\":\"Delta Exchange\"},{\"id\":\"deribit\",\"name\":\"Deribit\"},{\"id\":\"dextop\",\"name\":\"DEx.top\"},{\"id\":\"dextrade\",\"name\":\"Dex-Trade\"},{\"id\":\"dflow\",\"name\":\"Dflow\"},{\"id\":\"digifinex\",\"name\":\"Digifinex\"},{\"id\":\"digitalprice\",\"name\":\"Altsbit\"},{\"id\":\"dobitrade\",\"name\":\"Dobitrade\"},{\"id\":\"dove_wallet\",\"name\":\"Dove Wallet\"},{\"id\":\"dragonex\",\"name\":\"DragonEx\"},{\"id\":\"dsx\",\"name\":\"DSX\"},{\"id\":\"dydx\",\"name\":\"dYdX\"},{\"id\":\"ecxx\",\"name\":\"Ecxx\"},{\"id\":\"elitex\",\"name\":\"Elitex\"},{\"id\":\"eosex\",\"name\":\"EOSex\"},{\"id\":\"escodex\",\"name\":\"Escodex\"},{\"id\":\"eterbase\",\"name\":\"Eterbase\"},{\"id\":\"etherflyer\",\"name\":\"EtherFlyer\"},{\"id\":\"ethex\",\"name\":\"Ethex\"},{\"id\":\"everbloom\",\"name\":\"Everbloom\"},{\"id\":\"exmarkets\",\"name\":\"ExMarkets\"},{\"id\":\"exmo\",\"name\":\"EXMO\"},{\"id\":\"exnce\",\"name\":\"EXNCE\"},{\"id\":\"exrates\",\"name\":\"Exrates\"},{\"id\":\"extstock\",\"name\":\"ExtStock\"},{\"id\":\"exx\",\"name\":\"EXX\"},{\"id\":\"f1cx\",\"name\":\"F1CX\"},{\"id\":\"fatbtc\",\"name\":\"FatBTC\"},{\"id\":\"fcoin\",\"name\":\"FCoin\"},{\"id\":\"fex\",\"name\":\"FEX\"},{\"id\":\"financex\",\"name\":\"FinanceX\"},{\"id\":\"finexbox\",\"name\":\"FinexBox\"},{\"id\":\"fisco\",\"name\":\"Fisco\"},{\"id\":\"floatsv\",\"name\":\"Float SV\"},{\"id\":\"fmex\",\"name\":\"FMex\"},{\"id\":\"forkdelta\",\"name\":\"ForkDelta\"},{\"id\":\"freiexchange\",\"name\":\"Freiexchange\"},{\"id\":\"ftx\",\"name\":\"FTX\"},{\"id\":\"ftx_spot\",\"name\":\"FTX (Spot)\"},{\"id\":\"fubt\",\"name\":\"FUBT\"},{\"id\":\"gate\",\"name\":\"Gate.io\"},{\"id\":\"gate_futures\",\"name\":\"Gate.io (Futures)\"},{\"id\":\"gbx\",\"name\":\"Gibraltar Blockchain Exchange\"},{\"id\":\"gdac\",\"name\":\"GDAC\"},{\"id\":\"gdax\",\"name\":\"Coinbase Pro\"},{\"id\":\"gemini\",\"name\":\"Gemini\"},{\"id\":\"getbtc\",\"name\":\"GetBTC\"},{\"id\":\"gmo_japan\",\"name\":\"GMO Japan\"},{\"id\":\"gmo_japan_futures\",\"name\":\"GMO Japan (Futures)\"},{\"id\":\"gobaba\",\"name\":\"Gobaba\"},{\"id\":\"go_exchange\",\"name\":\"Go Exchange\"},{\"id\":\"gopax\",\"name\":\"GoPax\"},{\"id\":\"graviex\",\"name\":\"Graviex\"},{\"id\":\"hanbitco\",\"name\":\"Hanbitco\"},{\"id\":\"hb_top\",\"name\":\"Hb.top\"},{\"id\":\"hitbtc\",\"name\":\"HitBTC\"},{\"id\":\"hotbit\",\"name\":\"Hotbit\"},{\"id\":\"hpx\",\"name\":\"HPX\"},{\"id\":\"hubi\",\"name\":\"Hubi\"},{\"id\":\"huobi\",\"name\":\"Huobi Global\"},{\"id\":\"huobi_dm\",\"name\":\"Huobi DM\"},{\"id\":\"huobi_japan\",\"name\":\"Huobi Japan\"},{\"id\":\"huobi_korea\",\"name\":\"Huobi Korea\"},{\"id\":\"huobi_us\",\"name\":\"Huobi US (HBUS)\"},{\"id\":\"ice3x\",\"name\":\"Ice3x\"},{\"id\":\"idcm\",\"name\":\"IDCM\"},{\"id\":\"idex\",\"name\":\"Idex\"},{\"id\":\"incorex\",\"name\":\"IncoreX\"},{\"id\":\"independent_reserve\",\"name\":\"Independent Reserve\"},{\"id\":\"indodax\",\"name\":\"Indodax\"},{\"id\":\"indoex\",\"name\":\"Indoex\"},{\"id\":\"infinity_coin\",\"name\":\"Infinity Coin\"},{\"id\":\"instantbitex\",\"name\":\"Instant Bitex\"},{\"id\":\"iqfinex\",\"name\":\"IQFinex\"},{\"id\":\"ironex\",\"name\":\"Ironex\"},{\"id\":\"itbit\",\"name\":\"itBit\"},{\"id\":\"jex\",\"name\":\"Binance JEX\"},{\"id\":\"jex_futures\",\"name\":\"Binance JEX (Futures)\"},{\"id\":\"joyso\",\"name\":\"Joyso\"},{\"id\":\"kairex\",\"name\":\"KAiREX\"},{\"id\":\"kkcoin\",\"name\":\"KKCoin\"},{\"id\":\"k_kex\",\"name\":\"KKEX\"},{\"id\":\"koinok\",\"name\":\"Koinok\"},{\"id\":\"koinx\",\"name\":\"Koinx\"},{\"id\":\"korbit\",\"name\":\"Korbit\"},{\"id\":\"kraken\",\"name\":\"Kraken\"},{\"id\":\"kraken_futures\",\"name\":\"Kraken (Futures)\"},{\"id\":\"kryptono\",\"name\":\"Kryptono\"},{\"id\":\"kucoin\",\"name\":\"KuCoin\"},{\"id\":\"kumex\",\"name\":\"Kumex\"},{\"id\":\"kuna\",\"name\":\"Kuna Exchange\"},{\"id\":\"kyber_network\",\"name\":\"Kyber Network\"},{\"id\":\"lakebtc\",\"name\":\"LakeBTC\"},{\"id\":\"latoken\",\"name\":\"LATOKEN\"},{\"id\":\"lbank\",\"name\":\"LBank\"},{\"id\":\"letsdocoinz\",\"name\":\"Letsdocoinz\"},{\"id\":\"livecoin\",\"name\":\"Livecoin\"},{\"id\":\"localtrade\",\"name\":\"LocalTrade\"},{\"id\":\"lukki\",\"name\":\"Lukki\"},{\"id\":\"luno\",\"name\":\"Luno\"},{\"id\":\"lykke\",\"name\":\"Lykke\"},{\"id\":\"mandala\",\"name\":\"Mandala\"},{\"id\":\"max_maicoin\",\"name\":\"Max Maicoin\"},{\"id\":\"mercado_bitcoin\",\"name\":\"Mercado Bitcoin\"},{\"id\":\"mercatox\",\"name\":\"Mercatox\"},{\"id\":\"mercuriex\",\"name\":\"MercuriEx\"},{\"id\":\"mxc\",\"name\":\"MXC\"},{\"id\":\"nanu_exchange\",\"name\":\"Nanu Exchange\"},{\"id\":\"nash\",\"name\":\"Nash\"},{\"id\":\"neblidex\",\"name\":\"Neblidex\"},{\"id\":\"negociecoins\",\"name\":\"Negociecoins\"},{\"id\":\"neraex\",\"name\":\"Neraex\"},{\"id\":\"newdex\",\"name\":\"Newdex\"},{\"id\":\"nexybit\",\"name\":\"Nexybit\"},{\"id\":\"ninecoin\",\"name\":\"9coin\"},{\"id\":\"nlexch\",\"name\":\"NLexch\"},{\"id\":\"novadax\",\"name\":\"NovaDAX\"},{\"id\":\"novadex\",\"name\":\"Novadex\"},{\"id\":\"oasis_trade\",\"name\":\"OasisDEX\"},{\"id\":\"oceanex\",\"name\":\"Oceanex\"},{\"id\":\"oex\",\"name\":\"OEX\"},{\"id\":\"okcoin\",\"name\":\"OKCoin\"},{\"id\":\"okex\",\"name\":\"OKEx\"},{\"id\":\"okex_korea\",\"name\":\"OKEx Korea\"},{\"id\":\"okex_swap\",\"name\":\"OKEx (Futures)\"},{\"id\":\"omgfin\",\"name\":\"Omgfin\"},{\"id\":\"omnitrade\",\"name\":\"OmniTrade\"},{\"id\":\"ooobtc\",\"name\":\"OOOBTC\"},{\"id\":\"openledger\",\"name\":\"OpenLedger DEX\"},{\"id\":\"orderbook\",\"name\":\"Orderbook.io\"},{\"id\":\"ore_bz\",\"name\":\"Ore BZ\"},{\"id\":\"otcbtc\",\"name\":\"OTCBTC\"},{\"id\":\"ovex\",\"name\":\"Ovex\"},{\"id\":\"p2pb2b\",\"name\":\"P2PB2B\"},{\"id\":\"paribu\",\"name\":\"Paribu\"},{\"id\":\"paroexchange\",\"name\":\"Paro Exchange\"},{\"id\":\"paymium\",\"name\":\"Paymium\"},{\"id\":\"piexgo\",\"name\":\"Piexgo\"},{\"id\":\"poloniex\",\"name\":\"Poloniex\"},{\"id\":\"prime_xbt\",\"name\":\"Prime XBT\"},{\"id\":\"probit\",\"name\":\"Probit\"},{\"id\":\"purcow\",\"name\":\"Purcow\"},{\"id\":\"qbtc\",\"name\":\"QBTC\"},{\"id\":\"qtrade\",\"name\":\"qTrade\"},{\"id\":\"quoine\",\"name\":\"Liquid\"},{\"id\":\"radar_relay\",\"name\":\"Radar Relay\"},{\"id\":\"raidofinance\",\"name\":\"Raidofinance\"},{\"id\":\"raisex\",\"name\":\"Raisex\"},{\"id\":\"resfinex\",\"name\":\"Resfinex\"},{\"id\":\"rfinex\",\"name\":\"Rfinex\"},{\"id\":\"safe_trade\",\"name\":\"SafeTrade\"},{\"id\":\"satoexchange\",\"name\":\"SatoExchange\"},{\"id\":\"sato_wallet_ex\",\"name\":\"SatowalletEx\"},{\"id\":\"saturn_network\",\"name\":\"Saturn Network\"},{\"id\":\"secondbtc\",\"name\":\"SecondBTC\"},{\"id\":\"shortex\",\"name\":\"Shortex\"},{\"id\":\"simex\",\"name\":\"Simex\"},{\"id\":\"sistemkoin\",\"name\":\"Sistemkoin\"},{\"id\":\"six_x\",\"name\":\"6x\"},{\"id\":\"south_xchange\",\"name\":\"SouthXchange\"},{\"id\":\"stake_cube\",\"name\":\"StakeCube Exchange\"},{\"id\":\"stellar_term\",\"name\":\"StellarTerm\"},{\"id\":\"stocks_exchange\",\"name\":\"STEX\"},{\"id\":\"swiftex\",\"name\":\"Swiftex\"},{\"id\":\"switcheo\",\"name\":\"Switcheo\"},{\"id\":\"syex\",\"name\":\"Shangya Exchange\"},{\"id\":\"synthetix\",\"name\":\"Synthetix Exchange\"},{\"id\":\"tdax\",\"name\":\"Satang Pro\"},{\"id\":\"therocktrading\",\"name\":\"TheRockTrading\"},{\"id\":\"thetokenstore\",\"name\":\"Token.Store\"},{\"id\":\"thinkbit\",\"name\":\"ThinkBit Pro\"},{\"id\":\"three_xbit\",\"name\":\"3XBIT\"},{\"id\":\"tidebit\",\"name\":\"Tidebit\"},{\"id\":\"tidex\",\"name\":\"Tidex\"},{\"id\":\"tokenize\",\"name\":\"Tokenize\"},{\"id\":\"tokenjar\",\"name\":\"TokenJar\"},{\"id\":\"tokenomy\",\"name\":\"Tokenomy\"},{\"id\":\"tokens_net\",\"name\":\"TokensNet\"},{\"id\":\"toko_crypto\",\"name\":\"TokoCrypto\"},{\"id\":\"tokok\",\"name\":\"TOKOK\"},{\"id\":\"tokpie\",\"name\":\"Tokpie\"},{\"id\":\"topbtc\",\"name\":\"TopBTC\"},{\"id\":\"tradeio\",\"name\":\"Trade.io\"},{\"id\":\"trade_ogre\",\"name\":\"TradeOgre\"},{\"id\":\"trade_satoshi\",\"name\":\"Trade Satoshi\"},{\"id\":\"troca_ninja\",\"name\":\"Troca.Ninja\"},{\"id\":\"tron_trade\",\"name\":\"TronTrade\"},{\"id\":\"trx_market\",\"name\":\"PoloniDEX\"},{\"id\":\"tux_exchange\",\"name\":\"Tux Exchange\"},{\"id\":\"txbit\",\"name\":\"Txbit\"},{\"id\":\"uex\",\"name\":\"UEX\"},{\"id\":\"uniswap\",\"name\":\"Uniswap\"},{\"id\":\"unnamed\",\"name\":\"Unnamed\"},{\"id\":\"upbit\",\"name\":\"Upbit\"},{\"id\":\"upbit_indonesia\",\"name\":\"Upbit Indonesia \"},{\"id\":\"vb\",\"name\":\"VB\"},{\"id\":\"vbitex\",\"name\":\"Vbitex\"},{\"id\":\"vcc\",\"name\":\"VCC Exchange\"},{\"id\":\"vebitcoin\",\"name\":\"Vebitcoin\"},{\"id\":\"velic\",\"name\":\"Velic\"},{\"id\":\"vindax\",\"name\":\"Vindax\"},{\"id\":\"vinex\",\"name\":\"Vinex\"},{\"id\":\"vitex\",\"name\":\"ViteX\"},{\"id\":\"waves\",\"name\":\"Waves.Exchange\"},{\"id\":\"wazirx\",\"name\":\"WazirX\"},{\"id\":\"whale_ex\",\"name\":\"WhaleEx\"},{\"id\":\"whitebit\",\"name\":\"Whitebit\"},{\"id\":\"worldcore\",\"name\":\"Worldcore\"},{\"id\":\"xfutures\",\"name\":\"xFutures\"},{\"id\":\"xt\",\"name\":\"XT\"},{\"id\":\"yobit\",\"name\":\"YoBit\"},{\"id\":\"yunex\",\"name\":\"Yunex.io\"},{\"id\":\"zaif\",\"name\":\"Zaif\"},{\"id\":\"zb\",\"name\":\"ZB\"},{\"id\":\"zbg\",\"name\":\"ZBG\"},{\"id\":\"zbmega\",\"name\":\"ZB Mega\"},{\"id\":\"zebpay\",\"name\":\"Zebpay\"},{\"id\":\"zg\",\"name\":\"ZG.com\"},{\"id\":\"zgtop\",\"name\":\"ZG.TOP\"}]"; private readonly HttpClient Client; - public static string CoinGeckoName { get; } = "coingecko"; - public string Exchange { get; set; } - public string ExchangeName => Exchange ?? CoinGeckoName; - public bool CoinGeckoRate + + public string UnderlyingExchange { - get - { - return ExchangeName == CoinGeckoName; - } + get; + set; } public CoinGeckoRateProvider(IHttpClientFactory httpClientFactory) @@ -37,20 +34,19 @@ namespace BTCPayServer.Services.Rates Client.DefaultRequestHeaders.Add("Accept", "application/json"); } - public virtual Task GetRatesAsync(CancellationToken cancellationToken) + public virtual Task GetRatesAsync(CancellationToken cancellationToken) { - return CoinGeckoRate ? GetCoinGeckoRates(cancellationToken) : GetCoinGeckoExchangeSpecificRates(1, cancellationToken); + return UnderlyingExchange is null ? GetCoinGeckoRates(cancellationToken) : GetCoinGeckoExchangeSpecificRates(1, cancellationToken); } - private async Task GetCoinGeckoRates(CancellationToken cancellationToken) + private async Task GetCoinGeckoRates(CancellationToken cancellationToken) { using var resp = await GetWithBackoffAsync("exchange_rates", cancellationToken); resp.EnsureSuccessStatusCode(); - return new ExchangeRates(JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("rates").Children() + return JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("rates").Children() .Where(token => ((JProperty)token).Name != "btc") - .Select(token => new ExchangeRate(CoinGeckoName, - new CurrencyPair("BTC", ((JProperty)token).Name.ToString()), - new BidAsk(((JProperty)token).Value["value"].Value())))); + .Select(token => new PairRate(new CurrencyPair("BTC", ((JProperty)token).Name.ToString()), + new BidAsk(((JProperty)token).Value["value"].Value()))).ToArray(); } private async Task GetWithBackoffAsync(string request, CancellationToken cancellationToken) @@ -72,14 +68,13 @@ retry: return resp; } - private async Task GetCoinGeckoExchangeSpecificRates(int page, CancellationToken cancellationToken) + private async Task GetCoinGeckoExchangeSpecificRates(int page, CancellationToken cancellationToken) { - using var resp = await GetWithBackoffAsync($"exchanges/{Exchange}/tickers?page={page}", cancellationToken); + using var resp = await GetWithBackoffAsync($"exchanges/{UnderlyingExchange}/tickers?page={page}", cancellationToken); resp.EnsureSuccessStatusCode(); - List result = JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("tickers") - .Select(token => new ExchangeRate(ExchangeName, - new CurrencyPair(token.Value("base"), token.Value("target")), + List result = JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("tickers") + .Select(token => new PairRate(new CurrencyPair(token.Value("base"), token.Value("target")), new BidAsk(token.Value("last")))).ToList(); if (page == 1 && resp.Headers.TryGetValues("total", out var total) && resp.Headers.TryGetValues("per-page", out var perPage)) @@ -93,7 +88,7 @@ retry: totalPages++; } - var tasks = new List>(); + var tasks = new List>(); for (int i = 2; i <= totalPages; i++) { tasks.Add(GetCoinGeckoExchangeSpecificRates(i, cancellationToken)); @@ -105,7 +100,7 @@ retry: } } - return new ExchangeRates(result); + return result.ToArray(); } } } diff --git a/BTCPayServer.Rating/Providers/ExchangeSharpRateProvider.cs b/BTCPayServer.Rating/Providers/ExchangeSharpRateProvider.cs index 336a6c4d8..b34f89e94 100644 --- a/BTCPayServer.Rating/Providers/ExchangeSharpRateProvider.cs +++ b/BTCPayServer.Rating/Providers/ExchangeSharpRateProvider.cs @@ -11,17 +11,15 @@ using ExchangeSharp; namespace BTCPayServer.Services.Rates { - public class ExchangeSharpRateProvider : IRateProvider, IHasExchangeName + public class ExchangeSharpRateProvider : IRateProvider { readonly ExchangeAPI _ExchangeAPI; - readonly string _ExchangeName; - public ExchangeSharpRateProvider(string exchangeName, ExchangeAPI exchangeAPI, bool reverseCurrencyPair = false) + public ExchangeSharpRateProvider(ExchangeAPI exchangeAPI, bool reverseCurrencyPair = false) { if (exchangeAPI == null) throw new ArgumentNullException(nameof(exchangeAPI)); exchangeAPI.RequestTimeout = TimeSpan.FromSeconds(5.0); _ExchangeAPI = exchangeAPI; - _ExchangeName = exchangeName; ReverseCurrencyPair = reverseCurrencyPair; } @@ -30,9 +28,7 @@ namespace BTCPayServer.Services.Rates get; set; } - public string ExchangeName => _ExchangeName; - - public async Task GetRatesAsync(CancellationToken cancellationToken) + public async Task GetRatesAsync(CancellationToken cancellationToken) { await new SynchronizationContextRemover(); var rates = await _ExchangeAPI.GetTickersAsync(); @@ -43,14 +39,14 @@ namespace BTCPayServer.Services.Rates var exchangeRates = await Task.WhenAll(exchangeRateTasks); - return new ExchangeRates(exchangeRates + return exchangeRates .Where(t => t != null) - .ToArray()); + .ToArray(); } // ExchangeSymbolToGlobalSymbol throws exception which would kill perf ConcurrentDictionary notFoundSymbols = new ConcurrentDictionary(); - private async Task CreateExchangeRate(KeyValuePair ticker) + private async Task CreateExchangeRate(KeyValuePair ticker) { if (notFoundSymbols.TryGetValue(ticker.Key, out _)) return null; @@ -64,11 +60,7 @@ namespace BTCPayServer.Services.Rates } if(ReverseCurrencyPair) pair = new CurrencyPair(pair.Right, pair.Left); - var rate = new ExchangeRate(); - rate.CurrencyPair = pair; - rate.Exchange = _ExchangeName; - rate.BidAsk = new BidAsk(ticker.Value.Bid, ticker.Value.Ask); - return rate; + return new PairRate(pair, new BidAsk(ticker.Value.Bid, ticker.Value.Ask)); } catch (ArgumentException) { diff --git a/BTCPayServer.Rating/Providers/FallbackRateProvider.cs b/BTCPayServer.Rating/Providers/FallbackRateProvider.cs index f0f0d6174..c360d3f2b 100644 --- a/BTCPayServer.Rating/Providers/FallbackRateProvider.cs +++ b/BTCPayServer.Rating/Providers/FallbackRateProvider.cs @@ -17,7 +17,7 @@ namespace BTCPayServer.Services.Rates _Providers = providers; } - public async Task GetRatesAsync(CancellationToken cancellationToken) + public async Task GetRatesAsync(CancellationToken cancellationToken) { foreach (var p in _Providers) { @@ -31,7 +31,7 @@ namespace BTCPayServer.Services.Rates } catch(Exception ex) { Exceptions.Add(ex); } } - return new ExchangeRates(); + return Array.Empty(); } public List Exceptions { get; set; } = new List(); diff --git a/BTCPayServer.Rating/Providers/IHasExchangeName.cs b/BTCPayServer.Rating/Providers/IHasExchangeName.cs deleted file mode 100644 index 5d9fed6c3..000000000 --- a/BTCPayServer.Rating/Providers/IHasExchangeName.cs +++ /dev/null @@ -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; } - } -} diff --git a/BTCPayServer.Rating/Providers/IRateProvider.cs b/BTCPayServer.Rating/Providers/IRateProvider.cs index 711f3b13c..d1fece419 100644 --- a/BTCPayServer.Rating/Providers/IRateProvider.cs +++ b/BTCPayServer.Rating/Providers/IRateProvider.cs @@ -9,6 +9,6 @@ namespace BTCPayServer.Services.Rates { public interface IRateProvider { - Task GetRatesAsync(CancellationToken cancellationToken); + Task GetRatesAsync(CancellationToken cancellationToken); } } diff --git a/BTCPayServer.Rating/Providers/KrakenExchangeRateProvider.cs b/BTCPayServer.Rating/Providers/KrakenExchangeRateProvider.cs index 5ccdbab7d..135705047 100644 --- a/BTCPayServer.Rating/Providers/KrakenExchangeRateProvider.cs +++ b/BTCPayServer.Rating/Providers/KrakenExchangeRateProvider.cs @@ -14,7 +14,7 @@ using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.Rates { // Make sure that only one request is sent to kraken in general - public class KrakenExchangeRateProvider : IRateProvider, IHasExchangeName + public class KrakenExchangeRateProvider : IRateProvider { public KrakenExchangeRateProvider() { @@ -87,9 +87,9 @@ namespace BTCPayServer.Services.Rates { "ZGBP", "GBP" } }; - public async Task GetRatesAsync(CancellationToken cancellationToken) + public async Task GetRatesAsync(CancellationToken cancellationToken) { - var result = new ExchangeRates(); + var result = new List(); var symbols = await GetSymbolsAsync(cancellationToken); var normalizedPairsList = symbols.Where(s => !notFoundSymbols.ContainsKey(s)).Select(s => _Helper.NormalizeMarketSymbol(s)).ToList(); var csvPairsList = string.Join(",", normalizedPairsList); @@ -117,7 +117,7 @@ namespace BTCPayServer.Services.Rates global = await _Helper.ExchangeMarketSymbolToGlobalMarketSymbolAsync(symbol); } 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 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) diff --git a/BTCPayServer.Rating/Providers/NullRateProvider.cs b/BTCPayServer.Rating/Providers/NullRateProvider.cs index 75dcb3858..1569cb968 100644 --- a/BTCPayServer.Rating/Providers/NullRateProvider.cs +++ b/BTCPayServer.Rating/Providers/NullRateProvider.cs @@ -21,9 +21,9 @@ namespace BTCPayServer.Services.Rates return _Instance; } } - public Task GetRatesAsync(CancellationToken cancellationToken) + public Task GetRatesAsync(CancellationToken cancellationToken) { - return Task.FromResult(new ExchangeRates()); + return Task.FromResult(Array.Empty()); } } } diff --git a/BTCPayServer.Rating/Services/RateFetcher.cs b/BTCPayServer.Rating/Services/RateFetcher.cs index 459d6b148..16a8b4880 100644 --- a/BTCPayServer.Rating/Services/RateFetcher.cs +++ b/BTCPayServer.Rating/Services/RateFetcher.cs @@ -79,9 +79,9 @@ namespace BTCPayServer.Services.Rates result.Latency = query.Latency; if (query.Exception != null) 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(); diff --git a/BTCPayServer.Rating/Services/RateProviderFactory.cs b/BTCPayServer.Rating/Services/RateProviderFactory.cs index 53d34ea15..936b14bf3 100644 --- a/BTCPayServer.Rating/Services/RateProviderFactory.cs +++ b/BTCPayServer.Rating/Services/RateProviderFactory.cs @@ -25,7 +25,7 @@ namespace BTCPayServer.Services.Rates { _inner = inner; } - public async Task GetRatesAsync(CancellationToken cancellationToken) + public async Task GetRatesAsync(CancellationToken cancellationToken) { DateTimeOffset now = DateTimeOffset.UtcNow; try @@ -35,7 +35,7 @@ namespace BTCPayServer.Services.Rates catch (Exception ex) { Exception = ex; - return new ExchangeRates(); + return Array.Empty(); } finally { @@ -46,37 +46,15 @@ namespace BTCPayServer.Services.Rates public class QueryRateResult { public TimeSpan Latency { get; set; } - public ExchangeRates ExchangeRates { get; set; } + public PairRate[] PairRates { get; set; } public ExchangeException Exception { get; internal set; } + public string Exchange { get; internal set; } } public RateProviderFactory(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; - // We use 15 min because of limits with free version of bitcoinaverage - CacheSpan = TimeSpan.FromMinutes(15.0); 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 Dictionary _DirectProviders = new Dictionary(); public Dictionary 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("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("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD"); yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices"); @@ -103,14 +81,14 @@ namespace BTCPayServer.Services.Rates 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)); - Providers.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true)); - Providers.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), true)); - Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitBTCAPI(), true)); - Providers.Add("ndax", new ExchangeSharpRateProvider("ndax", new ExchangeNDAXAPI(), true)); + Providers.Add("binance", new ExchangeSharpRateProvider(new ExchangeBinanceAPI(), true)); + Providers.Add("bittrex", new ExchangeSharpRateProvider(new ExchangeBittrexAPI(), true)); + Providers.Add("poloniex", new ExchangeSharpRateProvider(new ExchangePoloniexAPI(), true)); + Providers.Add("hitbtc", new ExchangeSharpRateProvider(new ExchangeHitBTCAPI(), true)); + Providers.Add("ndax", new ExchangeSharpRateProvider(new ExchangeNDAXAPI(), true)); // 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("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS"))); Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK"))); @@ -132,11 +110,11 @@ namespace BTCPayServer.Services.Rates foreach (var supportedExchange in GetCoinGeckoSupportedExchanges()) { - if (!Providers.ContainsKey(supportedExchange.Id)) + if (!Providers.ContainsKey(supportedExchange.Id) && supportedExchange.Id != "coingecko") { var coingecko = new CoinGeckoRateProvider(_httpClientFactory) { - Exchange = supportedExchange.Id + UnderlyingExchange = supportedExchange.Id }; var bgFetcher = new BackgroundFetcherRateProvider(supportedExchange.Id, coingecko); bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0); @@ -191,8 +169,9 @@ namespace BTCPayServer.Services.Rates var value = await wrapper.GetRatesAsync(cancellationToken); return new QueryRateResult() { + Exchange = exchangeName, Latency = wrapper.Latency, - ExchangeRates = value, + PairRates = value, Exception = wrapper.Exception != null ? new ExchangeException() { Exception = wrapper.Exception, ExchangeName = exchangeName } : null }; } diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index fd5f2a57d..39c51b2f2 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -200,67 +200,27 @@ namespace BTCPayServer.Tests rateProvider.Providers.Clear(); var coinAverageMock = new MockRateProvider(); - coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() - { - Exchange = "coingecko", - CurrencyPair = CurrencyPair.Parse("BTC_USD"), - 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) - }); + coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_USD"), new BidAsk(5000m))); + coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_CAD"), new BidAsk(4500m))); + coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_LTC"), new BidAsk(162m))); + coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("LTC_USD"), new BidAsk(500m))); rateProvider.Providers.Add("coingecko", coinAverageMock); var bitflyerMock = new MockRateProvider(); - bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate() - { - Exchange = "bitflyer", - CurrencyPair = CurrencyPair.Parse("BTC_JPY"), - BidAsk = new BidAsk(700000m) - }); + bitflyerMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_JPY"), new BidAsk(700000m))); rateProvider.Providers.Add("bitflyer", bitflyerMock); var quadrigacx = new MockRateProvider(); - quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate() - { - Exchange = "quadrigacx", - CurrencyPair = CurrencyPair.Parse("BTC_CAD"), - BidAsk = new BidAsk(6000m) - }); + quadrigacx.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_CAD"), new BidAsk(6000m))); rateProvider.Providers.Add("quadrigacx", quadrigacx); var bittrex = new MockRateProvider(); - bittrex.ExchangeRates.Add(new Rating.ExchangeRate() - { - Exchange = "bittrex", - CurrencyPair = CurrencyPair.Parse("DOGE_BTC"), - BidAsk = new BidAsk(0.004m) - }); + bittrex.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("DOGE_BTC"), new BidAsk(0.004m))); rateProvider.Providers.Add("bittrex", bittrex); var bitfinex = new MockRateProvider(); - bitfinex.ExchangeRates.Add(new Rating.ExchangeRate() - { - Exchange = "bitfinex", - CurrencyPair = CurrencyPair.Parse("UST_BTC"), - BidAsk = new BidAsk(0.000136m) - }); + bitfinex.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("UST_BTC"), new BidAsk(0.000136m))); rateProvider.Providers.Add("bitfinex", bitfinex); } diff --git a/BTCPayServer.Tests/Mocks/MockRateProvider.cs b/BTCPayServer.Tests/Mocks/MockRateProvider.cs index ad33e1c92..52fa6e4a7 100644 --- a/BTCPayServer.Tests/Mocks/MockRateProvider.cs +++ b/BTCPayServer.Tests/Mocks/MockRateProvider.cs @@ -10,15 +10,15 @@ namespace BTCPayServer.Tests.Mocks { public class MockRateProvider : IRateProvider { - public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates(); + public List ExchangeRates { get; set; } = new List(); public MockRateProvider() { } - public Task GetRatesAsync(CancellationToken cancellationToken) + public Task GetRatesAsync(CancellationToken cancellationToken) { - return Task.FromResult(ExchangeRates); + return Task.FromResult(ExchangeRates.ToArray()); } } } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 6d4b0fb2b..e2dae7e36 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2684,14 +2684,14 @@ noninventoryitem: 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 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)) .ToList()) { Logs.Tester.LogInformation($"Testing {result.ExpectedName}"); result.Fetcher.InvalidateCache(); - var exchangeRates = result.ResultAsync.Result; + var exchangeRates = new ExchangeRates(result.ExpectedName, result.ResultAsync.Result); result.Fetcher.InvalidateCache(); Assert.NotNull(exchangeRates); Assert.NotEmpty(exchangeRates); @@ -2788,12 +2788,12 @@ noninventoryitem: class SpyRateProvider : IRateProvider { public bool Hit { get; set; } - public Task GetRatesAsync(CancellationToken cancellationToken) + public Task GetRatesAsync(CancellationToken cancellationToken) { Hit = true; - var rates = new ExchangeRates(); - rates.Add(new ExchangeRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(5000))); - return Task.FromResult(rates); + var rates = new List(); + rates.Add(new PairRate(CurrencyPair.Parse("BTC_USD"), new BidAsk(5000))); + return Task.FromResult(rates.ToArray()); } public void AssertHit() @@ -2890,7 +2890,6 @@ noninventoryitem: var factory = CreateBTCPayRateFactory(); factory.Providers.Clear(); - factory.CacheSpan = TimeSpan.FromSeconds(1); var fetcher = new RateFetcher(factory); factory.Providers.Clear(); var fetch = new BackgroundFetcherRateProvider("spy", spy);