mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-13 11:35:51 +01:00
BIP21 + LNURL fix (#4616)
In case of the unified invoice, the LNURL wasn't correct — with this change we are simply reusing th one that was issued on invoice creation instead of generating it anew on the fly. Also fixes missing uppercasing for the QR code in case of non-unified QR. And removes the `lightning:` scheme from the LNURL that's displayed to the user (unifies it with what we do for Onchain and Lightning)
This commit is contained in:
parent
0d1bab45a0
commit
753ffd401b
7 changed files with 87 additions and 30 deletions
|
@ -33,7 +33,8 @@ namespace BTCPayServer.Tests
|
|||
s.CreateNewStore();
|
||||
s.EnableCheckoutV2();
|
||||
s.AddLightningNode();
|
||||
s.AddDerivationScheme();
|
||||
// Use non-legacy derivation scheme
|
||||
s.AddDerivationScheme("BTC", "tpubDD79XF4pzhmPSJ9AyUay9YbXAeD1c6nkUqC32pnKARJH6Ja5hGUfGc76V82ahXpsKqN6UcSGXMkzR34aZq4W23C6DAdZFaVrzWqzj24F8BC");
|
||||
|
||||
// Configure store url
|
||||
var storeUrl = "https://satoshisteaks.com/";
|
||||
|
@ -59,9 +60,16 @@ namespace BTCPayServer.Tests
|
|||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
|
||||
Assert.Contains("Bitcoin", s.Driver.FindElement(By.CssSelector(".payment-method.active")).Text);
|
||||
Assert.Contains("LNURL", s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Text);
|
||||
var qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
var payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
var copyAddress = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
|
||||
Assert.Equal($"bitcoin:{address}", payUrl);
|
||||
Assert.StartsWith("bcrt", s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value"));
|
||||
Assert.DoesNotContain("lightning=", payUrl);
|
||||
Assert.Equal(address, copyAddress);
|
||||
Assert.Equal($"bitcoin:{address.ToUpperInvariant()}", qrValue);
|
||||
s.Driver.ElementDoesNotExist(By.Id("Lightning_BTC"));
|
||||
|
||||
// Switch to LNURL
|
||||
s.Driver.FindElement(By.CssSelector(".payment-method:nth-child(2)")).Click();
|
||||
|
@ -69,18 +77,26 @@ namespace BTCPayServer.Tests
|
|||
{
|
||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("lightning:lnurl", payUrl);
|
||||
Assert.StartsWith("lnurl", s.Driver.WaitForElement(By.Id("Lightning_BTC")).GetAttribute("value"));
|
||||
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
|
||||
});
|
||||
|
||||
// Default payment method
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector(".payment-method")).Count);
|
||||
Assert.Contains("Lightning", s.Driver.WaitForElement(By.CssSelector(".payment-method.active")).Text);
|
||||
Assert.Contains("Bitcoin", s.Driver.WaitForElement(By.CssSelector(".payment-method")).Text);
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("lightning:lnbcrt", payUrl);
|
||||
copyAddress = s.Driver.FindElement(By.Id("Lightning_BTC_LightningLike")).GetAttribute("value");
|
||||
Assert.Equal($"lightning:{address}", payUrl);
|
||||
Assert.Equal(address, copyAddress);
|
||||
Assert.Equal($"lightning:{address.ToUpperInvariant()}", qrValue);
|
||||
s.Driver.ElementDoesNotExist(By.Id("Address_BTC"));
|
||||
|
||||
// Lightning amount in Sats
|
||||
Assert.Contains("BTC", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
|
@ -90,6 +106,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Contains("Sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
|
||||
// Expire
|
||||
|
@ -114,6 +131,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice();
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
|
||||
// Details
|
||||
s.Driver.ToggleCollapse("PaymentDetails");
|
||||
|
@ -126,7 +144,7 @@ namespace BTCPayServer.Tests
|
|||
|
||||
// Pay partial amount
|
||||
await Task.Delay(200);
|
||||
var address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
var amountFraction = "0.00001";
|
||||
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
|
||||
Money.Parse(amountFraction));
|
||||
|
@ -176,28 +194,55 @@ namespace BTCPayServer.Tests
|
|||
|
||||
invoiceId = s.CreateInvoice();
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
var copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
|
||||
var copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value");
|
||||
Assert.StartsWith($"bitcoin:{address}?amount=", payUrl);
|
||||
Assert.Contains("?amount=", payUrl);
|
||||
Assert.Contains("&lightning=", payUrl);
|
||||
Assert.StartsWith("bcrt", copyAddressOnchain);
|
||||
Assert.Equal(address, copyAddressOnchain);
|
||||
Assert.StartsWith("lnbcrt", copyAddressLightning);
|
||||
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?amount=", qrValue);
|
||||
Assert.Contains("&lightning=LNBCRT", qrValue);
|
||||
|
||||
// BIP21 with LN as default payment method
|
||||
s.GoToHome();
|
||||
invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
Assert.Contains("&lightning=lnbcrt", payUrl);
|
||||
|
||||
// BIP21 with topup invoice
|
||||
// Ensure LNURL is enabled
|
||||
s.GoToHome();
|
||||
s.GoToLightningSettings();
|
||||
Assert.True(s.Driver.FindElement(By.Id("LNURLEnabled")).Selected);
|
||||
Assert.True(s.Driver.FindElement(By.Id("LNURLStandardInvoiceEnabled")).Selected);
|
||||
|
||||
// BIP21 with topup invoice
|
||||
invoiceId = s.CreateInvoice(amount: null);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector(".payment-method")));
|
||||
qrValue = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-qr-value");
|
||||
address = s.Driver.FindElement(By.CssSelector(".qr-container")).GetAttribute("data-clipboard");
|
||||
payUrl = s.Driver.FindElement(By.CssSelector(".btn-primary")).GetAttribute("href");
|
||||
Assert.StartsWith("bitcoin:", payUrl);
|
||||
Assert.DoesNotContain("&lightning=lnurl", payUrl);
|
||||
copyAddressOnchain = s.Driver.FindElement(By.Id("Address_BTC")).GetAttribute("value");
|
||||
copyAddressLightning = s.Driver.FindElement(By.Id("Lightning_BTC")).GetAttribute("value");
|
||||
Assert.StartsWith($"bitcoin:{address}", payUrl);
|
||||
Assert.Contains("?lightning=lnurl", payUrl);
|
||||
Assert.DoesNotContain("amount=", payUrl);
|
||||
Assert.StartsWith("bcrt", copyAddressOnchain);
|
||||
Assert.Equal(address, copyAddressOnchain);
|
||||
Assert.StartsWith("lnurl", copyAddressLightning);
|
||||
Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?lightning=LNURL", qrValue);
|
||||
|
||||
// Expiry message should not show amount for topup invoice
|
||||
expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds"));
|
||||
|
@ -223,6 +268,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
paymentInfo = s.Driver.FindElement(By.Id("PaymentInfo"));
|
||||
Assert.False(paymentInfo.Displayed);
|
||||
Assert.DoesNotContain("This invoice will expire in", paymentInfo.Text);
|
||||
|
|
|
@ -1613,10 +1613,10 @@ namespace BTCPayServer.Tests
|
|||
var paymentMethodUnified = Assert.IsType<PaymentModel>(
|
||||
Assert.IsType<ViewResult>(res).Model
|
||||
);
|
||||
Assert.StartsWith("bitcoin:", paymentMethodUnified.InvoiceBitcoinUrl);
|
||||
Assert.StartsWith("bitcoin:", paymentMethodUnified.InvoiceBitcoinUrlQR);
|
||||
Assert.Contains("&lightning=", paymentMethodUnified.InvoiceBitcoinUrl);
|
||||
Assert.Contains("&lightning=", paymentMethodUnified.InvoiceBitcoinUrlQR);
|
||||
Assert.StartsWith("bitcoin:bcrt", paymentMethodUnified.InvoiceBitcoinUrl);
|
||||
Assert.StartsWith("bitcoin:BCRT", paymentMethodUnified.InvoiceBitcoinUrlQR);
|
||||
Assert.Contains("&lightning=lnbcrt", paymentMethodUnified.InvoiceBitcoinUrl);
|
||||
Assert.Contains("&lightning=LNBCRT", paymentMethodUnified.InvoiceBitcoinUrlQR);
|
||||
|
||||
// Check correct casing: Addresses in payment URI need to be …
|
||||
// - lowercase in link version
|
||||
|
|
|
@ -655,7 +655,7 @@ namespace BTCPayServer.Controllers
|
|||
var enabledPaymentIds = store.GetEnabledPaymentIds(_NetworkProvider)
|
||||
.Where(pmId => storeBlob.CheckoutType == CheckoutType.V1 ||
|
||||
// Exclude LNURL for Checkout v2 + non-top up invoices
|
||||
(pmId.PaymentType is not LNURLPayPaymentType || invoice.IsUnsetTopUp()))
|
||||
pmId != lnurlId || invoice.IsUnsetTopUp())
|
||||
.ToArray();
|
||||
|
||||
// Exclude Lightning if OnChainWithLnInvoiceFallback is active and we have both payment methods
|
||||
|
@ -805,7 +805,7 @@ namespace BTCPayServer.Controllers
|
|||
AvailableCryptos = invoice.GetPaymentMethods()
|
||||
.Where(i => i.Network != null && storeBlob.CheckoutType == CheckoutType.V1 ||
|
||||
// Exclude LNURL for Checkout v2 + non-top up invoices
|
||||
i.GetId().PaymentType is not LNURLPayPaymentType || invoice.IsUnsetTopUp())
|
||||
i.GetId() != lnurlId || invoice.IsUnsetTopUp())
|
||||
.Select(kv =>
|
||||
{
|
||||
var availableCryptoPaymentMethodId = kv.GetId();
|
||||
|
|
|
@ -10,7 +10,6 @@ using BTCPayServer.Models;
|
|||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer.Models;
|
||||
|
@ -44,7 +43,6 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||
network => Encoders.ASCII.EncodeData(
|
||||
network.NBitcoinNetwork.GetBech32Encoder(Bech32Type.WITNESS_PUBKEY_ADDRESS, false)
|
||||
.HumanReadablePart));
|
||||
|
||||
}
|
||||
|
||||
class Prepare
|
||||
|
@ -58,10 +56,11 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||
StoreBlob storeBlob, IPaymentMethod paymentMethod)
|
||||
{
|
||||
var paymentMethodId = paymentMethod.GetId();
|
||||
var paymentMethodDetails = (BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails();
|
||||
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>(model.CryptoCode);
|
||||
model.ShowRecommendedFee = storeBlob.ShowRecommendedFee;
|
||||
model.FeeRate = ((BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails()).GetFeeRate();
|
||||
model.FeeRate = paymentMethodDetails.GetFeeRate();
|
||||
model.PaymentMethodName = GetPaymentMethodName(network);
|
||||
|
||||
string lightningFallback = null;
|
||||
|
@ -75,11 +74,20 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||
}
|
||||
else
|
||||
{
|
||||
var lnurl = invoiceResponse.CryptoInfo.FirstOrDefault(a =>
|
||||
var lnurlInfo = invoiceResponse.CryptoInfo.FirstOrDefault(a =>
|
||||
a.GetpaymentMethodId() == new PaymentMethodId(model.CryptoCode, PaymentTypes.LNURLPay));
|
||||
if (lnurl is not null)
|
||||
if (lnurlInfo is not null)
|
||||
{
|
||||
lightningFallback = LNURL.LNURL.EncodeUri(new Uri(lnurl.Url), "payRequest", true).ToString();
|
||||
lightningFallback = lnurlInfo.PaymentUrls?.AdditionalData["LNURLP"].ToObject<string>();
|
||||
|
||||
// This seems to be an edge case in the Selenium tests, in which the LNURLP isn't populated.
|
||||
// I have come across it only in the tests and this is supposed to make them happy.
|
||||
if (string.IsNullOrEmpty(lightningFallback))
|
||||
{
|
||||
var serverUrl = new Uri(lnurlInfo.Url[..lnurlInfo.Url.IndexOf("/i/", StringComparison.InvariantCultureIgnoreCase)]);
|
||||
var uri = new Uri($"{serverUrl}{network.CryptoCode}/lnurl/pay/i/{invoiceResponse.Id}");
|
||||
lightningFallback = LNURL.LNURL.EncodeUri(uri, "payRequest", true).ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(lightningFallback))
|
||||
|
|
|
@ -41,6 +41,8 @@ namespace BTCPayServer.Payments.Lightning
|
|||
|
||||
public override PaymentType PaymentType => PaymentTypes.LightningLike;
|
||||
|
||||
private const string UriScheme = "lightning:";
|
||||
|
||||
public IOptions<LightningNetworkOptions> Options { get; }
|
||||
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(
|
||||
|
@ -107,12 +109,13 @@ namespace BTCPayServer.Payments.Lightning
|
|||
StoreBlob storeBlob, IPaymentMethod paymentMethod)
|
||||
{
|
||||
var paymentMethodId = paymentMethod.GetId();
|
||||
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>(model.CryptoCode);
|
||||
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
var lnurl = cryptoInfo.PaymentUrls?.AdditionalData["LNURLP"].ToObject<string>();
|
||||
model.PaymentMethodName = GetPaymentMethodName(network);
|
||||
model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls?.AdditionalData["LNURLP"].ToObject<string>();
|
||||
model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl;
|
||||
model.BtcAddress = model.InvoiceBitcoinUrl;
|
||||
model.BtcAddress = lnurl?.Replace(UriScheme, "");
|
||||
model.InvoiceBitcoinUrl = lnurl;
|
||||
model.InvoiceBitcoinUrlQR = lnurl?.ToUpperInvariant().Replace(UriScheme.ToUpperInvariant(), UriScheme);
|
||||
model.PeerInfo = ((LNURLPayPaymentMethodDetails)paymentMethod.GetPaymentMethodDetails()).NodeInfo;
|
||||
if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC")
|
||||
{
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<template id="bitcoin-method-checkout-template">
|
||||
@await Component.InvokeAsync("UiExtensionPoint", new {location = "checkout-v2-bitcoin-pre-content", model = Model})
|
||||
<div class="payment-box">
|
||||
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-clipboard="model.btcAddress" data-clipboard-confirm-element="QR_Text_@Model.PaymentMethodId">
|
||||
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-qr-value="model.invoiceBitcoinUrlQR" :data-clipboard="model.btcAddress" data-clipboard-confirm-element="QR_Text_@Model.PaymentMethodId">
|
||||
<div>
|
||||
<qrcode :value="model.invoiceBitcoinUrlQR" tag="div" :options="qrOptions" />
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<template id="lightning-method-checkout-template">
|
||||
<div class="payment-box">
|
||||
@await Component.InvokeAsync("UiExtensionPoint" , new { location="checkout-v2-lightning-pre-content", model = Model})
|
||||
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-clipboard="model.btcAddress" data-clipboard-confirm-element="QR_Text_@Model.PaymentMethodId">
|
||||
<div v-if="model.invoiceBitcoinUrlQR" class="qr-container" :data-qr-value="model.invoiceBitcoinUrlQR" :data-clipboard="model.btcAddress" data-clipboard-confirm-element="QR_Text_@Model.PaymentMethodId">
|
||||
<div>
|
||||
<qrcode :value="model.invoiceBitcoinUrlQR" tag="div" :options="qrOptions" />
|
||||
</div>
|
||||
|
@ -12,10 +12,10 @@
|
|||
</div>
|
||||
<div v-if="model.btcAddress" class="input-group mt-3">
|
||||
<div class="form-floating">
|
||||
<input id="Address_@Model.PaymentMethodId" class="form-control-plaintext" readonly="readonly" :value="model.btcAddress">
|
||||
<label for="Address_@Model.PaymentMethodId" v-t="'lightning'"></label>
|
||||
<input id="Lightning_@Model.PaymentMethodId" class="form-control-plaintext" readonly="readonly" :value="model.btcAddress">
|
||||
<label for="Lightning_@Model.PaymentMethodId" v-t="'lightning'"></label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link" data-clipboard-target="#Address_@Model.PaymentMethodId" :data-clipboard-confirm="$t('copy_confirm')" v-t="'copy'"></button>
|
||||
<button type="button" class="btn btn-link" data-clipboard-target="#Lightning_@Model.PaymentMethodId" :data-clipboard-confirm="$t('copy_confirm')" v-t="'copy'"></button>
|
||||
</div>
|
||||
<a v-if="model.invoiceBitcoinUrl" class="btn btn-primary rounded-pill w-100 mt-4" target="_top"
|
||||
:href="model.invoiceBitcoinUrl" v-t="'pay_in_wallet'"></a>
|
||||
|
|
Loading…
Add table
Reference in a new issue