From 0723eec50886d8898eac3c15bb016f06d90ff4f5 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Sun, 15 Apr 2018 21:18:51 +0900 Subject: [PATCH] Fix rate handling --- BTCPayServer.Tests/BTCPayServerTester.cs | 22 ++++++---- BTCPayServer.Tests/ServerTester.cs | 19 ++++----- BTCPayServer.Tests/UnitTest1.cs | 39 ++++++++++++++++++ BTCPayServer/BTCPayServer.csproj | 2 +- BTCPayServer/Controllers/InvoiceController.cs | 6 +-- BTCPayServer/Controllers/RateController.cs | 10 +++-- BTCPayServer/Data/StoreData.cs | 26 ++---------- .../HostedServices/RatesHostedService.cs | 33 ++++++++++++--- .../Rates/BTCPayRateProviderFactory.cs | 23 ++++++++++- .../Services/Rates/CoinAverageRateProvider.cs | 41 ++++++++++++++++++- .../Services/Rates/IRateProviderFactory.cs | 30 +++++++++++++- .../Services/Rates/MockRateProvider.cs | 9 +++- .../Services/Rates/TweakRateProvider.cs | 4 +- 13 files changed, 203 insertions(+), 61 deletions(-) diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index b42107839..ccd7aec85 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -47,7 +47,7 @@ namespace BTCPayServer.Tests } public Uri LTCNBXplorerUri { get; set; } - + public Uri ServerUri { get; @@ -65,6 +65,9 @@ namespace BTCPayServer.Tests get; set; } + + public bool MockRates { get; set; } = true; + public void Start() { if (!Directory.Exists(_Directory)) @@ -101,12 +104,15 @@ namespace BTCPayServer.Tests .UseConfiguration(conf) .ConfigureServices(s => { - var mockRates = new MockRateProviderFactory(); - var btc = new MockRateProvider("BTC", new Rate("USD", 5000m), new Rate("CAD", 4500m)); - var ltc = new MockRateProvider("LTC", new Rate("USD", 500m)); - mockRates.AddMock(btc); - mockRates.AddMock(ltc); - s.AddSingleton(mockRates); + if (MockRates) + { + var mockRates = new MockRateProviderFactory(); + var btc = new MockRateProvider("BTC", new Rate("USD", 5000m), new Rate("CAD", 4500m)); + var ltc = new MockRateProvider("LTC", new Rate("USD", 500m)); + mockRates.AddMock(btc); + mockRates.AddMock(ltc); + s.AddSingleton(mockRates); + } s.AddLogging(l => { l.SetMinimumLevel(LogLevel.Information) @@ -121,7 +127,7 @@ namespace BTCPayServer.Tests _Host.Start(); InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository)); } - + public string HostName { get; diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 73fc2e425..2e3c4f651 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -34,21 +34,11 @@ namespace BTCPayServer.Tests public ServerTester(string scope) { _Directory = scope; - } - - public bool Dockerized - { - get; set; - } - - public void Start() - { if (Directory.Exists(_Directory)) Utils.DeleteDirectory(_Directory); if (!Directory.Exists(_Directory)) Directory.CreateDirectory(_Directory); - NetworkProvider = new BTCPayNetworkProvider(ChainType.Regtest); ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork); LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork); @@ -72,6 +62,15 @@ namespace BTCPayServer.Tests PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture); PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1"); PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false")); + } + + public bool Dockerized + { + get; set; + } + + public void Start() + { PayTester.Start(); } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 6825ff335..044a67ab5 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -616,12 +616,51 @@ namespace BTCPayServer.Tests } } + [Fact] + public void CanUseExchangeSpecificRate() + { + using (var tester = ServerTester.Create()) + { + tester.PayTester.MockRates = false; + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + List rates = new List(); + rates.Add(CreateInvoice(tester, user, "coinaverage")); + rates.Add(CreateInvoice(tester, user, "bitflyer")); + + foreach(var rate in rates) + { + Assert.Single(rates.Where(r => r == rate)); + } + } + } + + private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange) + { + var storeController = tester.PayTester.GetController(user.UserId); + var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model; + vm.PreferredExchange = exchange; + storeController.UpdateStore(user.StoreId, vm).Wait(); + var invoice2 = user.BitPay.CreateInvoice(new Invoice() + { + Price = 5000.0, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + return invoice2.CryptoInfo[0].Rate; + } [Fact] public void CanTweakRate() { using (var tester = ServerTester.Create()) { + tester.PayTester.MockRates = false; tester.Start(); var user = tester.NewAccount(); user.GrantAccess(); diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 5670ea00f..2cc18d247 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.0 - 1.0.1.82 + 1.0.1.83 NU1701,CA1816,CA1308,CA1810,CA2208 diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index af3496b9e..03a89cc84 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -165,7 +165,7 @@ namespace BTCPayServer.Controllers { var btc = _NetworkProvider.BTC; var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc); - var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc)); + var rateProvider = _RateProviders.GetRateProvider(btc, storeBlob.GetRateRules()); if (feeProvider != null && rateProvider != null) { var gettingFee = feeProvider.GetFeeRateAsync(); @@ -186,7 +186,7 @@ namespace BTCPayServer.Controllers private async Task CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store) { var storeBlob = store.GetStoreBlob(); - var rate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network)).GetRateAsync(entity.ProductInformation.Currency); + var rate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(entity.ProductInformation.Currency); PaymentMethod paymentMethod = new PaymentMethod(); paymentMethod.ParentEntity = entity; paymentMethod.Network = network; @@ -221,7 +221,7 @@ namespace BTCPayServer.Controllers if (limitValue.Currency == entity.ProductInformation.Currency) limitValueRate = paymentMethod.Rate; else - limitValueRate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network)).GetRateAsync(limitValue.Currency); + limitValueRate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(limitValue.Currency); var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate); if (compare(paymentMethod.Calculate().Due, limitValueCrypto)) diff --git a/BTCPayServer/Controllers/RateController.cs b/BTCPayServer/Controllers/RateController.cs index 3a2170ce6..63336ed0b 100644 --- a/BTCPayServer/Controllers/RateController.cs +++ b/BTCPayServer/Controllers/RateController.cs @@ -49,18 +49,20 @@ namespace BTCPayServer.Controllers var network = _NetworkProvider.GetNetwork(cryptoCode); if (network == null) return NotFound(); - var rateProvider = _RateProviderFactory.GetRateProvider(network); - if (rateProvider == null) - return NotFound(); + RateRules rules = null; if (storeId != null) { var store = await _StoreRepo.FindStore(storeId); if (store == null) return NotFound(); - rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider); + rules = store.GetStoreBlob().GetRateRules(); } + var rateProvider = _RateProviderFactory.GetRateProvider(network, rules); + if (rateProvider == null) + return NotFound(); + var allRates = (await rateProvider.GetRatesAsync()); return Json(allRates.Select(r => new NBitpayClient.Rate() diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index d953fabf1..6ac02a0d1 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -284,30 +284,12 @@ namespace BTCPayServer.Data } } - public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider) + public RateRules GetRateRules() { - if (!PreferredExchange.IsCoinAverage()) + return new RateRules(RateRules) { - // If the original rateProvider is a cache, use the same inner provider as fallback, and same memory cache to wrap it all - if (rateProvider is CachedRateProvider cachedRateProvider) - { - rateProvider = new FallbackRateProvider(new IRateProvider[] { - new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange }, - cachedRateProvider.Inner - }); - rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange }; - } - else - { - rateProvider = new FallbackRateProvider(new IRateProvider[] { - new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange }, - rateProvider - }); - } - } - if (RateRules == null || RateRules.Count == 0) - return rateProvider; - return new TweakRateProvider(network, rateProvider, RateRules.ToList()); + PreferredExchange = PreferredExchange + }; } } } diff --git a/BTCPayServer/HostedServices/RatesHostedService.cs b/BTCPayServer/HostedServices/RatesHostedService.cs index 781b87818..18b0b3a6b 100644 --- a/BTCPayServer/HostedServices/RatesHostedService.cs +++ b/BTCPayServer/HostedServices/RatesHostedService.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -6,24 +7,44 @@ using System.Threading.Tasks; using BTCPayServer.Services; using BTCPayServer.Services.Rates; using Microsoft.Extensions.Hosting; +using BTCPayServer.Logging; namespace BTCPayServer.HostedServices { public class RatesHostedService : IHostedService { private SettingsRepository _SettingsRepository; - private BTCPayRateProviderFactory _RateProviderFactory; - public RatesHostedService(SettingsRepository repo, IRateProviderFactory rateProviderFactory) + private IRateProviderFactory _RateProviderFactory; + public RatesHostedService(SettingsRepository repo, + IRateProviderFactory rateProviderFactory) { this._SettingsRepository = repo; - _RateProviderFactory = rateProviderFactory as BTCPayRateProviderFactory; + _RateProviderFactory = rateProviderFactory; } - public async Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken) + { + Init(); + return Task.CompletedTask; + } + + async void Init() { - if (_RateProviderFactory == null) - return; 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"); + // } + //} } public Task StopAsync(CancellationToken cancellationToken) diff --git a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs index d911f13b8..f62aa1166 100644 --- a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs +++ b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs @@ -51,9 +51,28 @@ namespace BTCPayServer.Services.Rates _Cache = new MemoryCache(_CacheOptions); } - public IRateProvider GetRateProvider(BTCPayNetwork network) + public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules) { - return new CachedRateProvider(network.CryptoCode, GetDefaultRateProvider(network), _Cache) { CacheSpan = CacheSpan }; + rules = rules ?? new RateRules(); + var rateProvider = GetDefaultRateProvider(network); + if (!rules.PreferredExchange.IsCoinAverage()) + { + rateProvider = CreateExchangeRateProvider(network, rules.PreferredExchange); + } + rateProvider = CreateCachedRateProvider(network, rateProvider, rules.PreferredExchange); + return new TweakRateProvider(network, rateProvider, rules); + } + + private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange) + { + var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider); + coinAverage.Exchange = exchange; + return coinAverage; + } + + private CachedRateProvider CreateCachedRateProvider(BTCPayNetwork network, IRateProvider rateProvider, string additionalScope) + { + return new CachedRateProvider(network.CryptoCode, rateProvider, _Cache) { CacheSpan = CacheSpan, AdditionalScope = additionalScope }; } private IRateProvider GetDefaultRateProvider(BTCPayNetwork network) diff --git a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs b/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs index e26356e6c..7d98f25a9 100644 --- a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs +++ b/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs @@ -30,13 +30,31 @@ namespace BTCPayServer.Services.Rates public string CryptoCode { get; set; } - public IRateProvider CreateRateProvider(IServiceProvider serviceProvider) + public CoinAverageRateProvider CreateRateProvider(IServiceProvider serviceProvider) { return new CoinAverageRateProvider(CryptoCode) { Authenticator = serviceProvider.GetService() }; } + + IRateProvider RateProviderDescription.CreateRateProvider(IServiceProvider serviceProvider) + { + return CreateRateProvider(serviceProvider); + } + } + + public class GetExchangeTickersResponse + { + public class Exchange + { + public string Name { get; set; } + [JsonProperty("display_name")] + public string DisplayName { get; set; } + public string[] Symbols { get; set; } + } + public bool Success { get; set; } + public Exchange[] Exchanges { get; set; } } public class RatesSetting @@ -181,5 +199,26 @@ namespace BTCPayServer.Services.Rates var resp = await _Client.SendAsync(request); resp.EnsureSuccessStatusCode(); } + + public async Task GetExchangeTickersAsync() + { + var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/symbols/exchanges/ticker"); + var resp = await _Client.SendAsync(request); + resp.EnsureSuccessStatusCode(); + var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync()); + var response = new GetExchangeTickersResponse(); + response.Success = jobj["success"].Value(); + var exchanges = (JObject)jobj["exchanges"]; + response.Exchanges = exchanges + .Properties() + .Select(p => + { + var exchange = JsonConvert.DeserializeObject(p.Value.ToString()); + exchange.Name = p.Name; + return exchange; + }) + .ToArray(); + return response; + } } } diff --git a/BTCPayServer/Services/Rates/IRateProviderFactory.cs b/BTCPayServer/Services/Rates/IRateProviderFactory.cs index 55a364668..5c3b76a77 100644 --- a/BTCPayServer/Services/Rates/IRateProviderFactory.cs +++ b/BTCPayServer/Services/Rates/IRateProviderFactory.cs @@ -1,12 +1,40 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Data; namespace BTCPayServer.Services.Rates { + public class RateRules : IEnumerable + { + private List rateRules; + + public RateRules() + { + rateRules = new List(); + } + public RateRules(List rateRules) + { + this.rateRules = rateRules?.ToList() ?? new List(); + } + public string PreferredExchange { get; set; } + + public IEnumerator GetEnumerator() + { + return rateRules.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } public interface IRateProviderFactory { - IRateProvider GetRateProvider(BTCPayNetwork network); + IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules); + TimeSpan CacheSpan { get; set; } + void InvalidateCache(); } } diff --git a/BTCPayServer/Services/Rates/MockRateProvider.cs b/BTCPayServer/Services/Rates/MockRateProvider.cs index c735c980a..28d8298d1 100644 --- a/BTCPayServer/Services/Rates/MockRateProvider.cs +++ b/BTCPayServer/Services/Rates/MockRateProvider.cs @@ -14,14 +14,21 @@ namespace BTCPayServer.Services.Rates } + public TimeSpan CacheSpan { get; set; } + public void AddMock(MockRateProvider mock) { _Mocks.Add(mock); } - public IRateProvider GetRateProvider(BTCPayNetwork network) + public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules) { return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode); } + + public void InvalidateCache() + { + + } } public class MockRateProvider : IRateProvider { diff --git a/BTCPayServer/Services/Rates/TweakRateProvider.cs b/BTCPayServer/Services/Rates/TweakRateProvider.cs index 292f9787f..dcca887cd 100644 --- a/BTCPayServer/Services/Rates/TweakRateProvider.cs +++ b/BTCPayServer/Services/Rates/TweakRateProvider.cs @@ -10,9 +10,9 @@ namespace BTCPayServer.Services.Rates { private BTCPayNetwork network; private IRateProvider rateProvider; - private List rateRules; + private RateRules rateRules; - public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, List rateRules) + public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, RateRules rateRules) { if (network == null) throw new ArgumentNullException(nameof(network));