diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index fe28aff21..02fc15516 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -868,6 +868,49 @@ namespace BTCPayServer.Tests Assert.Equal($"{tpub}-[p2sh]", result.ToString()); } + [Fact] + public void CanSetPaymentMethodLimits() + { + using (var tester = ServerTester.Create()) + { + tester.Start(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC"); + user.RegisterLightningNode("BTC", LightningConnectionType.Charge); + var vm = Assert.IsType(Assert.IsType(user.GetController().CheckoutExperience(user.StoreId).Result).Model); + vm.LightningMaxValue = "2 USD"; + vm.OnChainMinValue = "5 USD"; + Assert.IsType(user.GetController().CheckoutExperience(user.StoreId, vm).Result); + + var invoice = user.BitPay.CreateInvoice(new Invoice() + { + Price = 1.5, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + Assert.Single(invoice.CryptoInfo); + Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType); + + invoice = user.BitPay.CreateInvoice(new Invoice() + { + Price = 5.5, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + Assert.Single(invoice.CryptoInfo); + Assert.Equal(PaymentTypes.BTCLike.ToString(), invoice.CryptoInfo[0].PaymentType); + } + } + [Fact] public void CanUsePoSApp() { diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index 9bcb1b1c2..c590f16f8 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -149,7 +149,7 @@ namespace BTCPayServer.Controllers { StringBuilder errors = new StringBuilder(); errors.AppendLine("No payment method available for this store"); - foreach(var error in paymentMethodErrors) + foreach (var error in paymentMethodErrors) { errors.AppendLine(error); } @@ -196,21 +196,36 @@ namespace BTCPayServer.Controllers paymentDetails.SetNoTxFee(); paymentMethod.SetPaymentMethodDetails(paymentDetails); - // Check if Lightning Max value is exceeded + Func compare = null; + CurrencyValue limitValue = null; + string errorMessage = null; if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike && storeBlob.LightningMaxValue != null) { - var lightningMaxValue = storeBlob.LightningMaxValue; - var lightningMaxValueRate = 0.0m; - if (lightningMaxValue.Currency == entity.ProductInformation.Currency) - lightningMaxValueRate = paymentMethod.Rate; - else - lightningMaxValueRate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(lightningMaxValue.Currency); + compare = (a, b) => a > b; + limitValue = storeBlob.LightningMaxValue; + errorMessage = "The amount of the invoice is too high to be paid with lightning"; + } + else if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.BTCLike && + storeBlob.OnChainMinValue != null) + { + compare = (a, b) => a < b; + limitValue = storeBlob.OnChainMinValue; + errorMessage = "The amount of the invoice is too low to be paid on chain"; + } - var lightningMaxValueCrypto = Money.Coins(lightningMaxValue.Value / lightningMaxValueRate); - if (paymentMethod.Calculate().Due > lightningMaxValueCrypto) + if (compare != null) + { + var limitValueRate = 0.0m; + if (limitValue.Currency == entity.ProductInformation.Currency) + limitValueRate = paymentMethod.Rate; + else + limitValueRate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(limitValue.Currency); + + var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate); + if (compare(paymentMethod.Calculate().Due, limitValueCrypto)) { - throw new PaymentMethodUnavailableException("Lightning max value exceeded"); + throw new PaymentMethodUnavailableException(errorMessage); } } /////////////// diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index f63e3c7e7..6bc7df0e3 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -195,6 +195,7 @@ namespace BTCPayServer.Controllers vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto()); vm.SetLanguages(_LangService, storeBlob.DefaultLang); vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? ""; + vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? ""; vm.AllowCoinConversion = storeBlob.AllowCoinConversion; vm.CustomCSS = storeBlob.CustomCSS; vm.CustomLogo = storeBlob.CustomLogo; @@ -205,14 +206,24 @@ namespace BTCPayServer.Controllers [Route("{storeId}/checkout")] public async Task CheckoutExperience(string storeId, CheckoutExperienceViewModel model) { - CurrencyValue currencyValue = null; + CurrencyValue lightningMaxValue = null; if (!string.IsNullOrWhiteSpace(model.LightningMaxValue)) { - if (!CurrencyValue.TryParse(model.LightningMaxValue, out currencyValue)) + if (!CurrencyValue.TryParse(model.LightningMaxValue, out lightningMaxValue)) { - ModelState.AddModelError(nameof(model.LightningMaxValue), "Invalid currency value"); + ModelState.AddModelError(nameof(model.LightningMaxValue), "Invalid lightning max value"); } } + + CurrencyValue onchainMinValue = null; + if (!string.IsNullOrWhiteSpace(model.OnChainMinValue)) + { + if (!CurrencyValue.TryParse(model.OnChainMinValue, out onchainMinValue)) + { + ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value"); + } + } + var store = await _Repo.FindStore(storeId, GetUserId()); if (store == null) return NotFound(); @@ -227,7 +238,8 @@ namespace BTCPayServer.Controllers model.SetLanguages(_LangService, model.DefaultLang); blob.DefaultLang = model.DefaultLang; blob.AllowCoinConversion = model.AllowCoinConversion; - blob.LightningMaxValue = currencyValue; + blob.LightningMaxValue = lightningMaxValue; + blob.OnChainMinValue = onchainMinValue; blob.CustomLogo = model.CustomLogo; blob.CustomCSS = model.CustomCSS; if (store.SetStoreBlob(blob)) diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index 780593d79..e94887fda 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -262,6 +262,8 @@ namespace BTCPayServer.Data [JsonConverter(typeof(CurrencyValueJsonConverter))] public CurrencyValue LightningMaxValue { get; set; } + [JsonConverter(typeof(CurrencyValueJsonConverter))] + public CurrencyValue OnChainMinValue { get; set; } [JsonConverter(typeof(UriJsonConverter))] public Uri CustomLogo { get; set; } diff --git a/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs b/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs index 6d21bc7fd..d12df9101 100644 --- a/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs @@ -31,12 +31,17 @@ namespace BTCPayServer.Models.StoreViewModels [MaxLength(20)] public string LightningMaxValue { get; set; } + [Display(Name = "Do not propose on chain payment if the value of the invoice is below...")] + [MaxLength(20)] + public string OnChainMinValue { get; set; } + [Display(Name = "Link to a custom CSS stylesheet")] [Url] public Uri CustomCSS { get; set; } [Display(Name = "Link to a custom logo")] [Url] public Uri CustomLogo { get; set; } + public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto) { var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray(); diff --git a/BTCPayServer/Views/Stores/CheckoutExperience.cshtml b/BTCPayServer/Views/Stores/CheckoutExperience.cshtml index b3c54f6be..8899f4d97 100644 --- a/BTCPayServer/Views/Stores/CheckoutExperience.cshtml +++ b/BTCPayServer/Views/Stores/CheckoutExperience.cshtml @@ -44,6 +44,12 @@

Example: 5.50 USD

+
+ + + +

Example: 5.50 USD

+