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 BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Tests
{
@ -67,7 +69,7 @@ namespace BTCPayServer.Tests
[Fact]
public void CanPayUsingBIP70()
{
using (var tester = ServerTester.Create())
using(var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
@ -116,7 +118,7 @@ namespace BTCPayServer.Tests
[Fact]
public void CanUseServerInitiatedPairingCode()
{
using (var tester = ServerTester.Create())
using(var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
@ -141,9 +143,9 @@ namespace BTCPayServer.Tests
[Fact]
public void CanSendIPN()
{
using (var callbackServer = new CustomServer())
using(var callbackServer = new CustomServer())
{
using (var tester = ServerTester.Create())
using(var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
@ -176,7 +178,7 @@ namespace BTCPayServer.Tests
[Fact]
public void CantPairTwiceWithSamePubkey()
{
using (var tester = ServerTester.Create())
using(var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
@ -195,7 +197,7 @@ namespace BTCPayServer.Tests
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{
using (var tester = ServerTester.Create())
using(var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
@ -220,9 +222,7 @@ namespace BTCPayServer.Tests
StoreId = user.StoreId,
TextSearch = invoice.OrderId
}).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length);
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
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)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString();
@ -361,14 +376,14 @@ namespace BTCPayServer.Tests
private void Eventually(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
while (true)
while(true)
{
try
{
act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
catch(XunitException) when(!cts.Token.IsCancellationRequested)
{
cts.Token.WaitHandle.WaitOne(500);
}

View File

@ -33,6 +33,7 @@ using System.Threading.Tasks;
using System.Threading;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Authentication;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Hosting
{
@ -137,7 +138,10 @@ namespace BTCPayServer.Hosting
else
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<InvoiceNotificationManager>();
services.TryAddSingleton<IHostedService>(o => o.GetRequiredService<InvoiceWatcher>());

View File

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