From 33f1f956f7182db3a237d33cf8f7a20b796d4234 Mon Sep 17 00:00:00 2001 From: Kukks Date: Tue, 20 Jun 2023 13:21:00 +0200 Subject: [PATCH 01/10] Do not create an invoice on every lnurl query --- BTCPayServer.Client/Models/InvoiceData.cs | 1 + .../Controllers/UIInvoiceController.cs | 7 +- BTCPayServer/Controllers/UILNURLController.cs | 224 +++++++++++++----- .../Services/Invoices/InvoiceEntity.cs | 2 + 4 files changed, 178 insertions(+), 56 deletions(-) diff --git a/BTCPayServer.Client/Models/InvoiceData.cs b/BTCPayServer.Client/Models/InvoiceData.cs index 9e81621720..8650c75036 100644 --- a/BTCPayServer.Client/Models/InvoiceData.cs +++ b/BTCPayServer.Client/Models/InvoiceData.cs @@ -87,6 +87,7 @@ public class CheckoutOptions public string DefaultLanguage { get; set; } public CheckoutType? CheckoutType { get; set; } public bool? LazyPaymentMethods { get; set; } + public string? ExplicitRateScript { get; set; } } } public class InvoiceData : InvoiceDataBase diff --git a/BTCPayServer/Controllers/UIInvoiceController.cs b/BTCPayServer/Controllers/UIInvoiceController.cs index 21eb946314..153399e3ea 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.cs @@ -238,6 +238,10 @@ public async Task CreateInvoiceCoreRaw(CreateInvoiceRequest invoi { var storeBlob = store.GetStoreBlob(); var entity = _InvoiceRepository.CreateNewInvoice(); + if (!string.IsNullOrEmpty(invoice.Checkout.ExplicitRateScript) && RateRules.TryParse(invoice.Checkout.ExplicitRateScript, out var explicitRateRule) && explicitRateRule is not null) + { + entity.ExplicitRateRules = explicitRateRule; + } entity.ServerUrl = serverUrl; entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration); entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration); @@ -315,7 +319,6 @@ internal async Task CreateInvoiceCoreRaw(InvoiceEntity entity, St } entity.Status = InvoiceStatusLegacy.New; HashSet currencyPairsToFetch = new HashSet(); - var rules = storeBlob.GetRateRules(_NetworkProvider); var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any() if (invoicePaymentMethodFilter != null) { @@ -337,7 +340,7 @@ internal async Task CreateInvoiceCoreRaw(InvoiceEntity entity, St } } - var rateRules = storeBlob.GetRateRules(_NetworkProvider); + var rateRules = entity.ExplicitRateRules?? storeBlob.GetRateRules(_NetworkProvider); var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken); var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair); diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index c6f17639a1..d23fdaf30d 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -23,6 +23,7 @@ using BTCPayServer.Plugins.Crowdfund; using BTCPayServer.Plugins.PointOfSale; using BTCPayServer.Plugins.PointOfSale.Models; +using BTCPayServer.Rating; using BTCPayServer.Services; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; @@ -34,6 +35,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Caching.Memory; using NBitcoin; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -60,6 +62,8 @@ public class UILNURLController : Controller private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings; private readonly IPluginHookService _pluginHookService; private readonly InvoiceActivator _invoiceActivator; + private readonly IMemoryCache _memoryCache; + private readonly RateFetcher _rateFetcher; public UILNURLController(InvoiceRepository invoiceRepository, EventAggregator eventAggregator, @@ -74,7 +78,9 @@ public UILNURLController(InvoiceRepository invoiceRepository, PullPaymentHostedService pullPaymentHostedService, BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings, IPluginHookService pluginHookService, - InvoiceActivator invoiceActivator) + InvoiceActivator invoiceActivator, + IMemoryCache memoryCache, + RateFetcher rateFetcher) { _invoiceRepository = invoiceRepository; _eventAggregator = eventAggregator; @@ -90,6 +96,8 @@ public UILNURLController(InvoiceRepository invoiceRepository, _btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings; _pluginHookService = pluginHookService; _invoiceActivator = invoiceActivator; + _memoryCache = memoryCache; + _rateFetcher = rateFetcher; } [HttpGet("withdraw/pp/{pullPaymentId}")] @@ -318,13 +326,12 @@ public async Task GetLNURLForApp(string cryptoCode, string appId, createInvoice.Metadata = invoiceMetadata.ToJObject(); - return await GetLNURLRequest( - cryptoCode, + return await CreateLNURLRequestWithoutInvoice( new LNURLRequestParams(cryptoCode, store, store.GetStoreBlob(), createInvoice, additionalTags: new List { AppService.GetAppInternalTag(appId) }, - allowOverpay: false); + allowOverpay: false)); } public class EditLightningAddressVM @@ -377,25 +384,17 @@ public async Task ResolveLightningAddress(string username) return NotFound("Unknown username"); var blob = lightningAddressSettings.GetBlob(); - - return await GetLNURLRequest( - "BTC", - store, - store.GetStoreBlob(), - new CreateInvoiceRequest() - { - Currency = blob?.CurrencyCode, - Metadata = blob?.InvoiceMetadata - }, - new LNURLPayRequest() - { - MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null, - MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null, - }, - new Dictionary() - { - { "text/identifier", $"{username}@{Request.Host}" } - }); + return await CreateLNURLRequestWithoutInvoice(new LNURLRequestParams( + "BTC", + store, + store.GetStoreBlob(), + new CreateInvoiceRequest() {Currency = blob?.CurrencyCode, Metadata = blob?.InvoiceMetadata}, + new LNURLPayRequest() + { + MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null, + MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null, + }, + new Dictionary() {{"text/identifier", $"{username}@{Request.Host}"}})); } @@ -414,46 +413,128 @@ public async Task GetLNUrlForStore( var blob = store.GetStoreBlob(); if (!blob.AnyoneCanInvoice) return NotFound("'Anyone can invoice' is turned off"); - return await GetLNURLRequest( - cryptoCode, + return await CreateLNURLRequestWithoutInvoice(new LNURLRequestParams(cryptoCode, store, blob, new CreateInvoiceRequest { Currency = currencyCode - }); + })); } - - private async Task GetLNURLRequest( - string cryptoCode, - Data.StoreData store, - Data.StoreBlob blob, - CreateInvoiceRequest createInvoice, - LNURLPayRequest lnurlRequest = null, - Dictionary lnUrlMetadata = null, - List additionalTags = null, - bool allowOverpay = true) + class LNURLRequestParams + { + public LNURLRequestParams(string cryptoCode, + Data.StoreData store, + Data.StoreBlob blob, + CreateInvoiceRequest createInvoice, + LNURLPayRequest lnurlRequest = null, + Dictionary lnUrlMetadata = null, + List additionalTags = null, + bool allowOverpay = true) + { + CryptoCode = cryptoCode; + Store = store; + Blob = blob; + CreateInvoice = createInvoice; + LNURLRequest = lnurlRequest; + LNURLMetadata = lnUrlMetadata; + AdditionalTags = additionalTags; + AllowOverpay = allowOverpay; + } + public string CryptoCode { get; set; } + public Data.StoreData Store { get; set; } + public Data.StoreBlob Blob { get; set; } + public CreateInvoiceRequest CreateInvoice { get; set; } + public LNURLPayRequest LNURLRequest { get; set; } + public Dictionary LNURLMetadata { get; set; } + public List AdditionalTags { get; set; } + public bool AllowOverpay { get; set; } + } + private async Task CreateLNURLRequestWithoutInvoice(LNURLRequestParams requestParams) { - var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out _); + var pmi = GetLNUrlPaymentMethodId(requestParams.CryptoCode, requestParams.Store, out LNURLPaySupportedPaymentMethod lnUrlMethod); if (pmi is null) return NotFound("LNUrl or LN is disabled"); - InvoiceEntity i; - try - { - createInvoice.Checkout ??= new InvoiceDataBase.CheckoutOptions(); - createInvoice.Checkout.LazyPaymentMethods = false; - createInvoice.Checkout.PaymentMethods = new[] { pmi.ToStringNormalized() }; - i = await _invoiceController.CreateInvoiceCoreRaw(createInvoice, store, Request.GetAbsoluteRoot(), additionalTags); + + var k = Guid.NewGuid(); + + requestParams.LNURLRequest ??= new LNURLPayRequest(); + requestParams.LNURLMetadata ??= new Dictionary(); + + // Set the callback endpoint to trigger invoice generation + requestParams.LNURLRequest.Tag = "payRequest"; + requestParams.LNURLRequest.Callback = new Uri(_linkGenerator.GetUriByAction( + action: nameof(LNURLCallback), + controller: "UILNURL", + values: new { k }, + Request.Scheme, Request.Host, Request.PathBase)); + + + if (!requestParams.LNURLMetadata.ContainsKey("text/plain")) + { + var invMetadata = InvoiceMetadata.FromJObject(requestParams.CreateInvoice.Metadata); + var invoiceDescription = requestParams.Blob.LightningDescriptionTemplate + .Replace("{StoreName}", requestParams.Store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("{ItemDescription}", invMetadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) + .Replace("{OrderId}", invMetadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase); + requestParams.LNURLMetadata.Add("text/plain", invoiceDescription); } - catch (Exception e) + + requestParams.LNURLRequest.CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0; + requestParams.LNURLRequest.Metadata = JsonConvert.SerializeObject(requestParams.LNURLMetadata.Select(kv => new[] { kv.Key, kv.Value })); + // We don't think BTCPay handle well 0 sats payments, just in case make it minimum one sat. + if (requestParams.LNURLRequest.MinSendable is null || requestParams.LNURLRequest.MinSendable < LightMoney.Satoshis(1.0m)) + requestParams.LNURLRequest.MinSendable = LightMoney.Satoshis(1.0m); + + if (requestParams.LNURLRequest.MaxSendable is null) + requestParams.LNURLRequest.MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC); + + if (requestParams.CreateInvoice.Type != InvoiceType.TopUp) { - return this.CreateAPIError(null, e.Message); + LightMoney cAmount; + if (requestParams.CreateInvoice.Currency != pmi.CryptoCode) + { + var rr = requestParams.Store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider); + var rate = _rateFetcher.FetchRates( + new HashSet() + { + new CurrencyPair(pmi.CryptoCode, requestParams.CreateInvoice.Currency) + }, rr, CancellationToken.None).First(); + var rateResult = await rate.Value; + cAmount = LightMoney.FromUnit(rateResult.BidAsk.Bid, LightMoneyUnit.BTC); + requestParams.CreateInvoice.Checkout.ExplicitRateScript = + $"{pmi.CryptoCode}_{requestParams.CreateInvoice.Currency}={rateResult.BidAsk.Bid}"; + } + else + { + cAmount = LightMoney.FromUnit(requestParams.CreateInvoice.Amount.Value!, LightMoneyUnit.BTC); + + } + requestParams.LNURLRequest.MinSendable = cAmount; + + if (!requestParams.AllowOverpay) + requestParams.LNURLRequest.MaxSendable = requestParams.LNURLRequest.MinSendable; } - lnurlRequest = await CreateLNUrlRequestFromInvoice(cryptoCode, i, store, blob, lnurlRequest, lnUrlMetadata, allowOverpay); - return lnurlRequest is null ? NotFound() : Ok(lnurlRequest); - } + + requestParams.LNURLRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", requestParams.LNURLRequest) as LNURLPayRequest; + + + + + + + var invoiceParamsCacheEntryOptions = new MemoryCacheEntryOptions() + .SetAbsoluteExpiration(TimeSpan.FromMinutes(5)); // Set an appropriate expiration time + + // Store the invoice parameters in the cache + _memoryCache.Set($"{nameof(UILNURLController)}:{k}", requestParams, invoiceParamsCacheEntryOptions); + + return Ok(requestParams.LNURLRequest); + } + + private async Task CreateLNUrlRequestFromInvoice( string cryptoCode, InvoiceEntity i, @@ -545,19 +626,54 @@ PaymentMethodId GetLNUrlPaymentMethodId(string cryptoCode, Data.StoreData store, return pmi; } + + [HttpGet("pay/{k}")] + public async Task LNURLCallback(string k, long? amount = null, string comment = null) + { + if (!_memoryCache.TryGetValue($"{nameof(UILNURLController)}:{k}", out var lnurlReq) || lnurlReq is null) + return NotFound(); + if (amount is null) + { + return Ok(lnurlReq.LNURLRequest); + } + + InvoiceEntity i; + try + { + var pmi = GetLNUrlPaymentMethodId(lnurlReq.CryptoCode, lnurlReq.Store, out _); + lnurlReq.CreateInvoice.Checkout ??= new InvoiceDataBase.CheckoutOptions(); + lnurlReq.CreateInvoice.Checkout.LazyPaymentMethods = false; + lnurlReq.CreateInvoice.Checkout.PaymentMethods = new[] { pmi.ToStringNormalized() }; + i = await _invoiceController.CreateInvoiceCoreRaw(lnurlReq.CreateInvoice, lnurlReq.Store, Request.GetAbsoluteRoot(), lnurlReq.AdditionalTags); + return await GetLNURLForInvoice(i, lnurlReq.CryptoCode, amount, comment); + } + catch (Exception e) + { + return this.CreateAPIError(null, e.Message); + } + } + [HttpGet("pay/i/{invoiceId}")] [EnableCors(CorsPolicies.All)] [IgnoreAntiforgeryToken] public async Task GetLNURLForInvoice(string invoiceId, string cryptoCode, [FromQuery] long? amount = null, string comment = null) { + + var i = await _invoiceRepository.GetInvoice(invoiceId, true); + return await GetLNURLForInvoice(i, cryptoCode, amount, comment); + } + + [NonAction] + private async Task GetLNURLForInvoice(InvoiceEntity i, string cryptoCode, + [FromQuery] long? amount = null, string comment = null) + { var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); if (network is null || !network.SupportLightning) { return NotFound(); } - var i = await _invoiceRepository.GetInvoice(invoiceId, true); if (i is null) return NotFound(); @@ -578,7 +694,7 @@ public async Task GetLNURLForInvoice(string invoiceId, string cry { if (!await _invoiceActivator.ActivateInvoicePaymentMethod(pmi, i, store)) return NotFound(); - i = await _invoiceRepository.GetInvoice(invoiceId, true); + i = await _invoiceRepository.GetInvoice(i.Id, true); lightningPaymentMethod = i.GetPaymentMethod(pmi); paymentMethodDetails = lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails; } @@ -613,7 +729,7 @@ public async Task GetLNURLForInvoice(string invoiceId, string cry Url = _linkGenerator.GetUriByAction( nameof(UIInvoiceController.InvoiceReceipt), "UIInvoice", - new { invoiceId }, + new { i.Id }, Request.Scheme, Request.Host, Request.PathBase) @@ -693,8 +809,8 @@ public async Task GetLNURLForInvoice(string invoiceId, string cry if (updatePaymentMethod) { lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails); - await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod); - _eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(invoiceId, paymentMethodDetails, pmi)); + await _invoiceRepository.UpdateInvoicePaymentMethod(i.Id, lightningPaymentMethod); + _eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(i.Id, paymentMethodDetails, pmi)); } return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 36d3d7e19d..b7fef1c717 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -10,6 +10,7 @@ using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Lightning; +using BTCPayServer.Rating; using NBitcoin; using NBitcoin.DataEncoders; using NBitpayClient; @@ -465,6 +466,7 @@ private Uri FillPlaceholdersUri(string v) [JsonConverter(typeof(StringEnumConverter))] public CheckoutType? CheckoutType { get; set; } public bool LazyPaymentMethods { get; set; } + public RateRules? ExplicitRateRules { get; set; } public bool IsExpired() { From 623d7e3056beba474ac01c5ce442c08b700ba01a Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 21 Jun 2023 09:17:13 +0200 Subject: [PATCH 02/10] reduce code --- BTCPayServer/Controllers/UILNURLController.cs | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index d23fdaf30d..b74ad7d8f2 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -234,12 +234,8 @@ await _pullPaymentHostedService.Cancel( [HttpGet("pay/app/{appId}/{itemCode}")] public async Task GetLNURLForApp(string cryptoCode, string appId, string itemCode = null) { - var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); - if (network is null || !network.SupportLightning) - { - return NotFound(); - } - + if (!NetworkSupportsLightning(cryptoCode, out _)) + return null; var app = await _appService.GetApp(appId, null, true); if (app is null) { @@ -520,11 +516,6 @@ private async Task CreateLNURLRequestWithoutInvoice(LNURLRequestP requestParams.LNURLRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", requestParams.LNURLRequest) as LNURLPayRequest; - - - - - var invoiceParamsCacheEntryOptions = new MemoryCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromMinutes(5)); // Set an appropriate expiration time @@ -608,8 +599,7 @@ private async Task CreateLNUrlRequestFromInvoice( PaymentMethodId GetLNUrlPaymentMethodId(string cryptoCode, Data.StoreData store, out LNURLPaySupportedPaymentMethod lnUrlSettings) { lnUrlSettings = null; - var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); - if (network is null || !network.SupportLightning) + if (!NetworkSupportsLightning(cryptoCode, out _)) return null; var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay); var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); @@ -668,11 +658,8 @@ public async Task GetLNURLForInvoice(string invoiceId, string cry private async Task GetLNURLForInvoice(InvoiceEntity i, string cryptoCode, [FromQuery] long? amount = null, string comment = null) { - var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); - if (network is null || !network.SupportLightning) - { - return NotFound(); - } + if (!NetworkSupportsLightning(cryptoCode, out var network)) + return null; if (i is null) return NotFound(); @@ -829,6 +816,12 @@ private async Task GetLNURLForInvoice(InvoiceEntity i, string cry }); } + private bool NetworkSupportsLightning(string cryptoCode, out BTCPayNetwork network) + { + network = _btcPayNetworkProvider.GetNetwork(cryptoCode); + return !(network is null || !network.SupportLightning); + } + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [HttpGet("~/stores/{storeId}/plugins/lightning-address")] From b021039d870c98b767d9c980e1856296d800ddfe Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 21 Jun 2023 09:56:02 +0200 Subject: [PATCH 03/10] cleanup --- BTCPayServer/Controllers/UILNURLController.cs | 85 ++++++++++--------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index b74ad7d8f2..01138ab3dd 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -41,6 +41,7 @@ using Newtonsoft.Json.Linq; using LightningAddressData = BTCPayServer.Data.LightningAddressData; using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest; +using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer { @@ -252,6 +253,10 @@ public async Task GetLNURLForApp(string cryptoCode, string appId, { return NotFound(); } + + var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out var lnurlPM); + if (pmi is null) + return NotFound("LNUrl or LN is disabled"); ViewPointOfSaleViewModel.Item[] items; string currencyCode; @@ -276,9 +281,6 @@ public async Task GetLNURLForApp(string cryptoCode, string appId, ViewPointOfSaleViewModel.Item item = null; if (!string.IsNullOrEmpty(itemCode)) { - var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out _); - if (pmi is null) - return NotFound("LNUrl or LN is disabled"); var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode); item = items.FirstOrDefault(item1 => item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) || @@ -322,12 +324,12 @@ public async Task GetLNURLForApp(string cryptoCode, string appId, createInvoice.Metadata = invoiceMetadata.ToJObject(); - return await CreateLNURLRequestWithoutInvoice( new LNURLRequestParams(cryptoCode, - store, - store.GetStoreBlob(), + return await CreateLNURLRequestWithoutInvoice( new LNURLRequestParams( + store.Id, + pmi, createInvoice, additionalTags: new List { AppService.GetAppInternalTag(appId) }, - allowOverpay: false)); + allowOverpay: false), store, store.GetStoreBlob(), lnurlPM); } public class EditLightningAddressVM @@ -379,18 +381,20 @@ public async Task ResolveLightningAddress(string username) if (store is null) return NotFound("Unknown username"); + var pmi = GetLNUrlPaymentMethodId("BTC", store, out var lnurlPaymentMethod); + if (pmi is null) + return NotFound("LNUrl or LN is disabled"); var blob = lightningAddressSettings.GetBlob(); return await CreateLNURLRequestWithoutInvoice(new LNURLRequestParams( - "BTC", - store, - store.GetStoreBlob(), + store.Id, + pmi, new CreateInvoiceRequest() {Currency = blob?.CurrencyCode, Metadata = blob?.InvoiceMetadata}, new LNURLPayRequest() { MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null, MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null, }, - new Dictionary() {{"text/identifier", $"{username}@{Request.Host}"}})); + new Dictionary() {{"text/identifier", $"{username}@{Request.Host}"}}), store, store.GetStoreBlob(), lnurlPaymentMethod); } @@ -406,53 +410,51 @@ public async Task GetLNUrlForStore( if (store is null) return NotFound(); + var pmi = GetLNUrlPaymentMethodId("BTC", store, out var lnurlPaymentMethod); + if (pmi is null) + return NotFound("LNUrl or LN is disabled"); var blob = store.GetStoreBlob(); if (!blob.AnyoneCanInvoice) return NotFound("'Anyone can invoice' is turned off"); - return await CreateLNURLRequestWithoutInvoice(new LNURLRequestParams(cryptoCode, - store, - blob, + return await CreateLNURLRequestWithoutInvoice(new LNURLRequestParams( + storeId, + pmi, new CreateInvoiceRequest { Currency = currencyCode - })); + }), store, blob, lnurlPaymentMethod); } class LNURLRequestParams { - public LNURLRequestParams(string cryptoCode, - Data.StoreData store, - Data.StoreBlob blob, + public LNURLRequestParams( + string storeId, + PaymentMethodId paymentMethodId, CreateInvoiceRequest createInvoice, LNURLPayRequest lnurlRequest = null, Dictionary lnUrlMetadata = null, List additionalTags = null, bool allowOverpay = true) { - CryptoCode = cryptoCode; - Store = store; - Blob = blob; + StoreId = storeId; + PaymentMethodId = paymentMethodId; CreateInvoice = createInvoice; LNURLRequest = lnurlRequest; LNURLMetadata = lnUrlMetadata; AdditionalTags = additionalTags; AllowOverpay = allowOverpay; } - public string CryptoCode { get; set; } - public Data.StoreData Store { get; set; } - public Data.StoreBlob Blob { get; set; } + + public string StoreId { get; } + public PaymentMethodId PaymentMethodId { get; set; } public CreateInvoiceRequest CreateInvoice { get; set; } public LNURLPayRequest LNURLRequest { get; set; } public Dictionary LNURLMetadata { get; set; } public List AdditionalTags { get; set; } public bool AllowOverpay { get; set; } } - private async Task CreateLNURLRequestWithoutInvoice(LNURLRequestParams requestParams) + private async Task CreateLNURLRequestWithoutInvoice(LNURLRequestParams requestParams, + StoreData store, StoreBlob storeBlob, LNURLPaySupportedPaymentMethod lnurlPaySupportedPaymentMethod) { - var pmi = GetLNUrlPaymentMethodId(requestParams.CryptoCode, requestParams.Store, out LNURLPaySupportedPaymentMethod lnUrlMethod); - if (pmi is null) - return NotFound("LNUrl or LN is disabled"); - - var k = Guid.NewGuid(); requestParams.LNURLRequest ??= new LNURLPayRequest(); @@ -470,14 +472,14 @@ private async Task CreateLNURLRequestWithoutInvoice(LNURLRequestP if (!requestParams.LNURLMetadata.ContainsKey("text/plain")) { var invMetadata = InvoiceMetadata.FromJObject(requestParams.CreateInvoice.Metadata); - var invoiceDescription = requestParams.Blob.LightningDescriptionTemplate - .Replace("{StoreName}", requestParams.Store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) + var invoiceDescription = storeBlob.LightningDescriptionTemplate + .Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) .Replace("{ItemDescription}", invMetadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) .Replace("{OrderId}", invMetadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase); requestParams.LNURLMetadata.Add("text/plain", invoiceDescription); } - requestParams.LNURLRequest.CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0; + requestParams.LNURLRequest.CommentAllowed = lnurlPaySupportedPaymentMethod.LUD12Enabled ? 2000 : 0; requestParams.LNURLRequest.Metadata = JsonConvert.SerializeObject(requestParams.LNURLMetadata.Select(kv => new[] { kv.Key, kv.Value })); // We don't think BTCPay handle well 0 sats payments, just in case make it minimum one sat. if (requestParams.LNURLRequest.MinSendable is null || requestParams.LNURLRequest.MinSendable < LightMoney.Satoshis(1.0m)) @@ -489,18 +491,18 @@ private async Task CreateLNURLRequestWithoutInvoice(LNURLRequestP if (requestParams.CreateInvoice.Type != InvoiceType.TopUp) { LightMoney cAmount; - if (requestParams.CreateInvoice.Currency != pmi.CryptoCode) + if (requestParams.CreateInvoice.Currency != requestParams.PaymentMethodId.CryptoCode) { - var rr = requestParams.Store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider); + var rr = storeBlob.GetRateRules(_btcPayNetworkProvider); var rate = _rateFetcher.FetchRates( new HashSet() { - new CurrencyPair(pmi.CryptoCode, requestParams.CreateInvoice.Currency) + new CurrencyPair( requestParams.PaymentMethodId.CryptoCode, requestParams.CreateInvoice.Currency) }, rr, CancellationToken.None).First(); var rateResult = await rate.Value; cAmount = LightMoney.FromUnit(rateResult.BidAsk.Bid, LightMoneyUnit.BTC); requestParams.CreateInvoice.Checkout.ExplicitRateScript = - $"{pmi.CryptoCode}_{requestParams.CreateInvoice.Currency}={rateResult.BidAsk.Bid}"; + $"{ requestParams.PaymentMethodId.CryptoCode}_{requestParams.CreateInvoice.Currency}={rateResult.BidAsk.Bid}"; } else { @@ -630,12 +632,13 @@ public async Task LNURLCallback(string k, long? amount = null, st InvoiceEntity i; try { - var pmi = GetLNUrlPaymentMethodId(lnurlReq.CryptoCode, lnurlReq.Store, out _); + var store = await _storeRepository.FindStore(lnurlReq.StoreId); + lnurlReq.CreateInvoice.Checkout ??= new InvoiceDataBase.CheckoutOptions(); lnurlReq.CreateInvoice.Checkout.LazyPaymentMethods = false; - lnurlReq.CreateInvoice.Checkout.PaymentMethods = new[] { pmi.ToStringNormalized() }; - i = await _invoiceController.CreateInvoiceCoreRaw(lnurlReq.CreateInvoice, lnurlReq.Store, Request.GetAbsoluteRoot(), lnurlReq.AdditionalTags); - return await GetLNURLForInvoice(i, lnurlReq.CryptoCode, amount, comment); + lnurlReq.CreateInvoice.Checkout.PaymentMethods = new[] { lnurlReq.PaymentMethodId.ToStringNormalized() }; + i = await _invoiceController.CreateInvoiceCoreRaw(lnurlReq.CreateInvoice, store, Request.GetAbsoluteRoot(), lnurlReq.AdditionalTags); + return await GetLNURLForInvoice(i, lnurlReq.PaymentMethodId.CryptoCode, amount, comment); } catch (Exception e) { From 7f26a97eab8f7bb5a1f6a8b09ba968d1570fe7e5 Mon Sep 17 00:00:00 2001 From: Kukks Date: Wed, 21 Jun 2023 10:17:46 +0200 Subject: [PATCH 04/10] fixes --- BTCPayServer/Controllers/UILNURLController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index 01138ab3dd..edfcf58343 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -459,19 +459,19 @@ private async Task CreateLNURLRequestWithoutInvoice(LNURLRequestP requestParams.LNURLRequest ??= new LNURLPayRequest(); requestParams.LNURLMetadata ??= new Dictionary(); - + requestParams.CreateInvoice.Currency ??= storeBlob.DefaultCurrency; // Set the callback endpoint to trigger invoice generation requestParams.LNURLRequest.Tag = "payRequest"; requestParams.LNURLRequest.Callback = new Uri(_linkGenerator.GetUriByAction( action: nameof(LNURLCallback), controller: "UILNURL", - values: new { k }, + values: new {requestParams.PaymentMethodId.CryptoCode, k }, Request.Scheme, Request.Host, Request.PathBase)); if (!requestParams.LNURLMetadata.ContainsKey("text/plain")) { - var invMetadata = InvoiceMetadata.FromJObject(requestParams.CreateInvoice.Metadata); + var invMetadata = InvoiceMetadata.FromJObject(requestParams.CreateInvoice.Metadata?? new JObject()); var invoiceDescription = storeBlob.LightningDescriptionTemplate .Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) .Replace("{ItemDescription}", invMetadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase) @@ -488,7 +488,7 @@ private async Task CreateLNURLRequestWithoutInvoice(LNURLRequestP if (requestParams.LNURLRequest.MaxSendable is null) requestParams.LNURLRequest.MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC); - if (requestParams.CreateInvoice.Type != InvoiceType.TopUp) + if (requestParams.CreateInvoice.Type != InvoiceType.TopUp && requestParams.CreateInvoice.Amount is not null) { LightMoney cAmount; if (requestParams.CreateInvoice.Currency != requestParams.PaymentMethodId.CryptoCode) From a512cb90d464f65341faeaa4120f2b81ba18d76e Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Fri, 23 Jun 2023 15:53:52 +0200 Subject: [PATCH 05/10] Fix test and build warnings --- BTCPayServer.Client/Models/InvoiceData.cs | 2 +- BTCPayServer.Tests/SeleniumTests.cs | 72 ++++++++++--------- .../Services/Invoices/InvoiceEntity.cs | 6 +- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/BTCPayServer.Client/Models/InvoiceData.cs b/BTCPayServer.Client/Models/InvoiceData.cs index 8650c75036..7710700185 100644 --- a/BTCPayServer.Client/Models/InvoiceData.cs +++ b/BTCPayServer.Client/Models/InvoiceData.cs @@ -87,7 +87,7 @@ public class CheckoutOptions public string DefaultLanguage { get; set; } public CheckoutType? CheckoutType { get; set; } public bool? LazyPaymentMethods { get; set; } - public string? ExplicitRateScript { get; set; } + public string ExplicitRateScript { get; set; } } } public class InvoiceData : InvoiceDataBase diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index b0ce5df902..fab4c3af46 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -2361,11 +2361,11 @@ public async Task CanUseLNAddress() var lnaddress1 = Guid.NewGuid().ToString(); s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress1); s.Driver.FindElement(By.CssSelector("button[value='add']")).Click(); - s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success); + s.FindAlertMessage(); s.Driver.ToggleCollapse("AddAddress"); - var lnaddress2 = "EUR" + Guid.NewGuid().ToString(); + var lnaddress2 = "EUR" + Guid.NewGuid(); s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress2); lnaddress2 = lnaddress2.ToLowerInvariant(); @@ -2375,9 +2375,10 @@ public async Task CanUseLNAddress() s.Driver.FindElement(By.Id("Add_Max")).SendKeys("10"); s.Driver.FindElement(By.Id("Add_InvoiceMetadata")).SendKeys("{\"test\":\"lol\"}"); s.Driver.FindElement(By.CssSelector("button[value='add']")).Click(); - s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success); + s.FindAlertMessage(); var addresses = s.Driver.FindElements(By.ClassName("lightning-address-value")); + var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}"; Assert.Equal(2, addresses.Count); foreach (IWebElement webElement in addresses) @@ -2386,52 +2387,36 @@ public async Task CanUseLNAddress() //cannot test this directly as https is not supported on our e2e tests // var request = await LNURL.LNURL.FetchPayRequestViaInternetIdentifier(value, new HttpClient()); - var lnurl = new Uri(LNURL.LNURL.ExtractUriFromInternetIdentifier(value).ToString() - .Replace("https", "http")); - var request = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurl, new HttpClient()); + var lnurl = new Uri(LNURL.LNURL.ExtractUriFromInternetIdentifier(value).ToString().Replace("https", "http")); + var request = (LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurl, new HttpClient()); var m = request.ParsedMetadata.ToDictionary(o => o.Key, o => o.Value); switch (value) { - case { } v when v.StartsWith(lnaddress2): - Assert.StartsWith(lnaddress2 + "@", m["text/identifier"]); - lnaddress2 = m["text/identifier"]; - Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi)); - Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi)); - break; - - case { } v when v.StartsWith(lnaddress1): - Assert.StartsWith(lnaddress1 + "@", m["text/identifier"]); + case not null when value.Equals($"{lnaddress1}{emailSuffix}"): lnaddress1 = m["text/identifier"]; Assert.Equal(1, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi)); Assert.Equal(6.12m, request.MaxSendable.ToDecimal(LightMoneyUnit.BTC)); break; + + case not null when value.Equals($"{lnaddress2}{emailSuffix}"): + lnaddress2 = m["text/identifier"]; + Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi)); + Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi)); + break; + default: - Assert.False(true, "Should have matched"); + Assert.False(true, "Should have matched one of the Lightning addresses"); break; } } + + // Check that no BTCPay invoice got generated on initial LNURL request var repo = s.Server.PayTester.GetService(); - var invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } }); - Assert.Equal(2, invoices.Length); - var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}"; - foreach (var i in invoices) - { - var lightningPaymentMethod = i.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay)); - var paymentMethodDetails = - lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails; - Assert.Contains( - paymentMethodDetails.ConsumedLightningAddress, - new[] { lnaddress1, lnaddress2 }); - - if (paymentMethodDetails.ConsumedLightningAddress == lnaddress2) - { - Assert.Equal("lol", i.Metadata.AdditionalData["test"].Value()); - } - } + var invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } }); + Assert.Empty(invoices); var lnUsername = lnaddress1.Split('@')[0]; - LNURLPayRequest req; using (var resp = await s.Server.PayTester.HttpClient.GetAsync($"/.well-known/lnurlp/{lnUsername}")) { @@ -2484,6 +2469,25 @@ public async Task CanUseLNAddress() Assert.NotNull(succ.Pr); Assert.Equal(new LightMoney(2001), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount); } + + // Again, check the invoices + invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } }); + Assert.Equal(2, invoices.Length); + + foreach (var i in invoices) + { + var lightningPaymentMethod = i.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay)); + var paymentMethodDetails = + lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails; + Assert.Contains( + paymentMethodDetails.ConsumedLightningAddress, + new[] { lnaddress1, lnaddress2 }); + + if (paymentMethodDetails.ConsumedLightningAddress == lnaddress2) + { + Assert.Equal("lol", i.Metadata.AdditionalData["test"].Value()); + } + } } [Fact] diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index fc03b2a810..fdd990aea6 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -466,7 +466,7 @@ private Uri FillPlaceholdersUri(string v) [JsonConverter(typeof(StringEnumConverter))] public CheckoutType? CheckoutType { get; set; } public bool LazyPaymentMethods { get; set; } - public RateRules? ExplicitRateRules { get; set; } + public RateRules ExplicitRateRules { get; set; } public bool IsExpired() { @@ -492,13 +492,13 @@ public InvoiceResponse EntityToDTO() Currency = Currency, PaymentSubtotals = new Dictionary(), PaymentTotals = new Dictionary(), - SupportedTransactionCurrencies = new Dictionary(), + SupportedTransactionCurrencies = new Dictionary(), Addresses = new Dictionary(), PaymentCodes = new Dictionary(), ExchangeRates = new Dictionary>() }; - dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id; + dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id={Id}"; dto.CryptoInfo = new List(); dto.MinerFees = new Dictionary(); foreach (var info in this.GetPaymentMethods()) From d4828f8d0edb0878ed1cca5cbafc061b3a46e2f0 Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Fri, 23 Jun 2023 17:53:01 +0200 Subject: [PATCH 06/10] Adapt controller and partially fix tests --- BTCPayServer.Tests/SeleniumTests.cs | 24 ++++++++++++------- BTCPayServer/Controllers/UILNURLController.cs | 19 +++++++++------ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index fab4c3af46..5fe21e575a 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -2409,14 +2409,8 @@ public async Task CanUseLNAddress() break; } } - - // Check that no BTCPay invoice got generated on initial LNURL request - var repo = s.Server.PayTester.GetService(); - var invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } }); - Assert.Empty(invoices); var lnUsername = lnaddress1.Split('@')[0]; - LNURLPayRequest req; using (var resp = await s.Server.PayTester.HttpClient.GetAsync($"/.well-known/lnurlp/{lnUsername}")) { @@ -2428,30 +2422,42 @@ public async Task CanUseLNAddress() Assert.Equal(new LightMoney(1000), req.MinSendable); Assert.Equal(LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC), req.MaxSendable); } + lnUsername = lnaddress2.Split('@')[0]; using (var resp = await s.Server.PayTester.HttpClient.GetAsync($"/.well-known/lnurlp/{lnUsername}")) { var str = await resp.Content.ReadAsStringAsync(); req = JsonConvert.DeserializeObject(str); + Assert.Contains(req.ParsedMetadata, m => m.Key == "text/identifier" && m.Value == lnaddress2); + Assert.Contains(req.ParsedMetadata, m => m.Key == "text/plain" && m.Value.StartsWith("Paid to")); Assert.Equal(new LightMoney(2000), req.MinSendable); Assert.Equal(new LightMoney(10_000), req.MaxSendable); } + // Check if we can get the same payrequest through the callback using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback)) { var str = await resp.Content.ReadAsStringAsync(); req = JsonConvert.DeserializeObject(str); + Assert.Contains(req.ParsedMetadata, m => m.Key == "text/identifier" && m.Value == lnaddress2); + Assert.Contains(req.ParsedMetadata, m => m.Key == "text/plain" && m.Value.StartsWith("Paid to")); Assert.Equal(new LightMoney(2000), req.MinSendable); Assert.Equal(new LightMoney(10_000), req.MaxSendable); } // Can we ask for invoice? (Should fail, below minSpendable) - using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=1999")) + using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=999")) { var str = await resp.Content.ReadAsStringAsync(); var err = JsonConvert.DeserializeObject(str); Assert.Equal("Amount is out of bounds.", err.Reason); } + + // Check that no BTCPay invoice got generated on initial LN Address requests + var repo = s.Server.PayTester.GetService(); + var invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } }); + Assert.Empty(invoices); + // Can we ask for invoice? using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=2000")) { @@ -2460,6 +2466,8 @@ public async Task CanUseLNAddress() Assert.NotNull(succ.Pr); Assert.Equal(new LightMoney(2000), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount); } + invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } }); + Assert.Single(invoices); // Can we change comment? using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=2001")) @@ -2469,8 +2477,6 @@ public async Task CanUseLNAddress() Assert.NotNull(succ.Pr); Assert.Equal(new LightMoney(2001), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount); } - - // Again, check the invoices invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } }); Assert.Equal(2, invoices.Length); diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index edfcf58343..fd8833f6ce 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -384,20 +384,20 @@ public async Task ResolveLightningAddress(string username) var pmi = GetLNUrlPaymentMethodId("BTC", store, out var lnurlPaymentMethod); if (pmi is null) return NotFound("LNUrl or LN is disabled"); + var blob = lightningAddressSettings.GetBlob(); return await CreateLNURLRequestWithoutInvoice(new LNURLRequestParams( store.Id, pmi, - new CreateInvoiceRequest() {Currency = blob?.CurrencyCode, Metadata = blob?.InvoiceMetadata}, - new LNURLPayRequest() + new CreateInvoiceRequest {Currency = blob?.CurrencyCode, Metadata = blob?.InvoiceMetadata}, + new LNURLPayRequest { MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null, MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null, }, - new Dictionary() {{"text/identifier", $"{username}@{Request.Host}"}}), store, store.GetStoreBlob(), lnurlPaymentMethod); + new Dictionary {{"text/identifier", $"{username}@{Request.Host}"}}), store,store.GetStoreBlob(), lnurlPaymentMethod); } - [HttpGet("pay")] [EnableCors(CorsPolicies.All)] [IgnoreAntiforgeryToken] @@ -548,7 +548,7 @@ private async Task CreateLNUrlRequestFromInvoice( return null; var paymentMethodDetails = (LNURLPayPaymentMethodDetails)pm.GetPaymentMethodDetails(); bool updatePaymentMethodDetails = false; - if (lnUrlMetadata?.TryGetValue("text/identifier", out var lnAddress) is true && lnAddress is not null) + if (lnUrlMetadata.TryGetValue("text/identifier", out var lnAddress) && lnAddress is not null) { paymentMethodDetails.ConsumedLightningAddress = lnAddress; updatePaymentMethodDetails = true; @@ -628,6 +628,11 @@ public async Task LNURLCallback(string k, long? amount = null, st { return Ok(lnurlReq.LNURLRequest); } + + var lnurlPayRequest = lnurlReq.LNURLRequest; + var amt = new LightMoney(amount.Value); + if (amt < lnurlPayRequest.MinSendable || amt > lnurlPayRequest.MaxSendable) + return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Amount is out of bounds." }); InvoiceEntity i; try @@ -705,7 +710,7 @@ private async Task GetLNURLForInvoice(InvoiceEntity i, string cry return Ok(lnurlPayRequest); var amt = new LightMoney(amount.Value); - if (amt < lnurlPayRequest.MinSendable || amount > lnurlPayRequest.MaxSendable) + if (amt < lnurlPayRequest.MinSendable || amt > lnurlPayRequest.MaxSendable) return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Amount is out of bounds." }); LNURLPayRequest.LNURLPayRequestCallbackResponse.ILNURLPayRequestSuccessAction successAction = null; @@ -787,7 +792,7 @@ private async Task GetLNURLForInvoice(InvoiceEntity i, string cry string.IsNullOrEmpty(ex.Message) ? "" : $": {ex.Message}") }); } - + paymentMethodDetails.BOLT11 = invoice.BOLT11; paymentMethodDetails.PaymentHash = string.IsNullOrEmpty(invoice.PaymentHash) ? null : uint256.Parse(invoice.PaymentHash); paymentMethodDetails.Preimage = string.IsNullOrEmpty(invoice.Preimage) ? null : uint256.Parse(invoice.Preimage); From f1e5f1b759f8be52de54709dfab70f8612a87491 Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 23 Jun 2023 16:28:14 +0200 Subject: [PATCH 07/10] try fix --- BTCPayServer.Tests/SeleniumTests.cs | 16 +++++++++++++++- BTCPayServer/Controllers/UILNURLController.cs | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 5fe21e575a..d2dc6d6ee8 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -2381,6 +2381,8 @@ public async Task CanUseLNAddress() var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}"; Assert.Equal(2, addresses.Count); + LNURLPayRequest.LNURLPayRequestCallbackResponse lnAddressOneResponse = null; + LNURLPayRequest.LNURLPayRequestCallbackResponse lnAddressTwoResponse = null; foreach (IWebElement webElement in addresses) { var value = webElement.GetAttribute("value"); @@ -2392,10 +2394,22 @@ public async Task CanUseLNAddress() var m = request.ParsedMetadata.ToDictionary(o => o.Key, o => o.Value); switch (value) { - case not null when value.Equals($"{lnaddress1}{emailSuffix}"): + case { } v when v.StartsWith(lnaddress2): + Assert.StartsWith(lnaddress2 + "@", m["text/identifier"]); + lnaddress2 = m["text/identifier"]; + Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi)); + Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi)); + lnAddressTwoResponse = await request.SendRequest(request.MinSendable, ((BTCPayNetwork)s.Server.DefaultNetwork).NBitcoinNetwork, + new HttpClient()); + break; + + case { } v when v.StartsWith(lnaddress1): + Assert.StartsWith(lnaddress1 + "@", m["text/identifier"]); lnaddress1 = m["text/identifier"]; Assert.Equal(1, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi)); Assert.Equal(6.12m, request.MaxSendable.ToDecimal(LightMoneyUnit.BTC)); + lnAddressOneResponse = await request.SendRequest(request.MinSendable, ((BTCPayNetwork)s.Server.DefaultNetwork).NBitcoinNetwork, + new HttpClient()); break; case not null when value.Equals($"{lnaddress2}{emailSuffix}"): diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index fd8833f6ce..56f63728a9 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -643,6 +643,7 @@ public async Task LNURLCallback(string k, long? amount = null, st lnurlReq.CreateInvoice.Checkout.LazyPaymentMethods = false; lnurlReq.CreateInvoice.Checkout.PaymentMethods = new[] { lnurlReq.PaymentMethodId.ToStringNormalized() }; i = await _invoiceController.CreateInvoiceCoreRaw(lnurlReq.CreateInvoice, store, Request.GetAbsoluteRoot(), lnurlReq.AdditionalTags); + await CreateLNUrlRequestFromInvoice(lnurlReq.PaymentMethodId.CryptoCode,i, store,store.GetStoreBlob(),lnurlReq.LNURLRequest, lnurlReq.LNURLMetadata, lnurlReq.AllowOverpay); return await GetLNURLForInvoice(i, lnurlReq.PaymentMethodId.CryptoCode, amount, comment); } catch (Exception e) From 86a44b6f1eea5fbe39df7ff81581b07e799c7d5e Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 26 Jun 2023 10:21:11 +0200 Subject: [PATCH 08/10] make sure i is not stale --- BTCPayServer/Controllers/UILNURLController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index 56f63728a9..e873a67d2b 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -594,6 +594,7 @@ private async Task CreateLNUrlRequestFromInvoice( { pm.SetPaymentMethodDetails(paymentMethodDetails); await _invoiceRepository.UpdateInvoicePaymentMethod(i.Id, pm); + i.SetPaymentMethod(pm); } return lnurlRequest; } From d99bd28386ef785d706fc7c3f4aacc2c1b655731 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 26 Jun 2023 10:40:20 +0200 Subject: [PATCH 09/10] prevent explicit rate if modify invoice is not available --- .../GreenField/GreenfieldInvoiceController.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs index 5b6521dc16..0f4bfd0077 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs @@ -41,6 +41,7 @@ public class GreenfieldInvoiceController : Controller private readonly RateFetcher _rateProvider; private readonly InvoiceActivator _invoiceActivator; private readonly ApplicationDbContextFactory _dbContextFactory; + private readonly IAuthorizationService _authorizationService; public LanguageService LanguageService { get; } @@ -48,7 +49,7 @@ public GreenfieldInvoiceController(UIInvoiceController invoiceController, Invoic LinkGenerator linkGenerator, LanguageService languageService, BTCPayNetworkProvider btcPayNetworkProvider, CurrencyNameTable currencyNameTable, RateFetcher rateProvider, InvoiceActivator invoiceActivator, - PullPaymentHostedService pullPaymentService, ApplicationDbContextFactory dbContextFactory) + PullPaymentHostedService pullPaymentService, ApplicationDbContextFactory dbContextFactory, IAuthorizationService authorizationService) { _invoiceController = invoiceController; _invoiceRepository = invoiceRepository; @@ -59,6 +60,7 @@ public GreenfieldInvoiceController(UIInvoiceController invoiceController, Invoic _invoiceActivator = invoiceActivator; _pullPaymentService = pullPaymentService; _dbContextFactory = dbContextFactory; + _authorizationService = authorizationService; LanguageService = languageService; } @@ -188,6 +190,12 @@ public async Task CreateInvoice(string storeId, CreateInvoiceRequ { ModelState.AddModelError(nameof(request.Amount), $"The amount should less than {GreenfieldConstants.MaxAmount}."); } + if (!string.IsNullOrEmpty(request.Checkout.ExplicitRateScript) && + !(await _authorizationService.AuthorizeAsync(User, Policies.CanModifyInvoices)).Succeeded) + { + request.AddModelError(invoiceRequest => invoiceRequest.Checkout.ExplicitRateScript, + $"You are not authorized to use explicit rate script (missing {Policies.CanModifyInvoices} permission)", this); + } request.Checkout ??= new CreateInvoiceRequest.CheckoutOptions(); if (request.Checkout.PaymentMethods?.Any() is true) { From af44d6aeacd720ece43051a34daa51c11c56d3e1 Mon Sep 17 00:00:00 2001 From: Kukks Date: Mon, 26 Jun 2023 10:48:27 +0200 Subject: [PATCH 10/10] Partially Revert "Adapt controller and partially fix tests" This reverts commit d4828f8d0edb0878ed1cca5cbafc061b3a46e2f0. --- BTCPayServer.Tests/SeleniumTests.cs | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index d2dc6d6ee8..d33d8b460f 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -2423,8 +2423,14 @@ public async Task CanUseLNAddress() break; } } + + // Check that no BTCPay invoice got generated on initial LNURL request + var repo = s.Server.PayTester.GetService(); + var invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } }); + Assert.Empty(invoices); var lnUsername = lnaddress1.Split('@')[0]; + LNURLPayRequest req; using (var resp = await s.Server.PayTester.HttpClient.GetAsync($"/.well-known/lnurlp/{lnUsername}")) { @@ -2436,42 +2442,30 @@ public async Task CanUseLNAddress() Assert.Equal(new LightMoney(1000), req.MinSendable); Assert.Equal(LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC), req.MaxSendable); } - lnUsername = lnaddress2.Split('@')[0]; using (var resp = await s.Server.PayTester.HttpClient.GetAsync($"/.well-known/lnurlp/{lnUsername}")) { var str = await resp.Content.ReadAsStringAsync(); req = JsonConvert.DeserializeObject(str); - Assert.Contains(req.ParsedMetadata, m => m.Key == "text/identifier" && m.Value == lnaddress2); - Assert.Contains(req.ParsedMetadata, m => m.Key == "text/plain" && m.Value.StartsWith("Paid to")); Assert.Equal(new LightMoney(2000), req.MinSendable); Assert.Equal(new LightMoney(10_000), req.MaxSendable); } - // Check if we can get the same payrequest through the callback using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback)) { var str = await resp.Content.ReadAsStringAsync(); req = JsonConvert.DeserializeObject(str); - Assert.Contains(req.ParsedMetadata, m => m.Key == "text/identifier" && m.Value == lnaddress2); - Assert.Contains(req.ParsedMetadata, m => m.Key == "text/plain" && m.Value.StartsWith("Paid to")); Assert.Equal(new LightMoney(2000), req.MinSendable); Assert.Equal(new LightMoney(10_000), req.MaxSendable); } // Can we ask for invoice? (Should fail, below minSpendable) - using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=999")) + using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=1999")) { var str = await resp.Content.ReadAsStringAsync(); var err = JsonConvert.DeserializeObject(str); Assert.Equal("Amount is out of bounds.", err.Reason); } - - // Check that no BTCPay invoice got generated on initial LN Address requests - var repo = s.Server.PayTester.GetService(); - var invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } }); - Assert.Empty(invoices); - // Can we ask for invoice? using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=2000")) { @@ -2480,8 +2474,6 @@ public async Task CanUseLNAddress() Assert.NotNull(succ.Pr); Assert.Equal(new LightMoney(2000), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount); } - invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } }); - Assert.Single(invoices); // Can we change comment? using (var resp = await s.Server.PayTester.HttpClient.GetAsync(req.Callback + "?amount=2001")) @@ -2491,6 +2483,8 @@ public async Task CanUseLNAddress() Assert.NotNull(succ.Pr); Assert.Equal(new LightMoney(2001), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount); } + + // Again, check the invoices invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } }); Assert.Equal(2, invoices.Length);