Merge pull request #4677 from dennisreimann/fix-4663

This commit is contained in:
Andrew Camilleri
2023-02-22 12:30:16 +01:00
committed by GitHub
8 changed files with 126 additions and 113 deletions

View File

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

View File

@@ -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<IActionResult> GetLNURLForPullPayment(string cryptoCode, string pullPaymentId, string pr, CancellationToken cancellationToken)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(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<LightningSupportedPaymentMethod>()
@@ -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<string[]> 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)
{

View File

@@ -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
{

View File

@@ -77,7 +77,6 @@ namespace BTCPayServer.Data
this IEnumerable<IPayoutHandler> payoutHandlers, StoreData storeData)
{
return (await Task.WhenAll(payoutHandlers.Select(handler => handler.GetSupportedPaymentMethods(storeData)))).SelectMany(ids => ids).ToList();
}
}
}

View File

@@ -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<LNURLPayRequest.LNURLPayRequestCallbackResponse>();
bolt11 = res.Pr;
}
else
{
var res = JObject.Parse(response).ToObject<LNUrlStatusResponse>();
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);

View File

@@ -2,19 +2,23 @@
@using BTCPayServer.Abstractions.TagHelpers
<template id="lnurl-withdraw-template">
<template v-if="display">
<button v-if="isV2" class="btn btn-secondary rounded-pill w-100 mt-4" type="button"
:disabled="scanning || submitting" v-on:click="startScan" :id="btnId"
:class="{ 'loading': scanning || submitting, 'text-secondary': !supported }">{{btnText}}</button>
<bp-loading-button v-else>
<button class="action-button" style="margin: 0 45px;width:calc(100% - 90px) !important"
:disabled="scanning || submitting" v-on:click="startScan" :id="btnId"
:class="{ 'loading': scanning || submitting, 'action-button': supported, 'btn btn-text w-100': !supported }">
<span class="button-text">{{btnText}}</span>
<div class="loader-wrapper">
@await Html.PartialAsync("~/Views/UIInvoice/Checkout-Spinner.cshtml")
</div>
</button>
</bp-loading-button>
<div class="mt-4">
<p id="CheatSuccessMessage" class="alert alert-success text-break" v-if="successMessage" v-text="successMessage"></p>
<p id="CheatErrorMessage" class="alert alert-danger text-break" v-if="errorMessage" v-text="errorMessage"></p>
<button v-if="isV2" class="btn btn-secondary rounded-pill w-100" type="button"
:disabled="scanning || submitting" v-on:click="startScan" :id="btnId"
:class="{ 'loading': scanning || submitting, 'text-secondary': !supported }">{{btnText}}</button>
<bp-loading-button v-else>
<button class="action-button" style="margin: 0 45px;width:calc(100% - 90px) !important"
:disabled="scanning || submitting" v-on:click="startScan" :id="btnId"
:class="{ 'loading': scanning || submitting, 'action-button': supported, 'btn btn-text w-100': !supported }">
<span class="button-text">{{btnText}}</span>
<div class="loader-wrapper">
@await Html.PartialAsync("~/Views/UIInvoice/Checkout-Spinner.cshtml")
</div>
</button>
</bp-loading-button>
</div>
</template>
</template>
<script type="text/javascript">
@@ -61,7 +65,9 @@ Vue.component("lnurl-withdraw-checkout", {
scanning: false,
submitting: false,
readerAbortController: null,
amount: 0
amount: 0,
successMessage: null,
errorMessage: null
}
},
methods: {
@@ -72,13 +78,13 @@ Vue.component("lnurl-withdraw-checkout", {
}
if (this.model.isUnsetTopUp) {
const amountStr = prompt("How many sats do you want to pay?")
if (amountStr){
if (amountStr) {
try {
this.amount = parseInt(amountStr)
} catch {
alert("Please provide a valid number amount in sats");
}
}else{
} else {
return;
}
}
@@ -87,9 +93,9 @@ Vue.component("lnurl-withdraw-checkout", {
self.submitting = false;
self.scanning = true;
if (!this.supported) {
const result = prompt("Enter LNURL withdraw");
const result = prompt("Enter LNURL-Withdraw");
if (result) {
self.sendData.bind(self)(result);
await self.sendData.bind(self)(result);
return;
}
self.scanning = false;
@@ -103,7 +109,7 @@ Vue.component("lnurl-withdraw-checkout", {
self.readerAbortController.abort()
})
ndef.addEventListener('reading', ({message, serialNumber}) => {
ndef.addEventListener('reading', async ({message, serialNumber}) => {
//Decode NDEF data from tag
const record = message.records[0]
const textDecoder = new TextDecoder('utf-8')
@@ -111,38 +117,38 @@ Vue.component("lnurl-withdraw-checkout", {
//User feedback, show loader icon
self.scanning = false;
self.sendData.bind(self)(lnurl);
await self.sendData.bind(self)(lnurl);
})
} catch(e) {
} catch (e) {
self.scanning = false;
self.submitting = false;
}
},
sendData: function (lnurl) {
sendData: async function (lnurl) {
this.submitting = true;
//Post LNURLW data to server
var xhr = new XMLHttpRequest()
xhr.open('POST', this.url, true)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(JSON.stringify({lnurl, invoiceId: this.model.invoiceId, amount: this.amount}))
const self = this;
//User feedback, reset on failure
xhr.onload = function () {
if (xhr.readyState === xhr.DONE) {
console.log(xhr.response);
console.log(xhr.responseText);
self.scanning = false;
self.submitting = false;
if(self.readerAbortController) {
self.readerAbortController.abort()
}
if(xhr.response){
alert(xhr.response)
}
this.successMessage = null;
this.errorMessage = null;
// Post LNURL-Withdraw data to server
const body = JSON.stringify({ lnurl, invoiceId: this.model.invoiceId, amount: this.amount })
const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body }
const response = await fetch(this.url, opts)
// Handle response
try {
const result = await response.text()
if (response.ok) {
this.successMessage = result;
} else {
this.errorMessage = result;
}
} catch (error) {
this.errorMessage = error;
}
this.scanning = false;
this.submitting = false;
if (this.readerAbortController) {
this.readerAbortController.abort()
}
}
}

View File

@@ -14,7 +14,7 @@
<qrcode :value="currentFragment" :options="qrOptions"></qrcode>
</component>
</div>
<ul class="nav justify-content-center mt-4 mb-3" v-if="modes && Object.keys(modes).length > 1">
<ul class="nav btcpay-pills justify-content-center mt-4 mb-3" v-if="modes && Object.keys(modes).length > 1">
<li class="nav-item" v-for="(item, key) in modes">
<a class="btcpay-pill" :class="{ 'active': key === mode }" href="#" v-on:click="mode = key">{{item.title}}</a>
</li>

View File

@@ -305,7 +305,7 @@
},
"enableForStandardInvoices": {
"type": "boolean",
"description": "Whether to allow this payment method to also be used for standard invoices and not just topup invoices."
"description": "Whether to allow this payment method to also be used for standard invoices and not just top-up invoices."
},
"lud12Enabled": {
"type": "boolean",