mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-20 15:34:24 +01:00
Use CoinAverage as rate provider + add caching to avoid hitting limits
This commit is contained in:
@@ -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
|
||||||
{
|
{
|
||||||
@@ -67,7 +69,7 @@ namespace BTCPayServer.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void CanPayUsingBIP70()
|
public void CanPayUsingBIP70()
|
||||||
{
|
{
|
||||||
using (var tester = ServerTester.Create())
|
using(var tester = ServerTester.Create())
|
||||||
{
|
{
|
||||||
tester.Start();
|
tester.Start();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
@@ -116,7 +118,7 @@ namespace BTCPayServer.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void CanUseServerInitiatedPairingCode()
|
public void CanUseServerInitiatedPairingCode()
|
||||||
{
|
{
|
||||||
using (var tester = ServerTester.Create())
|
using(var tester = ServerTester.Create())
|
||||||
{
|
{
|
||||||
tester.Start();
|
tester.Start();
|
||||||
var acc = tester.NewAccount();
|
var acc = tester.NewAccount();
|
||||||
@@ -141,9 +143,9 @@ namespace BTCPayServer.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void CanSendIPN()
|
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();
|
tester.Start();
|
||||||
var acc = tester.NewAccount();
|
var acc = tester.NewAccount();
|
||||||
@@ -176,7 +178,7 @@ namespace BTCPayServer.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void CantPairTwiceWithSamePubkey()
|
public void CantPairTwiceWithSamePubkey()
|
||||||
{
|
{
|
||||||
using (var tester = ServerTester.Create())
|
using(var tester = ServerTester.Create())
|
||||||
{
|
{
|
||||||
tester.Start();
|
tester.Start();
|
||||||
var acc = tester.NewAccount();
|
var acc = tester.NewAccount();
|
||||||
@@ -195,7 +197,7 @@ namespace BTCPayServer.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||||
{
|
{
|
||||||
using (var tester = ServerTester.Create())
|
using(var tester = ServerTester.Create())
|
||||||
{
|
{
|
||||||
tester.Start();
|
tester.Start();
|
||||||
var user = tester.NewAccount();
|
var user = tester.NewAccount();
|
||||||
@@ -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();
|
||||||
@@ -361,14 +376,14 @@ namespace BTCPayServer.Tests
|
|||||||
private void Eventually(Action act)
|
private void Eventually(Action act)
|
||||||
{
|
{
|
||||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||||
while (true)
|
while(true)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
act();
|
act();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
catch(XunitException) when(!cts.Token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
cts.Token.WaitHandle.WaitOne(500);
|
cts.Token.WaitHandle.WaitOne(500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>());
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user