diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs index e795f3c1b..e2fe1132c 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs @@ -698,6 +698,7 @@ namespace BTCPayServer.Controllers if (model == null) { // see if the invoice actually exists and is in a state for which we do not display the checkout + // TODO: Can happen if the invoice has lazy activation which failed for all payment methods. We should display error instead... var invoice = await _InvoiceRepository.GetInvoice(invoiceId); var store = invoice != null ? await _StoreRepository.GetStoreByInvoiceId(invoice.Id) : null; var receipt = invoice != null && store != null ? InvoiceDataBase.ReceiptOptions.Merge(store.GetStoreBlob().ReceiptOptions, invoice.ReceiptOptions) : null; @@ -713,8 +714,9 @@ namespace BTCPayServer.Controllers return View(model); } - private async Task GetCheckoutModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang) + private async Task GetCheckoutModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang, HashSet? excludedPaymentMethodIds = null) { + var originalPaymentMethodId = paymentMethodId; var invoice = await _InvoiceRepository.GetInvoice(invoiceId); if (invoice == null) return null; @@ -722,11 +724,13 @@ namespace BTCPayServer.Controllers var store = await _StoreRepository.FindStore(invoice.StoreId); if (store == null) return null; - + excludedPaymentMethodIds ??= new HashSet(); bool isDefaultPaymentId = false; var storeBlob = store.GetStoreBlob(); - var displayedPaymentMethods = invoice.GetPaymentPrompts().Select(p => p.PaymentMethodId).ToHashSet(); + var displayedPaymentMethods = invoice.GetPaymentPrompts() + .Where(p => !excludedPaymentMethodIds.Contains(p.PaymentMethodId)) + .Select(p => p.PaymentMethodId).ToHashSet(); var btcId = PaymentTypes.CHAIN.GetPaymentMethodId("BTC"); @@ -822,7 +826,14 @@ namespace BTCPayServer.Controllers if (prompt is null) return null; if (activated) - return await GetCheckoutModel(invoiceId, paymentMethodId, lang); + return await GetCheckoutModel(invoiceId, paymentMethodId, lang, excludedPaymentMethodIds); + + if (!prompt.Activated) + { + // It failed to activate. Let's try to exclude it and retry + excludedPaymentMethodIds.Add(prompt.PaymentMethodId); + return await GetCheckoutModel(invoiceId, originalPaymentMethodId, lang, excludedPaymentMethodIds); + } var accounting = prompt.Calculate(); diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinCheckoutModelExtension.cs b/BTCPayServer/Payments/Bitcoin/BitcoinCheckoutModelExtension.cs index 06cf0133b..02cd562b4 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinCheckoutModelExtension.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinCheckoutModelExtension.cs @@ -47,19 +47,20 @@ namespace BTCPayServer.Payments.Bitcoin public PaymentMethodId PaymentMethodId { get; } public void ModifyCheckoutModel(CheckoutModelContext context) { - if (context is not { Handler: BitcoinLikePaymentHandler handler}) + if (context is not { Handler: BitcoinLikePaymentHandler handler }) return; + var prompt = context.Prompt; var details = handler.ParsePaymentPromptDetails(prompt.Details); context.Model.CheckoutBodyComponentName = CheckoutBodyComponentName; context.Model.ShowRecommendedFee = context.StoreBlob.ShowRecommendedFee; context.Model.FeeRate = details.RecommendedFeeRate.SatoshiPerByte; - + var bip21Case = _Network.SupportLightning && context.StoreBlob.OnChainWithLnInvoiceFallback; var amountInSats = bip21Case && context.StoreBlob.LightningAmountInSatoshi && context.Model.PaymentMethodCurrency == "BTC"; string? lightningFallback = null; - if (context.Model.Activated && bip21Case) + if (bip21Case) { var lnPmi = PaymentTypes.LN.GetPaymentMethodId(handler.Network.CryptoCode); var lnPrompt = context.InvoiceEntity.GetPaymentPrompt(lnPmi); @@ -85,54 +86,49 @@ namespace BTCPayServer.Payments.Bitcoin } } - if (context.Model.Activated) + + var paymentData = context.InvoiceEntity.GetAllBitcoinPaymentData(handler, true)?.MinBy(o => o.ConfirmationCount); + if (paymentData is not null) { - var paymentData = context.InvoiceEntity.GetAllBitcoinPaymentData(handler, true)?.MinBy(o => o.ConfirmationCount); - if (paymentData is not null) - { - context.Model.RequiredConfirmations = NBXplorerListener.ConfirmationRequired(context.InvoiceEntity, paymentData); - context.Model.ReceivedConfirmations = paymentData.ConfirmationCount; - } - - // We're leading the way in Bitcoin community with adding UPPERCASE Bech32 addresses in QR Code - // - // Correct casing: Addresses in payment URI need to be … - // - lowercase in link version - // - uppercase in QR version - // - // The keys (e.g. "bitcoin:" or "lightning=" should be lowercase! - - // cryptoInfo.PaymentUrls?.BIP21: bitcoin:bcrt1qxp2qa5?amount=0.00044007 - var bip21 = paymentLinkExtension.GetPaymentLink(prompt, context.UrlHelper); - context.Model.InvoiceBitcoinUrl = context.Model.InvoiceBitcoinUrlQR = bip21 ?? ""; - // model.InvoiceBitcoinUrl: bitcoin:bcrt1qxp2qa5?amount=0.00044007 - // model.InvoiceBitcoinUrlQR: bitcoin:bcrt1qxp2qa5?amount=0.00044007 - - if (!string.IsNullOrEmpty(lightningFallback)) - { - var delimiterUrl = context.Model.InvoiceBitcoinUrl.Contains("?") ? "&" : "?"; - context.Model.InvoiceBitcoinUrl += $"{delimiterUrl}{lightningFallback}"; - // model.InvoiceBitcoinUrl: bitcoin:bcrt1qxp2qa5dhn7?amount=0.00044007&lightning=lnbcrt440070n1... - - var delimiterUrlQR = context.Model.InvoiceBitcoinUrlQR.Contains("?") ? "&" : "?"; - context.Model.InvoiceBitcoinUrlQR += $"{delimiterUrlQR}{lightningFallback.ToUpperInvariant().Replace("LIGHTNING=", "lightning=", StringComparison.OrdinalIgnoreCase)}"; - // model.InvoiceBitcoinUrlQR: bitcoin:bcrt1qxp2qa5dhn7?amount=0.00044007&lightning=LNBCRT4400... - } - - if (_Network.CryptoCode.Equals("BTC", StringComparison.InvariantCultureIgnoreCase) && _bech32Prefix is not null && context.Model.Address.StartsWith(_bech32Prefix, StringComparison.OrdinalIgnoreCase)) - { - context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrlQR.Replace( - $"{_Network.NBitcoinNetwork.UriScheme}:{context.Model.Address}", $"{_Network.NBitcoinNetwork.UriScheme}:{context.Model.Address.ToUpperInvariant()}", - StringComparison.OrdinalIgnoreCase); - // model.InvoiceBitcoinUrlQR: bitcoin:BCRT1QXP2QA5DHN...?amount=0.00044007&lightning=LNBCRT4400... - } - } - else - { - context.Model.InvoiceBitcoinUrl = context.Model.InvoiceBitcoinUrlQR = string.Empty; + context.Model.RequiredConfirmations = NBXplorerListener.ConfirmationRequired(context.InvoiceEntity, paymentData); + context.Model.ReceivedConfirmations = paymentData.ConfirmationCount; } - if (context.Model.Activated && amountInSats) + // We're leading the way in Bitcoin community with adding UPPERCASE Bech32 addresses in QR Code + // + // Correct casing: Addresses in payment URI need to be … + // - lowercase in link version + // - uppercase in QR version + // + // The keys (e.g. "bitcoin:" or "lightning=" should be lowercase! + + // cryptoInfo.PaymentUrls?.BIP21: bitcoin:bcrt1qxp2qa5?amount=0.00044007 + var bip21 = paymentLinkExtension.GetPaymentLink(prompt, context.UrlHelper); + context.Model.InvoiceBitcoinUrl = context.Model.InvoiceBitcoinUrlQR = bip21 ?? ""; + // model.InvoiceBitcoinUrl: bitcoin:bcrt1qxp2qa5?amount=0.00044007 + // model.InvoiceBitcoinUrlQR: bitcoin:bcrt1qxp2qa5?amount=0.00044007 + + if (!string.IsNullOrEmpty(lightningFallback)) + { + var delimiterUrl = context.Model.InvoiceBitcoinUrl.Contains("?") ? "&" : "?"; + context.Model.InvoiceBitcoinUrl += $"{delimiterUrl}{lightningFallback}"; + // model.InvoiceBitcoinUrl: bitcoin:bcrt1qxp2qa5dhn7?amount=0.00044007&lightning=lnbcrt440070n1... + + var delimiterUrlQR = context.Model.InvoiceBitcoinUrlQR.Contains("?") ? "&" : "?"; + context.Model.InvoiceBitcoinUrlQR += $"{delimiterUrlQR}{lightningFallback.ToUpperInvariant().Replace("LIGHTNING=", "lightning=", StringComparison.OrdinalIgnoreCase)}"; + // model.InvoiceBitcoinUrlQR: bitcoin:bcrt1qxp2qa5dhn7?amount=0.00044007&lightning=LNBCRT4400... + } + + if (_Network.CryptoCode.Equals("BTC", StringComparison.InvariantCultureIgnoreCase) && _bech32Prefix is not null && context.Model.Address.StartsWith(_bech32Prefix, StringComparison.OrdinalIgnoreCase)) + { + context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrlQR.Replace( + $"{_Network.NBitcoinNetwork.UriScheme}:{context.Model.Address}", $"{_Network.NBitcoinNetwork.UriScheme}:{context.Model.Address.ToUpperInvariant()}", + StringComparison.OrdinalIgnoreCase); + // model.InvoiceBitcoinUrlQR: bitcoin:BCRT1QXP2QA5DHN...?amount=0.00044007&lightning=LNBCRT4400... + } + + + if (amountInSats) { PreparePaymentModelForAmountInSats(context.Model, context.Prompt.Rate, _displayFormatter); } diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroCheckoutModelExtension.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroCheckoutModelExtension.cs index 1b51dcd03..4104812c2 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroCheckoutModelExtension.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroCheckoutModelExtension.cs @@ -37,26 +37,18 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments if (context is not { Handler: MoneroLikePaymentMethodHandler handler }) return; context.Model.CheckoutBodyComponentName = BitcoinCheckoutModelExtension.CheckoutBodyComponentName; - if (context.Model.Activated) - { - var details = context.InvoiceEntity.GetPayments(true) + var details = context.InvoiceEntity.GetPayments(true) .Select(p => p.GetDetails(handler)) .Where(p => p is not null) .FirstOrDefault(); - if (details is not null) - { - context.Model.ReceivedConfirmations = details.ConfirmationCount; - context.Model.RequiredConfirmations = (int)MoneroListener.ConfirmationsRequired(details, context.InvoiceEntity.SpeedPolicy); - } - - context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper); - context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl; - } - else + if (details is not null) { - context.Model.InvoiceBitcoinUrl = ""; - context.Model.InvoiceBitcoinUrlQR = ""; + context.Model.ReceivedConfirmations = details.ConfirmationCount; + context.Model.RequiredConfirmations = (int)MoneroListener.ConfirmationsRequired(details, context.InvoiceEntity.SpeedPolicy); } + + context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper); + context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl; } } } diff --git a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashCheckoutModelExtension.cs b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashCheckoutModelExtension.cs index 234aba63f..ae7129845 100644 --- a/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashCheckoutModelExtension.cs +++ b/BTCPayServer/Services/Altcoins/Zcash/Payments/ZcashCheckoutModelExtension.cs @@ -37,25 +37,17 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Payments if (context is not { Handler: ZcashLikePaymentMethodHandler handler }) return; context.Model.CheckoutBodyComponentName = BitcoinCheckoutModelExtension.CheckoutBodyComponentName; - if (context.Model.Activated) - { - var details = context.InvoiceEntity.GetPayments(true) + var details = context.InvoiceEntity.GetPayments(true) .Select(p => p.GetDetails(handler)) .Where(p => p is not null) .FirstOrDefault(); - if (details is not null) - { - context.Model.ReceivedConfirmations = details.ConfirmationCount; - context.Model.RequiredConfirmations = (int)ZcashListener.ConfirmationsRequired(context.InvoiceEntity.SpeedPolicy); - } - context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper); - context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl; - } - else + if (details is not null) { - context.Model.InvoiceBitcoinUrl = ""; - context.Model.InvoiceBitcoinUrlQR = ""; + context.Model.ReceivedConfirmations = details.ConfirmationCount; + context.Model.RequiredConfirmations = (int)ZcashListener.ConfirmationsRequired(context.InvoiceEntity.SpeedPolicy); } + context.Model.InvoiceBitcoinUrl = paymentLinkExtension.GetPaymentLink(context.Prompt, context.UrlHelper); + context.Model.InvoiceBitcoinUrlQR = context.Model.InvoiceBitcoinUrl; } } }