Merge pull request #6283 from NicolasDorier/activationbug

Fix: An unactivated payment method failing to activate would crash the checkout
This commit is contained in:
Nicolas Dorier 2024-10-08 15:50:28 +09:00 committed by GitHub
commit 663f97265a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 72 additions and 81 deletions

View file

@ -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<CheckoutModel?> GetCheckoutModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang)
private async Task<CheckoutModel?> GetCheckoutModel(string invoiceId, PaymentMethodId? paymentMethodId, string? lang, HashSet<PaymentMethodId>? 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<PaymentMethodId>();
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();

View file

@ -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);
}

View file

@ -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<MoneroLikePaymentData>(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;
}
}
}

View file

@ -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<ZcashLikePaymentData>(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;
}
}
}