diff --git a/BTCPayServer.Tests/Checkoutv2Tests.cs b/BTCPayServer.Tests/Checkoutv2Tests.cs index 282046a14..dc101f6c3 100644 --- a/BTCPayServer.Tests/Checkoutv2Tests.cs +++ b/BTCPayServer.Tests/Checkoutv2Tests.cs @@ -231,7 +231,7 @@ namespace BTCPayServer.Tests Assert.True(s.Driver.FindElement(By.Id("LNURLEnabled")).Selected); Assert.True(s.Driver.FindElement(By.Id("LNURLStandardInvoiceEnabled")).Selected); - // BIP21 with topup invoice + // BIP21 with top-up invoice invoiceId = s.CreateInvoice(amount: null); s.GoToInvoiceCheckout(invoiceId); s.Driver.WaitUntilAvailable(By.Id("Checkout-v2")); @@ -250,7 +250,7 @@ namespace BTCPayServer.Tests Assert.StartsWith($"bitcoin:{address.ToUpperInvariant()}?lightning=LNURL", qrValue); s.Driver.FindElement(By.Id("PayByLNURL")); - // Expiry message should not show amount for topup invoice + // Expiry message should not show amount for top-up invoice expirySeconds = s.Driver.FindElement(By.Id("ExpirySeconds")); expirySeconds.Clear(); expirySeconds.SendKeys("5"); diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index f55bc7fc8..6e3dd634d 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -27,6 +27,7 @@ using BTCPayServer.Services.Stores; using LNURL; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using NBitcoin; @@ -84,7 +85,6 @@ namespace BTCPayServer [HttpGet("withdraw/pp/{pullPaymentId}")] public async Task GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, CancellationToken cancellationToken) { - var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); if (network is null || !network.SupportLightning) { @@ -107,7 +107,7 @@ namespace BTCPayServer var progress = _pullPaymentHostedService.CalculatePullPaymentProgress(pp, DateTimeOffset.UtcNow); var remaining = progress.Limit - progress.Completed - progress.Awaiting; - var request = new LNURLWithdrawRequest() + var request = new LNURLWithdrawRequest { MaxWithdrawable = LightMoney.FromUnit(remaining, LightMoneyUnit.BTC), K1 = pullPaymentId, @@ -121,7 +121,7 @@ namespace BTCPayServer Callback = new Uri(Request.GetCurrentUrl()), // It's not `pp.GetBlob().Description` because this would be HTML // and LNUrl UI's doesn't expect HTML there - DefaultDescription = pp.GetBlob().Name ?? String.Empty, + DefaultDescription = pp.GetBlob().Name ?? string.Empty, }; if (pr is null) { @@ -130,11 +130,11 @@ namespace BTCPayServer if (!BOLT11PaymentRequest.TryParse(pr, out var result, network.NBitcoinNetwork) || result is null) { - return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Pr was not a valid BOLT11" }); + return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request was not a valid BOLT11" }); } if (result.MinimumAmount < request.MinWithdrawable || result.MinimumAmount > request.MaxWithdrawable) - return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Pr was not within bounds" }); + return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = $"Payment request was not within bounds ({request.MinWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} - {request.MaxWithdrawable.ToUnit(LightMoneyUnit.Satoshi)} sats)" }); var store = await _storeRepository.FindStore(pp.StoreId); var pm = store!.GetSupportedPaymentMethods(_btcPayNetworkProvider) .OfType() @@ -154,7 +154,7 @@ namespace BTCPayServer }); if (claimResponse.Result != ClaimRequest.ClaimResult.Ok) - return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Pr could not be paid" }); + return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" }); switch (claimResponse.PayoutData.State) { case PayoutState.AwaitingPayment: @@ -169,7 +169,7 @@ namespace BTCPayServer { case PayResult.Ok: case PayResult.Unknown: - await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest() + await _pullPaymentHostedService.MarkPaid(new MarkPayoutRequest { PayoutId = claimResponse.PayoutData.Id, State = claimResponse.PayoutData.State, @@ -185,15 +185,13 @@ namespace BTCPayServer case PayResult.Error: default: await _pullPaymentHostedService.Cancel( - new PullPaymentHostedService.CancelRequest(new string[] - { - claimResponse.PayoutData.Id - }, null)); + new PullPaymentHostedService.CancelRequest(new [] + { claimResponse.PayoutData.Id }, null)); - return Ok(new LNUrlStatusResponse + return BadRequest(new LNUrlStatusResponse { Status = "ERROR", - Reason = payResult.Message + Reason = payResult.Message ?? payResult.Result.ToString() }); } } @@ -208,7 +206,7 @@ namespace BTCPayServer case PayoutState.Completed: return Ok(new LNUrlStatusResponse { Status = "OK" }); case PayoutState.Cancelled: - return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Pr could not be paid" }); + return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Payment request could not be paid" }); } return Ok(request); @@ -514,8 +512,8 @@ namespace BTCPayServer return NotFound(); } - var min = new LightMoney(isTopup ? 1m : accounting.Due.ToUnit(MoneyUnit.Satoshi), - LightMoneyUnit.Satoshi); + var amt = amount.HasValue ? new LightMoney(amount.Value) : null; + var min = new LightMoney(isTopup ? 1m : accounting.Due.ToUnit(MoneyUnit.Satoshi), LightMoneyUnit.Satoshi); var max = isTopup ? LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC) : min; List lnurlMetadata = new(); @@ -533,13 +531,12 @@ namespace BTCPayServer } var metadata = JsonConvert.SerializeObject(lnurlMetadata); - if (amount.HasValue && (amount < min || amount > max)) + if (amt != null && (amt < min || amount > max)) { return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Amount is out of bounds." }); } - + LNURLPayRequest.LNURLPayRequestCallbackResponse.ILNURLPayRequestSuccessAction successAction = null; - if ((i.ReceiptOptions?.Enabled ?? blob.ReceiptOptions.Enabled) is true) { successAction = @@ -547,11 +544,17 @@ namespace BTCPayServer { Tag = "url", Description = "Thank you for your purchase. Here is your receipt", - Url = _linkGenerator.GetUriByAction(HttpContext, "InvoiceReceipt", "UIInvoice", new { invoiceId }) + Url = _linkGenerator.GetUriByAction( + nameof(UIInvoiceController.InvoiceReceipt), + "UIInvoice", + new { invoiceId }, + Request.Scheme, + Request.Host, + Request.PathBase) }; } - if (amount is null) + if (amt is null) { return Ok(new LNURLPayRequest { @@ -564,7 +567,7 @@ namespace BTCPayServer }); } - if (string.IsNullOrEmpty(paymentMethodDetails.BOLT11) || paymentMethodDetails.GeneratedBoltAmount != amount) + if (string.IsNullOrEmpty(paymentMethodDetails.BOLT11) || paymentMethodDetails.GeneratedBoltAmount != amt) { var client = _lightningLikePaymentHandler.CreateLightningClient( @@ -585,7 +588,7 @@ namespace BTCPayServer try { var expiry = i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow; - var param = new CreateInvoiceParams(amount.Value, metadata, expiry) + var param = new CreateInvoiceParams(amt, metadata, expiry) { PrivateRouteHints = blob.LightningPrivateRouteHints, DescriptionHashOnly = true @@ -615,7 +618,7 @@ namespace BTCPayServer paymentMethodDetails.PaymentHash = string.IsNullOrEmpty(invoice.PaymentHash) ? null : uint256.Parse(invoice.PaymentHash); paymentMethodDetails.Preimage = string.IsNullOrEmpty(invoice.Preimage) ? null : uint256.Parse(invoice.Preimage); paymentMethodDetails.InvoiceId = invoice.Id; - paymentMethodDetails.GeneratedBoltAmount = new LightMoney(amount.Value); + paymentMethodDetails.GeneratedBoltAmount = amt; if (lnurlSupportedPaymentMethod.LUD12Enabled) { paymentMethodDetails.ProvidedComment = comment; @@ -635,7 +638,7 @@ namespace BTCPayServer }); } - if (paymentMethodDetails.GeneratedBoltAmount == amount) + if (paymentMethodDetails.GeneratedBoltAmount == amt) { if (lnurlSupportedPaymentMethod.LUD12Enabled && paymentMethodDetails.ProvidedComment != comment) { diff --git a/BTCPayServer/Data/Payouts/LightningLike/LightningLikePayoutHandler.cs b/BTCPayServer/Data/Payouts/LightningLike/LightningLikePayoutHandler.cs index 4f776fbd1..789c63390 100644 --- a/BTCPayServer/Data/Payouts/LightningLike/LightningLikePayoutHandler.cs +++ b/BTCPayServer/Data/Payouts/LightningLike/LightningLikePayoutHandler.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Models; @@ -15,8 +14,6 @@ using LNURL; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NBitcoin; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace BTCPayServer.Data.Payouts.LightningLike { diff --git a/BTCPayServer/Data/Payouts/PayoutExtensions.cs b/BTCPayServer/Data/Payouts/PayoutExtensions.cs index 03f7e65e3..8e5f04a9a 100644 --- a/BTCPayServer/Data/Payouts/PayoutExtensions.cs +++ b/BTCPayServer/Data/Payouts/PayoutExtensions.cs @@ -77,7 +77,6 @@ namespace BTCPayServer.Data this IEnumerable payoutHandlers, StoreData storeData) { return (await Task.WhenAll(payoutHandlers.Select(handler => handler.GetSupportedPaymentMethods(storeData)))).SelectMany(ids => ids).ToList(); - } } } diff --git a/BTCPayServer/Plugins/NFC/NFCController.cs b/BTCPayServer/Plugins/NFC/NFCController.cs index 57f51e062..db43ac429 100644 --- a/BTCPayServer/Plugins/NFC/NFCController.cs +++ b/BTCPayServer/Plugins/NFC/NFCController.cs @@ -1,6 +1,7 @@ using System; using System.Net.Http; using System.Threading.Tasks; +using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Client.Models; using BTCPayServer.Data.Payouts.LightningLike; using BTCPayServer.Lightning; @@ -13,6 +14,7 @@ using LNURL; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NBitcoin; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Plugins.NFC { @@ -21,19 +23,16 @@ namespace BTCPayServer.Plugins.NFC { private readonly IHttpClientFactory _httpClientFactory; private readonly InvoiceRepository _invoiceRepository; - private readonly UILNURLController _uilnurlController; private readonly InvoiceActivator _invoiceActivator; private readonly StoreRepository _storeRepository; public NFCController(IHttpClientFactory httpClientFactory, InvoiceRepository invoiceRepository, - UILNURLController uilnurlController, InvoiceActivator invoiceActivator, StoreRepository storeRepository) { _httpClientFactory = httpClientFactory; _invoiceRepository = invoiceRepository; - _uilnurlController = uilnurlController; _invoiceActivator = invoiceActivator; _storeRepository = storeRepository; } @@ -59,7 +58,7 @@ namespace BTCPayServer.Plugins.NFC if (!methods.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LNURLPay), out var lnurlPaymentMethod) && !methods.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LightningLike), out lnPaymentMethod)) { - return BadRequest("destination for lnurlw was not specified"); + return BadRequest("Destination for lnurlw was not specified"); } Uri uri; @@ -69,7 +68,7 @@ namespace BTCPayServer.Plugins.NFC uri = LNURL.LNURL.Parse(request.Lnurl, out tag); if (uri is null) { - return BadRequest("lnurl was malformed"); + return BadRequest("LNURL was malformed"); } } catch (Exception e) @@ -77,20 +76,28 @@ namespace BTCPayServer.Plugins.NFC return BadRequest(e.Message); } - if (!string.IsNullOrEmpty(tag) && !tag.Equals("withdrawRequest")) { - return BadRequest("lnurl was not lnurl-withdraw"); + return BadRequest("LNURL was not LNURL-Withdraw"); } var httpClient = _httpClientFactory.CreateClient(uri.IsOnion() ? LightningLikePayoutHandler.LightningLikePayoutHandlerOnionNamedClient : LightningLikePayoutHandler.LightningLikePayoutHandlerClearnetNamedClient); - var info = (await - LNURL.LNURL.FetchInformation(uri, "withdrawRequest", httpClient)) as LNURLWithdrawRequest; + LNURLWithdrawRequest info; + try + { + info = await LNURL.LNURL.FetchInformation(uri, tag, httpClient) as LNURLWithdrawRequest; + } + catch (Exception ex) + { + var details = ex.InnerException?.Message ?? ex.Message; + return BadRequest($"Could not fetch info from LNURL-Withdraw: {details}"); + } + if (info?.Callback is null) { - return BadRequest("Could not fetch info from lnurl-withdraw "); + return BadRequest("Could not fetch info from LNURL-Withdraw"); } httpClient = _httpClientFactory.CreateClient(info.Callback.IsOnion() @@ -98,13 +105,9 @@ namespace BTCPayServer.Plugins.NFC : LightningLikePayoutHandler.LightningLikePayoutHandlerClearnetNamedClient); string bolt11 = null; - if (lnPaymentMethod is not null) { - if (lnPaymentMethod.GetPaymentMethodDetails() is LightningLikePaymentMethodDetails - { - Activated: false - } lnPMD) + if (lnPaymentMethod.GetPaymentMethodDetails() is LightningLikePaymentMethodDetails { Activated: false } lnPMD) { var store = await _storeRepository.FindStore(invoice.StoreId); await _invoiceActivator.ActivateInvoicePaymentMethod(lnPaymentMethod.GetId(), invoice, store); @@ -115,17 +118,19 @@ namespace BTCPayServer.Plugins.NFC if (invoice.Type == InvoiceType.TopUp && request.Amount is not null) { due = new LightMoney(request.Amount.Value, LightMoneyUnit.Satoshi); - }else if (invoice.Type == InvoiceType.TopUp) + } + else if (invoice.Type == InvoiceType.TopUp) { - return BadRequest("This is a topup invoice and you need to provide the amount in sats to pay."); + return BadRequest("This is a top-up invoice and you need to provide the amount in sats to pay."); } else { - due = new LightMoney(lnPaymentMethod.Calculate().Due); + due = new LightMoney(lnPaymentMethod.Calculate().Due); } + if (info.MinWithdrawable > due || due > info.MaxWithdrawable) { - return BadRequest("invoice amount is not payable with the lnurl allowed amounts."); + return BadRequest("Invoice amount is not payable with the LNURL allowed amounts."); } if (lnPMD?.Activated is true) @@ -140,36 +145,39 @@ namespace BTCPayServer.Plugins.NFC if (invoice.Type == InvoiceType.TopUp && request.Amount is not null) { due = new Money(request.Amount.Value, MoneyUnit.Satoshi); - }else if (invoice.Type == InvoiceType.TopUp) + } + else if (invoice.Type == InvoiceType.TopUp) { - return BadRequest("This is a topup invoice and you need to provide the amount in sats to pay."); + return BadRequest("This is a top-up invoice and you need to provide the amount in sats to pay."); } else { - due = lnurlPaymentMethod.Calculate().Due; + due = lnurlPaymentMethod.Calculate().Due; } - var response = await _uilnurlController.GetLNURLForInvoice(request.InvoiceId, "BTC", - due.Satoshi); + var amount = LightMoney.Satoshis(due.Satoshi); + var actionPath = Url.Action(nameof(UILNURLController.GetLNURLForInvoice), "UILNURL", + new { invoiceId = request.InvoiceId, cryptoCode = "BTC", amount = amount.MilliSatoshi }); + var url = Request.GetAbsoluteUri(actionPath); + var resp = await httpClient.GetAsync(url); + var response = await resp.Content.ReadAsStringAsync(); - if (response is ObjectResult objectResult) + if (resp.IsSuccessStatusCode) { - switch (objectResult.Value) - { - case LNURLPayRequest.LNURLPayRequestCallbackResponse lnurlPayRequestCallbackResponse: - bolt11 = lnurlPayRequestCallbackResponse.Pr; - break; - case LNUrlStatusResponse lnUrlStatusResponse: - - return BadRequest( - $"Could not fetch bolt11 invoice to pay to: {lnUrlStatusResponse.Reason}"); - } + var res = JObject.Parse(response).ToObject(); + bolt11 = res.Pr; + } + else + { + var res = JObject.Parse(response).ToObject(); + return BadRequest( + $"Could not fetch BOLT11 invoice to pay to: {res.Reason}"); } } if (bolt11 is null) { - return BadRequest("Could not fetch bolt11 invoice to pay to."); + return BadRequest("Could not fetch BOLT11 invoice to pay to."); } var result = await info.SendRequest(bolt11, httpClient); diff --git a/BTCPayServer/Views/Shared/NFC/CheckoutEnd.cshtml b/BTCPayServer/Views/Shared/NFC/CheckoutEnd.cshtml index 2d65278f4..1d05f4560 100644 --- a/BTCPayServer/Views/Shared/NFC/CheckoutEnd.cshtml +++ b/BTCPayServer/Views/Shared/NFC/CheckoutEnd.cshtml @@ -2,19 +2,23 @@ @using BTCPayServer.Abstractions.TagHelpers