Refactoring: Do not query database when asking for Coinaverage rates, periodically get exchange list

This commit is contained in:
nicolas.dorier
2018-04-18 16:07:16 +09:00
parent 84cd9e570f
commit 5cb8cdd511
8 changed files with 157 additions and 74 deletions

View File

@@ -23,7 +23,6 @@ namespace BTCPayServer.Controllers
{ {
private UserManager<ApplicationUser> _UserManager; private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository; SettingsRepository _SettingsRepository;
private IRateProviderFactory _RateProviderFactory;
public ServerController(UserManager<ApplicationUser> userManager, public ServerController(UserManager<ApplicationUser> userManager,
IRateProviderFactory rateProviderFactory, IRateProviderFactory rateProviderFactory,
@@ -31,7 +30,6 @@ namespace BTCPayServer.Controllers
{ {
_UserManager = userManager; _UserManager = userManager;
_SettingsRepository = settingsRepository; _SettingsRepository = settingsRepository;
_RateProviderFactory = rateProviderFactory;
} }
[Route("server/rates")] [Route("server/rates")]
@@ -47,22 +45,6 @@ namespace BTCPayServer.Controllers
} }
class TestCoinAverageAuthenticator : ICoinAverageAuthenticator
{
private RatesSetting settings;
public TestCoinAverageAuthenticator(RatesSetting settings)
{
this.settings = settings;
}
public Task AddHeader(HttpRequestMessage message)
{
var sig = settings.GetCoinAverageSignature();
if (sig != null)
message.Headers.Add("X-signature", settings.GetCoinAverageSignature());
return Task.CompletedTask;
}
}
[Route("server/rates")] [Route("server/rates")]
[HttpPost] [HttpPost]
public async Task<IActionResult> Rates(RatesViewModel vm) public async Task<IActionResult> Rates(RatesViewModel vm)
@@ -73,10 +55,14 @@ namespace BTCPayServer.Controllers
rates.CacheInMinutes = vm.CacheMinutes; rates.CacheInMinutes = vm.CacheMinutes;
try try
{ {
if (rates.GetCoinAverageSignature() != null) var settings = new CoinAverageSettings()
{
KeyPair = (vm.PublicKey, vm.PrivateKey)
};
if (settings.GetCoinAverageSignature() != null)
{ {
await new CoinAverageRateProvider("BTC") await new CoinAverageRateProvider("BTC")
{ Authenticator = new TestCoinAverageAuthenticator(rates) }.TestAuthAsync(); { Authenticator = settings }.TestAuthAsync();
} }
} }
catch catch
@@ -86,7 +72,6 @@ namespace BTCPayServer.Controllers
if (!ModelState.IsValid) if (!ModelState.IsValid)
return View(vm); return View(vm);
await _SettingsRepository.UpdateSetting(rates); await _SettingsRepository.UpdateSetting(rates);
((BTCPayRateProviderFactory)_RateProviderFactory).CacheSpan = TimeSpan.FromMinutes(vm.CacheMinutes);
StatusMessage = "Rate settings successfully updated"; StatusMessage = "Rate settings successfully updated";
return RedirectToAction(nameof(Rates)); return RedirectToAction(nameof(Rates));
} }

View File

@@ -8,6 +8,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using System.Runtime.CompilerServices;
namespace BTCPayServer.HostedServices namespace BTCPayServer.HostedServices
{ {
@@ -15,41 +16,78 @@ namespace BTCPayServer.HostedServices
{ {
private SettingsRepository _SettingsRepository; private SettingsRepository _SettingsRepository;
private IRateProviderFactory _RateProviderFactory; private IRateProviderFactory _RateProviderFactory;
private CoinAverageSettings _coinAverageSettings;
public RatesHostedService(SettingsRepository repo, public RatesHostedService(SettingsRepository repo,
CoinAverageSettings coinAverageSettings,
IRateProviderFactory rateProviderFactory) IRateProviderFactory rateProviderFactory)
{ {
this._SettingsRepository = repo; this._SettingsRepository = repo;
_RateProviderFactory = rateProviderFactory; _RateProviderFactory = rateProviderFactory;
_coinAverageSettings = coinAverageSettings;
} }
CancellationTokenSource _Cts = new CancellationTokenSource();
List<Task> _Tasks = new List<Task>();
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
Init(); _Tasks.Add(RefreshCoinAverageSupportedExchanges(_Cts.Token));
_Tasks.Add(RefreshCoinAverageSettings(_Cts.Token));
return Task.CompletedTask; return Task.CompletedTask;
} }
async void Init()
async Task Timer(Func<Task> act, CancellationToken cancellation, [CallerMemberName]string caller = null)
{
while (!cancellation.IsCancellationRequested)
{
try
{
await act();
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
{
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, caller + " failed");
try
{
await Task.Delay(TimeSpan.FromMinutes(1), cancellation);
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested) { }
}
}
}
Task RefreshCoinAverageSupportedExchanges(CancellationToken cancellation)
{
return Timer(async () =>
{
var tickers = await new CoinAverageRateProvider("BTC").GetExchangeTickersAsync();
await Task.Delay(TimeSpan.FromHours(5), cancellation);
}, cancellation);
}
Task RefreshCoinAverageSettings(CancellationToken cancellation)
{
return Timer(async () =>
{ {
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting(); var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes); _RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
//string[] availableExchanges = null; {
//// So we don't run this in testing _coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
//if(_RateProviderFactory is BTCPayRateProviderFactory) }
//{ await _SettingsRepository.WaitSettingsChanged<RatesSetting>(cancellation);
// try }, cancellation);
// {
// await new CoinAverageRateProvider("BTC").GetExchangeTickersAsync();
// }
// catch(Exception ex)
// {
// Logs.PayServer.LogWarning(ex, "Failed to get exchange tickers");
// }
//}
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
return Task.CompletedTask; _Cts.Cancel();
return Task.WhenAll(_Tasks.ToArray());
} }
} }
} }

View File

@@ -109,7 +109,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<BTCPayServerEnvironment>(); services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>(); services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton<EventAggregator>(); services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<ICoinAverageAuthenticator, BTCPayCoinAverageAuthenticator>(); services.TryAddSingleton<CoinAverageSettings>();
services.TryAddSingleton<ICoinAverageAuthenticator, CoinAverageSettings>();
services.TryAddSingleton<ApplicationDbContextFactory>(o => services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{ {
var opts = o.GetRequiredService<BTCPayServerOptions>(); var opts = o.GetRequiredService<BTCPayServerOptions>();

View File

@@ -59,41 +59,11 @@ namespace BTCPayServer.Services.Rates
public class RatesSetting public class RatesSetting
{ {
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public string PublicKey { get; set; } public string PublicKey { get; set; }
public string PrivateKey { get; set; } public string PrivateKey { get; set; }
[DefaultValue(15)] [DefaultValue(15)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int CacheInMinutes { get; set; } = 15; public int CacheInMinutes { get; set; } = 15;
public string GetCoinAverageSignature()
{
if (string.IsNullOrEmpty(PublicKey) || string.IsNullOrEmpty(PrivateKey))
return null;
var timestamp = (int)((DateTime.UtcNow - _epochUtc).TotalSeconds);
var payload = timestamp + "." + PublicKey;
var digestValueBytes = new HMACSHA256(Encoding.ASCII.GetBytes(PrivateKey)).ComputeHash(Encoding.ASCII.GetBytes(payload));
var digestValueHex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(digestValueBytes);
return payload + "." + digestValueHex;
}
}
public class BTCPayCoinAverageAuthenticator : ICoinAverageAuthenticator
{
private SettingsRepository settingsRepo;
public BTCPayCoinAverageAuthenticator(SettingsRepository settingsRepo)
{
this.settingsRepo = settingsRepo;
}
public async Task AddHeader(HttpRequestMessage message)
{
var settings = (await settingsRepo.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
var signature = settings.GetCoinAverageSignature();
if (signature != null)
{
message.Headers.Add("X-signature", signature);
}
}
} }
public interface ICoinAverageAuthenticator public interface ICoinAverageAuthenticator

View File

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
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 string[] AvailableExchanges { get; set; } = Array.Empty<string>();
public Task AddHeader(HttpRequestMessage message)
{
var signature = GetCoinAverageSignature();
if (signature != null)
{
message.Headers.Add("X-signature", signature);
}
return Task.CompletedTask;
}
public string GetCoinAverageSignature()
{
var keyPair = KeyPair;
if (!keyPair.HasValue)
return null;
if (string.IsNullOrEmpty(keyPair.Value.PublicKey) || string.IsNullOrEmpty(keyPair.Value.PrivateKey))
return null;
var timestamp = (int)((DateTime.UtcNow - _epochUtc).TotalSeconds);
var payload = timestamp + "." + keyPair.Value.PublicKey;
var digestValueBytes = new HMACSHA256(Encoding.ASCII.GetBytes(keyPair.Value.PrivateKey)).ComputeHash(Encoding.ASCII.GetBytes(payload));
var digestValueHex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(digestValueBytes);
return payload + "." + digestValueHex;
}
}
}

View File

@@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore;
using BTCPayServer.Models; using BTCPayServer.Models;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Threading;
namespace BTCPayServer.Services namespace BTCPayServer.Services
{ {
@@ -51,6 +52,22 @@ namespace BTCPayServer.Services
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
} }
} }
IReadOnlyCollection<TaskCompletionSource<bool>> value;
lock (_Subscriptions)
{
if(_Subscriptions.TryGetValue(typeof(T), out value))
{
_Subscriptions.Remove(typeof(T));
}
}
if(value != null)
{
foreach(var v in value)
{
v.TrySetResult(true);
}
}
} }
private T Deserialize<T>(string value) private T Deserialize<T>(string value)
@@ -62,5 +79,35 @@ namespace BTCPayServer.Services
{ {
return JsonConvert.SerializeObject(obj); return JsonConvert.SerializeObject(obj);
} }
MultiValueDictionary<Type, TaskCompletionSource<bool>> _Subscriptions = new MultiValueDictionary<Type, TaskCompletionSource<bool>>();
public async Task WaitSettingsChanged<T>(CancellationToken cancellation)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellation.Register(() =>
{
try
{
tcs.TrySetCanceled();
}
catch { }
}))
{
lock (_Subscriptions)
{
_Subscriptions.Add(typeof(T), tcs);
}
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
tcs.Task.ContinueWith(_ =>
{
lock (_Subscriptions)
{
_Subscriptions.Remove(typeof(T), tcs);
}
}, TaskScheduler.Default);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
await tcs.Task;
}
}
} }
} }

View File

@@ -29,7 +29,7 @@
<label class="sr-only" asp-for="PrivateKey"></label> <label class="sr-only" asp-for="PrivateKey"></label>
<input asp-for="PrivateKey" style="width:50%;" class="form-control" placeholder="Private key" /> <input asp-for="PrivateKey" style="width:50%;" class="form-control" placeholder="Private key" />
<span asp-validation-for="PrivateKey" class="text-danger"></span> <span asp-validation-for="PrivateKey" class="text-danger"></span>
<p class="form-text text-muted">You can find the information on <a href="https://bitcoinaverage.com/en/apikeys">bitcoinaverage api key page</a></p> <p class="form-text text-muted">You can find the information on <a target="_blank" href="https://bitcoinaverage.com/en/apikeys">bitcoinaverage api key page</a></p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label asp-for="CacheMinutes"></label> <label asp-for="CacheMinutes"></label>