Use CoinAverage as rate provider + add caching to avoid hitting limits

This commit is contained in:
nicolas.dorier
2017-10-27 11:39:11 +09:00
parent 55a4c3c08d
commit b71f9d0a08
5 changed files with 358 additions and 169 deletions

View File

@@ -20,6 +20,8 @@ using System.Diagnostics;
using Microsoft.EntityFrameworkCore.Extensions; using Microsoft.EntityFrameworkCore.Extensions;
using BTCPayServer.Data; using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Tests namespace BTCPayServer.Tests
{ {
@@ -220,9 +222,7 @@ namespace BTCPayServer.Tests
StoreId = user.StoreId, StoreId = user.StoreId,
TextSearch = invoice.OrderId TextSearch = invoice.OrderId
}).GetAwaiter().GetResult(); }).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length); Assert.Equal(1, textSearchResult.Length);
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery() textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
{ {
StoreId = user.StoreId, StoreId = user.StoreId,
@@ -352,6 +352,21 @@ namespace BTCPayServer.Tests
} }
} }
[Fact]
public void CheckRatesProvider()
{
var coinAverage = new CoinAverageRateProvider();
var jpy = coinAverage.GetRateAsync("JPY").GetAwaiter().GetResult();
var jpy2 = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRateAsync("JPY").GetAwaiter().GetResult();
var cached = new CachedRateProvider(coinAverage, new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }));
cached.CacheSpan = TimeSpan.FromSeconds(10);
var a = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
//Manually check that cache get hit after 10 sec
var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx) private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{ {
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString(); var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString();

View File

@@ -33,6 +33,7 @@ using System.Threading.Tasks;
using System.Threading; using System.Threading;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using BTCPayServer.Authentication; using BTCPayServer.Authentication;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
@@ -137,7 +138,10 @@ namespace BTCPayServer.Hosting
else else
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/")); return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
}); });
services.TryAddSingleton<IRateProvider, BitpayRateProvider>(); services.TryAddSingleton<IRateProvider>(o =>
{
return new CachedRateProvider(new CoinAverageRateProvider(), o.GetRequiredService<IMemoryCache>()) { CacheSpan = TimeSpan.FromMinutes(1.0) };
});
services.TryAddSingleton<InvoiceWatcher>(); services.TryAddSingleton<InvoiceWatcher>();
services.TryAddSingleton<InvoiceNotificationManager>(); services.TryAddSingleton<InvoiceNotificationManager>();
services.TryAddSingleton<IHostedService>(o => o.GetRequiredService<InvoiceWatcher>()); services.TryAddSingleton<IHostedService>(o => o.GetRequiredService<InvoiceWatcher>());

View File

@@ -67,7 +67,7 @@ namespace BTCPayServer.Hosting
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
services.ConfigureBTCPayServer(Configuration); services.ConfigureBTCPayServer(Configuration);
services.AddMemoryCache();
services.AddIdentity<ApplicationUser, IdentityRole>() services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Services.Rates
{
public class CachedRateProvider : IRateProvider
{
private IRateProvider _Inner;
private IMemoryCache _MemoryCache;
public CachedRateProvider(IRateProvider inner, IMemoryCache memoryCache)
{
if(inner == null)
throw new ArgumentNullException(nameof(inner));
if(memoryCache == null)
throw new ArgumentNullException(nameof(memoryCache));
this._Inner = inner;
this._MemoryCache = memoryCache;
}
public TimeSpan CacheSpan
{
get;
set;
} = TimeSpan.FromMinutes(1.0);
public Task<decimal> GetRateAsync(string currency)
{
return _MemoryCache.GetOrCreateAsync("CURR_" + currency, (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRateAsync(currency);
});
}
private bool TryGetFromCache(string key, out object obj)
{
obj = _MemoryCache.Get(key);
return obj != null;
}
public Task<ICollection<Rate>> GetRatesAsync()
{
return _MemoryCache.GetOrCreateAsync("GLOBAL_RATES", (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRatesAsync();
});
}
}
}

View File

@@ -0,0 +1,116 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public class CoinAverageException : Exception
{
public CoinAverageException(string message) : base(message)
{
}
}
public class CoinAverageRateProvider : IRateProvider
{
public class RatesJson
{
public class RateJson
{
public string Code
{
get; set;
}
public decimal Rate
{
get; set;
}
}
[JsonProperty("rates")]
public JObject RatesInternal
{
get; set;
}
[JsonIgnore]
public List<RateJson> Rates
{
get; set;
}
[JsonIgnore]
public Dictionary<string, decimal> RatesByCurrency
{
get; set;
}
public decimal GetRate(string currency)
{
if(!RatesByCurrency.TryGetValue(currency.ToUpperInvariant(), out decimal currUSD))
throw new RateUnavailableException(currency);
if(!RatesByCurrency.TryGetValue("BTC", out decimal btcUSD))
throw new RateUnavailableException(currency);
return currUSD / btcUSD;
}
public void CalculateDictionary()
{
RatesByCurrency = new Dictionary<string, decimal>();
Rates = new List<RateJson>();
foreach(var rate in RatesInternal.OfType<JProperty>())
{
var rateJson = new RateJson();
rateJson.Code = rate.Name;
rateJson.Rate = rate.Value["rate"].Value<decimal>();
RatesByCurrency.Add(rate.Name, rateJson.Rate);
Rates.Add(rateJson);
}
}
}
static HttpClient _Client = new HttpClient();
public string Market
{
get; set;
} = "global";
public async Task<decimal> GetRateAsync(string currency)
{
RatesJson rates = await GetRatesCore();
return rates.GetRate(currency);
}
private async Task<RatesJson> GetRatesCore()
{
var resp = await _Client.GetAsync("https://apiv2.bitcoinaverage.com/constants/exchangerates/" + Market);
using(resp)
{
if((int)resp.StatusCode == 401)
throw new CoinAverageException("Unauthorized access to the API");
if((int)resp.StatusCode == 429)
throw new CoinAverageException("Exceed API limits");
if((int)resp.StatusCode == 403)
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
resp.EnsureSuccessStatusCode();
var rates = JsonConvert.DeserializeObject<RatesJson>(await resp.Content.ReadAsStringAsync());
rates.CalculateDictionary();
return rates;
}
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
RatesJson rates = await GetRatesCore();
return rates.Rates.Select(o => new Rate()
{
Currency = o.Code,
Value = rates.GetRate(o.Code)
}).ToList();
}
}
}