btcpayserver/BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs

108 lines
31 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
2022-04-04 14:47:22 +09:00
internal static readonly string SupportedExchanges = "[{\"id\":\"1bch\",\"name\":\"1BCH\"},{\"id\":\"aave\",\"name\":\"Aave\"},{\"id\":\"aax\",\"name\":\"AAX\"},{\"id\":\"aax_futures\",\"name\":\"AAX Futures\"},{\"id\":\"abcc\",\"name\":\"ABCC\"},{\"id\":\"acdx\",\"name\":\"ACDX\"},{\"id\":\"acdx_futures\",\"name\":\"ACDX Futures\"},{\"id\":\"acsi_finance\",\"name\":\"Acsi Finance\"},{\"id\":\"aex\",\"name\":\"AEX\"},{\"id\":\"agora_swap\",\"name\":\"Agora Swap\"},{\"id\":\"algebra_finance\",\"name\":\"Algebra finance\"},{\"id\":\"allcoin\",\"name\":\"Allcoin\"},{\"id\":\"alpha_five\",\"name\":\"Alpha5\"},{\"id\":\"altcointrader\",\"name\":\"AltcoinTrader\"},{\"id\":\"alterdice\",\"name\":\"AlterDice\"},{\"id\":\"altmarkets\",\"name\":\"Altmarkets\"},{\"id\":\"anyswap\",\"name\":\"Anyswap\"},{\"id\":\"apeswap\",\"name\":\"ApeSwap\"},{\"id\":\"apeswap_polygon\",\"name\":\"ApeSwap (Polygon)\"},{\"id\":\"aprobit\",\"name\":\"Aprobit\"},{\"id\":\"artisturba\",\"name\":\"Artis Turba\"},{\"id\":\"astroport\",\"name\":\"Astroport\"},{\"id\":\"atomars\",\"name\":\"Atomars\"},{\"id\":\"auroraswap\",\"name\":\"AuroraSwap\"},{\"id\":\"autoshark_finance\",\"name\":\"AutoShark Finance\"},{\"id\":\"azbit\",\"name\":\"Azbit\"},{\"id\":\"b2bx\",\"name\":\"B2BX\"},{\"id\":\"baguette\",\"name\":\"Baguette\"},{\"id\":\"bakeryswap\",\"name\":\"Bakeryswap\"},{\"id\":\"bakkt\",\"name\":\"Bakkt\"},{\"id\":\"balanced_network\",\"name\":\"Balanced Network\"},{\"id\":\"balancer\",\"name\":\"Balancer (v2)\"},{\"id\":\"balancer_arbitrum\",\"name\":\"Balancer (Arbitrum)\"},{\"id\":\"balancer_polygon\",\"name\":\"Balancer (Polygon)\"},{\"id\":\"balancer_v1\",\"name\":\"Balancer (v1)\"},{\"id\":\"bamboo_relay\",\"name\":\"Bamboo Relay\"},{\"id\":\"bancor\",\"name\":\"Bancor Network\"},{\"id\":\"basefex\",\"name\":\"BaseFEX\"},{\"id\":\"bcex\",\"name\":\"BCEX\"},{\"id\":\"beamswap\",\"name\":\"Beamswap\"},{\"id\":\"beaxy\",\"name\":\"Beaxy\"},{\"id\":\"beethovenx\",\"name\":\"Beethoven X\"},{\"id\":\"benswap_smart_bitcoin_cash\",\"name\":\"Benswap\"},{\"id\":\"bgogo\",\"name\":\"Bgogo\"},{\"id\":\"bibo\",\"name\":\"Bibo\"},{\"id\":\"bibox\",\"name\":\"Bibox\"},{\"id\":\"bibox_futures\",\"name\":\"Bibox (Futures)\"},{\"id\":\"biconomy\",\"name\":\"Biconomy\"},{\"id\":\"bidesk\",\"name\":\"Bidesk\"},{\"id\":\"bigone\",\"name\":\"BigONE\"},{\"id\":\"bigone_futures\",\"name\":\"BigONE Futures\"},{\"id\":\"bilaxy\",\"name\":\"Bilaxy\"},{\"id\":\"binance\",\"name\":\"Binance\"},{\"id\":\"binance_dex\",\"name\":\"Binance DEX\"},{\"id\":\"binance_dex_mini\",\"name\":\"Binance DEX (Mini)\"},{\"id\":\"binance_futures\",\"name\":\"Binance (Futures)\"},{\"id\":\"binance_jersey\",\"name\":\"Binance Jersey\"},{\"id\":\"binance_us\",\"name\":\"Binance US\"},{\"id\":\"bingx\",\"name\":\"BingX\"},{\"id\":\"bione\",\"name\":\"BiONE\"},{\"id\":\"birake\",\"name\":\"Birake\"},{\"id\":\"bisq\",\"name\":\"Bisq\"},{\"id\":\"biswap\",\"name\":\"Biswap\"},{\"id\":\"bit2c\",\"name\":\"Bit2c\"},{\"id\":\"bitalong\",\"name\":\"Bitalong\"},{\"id\":\"bitazza\",\"name\":\"Bitazza\"},{\"id\":\"bitbank\",\"name\":\"Bitbank\"},{\"id\":\"bitbay\",\"name\":\"Zonda\"},{\"id\":\"bitbns\",\"name\":\"BitBNS\"},{\"id\":\"bitbox\",\"name\":\"BITFRONT\"},{\"id\":\"bitci\",\"name\":\"Bitci\"},{\"id\":\"bitcoin_com\",\"name\":\"FMFW.io\"},{\"id\":\"bit_com\",\"name\":\"Bit.com\"},{\"id\":\"bit_com_futures\",\"name\":\"Bit.com (Futures)\"},{\"id\":\"bitcratic\",\"name\":\"Bitcratic\"},{\"id\":\"bitexbook\",\"name\":\"BITEXBOOK\"},{\"id\":\"bitexen\",\"name\":\"Bitexen\"},{\"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\":\"bitget\",\"name\":\"Bitget\"},{\"id\":\"bitget_futures\",\"name\":\"Bitget Futures\"},{\"id\":\"bithash\",\"name\
private readonly HttpClient Client;
2020-06-28 17:55:27 +09:00
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)
{
2020-06-28 17:55:27 +09:00
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();
}
}
}