diff --git a/BTCPayServer.Client/Models/StoreBaseData.cs b/BTCPayServer.Client/Models/StoreBaseData.cs index 61e8ed747..824756053 100644 --- a/BTCPayServer.Client/Models/StoreBaseData.cs +++ b/BTCPayServer.Client/Models/StoreBaseData.cs @@ -30,14 +30,21 @@ namespace BTCPayServer.Client.Models public double PaymentTolerance { get; set; } = 0; public bool AnyoneCanCreateInvoice { get; set; } + + public bool RequiresRefundEmail { get; set; } + public bool LightningAmountInSatoshi { get; set; } + public bool LightningPrivateRouteHints { get; set; } + public bool OnChainWithLnInvoiceFallback { get; set; } + public bool RedirectAutomatically { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public bool ShowRecommendedFee { get; set; } = true; [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int RecommendedFeeBlockTarget { get; set; } = 1; + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string DefaultLang { get; set; } = "en"; - public bool LightningAmountInSatoshi { get; set; } public string CustomLogo { get; set; } @@ -45,16 +52,13 @@ namespace BTCPayServer.Client.Models public string HtmlTitle { get; set; } - public bool RedirectAutomatically { get; set; } - public bool RequiresRefundEmail { get; set; } [JsonConverter(typeof(StringEnumConverter))] [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never; public bool PayJoinEnabled { get; set; } - public bool LightningPrivateRouteHints { get; set; } [JsonExtensionData] diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index ad2f59a1c..a362fff11 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -770,30 +770,31 @@ namespace BTCPayServer.Tests BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.00005m)); }); await tester.ExplorerNode.GenerateAsync(1); + await Task.Delay(100); // wait a bit for payment to process before fetching new invoice var newInvoice = await user.BitPay.GetInvoiceAsync(invoice.Id); - var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; - var oldBolt11= invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; - Assert.NotEqual(newBolt11,oldBolt11); + var newBolt11 = newInvoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; + var oldBolt11 = invoice.CryptoInfo.First(o => o.PaymentUrls.BOLT11 != null).PaymentUrls.BOLT11; + Assert.NotEqual(newBolt11, oldBolt11); Assert.Equal(newInvoice.BtcDue.GetValue(), BOLT11PaymentRequest.Parse(newBolt11, Network.RegTest).MinimumAmount.ToDecimal(LightMoneyUnit.BTC)); - - Logs.Tester.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue()} via lightning" ); + + Logs.Tester.LogInformation($"Paying invoice {newInvoice.Id} remaining due amount {newInvoice.BtcDue.GetValue()} via lightning"); var evt = await tester.WaitForEvent(async () => { await tester.SendLightningPaymentAsync(newInvoice); }, evt => evt.InvoiceId == invoice.Id); var fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId); - Assert.Contains(fetchedInvoice.Status, new []{InvoiceStatus.Complete, InvoiceStatus.Confirmed}); + Assert.Contains(fetchedInvoice.Status, new[] { InvoiceStatus.Complete, InvoiceStatus.Confirmed }); Assert.Equal(InvoiceExceptionStatus.None, fetchedInvoice.ExceptionStatus); - - Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice " ); + + Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice "); evt = await tester.WaitForEvent(async () => { await tester.SendLightningPaymentAsync(invoice); }, evt => evt.InvoiceId == invoice.Id); Assert.Equal(evt.InvoiceId, invoice.Id); fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId); - Assert.Equal( 3,fetchedInvoice.Payments.Count); + Assert.Equal(3, fetchedInvoice.Payments.Count); } [Fact(Timeout = 60 * 2 * 1000)] @@ -1508,7 +1509,7 @@ namespace BTCPayServer.Tests ); } } - + // [Fact(Timeout = TestTimeout)] [Fact()] [Trait("Integration", "Integration")] @@ -1527,9 +1528,9 @@ namespace BTCPayServer.Tests BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest), Money.Coins(0.01m)); }); - - + + var payments = Assert.IsType( Assert.IsType(await user.GetController().Invoice(invoice.Id)).Model) .Payments; @@ -1990,7 +1991,60 @@ namespace BTCPayServer.Tests }; var criteriaCompat = store.GetPaymentMethodCriteria(tester.NetworkProvider, blob); Assert.Single(criteriaCompat); - Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", BitcoinPaymentType.Instance) )); + Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", BitcoinPaymentType.Instance))); + } + } + + [Fact] + [Trait("Integration", "Integration")] + public async Task CanSetUnifiedQrCode() + { + using (var tester = ServerTester.Create()) + { + tester.ActivateLightning(); + await tester.StartAsync(); + await tester.EnsureChannelsSetup(); + var user = tester.NewAccount(); + user.GrantAccess(); + user.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit); + user.RegisterLightningNode("BTC", LightningConnectionType.CLightning); + + var invoice = user.BitPay.CreateInvoice( + new Invoice() + { + Price = 5.5m, + Currency = "USD", + PosData = "posData", + OrderId = "orderId", + ItemDesc = "Some description", + FullNotifications = true + }, Facade.Merchant); + + // validate that invoice data model doesn't have lightning string initially + var res = await user.GetController().Checkout(invoice.Id); + var paymentMethodFirst = Assert.IsType( + Assert.IsType(res).Model + ); + Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR); + + // enable unified QR code in settings + var vm = Assert.IsType(Assert + .IsType(user.GetController().CheckoutExperience()).Model + ); + vm.OnChainWithLnInvoiceFallback = true; + Assert.IsType( + user.GetController().CheckoutExperience(vm).Result + ); + + // validate that QR code now has both onchain and offchain payment urls + res = await user.GetController().Checkout(invoice.Id); + var paymentMethodSecond = Assert.IsType( + Assert.IsType(res).Model + ); + Assert.Contains("&lightning=", paymentMethodSecond.InvoiceBitcoinUrlQR); + Assert.StartsWith("BITCOIN:", paymentMethodSecond.InvoiceBitcoinUrlQR); + var split = paymentMethodSecond.InvoiceBitcoinUrlQR.Split('?')[0]; + Assert.True($"BITCOIN:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split); } } @@ -2030,7 +2084,7 @@ namespace BTCPayServer.Tests Assert.Single(invoice.CryptoInfo); Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType); - + //test backward compat var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId); var blob = store.GetStoreBlob(); @@ -2044,8 +2098,8 @@ namespace BTCPayServer.Tests }; var criteriaCompat = store.GetPaymentMethodCriteria(tester.NetworkProvider, blob); Assert.Single(criteriaCompat); - Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && !methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", LightningPaymentType.Instance) )); - + Assert.NotNull(criteriaCompat.FirstOrDefault(methodCriteria => methodCriteria.Value.ToString() == "2 USD" && !methodCriteria.Above && methodCriteria.PaymentMethod == new PaymentMethodId("BTC", LightningPaymentType.Instance))); + } } diff --git a/BTCPayServer/Controllers/GreenField/StoresController.cs b/BTCPayServer/Controllers/GreenField/StoresController.cs index d113d58b2..5d87f2c0f 100644 --- a/BTCPayServer/Controllers/GreenField/StoresController.cs +++ b/BTCPayServer/Controllers/GreenField/StoresController.cs @@ -120,21 +120,22 @@ namespace BTCPayServer.Controllers.GreenField //we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572) NetworkFeeMode = storeBlob.NetworkFeeMode, RequiresRefundEmail = storeBlob.RequiresRefundEmail, + LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi, + LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints, + OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback, + RedirectAutomatically = storeBlob.RedirectAutomatically, ShowRecommendedFee = storeBlob.ShowRecommendedFee, RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget, DefaultLang = storeBlob.DefaultLang, MonitoringExpiration = storeBlob.MonitoringExpiration, InvoiceExpiration = storeBlob.InvoiceExpiration, - LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi, CustomLogo = storeBlob.CustomLogo, CustomCSS = storeBlob.CustomCSS, HtmlTitle = storeBlob.HtmlTitle, AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice, LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate, PaymentTolerance = storeBlob.PaymentTolerance, - RedirectAutomatically = storeBlob.RedirectAutomatically, - PayJoinEnabled = storeBlob.PayJoinEnabled, - LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints + PayJoinEnabled = storeBlob.PayJoinEnabled }; } @@ -155,21 +156,22 @@ namespace BTCPayServer.Controllers.GreenField //we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572) blob.NetworkFeeMode = restModel.NetworkFeeMode; blob.RequiresRefundEmail = restModel.RequiresRefundEmail; + blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi; + blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints; + blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback; + blob.RedirectAutomatically = restModel.RedirectAutomatically; blob.ShowRecommendedFee = restModel.ShowRecommendedFee; blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget; blob.DefaultLang = restModel.DefaultLang; blob.MonitoringExpiration = restModel.MonitoringExpiration; blob.InvoiceExpiration = restModel.InvoiceExpiration; - blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi; blob.CustomLogo = restModel.CustomLogo; blob.CustomCSS = restModel.CustomCSS; blob.HtmlTitle = restModel.HtmlTitle; blob.AnyoneCanInvoice = restModel.AnyoneCanCreateInvoice; blob.LightningDescriptionTemplate = restModel.LightningDescriptionTemplate; blob.PaymentTolerance = restModel.PaymentTolerance; - blob.RedirectAutomatically = restModel.RedirectAutomatically; blob.PayJoinEnabled = restModel.PayJoinEnabled; - blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints; model.SetStoreBlob(blob); } diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index e5b2f2ccc..bce95b717 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -215,7 +215,7 @@ namespace BTCPayServer.Controllers if (paymentMethodCriteria.Value != null) { currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, paymentMethodCriteria.Value.Currency)); - } + } } } @@ -305,7 +305,9 @@ namespace BTCPayServer.Controllers }).ToArray()); } - private async Task CreatePaymentMethodAsync(Dictionary> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetworkBase network, InvoiceEntity entity, StoreData store, InvoiceLogs logs) + private async Task CreatePaymentMethodAsync(Dictionary> fetchingByCurrencyPair, + IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetworkBase network, InvoiceEntity entity, + StoreData store, InvoiceLogs logs) { try { @@ -317,12 +319,14 @@ namespace BTCPayServer.Controllers { return null; } - PaymentMethod paymentMethod = new PaymentMethod(); - paymentMethod.ParentEntity = entity; - paymentMethod.Network = network; + var paymentMethod = new PaymentMethod + { + ParentEntity = entity, + Network = network, + Rate = rate.BidAsk.Bid, + PreferOnion = Uri.TryCreate(entity.ServerUrl, UriKind.Absolute, out var u) && u.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) + }; paymentMethod.SetId(supportedPaymentMethod.PaymentId); - paymentMethod.Rate = rate.BidAsk.Bid; - paymentMethod.PreferOnion = Uri.TryCreate(entity.ServerUrl, UriKind.Absolute, out var u) && u.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase); using (logs.Measure($"{logPrefix} Payment method details creation")) { @@ -339,7 +343,7 @@ namespace BTCPayServer.Controllers { var amount = paymentMethod.Calculate().Due.GetValue(network as BTCPayNetwork); var limitValueCrypto = criteria.Value.Value / currentRateToCrypto.BidAsk.Bid; - + if (amount < limitValueCrypto && criteria.Above) { logs.Write($"{logPrefix} invoice amount below accepted value for payment method", InvoiceEventData.EventSeverity.Error); @@ -369,7 +373,7 @@ namespace BTCPayServer.Controllers } catch (Exception ex) { - logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})", InvoiceEventData.EventSeverity.Error); + logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex})", InvoiceEventData.EventSeverity.Error); } return null; } diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 0d7153532..102d5ca5e 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -375,16 +375,19 @@ namespace BTCPayServer.Controllers Type = criteria.Above ? PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan : PaymentMethodCriteriaViewModel.CriteriaType.LessThan, Value = criteria.Value?.ToString() ?? "" }).ToList(); + + vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail; + vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; + vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints; + vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback; + vm.RedirectAutomatically = storeBlob.RedirectAutomatically; + vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee; + vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget; + vm.CustomCSS = storeBlob.CustomCSS; vm.CustomLogo = storeBlob.CustomLogo; vm.HtmlTitle = storeBlob.HtmlTitle; vm.SetLanguages(_LangService, storeBlob.DefaultLang); - vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail; - vm.ShowRecommendedFee = storeBlob.ShowRecommendedFee; - vm.RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget; - vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; - vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints; - vm.RedirectAutomatically = storeBlob.RedirectAutomatically; return View(vm); } @@ -450,16 +453,20 @@ namespace BTCPayServer.Controllers blob.LightningMaxValue = null; blob.OnChainMinValue = null; #pragma warning restore 612 + + blob.RequiresRefundEmail = model.RequiresRefundEmail; + blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi; + blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints; + blob.OnChainWithLnInvoiceFallback = model.OnChainWithLnInvoiceFallback; + blob.RedirectAutomatically = model.RedirectAutomatically; + blob.ShowRecommendedFee = model.ShowRecommendedFee; + blob.RecommendedFeeBlockTarget = model.RecommendedFeeBlockTarget; + blob.CustomLogo = model.CustomLogo; blob.CustomCSS = model.CustomCSS; blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle; blob.DefaultLang = model.DefaultLang; - blob.RequiresRefundEmail = model.RequiresRefundEmail; - blob.ShowRecommendedFee = model.ShowRecommendedFee; - blob.RecommendedFeeBlockTarget = model.RecommendedFeeBlockTarget; - blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi; - blob.LightningPrivateRouteHints = model.LightningPrivateRouteHints; - blob.RedirectAutomatically = model.RedirectAutomatically; + if (CurrentStore.SetStoreBlob(blob)) { needUpdate = true; diff --git a/BTCPayServer/Data/StoreBlob.cs b/BTCPayServer/Data/StoreBlob.cs index e86d767c8..79d3f8d65 100644 --- a/BTCPayServer/Data/StoreBlob.cs +++ b/BTCPayServer/Data/StoreBlob.cs @@ -31,22 +31,17 @@ namespace BTCPayServer.Data [Obsolete("Use NetworkFeeMode instead")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public bool? NetworkFeeDisabled - { - get; set; - } + public bool? NetworkFeeDisabled { get; set; } [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] - public NetworkFeeMode NetworkFeeMode - { - get; - set; - } + public NetworkFeeMode NetworkFeeMode { get; set; } public bool RequiresRefundEmail { get; set; } - + public bool LightningAmountInSatoshi { get; set; } + public bool LightningPrivateRouteHints { get; set; } + public bool OnChainWithLnInvoiceFallback { get; set; } + public bool RedirectAutomatically { get; set; } public bool ShowRecommendedFee { get; set; } - public int RecommendedFeeBlockTarget { get; set; } CurrencyPair[] _DefaultCurrencyPairs; @@ -81,11 +76,7 @@ namespace BTCPayServer.Data [DefaultValue(typeof(TimeSpan), "00:15:00")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] [JsonConverter(typeof(TimeSpanJsonConverter.Minutes))] - public TimeSpan InvoiceExpiration - { - get; - set; - } + public TimeSpan InvoiceExpiration { get; set; } public decimal Spread { get; set; } = 0.0m; @@ -106,8 +97,6 @@ namespace BTCPayServer.Data [Obsolete] [JsonConverter(typeof(CurrencyValueJsonConverter))] public CurrencyValue LightningMaxValue { get; set; } - public bool LightningAmountInSatoshi { get; set; } - public bool LightningPrivateRouteHints { get; set; } public string CustomCSS { get; set; } public string CustomLogo { get; set; } @@ -185,10 +174,10 @@ namespace BTCPayServer.Data public Dictionary WalletKeyPathRoots { get; set; } public EmailSettings EmailSettings { get; set; } - public bool RedirectAutomatically { get; set; } public bool PayJoinEnabled { get; set; } public StoreHints Hints { get; set; } + public class StoreHints { public bool Wallet { get; set; } @@ -229,7 +218,7 @@ namespace BTCPayServer.Data public CurrencyValue Value { get; set; } public bool Above { get; set; } } - + public class RateRule_Obsolete { public RateRule_Obsolete() diff --git a/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs b/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs index 8d9bbbea6..7a8648101 100644 --- a/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/CheckoutExperienceViewModel.cs @@ -31,6 +31,31 @@ namespace BTCPayServer.Models.StoreViewModels [Display(Name = "Default payment method on checkout")] public string DefaultPaymentMethod { get; set; } + + + [Display(Name = "Requires a refund email")] + public bool RequiresRefundEmail { get; set; } + + [Display(Name = "Display lightning payment amounts in Satoshis")] + public bool LightningAmountInSatoshi { get; set; } + + [Display(Name = "Add hop hints for private channels to the lightning invoice")] + public bool LightningPrivateRouteHints { get; set; } + + [Display(Name = "Include lightning invoice fallback to on-chain BIP21 payment url")] + public bool OnChainWithLnInvoiceFallback { get; set; } + + [Display(Name = "Redirect invoice to redirect url automatically after paid")] + public bool RedirectAutomatically { get; set; } + + [Display(Name = "Show recommended fee")] + public bool ShowRecommendedFee { get; set; } + + [Display(Name = "Recommended fee confirmation target blocks")] + [Range(1, double.PositiveInfinity)] + public int RecommendedFeeBlockTarget { get; set; } + + [Display(Name = "Default language on checkout")] public string DefaultLang { get; set; } @@ -42,25 +67,6 @@ namespace BTCPayServer.Models.StoreViewModels [Display(Name = "Custom HTML title to display on Checkout page")] public string HtmlTitle { get; set; } - [Display(Name = "Requires a refund email")] - public bool RequiresRefundEmail { get; set; } - - [Display(Name = "Show recommended fee")] - public bool ShowRecommendedFee { get; set; } - - [Display(Name = "Recommended fee confirmation target blocks")] - [Range(1, double.PositiveInfinity)] - public int RecommendedFeeBlockTarget { get; set; } - - [Display(Name = "Display lightning payment amounts in Satoshis")] - public bool LightningAmountInSatoshi { get; set; } - - [Display(Name = "Add hop hints for private channels to the lightning invoice")] - public bool LightningPrivateRouteHints { get; set; } - - [Display(Name = "Redirect invoice to redirect url automatically after paid")] - public bool RedirectAutomatically { get; set; } - public List PaymentMethodCriteria { get; set; } } diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs index e1c582752..465d0af9d 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs @@ -46,8 +46,7 @@ namespace BTCPayServer.Payments.Bitcoin public Task ReserveAddress; } - public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse, - StoreBlob storeBlob) + public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse, StoreBlob storeBlob) { var paymentMethodId = new PaymentMethodId(model.CryptoCode, PaymentTypes.BTCLike); @@ -55,9 +54,33 @@ namespace BTCPayServer.Payments.Bitcoin var network = _networkProvider.GetNetwork(model.CryptoCode); model.IsLightning = false; model.PaymentMethodName = GetPaymentMethodName(network); - model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21; - model.InvoiceBitcoinUrlQR = cryptoInfo.PaymentUrls.BIP21; + + + var lightningFallback = ""; + if (storeBlob.OnChainWithLnInvoiceFallback) + { + var lightningInfo = invoiceResponse.CryptoInfo.FirstOrDefault(a => + a.GetpaymentMethodId() == new PaymentMethodId(model.CryptoCode, PaymentTypes.LightningLike)); + if (!String.IsNullOrEmpty(lightningInfo?.PaymentUrls?.BOLT11)) + lightningFallback = "&" + lightningInfo.PaymentUrls.BOLT11.Replace("lightning:", "lightning=", StringComparison.OrdinalIgnoreCase); + } + + model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21 + lightningFallback; + // We're trying to make as many characters uppercase to make QR smaller + // Ref: https://github.com/btcpayserver/btcpayserver/pull/2060#issuecomment-723828348 + model.InvoiceBitcoinUrlQR = cryptoInfo.PaymentUrls.BIP21 + .Replace("bitcoin:", "BITCOIN:", StringComparison.OrdinalIgnoreCase) + + lightningFallback.ToUpperInvariant().Replace("LIGHTNING=", "lightning=", StringComparison.OrdinalIgnoreCase); + + if (bech32Prefixes.Any(a => model.BtcAddress.StartsWith(a, StringComparison.OrdinalIgnoreCase))) + { + model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrlQR.Replace( + $"BITCOIN:{model.BtcAddress}", $"BITCOIN:{model.BtcAddress.ToUpperInvariant()}", + StringComparison.OrdinalIgnoreCase + ); + } } + private static string[] bech32Prefixes = new[] { "bc1", "tb1", "bcrt1" }; public override string GetCryptoImage(PaymentMethodId paymentMethodId) { diff --git a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs index 0f2580e21..4d1c86ecc 100644 --- a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs +++ b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs @@ -12,15 +12,13 @@ namespace BTCPayServer.Payments public class BitcoinPaymentType : PaymentType { public static BitcoinPaymentType Instance { get; } = new BitcoinPaymentType(); - private BitcoinPaymentType() - { - - } + + private BitcoinPaymentType() { } public override string ToPrettyString() => "On-Chain"; public override string GetId() => "BTCLike"; + public override string GetBadge() => "🔗"; public override string ToStringNormalized() => "OnChain"; - public override string GetBadge() => "🔗"; public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str) { @@ -72,8 +70,8 @@ namespace BTCPayServer.Payments public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri) { - var bip21 = ((BTCPayNetwork)network).GenerateBIP21(paymentMethodDetails.GetPaymentDestination(), cryptoInfoDue); - + var bip21 = ((BTCPayNetwork)network).GenerateBIP21(paymentMethodDetails.GetPaymentDestination(), cryptoInfoDue); + if ((paymentMethodDetails as BitcoinLikeOnChainPaymentMethod)?.PayjoinEnabled is true) { bip21 += $"&{PayjoinClient.BIP21EndpointKey}={serverUri.WithTrailingSlash()}{network.CryptoCode}/{PayjoinClient.BIP21EndpointKey}"; diff --git a/BTCPayServer/Payments/PaymentTypes.Lightning.cs b/BTCPayServer/Payments/PaymentTypes.Lightning.cs index d69c4bb38..cc7f4d530 100644 --- a/BTCPayServer/Payments/PaymentTypes.Lightning.cs +++ b/BTCPayServer/Payments/PaymentTypes.Lightning.cs @@ -11,18 +11,13 @@ namespace BTCPayServer.Payments { public static LightningPaymentType Instance { get; } = new LightningPaymentType(); - private LightningPaymentType() - { - } + private LightningPaymentType() { } public override string ToPrettyString() => "Off-Chain"; public override string GetId() => "LightningLike"; - public override string GetBadge() => "⚡"; + public override string GetBadge() => "⚡"; + public override string ToStringNormalized() => "LightningNetwork"; - public override string ToStringNormalized() - { - return "LightningNetwork"; - } public override CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str) { return ((BTCPayNetwork)network)?.ToObject(str); @@ -35,7 +30,7 @@ namespace BTCPayServer.Payments public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str) { - return JsonConvert.DeserializeObject(str); + return JsonConvert.DeserializeObject(str); } public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details) @@ -57,8 +52,10 @@ namespace BTCPayServer.Payments public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, Money cryptoInfoDue, string serverUri) { - return - $"lightning:{paymentMethodDetails.GetPaymentDestination().ToUpperInvariant().Replace("LIGHTNING:", "", StringComparison.InvariantCultureIgnoreCase)}"; + var lnInvoiceTrimmedOfScheme = paymentMethodDetails.GetPaymentDestination().ToLowerInvariant() + .Replace("lightning:", "", StringComparison.InvariantCultureIgnoreCase); + + return $"lightning:{lnInvoiceTrimmedOfScheme}"; } public override string InvoiceViewPaymentPartialName { get; } = "ViewLightningLikePaymentData"; diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index cd94407a1..3c46b4346 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -36,73 +36,34 @@ namespace BTCPayServer.Services.Invoices } public string OrderId { get; set; } [JsonProperty(PropertyName = "buyerName")] - public string BuyerName - { - get; set; - } + public string BuyerName { get; set; } [JsonProperty(PropertyName = "buyerEmail")] - public string BuyerEmail - { - get; set; - } + public string BuyerEmail { get; set; } [JsonProperty(PropertyName = "buyerCountry")] - public string BuyerCountry - { - get; set; - } + public string BuyerCountry { get; set; } [JsonProperty(PropertyName = "buyerZip")] - public string BuyerZip - { - get; set; - } + public string BuyerZip { get; set; } [JsonProperty(PropertyName = "buyerState")] - public string BuyerState - { - get; set; - } + public string BuyerState { get; set; } [JsonProperty(PropertyName = "buyerCity")] - public string BuyerCity - { - get; set; - } + public string BuyerCity { get; set; } [JsonProperty(PropertyName = "buyerAddress2")] - public string BuyerAddress2 - { - get; set; - } + public string BuyerAddress2 { get; set; } [JsonProperty(PropertyName = "buyerAddress1")] - public string BuyerAddress1 - { - get; set; - } + public string BuyerAddress1 { get; set; } [JsonProperty(PropertyName = "buyerPhone")] - public string BuyerPhone - { - get; set; - } + public string BuyerPhone { get; set; } [JsonProperty(PropertyName = "itemDesc")] - public string ItemDesc - { - get; set; - } + public string ItemDesc { get; set; } [JsonProperty(PropertyName = "itemCode")] - public string ItemCode - { - get; set; - } + public string ItemCode { get; set; } [JsonProperty(PropertyName = "physical")] - public bool? Physical - { - get; set; - } + public bool? Physical { get; set; } [JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)] - public decimal? TaxIncluded - { - get; set; - } + public decimal? TaxIncluded { get; set; } public string PosData { get; set; } [JsonExtensionData] public IDictionary AdditionalData { get; set; } @@ -122,133 +83,64 @@ namespace BTCPayServer.Services.Invoices class BuyerInformation { [JsonProperty(PropertyName = "buyerName")] - public string BuyerName - { - get; set; - } + public string BuyerName { get; set; } [JsonProperty(PropertyName = "buyerEmail")] - public string BuyerEmail - { - get; set; - } + public string BuyerEmail { get; set; } [JsonProperty(PropertyName = "buyerCountry")] - public string BuyerCountry - { - get; set; - } + public string BuyerCountry { get; set; } [JsonProperty(PropertyName = "buyerZip")] - public string BuyerZip - { - get; set; - } + public string BuyerZip { get; set; } [JsonProperty(PropertyName = "buyerState")] - public string BuyerState - { - get; set; - } + public string BuyerState { get; set; } [JsonProperty(PropertyName = "buyerCity")] - public string BuyerCity - { - get; set; - } + public string BuyerCity { get; set; } [JsonProperty(PropertyName = "buyerAddress2")] - public string BuyerAddress2 - { - get; set; - } + public string BuyerAddress2 { get; set; } [JsonProperty(PropertyName = "buyerAddress1")] - public string BuyerAddress1 - { - get; set; - } + public string BuyerAddress1 { get; set; } [JsonProperty(PropertyName = "buyerPhone")] - public string BuyerPhone - { - get; set; - } + public string BuyerPhone { get; set; } } + class ProductInformation { [JsonProperty(PropertyName = "itemDesc")] - public string ItemDesc - { - get; set; - } + public string ItemDesc { get; set; } [JsonProperty(PropertyName = "itemCode")] - public string ItemCode - { - get; set; - } + public string ItemCode { get; set; } [JsonProperty(PropertyName = "physical")] - public bool Physical - { - get; set; - } + public bool Physical { get; set; } [JsonProperty(PropertyName = "price")] - public decimal Price - { - get; set; - } + public decimal Price { get; set; } [JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)] - public decimal TaxIncluded - { - get; set; - } + public decimal TaxIncluded { get; set; } [JsonProperty(PropertyName = "currency")] - public string Currency - { - get; set; - } + public string Currency { get; set; } } + [JsonIgnore] public BTCPayNetworkProvider Networks { get; set; } public const int InternalTagSupport_Version = 1; public const int GreenfieldInvoices_Version = 2; public const int Lastest_Version = 2; public int Version { get; set; } - public string Id - { - get; set; - } - public string StoreId - { - get; set; - } + public string Id { get; set; } + public string StoreId { get; set; } - public SpeedPolicy SpeedPolicy - { - get; set; - } + public SpeedPolicy SpeedPolicy { get; set; } [Obsolete("Use GetPaymentMethod(network) instead")] - public decimal Rate - { - get; set; - } - public DateTimeOffset InvoiceTime - { - get; set; - } - public DateTimeOffset ExpirationTime - { - get; set; - } + public decimal Rate { get; set; } + public DateTimeOffset InvoiceTime { get; set; } + public DateTimeOffset ExpirationTime { get; set; } [Obsolete("Use GetPaymentMethod(network).GetPaymentMethodDetails().GetDestinationAddress() instead")] - public string DepositAddress - { - get; set; - } - - public InvoiceMetadata Metadata - { - get; - set; - } + public string DepositAddress { get; set; } + public InvoiceMetadata Metadata { get; set; } public decimal Price { get; set; } public string Currency { get; set; } @@ -267,18 +159,10 @@ namespace BTCPayServer.Services.Invoices } [Obsolete("Use GetDerivationStrategies instead")] - public string DerivationStrategy - { - get; - set; - } + public string DerivationStrategy { get; set; } [Obsolete("Use GetPaymentMethodFactories() instead")] - public string DerivationStrategies - { - get; - set; - } + public string DerivationStrategies { get; set; } public IEnumerable GetSupportedPaymentMethod(PaymentMethodId paymentMethodId) where T : ISupportedPaymentMethod { return @@ -335,28 +219,18 @@ namespace BTCPayServer.Services.Invoices } [JsonIgnore] - public InvoiceStatus Status - { - get; - set; - } + public InvoiceStatus Status { get; set; } [JsonProperty(PropertyName = "status")] [Obsolete("Use Status instead")] public string StatusString => InvoiceState.ToString(Status); [JsonIgnore] - public InvoiceExceptionStatus ExceptionStatus - { - get; set; - } + public InvoiceExceptionStatus ExceptionStatus { get; set; } [JsonProperty(PropertyName = "exceptionStatus")] [Obsolete("Use ExceptionStatus instead")] public string ExceptionStatusString => InvoiceState.ToString(ExceptionStatus); [Obsolete("Use GetPayments instead")] - public List Payments - { - get; set; - } + public List Payments { get; set; } #pragma warning disable CS0618 public List GetPayments() @@ -372,22 +246,10 @@ namespace BTCPayServer.Services.Invoices return GetPayments(network.CryptoCode); } #pragma warning restore CS0618 - public bool Refundable - { - get; - set; - } - public string RefundMail - { - get; - set; - } + public bool Refundable { get; set; } + public string RefundMail { get; set; } [JsonProperty("redirectURL")] - public string RedirectURLTemplate - { - get; - set; - } + public string RedirectURLTemplate { get; set; } [JsonIgnore] public Uri RedirectURL => FillPlaceholdersUri(RedirectURLTemplate); @@ -401,65 +263,29 @@ namespace BTCPayServer.Services.Invoices return null; } - public bool RedirectAutomatically - { - get; - set; - } + public bool RedirectAutomatically { get; set; } [Obsolete("Use GetPaymentMethod(network).GetTxFee() instead")] - public Money TxFee - { - get; - set; - } - public bool FullNotifications - { - get; - set; - } - public string NotificationEmail - { - get; - set; - } + public Money TxFee { get; set; } + public bool FullNotifications { get; set; } + public string NotificationEmail { get; set; } [JsonProperty("notificationURL")] - public string NotificationURLTemplate - { - get; - set; - } + public string NotificationURLTemplate { get; set; } [JsonIgnore] public Uri NotificationURL => FillPlaceholdersUri(NotificationURLTemplate); - public string ServerUrl - { - get; - set; - } + public string ServerUrl { get; set; } [Obsolete("Use Set/GetPaymentMethod() instead")] [JsonProperty(PropertyName = "cryptoData")] public JObject PaymentMethod { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public DateTimeOffset MonitoringExpiration - { - get; - set; - } - public HistoricalAddressInvoiceData[] HistoricalAddresses - { - get; - set; - } + public DateTimeOffset MonitoringExpiration { get; set; } + public HistoricalAddressInvoiceData[] HistoricalAddresses { get; set; } - public HashSet AvailableAddressHashes - { - get; - set; - } + public HashSet AvailableAddressHashes { get; set; } public bool ExtendedNotifications { get; set; } public List Events { get; internal set; } public double PaymentTolerance { get; set; } @@ -559,7 +385,7 @@ namespace BTCPayServer.Services.Invoices { var minerInfo = new MinerFeeInfo(); minerInfo.TotalFee = accounting.NetworkFee.Satoshi; - minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod) details).FeeRate + minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate .GetFee(1).Satoshi; dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo); cryptoInfo.PaymentUrls = new InvoicePaymentUrls() @@ -1181,7 +1007,7 @@ namespace BTCPayServer.Services.Invoices { return null; } - + paymentData.Network = Network; if (paymentData is BitcoinLikePaymentData bitcoin) { @@ -1229,9 +1055,10 @@ namespace BTCPayServer.Services.Invoices PaymentType paymentType; if (string.IsNullOrEmpty(CryptoPaymentDataType)) { - paymentType = BitcoinPaymentType.Instance;; + paymentType = BitcoinPaymentType.Instance; + ; } - else if(!PaymentTypes.TryParse(CryptoPaymentDataType, out paymentType)) + else if (!PaymentTypes.TryParse(CryptoPaymentDataType, out paymentType)) { return null; } diff --git a/BTCPayServer/Views/Stores/CheckoutExperience.cshtml b/BTCPayServer/Views/Stores/CheckoutExperience.cshtml index 8c8a2ba34..f1613a169 100644 --- a/BTCPayServer/Views/Stores/CheckoutExperience.cshtml +++ b/BTCPayServer/Views/Stores/CheckoutExperience.cshtml @@ -1,4 +1,4 @@ -@using BTCPayServer.Payments +@using BTCPayServer.Payments @model CheckoutExperienceViewModel @{ Layout = "../Shared/_NavLayout.cshtml"; @@ -36,9 +36,9 @@ @for (var index = 0; index < Model.PaymentMethodCriteria.Count; index++) { var criteria = Model.PaymentMethodCriteria[index]; - + - + @PaymentMethodId.Parse(criteria.PaymentMethod).ToPrettyString() @@ -49,43 +49,46 @@ - + } } -
+
- +
- + -
- + -
- + + +
+
+
+
+ -
- +

Fee will be shown for BTC and LTC onchain payments only.

@@ -103,19 +106,19 @@
- +
- +
- +

Bundled Themes: diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json index ed4a2e496..9ed82f239 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json @@ -320,6 +320,31 @@ "default": false, "description": "If true, then no authentication is needed to create invoices on this store." }, + "requiresRefundEmail": { + "type": "boolean", + "default": false, + "description": "If true, the checkout page will ask to enter an email address before accessing payment information." + }, + "lightningAmountInSatoshi": { + "type": "boolean", + "default": false, + "description": "If true, lightning payment methods show amount in satoshi in the checkout page." + }, + "lightningPrivateRouteHints": { + "type": "boolean", + "default": false, + "description": "Should private route hints be included in the lightning payment of the checkout page." + }, + "onChainWithLnInvoiceFallback": { + "type": "boolean", + "default": false, + "description": "Include lightning invoice fallback to on-chain BIP21 payment url." + }, + "redirectAutomatically": { + "type": "boolean", + "default": false, + "description": "After successfull payment, should the checkout page redirect the user automatically to the redirect URL of the invoice?" + }, "showRecommendedFee": { "type": "boolean", "default": true @@ -335,11 +360,6 @@ "default": "en", "description": "The default language to use in the checkout page. (The different translations available are listed [here](https://github.com/btcpayserver/btcpayserver/tree/master/BTCPayServer/wwwroot/locales)" }, - "lightningAmountInSatoshi": { - "type": "boolean", - "default": false, - "description": "If true, lightning payment methods show amount in satoshi in the checkout page." - }, "customLogo": { "type": "string", "nullable": true, @@ -355,16 +375,6 @@ "nullable": true, "description": "The HTML title of the checkout page (when you over the tab in your browser)" }, - "redirectAutomatically": { - "type": "boolean", - "default": false, - "description": "After successfull payment, should the checkout page redirect the user automatically to the redirect URL of the invoice?" - }, - "requiresRefundEmail": { - "type": "boolean", - "default": false, - "description": "If true, the checkout page will ask to enter an email address before accessing payment information." - }, "networkFeeMode": { "$ref": "#/components/schemas/NetworkFeeMode" }, @@ -372,11 +382,6 @@ "type": "boolean", "default": false, "description": "If true, payjoin will be proposed in the checkout page if possible. ([More information](https://docs.btcpayserver.org/Payjoin/))" - }, - "lightningPrivateRouteHints": { - "type": "boolean", - "default": false, - "description": "Should private route hints be included in the lightning payment of the checkout page." } } },