btcpayserver/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs

107 lines
21 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
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\"},
private readonly HttpClient Client;
public string UnderlyingExchange
2020-01-17 14:56:05 +09:00
{
get;
set;
2020-01-17 14:56:05 +09:00
}
public CoinGeckoRateProvider(IHttpClientFactory httpClientFactory)
{
if (httpClientFactory == null)
{
return;;
}
Client = httpClientFactory.CreateClient();
Client.BaseAddress = new Uri("https://api.coingecko.com/api/v3/");
Client.DefaultRequestHeaders.Add("Accept", "application/json");
}
public virtual Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
return UnderlyingExchange is null ? GetCoinGeckoRates(cancellationToken) : GetCoinGeckoExchangeSpecificRates(1, cancellationToken);
}
private async Task<PairRate[]> GetCoinGeckoRates(CancellationToken cancellationToken)
{
using var resp = await GetWithBackoffAsync("exchange_rates", cancellationToken);
resp.EnsureSuccessStatusCode();
return JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("rates").Children()
.Where(token => ((JProperty)token).Name != "btc")
.Select(token => new PairRate(new CurrencyPair("BTC", ((JProperty)token).Name.ToString()),
new BidAsk(((JProperty)token).Value["value"].Value<decimal>()))).ToArray();
}
private async Task<HttpResponseMessage> GetWithBackoffAsync(string request, CancellationToken cancellationToken)
{
TimeSpan retryWait = TimeSpan.FromSeconds(1);
retry:
var resp = await Client.GetAsync(request, cancellationToken);
if (resp.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
resp.Dispose();
if (retryWait < TimeSpan.FromSeconds(60))
{
await Task.Delay(retryWait, cancellationToken);
retryWait = TimeSpan.FromSeconds(retryWait.TotalSeconds * 2);
goto retry;
}
resp.EnsureSuccessStatusCode();
}
return resp;
}
private async Task<PairRate[]> GetCoinGeckoExchangeSpecificRates(int page, CancellationToken cancellationToken)
{
using var resp = await GetWithBackoffAsync($"exchanges/{UnderlyingExchange}/tickers?page={page}", cancellationToken);
resp.EnsureSuccessStatusCode();
List<PairRate> result = JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("tickers")
.Select(token => new PairRate(new CurrencyPair(token.Value<string>("base"), token.Value<string>("target")),
new BidAsk(token.Value<decimal>("last")))).ToList();
if (page == 1 && resp.Headers.TryGetValues("total", out var total) &&
resp.Headers.TryGetValues("per-page", out var perPage))
{
var totalItems = int.Parse(total.First());
var perPageItems = int.Parse(perPage.First());
var totalPages = totalItems / perPageItems;
if (totalItems % perPageItems != 0)
{
totalPages++;
}
var tasks = new List<Task<PairRate[]>>();
for (int i = 2; i <= totalPages; i++)
{
tasks.Add(GetCoinGeckoExchangeSpecificRates(i, cancellationToken));
}
foreach (var t in (await Task.WhenAll(tasks)))
{
result.AddRange(t);
}
}
return result.ToArray();
}
}
}