diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index 77c5a9be1..9feaab1f5 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -2,6 +2,7 @@ using BTCPayServer.Hosting; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; +using BTCPayServer.Rating; using BTCPayServer.Security; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; @@ -106,15 +107,6 @@ namespace BTCPayServer.Tests .UseConfiguration(conf) .ConfigureServices(s => { - 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) @@ -128,6 +120,30 @@ namespace BTCPayServer.Tests .Build(); _Host.Start(); InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository)); + + var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory)); + rateProvider.DirectProviders.Clear(); + + var coinAverageMock = new MockRateProvider(); + coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "coinaverage", + CurrencyPair = CurrencyPair.Parse("BTC_USD"), + Value = 5000m + }); + coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "coinaverage", + CurrencyPair = CurrencyPair.Parse("BTC_CAD"), + Value = 4500m + }); + coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate() + { + Exchange = "coinaverage", + CurrencyPair = CurrencyPair.Parse("LTC_USD"), + Value = 500m + }); + rateProvider.DirectProviders.Add("coinaverage", coinAverageMock); } public string HostName diff --git a/BTCPayServer.Tests/Mocks/MockRateProvider.cs b/BTCPayServer.Tests/Mocks/MockRateProvider.cs new file mode 100644 index 000000000..bd151c8ad --- /dev/null +++ b/BTCPayServer.Tests/Mocks/MockRateProvider.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Rating; +using BTCPayServer.Services.Rates; + +namespace BTCPayServer.Tests.Mocks +{ + public class MockRateProvider : IRateProvider + { + public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates(); + public Task GetRatesAsync() + { + return Task.FromResult(ExchangeRates); + } + } +} diff --git a/BTCPayServer.Tests/RateRulesTest.cs b/BTCPayServer.Tests/RateRulesTest.cs index 4236571d6..abd2c5201 100644 --- a/BTCPayServer.Tests/RateRulesTest.cs +++ b/BTCPayServer.Tests/RateRulesTest.cs @@ -41,7 +41,8 @@ namespace BTCPayServer.Tests { Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString()); } - Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * 2.32", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"), 2.32m).ToString()); + rules.GlobalMultiplier = 2.32m; + Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * 2.32", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString()); //////////////// // Check errors conditions @@ -107,7 +108,8 @@ namespace BTCPayServer.Tests builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5"); builder.AppendLine("DOGE_BTC = 2000"); Assert.True(RateRules.TryParse(builder.ToString(), out rules)); - rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"), 1.1m); + rules.GlobalMultiplier = 1.1m; + rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")); Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString()); rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m); Assert.True(rule2.Reevaluate()); diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index b068f0cb9..4c64aa2df 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -35,6 +35,7 @@ using BTCPayServer.Services.Apps; using BTCPayServer.Services.Stores; using System.Net.Http; using System.Text; +using BTCPayServer.Rating; namespace BTCPayServer.Tests { @@ -108,22 +109,11 @@ namespace BTCPayServer.Tests { var entity = new InvoiceEntity(); #pragma warning disable CS0618 - entity.TxFee = Money.Coins(0.1m); - entity.Rate = 5000; - entity.Payments = new System.Collections.Generic.List(); + entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) }); entity.ProductInformation = new ProductInformation() { Price = 5000 }; - // Some check that handling legacy stuff does not break things - var paymentMethod = entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike); - paymentMethod.Calculate(); - Assert.NotNull(paymentMethod); - Assert.Null(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike)); - entity.SetPaymentMethod(new PaymentMethod() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee }); - Assert.NotNull(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike)); - Assert.NotNull(entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike)); - //////////////////// - + var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike); var accounting = paymentMethod.Calculate(); Assert.Equal(Money.Coins(1.1m), accounting.Due); Assert.Equal(Money.Coins(1.1m), accounting.TotalDue); @@ -1128,8 +1118,6 @@ namespace BTCPayServer.Tests var txFee = Money.Zero; - var rate = user.BitPay.GetRates(); - var cashCow = tester.ExplorerNode; var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); @@ -1233,40 +1221,52 @@ namespace BTCPayServer.Tests [Fact] public void CheckQuadrigacxRateProvider() { - var quadri = new QuadrigacxRateProvider("BTC"); + var quadri = new QuadrigacxRateProvider(); var rates = quadri.GetRatesAsync().GetAwaiter().GetResult(); Assert.NotEmpty(rates); Assert.NotEqual(0.0m, rates.First().Value); - Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult()); - Assert.NotEqual(0.0m, quadri.GetRateAsync("USD").GetAwaiter().GetResult()); - Assert.Throws(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult()); - - quadri = new QuadrigacxRateProvider("LTC"); - rates = quadri.GetRatesAsync().GetAwaiter().GetResult(); - Assert.NotEmpty(rates); - Assert.NotEqual(0.0m, rates.First().Value); - Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult()); - Assert.Throws(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult()); - Assert.Throws(() => quadri.GetRateAsync("USD").GetAwaiter().GetResult()); + Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Value); + Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Value); + Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Value); + Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD"))); } [Fact] public void CheckRatesProvider() { - var coinAverage = new CoinAverageRateProvider("BTC"); - 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 provider = new BTCPayNetworkProvider(NetworkType.Mainnet); + var coinAverage = new CoinAverageRateProvider(provider); + var rates = coinAverage.GetRatesAsync().GetAwaiter().GetResult(); + Assert.NotNull(rates.GetRate("coinaverage", new CurrencyPair("BTC", "JPY"))); + var ratesBitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRatesAsync().GetAwaiter().GetResult(); + Assert.NotNull(ratesBitpay.GetRate("bitpay", new CurrencyPair("BTC", "JPY"))); - var cached = new CachedRateProvider("BTC", 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(); + RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules); + + var factory = new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, new CoinAverageSettings()); + factory.DirectProviders.Clear(); + factory.CacheSpan = TimeSpan.FromSeconds(10); + + var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + Assert.False(fetchedRate.Cached); + fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + Assert.True(fetchedRate.Cached); + + Thread.Sleep(11000); + fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + Assert.False(fetchedRate.Cached); + fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + Assert.True(fetchedRate.Cached); + // Should cache at exchange level so this should hit the cache + var fetchedRate2 = factory.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult(); + Assert.True(fetchedRate.Cached); + Assert.NotEqual(fetchedRate.Value.Value, fetchedRate2.Value.Value); + + // Should cache at exchange level this should not hit the cache as it is different exchange + RateRules.TryParse("X_X = bittrex(X_X);", out rateRules); + fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult(); + Assert.False(fetchedRate.Cached); - var bitstamp = new CoinAverageRateProvider("BTC") { Exchange = "bitstamp" }; - var bitstampRate = bitstamp.GetRateAsync("USD").GetAwaiter().GetResult(); - Assert.Throws(() => bitstamp.GetRateAsync("XXXXX").GetAwaiter().GetResult()); } private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx) diff --git a/BTCPayServer/BTCPayNetwork.cs b/BTCPayServer/BTCPayNetwork.cs index 540a090c8..bc0be4841 100644 --- a/BTCPayServer/BTCPayNetwork.cs +++ b/BTCPayServer/BTCPayNetwork.cs @@ -44,7 +44,6 @@ namespace BTCPayServer public string CryptoCode { get; internal set; } public string BlockExplorerLink { get; internal set; } public string UriScheme { get; internal set; } - public RateProviderDescription DefaultRateProvider { get; set; } [Obsolete("Should not be needed")] public bool IsBTC @@ -62,6 +61,7 @@ namespace BTCPayServer public BTCPayDefaultSettings DefaultSettings { get; set; } public KeyPath CoinType { get; internal set; } public int MaxTrackedConfirmation { get; internal set; } = 6; + public string[] DefaultRateRules { get; internal set; } = Array.Empty(); public override string ToString() { diff --git a/BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs b/BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs index ca803f0a4..911edff2c 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs +++ b/BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs @@ -14,9 +14,6 @@ namespace BTCPayServer public void InitBitcoin() { var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC"); - var coinaverage = new CoinAverageRateProviderDescription("BTC"); - var bitpay = new BitpayRateProviderDescription(); - var btcRate = new FallbackRateProviderDescription(new RateProviderDescription[] { coinaverage, bitpay }); Add(new BTCPayNetwork() { CryptoCode = nbxplorerNetwork.CryptoCode, @@ -24,7 +21,6 @@ namespace BTCPayServer NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, NBXplorerNetwork = nbxplorerNetwork, UriScheme = "bitcoin", - DefaultRateProvider = btcRate, CryptoImagePath = "imlegacy/bitcoin-symbol.svg", LightningImagePath = "imlegacy/btc-lightning.svg", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), diff --git a/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs b/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs index 18091ad91..9fbff33a9 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs +++ b/BTCPayServer/BTCPayNetworkProvider.Dogecoin.cs @@ -20,7 +20,7 @@ namespace BTCPayServer NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, NBXplorerNetwork = nbxplorerNetwork, UriScheme = "dogecoin", - DefaultRateProvider = new CoinAverageRateProviderDescription("DOGE"), + DefaultRateRules = new[] { "DOGE_X = bittrex(DOGE_BTC) * BTC_X" }, CryptoImagePath = "imlegacy/dogecoin.png", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'") diff --git a/BTCPayServer/BTCPayNetworkProvider.Litecoin.cs b/BTCPayServer/BTCPayNetworkProvider.Litecoin.cs index 98550f9e0..42b3de248 100644 --- a/BTCPayServer/BTCPayNetworkProvider.Litecoin.cs +++ b/BTCPayServer/BTCPayNetworkProvider.Litecoin.cs @@ -20,7 +20,6 @@ namespace BTCPayServer NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork, NBXplorerNetwork = nbxplorerNetwork, UriScheme = "litecoin", - DefaultRateProvider = new CoinAverageRateProviderDescription("LTC"), CryptoImagePath = "imlegacy/litecoin-symbol.svg", LightningImagePath = "imlegacy/ltc-lightning.svg", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 2b95e18c3..ab2c4430b 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -460,7 +460,7 @@ namespace BTCPayServer.Controllers StatusMessage = $"Invoice {result.Data.Id} just created!"; return RedirectToAction(nameof(ListInvoices)); } - catch (RateUnavailableException) + catch (BitpayHttpException) { ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency"); return View(model); diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 03a89cc84..96299651b 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -40,13 +40,14 @@ using NBXplorer.DerivationStrategy; using NBXplorer; using BTCPayServer.HostedServices; using BTCPayServer.Payments; +using BTCPayServer.Rating; namespace BTCPayServer.Controllers { public partial class InvoiceController : Controller { InvoiceRepository _InvoiceRepository; - IRateProviderFactory _RateProviders; + BTCPayRateProviderFactory _RateProvider; StoreRepository _StoreRepository; UserManager _UserManager; private CurrencyNameTable _CurrencyNameTable; @@ -59,7 +60,7 @@ namespace BTCPayServer.Controllers InvoiceRepository invoiceRepository, CurrencyNameTable currencyNameTable, UserManager userManager, - IRateProviderFactory rateProviders, + BTCPayRateProviderFactory rateProvider, StoreRepository storeRepository, EventAggregator eventAggregator, BTCPayWalletProvider walletProvider, @@ -69,7 +70,7 @@ namespace BTCPayServer.Controllers _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); _StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository)); _InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository)); - _RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders)); + _RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider)); _UserManager = userManager; _EventAggregator = eventAggregator; _NetworkProvider = networkProvider; @@ -111,6 +112,23 @@ namespace BTCPayServer.Controllers entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy); + HashSet currencyPairsToFetch = new HashSet(); + var rules = storeBlob.GetRateRules(_NetworkProvider); + + foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider) + .Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode)) + .Where(c => c != null)) + { + currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency)); + if (storeBlob.LightningMaxValue != null) + currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency)); + if (storeBlob.OnChainMinValue != null) + currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.OnChainMinValue.Currency)); + } + + var rateRules = storeBlob.GetRateRules(_NetworkProvider); + var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules); + var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider) .Select(c => (Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())), @@ -119,19 +137,45 @@ namespace BTCPayServer.Controllers .Where(c => c.Network != null) .Select(o => (SupportedPaymentMethod: o.SupportedPaymentMethod, - PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, store))) + PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store))) .ToList(); List paymentMethodErrors = new List(); List supported = new List(); var paymentMethods = new PaymentMethodDictionary(); + + foreach(var pair in fetchingByCurrencyPair) + { + var rateResult = await pair.Value; + bool hasError = false; + if(rateResult.Errors.Count != 0) + { + var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray()); + paymentMethodErrors.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})"); + hasError = true; + } + if(rateResult.ExchangeExceptions.Count != 0) + { + foreach(var ex in rateResult.ExchangeExceptions) + { + paymentMethodErrors.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})"); + } + hasError = true; + } + if(hasError) + { + paymentMethodErrors.Add($"{pair.Key}: The rule is {rateResult.Rule}"); + paymentMethodErrors.Add($"{pair.Key}: Evaluated rule is {rateResult.EvaluatedRule}"); + } + } + foreach (var o in supportedPaymentMethods) { try { var paymentMethod = await o.PaymentMethod; if (paymentMethod == null) - throw new PaymentMethodUnavailableException("Payment method unavailable (The handler returned null)"); + throw new PaymentMethodUnavailableException("Payment method unavailable"); supported.Add(o.SupportedPaymentMethod); paymentMethods.Add(paymentMethod); } @@ -158,23 +202,6 @@ namespace BTCPayServer.Controllers entity.SetSupportedPaymentMethods(supported); entity.SetPaymentMethods(paymentMethods); -#pragma warning disable CS0618 - // Legacy Bitpay clients expect information for BTC information, even if the store do not support it - var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain); - if (!legacyBTCisSet && _NetworkProvider.BTC != null) - { - var btc = _NetworkProvider.BTC; - var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc); - var rateProvider = _RateProviders.GetRateProvider(btc, storeBlob.GetRateRules()); - if (feeProvider != null && rateProvider != null) - { - var gettingFee = feeProvider.GetFeeRateAsync(); - var gettingRate = rateProvider.GetRateAsync(invoice.Currency); - entity.TxFee = GetTxFee(storeBlob, await gettingFee); - entity.Rate = await gettingRate; - } -#pragma warning restore CS0618 - } entity.PosData = invoice.PosData; entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider); @@ -183,15 +210,17 @@ namespace BTCPayServer.Controllers return new DataWrapper(resp) { Facade = "pos/invoice" }; } - private async Task CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store) + private async Task CreatePaymentMethodAsync(Dictionary> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store) { var storeBlob = store.GetStoreBlob(); - var rate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(entity.ProductInformation.Currency); + var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)]; + if (rate.Value == null) + return null; PaymentMethod paymentMethod = new PaymentMethod(); paymentMethod.ParentEntity = entity; paymentMethod.Network = network; paymentMethod.SetId(supportedPaymentMethod.PaymentId); - paymentMethod.Rate = rate; + paymentMethod.Rate = rate.Value.Value; var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network); if (storeBlob.NetworkFeeDisabled) paymentDetails.SetNoTxFee(); @@ -217,16 +246,14 @@ namespace BTCPayServer.Controllers if (compare != null) { - var limitValueRate = 0.0m; - if (limitValue.Currency == entity.ProductInformation.Currency) - limitValueRate = paymentMethod.Rate; - else - limitValueRate = await _RateProviders.GetRateProvider(network, storeBlob.GetRateRules()).GetRateAsync(limitValue.Currency); - - var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate); - if (compare(paymentMethod.Calculate().Due, limitValueCrypto)) + var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)]; + if (limitValueRate.Value.HasValue) { - throw new PaymentMethodUnavailableException(errorMessage); + var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value); + if (compare(paymentMethod.Calculate().Due, limitValueCrypto)) + { + throw new PaymentMethodUnavailableException(errorMessage); + } } } /////////////// @@ -243,13 +270,6 @@ namespace BTCPayServer.Controllers return paymentMethod; } -#pragma warning disable CS0618 - private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate) - { - return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100); - } -#pragma warning restore CS0618 - private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy) { if (transactionSpeed == null) diff --git a/BTCPayServer/Controllers/RateController.cs b/BTCPayServer/Controllers/RateController.cs index 63336ed0b..35a000497 100644 --- a/BTCPayServer/Controllers/RateController.cs +++ b/BTCPayServer/Controllers/RateController.cs @@ -8,17 +8,19 @@ using System.Threading.Tasks; using BTCPayServer.Filters; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; +using BTCPayServer.Rating; +using Newtonsoft.Json; namespace BTCPayServer.Controllers { public class RateController : Controller { - IRateProviderFactory _RateProviderFactory; + BTCPayRateProviderFactory _RateProviderFactory; BTCPayNetworkProvider _NetworkProvider; CurrencyNameTable _CurrencyNameTable; StoreRepository _StoreRepo; public RateController( - IRateProviderFactory rateProviderFactory, + BTCPayRateProviderFactory rateProviderFactory, BTCPayNetworkProvider networkProvider, StoreRepository storeRepo, CurrencyNameTable currencyNameTable) @@ -32,45 +34,90 @@ namespace BTCPayServer.Controllers [Route("rates")] [HttpGet] [BitpayAPIConstraint] - public async Task GetRates(string cryptoCode = null, string storeId = null) + public async Task GetRates(string currencyPairs, string storeId) { - var result = await GetRates2(cryptoCode, storeId); + var result = await GetRates2(currencyPairs, storeId); var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[]; - if(rates == null) + if (rates == null) return result; - return Json(new DataWrapper(rates)); + return Json(new DataWrapper(rates)); } [Route("api/rates")] [HttpGet] - public async Task GetRates2(string cryptoCode = null, string storeId = null) + public async Task GetRates2(string currencyPairs, string storeId) { - cryptoCode = cryptoCode ?? "BTC"; - var network = _NetworkProvider.GetNetwork(cryptoCode); - if (network == null) - return NotFound(); - - RateRules rules = null; - if (storeId != null) + if(storeId == null || currencyPairs == null) { - var store = await _StoreRepo.FindStore(storeId); - if (store == null) - return NotFound(); - rules = store.GetStoreBlob().GetRateRules(); + var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings) and currencyPairs (eg. BTC_USD,LTC_CAD)" }); + result.StatusCode = 400; + return result; } - var rateProvider = _RateProviderFactory.GetRateProvider(network, rules); - if (rateProvider == null) - return NotFound(); + + var store = await _StoreRepo.FindStore(storeId); + if (store == null) + { + var result = Json(new BitpayErrorsModel() { Error = "Store not found" }); + result.StatusCode = 404; + return result; + } + var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider); - var allRates = (await rateProvider.GetRatesAsync()); - return Json(allRates.Select(r => - new NBitpayClient.Rate() + HashSet pairs = new HashSet(); + foreach(var currency in currencyPairs.Split(',')) + { + if(!CurrencyPair.TryParse(currency, out var pair)) + { + var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" }); + result.StatusCode = 400; + return result; + } + pairs.Add(pair); + } + + var fetching = _RateProviderFactory.FetchRates(pairs, rules); + await Task.WhenAll(fetching.Select(f => f.Value).ToArray()); + return Json(pairs + .Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().Value)) + .Where(r => r.Value.HasValue) + .Select(r => + new Rate() { - Code = r.Currency, - Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name, - Value = r.Value + CryptoCode = r.Pair.Left, + Code = r.Pair.Right, + Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right)?.Name, + Value = r.Value.Value }).Where(n => n.Name != null).ToArray()); } + + public class Rate + { + + [JsonProperty(PropertyName = "name")] + public string Name + { + get; + set; + } + [JsonProperty(PropertyName = "cryptoCode")] + public string CryptoCode + { + get; + set; + } + [JsonProperty(PropertyName = "code")] + public string Code + { + get; + set; + } + [JsonProperty(PropertyName = "rate")] + public decimal Value + { + get; + set; + } + } } } diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 71bdb98d6..8190ae220 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -24,10 +24,10 @@ namespace BTCPayServer.Controllers { private UserManager _UserManager; SettingsRepository _SettingsRepository; - private IRateProviderFactory _RateProviderFactory; + private BTCPayRateProviderFactory _RateProviderFactory; public ServerController(UserManager userManager, - IRateProviderFactory rateProviderFactory, + BTCPayRateProviderFactory rateProviderFactory, SettingsRepository settingsRepository) { _UserManager = userManager; @@ -99,7 +99,7 @@ namespace BTCPayServer.Controllers }; if (!withAuth || settings.GetCoinAverageSignature() != null) { - return new CoinAverageRateProvider("BTC") + return new CoinAverageRateProvider() { Authenticator = settings }; } return null; diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index b2758e5fa..a8f3ae79d 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -276,7 +276,7 @@ namespace BTCPayServer.Controllers var storeBlob = store.GetStoreBlob(); var vm = new StoreViewModel(); - vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange); + vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName); vm.Id = store.Id; vm.StoreName = store.StoreName; vm.StoreWebsite = store.StoreWebsite; @@ -370,7 +370,7 @@ namespace BTCPayServer.Controllers needUpdate = true; } - if (!blob.PreferredExchange.IsCoinAverage() && newExchange) + if (newExchange) { if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase)) @@ -392,12 +392,12 @@ namespace BTCPayServer.Controllers }); } - private (String DisplayName, String Name)[] GetSupportedExchanges() + private CoinAverageExchange[] GetSupportedExchanges() { - return new[] { ("Coin Average", "coinaverage") } - .Concat(_CoinAverage.AvailableExchanges) - .OrderBy(s => s.Item1, StringComparer.OrdinalIgnoreCase) - .ToArray(); + return _CoinAverage.AvailableExchanges + .Select(c => c.Value) + .OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); } private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network) diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index 1446e5ad6..2d3c79042 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -18,6 +18,7 @@ using System.ComponentModel.DataAnnotations; using BTCPayServer.Services; using System.Security.Claims; using BTCPayServer.Security; +using BTCPayServer.Rating; namespace BTCPayServer.Data { @@ -207,7 +208,10 @@ namespace BTCPayServer.Data public StoreBlob GetStoreBlob() { - return StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject(Encoding.UTF8.GetString(StoreBlob)); + var result = StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject(Encoding.UTF8.GetString(StoreBlob)); + if (result.PreferredExchange == null) + result.PreferredExchange = CoinAverageRateProvider.CoinAverageName; + return result; } public bool SetStoreBlob(StoreBlob storeBlob) @@ -221,9 +225,9 @@ namespace BTCPayServer.Data } } - public class RateRule + public class RateRule_Obsolete { - public RateRule() + public RateRule_Obsolete() { RuleName = "Multiplier"; } @@ -275,8 +279,8 @@ namespace BTCPayServer.Data public void SetRateMultiplier(double rate) { - RateRules = new List(); - RateRules.Add(new RateRule() { Multiplier = rate }); + RateRules = new List(); + RateRules.Add(new RateRule_Obsolete() { Multiplier = rate }); } public decimal GetRateMultiplier() { @@ -290,7 +294,7 @@ namespace BTCPayServer.Data return rate; } - public List RateRules { get; set; } = new List(); + public List RateRules { get; set; } = new List(); public string PreferredExchange { get; set; } [JsonConverter(typeof(CurrencyValueJsonConverter))] @@ -303,6 +307,10 @@ namespace BTCPayServer.Data [JsonConverter(typeof(UriJsonConverter))] public Uri CustomCSS { get; set; } + public bool RateScripting { get; set; } + + public string RateScript { get; set; } + string _LightningDescriptionTemplate; public string LightningDescriptionTemplate @@ -317,12 +325,44 @@ namespace BTCPayServer.Data } } - public RateRules GetRateRules() + public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider) { - return new RateRules(RateRules) + if (!RateScripting || + string.IsNullOrEmpty(RateScript) || + !BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules)) { - PreferredExchange = PreferredExchange - }; + return GetDefaultRateRules(networkProvider); + } + else + { + rules.GlobalMultiplier = GetRateMultiplier(); + return rules; + } + } + + public RateRules GetDefaultRateRules(BTCPayNetworkProvider networkProvider) + { + StringBuilder builder = new StringBuilder(); + foreach (var network in networkProvider.GetAll()) + { + if (network.DefaultRateRules.Length != 0) + { + builder.AppendLine($"// Default rate rules for {network.CryptoCode}"); + foreach (var line in network.DefaultRateRules) + { + builder.AppendLine(line); + } + builder.AppendLine($"////////"); + builder.AppendLine(); + } + } + + var preferredExchange = string.IsNullOrEmpty(PreferredExchange) ? "coinaverage" : PreferredExchange; + builder.AppendLine($"X_X = {preferredExchange}(X_X);"); + + BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules); + rules.GlobalMultiplier = GetRateMultiplier(); + return rules; } } } diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 7c2bf6237..7bda961d9 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -104,12 +104,6 @@ namespace BTCPayServer return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite"; } - public static bool IsCoinAverage(this string exchangeName) - { - string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" }; - return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false; - } - public static async Task> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken)) { hashes = hashes.Distinct().ToArray(); diff --git a/BTCPayServer/HostedServices/RatesHostedService.cs b/BTCPayServer/HostedServices/RatesHostedService.cs index dcb0b404d..77d4dde18 100644 --- a/BTCPayServer/HostedServices/RatesHostedService.cs +++ b/BTCPayServer/HostedServices/RatesHostedService.cs @@ -17,15 +17,15 @@ namespace BTCPayServer.HostedServices public class RatesHostedService : BaseAsyncService { private SettingsRepository _SettingsRepository; - private IRateProviderFactory _RateProviderFactory; private CoinAverageSettings _coinAverageSettings; + BTCPayRateProviderFactory _RateProviderFactory; public RatesHostedService(SettingsRepository repo, - CoinAverageSettings coinAverageSettings, - IRateProviderFactory rateProviderFactory) + BTCPayRateProviderFactory rateProviderFactory, + CoinAverageSettings coinAverageSettings) { this._SettingsRepository = repo; - _RateProviderFactory = rateProviderFactory; _coinAverageSettings = coinAverageSettings; + _RateProviderFactory = rateProviderFactory; } internal override Task[] InitializeTasks() @@ -40,11 +40,15 @@ namespace BTCPayServer.HostedServices async Task RefreshCoinAverageSupportedExchanges() { await new SynchronizationContextRemover(); - var tickers = await new CoinAverageRateProvider("BTC") { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync(); - _coinAverageSettings.AvailableExchanges = tickers + var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync(); + var exchanges = new CoinAverageExchanges(); + foreach(var item in tickers .Exchanges - .Select(c => (c.DisplayName, c.Name)) - .ToArray(); + .Select(c => new CoinAverageExchange(c.Name, c.DisplayName))) + { + exchanges.Add(item); + } + _coinAverageSettings.AvailableExchanges = exchanges; await Task.Delay(TimeSpan.FromHours(5), Cancellation); } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 2c295ab28..aa3189932 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -68,7 +68,6 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); services.TryAddSingleton(o => { var opts = o.GetRequiredService(); @@ -129,7 +128,7 @@ namespace BTCPayServer.Hosting else return new Bitpay(new Key(), new Uri("https://test.bitpay.com/")); }); - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddScoped(); services.AddTransient(); diff --git a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs index 1ed1dce96..b100f33aa 100644 --- a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs @@ -1,5 +1,6 @@ using BTCPayServer.Services; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Rates; using BTCPayServer.Validations; using Microsoft.AspNetCore.Mvc.Rendering; using System; @@ -49,10 +50,10 @@ namespace BTCPayServer.Models.StoreViewModels public List DerivationSchemes { get; set; } = new List(); - public void SetExchangeRates((String DisplayName, String Name)[] supportedList, string preferredExchange) + public void SetExchangeRates(CoinAverageExchange[] supportedList, string preferredExchange) { - var defaultStore = preferredExchange ?? "coinaverage"; - var choices = supportedList.Select(o => new Format() { Name = o.DisplayName, Value = o.Name }).ToArray(); + var defaultStore = preferredExchange ?? CoinAverageRateProvider.CoinAverageName; + var choices = supportedList.Select(o => new Format() { Name = o.Display, Value = o.Name }).ToArray(); var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault(); Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen); PreferredExchange = chosen.Value; @@ -67,7 +68,7 @@ namespace BTCPayServer.Models.StoreViewModels { get { - return PreferredExchange.IsCoinAverage() ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}"; + return PreferredExchange == CoinAverageRateProvider.CoinAverageName ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}"; } } diff --git a/BTCPayServer/Rating/ExchangeRates.cs b/BTCPayServer/Rating/ExchangeRates.cs index 576201f0f..868c70a3b 100644 --- a/BTCPayServer/Rating/ExchangeRates.cs +++ b/BTCPayServer/Rating/ExchangeRates.cs @@ -9,6 +9,18 @@ namespace BTCPayServer.Rating { public class ExchangeRates : IEnumerable { + Dictionary _AllRates = new Dictionary(); + public ExchangeRates() + { + + } + public ExchangeRates(IEnumerable rates) + { + foreach (var rate in rates) + { + Add(rate); + } + } List _Rates = new List(); public MultiValueDictionary ByExchange { @@ -18,8 +30,22 @@ namespace BTCPayServer.Rating public void Add(ExchangeRate rate) { - _Rates.Add(rate); - ByExchange.Add(rate.Exchange, rate); + // 1 DOGE is always 1 DOGE + if (rate.CurrencyPair.Left == rate.CurrencyPair.Right) + return; + var key = $"({rate.Exchange}) {rate.CurrencyPair}"; + if (_AllRates.TryAdd(key, rate)) + { + _Rates.Add(rate); + ByExchange.Add(rate.Exchange, rate); + } + else + { + if (rate.Value.HasValue) + { + _AllRates[key].Value = rate.Value; + } + } } public IEnumerator GetEnumerator() @@ -34,7 +60,7 @@ namespace BTCPayServer.Rating public void SetRate(string exchangeName, CurrencyPair currencyPair, decimal value) { - if(ByExchange.TryGetValue(exchangeName, out var rates)) + if (ByExchange.TryGetValue(exchangeName, out var rates)) { var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair); if (rate != null) @@ -43,6 +69,8 @@ namespace BTCPayServer.Rating } public decimal? GetRate(string exchangeName, CurrencyPair currencyPair) { + if (currencyPair.Left == currencyPair.Right) + return 1.0m; if (ByExchange.TryGetValue(exchangeName, out var rates)) { var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair); diff --git a/BTCPayServer/Rating/RateRules.cs b/BTCPayServer/Rating/RateRules.cs index a5964328c..539e4c77a 100644 --- a/BTCPayServer/Rating/RateRules.cs +++ b/BTCPayServer/Rating/RateRules.cs @@ -93,6 +93,8 @@ namespace BTCPayServer.Rating SyntaxNode root; RuleList ruleList; + public decimal GlobalMultiplier { get; set; } = 1.0m; + RateRules(SyntaxNode root) { ruleList = new RuleList(); @@ -113,15 +115,15 @@ namespace BTCPayServer.Rating return true; } - public RateRule GetRuleFor(CurrencyPair currencyPair, decimal globalMultiplier = 1.0m) + public RateRule GetRuleFor(CurrencyPair currencyPair) { if (currencyPair.Left == "X" || currencyPair.Right == "X") throw new ArgumentException(paramName: nameof(currencyPair), message: "Invalid X currency"); var candidate = FindBestCandidate(currencyPair); - if (globalMultiplier != decimal.One) + if (GlobalMultiplier != decimal.One) { - candidate = CreateExpression($"({candidate}) * {globalMultiplier.ToString(CultureInfo.InvariantCulture)}"); + candidate = CreateExpression($"({candidate}) * {GlobalMultiplier.ToString(CultureInfo.InvariantCulture)}"); } return new RateRule(this, currencyPair, candidate); } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 9330a218a..5fd74741b 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -338,14 +338,14 @@ namespace BTCPayServer.Services.Invoices }; dto.CryptoInfo = new List(); - foreach (var info in this.GetPaymentMethods(networkProvider, true)) + foreach (var info in this.GetPaymentMethods(networkProvider)) { var accounting = info.Calculate(); var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo(); cryptoInfo.CryptoCode = info.GetId().CryptoCode; cryptoInfo.PaymentType = info.GetId().PaymentType.ToString(); cryptoInfo.Rate = info.Rate; - cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString(); + cryptoInfo.Price = (accounting.TotalDue - accounting.NetworkFee).ToString(); cryptoInfo.Due = accounting.Due.ToString(); cryptoInfo.Paid = accounting.Paid.ToString(); @@ -396,8 +396,7 @@ namespace BTCPayServer.Services.Invoices dto.PaymentUrls = cryptoInfo.PaymentUrls; } #pragma warning restore CS0618 - if (!info.IsPhantomBTC) - dto.CryptoInfo.Add(cryptoInfo); + dto.CryptoInfo.Add(cryptoInfo); } Populate(ProductInformation, dto); @@ -432,26 +431,15 @@ namespace BTCPayServer.Services.Invoices return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType), networkProvider); } - public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider, bool alwaysIncludeBTC = false) + public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider) { PaymentMethodDictionary rates = new PaymentMethodDictionary(networkProvider); var serializer = new Serializer(Dummy); - PaymentMethod phantom = null; #pragma warning disable CS0618 - // Legacy - if (alwaysIncludeBTC) - { - var btcNetwork = networkProvider?.GetNetwork("BTC"); - phantom = new PaymentMethod() { ParentEntity = this, IsPhantomBTC = true, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress, Network = btcNetwork }; - if (btcNetwork != null || networkProvider == null) - rates.Add(phantom); - } if (PaymentMethod != null) { foreach (var prop in PaymentMethod.Properties()) { - if (prop.Name == "BTC" && phantom != null) - rates.Remove(phantom); var r = serializer.ToObject(prop.Value.ToString()); var paymentMethodId = PaymentMethodId.Parse(prop.Name); r.CryptoCode = paymentMethodId.CryptoCode; @@ -635,20 +623,17 @@ namespace BTCPayServer.Services.Invoices [Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")] public string DepositAddress { get; set; } - [JsonIgnore] - public bool IsPhantomBTC { get; set; } - public PaymentMethodAccounting Calculate(Func paymentPredicate = null) { paymentPredicate = paymentPredicate ?? new Func((p) => true); - var paymentMethods = ParentEntity.GetPaymentMethods(null, IsPhantomBTC); + var paymentMethods = ParentEntity.GetPaymentMethods(null); var totalDue = ParentEntity.ProductInformation.Price / Rate; var paid = 0m; var cryptoPaid = 0.0m; int precision = 8; - var paidTxFee = 0m; + var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision)); bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision); int txRequired = 0; var payments = @@ -662,9 +647,8 @@ namespace BTCPayServer.Services.Invoices if (!paidEnough) { totalDue += txFee; - paidTxFee += txFee; } - paidEnough |= paid >= Extensions.RoundUp(totalDue, precision); + paidEnough |= Extensions.RoundUp(paid, precision) >= Extensions.RoundUp(totalDue, precision); if (GetId() == _.GetPaymentMethodId()) { cryptoPaid += _.GetCryptoPaymentData().GetValue(); @@ -680,16 +664,15 @@ namespace BTCPayServer.Services.Invoices { txRequired++; totalDue += GetTxFee(); - paidTxFee += GetTxFee(); } accounting.TotalDue = Money.Coins(Extensions.RoundUp(totalDue, precision)); - accounting.Paid = Money.Coins(paid); + accounting.Paid = Money.Coins(Extensions.RoundUp(paid, precision)); accounting.TxRequired = txRequired; - accounting.CryptoPaid = Money.Coins(cryptoPaid); + accounting.CryptoPaid = Money.Coins(Extensions.RoundUp(cryptoPaid, precision)); accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero); accounting.DueUncapped = accounting.TotalDue - accounting.Paid; - accounting.NetworkFee = Money.Coins(paidTxFee); + accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost; return accounting; } diff --git a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs index 1d71ec469..72ce27d20 100644 --- a/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs +++ b/BTCPayServer/Services/Rates/BTCPayRateProviderFactory.cs @@ -3,13 +3,35 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Rating; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; namespace BTCPayServer.Services.Rates { - public class BTCPayRateProviderFactory : IRateProviderFactory + public class ExchangeException { + public Exception Exception { get; set; } + public string ExchangeName { get; set; } + } + public class RateResult + { + public List ExchangeExceptions { get; set; } = new List(); + public string Rule { get; set; } + public string EvaluatedRule { get; set; } + public HashSet Errors { get; set; } + public decimal? Value { get; set; } + public bool Cached { get; internal set; } + } + + public class BTCPayRateProviderFactory + { + class QueryRateResult + { + public bool CachedResult { get; set; } + public List Exceptions { get; set; } + public ExchangeRates ExchangeRates { get; set; } + } IMemoryCache _Cache; private IOptions _CacheOptions; @@ -20,18 +42,41 @@ namespace BTCPayServer.Services.Rates return _Cache; } } - public BTCPayRateProviderFactory(IOptions cacheOptions, IServiceProvider serviceProvider) + CoinAverageSettings _CoinAverageSettings; + public BTCPayRateProviderFactory(IOptions cacheOptions, + BTCPayNetworkProvider btcpayNetworkProvider, + CoinAverageSettings coinAverageSettings) { if (cacheOptions == null) throw new ArgumentNullException(nameof(cacheOptions)); + _CoinAverageSettings = coinAverageSettings; _Cache = new MemoryCache(cacheOptions); _CacheOptions = cacheOptions; // We use 15 min because of limits with free version of bitcoinaverage CacheSpan = TimeSpan.FromMinutes(15.0); - this.serviceProvider = serviceProvider; + this.btcpayNetworkProvider = btcpayNetworkProvider; + InitExchanges(); } - IServiceProvider serviceProvider; + public bool UseCoinAverageAsFallback { get; set; } = true; + + private void InitExchanges() + { + DirectProviders.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider()); + } + + + private readonly Dictionary _DirectProviders = new Dictionary(); + public Dictionary DirectProviders + { + get + { + return _DirectProviders; + } + } + + + BTCPayNetworkProvider btcpayNetworkProvider; TimeSpan _CacheSpan; public TimeSpan CacheSpan { @@ -51,45 +96,87 @@ namespace BTCPayServer.Services.Rates _Cache = new MemoryCache(_CacheOptions); } - public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules) + public async Task FetchRate(CurrencyPair pair, RateRules rules) { - 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); + return await FetchRates(new HashSet(new[] { pair }), rules).First().Value; } - private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange) + public Dictionary> FetchRates(HashSet pairs, RateRules rules) + { + if (rules == null) + throw new ArgumentNullException(nameof(rules)); + + var fetchingRates = new Dictionary>(); + var fetchingExchanges = new Dictionary>(); + var consolidatedRates = new ExchangeRates(); + + foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p)))) + { + var dependentQueries = new List>(); + foreach (var requiredExchange in i.RateRule.ExchangeRates) + { + if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching)) + { + fetching = QueryRates(requiredExchange.Exchange); + fetchingExchanges.Add(requiredExchange.Exchange, fetching); + } + dependentQueries.Add(fetching); + } + fetchingRates.Add(i.Pair, GetRuleValue(dependentQueries, i.RateRule)); + } + return fetchingRates; + } + + private async Task GetRuleValue(List> dependentQueries, RateRule rateRule) + { + var result = new RateResult(); + result.Cached = true; + foreach (var queryAsync in dependentQueries) + { + var query = await queryAsync; + if (!query.CachedResult) + result.Cached = false; + result.ExchangeExceptions.AddRange(query.Exceptions); + foreach (var rule in query.ExchangeRates) + { + rateRule.ExchangeRates.Add(rule); + } + } + rateRule.Reevaluate(); + result.Value = rateRule.Value; + result.Errors = rateRule.Errors; + result.EvaluatedRule = rateRule.ToString(true); + result.Rule = rateRule.ToString(false); + return result; + } + + + private async Task QueryRates(string exchangeName) { List providers = new List(); - - if(exchange == "quadrigacx") + if (DirectProviders.TryGetValue(exchangeName, out var directProvider)) + providers.Add(directProvider); + if (_CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName)) { - providers.Add(new QuadrigacxRateProvider(network.CryptoCode)); + providers.Add(new CoinAverageRateProvider(btcpayNetworkProvider) + { + Exchange = exchangeName, + Authenticator = _CoinAverageSettings + }); } - - var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider); - coinAverage.Exchange = exchange; - providers.Add(coinAverage); - return new FallbackRateProvider(providers.ToArray()); - } - - 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) - { - if(network.DefaultRateProvider == null) + var fallback = new FallbackRateProvider(providers.ToArray()); + var cached = new CachedRateProvider(exchangeName, fallback, _Cache) { - throw new RateUnavailableException(network.CryptoCode); - } - return network.DefaultRateProvider.CreateRateProvider(serviceProvider); + CacheSpan = CacheSpan + }; + var value = await cached.GetRatesAsync(); + return new QueryRateResult() + { + CachedResult = !fallback.Used, + ExchangeRates = value, + Exceptions = fallback.Exceptions + .Select(c => new ExchangeException() { Exception = c, ExchangeName = exchangeName }).ToList() + }; } } } diff --git a/BTCPayServer/Services/Rates/BitpayRateProvider.cs b/BTCPayServer/Services/Rates/BitpayRateProvider.cs index edf44aab1..0898fd883 100644 --- a/BTCPayServer/Services/Rates/BitpayRateProvider.cs +++ b/BTCPayServer/Services/Rates/BitpayRateProvider.cs @@ -5,18 +5,13 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using NBitcoin; +using BTCPayServer.Rating; namespace BTCPayServer.Services.Rates { - public class BitpayRateProviderDescription : RateProviderDescription - { - public IRateProvider CreateRateProvider(IServiceProvider serviceProvider) - { - return new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))); - } - } public class BitpayRateProvider : IRateProvider { + public const string BitpayName = "bitpay"; Bitpay _Bitpay; public BitpayRateProvider(Bitpay bitpay) { @@ -24,21 +19,13 @@ namespace BTCPayServer.Services.Rates throw new ArgumentNullException(nameof(bitpay)); _Bitpay = bitpay; } - public async Task GetRateAsync(string currency) - { - var rates = await _Bitpay.GetRatesAsync().ConfigureAwait(false); - var rate = rates.GetRate(currency); - if (rate == 0m) - throw new RateUnavailableException(currency); - return (decimal)rate; - } - public async Task> GetRatesAsync() + public async Task GetRatesAsync() { - return (await _Bitpay.GetRatesAsync().ConfigureAwait(false)) + return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false)) .AllRates - .Select(r => new Rate() { Currency = r.Code, Value = r.Value }) - .ToList(); + .Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), Value = r.Value }) + .ToList()); } } } diff --git a/BTCPayServer/Services/Rates/CachedRateProvider.cs b/BTCPayServer/Services/Rates/CachedRateProvider.cs index 9f6016b6c..6f1e6bbe4 100644 --- a/BTCPayServer/Services/Rates/CachedRateProvider.cs +++ b/BTCPayServer/Services/Rates/CachedRateProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using BTCPayServer.Rating; using BTCPayServer.Services.Rates; using Microsoft.Extensions.Caching.Memory; @@ -10,9 +11,8 @@ namespace BTCPayServer.Services.Rates { private IRateProvider _Inner; private IMemoryCache _MemoryCache; - private string _CryptoCode; - public CachedRateProvider(string cryptoCode, IRateProvider inner, IMemoryCache memoryCache) + public CachedRateProvider(string exchangeName, IRateProvider inner, IMemoryCache memoryCache) { if (inner == null) throw new ArgumentNullException(nameof(inner)); @@ -20,7 +20,7 @@ namespace BTCPayServer.Services.Rates throw new ArgumentNullException(nameof(memoryCache)); this._Inner = inner; this.MemoryCache = memoryCache; - this._CryptoCode = cryptoCode; + this.ExchangeName = exchangeName; } public IRateProvider Inner @@ -31,31 +31,22 @@ namespace BTCPayServer.Services.Rates } } + public string ExchangeName { get; set; } + public TimeSpan CacheSpan { get; set; } = TimeSpan.FromMinutes(1.0); public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; } - - public Task GetRateAsync(string currency) - { - return MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) => - { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan; - return _Inner.GetRateAsync(currency); - }); - } - public Task> GetRatesAsync() + public Task GetRatesAsync() { - return MemoryCache.GetOrCreateAsync("GLOBAL_RATES_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) => + return MemoryCache.GetOrCreateAsync("EXCHANGE_RATES_" + ExchangeName, (ICacheEntry entry) => { entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan; return _Inner.GetRatesAsync(); }); } - - public string AdditionalScope { get; set; } } } diff --git a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs b/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs index 5abe33b48..fda213130 100644 --- a/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs +++ b/BTCPayServer/Services/Rates/CoinAverageRateProvider.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using System.ComponentModel; +using BTCPayServer.Rating; namespace BTCPayServer.Services.Rates { @@ -21,29 +22,6 @@ namespace BTCPayServer.Services.Rates } } - public class CoinAverageRateProviderDescription : RateProviderDescription - { - public CoinAverageRateProviderDescription(string crypto) - { - CryptoCode = crypto; - } - - public string CryptoCode { get; set; } - - 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 @@ -69,18 +47,25 @@ namespace BTCPayServer.Services.Rates public interface ICoinAverageAuthenticator { Task AddHeader(HttpRequestMessage message); - } + } public class CoinAverageRateProvider : IRateProvider { + public const string CoinAverageName = "coinaverage"; + BTCPayNetworkProvider _NetworkProvider; + public CoinAverageRateProvider() + { + + } + public CoinAverageRateProvider(BTCPayNetworkProvider networkProvider) + { + if (networkProvider == null) + throw new ArgumentNullException(nameof(networkProvider)); + _NetworkProvider = networkProvider; + } static HttpClient _Client = new HttpClient(); - public CoinAverageRateProvider(string cryptoCode) - { - CryptoCode = cryptoCode ?? "BTC"; - } - - public string Exchange { get; set; } + public string Exchange { get; set; } = CoinAverageName; public string CryptoCode { get; set; } @@ -88,27 +73,19 @@ namespace BTCPayServer.Services.Rates { get; set; } = "global"; - public async Task GetRateAsync(string currency) - { - var rates = await GetRatesCore(); - return GetRate(rates, currency); - } - - private decimal GetRate(Dictionary rates, string currency) - { - if (currency == "BTC") - return 1.0m; - if (rates.TryGetValue(currency, out decimal result)) - return result; - throw new RateUnavailableException(currency); - } public ICoinAverageAuthenticator Authenticator { get; set; } - private async Task> GetRatesCore() + private bool TryToDecimal(JProperty p, out decimal v) { - string url = Exchange == null ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short" - : $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}"; + JToken token = p.Value[Exchange == CoinAverageName ? "last" : "bid"]; + return decimal.TryParse(token.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v); + } + + public async Task GetRatesAsync() + { + string url = Exchange == CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short" + : $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}"; var request = new HttpRequestMessage(HttpMethod.Get, url); var auth = Authenticator; @@ -128,36 +105,34 @@ namespace BTCPayServer.Services.Rates throw new CoinAverageException("Unauthorized access to the API, premium plan needed"); resp.EnsureSuccessStatusCode(); var rates = JObject.Parse(await resp.Content.ReadAsStringAsync()); - if(Exchange != null) + if (Exchange != CoinAverageName) { rates = (JObject)rates["symbols"]; } - return rates.Properties() - .Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase) && TryToDecimal(p, out decimal unused)) - .ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p => - { - TryToDecimal(p, out decimal v); - return v; - }); + + var exchangeRates = new ExchangeRates(); + foreach (var prop in rates.Properties()) + { + ExchangeRate exchangeRate = new ExchangeRate(); + exchangeRate.Exchange = Exchange; + if (!TryToDecimal(prop, out decimal value)) + continue; + exchangeRate.Value = value; + for (int i = 3; i < 5; i++) + { + var potentialCryptoName = prop.Name.Substring(0, i); + if (_NetworkProvider.GetNetwork(potentialCryptoName) != null) + { + exchangeRate.CurrencyPair = new CurrencyPair(potentialCryptoName, prop.Name.Substring(i)); + } + } + if (exchangeRate.CurrencyPair != null) + exchangeRates.Add(exchangeRate); + } + return exchangeRates; } } - private bool TryToDecimal(JProperty p, out decimal v) - { - JToken token = p.Value[Exchange == null ? "last" : "bid"]; - return decimal.TryParse(token.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v); - } - - public async Task> GetRatesAsync() - { - var rates = await GetRatesCore(); - return rates.Select(o => new Rate() - { - Currency = o.Key, - Value = o.Value - }).ToList(); - } - public async Task TestAuthAsync() { var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/blockchain/tx_price/BTCUSD/8a3b4394ba811a9e2b0bbf3cc56888d053ea21909299b2703cdc35e156c860ff"); @@ -217,7 +192,7 @@ namespace BTCPayServer.Services.Rates var exchanges = (JObject)jobj["exchanges"]; response.Exchanges = exchanges .Properties() - .Select(p => + .Select(p => { var exchange = JsonConvert.DeserializeObject(p.Value.ToString()); exchange.Name = p.Name; diff --git a/BTCPayServer/Services/Rates/CoinAverageSettings.cs b/BTCPayServer/Services/Rates/CoinAverageSettings.cs index d76be9d64..aff18270b 100644 --- a/BTCPayServer/Services/Rates/CoinAverageSettings.cs +++ b/BTCPayServer/Services/Rates/CoinAverageSettings.cs @@ -20,12 +20,35 @@ namespace BTCPayServer.Services.Rates return _Settings.AddHeader(message); } } + + public class CoinAverageExchange + { + public CoinAverageExchange(string name, string display) + { + Name = name; + Display = display; + } + public string Name { get; set; } + public string Display { get; set; } + } + public class CoinAverageExchanges : Dictionary + { + public CoinAverageExchanges() + { + Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average")); + } + + public void Add(CoinAverageExchange exchange) + { + Add(exchange.Name, exchange); + } + } 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 DisplayName, String Name)[] AvailableExchanges { get; set; } = Array.Empty<(String DisplayName, String Name)>(); + public CoinAverageExchanges AvailableExchanges { get; set; } = new CoinAverageExchanges(); public CoinAverageSettings() { @@ -37,8 +60,9 @@ namespace BTCPayServer.Services.Rates // b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),"); //} //b.AppendLine("}.ToArray()"); - - AvailableExchanges = new[] { + AvailableExchanges = new CoinAverageExchanges(); + foreach(var item in + new[] { (DisplayName: "BitBargain", Name: "bitbargain"), (DisplayName: "Tidex", Name: "tidex"), (DisplayName: "LocalBitcoins", Name: "localbitcoins"), @@ -89,7 +113,10 @@ namespace BTCPayServer.Services.Rates (DisplayName: "Quoine", Name: "quoine"), (DisplayName: "BTC Markets", Name: "btcmarkets"), (DisplayName: "Bitso", Name: "bitso"), - }.ToArray(); + }) + { + AvailableExchanges.TryAdd(item.Name, new CoinAverageExchange(item.Name, item.DisplayName)); + } } public Task AddHeader(HttpRequestMessage message) diff --git a/BTCPayServer/Services/Rates/FallbackRateProvider.cs b/BTCPayServer/Services/Rates/FallbackRateProvider.cs index 18f31dfc0..2e618cb3a 100644 --- a/BTCPayServer/Services/Rates/FallbackRateProvider.cs +++ b/BTCPayServer/Services/Rates/FallbackRateProvider.cs @@ -2,58 +2,35 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Rating; namespace BTCPayServer.Services.Rates { - public class FallbackRateProviderDescription : RateProviderDescription - { - public FallbackRateProviderDescription(RateProviderDescription[] rateProviders) - { - RateProviders = rateProviders; - } - - public RateProviderDescription[] RateProviders { get; set; } - - public IRateProvider CreateRateProvider(IServiceProvider serviceProvider) - { - return new FallbackRateProvider(RateProviders.Select(r => r.CreateRateProvider(serviceProvider)).ToArray()); - } - } - public class FallbackRateProvider : IRateProvider { - IRateProvider[] _Providers; + public bool Used { get; set; } public FallbackRateProvider(IRateProvider[] providers) { if (providers == null) throw new ArgumentNullException(nameof(providers)); _Providers = providers; } - public async Task GetRateAsync(string currency) - { - foreach(var p in _Providers) - { - try - { - return await p.GetRateAsync(currency).ConfigureAwait(false); - } - catch { } - } - throw new RateUnavailableException(currency); - } - public async Task> GetRatesAsync() + public async Task GetRatesAsync() { + Used = true; foreach (var p in _Providers) { try { return await p.GetRatesAsync().ConfigureAwait(false); } - catch { } + catch(Exception ex) { Exceptions.Add(ex); } } - throw new RateUnavailableException("ALL"); + return new ExchangeRates(); } + + public List Exceptions { get; set; } = new List(); } } diff --git a/BTCPayServer/Services/Rates/IRateProvider.cs b/BTCPayServer/Services/Rates/IRateProvider.cs index 19a33ae45..00b26c5bd 100644 --- a/BTCPayServer/Services/Rates/IRateProvider.cs +++ b/BTCPayServer/Services/Rates/IRateProvider.cs @@ -2,32 +2,12 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; +using BTCPayServer.Rating; namespace BTCPayServer.Services.Rates { - public class Rate - { - public Rate() - { - - } - public Rate(string currency, decimal value) - { - Value = value; - Currency = currency; - } - public string Currency - { - get; set; - } - public decimal Value - { - get; set; - } - } public interface IRateProvider { - Task GetRateAsync(string currency); - Task> GetRatesAsync(); + Task GetRatesAsync(); } } diff --git a/BTCPayServer/Services/Rates/IRateProviderFactory.cs b/BTCPayServer/Services/Rates/IRateProviderFactory.cs deleted file mode 100644 index 5c3b76a77..000000000 --- a/BTCPayServer/Services/Rates/IRateProviderFactory.cs +++ /dev/null @@ -1,40 +0,0 @@ -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, RateRules rules); - TimeSpan CacheSpan { get; set; } - void InvalidateCache(); - } -} diff --git a/BTCPayServer/Services/Rates/MockRateProvider.cs b/BTCPayServer/Services/Rates/MockRateProvider.cs deleted file mode 100644 index 28d8298d1..000000000 --- a/BTCPayServer/Services/Rates/MockRateProvider.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Linq; -using System.Threading.Tasks; - -namespace BTCPayServer.Services.Rates -{ - public class MockRateProviderFactory : IRateProviderFactory - { - List _Mocks = new List(); - public MockRateProviderFactory() - { - - } - - public TimeSpan CacheSpan { get; set; } - - public void AddMock(MockRateProvider mock) - { - _Mocks.Add(mock); - } - public IRateProvider GetRateProvider(BTCPayNetwork network, RateRules rules) - { - return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode); - } - - public void InvalidateCache() - { - - } - } - public class MockRateProvider : IRateProvider - { - List _Rates; - - public string CryptoCode { get; } - - public MockRateProvider(string cryptoCode, params Rate[] rates) - { - _Rates = new List(rates); - CryptoCode = cryptoCode; - } - public MockRateProvider(string cryptoCode, List rates) - { - _Rates = rates; - CryptoCode = cryptoCode; - } - public Task GetRateAsync(string currency) - { - var rate = _Rates.FirstOrDefault(r => r.Currency.Equals(currency, StringComparison.OrdinalIgnoreCase)); - if (rate == null) - throw new RateUnavailableException(currency); - return Task.FromResult(rate.Value); - } - - public Task> GetRatesAsync() - { - ICollection rates = _Rates; - return Task.FromResult(rates); - } - } -} diff --git a/BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs b/BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs index 34ae2b12d..10fb75189 100644 --- a/BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs +++ b/BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs @@ -4,32 +4,15 @@ using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using BTCPayServer.Rating; using Newtonsoft.Json.Linq; namespace BTCPayServer.Services.Rates { public class QuadrigacxRateProvider : IRateProvider { - public QuadrigacxRateProvider(string crypto) - { - CryptoCode = crypto; - } - public string CryptoCode { get; set; } + public const string QuadrigacxName = "quadrigacx"; static HttpClient _Client = new HttpClient(); - public async Task GetRateAsync(string currency) - { - return await GetRatesAsyncCore(CryptoCode, currency); - } - - private async Task GetRatesAsyncCore(string cryptoCode, string currency) - { - var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book={cryptoCode.ToLowerInvariant()}_{currency.ToLowerInvariant()}"); - response.EnsureSuccessStatusCode(); - var rates = JObject.Parse(await response.Content.ReadAsStringAsync()); - if (!TryToDecimal(rates, out var result)) - throw new RateUnavailableException(currency); - return result; - } private bool TryToDecimal(JObject p, out decimal v) { @@ -40,26 +23,26 @@ namespace BTCPayServer.Services.Rates return decimal.TryParse(token.Value(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v); } - public async Task> GetRatesAsync() + public async Task GetRatesAsync() { var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all"); response.EnsureSuccessStatusCode(); var rates = JObject.Parse(await response.Content.ReadAsStringAsync()); - List result = new List(); + var exchangeRates = new ExchangeRates(); foreach (var prop in rates.Properties()) { - var rate = new Rate(); - var splitted = prop.Name.Split('_'); - var crypto = splitted[0].ToUpperInvariant(); - if (crypto != CryptoCode) + var rate = new ExchangeRate(); + if (!Rating.CurrencyPair.TryParse(prop.Name, out var pair)) + continue; + rate.CurrencyPair = pair; + rate.Exchange = QuadrigacxName; + if (!TryToDecimal((JObject)prop.Value, out var v)) continue; - rate.Currency = splitted[1].ToUpperInvariant(); - TryToDecimal((JObject)prop.Value, out var v); rate.Value = v; - result.Add(rate); + exchangeRates.Add(rate); } - return result; + return exchangeRates; } } } diff --git a/BTCPayServer/Services/Rates/RateProviderDescription.cs b/BTCPayServer/Services/Rates/RateProviderDescription.cs deleted file mode 100644 index bffac1b37..000000000 --- a/BTCPayServer/Services/Rates/RateProviderDescription.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace BTCPayServer.Services.Rates -{ - public interface RateProviderDescription - { - IRateProvider CreateRateProvider(IServiceProvider serviceProvider); - } -} diff --git a/BTCPayServer/Services/Rates/RateUnavailableException.cs b/BTCPayServer/Services/Rates/RateUnavailableException.cs deleted file mode 100644 index a21cbf71f..000000000 --- a/BTCPayServer/Services/Rates/RateUnavailableException.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace BTCPayServer.Services.Rates -{ - public class RateUnavailableException : Exception - { - public RateUnavailableException(string currency) : base("Rate unavailable for currency " + currency) - { - if (currency == null) - throw new ArgumentNullException(nameof(currency)); - Currency = currency; - } - - public string Currency - { - get; set; - } - } -} diff --git a/BTCPayServer/Services/Rates/TweakRateProvider.cs b/BTCPayServer/Services/Rates/TweakRateProvider.cs deleted file mode 100644 index dcca887cd..000000000 --- a/BTCPayServer/Services/Rates/TweakRateProvider.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using BTCPayServer.Data; - -namespace BTCPayServer.Services.Rates -{ - public class TweakRateProvider : IRateProvider - { - private BTCPayNetwork network; - private IRateProvider rateProvider; - private RateRules rateRules; - - public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, RateRules rateRules) - { - if (network == null) - throw new ArgumentNullException(nameof(network)); - if (rateProvider == null) - throw new ArgumentNullException(nameof(rateProvider)); - if (rateRules == null) - throw new ArgumentNullException(nameof(rateRules)); - this.network = network; - this.rateProvider = rateProvider; - this.rateRules = rateRules; - } - - public async Task GetRateAsync(string currency) - { - var rate = await rateProvider.GetRateAsync(currency); - foreach(var rule in rateRules) - { - rate = rule.Apply(network, rate); - } - return rate; - } - - public async Task> GetRatesAsync() - { - List rates = new List(); - foreach (var rate in await rateProvider.GetRatesAsync()) - { - var localRate = rate.Value; - foreach (var rule in rateRules) - { - localRate = rule.Apply(network, localRate); - } - rates.Add(new Rate(rate.Currency, localRate)); - } - return rates; - } - } -}