Skip to content

Commit

Permalink
CoinGecko Rate Provider
Browse files Browse the repository at this point in the history
The CoinGecko rate provider is similar to the bitcoin average one, in that you can ask it for a rate from its aggregated sourcing or you can get rates from specific exchanges. I've added support for both.
I haven't integrated it or replaced coinaverage just to see if we should use it as the default and switch everyone to it or what other action to take.
  • Loading branch information
Kukks authored and NicolasDorier committed Jan 13, 2020
1 parent 1a3da09 commit 58d9a48
Show file tree
Hide file tree
Showing 17 changed files with 227 additions and 212 deletions.
16 changes: 16 additions & 0 deletions BTCPayServer.Rating/AvailableRateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace BTCPayServer.Rating
{
public class AvailableRateProvider
{
public string Name { get; set; }
public string Url { get; set; }
public string Id { get; set; }

public AvailableRateProvider(string id, string name, string url)
{
Id = id;
Name = name;
Url = url;
}
}
}
3 changes: 0 additions & 3 deletions BTCPayServer.Rating/CurrencyPair.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BTCPayServer.Rating
{
Expand Down
1 change: 0 additions & 1 deletion BTCPayServer.Rating/ExchangeRates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;

namespace BTCPayServer.Rating
{
Expand Down
4 changes: 0 additions & 4 deletions BTCPayServer.Rating/Providers/CoinAverageRateProvider.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
using Newtonsoft.Json;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using BTCPayServer.Rating;
Expand Down
117 changes: 1 addition & 116 deletions BTCPayServer.Rating/Providers/CoinAverageSettings.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
Expand All @@ -20,125 +18,12 @@ public Task AddHeader(HttpRequestMessage message)
return _Settings.AddHeader(message);
}
}

public class CoinAverageExchange
{
public CoinAverageExchange(string name, string display, string url)
{
Name = name;
Display = display;
Url = url;
}
public string Name { get; set; }
public string Display { get; set; }
public string Url
{
get;
set;
}
}
public class CoinAverageExchanges : Dictionary<string, CoinAverageExchange>
{
public CoinAverageExchanges()
{
}

public void Add(CoinAverageExchange exchange)
{
if (!TryAdd(exchange.Name, exchange))
{
this.Remove(exchange.Name);
this.Add(exchange.Name, exchange);
}
}
}
public class CoinAverageSettings : ICoinAverageAuthenticator
{
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

public (String PublicKey, String PrivateKey)? KeyPair { get; set; }
public CoinAverageExchanges AvailableExchanges { get; set; } = new CoinAverageExchanges();

public CoinAverageSettings()
{
//GENERATED BY:
//StringBuilder b = new StringBuilder();
//b.AppendLine("_coinAverageSettings.AvailableExchanges = new[] {");
//foreach (var availableExchange in _coinAverageSettings.AvailableExchanges)
//{
// b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),");
//}
//b.AppendLine("}.ToArray()");
AvailableExchanges = new CoinAverageExchanges();
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"),
})
{
AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}"));
}
// Keep back-compat
AvailableExchanges.Add(new CoinAverageExchange("gdax", string.Empty, $"https://apiv2.bitcoinaverage.com/exchanges/coinbasepro"));
}


public Task AddHeader(HttpRequestMessage message)
{
var signature = GetCoinAverageSignature();
Expand Down
96 changes: 96 additions & 0 deletions BTCPayServer.Rating/Providers/CoinGeckoRateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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, IHasExchangeName
{
private readonly HttpClient Client;
public static string CoinGeckoName { get; } = "coingecko";
public string Exchange { get; set; }
public string ExchangeName => Exchange ?? CoinGeckoName;

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");
}

private IEnumerable<AvailableRateProvider> _availableExchanges;

public virtual async Task<IEnumerable<AvailableRateProvider>> 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<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
return ExchangeName == CoinGeckoName ? GetCoinGeckoRates() : GetCoinGeckoExchangeSpecificRates();
}

private async Task<ExchangeRates> GetCoinGeckoRates()
{
var resp = await Client.GetAsync("exchange_rates");
resp.EnsureSuccessStatusCode();
return new ExchangeRates(JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("rates").Children()
.Select(token => new ExchangeRate(CoinGeckoName,
new CurrencyPair("BTC", ((JProperty)token).Name.ToString()),
new BidAsk(((JProperty)token).Value["value"].Value<decimal>()))));
}

private async Task<ExchangeRates> GetCoinGeckoExchangeSpecificRates(int page = 1)
{
var resp = await Client.GetAsync($"exchanges/{Exchange}/tickers?page={page}");

resp.EnsureSuccessStatusCode();
List<ExchangeRate> result = JObject.Parse(await resp.Content.ReadAsStringAsync()).GetValue("tickers")
.Select(token => new ExchangeRate(ExchangeName,
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<ExchangeRates>>();
for (int i = 2; i <= totalPages; i++)
{
tasks.Add(GetCoinGeckoExchangeSpecificRates(i));
}

foreach (var t in (await Task.WhenAll(tasks)))
{
result.AddRange(t);
}
}

return new ExchangeRates(result);
}
}
}
1 change: 0 additions & 1 deletion BTCPayServer.Rating/RateRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down
57 changes: 34 additions & 23 deletions BTCPayServer.Rating/Services/RateProviderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ public RateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
_CacheOptions = cacheOptions;
// We use 15 min because of limits with free version of bitcoinaverage
CacheSpan = TimeSpan.FromMinutes(15.0);
InitExchanges();
}
private IOptions<MemoryCacheOptions> _CacheOptions;
TimeSpan _CacheSpan;
Expand All @@ -81,7 +80,7 @@ public void InvalidateCache()
provider.CacheSpan = CacheSpan;
provider.MemoryCache = cache;
}
if (Providers.TryGetValue(CoinAverageRateProvider.CoinAverageName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c)
if (Providers.TryGetValue(CoinGeckoRateProvider.CoinGeckoName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c)
{
c.RefreshRate = CacheSpan;
c.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
Expand All @@ -98,7 +97,7 @@ public Dictionary<string, IRateProvider> Providers
}
}

private void InitExchanges()
public async Task 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));
Expand All @@ -112,6 +111,7 @@ private void InitExchanges()
// 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 });
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
Expand All @@ -129,7 +129,7 @@ private void InitExchanges()
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 == CoinAverageRateProvider.CoinAverageName)
if(provider.Key == CoinGeckoRateProvider.CoinGeckoName)
{
prov.RefreshRate = CacheSpan;
prov.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
Expand All @@ -143,40 +143,51 @@ private void InitExchanges()
}

var cache = new MemoryCache(_CacheOptions);
foreach (var supportedExchange in GetSupportedExchanges())
foreach (var supportedExchange in await GetSupportedExchanges(true))
{
if (!Providers.ContainsKey(supportedExchange.Key))
if (!Providers.ContainsKey(supportedExchange.Id))
{
var coinAverage = new CoinAverageRateProvider()
var coinAverage = new CoinGeckoRateProvider(_httpClientFactory)
{
Exchange = supportedExchange.Key,
HttpClient = _httpClientFactory?.CreateClient(),
Authenticator = _CoinAverageSettings
Exchange = supportedExchange.Id
};
var cached = new CachedRateProvider(supportedExchange.Key, coinAverage, cache)
var cached = new CachedRateProvider(supportedExchange.Id, coinAverage, cache)
{
CacheSpan = CacheSpan
};
Providers.Add(supportedExchange.Key, cached);
Providers.Add(supportedExchange.Id, cached);
}
}
}

public CoinAverageExchanges GetSupportedExchanges()
public async Task<IEnumerable<AvailableRateProvider>> GetSupportedExchanges(bool reload = false)
{
CoinAverageExchanges exchanges = new CoinAverageExchanges();
foreach (var exchange in _CoinAverageSettings.AvailableExchanges)
IEnumerable<AvailableRateProvider> exchanges;
switch (Providers[CoinGeckoRateProvider.CoinGeckoName])
{
exchanges.Add(exchange.Value);
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;
}

// Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average", $"https://apiv2.bitcoinaverage.com/indices/global/ticker/short"));
exchanges.Add(new CoinAverageExchange("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD"));
exchanges.Add(new CoinAverageExchange("ndax", "NDAX", "https://ndax.io/api/returnTicker"));
exchanges.Add(new CoinAverageExchange("bitbank", "Bitbank", "https://public.bitbank.cc/prices"));

return exchanges;
return new[]
{
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);
}

public async Task<QueryRateResult> QueryRates(string exchangeName, CancellationToken cancellationToken)
Expand Down
Loading

0 comments on commit 58d9a48

Please sign in to comment.