mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 21:32:27 +01:00
Use CoinAverage as rate provider + add caching to avoid hitting limits
This commit is contained in:
parent
55a4c3c08d
commit
b71f9d0a08
@ -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);
|
||||
}
|
||||
|
@ -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>());
|
||||
|
@ -67,7 +67,7 @@ namespace BTCPayServer.Hosting
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.ConfigureBTCPayServer(Configuration);
|
||||
|
||||
services.AddMemoryCache();
|
||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
54
BTCPayServer/Services/Rates/CachedRateProvider.cs
Normal file
54
BTCPayServer/Services/Rates/CachedRateProvider.cs
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
116
BTCPayServer/Services/Rates/CoinAverageRateProvider.cs
Normal file
116
BTCPayServer/Services/Rates/CoinAverageRateProvider.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user