diff --git a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Litecoin.cs b/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Litecoin.cs index b492947b3..3b7458f67 100644 --- a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Litecoin.cs +++ b/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Litecoin.cs @@ -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), diff --git a/BTCPayServer.Common/Altcoins/Monero/BTCPayNetworkProvider.Monero.cs b/BTCPayServer.Common/Altcoins/Monero/BTCPayNetworkProvider.Monero.cs index 208aea77c..f81731ff0 100644 --- a/BTCPayServer.Common/Altcoins/Monero/BTCPayNetworkProvider.Monero.cs +++ b/BTCPayServer.Common/Altcoins/Monero/BTCPayNetworkProvider.Monero.cs @@ -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" }); } diff --git a/BTCPayServer.Rating/AvailableRateProvider.cs b/BTCPayServer.Rating/AvailableRateProvider.cs index 2265d06c4..74c636cb4 100644 --- a/BTCPayServer.Rating/AvailableRateProvider.cs +++ b/BTCPayServer.Rating/AvailableRateProvider.cs @@ -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; } } } diff --git a/BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs b/BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs index c7bca5e70..dbd504ad3 100644 --- a/BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs +++ b/BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs @@ -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(); return response; } - - public async Task 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(); - var exchanges = (JObject)jobj["exchanges"]; - response.Exchanges = exchanges - .Properties() - .Select(p => - { - var exchange = JsonConvert.DeserializeObject(p.Value.ToString()); - exchange.Name = p.Name; - return exchange; - }) - .ToArray(); - return response; - } } public class GetRateLimitsResponse diff --git a/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs b/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs index e08a43c74..24d4f4b1e 100644 --- a/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs +++ b/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs @@ -11,6 +11,8 @@ namespace BTCPayServer.Services.Rates { public class CoinGeckoRateProvider : IRateProvider, IHasExchangeName { + // 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; } @@ -27,21 +29,6 @@ namespace BTCPayServer.Services.Rates Client.DefaultRequestHeaders.Add("Accept", "application/json"); } - private IEnumerable _availableExchanges; - - public virtual async Task> GetAvailableExchanges(bool reload = false) - { - if (_availableExchanges != null && !reload) return _availableExchanges; - var resp = await Client.GetAsync("exchanges/list"); - resp.EnsureSuccessStatusCode(); - _availableExchanges = JArray.Parse(await resp.Content.ReadAsStringAsync()) - .Select(token => - new AvailableRateProvider(token["id"].ToString().ToLowerInvariant(), token["name"].ToString(), - $"{Client.BaseAddress}exchanges/{token["id"]}/tickers")); - - return _availableExchanges; - } - public virtual Task GetRatesAsync(CancellationToken cancellationToken) { return ExchangeName == CoinGeckoName ? GetCoinGeckoRates() : GetCoinGeckoExchangeSpecificRates(); @@ -49,9 +36,10 @@ namespace BTCPayServer.Services.Rates private async Task GetCoinGeckoRates() { - var resp = await Client.GetAsync("exchange_rates"); + using var resp = await Client.GetAsync("exchange_rates"); resp.EnsureSuccessStatusCode(); return new ExchangeRates(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())))); @@ -59,7 +47,7 @@ namespace BTCPayServer.Services.Rates private async Task GetCoinGeckoExchangeSpecificRates(int page = 1) { - var resp = await Client.GetAsync($"exchanges/{Exchange}/tickers?page={page}"); + using var resp = await Client.GetAsync($"exchanges/{Exchange}/tickers?page={page}"); resp.EnsureSuccessStatusCode(); List result = JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("tickers") diff --git a/BTCPayServer.Rating/Services/RateProviderFactory.cs b/BTCPayServer.Rating/Services/RateProviderFactory.cs index 8911f7d5e..e177eee12 100644 --- a/BTCPayServer.Rating/Services/RateProviderFactory.cs +++ b/BTCPayServer.Rating/Services/RateProviderFactory.cs @@ -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 _CacheOptions; TimeSpan _CacheSpan; @@ -96,8 +98,22 @@ namespace BTCPayServer.Services.Rates return _DirectProviders; } } + internal IEnumerable 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> GetSupportedExchanges(bool reload = false) + IEnumerable _AvailableRateProviders = null; + public IEnumerable GetSupportedExchanges() { - IEnumerable 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(); + 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 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 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 QueryRates(string exchangeName, CancellationToken cancellationToken) diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 9df171299..fd5f2a57d 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -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"), - }); } diff --git a/BTCPayServer.Tests/Mocks/MockRateProvider.cs b/BTCPayServer.Tests/Mocks/MockRateProvider.cs index b84e95377..ad33e1c92 100644 --- a/BTCPayServer.Tests/Mocks/MockRateProvider.cs +++ b/BTCPayServer.Tests/Mocks/MockRateProvider.cs @@ -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 AvailableRateProviders { get; set; } = new List(); - public MockRateProvider():base(null) + public MockRateProvider() { } - public override Task GetRatesAsync(CancellationToken cancellationToken) + public Task GetRatesAsync(CancellationToken cancellationToken) { return Task.FromResult(ExchangeRates); } - - public override Task> GetAvailableExchanges(bool reload = false) - { - return Task.FromResult((IEnumerable)AvailableRateProviders); - } } } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 09f7fcb4a..af96e6471 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -962,9 +962,9 @@ namespace BTCPayServer.Tests Assert.Null(GetRatesResult?.Data); var store = acc.GetController(); - var ratesVM = (RatesViewModel)(Assert.IsType(await store.Rates()).Model); + var ratesVM = (RatesViewModel)(Assert.IsType(store.Rates()).Model); ratesVM.DefaultCurrencyPairs = "BTC_USD,LTC_USD"; - store.Rates(ratesVM).Wait(); + await store.Rates(ratesVM); store = acc.GetController(); rateController = acc.GetController(); GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, default) @@ -1240,9 +1240,9 @@ namespace BTCPayServer.Tests user.GrantAccess(); user.RegisterDerivationScheme("BTC"); List rates = new List(); - 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 CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD") { var storeController = user.GetController(); - 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(); - 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(); - var rateVm = Assert.IsType(Assert.IsType( await store.Rates()).Model); + var rateVm = Assert.IsType(Assert.IsType(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(store.Rates(rateVm, "Save").Result); - rateVm = Assert.IsType(Assert.IsType( await store.Rates()).Model); + Assert.IsType(await store.Rates(rateVm, "Save")); + rateVm = Assert.IsType(Assert.IsType(store.Rates()).Model); Assert.Equal("bitflyer", rateVm.PreferredExchange); rateVm.ScriptTest = "BTC_JPY,BTC_CAD"; rateVm.Spread = 10; store = user.GetController(); - rateVm = Assert.IsType(Assert.IsType(store.Rates(rateVm, "Test").Result).Model); + rateVm = Assert.IsType(Assert.IsType(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(store.Rates(rateVm, "Save").Result); + Assert.IsType(await store.Rates(rateVm, "Save")); Assert.IsType(store.ShowRateRulesPost(true).Result); - Assert.IsType(store.Rates(rateVm, "Save").Result); + Assert.IsType(await store.Rates(rateVm, "Save")); store = user.GetController(); - rateVm = Assert.IsType(Assert.IsType( await store.Rates()).Model); + rateVm = Assert.IsType(Assert.IsType(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(Assert.IsType(store.Rates(rateVm, "Test").Result).Model); + rateVm = Assert.IsType(Assert.IsType(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(Assert.IsType(store.Rates(rateVm, "Test").Result).Model); + rateVm = Assert.IsType(Assert.IsType(await store.Rates(rateVm, "Test")).Model); Assert.True(rateVm.TestRateRules.All(t => !t.Error)); - Assert.IsType(store.Rates(rateVm, "Save").Result); + Assert.IsType(await store.Rates(rateVm, "Save")); store = user.GetController(); - rateVm = Assert.IsType(Assert.IsType( await store.Rates()).Model); + rateVm = Assert.IsType(Assert.IsType(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() diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index 3e21ef995..de189636e 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -86,14 +86,14 @@ namespace BTCPayServer.Configuration var supportedChains = conf.GetOrDefault("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(); var parentChains = elementsBased.Select(network => network.NetworkCryptoCode.ToUpperInvariant()).Distinct(); var allSubChains = networkProvider.GetAll().OfType() - .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) diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 877de6362..e8a67094e 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -193,9 +193,9 @@ namespace BTCPayServer.Controllers [HttpGet] [Route("{storeId}/rates")] - public async Task 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> GetSupportedExchanges() + private IEnumerable 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); diff --git a/BTCPayServer/HostedServices/RatesHostedService.cs b/BTCPayServer/HostedServices/RatesHostedService.cs index e9408c46e..b75f4f562 100644 --- a/BTCPayServer/HostedServices/RatesHostedService.cs +++ b/BTCPayServer/HostedServices/RatesHostedService.cs @@ -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()) ?? new RatesSetting(); diff --git a/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs b/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs index d2114adcc..0341ed1ab 100644 --- a/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/RatesViewModel.cs @@ -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 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 TestRateRules { get; set; } public SelectList Exchanges { get; set; } diff --git a/BTCPayServer/Properties/launchSettings.json b/BTCPayServer/Properties/launchSettings.json index a3d3c1fd1..08c21a244 100644 --- a/BTCPayServer/Properties/launchSettings.json +++ b/BTCPayServer/Properties/launchSettings.json @@ -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", diff --git a/BTCPayServer/Views/Shared/Confirm.cshtml b/BTCPayServer/Views/Shared/Confirm.cshtml index 8632fe749..22f2975cb 100644 --- a/BTCPayServer/Views/Shared/Confirm.cshtml +++ b/BTCPayServer/Views/Shared/Confirm.cshtml @@ -27,7 +27,7 @@
- +
diff --git a/BTCPayServer/Views/Stores/Rates.cshtml b/BTCPayServer/Views/Stores/Rates.cshtml index 2970584be..23973de7d 100644 --- a/BTCPayServer/Views/Stores/Rates.cshtml +++ b/BTCPayServer/Views/Stores/Rates.cshtml @@ -18,15 +18,61 @@ {
Scripting
- Rate script allows you to express precisely how you want to calculate rates for currency pairs. -

- Supported exchanges are: - @for (int i = 0; i < Model.AvailableExchanges.Count(); i++) - { - @Model.AvailableExchanges.ElementAt(i).Name@(i == Model.AvailableExchanges.Count() - 1 ? "" : ",") - } -

-

Click here for more information

+

Rate script allows you to express precisely how you want to calculate rates for currency pairs.

+

We are retrieving the rate of each exchange either directly, via CoinGecko (free) or BitcoinAverage (commercial)

+
+
+
+

+ +

+
+
+
+ @foreach (var exchange in Model.AvailableExchanges.Where(a => a.Source == BTCPayServer.Rating.RateSource.Direct)) + { + @exchange.Id  + } +
+
+
+
+
+

+ +

+
+
+
+ @foreach (var exchange in Model.AvailableExchanges.Where(a => a.Source == BTCPayServer.Rating.RateSource.Coingecko)) + { + @exchange.Id  + } +
+
+
+
+
+

+ +

+
+
+
+ @foreach (var exchange in Model.AvailableExchanges.Where(a => a.Source == BTCPayServer.Rating.RateSource.CoinAverage)) + { + @exchange.Id  + } +
+
+
+
} @if (Model.TestRateRules != null) @@ -110,7 +156,7 @@