From 575b829799b3eb2ac88688fc036dd1076d5cfb92 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Tue, 21 Feb 2023 21:06:36 +0100 Subject: [PATCH 1/2] Fix LNURL-Withdraw payments Fixes comparisons of `long` and `LightMoney`, which did not work, because the `amount` provided was in sats and Lightmoney compares to millisats. Closes #4663. --- BTCPayServer/Controllers/UILNURLController.cs | 53 ++++++----- BTCPayServer/Plugins/NFC/NFCController.cs | 53 ++++++----- .../Views/Shared/NFC/CheckoutEnd.cshtml | 94 ++++++++++--------- 3 files changed, 108 insertions(+), 92 deletions(-) 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/Plugins/NFC/NFCController.cs b/BTCPayServer/Plugins/NFC/NFCController.cs index 57f51e062..a39400d94 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; } @@ -86,8 +85,17 @@ namespace BTCPayServer.Plugins.NFC 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 "); @@ -101,10 +109,7 @@ namespace BTCPayServer.Plugins.NFC 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); @@ -149,27 +154,29 @@ namespace BTCPayServer.Plugins.NFC 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