diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 7571bf6f4..6c1bfa5cf 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -632,7 +632,7 @@ namespace BTCPayServer.Tests var bitflyer2 = CreateInvoice(tester, user, "bitflyer"); Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache rates.Add(bitflyer); - + foreach(var rate in rates) { Assert.Single(rates.Where(r => r == rate)); diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index de4bf4b49..a08f7f6b9 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -23,7 +23,6 @@ namespace BTCPayServer.Controllers { private UserManager _UserManager; SettingsRepository _SettingsRepository; - private IRateProviderFactory _RateProviderFactory; public ServerController(UserManager userManager, IRateProviderFactory rateProviderFactory, @@ -31,7 +30,6 @@ namespace BTCPayServer.Controllers { _UserManager = userManager; _SettingsRepository = settingsRepository; - _RateProviderFactory = rateProviderFactory; } [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")] [HttpPost] public async Task Rates(RatesViewModel vm) @@ -73,10 +55,14 @@ namespace BTCPayServer.Controllers rates.CacheInMinutes = vm.CacheMinutes; try { - if (rates.GetCoinAverageSignature() != null) + var settings = new CoinAverageSettings() + { + KeyPair = (vm.PublicKey, vm.PrivateKey) + }; + if (settings.GetCoinAverageSignature() != null) { await new CoinAverageRateProvider("BTC") - { Authenticator = new TestCoinAverageAuthenticator(rates) }.TestAuthAsync(); + { Authenticator = settings }.TestAuthAsync(); } } catch @@ -86,7 +72,6 @@ namespace BTCPayServer.Controllers if (!ModelState.IsValid) return View(vm); await _SettingsRepository.UpdateSetting(rates); - ((BTCPayRateProviderFactory)_RateProviderFactory).CacheSpan = TimeSpan.FromMinutes(vm.CacheMinutes); StatusMessage = "Rate settings successfully updated"; return RedirectToAction(nameof(Rates)); } diff --git a/BTCPayServer/HostedServices/RatesHostedService.cs b/BTCPayServer/HostedServices/RatesHostedService.cs index 18b0b3a6b..373e6287d 100644 --- a/BTCPayServer/HostedServices/RatesHostedService.cs +++ b/BTCPayServer/HostedServices/RatesHostedService.cs @@ -8,6 +8,7 @@ using BTCPayServer.Services; using BTCPayServer.Services.Rates; using Microsoft.Extensions.Hosting; using BTCPayServer.Logging; +using System.Runtime.CompilerServices; namespace BTCPayServer.HostedServices { @@ -15,41 +16,78 @@ namespace BTCPayServer.HostedServices { private SettingsRepository _SettingsRepository; private IRateProviderFactory _RateProviderFactory; - public RatesHostedService(SettingsRepository repo, + private CoinAverageSettings _coinAverageSettings; + public RatesHostedService(SettingsRepository repo, + CoinAverageSettings coinAverageSettings, IRateProviderFactory rateProviderFactory) { this._SettingsRepository = repo; _RateProviderFactory = rateProviderFactory; + _coinAverageSettings = coinAverageSettings; } + + + CancellationTokenSource _Cts = new CancellationTokenSource(); + + List _Tasks = new List(); + public Task StartAsync(CancellationToken cancellationToken) { - Init(); + _Tasks.Add(RefreshCoinAverageSupportedExchanges(_Cts.Token)); + _Tasks.Add(RefreshCoinAverageSettings(_Cts.Token)); return Task.CompletedTask; } - async void Init() - { - var rates = (await _SettingsRepository.GetSettingAsync()) ?? new RatesSetting(); - _RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes); - //string[] availableExchanges = null; - //// So we don't run this in testing - //if(_RateProviderFactory is BTCPayRateProviderFactory) - //{ - // try - // { - // await new CoinAverageRateProvider("BTC").GetExchangeTickersAsync(); - // } - // catch(Exception ex) - // { - // Logs.PayServer.LogWarning(ex, "Failed to get exchange tickers"); - // } - //} + async Task Timer(Func 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()) ?? new RatesSetting(); + _RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes); + if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey)) + { + _coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey); + } + await _SettingsRepository.WaitSettingsChanged(cancellation); + }, cancellation); } public Task StopAsync(CancellationToken cancellationToken) { - return Task.CompletedTask; + _Cts.Cancel(); + return Task.WhenAll(_Tasks.ToArray()); } } } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index d98db23a2..76dc65f92 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -109,7 +109,8 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(o => { var opts = o.GetRequiredService(); diff --git a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs b/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs index 7d98f25a9..7cdbb29aa 100644 --- a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs +++ b/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs @@ -59,41 +59,11 @@ namespace BTCPayServer.Services.Rates 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 PrivateKey { get; set; } [DefaultValue(15)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] 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()) ?? new RatesSetting(); - var signature = settings.GetCoinAverageSignature(); - if (signature != null) - { - message.Headers.Add("X-signature", signature); - } - } } public interface ICoinAverageAuthenticator diff --git a/BTCPayServer/Services/Rates/CoinAverageSettings.cs b/BTCPayServer/Services/Rates/CoinAverageSettings.cs new file mode 100644 index 000000000..ff69aa38f --- /dev/null +++ b/BTCPayServer/Services/Rates/CoinAverageSettings.cs @@ -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(); + + 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; + } + } +} diff --git a/BTCPayServer/Services/SettingsRepository.cs b/BTCPayServer/Services/SettingsRepository.cs index 51e80bfa7..a6911abc2 100644 --- a/BTCPayServer/Services/SettingsRepository.cs +++ b/BTCPayServer/Services/SettingsRepository.cs @@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore; using BTCPayServer.Models; using Microsoft.EntityFrameworkCore.Infrastructure.Internal; using Newtonsoft.Json; +using System.Threading; namespace BTCPayServer.Services { @@ -51,6 +52,22 @@ namespace BTCPayServer.Services await ctx.SaveChangesAsync(); } } + + IReadOnlyCollection> 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(string value) @@ -62,5 +79,35 @@ namespace BTCPayServer.Services { return JsonConvert.SerializeObject(obj); } + + MultiValueDictionary> _Subscriptions = new MultiValueDictionary>(); + public async Task WaitSettingsChanged(CancellationToken cancellation) + { + var tcs = new TaskCompletionSource(); + 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; + } + } } } diff --git a/BTCPayServer/Views/Server/Rates.cshtml b/BTCPayServer/Views/Server/Rates.cshtml index 15b2a5522..a8dfc17f7 100644 --- a/BTCPayServer/Views/Server/Rates.cshtml +++ b/BTCPayServer/Views/Server/Rates.cshtml @@ -29,7 +29,7 @@ -

You can find the information on bitcoinaverage api key page

+

You can find the information on bitcoinaverage api key page