diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj index 4e0ce9ad9..741cbf576 100644 --- a/BTCPayServer.Client/BTCPayServer.Client.csproj +++ b/BTCPayServer.Client/BTCPayServer.Client.csproj @@ -29,7 +29,7 @@ - + diff --git a/BTCPayServer.Client/BTCPayServerClient.LNURLPayPaymentMethods.cs b/BTCPayServer.Client/BTCPayServerClient.LNURLPayPaymentMethods.cs new file mode 100644 index 000000000..0dd150913 --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.LNURLPayPaymentMethods.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + public virtual async Task> + GetStoreLNURLPayPaymentMethods(string storeId, bool? enabled = null, + CancellationToken token = default) + { + var query = new Dictionary(); + if (enabled != null) + { + query.Add(nameof(enabled), enabled); + } + + var response = + await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay", + query), token); + return await HandleResponse>(response); + } + + public virtual async Task GetStoreLNURLPayPaymentMethod( + string storeId, + string cryptoCode, CancellationToken token = default) + { + var response = + await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}"), token); + return await HandleResponse(response); + } + + public virtual async Task RemoveStoreLNURLPayPaymentMethod(string storeId, + string cryptoCode, CancellationToken token = default) + { + var response = + await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}", + method: HttpMethod.Delete), token); + await HandleResponse(response); + } + + public virtual async Task UpdateStoreLNURLPayPaymentMethod( + string storeId, + string cryptoCode, LNURLPayPaymentMethodData paymentMethod, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}", + bodyPayload: paymentMethod, method: HttpMethod.Put), token); + return await HandleResponse(response); + } + } +} diff --git a/BTCPayServer.Client/Models/LNURLPayPaymentMethodBaseData.cs b/BTCPayServer.Client/Models/LNURLPayPaymentMethodBaseData.cs new file mode 100644 index 000000000..4f69254be --- /dev/null +++ b/BTCPayServer.Client/Models/LNURLPayPaymentMethodBaseData.cs @@ -0,0 +1,14 @@ +namespace BTCPayServer.Client.Models +{ + public class LNURLPayPaymentMethodBaseData + { + public bool UseBech32Scheme { get; set; } + public bool EnableForStandardInvoices { get; set; } + public bool LUD12Enabled { get; set; } + + public LNURLPayPaymentMethodBaseData() + { + + } + } +} diff --git a/BTCPayServer.Client/Models/LNURLPayPaymentMethodData.cs b/BTCPayServer.Client/Models/LNURLPayPaymentMethodData.cs new file mode 100644 index 000000000..c3825d385 --- /dev/null +++ b/BTCPayServer.Client/Models/LNURLPayPaymentMethodData.cs @@ -0,0 +1,27 @@ +namespace BTCPayServer.Client.Models +{ + public class LNURLPayPaymentMethodData: LNURLPayPaymentMethodBaseData + { + /// + /// Whether the payment method is enabled + /// + public bool Enabled { get; set; } + + /// + /// Crypto code of the payment method + /// + public string CryptoCode { get; set; } + + public LNURLPayPaymentMethodData() + { + } + + public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme, bool enableForStandardInvoices) + { + Enabled = enabled; + CryptoCode = cryptoCode; + UseBech32Scheme = useBech32Scheme; + EnableForStandardInvoices = enableForStandardInvoices; + } + } +} diff --git a/BTCPayServer.Data/Data/PayoutData.cs b/BTCPayServer.Data/Data/PayoutData.cs index 16fdb692c..b021a44ec 100644 --- a/BTCPayServer.Data/Data/PayoutData.cs +++ b/BTCPayServer.Data/Data/PayoutData.cs @@ -26,7 +26,6 @@ namespace BTCPayServer.Data public string? Destination { get; set; } #nullable restore - internal static void OnModelCreating(ModelBuilder builder) { builder.Entity() diff --git a/BTCPayServer.Rating/BTCPayServer.Rating.csproj b/BTCPayServer.Rating/BTCPayServer.Rating.csproj index 01f4932d4..c84160324 100644 --- a/BTCPayServer.Rating/BTCPayServer.Rating.csproj +++ b/BTCPayServer.Rating/BTCPayServer.Rating.csproj @@ -7,7 +7,7 @@ - + diff --git a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs index 64901e284..6b127aa13 100644 --- a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs +++ b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs @@ -77,7 +77,7 @@ namespace BTCPayServer.Tests // Get enabled state from overview action StoreViewModel storeModel; - response = await controller.UpdateStore(); + response = controller.UpdateStore(); storeModel = (StoreViewModel)Assert.IsType(response).Model; var lnNode = storeModel.LightningNodes.Find(node => node.CryptoCode == cryptoCode); Assert.NotNull(lnNode); @@ -89,7 +89,7 @@ namespace BTCPayServer.Tests Assert.IsType(response); // Get enabled state from overview action - response = await controller.UpdateStore(); + response = controller.UpdateStore(); storeModel = (StoreViewModel)Assert.IsType(response).Model; var derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode); Assert.NotNull(derivationScheme); @@ -98,7 +98,7 @@ namespace BTCPayServer.Tests // Disable wallet response = controller.SetWalletEnabled(storeId, cryptoCode, false).GetAwaiter().GetResult(); Assert.IsType(response); - response = await controller.UpdateStore(); + response = controller.UpdateStore(); storeModel = (StoreViewModel)Assert.IsType(response).Model; derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode); Assert.NotNull(derivationScheme); diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index 8b9e5463f..1a561d675 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -20,7 +20,7 @@ - + diff --git a/BTCPayServer.Tests/CheckoutUITests.cs b/BTCPayServer.Tests/CheckoutUITests.cs index c9fa293ee..eec46ca20 100644 --- a/BTCPayServer.Tests/CheckoutUITests.cs +++ b/BTCPayServer.Tests/CheckoutUITests.cs @@ -137,10 +137,10 @@ namespace BTCPayServer.Tests s.RegisterNewUser(true); var store = s.CreateNewStore(); s.AddLightningNode(); - s.GoToStore(store.storeId); + s.GoToStore(store.storeId, StoreNavPages.Payment); s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true); s.Driver.FindElement(By.Id("Save")).Click(); - Assert.Contains("Store successfully updated", s.FindAlertMessage().Text); + Assert.Contains("Payment settings successfully updated", s.FindAlertMessage().Text); var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com"); s.GoToInvoiceCheckout(invoiceId); diff --git a/BTCPayServer.Tests/CrowdfundTests.cs b/BTCPayServer.Tests/CrowdfundTests.cs index ffea8b55e..28409aea8 100644 --- a/BTCPayServer.Tests/CrowdfundTests.cs +++ b/BTCPayServer.Tests/CrowdfundTests.cs @@ -165,7 +165,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); - await user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never); + await user.SetNetworkFeeMode(NetworkFeeMode.Never); var apps = user.GetController(); var vm = Assert.IsType(Assert.IsType(apps.CreateApp().Result).Model); vm.Name = "test"; diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 7f35ee1d4..da1646329 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -2042,7 +2042,7 @@ namespace BTCPayServer.Tests void VerifyLightning(Dictionary dictionary) { Assert.True(dictionary.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToStringNormalized(), out var item)); - var lightningNetworkPaymentMethodBaseData =Assert.IsType(item.Data).ToObject(); + var lightningNetworkPaymentMethodBaseData = Assert.IsType(item.Data).ToObject(); Assert.Equal("Internal Node", lightningNetworkPaymentMethodBaseData.ConnectionString); } @@ -2057,7 +2057,7 @@ namespace BTCPayServer.Tests void VerifyOnChain(Dictionary dictionary) { Assert.True(dictionary.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), out var item)); - var paymentMethodBaseData =Assert.IsType(item.Data).ToObject(); + var paymentMethodBaseData = Assert.IsType(item.Data).ToObject(); Assert.Equal(randK, paymentMethodBaseData.DerivationScheme); } @@ -2091,6 +2091,5 @@ namespace BTCPayServer.Tests } - } } diff --git a/BTCPayServer.Tests/PayJoinTests.cs b/BTCPayServer.Tests/PayJoinTests.cs index 95a25704a..2aa1a2ce9 100644 --- a/BTCPayServer.Tests/PayJoinTests.cs +++ b/BTCPayServer.Tests/PayJoinTests.cs @@ -20,6 +20,7 @@ using BTCPayServer.Views.Wallets; using Microsoft.AspNetCore.Http; using NBitcoin; using BTCPayServer.BIP78.Sender; +using BTCPayServer.Views.Stores; using NBitcoin.Payment; using NBitpayClient; using NBXplorer.DerivationStrategy; @@ -301,7 +302,7 @@ namespace BTCPayServer.Tests Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21); s.GoToHome(); - s.GoToStore(receiver.storeId); + s.GoToStore(receiver.storeId, StoreNavPages.Payment); Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected); var sender = s.CreateNewStore(); @@ -570,9 +571,9 @@ namespace BTCPayServer.Tests address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address; tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m)); await notifications.NextEventAsync(); - await bob.ModifyStore(s => s.PayJoinEnabled = true); + await bob.ModifyPayment(p => p.PayJoinEnabled = true); var invoice = bob.BitPay.CreateInvoice( - new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true }); + new Invoice { Price = 0.1m, Currency = "BTC", FullNotifications = true }); var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21, tester.ExplorerClient.Network.NBitcoinNetwork); diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 19c286143..de3bc15e1 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -109,7 +109,7 @@ namespace BTCPayServer.Tests { await RegisterAsync(isAdmin); await CreateStoreAsync(); - var store = this.GetController(); + var store = GetController(); var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant); Assert.IsType(await store.RequestPairing(pairingCode.ToString())); await store.Pair(pairingCode.ToString(), StoreId); @@ -127,19 +127,19 @@ namespace BTCPayServer.Tests public async Task SetNetworkFeeMode(NetworkFeeMode mode) { - await ModifyStore(store => + await ModifyPayment(payment => { - store.NetworkFeeMode = mode; + payment.NetworkFeeMode = mode; }); } - public async Task ModifyStore(Action modify) + public async Task ModifyPayment(Action modify) { var storeController = GetController(); - var response = await storeController.UpdateStore(); - StoreViewModel store = (StoreViewModel)((ViewResult)response).Model; - modify(store); - storeController.UpdateStore(store).GetAwaiter().GetResult(); + var response = await storeController.Payment(); + PaymentViewModel payment = (PaymentViewModel)((ViewResult)response).Model; + modify(payment); + await storeController.Payment(payment); } public T GetController(bool setImplicitStore = true) where T : Controller @@ -190,7 +190,7 @@ namespace BTCPayServer.Tests public Task EnablePayJoin() { - return ModifyStore(s => s.PayJoinEnabled = true); + return ModifyPayment(p => p.PayJoinEnabled = true); } public GenerateWalletResponse GenerateWalletResponseV { get; set; } @@ -240,23 +240,26 @@ namespace BTCPayServer.Tests public bool IsAdmin { get; internal set; } - public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true) + public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true, Action setViewModel = null) { - RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult(); + RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant, setViewModel: setViewModel).GetAwaiter().GetResult(); } - public Task RegisterLightningNodeAsync(string cryptoCode, bool isMerchant = true, string storeId = null) + public Task RegisterLightningNodeAsync(string cryptoCode, bool isMerchant = true, string storeId = null, Action setViewModel = null) { - return RegisterLightningNodeAsync(cryptoCode, null, isMerchant, storeId); + return RegisterLightningNodeAsync(cryptoCode, null, isMerchant, storeId, setViewModel); } - public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType? connectionType, bool isMerchant = true, string storeId = null) + public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType? connectionType, bool isMerchant = true, string storeId = null, Action setViewModel = null) { var storeController = GetController(); var connectionString = parent.GetLightningConnectionString(connectionType, isMerchant); var nodeType = connectionString == LightningSupportedPaymentMethod.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom; + var vm = new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true }; + if (setViewModel != null) + setViewModel(vm); await storeController.SetupLightningNode(storeId ?? StoreId, - new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true }, "save", cryptoCode); + vm, "save", cryptoCode); if (storeController.ModelState.ErrorCount != 0) Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage); } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 55c9d0286..892c05589 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -24,6 +24,7 @@ using BTCPayServer.Fido2.Models; using BTCPayServer.HostedServices; using BTCPayServer.Hosting; using BTCPayServer.Lightning; +using BTCPayServer.Lightning.CLightning; using BTCPayServer.Models; using BTCPayServer.Models.AccountViewModels; using BTCPayServer.Models.AppViewModels; @@ -818,11 +819,11 @@ namespace BTCPayServer.Tests // Set tolerance to 50% var stores = user.GetController(); - var response = await stores.UpdateStore(); - var vm = Assert.IsType(Assert.IsType(response).Model); + var response = await stores.Payment(); + var vm = Assert.IsType(Assert.IsType(response).Model); Assert.Equal(0.0, vm.PaymentTolerance); vm.PaymentTolerance = 50.0; - Assert.IsType(stores.UpdateStore(vm).Result); + Assert.IsType(stores.Payment(vm).Result); var invoice = user.BitPay.CreateInvoice( new Invoice() @@ -996,8 +997,7 @@ namespace BTCPayServer.Tests Assert.Equal(4, tor.Services.Length); } - - + [Fact(Timeout = 60 * 2 * 1000)] [Trait("Integration", "Integration")] [Trait("Lightning", "Lightning")] @@ -1012,7 +1012,7 @@ namespace BTCPayServer.Tests await user.RegisterDerivationSchemeAsync("BTC"); await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning); await user.SetNetworkFeeMode(NetworkFeeMode.Never); - await user.ModifyStore(model => model.SpeedPolicy = SpeedPolicy.HighSpeed); + await user.ModifyPayment(p => p.SpeedPolicy = SpeedPolicy.HighSpeed); var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC")); await tester.WaitForEvent(async () => { @@ -1042,14 +1042,22 @@ namespace BTCPayServer.Tests Assert.Contains(fetchedInvoice.Status, new[] { InvoiceStatusLegacy.Complete, InvoiceStatusLegacy.Confirmed }); Assert.Equal(InvoiceExceptionStatus.None, fetchedInvoice.ExceptionStatus); - Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice "); - evt = await tester.WaitForEvent(async () => + //BTCPay will attempt to cancel previous bolt11 invoices so that there are less weird edge case scenarios + Logs.Tester.LogInformation($"Attempting to pay invoice {invoice.Id} original full amount bolt11 invoice "); + await Assert.ThrowsAsync(async () => { await tester.SendLightningPaymentAsync(invoice); - }, evt => evt.InvoiceId == invoice.Id); - Assert.Equal(evt.InvoiceId, invoice.Id); - fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId); - Assert.Equal(3, fetchedInvoice.Payments.Count); + }); + + //NOTE: Eclair does not support cancelling invoice so the below test case would make sense for it + // Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice "); + // evt = await tester.WaitForEvent(async () => + // { + // await tester.SendLightningPaymentAsync(invoice); + // }, evt => evt.InvoiceId == invoice.Id); + // Assert.Equal(evt.InvoiceId, invoice.Id); + // fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId); + // Assert.Equal(3, fetchedInvoice.Payments.Count); } [Fact(Timeout = 60 * 2 * 1000)] @@ -1065,7 +1073,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(true); var storeController = user.GetController(); - var storeResponse = await storeController.UpdateStore(); + var storeResponse = storeController.UpdateStore(); Assert.IsType(storeResponse); Assert.IsType(await storeController.SetupLightningNode(user.StoreId, "BTC")); @@ -1089,7 +1097,7 @@ namespace BTCPayServer.Tests new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri }, "save", "BTC").GetAwaiter().GetResult()); - storeResponse = await storeController.UpdateStore(); + storeResponse = storeController.UpdateStore(); var storeVm = Assert.IsType(Assert .IsType(storeResponse).Model); @@ -1205,7 +1213,7 @@ namespace BTCPayServer.Tests var acc = tester.NewAccount(); acc.GrantAccess(); acc.RegisterDerivationScheme("BTC"); - await acc.ModifyStore(s => s.SpeedPolicy = SpeedPolicy.LowSpeed); + await acc.ModifyPayment(p => p.SpeedPolicy = SpeedPolicy.LowSpeed); var invoice = acc.BitPay.CreateInvoice(new Invoice { Price = 5.0m, @@ -2032,7 +2040,7 @@ namespace BTCPayServer.Tests }); Assert.Equal(404, (int)response.StatusCode); - await user.ModifyStore(s => s.AnyoneCanCreateInvoice = true); + await user.ModifyPayment(p => p.AnyoneCanCreateInvoice = true); Logs.Tester.LogInformation("Bad store with anyone can create invoice = 403"); response = await tester.PayTester.HttpClient.SendAsync( @@ -2306,7 +2314,7 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(true); user.RegisterDerivationScheme("BTC"); - await user.ModifyStore(s => + await user.ModifyPayment(s => { Assert.Equal("USD", s.DefaultCurrency); s.DefaultCurrency = "EUR"; @@ -2357,7 +2365,7 @@ namespace BTCPayServer.Tests // We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice var vm = Assert.IsType(Assert .IsType(user.GetController().CheckoutExperience()).Model); - Assert.Equal(2, vm.PaymentMethodCriteria.Count); + Assert.Equal(3, vm.PaymentMethodCriteria.Count); var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString())); Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod); criteria.Value = "5 USD"; @@ -2448,12 +2456,12 @@ namespace BTCPayServer.Tests Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR); // enable unified QR code in settings - var vm = Assert.IsType(Assert - .IsType(await user.GetController().UpdateStore()).Model + var vm = Assert.IsType(Assert + .IsType(await user.GetController().Payment()).Model ); vm.OnChainWithLnInvoiceFallback = true; Assert.IsType( - user.GetController().UpdateStore(vm).Result + user.GetController().Payment(vm).Result ); // validate that QR code now has both onchain and offchain payment urls @@ -2470,7 +2478,7 @@ namespace BTCPayServer.Tests Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split); // Fallback lightning invoice should be uppercase inside the QR code. - var lightningFallback = paymentMethodSecond.InvoiceBitcoinUrlQR.Split(new string[] { "&lightning=" }, StringSplitOptions.None)[1]; + var lightningFallback = paymentMethodSecond.InvoiceBitcoinUrlQR.Split(new [] { "&lightning=" }, StringSplitOptions.None)[1]; Assert.True(lightningFallback.ToUpperInvariant() == lightningFallback); } } @@ -2488,10 +2496,8 @@ namespace BTCPayServer.Tests var user = tester.NewAccount(); user.GrantAccess(true); user.RegisterLightningNode("BTC", LightningConnectionType.Charge); - var vm = Assert.IsType(Assert - .IsType(user.GetController().CheckoutExperience()).Model); - Assert.Single(vm.PaymentMethodCriteria); - var criteria = vm.PaymentMethodCriteria.First(); + var vm = user.GetController().CheckoutExperience().AssertViewModel(); + var criteria = Assert.Single(vm.PaymentMethodCriteria); Assert.Equal(new PaymentMethodId("BTC", LightningPaymentType.Instance).ToString(), criteria.PaymentMethod); criteria.Value = "2 USD"; criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan; @@ -2499,18 +2505,42 @@ namespace BTCPayServer.Tests .Result); var invoice = user.BitPay.CreateInvoice( - new Invoice() + new Invoice { Price = 1.5m, - Currency = "USD", - PosData = "posData", - OrderId = "orderId", - ItemDesc = "Some description", - FullNotifications = true + Currency = "USD" }, Facade.Merchant); Assert.Single(invoice.CryptoInfo); Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType); + + // Activating LNUrl, we should still have only 1 payment criteria that can be set. + user.RegisterLightningNode("BTC", LightningConnectionType.Charge, setViewModel: vm => + { + vm.LNURLEnabled = true; + vm.LNURLStandardInvoiceEnabled = true; + }); + vm = user.GetController().CheckoutExperience().AssertViewModel(); + criteria = Assert.Single(vm.PaymentMethodCriteria); + Assert.Equal(new PaymentMethodId("BTC", LightningPaymentType.Instance).ToString(), criteria.PaymentMethod); + Assert.IsType(user.GetController().CheckoutExperience(vm).Result); + + // However, creating an invoice should show LNURL + invoice = user.BitPay.CreateInvoice( + new Invoice + { + Price = 1.5m, + Currency = "USD" + }, Facade.Merchant); + Assert.Equal(2, invoice.CryptoInfo.Length); + + // Make sure this throw: Since BOLT11 and LN Url share the same criteria, there should be no payment method available + Assert.Throws(() => user.BitPay.CreateInvoice( + new Invoice + { + Price = 2.5m, + Currency = "USD" + }, Facade.Merchant)); } } diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index 649a83256..800f81083 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using NBitcoin; using NBXplorer.Models; -using YamlDotNet.Core.Tokens; using InvoiceData = BTCPayServer.Client.Models.InvoiceData; using Language = BTCPayServer.Client.Models.Language; using NotificationData = BTCPayServer.Client.Models.NotificationData; @@ -37,6 +36,7 @@ namespace BTCPayServer.Controllers.GreenField private readonly StoreOnChainPaymentMethodsController _chainPaymentMethodsController; private readonly StoreOnChainWalletsController _storeOnChainWalletsController; private readonly StoreLightningNetworkPaymentMethodsController _storeLightningNetworkPaymentMethodsController; + private readonly StoreLNURLPayPaymentMethodsController _storeLnurlPayPaymentMethodsController; private readonly HealthController _healthController; private readonly GreenFieldPaymentRequestsController _paymentRequestController; private readonly ApiKeysController _apiKeysController; @@ -58,6 +58,7 @@ namespace BTCPayServer.Controllers.GreenField StoreOnChainPaymentMethodsController chainPaymentMethodsController, StoreOnChainWalletsController storeOnChainWalletsController, StoreLightningNetworkPaymentMethodsController storeLightningNetworkPaymentMethodsController, + StoreLNURLPayPaymentMethodsController storeLnurlPayPaymentMethodsController, HealthController healthController, GreenFieldPaymentRequestsController paymentRequestController, ApiKeysController apiKeysController, @@ -79,6 +80,7 @@ namespace BTCPayServer.Controllers.GreenField _chainPaymentMethodsController = chainPaymentMethodsController; _storeOnChainWalletsController = storeOnChainWalletsController; _storeLightningNetworkPaymentMethodsController = storeLightningNetworkPaymentMethodsController; + _storeLnurlPayPaymentMethodsController = storeLnurlPayPaymentMethodsController; _healthController = healthController; _paymentRequestController = paymentRequestController; _apiKeysController = apiKeysController; @@ -141,6 +143,7 @@ namespace BTCPayServer.Controllers.GreenField _storeLightningNodeApiController, _internalLightningNodeApiController, _storeLightningNetworkPaymentMethodsController, + _storeLnurlPayPaymentMethodsController, _greenFieldInvoiceController, _greenFieldServerInfoController, _storeWebhooksController, @@ -165,6 +168,7 @@ namespace BTCPayServer.Controllers.GreenField private readonly StoreLightningNodeApiController _storeLightningNodeApiController; private readonly InternalLightningNodeApiController _lightningNodeApiController; private readonly StoreLightningNetworkPaymentMethodsController _storeLightningNetworkPaymentMethodsController; + private readonly StoreLNURLPayPaymentMethodsController _storeLnurlPayPaymentMethodsController; private readonly GreenFieldInvoiceController _greenFieldInvoiceController; private readonly GreenFieldServerInfoController _greenFieldServerInfoController; private readonly StoreWebhooksController _storeWebhooksController; @@ -183,6 +187,7 @@ namespace BTCPayServer.Controllers.GreenField StoreLightningNodeApiController storeLightningNodeApiController, InternalLightningNodeApiController lightningNodeApiController, StoreLightningNetworkPaymentMethodsController storeLightningNetworkPaymentMethodsController, + StoreLNURLPayPaymentMethodsController storeLnurlPayPaymentMethodsController, GreenFieldInvoiceController greenFieldInvoiceController, GreenFieldServerInfoController greenFieldServerInfoController, StoreWebhooksController storeWebhooksController, @@ -202,6 +207,7 @@ namespace BTCPayServer.Controllers.GreenField _storeLightningNodeApiController = storeLightningNodeApiController; _lightningNodeApiController = lightningNodeApiController; _storeLightningNetworkPaymentMethodsController = storeLightningNetworkPaymentMethodsController; + _storeLnurlPayPaymentMethodsController = storeLnurlPayPaymentMethodsController; _greenFieldInvoiceController = greenFieldInvoiceController; _greenFieldServerInfoController = greenFieldServerInfoController; _storeWebhooksController = storeWebhooksController; @@ -746,7 +752,39 @@ namespace BTCPayServer.Controllers.GreenField { return GetFromActionResult(await _storesController.UpdateStore(storeId, request)); } + + public override Task> + GetStoreLNURLPayPaymentMethods(string storeId, bool? enabled, + CancellationToken token = default) + { + return Task.FromResult(GetFromActionResult( + _storeLnurlPayPaymentMethodsController.GetLNURLPayPaymentMethods(storeId, enabled))); + } + public override Task GetStoreLNURLPayPaymentMethod( + string storeId, string cryptoCode, CancellationToken token = default) + { + return Task.FromResult(GetFromActionResult( + _storeLnurlPayPaymentMethodsController.GetLNURLPayPaymentMethod(storeId, cryptoCode))); + } + + public override async Task RemoveStoreLNURLPayPaymentMethod(string storeId, string cryptoCode, + CancellationToken token = default) + { + HandleActionResult( + await _storeLnurlPayPaymentMethodsController.RemoveLNURLPayPaymentMethod(storeId, + cryptoCode)); + } + + public override async Task UpdateStoreLNURLPayPaymentMethod( + string storeId, string cryptoCode, + LNURLPayPaymentMethodData paymentMethod, CancellationToken token = default) + { + return GetFromActionResult(await + _storeLnurlPayPaymentMethodsController.UpdateLNURLPayPaymentMethod(storeId, cryptoCode, + paymentMethod)); + } + public override Task> GetStoreLightningNetworkPaymentMethods(string storeId, bool? enabled, CancellationToken token = default) diff --git a/BTCPayServer/Controllers/GreenField/StoreLNURLPayPaymentMethodsController.cs b/BTCPayServer/Controllers/GreenField/StoreLNURLPayPaymentMethodsController.cs new file mode 100644 index 000000000..64a821745 --- /dev/null +++ b/BTCPayServer/Controllers/GreenField/StoreLNURLPayPaymentMethodsController.cs @@ -0,0 +1,180 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Abstractions.Constants; +using BTCPayServer.Abstractions.Contracts; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Configuration; +using BTCPayServer.Data; +using BTCPayServer.HostedServices; +using BTCPayServer.Lightning; +using BTCPayServer.Payments; +using BTCPayServer.Payments.Lightning; +using BTCPayServer.Security; +using BTCPayServer.Services.Stores; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using StoreData = BTCPayServer.Data.StoreData; + +namespace BTCPayServer.Controllers.GreenField +{ + [ApiController] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + public class StoreLNURLPayPaymentMethodsController : ControllerBase + { + private StoreData Store => HttpContext.GetStoreData(); + private readonly StoreRepository _storeRepository; + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; + private readonly IAuthorizationService _authorizationService; + private readonly ISettingsRepository _settingsRepository; + + public StoreLNURLPayPaymentMethodsController( + StoreRepository storeRepository, + BTCPayNetworkProvider btcPayNetworkProvider, + IAuthorizationService authorizationService, + ISettingsRepository settingsRepository) + { + _storeRepository = storeRepository; + _btcPayNetworkProvider = btcPayNetworkProvider; + _authorizationService = authorizationService; + _settingsRepository = settingsRepository; + } + + public static IEnumerable GetLNURLPayPaymentMethods(StoreData store, + BTCPayNetworkProvider networkProvider, bool? enabled) + { + var blob = store.GetStoreBlob(); + var excludedPaymentMethods = blob.GetExcludedPaymentMethods(); + + return store.GetSupportedPaymentMethods(networkProvider) + .Where((method) => method.PaymentId.PaymentType == PaymentTypes.LNURLPay) + .OfType() + .Select(paymentMethod => + new LNURLPayPaymentMethodData( + paymentMethod.PaymentId.CryptoCode, + !excludedPaymentMethods.Match(paymentMethod.PaymentId), + paymentMethod.UseBech32Scheme, paymentMethod.EnableForStandardInvoices + ) + ) + .Where((result) => enabled is null || enabled == result.Enabled) + .ToList(); + } + + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/payment-methods/LNURLPay")] + public ActionResult> GetLNURLPayPaymentMethods( + string storeId, + [FromQuery] bool? enabled) + { + return Ok(GetLNURLPayPaymentMethods(Store, _btcPayNetworkProvider, enabled)); + } + + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")] + public IActionResult GetLNURLPayPaymentMethod(string storeId, string cryptoCode) + { + if (!GetNetwork(cryptoCode, out BTCPayNetwork _)) + { + return NotFound(); + } + + var method = GetExistingLNURLPayPaymentMethod(cryptoCode); + if (method is null) + { + return this.CreateAPIError(404, "paymentmethod-not-found", "The LNURL Payment Method isn't activated"); + } + + return Ok(method); + } + + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")] + public async Task RemoveLNURLPayPaymentMethod( + string storeId, + string cryptoCode) + { + if (!GetNetwork(cryptoCode, out BTCPayNetwork _)) + { + return NotFound(); + } + + var id = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay); + var store = Store; + store.SetSupportedPaymentMethod(id, null); + await _storeRepository.UpdateStore(store); + return Ok(); + } + + [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPut("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")] + public async Task UpdateLNURLPayPaymentMethod(string storeId, string cryptoCode, + [FromBody] LNURLPayPaymentMethodData paymentMethodData) + { + var paymentMethodId = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay); + + if (!GetNetwork(cryptoCode, out var network)) + { + return NotFound(); + } + + var lnMethod = StoreLightningNetworkPaymentMethodsController.GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, + cryptoCode, Store); + + if ((lnMethod is null || lnMethod.Enabled is false) && paymentMethodData.Enabled) + { + ModelState.AddModelError(nameof(LNURLPayPaymentMethodData.Enabled), + "LNURL Pay cannot be enabled unless the lightning payment method is configured and enabled on this store"); + } + + if (!ModelState.IsValid) + return this.CreateValidationError(ModelState); + + LNURLPaySupportedPaymentMethod? paymentMethod = new LNURLPaySupportedPaymentMethod() + { + CryptoCode = cryptoCode, + UseBech32Scheme = paymentMethodData.UseBech32Scheme, + EnableForStandardInvoices = paymentMethodData.EnableForStandardInvoices + }; + + var store = Store; + var storeBlob = store.GetStoreBlob(); + store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod); + storeBlob.SetExcluded(paymentMethodId, !paymentMethodData.Enabled); + store.SetStoreBlob(storeBlob); + await _storeRepository.UpdateStore(store); + return Ok(GetExistingLNURLPayPaymentMethod(cryptoCode, store)); + } + + private LNURLPayPaymentMethodData? GetExistingLNURLPayPaymentMethod(string cryptoCode, + StoreData? store = null) + { + store ??= Store; + var storeBlob = store.GetStoreBlob(); + var id = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay); + var paymentMethod = store + .GetSupportedPaymentMethods(_btcPayNetworkProvider) + .OfType() + .FirstOrDefault(method => method.PaymentId == id); + + var excluded = storeBlob.IsExcluded(id); + return paymentMethod is null + ? null + : new LNURLPayPaymentMethodData( + paymentMethod.PaymentId.CryptoCode, + !excluded, + paymentMethod.UseBech32Scheme, paymentMethod.EnableForStandardInvoices + ); + } + + private bool GetNetwork(string cryptoCode, [MaybeNullWhen(false)] out BTCPayNetwork network) + { + network = _btcPayNetworkProvider.GetNetwork(cryptoCode); + network = network?.SupportLightning is true ? network : null; + return network != null; + } + } +} diff --git a/BTCPayServer/Controllers/GreenField/StoreLightningNetworkPaymentMethodsController.cs b/BTCPayServer/Controllers/GreenField/StoreLightningNetworkPaymentMethodsController.cs index b0c3c1f67..704d6169f 100644 --- a/BTCPayServer/Controllers/GreenField/StoreLightningNetworkPaymentMethodsController.cs +++ b/BTCPayServer/Controllers/GreenField/StoreLightningNetworkPaymentMethodsController.cs @@ -84,7 +84,7 @@ namespace BTCPayServer.Controllers.GreenField return NotFound(); } - var method = GetExistingLightningLikePaymentMethod(cryptoCode); + var method = GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, Store); if (method is null) { return NotFound(); @@ -97,8 +97,7 @@ namespace BTCPayServer.Controllers.GreenField [HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")] public async Task RemoveLightningNetworkPaymentMethod( string storeId, - string cryptoCode, - int offset = 0, int amount = 10) + string cryptoCode) { if (!GetNetwork(cryptoCode, out BTCPayNetwork _)) { @@ -188,17 +187,17 @@ namespace BTCPayServer.Controllers.GreenField storeBlob.SetExcluded(paymentMethodId, !request.Enabled); store.SetStoreBlob(storeBlob); await _storeRepository.UpdateStore(store); - return Ok(GetExistingLightningLikePaymentMethod(cryptoCode, store)); + return Ok(GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, store)); } - private LightningNetworkPaymentMethodData? GetExistingLightningLikePaymentMethod(string cryptoCode, - StoreData? store = null) + public static LightningNetworkPaymentMethodData? GetExistingLightningLikePaymentMethod(BTCPayNetworkProvider btcPayNetworkProvider, string cryptoCode, + StoreData store) { - store ??= Store; + var storeBlob = store.GetStoreBlob(); var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); var paymentMethod = store - .GetSupportedPaymentMethods(_btcPayNetworkProvider) + .GetSupportedPaymentMethods(btcPayNetworkProvider) .OfType() .FirstOrDefault(method => method.PaymentId == id); diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 8e01abc22..eb1b17378 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -374,7 +374,8 @@ namespace BTCPayServer.Controllers Overpaid = _CurrencyNameTable.DisplayFormatCurrency( accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode), Address = data.GetPaymentMethodDetails().GetPaymentDestination(), - Rate = ExchangeRate(data) + Rate = ExchangeRate(data), + PaymentMethodRaw = data }; }).ToList() }; diff --git a/BTCPayServer/Controllers/PublicLightningNodeInfoController.cs b/BTCPayServer/Controllers/PublicLightningNodeInfoController.cs index 65b9d6848..b6c615849 100644 --- a/BTCPayServer/Controllers/PublicLightningNodeInfoController.cs +++ b/BTCPayServer/Controllers/PublicLightningNodeInfoController.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using BTCPayServer.Data; using BTCPayServer.Filters; using BTCPayServer.Lightning; +using BTCPayServer.Logging; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; using BTCPayServer.Services.Stores; @@ -41,7 +42,8 @@ namespace BTCPayServer.Controllers { var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store); var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode); - var nodeInfo = await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails, network); + var nodeInfo = + await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails, network, new InvoiceLogs()); return View(new ShowLightningNodeInfoViewModel { diff --git a/BTCPayServer/Controllers/StoresController.LightningLike.cs b/BTCPayServer/Controllers/StoresController.LightningLike.cs index 35e88d692..78b313793 100644 --- a/BTCPayServer/Controllers/StoresController.LightningLike.cs +++ b/BTCPayServer/Controllers/StoresController.LightningLike.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using BTCPayServer.Data; using BTCPayServer.Lightning; +using BTCPayServer.Logging; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Lightning; @@ -48,7 +49,6 @@ namespace BTCPayServer.Controllers } var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike); - var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store); LightningSupportedPaymentMethod paymentMethod = null; if (vm.LightningNodeType == LightningNodeType.Internal) @@ -92,6 +92,7 @@ namespace BTCPayServer.Controllers CryptoCode = paymentMethodId.CryptoCode }; paymentMethod.SetLightningUrl(connectionString); + } switch (command) @@ -99,8 +100,22 @@ namespace BTCPayServer.Controllers case "save": var storeBlob = store.GetStoreBlob(); storeBlob.Hints.Lightning = false; + + var lnurl = new PaymentMethodId(vm.CryptoCode, PaymentTypes.LNURLPay); + storeBlob.SetExcluded(lnurl, !vm.LNURLEnabled); + store.SetSupportedPaymentMethod(new LNURLPaySupportedPaymentMethod() + { + CryptoCode = vm.CryptoCode, + EnableForStandardInvoices = vm.LNURLStandardInvoiceEnabled, + UseBech32Scheme = vm.LNURLBech32Mode, + LUD12Enabled = vm.LUD12Enabled + }); + store.SetStoreBlob(storeBlob); store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod); + + + await _Repo.UpdateStore(store); TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated."; return RedirectToAction(nameof(UpdateStore), new { storeId }); @@ -109,7 +124,7 @@ namespace BTCPayServer.Controllers var handler = _ServiceProvider.GetRequiredService(); try { - var info = await handler.GetNodeInfo(paymentMethod, network, Request.IsOnion()); + var info = await handler.GetNodeInfo(paymentMethod, network, new InvoiceLogs(), Request.IsOnion()); if (!vm.SkipPortTest) { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); @@ -163,7 +178,9 @@ namespace BTCPayServer.Controllers { vm.CanUseInternalNode = await CanUseInternalLightning(); var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store); - if (lightning != null) + + var lnSet = lightning != null; + if (lnSet) { vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom; vm.ConnectionString = lightning.GetDisplayableConnectionString(); @@ -172,6 +189,20 @@ namespace BTCPayServer.Controllers { vm.LightningNodeType = vm.CanUseInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom; } + + var lnurl = GetExistingLNURLSupportedPaymentMethod(vm.CryptoCode, store); + if (lnurl != null) + { + vm.LNURLEnabled = !store.GetStoreBlob().GetExcludedPaymentMethods().Match(lnurl.PaymentId); + vm.LNURLBech32Mode = lnurl.UseBech32Scheme; + vm.LNURLStandardInvoiceEnabled = lnurl.EnableForStandardInvoices; + vm.LUD12Enabled = lnurl.LUD12Enabled; + } + else + { + vm.LNURLEnabled = !lnSet; + vm.DisableBolt11PaymentMethod = false; + } } private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store) @@ -182,5 +213,13 @@ namespace BTCPayServer.Controllers .FirstOrDefault(d => d.PaymentId == id); return existing; } + private LNURLPaySupportedPaymentMethod GetExistingLNURLSupportedPaymentMethod(string cryptoCode, StoreData store) + { + var id = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay); + var existing = store.GetSupportedPaymentMethods(_NetworkProvider) + .OfType() + .FirstOrDefault(d => d.PaymentId == id); + return existing; + } } } diff --git a/BTCPayServer/Controllers/StoresController.cs b/BTCPayServer/Controllers/StoresController.cs index 5cd302408..d58570596 100644 --- a/BTCPayServer/Controllers/StoresController.cs +++ b/BTCPayServer/Controllers/StoresController.cs @@ -372,7 +372,11 @@ namespace BTCPayServer.Controllers var storeBlob = CurrentStore.GetStoreBlob(); var vm = new CheckoutExperienceViewModel(); SetCryptoCurrencies(vm, CurrentStore); - vm.PaymentMethodCriteria = CurrentStore.GetSupportedPaymentMethods(_NetworkProvider).Select(method => + vm.PaymentMethodCriteria = CurrentStore.GetSupportedPaymentMethods(_NetworkProvider) + .Where(s => !storeBlob.GetExcludedPaymentMethods().Match(s.PaymentId)) + .Where(s => _NetworkProvider.GetNetwork(s.PaymentId.CryptoCode) != null) + .Where(s => s.PaymentId.PaymentType != PaymentTypes.LNURLPay) + .Select(method => { var existing = storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria => @@ -461,13 +465,36 @@ namespace BTCPayServer.Controllers return View(model); } - blob.PaymentMethodCriteria = model.PaymentMethodCriteria - .Where(viewModel => !string.IsNullOrEmpty(viewModel.Value)).Select(viewModel => + // Payment criteria for Off-Chain should also affect LNUrl + foreach (var newCriteria in model.PaymentMethodCriteria.ToList()) + { + var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod); + if (paymentMethodId.PaymentType == PaymentTypes.LightningLike) + model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel() + { + PaymentMethod = new PaymentMethodId(paymentMethodId.CryptoCode, PaymentTypes.LNURLPay).ToString(), + Type = newCriteria.Type, + Value = newCriteria.Value + }); + // Should not be able to set LNUrlPay criteria directly in UI + if (paymentMethodId.PaymentType == PaymentTypes.LNURLPay) + model.PaymentMethodCriteria.Remove(newCriteria); + } + blob.PaymentMethodCriteria ??= new List(); + foreach (var newCriteria in model.PaymentMethodCriteria) + { + var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod); + var existingCriteria = blob.PaymentMethodCriteria.FirstOrDefault(c => c.PaymentMethod == paymentMethodId); + if (existingCriteria != null) + blob.PaymentMethodCriteria.Remove(existingCriteria); + CurrencyValue.TryParse(newCriteria.Value, out var cv); + blob.PaymentMethodCriteria.Add(new PaymentMethodCriteria() { - CurrencyValue.TryParse(viewModel.Value, out var cv); - return new PaymentMethodCriteria() { Above = viewModel.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan, Value = cv, PaymentMethod = PaymentMethodId.Parse(viewModel.PaymentMethod) }; - }).ToList(); - + Above = newCriteria.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan, + Value = cv, + PaymentMethod = paymentMethodId + }); + } blob.RequiresRefundEmail = model.RequiresRefundEmail; blob.LazyPaymentMethods = model.LazyPaymentMethods; blob.RedirectAutomatically = model.RedirectAutomatically; @@ -493,8 +520,8 @@ namespace BTCPayServer.Controllers }); } - - private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm) + private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, + out List derivationSchemes, out List lightningNodes) { var excludeFilters = storeBlob.GetExcludedPaymentMethods(); var derivationByCryptoCode = @@ -506,8 +533,12 @@ namespace BTCPayServer.Controllers var lightningByCryptoCode = store .GetSupportedPaymentMethods(_NetworkProvider) .OfType() + .Where(method => method.PaymentId.PaymentType == LightningPaymentType.Instance) .ToDictionary(c => c.CryptoCode.ToUpperInvariant()); + derivationSchemes = new List(); + lightningNodes = new List(); + foreach (var paymentMethodId in _paymentMethodHandlerDictionary.Distinct().SelectMany(handler => handler.GetSupportedPaymentMethods())) { switch (paymentMethodId.PaymentType) @@ -517,7 +548,7 @@ namespace BTCPayServer.Controllers var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode); var value = strategy?.ToPrettyString() ?? string.Empty; - vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme() + derivationSchemes.Add(new StoreDerivationScheme { Crypto = paymentMethodId.CryptoCode, WalletSupported = network.WalletSupported, @@ -529,10 +560,14 @@ namespace BTCPayServer.Controllers #endif }); break; + + case LNURLPayPaymentType lnurlPayPaymentType: + break; + case LightningPaymentType _: var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode); var isEnabled = !excludeFilters.Match(paymentMethodId) && lightning != null; - vm.LightningNodes.Add(new StoreViewModel.LightningNode + lightningNodes.Add(new StoreLightningNode { CryptoCode = paymentMethodId.CryptoCode, Address = lightning?.GetDisplayableConnectionString(), @@ -544,30 +579,92 @@ namespace BTCPayServer.Controllers } [HttpGet("{storeId}")] - public async Task UpdateStore() + public IActionResult UpdateStore() { var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); var storeBlob = store.GetStoreBlob(); - var vm = new StoreViewModel(); - vm.Id = store.Id; - vm.StoreName = store.StoreName; - vm.StoreWebsite = store.StoreWebsite; - vm.DefaultCurrency = storeBlob.DefaultCurrency; - vm.NetworkFeeMode = storeBlob.NetworkFeeMode; - vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice; - vm.SpeedPolicy = store.SpeedPolicy; - vm.CanDelete = _Repo.CanDeleteStores(); - AddPaymentMethods(store, storeBlob, vm); + var vm = new StoreViewModel + { + Id = store.Id, + CanDelete = _Repo.CanDeleteStores(), + StoreName = store.StoreName, + StoreWebsite = store.StoreWebsite, + HintWallet = storeBlob.Hints.Wallet, + HintLightning = storeBlob.Hints.Lightning + }; + + AddPaymentMethods(store, storeBlob, + out var derivationSchemes, out var lightningNodes); + + vm.DerivationSchemes = derivationSchemes; + vm.LightningNodes = lightningNodes; + + return View(vm); + } + + [HttpPost("{storeId}")] + public async Task UpdateStore(StoreViewModel model, string command = null) + { + bool needUpdate = false; + if (CurrentStore.StoreName != model.StoreName) + { + needUpdate = true; + CurrentStore.StoreName = model.StoreName; + } + if (CurrentStore.StoreWebsite != model.StoreWebsite) + { + needUpdate = true; + CurrentStore.StoreWebsite = model.StoreWebsite; + } + + var blob = CurrentStore.GetStoreBlob(); + if (CurrentStore.SetStoreBlob(blob)) + { + needUpdate = true; + } + + if (needUpdate) + { + await _Repo.UpdateStore(CurrentStore); + + TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; + } + + return RedirectToAction(nameof(UpdateStore), new + { + storeId = CurrentStore.Id + }); + } + + [HttpGet("{storeId}/payment")] + public async Task Payment() + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + + var storeBlob = store.GetStoreBlob(); + var vm = new PaymentViewModel + { + NetworkFeeMode = storeBlob.NetworkFeeMode, + AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice, + SpeedPolicy = store.SpeedPolicy, + PaymentTolerance = storeBlob.PaymentTolerance, + DefaultCurrency = storeBlob.DefaultCurrency + }; + + AddPaymentMethods(store, storeBlob, + out var derivationSchemes, out var lightningNodes); + + vm.DerivationSchemes = derivationSchemes; + vm.LightningNodes = lightningNodes; vm.MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes; vm.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes; vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate; - vm.PaymentTolerance = storeBlob.PaymentTolerance; vm.PayJoinEnabled = storeBlob.PayJoinEnabled; - vm.HintWallet = storeBlob.Hints.Wallet; - vm.HintLightning = storeBlob.Hints.Lightning; vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints; vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback; @@ -581,12 +678,12 @@ namespace BTCPayServer.Controllers .GetSupportedPaymentMethods(_NetworkProvider) .OfType() .Any(settings => settings.Network.SupportPayJoin && settings.IsHotWallet); - + return View(vm); } - [HttpPost("{storeId}")] - public async Task UpdateStore(StoreViewModel model, string command = null) + [HttpPost("{storeId}/payment")] + public async Task Payment(PaymentViewModel model, string command = null) { bool needUpdate = false; if (CurrentStore.SpeedPolicy != model.SpeedPolicy) @@ -594,16 +691,6 @@ namespace BTCPayServer.Controllers needUpdate = true; CurrentStore.SpeedPolicy = model.SpeedPolicy; } - if (CurrentStore.StoreName != model.StoreName) - { - needUpdate = true; - CurrentStore.StoreName = model.StoreName; - } - if (CurrentStore.StoreWebsite != model.StoreWebsite) - { - needUpdate = true; - CurrentStore.StoreWebsite = model.StoreWebsite; - } var blob = CurrentStore.GetStoreBlob(); blob.DefaultCurrency = model.DefaultCurrency; @@ -630,7 +717,7 @@ namespace BTCPayServer.Controllers { await _Repo.UpdateStore(CurrentStore); - TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; + TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated"; if (payjoinChanged && blob.PayJoinEnabled) { @@ -646,13 +733,13 @@ namespace BTCPayServer.Controllers TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Warning, - Html = $"The store was updated successfully. However, payjoin will not work for {string.Join(", ", problematicPayjoinEnabledMethods)} until you configure them to be a hot wallet." + Html = $"The payment settings were updated successfully. However, payjoin will not work for {string.Join(", ", problematicPayjoinEnabledMethods)} until you configure them to be a hot wallet." }); } } } - return RedirectToAction(nameof(UpdateStore), new + return RedirectToAction(nameof(Payment), new { storeId = CurrentStore.Id }); diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 6f7579046..62d5769cd 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -35,6 +35,7 @@ using NBitpayClient; using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json.Linq; +using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo; namespace BTCPayServer { diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 141fac9d9..d1b62eb39 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -5,6 +5,7 @@ using System.Threading; using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Models; +using BTCPayServer.Abstractions.Services; using BTCPayServer.Common; using BTCPayServer.Client; using BTCPayServer.Configuration; @@ -329,6 +330,8 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(provider => provider.GetService()); + services.AddSingleton(); + services.AddSingleton(provider => provider.GetService()); services.AddSingleton(); services.AddSingleton(); diff --git a/BTCPayServer/LNURL/LNURLController.cs b/BTCPayServer/LNURL/LNURLController.cs new file mode 100644 index 000000000..ae35b84b5 --- /dev/null +++ b/BTCPayServer/LNURL/LNURLController.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; +using BTCPayServer.Controllers; +using BTCPayServer.Data; +using BTCPayServer.Events; +using BTCPayServer.Lightning; +using BTCPayServer.Models.AppViewModels; +using BTCPayServer.Payments; +using BTCPayServer.Payments.Lightning; +using BTCPayServer.Services; +using BTCPayServer.Services.Apps; +using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Stores; +using LNURL; +using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using NBitcoin.Crypto; +using Newtonsoft.Json; + +namespace BTCPayServer +{ + [Route("~/{cryptoCode}/[controller]/")] + public class LNURLController : Controller + { + private readonly InvoiceRepository _invoiceRepository; + private readonly EventAggregator _eventAggregator; + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; + private readonly LightningLikePaymentHandler _lightningLikePaymentHandler; + private readonly StoreRepository _storeRepository; + private readonly AppService _appService; + private readonly InvoiceController _invoiceController; + + public LNURLController(InvoiceRepository invoiceRepository, + EventAggregator eventAggregator, + BTCPayNetworkProvider btcPayNetworkProvider, + LightningLikePaymentHandler lightningLikePaymentHandler, + StoreRepository storeRepository, + AppService appService, + InvoiceController invoiceController) + { + _invoiceRepository = invoiceRepository; + _eventAggregator = eventAggregator; + _btcPayNetworkProvider = btcPayNetworkProvider; + _lightningLikePaymentHandler = lightningLikePaymentHandler; + _storeRepository = storeRepository; + _appService = appService; + _invoiceController = invoiceController; + } + + [HttpGet("pay/i/{invoiceId}")] + public async Task GetLNURLForInvoice(string invoiceId, string cryptoCode, + [FromQuery] long? amount = null, string comment = null) + { + var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); + if (network is null || !network.SupportLightning) + { + return NotFound(); + } + + var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay); + var i = await _invoiceRepository.GetInvoice(invoiceId, true); + if (i.Status == InvoiceStatusLegacy.New) + { + var isTopup = i.IsUnsetTopUp(); + var lnurlSupportedPaymentMethod = + i.GetSupportedPaymentMethod(pmi).FirstOrDefault(); + if (lnurlSupportedPaymentMethod is null || + (!isTopup && !lnurlSupportedPaymentMethod.EnableForStandardInvoices)) + { + return NotFound(); + } + + var lightningPaymentMethod = i.GetPaymentMethod(pmi); + var accounting = lightningPaymentMethod.Calculate(); + var paymentMethodDetails = + lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails; + if (paymentMethodDetails.LightningSupportedPaymentMethod is null) + { + return NotFound(); + } + + 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 List(); + + lnurlMetadata.Add(new[] { "text/plain", i.Id }); + + var metadata = JsonConvert.SerializeObject(lnurlMetadata); + if (amount.HasValue && (amount < min || amount > max)) + { + return BadRequest(new LNUrlStatusResponse + { + Status = "ERROR", Reason = "Amount is out of bounds." + }); + } + + if (amount.HasValue && string.IsNullOrEmpty(paymentMethodDetails.BOLT11) || + paymentMethodDetails.GeneratedBoltAmount != amount) + { + var client = + _lightningLikePaymentHandler.CreateLightningClient( + paymentMethodDetails.LightningSupportedPaymentMethod, network); + if (!string.IsNullOrEmpty(paymentMethodDetails.BOLT11)) + { + try + { + await client.CancelInvoice(paymentMethodDetails.InvoiceId); + } + catch (Exception) + { + //not a fully supported option + } + } + + var descriptionHash = new uint256(Hashes.SHA256(Encoding.UTF8.GetBytes(metadata))); + LightningInvoice invoice; + try + { + invoice = await client.CreateInvoice(new CreateInvoiceParams(amount.Value, + descriptionHash, + i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow)); + if (!BOLT11PaymentRequest.Parse(invoice.BOLT11, network.NBitcoinNetwork) + .VerifyDescriptionHash(metadata)) + { + return BadRequest(new LNUrlStatusResponse + { + Status = "ERROR", + Reason = "Lightning node could not generate invoice with a VALID description hash" + }); + } + } + catch (Exception) + { + return BadRequest(new LNUrlStatusResponse + { + Status = "ERROR", + Reason = "Lightning node could not generate invoice with description hash" + }); + } + + paymentMethodDetails.BOLT11 = invoice.BOLT11; + paymentMethodDetails.InvoiceId = invoice.Id; + paymentMethodDetails.GeneratedBoltAmount = new LightMoney(amount.Value); + if (lnurlSupportedPaymentMethod.LUD12Enabled) + { + paymentMethodDetails.ProvidedComment = comment; + } + + lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails); + await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod); + + _eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(invoiceId, + paymentMethodDetails, pmi)); + return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse + { + Disposable = true, Routes = Array.Empty(), Pr = paymentMethodDetails.BOLT11 + }); + } + + if (amount.HasValue && paymentMethodDetails.GeneratedBoltAmount == amount) + { + if (lnurlSupportedPaymentMethod.LUD12Enabled && paymentMethodDetails.ProvidedComment != comment) + { + paymentMethodDetails.ProvidedComment = comment; + lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails); + await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod); + } + + return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse + { + Disposable = true, Routes = Array.Empty(), Pr = paymentMethodDetails.BOLT11 + }); + } + + if (amount is null) + { + return Ok(new LNURLPayRequest + { + Tag = "payRequest", + MinSendable = min, + MaxSendable = max, + CommentAllowed = lnurlSupportedPaymentMethod.LUD12Enabled ? 2000 : 0, + Metadata = metadata, + Callback = new Uri(Request.GetCurrentUrl()) + }); + } + } + + return BadRequest(new LNUrlStatusResponse + { + Status = "ERROR", Reason = "Invoice not in a valid payable state" + }); + } + } +} diff --git a/BTCPayServer/Models/InvoiceResponse.cs b/BTCPayServer/Models/InvoiceResponse.cs index d28a2e088..24f004c3f 100644 --- a/BTCPayServer/Models/InvoiceResponse.cs +++ b/BTCPayServer/Models/InvoiceResponse.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using BTCPayServer.Services.Invoices; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -79,7 +80,7 @@ namespace BTCPayServer.Models } [JsonProperty("cryptoInfo")] - public List CryptoInfo { get; set; } + public List CryptoInfo { get; set; } //"price":5 [JsonProperty("price")] @@ -262,7 +263,7 @@ namespace BTCPayServer.Models [JsonProperty("addresses")] public Dictionary Addresses { get; set; } [JsonProperty("paymentCodes")] - public Dictionary PaymentCodes { get; set; } + public Dictionary PaymentCodes { get; set; } [JsonProperty("buyer")] public JObject Buyer { get; set; } } diff --git a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs index 8ee52cb36..a82e90a9a 100644 --- a/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs +++ b/BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs @@ -30,6 +30,7 @@ namespace BTCPayServer.Models.InvoicingModels { public string Crypto { get; set; } public string BOLT11 { get; set; } + public PaymentType Type { get; set; } } public class InvoiceDetailsModel @@ -45,6 +46,8 @@ namespace BTCPayServer.Models.InvoicingModels public string Overpaid { get; set; } [JsonIgnore] public PaymentMethodId PaymentMethodId { get; set; } + + public PaymentMethod PaymentMethodRaw { get; set; } } public class AddressModel { diff --git a/BTCPayServer/Models/StoreViewModels/LightningNodeViewModel.cs b/BTCPayServer/Models/StoreViewModels/LightningNodeViewModel.cs index b4b97d571..bbe127574 100644 --- a/BTCPayServer/Models/StoreViewModels/LightningNodeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/LightningNodeViewModel.cs @@ -10,7 +10,22 @@ namespace BTCPayServer.Models.StoreViewModels public class LightningNodeViewModel { + [Display(Name = "Enable LNURL")] + public bool LNURLEnabled { get; set; } + + [Display(Name = "LNURL Classic Mode")] + public bool LNURLBech32Mode { get; set; } = true; + + [Display(Name = "LNURL enabled for standard invoices")] + public bool LNURLStandardInvoiceEnabled { get; set; } + + [Display(Name = "Allow payee to pass a comment")] + public bool LUD12Enabled { get; set; } + + [Display(Name = "Do not offer BOLT11 for standard invoices")] + public bool DisableBolt11PaymentMethod { get; set; } public LightningNodeType LightningNodeType { get; set; } + [Display(Name = "Connection string")] public string ConnectionString { get; set; } public string CryptoCode { get; set; } diff --git a/BTCPayServer/Models/StoreViewModels/PaymentViewModel.cs b/BTCPayServer/Models/StoreViewModels/PaymentViewModel.cs new file mode 100644 index 000000000..06d24ca45 --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/PaymentViewModel.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using BTCPayServer.Client.Models; +using BTCPayServer.Validation; +using static BTCPayServer.Data.StoreBlob; + +namespace BTCPayServer.Models.StoreViewModels +{ + public class PaymentViewModel + { + public List DerivationSchemes { get; set; } + public List LightningNodes { get; set; } + public bool IsOnchainSetup { get; set; } + public bool IsLightningSetup { get; set; } + public bool CanUsePayJoin { get; set; } + + [Display(Name = "Allow anyone to create invoice")] + public bool AnyoneCanCreateInvoice { get; set; } + + [Display(Name = "Invoice expires if the full amount has not been paid after …")] + [Range(1, 60 * 24 * 24)] + public int InvoiceExpiration { get; set; } + + [Display(Name = "Payment invalid if transactions fails to confirm … after invoice expiration")] + [Range(10, 60 * 24 * 24)] + public int MonitoringExpiration { get; set; } + + [Display(Name = "Consider the invoice confirmed when the payment transaction …")] + public SpeedPolicy SpeedPolicy { get; set; } + + [Display(Name = "Add additional fee (network fee) to invoice …")] + public NetworkFeeMode NetworkFeeMode { get; set; } + + [Display(Name = "Description template of the lightning invoice")] + public string LightningDescriptionTemplate { get; set; } + + [Display(Name = "Enable Payjoin/P2EP")] + public bool PayJoinEnabled { get; set; } + + [Display(Name = "Show recommended fee")] + public bool ShowRecommendedFee { get; set; } + + [Display(Name = "Recommended fee confirmation target blocks")] + [Range(1, double.PositiveInfinity)] + public int RecommendedFeeBlockTarget { get; set; } + + [Display(Name = "Display Lightning payment amounts in Satoshis")] + public bool LightningAmountInSatoshi { get; set; } + + [Display(Name = "Add hop hints for private channels to the Lightning invoice")] + public bool LightningPrivateRouteHints { get; set; } + + [Display(Name = "Include Lightning invoice fallback to on-chain BIP21 payment URL")] + public bool OnChainWithLnInvoiceFallback { get; set; } + + [Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")] + [Range(0, 100)] + public double PaymentTolerance { get; set; } + + [Display(Name = "Default currency")] + [MaxLength(10)] + public string DefaultCurrency { get; set; } + } +} diff --git a/BTCPayServer/Models/StoreViewModels/StoreDerivationScheme.cs b/BTCPayServer/Models/StoreViewModels/StoreDerivationScheme.cs new file mode 100644 index 000000000..938deae14 --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/StoreDerivationScheme.cs @@ -0,0 +1,12 @@ +namespace BTCPayServer.Models.StoreViewModels +{ + public class StoreDerivationScheme + { + public string Crypto { get; set; } + public string Value { get; set; } + public WalletId WalletId { get; set; } + public bool WalletSupported { get; set; } + public bool Enabled { get; set; } + public bool Collapsed { get; set; } + } +} diff --git a/BTCPayServer/Models/StoreViewModels/StoreLightningNode.cs b/BTCPayServer/Models/StoreViewModels/StoreLightningNode.cs new file mode 100644 index 000000000..8bb7ff33a --- /dev/null +++ b/BTCPayServer/Models/StoreViewModels/StoreLightningNode.cs @@ -0,0 +1,9 @@ +namespace BTCPayServer.Models.StoreViewModels +{ + public class StoreLightningNode + { + public string CryptoCode { get; set; } + public string Address { get; set; } + public bool Enabled { get; set; } + } +} diff --git a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs index e086d61b5..3b3c54d5c 100644 --- a/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/StoreViewModel.cs @@ -8,130 +8,24 @@ namespace BTCPayServer.Models.StoreViewModels { public class StoreViewModel { - public class DerivationScheme - { - public string Crypto { get; set; } - public string Value { get; set; } - public WalletId WalletId { get; set; } - public bool WalletSupported { get; set; } - public bool Enabled { get; set; } - public bool Collapsed { get; set; } - } - - public class AdditionalPaymentMethod - { - public string Provider { get; set; } - public bool Enabled { get; set; } - public string Action { get; set; } - } - public StoreViewModel() - { - - } - + public List DerivationSchemes { get; set; } + public List LightningNodes { get; set; } + public bool HintWallet { get; set; } + public bool HintLightning { get; set; } public bool CanDelete { get; set; } + [Display(Name = "Store ID")] public string Id { get; set; } + [Display(Name = "Store Name")] [Required] [MaxLength(50)] [MinLength(1)] - public string StoreName - { - get; set; - } + public string StoreName { get; set; } [Uri] [Display(Name = "Store Website")] [MaxLength(500)] - public string StoreWebsite - { - get; - set; - } - - [Display(Name = "Default currency")] - [MaxLength(10)] - public string DefaultCurrency { get; set; } - - [Display(Name = "Allow anyone to create invoice")] - public bool AnyoneCanCreateInvoice { get; set; } - - public List DerivationSchemes { get; set; } = new List(); - - [Display(Name = "Invoice expires if the full amount has not been paid after …")] - [Range(1, 60 * 24 * 24)] - public int InvoiceExpiration - { - get; - set; - } - - [Display(Name = "Payment invalid if transactions fails to confirm … after invoice expiration")] - [Range(10, 60 * 24 * 24)] - public int MonitoringExpiration - { - get; - set; - } - - [Display(Name = "Consider the invoice confirmed when the payment transaction …")] - public SpeedPolicy SpeedPolicy - { - get; set; - } - - [Display(Name = "Add additional fee (network fee) to invoice …")] - public NetworkFeeMode NetworkFeeMode - { - get; set; - } - - [Display(Name = "Description template of the lightning invoice")] - public string LightningDescriptionTemplate { get; set; } - - [Display(Name = "Enable Payjoin/P2EP")] - public bool PayJoinEnabled { get; set; } - public bool CanUsePayJoin { get; set; } - public bool IsOnchainSetup { get; set; } - public bool IsLightningSetup { get; set; } - - public bool HintWallet { get; set; } - public bool HintLightning { get; set; } - - [Display(Name = "Show recommended fee")] - public bool ShowRecommendedFee { get; set; } - - [Display(Name = "Recommended fee confirmation target blocks")] - [Range(1, double.PositiveInfinity)] - public int RecommendedFeeBlockTarget { get; set; } - - [Display(Name = "Display Lightning payment amounts in Satoshis")] - public bool LightningAmountInSatoshi { get; set; } - - [Display(Name = "Add hop hints for private channels to the Lightning invoice")] - public bool LightningPrivateRouteHints { get; set; } - - [Display(Name = "Include Lightning invoice fallback to on-chain BIP21 payment URL")] - public bool OnChainWithLnInvoiceFallback { get; set; } - - public class LightningNode - { - public string CryptoCode { get; set; } - public string Address { get; set; } - public bool Enabled { get; set; } - } - public List LightningNodes - { - get; set; - } = new List(); - - [Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")] - [Range(0, 100)] - public double PaymentTolerance - { - get; - set; - } + public string StoreWebsite { get; set; } } } diff --git a/BTCPayServer/Payments/IPaymentMethodDetails.cs b/BTCPayServer/Payments/IPaymentMethodDetails.cs index 85706e339..e1d058e56 100644 --- a/BTCPayServer/Payments/IPaymentMethodDetails.cs +++ b/BTCPayServer/Payments/IPaymentMethodDetails.cs @@ -18,5 +18,6 @@ namespace BTCPayServer.Payments decimal GetNextNetworkFee(); bool Activated {get;set;} + virtual string GetAdditionalDataPartialName() => null; } } diff --git a/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentHandler.cs b/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentHandler.cs new file mode 100644 index 000000000..3c54d078f --- /dev/null +++ b/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentHandler.cs @@ -0,0 +1,170 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Configuration; +using BTCPayServer.Data; +using BTCPayServer.HostedServices; +using BTCPayServer.Lightning; +using BTCPayServer.Logging; +using BTCPayServer.Models; +using BTCPayServer.Models.InvoicingModels; +using BTCPayServer.Client.Models; +using BTCPayServer.Services; +using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Rates; +using Microsoft.Extensions.Options; +using NBitcoin; + +namespace BTCPayServer.Payments.Lightning +{ + public class LNURLPayPaymentHandler : PaymentMethodHandlerBase + { + private readonly BTCPayNetworkProvider _networkProvider; + private readonly CurrencyNameTable _currencyNameTable; + private readonly LightningLikePaymentHandler _lightningLikePaymentHandler; + + public LNURLPayPaymentHandler( + BTCPayNetworkProvider networkProvider, + CurrencyNameTable currencyNameTable, + IOptions options, + LightningLikePaymentHandler lightningLikePaymentHandler) + { + _networkProvider = networkProvider; + _currencyNameTable = currencyNameTable; + _lightningLikePaymentHandler = lightningLikePaymentHandler; + Options = options; + } + + public override PaymentType PaymentType => PaymentTypes.LightningLike; + + public IOptions Options { get; } + + public override async Task CreatePaymentMethodDetails( + InvoiceLogs logs, + LNURLPaySupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, Data.StoreData store, + BTCPayNetwork network, object preparePaymentObject) + { + if (!supportedPaymentMethod.EnableForStandardInvoices && + paymentMethod.ParentEntity.Type == InvoiceType.Standard) + { + throw new PaymentMethodUnavailableException("LNURL is not enabled for standard invoices"); + } + if (string.IsNullOrEmpty(paymentMethod.ParentEntity.Id)) + { + var lnSupported = store.GetSupportedPaymentMethods(_networkProvider) + .OfType().SingleOrDefault(method => + method.PaymentId.CryptoCode == supportedPaymentMethod.CryptoCode && + method.PaymentId.PaymentType == LightningPaymentType.Instance); + + if (lnSupported is null) + { + throw new PaymentMethodUnavailableException("LNURL requires a lightning node to be configured for the store."); + } + + return new LNURLPayPaymentMethodDetails() + { + Activated = false, LightningSupportedPaymentMethod = lnSupported + }; + } + + + var lnLightningSupportedPaymentMethod = + ((LNURLPayPaymentMethodDetails)paymentMethod.GetPaymentMethodDetails()).LightningSupportedPaymentMethod; + + NodeInfo? nodeInfo = null; + if (lnLightningSupportedPaymentMethod != null) + { + nodeInfo = (await _lightningLikePaymentHandler.GetNodeInfo(lnLightningSupportedPaymentMethod, _networkProvider.GetNetwork(supportedPaymentMethod.CryptoCode), logs, paymentMethod.PreferOnion)).FirstOrDefault(); + } + + return new LNURLPayPaymentMethodDetails + { + Activated = true, + LightningSupportedPaymentMethod = lnLightningSupportedPaymentMethod, + BTCPayInvoiceId = paymentMethod.ParentEntity.Id, + Bech32Mode = supportedPaymentMethod.UseBech32Scheme, + NodeInfo = nodeInfo?.ToString() + }; + } + + public override IEnumerable GetSupportedPaymentMethods() + { + return _networkProvider + .GetAll() + .OfType() + .Where(network => network.NBitcoinNetwork.Consensus.SupportSegwit && network.SupportLightning) + .Select(network => new PaymentMethodId(network.CryptoCode, PaymentTypes.LNURLPay)); + } + + public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse, + StoreBlob storeBlob, IPaymentMethod paymentMethod) + { + var paymentMethodId = paymentMethod.GetId(); + var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); + var network = _networkProvider.GetNetwork(model.CryptoCode); + model.PaymentMethodName = GetPaymentMethodName(network); + model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls?.AdditionalData["LNURLP"].ToObject(); + model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl; + model.BtcAddress = model.InvoiceBitcoinUrl; + model.PeerInfo = ((LNURLPayPaymentMethodDetails) paymentMethod.GetPaymentMethodDetails()).NodeInfo; + if ( storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC") + { + var satoshiCulture = new CultureInfo(CultureInfo.InvariantCulture.Name); + satoshiCulture.NumberFormat.NumberGroupSeparator = " "; + model.CryptoCode = "Sats"; + model.BtcDue = Money.Parse(model.BtcDue).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture); + model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture); + model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi) + .ToString("N0", satoshiCulture); + model.NetworkFee = new Money(model.NetworkFee, MoneyUnit.BTC).ToUnit(MoneyUnit.Satoshi); + model.Rate = + _currencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, model.InvoiceCurrency); + } + } + + public override string GetCryptoImage(PaymentMethodId paymentMethodId) + { + var network = _networkProvider.GetNetwork(paymentMethodId.CryptoCode); + return GetCryptoImage(network); + } + + private string GetCryptoImage(BTCPayNetworkBase network) + { + return ((BTCPayNetwork)network).LightningImagePath; + } + + public override string GetPaymentMethodName(PaymentMethodId paymentMethodId) + { + var network = _networkProvider.GetNetwork(paymentMethodId.CryptoCode); + return GetPaymentMethodName(network); + } + + public override CheckoutUIPaymentMethodSettings GetCheckoutUISettings() + { + return new CheckoutUIPaymentMethodSettings() + { + ExtensionPartial = "Lightning/LightningLikeMethodCheckout", + CheckoutBodyVueComponentName = "LightningLikeMethodCheckout", + CheckoutHeaderVueComponentName = "LightningLikeMethodCheckoutHeader", + NoScriptPartialName = "Lightning/LightningLikeMethodCheckoutNoScript" + }; + } + + private string GetPaymentMethodName(BTCPayNetworkBase network) + { + return $"{network.DisplayName} (Lightning LNURL)"; + } + + public override object PreparePayment(LNURLPaySupportedPaymentMethod supportedPaymentMethod, + Data.StoreData store, + BTCPayNetworkBase network) + { + // pass a non null obj, so that if lazy payment feature is used, it has a marker to trigger activation + return new { }; + } + } +} diff --git a/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentMethodDetails.cs b/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentMethodDetails.cs new file mode 100644 index 000000000..3caeb0a41 --- /dev/null +++ b/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentMethodDetails.cs @@ -0,0 +1,34 @@ +using BTCPayServer.Client.JsonConverters; +using BTCPayServer.Lightning; +using BTCPayServer.Payments.Lightning; +using Newtonsoft.Json; + +namespace BTCPayServer.Payments +{ + public class LNURLPayPaymentMethodDetails : LightningLikePaymentMethodDetails + { + public LightningSupportedPaymentMethod LightningSupportedPaymentMethod { get; set; } + + [JsonConverter(typeof(LightMoneyJsonConverter))] + public LightMoney GeneratedBoltAmount { get; set; } + public string BTCPayInvoiceId { get; set; } + public bool Bech32Mode { get; set; } + + public string ProvidedComment { get; set; } + + public override PaymentType GetPaymentType() + { + return LNURLPayPaymentType.Instance; + } + + public override string GetAdditionalDataPartialName() + { + if (string.IsNullOrEmpty(ProvidedComment)) + { + return null; + } + + return "LNURL/AdditionalPaymentMethodDetails"; + } + } +} diff --git a/BTCPayServer/Payments/LNURLPay/LNURLPaySupportedPaymentMethod.cs b/BTCPayServer/Payments/LNURLPay/LNURLPaySupportedPaymentMethod.cs new file mode 100644 index 000000000..e7e5230fe --- /dev/null +++ b/BTCPayServer/Payments/LNURLPay/LNURLPaySupportedPaymentMethod.cs @@ -0,0 +1,22 @@ +#nullable enable +using System; +using BTCPayServer.Lightning; +using Newtonsoft.Json; + +namespace BTCPayServer.Payments.Lightning +{ + public class LNURLPaySupportedPaymentMethod : ISupportedPaymentMethod + { + public string CryptoCode { get; set; } = string.Empty; + + [JsonIgnore] + public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LNURLPay); + + public bool UseBech32Scheme { get; set; } + + public bool EnableForStandardInvoices { get; set; } = false; + + public bool LUD12Enabled { get; set; } = true; + + } +} diff --git a/BTCPayServer/Payments/LNURLPay/PaymentTypes.LNURL.cs b/BTCPayServer/Payments/LNURLPay/PaymentTypes.LNURL.cs new file mode 100644 index 000000000..5e3cc6a78 --- /dev/null +++ b/BTCPayServer/Payments/LNURLPay/PaymentTypes.LNURL.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using BTCPayServer.Client.Models; +using BTCPayServer.Controllers.GreenField; +using BTCPayServer.Payments.Lightning; +using BTCPayServer.Services.Invoices; +using NBitcoin; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Payments +{ + public class LNURLPayPaymentType : LightningPaymentType + { + public new static LNURLPayPaymentType Instance { get; } = new LNURLPayPaymentType(); + public override string ToPrettyString() => "LNURL-Pay"; + public override string GetId() => "LNURLPAY"; + public override string ToStringNormalized() => "LNURLPAY"; + public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str) + { + return JsonConvert.DeserializeObject(str); + } + + public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, + JToken value) + { + return JsonConvert.DeserializeObject(value.ToString()); + } + + public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails, + Money cryptoInfoDue, string serverUri) + { + if (!paymentMethodDetails.Activated) + { + return null; + } + var lnurlPaymentMethodDetails = (LNURLPayPaymentMethodDetails)paymentMethodDetails; + var uri = new Uri( + $"{serverUri.WithTrailingSlash()}{network.CryptoCode}/lnurl/pay/i/{lnurlPaymentMethodDetails.BTCPayInvoiceId}"); + return LNURL.LNURL.EncodeUri(uri, "payRequest", lnurlPaymentMethodDetails.Bech32Mode).ToString(); + } + + public override string InvoiceViewPaymentPartialName { get; } = "Lightning/ViewLightningLikePaymentData"; + public override object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore) + { + if (supportedPaymentMethod is LNURLPaySupportedPaymentMethod lightningSupportedPaymentMethod) + return new LNURLPayPaymentMethodBaseData() + { + UseBech32Scheme = lightningSupportedPaymentMethod.UseBech32Scheme, + EnableForStandardInvoices = lightningSupportedPaymentMethod.EnableForStandardInvoices, + LUD12Enabled = lightningSupportedPaymentMethod.LUD12Enabled + }; + return null; + } + + public override bool IsPaymentType(string paymentType) + { + return IsPaymentTypeBase(paymentType); + } + + public override void PopulateCryptoInfo(PaymentMethod details, InvoiceCryptoInfo invoiceCryptoInfo, string serverUrl) + { + invoiceCryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls() + { + AdditionalData = new Dictionary() + { + {"LNURLP", JToken.FromObject(GetPaymentLink(details.Network, details.GetPaymentMethodDetails(), invoiceCryptoInfo.Due, + serverUrl))} + } + }; + } + } +} diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs index 7e16216c4..aed52bd31 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs @@ -16,6 +16,7 @@ namespace BTCPayServer.Payments.Lightning public string BOLT11 { get; set; } [JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))] public uint256 PaymentHash { get; set; } + public string PaymentType { get; set; } public string GetDestination() { @@ -33,7 +34,7 @@ namespace BTCPayServer.Payments.Lightning public PaymentType GetPaymentType() { - return PaymentTypes.LightningLike; + return string.IsNullOrEmpty(PaymentType) ? PaymentTypes.LightningLike : PaymentTypes.Parse(PaymentType); } public string[] GetSearchTerms() diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index c790deabf..17a9c4a88 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -68,7 +68,7 @@ namespace BTCPayServer.Payments.Lightning } //direct casting to (BTCPayNetwork) is fixed in other pull requests with better generic interfacing for handlers var storeBlob = store.GetStoreBlob(); - var test = GetNodeInfo(supportedPaymentMethod, network, paymentMethod.PreferOnion); + var nodeInfo = GetNodeInfo(supportedPaymentMethod, network, logs, paymentMethod.PreferOnion); var invoice = paymentMethod.ParentEntity; decimal due = Extensions.RoundUp(invoice.Price / paymentMethod.Rate, network.Divisibility); @@ -109,56 +109,80 @@ namespace BTCPayServer.Payments.Lightning } } - var nodeInfo = await test; return new LightningLikePaymentMethodDetails { Activated = true, BOLT11 = lightningInvoice.BOLT11, PaymentHash = BOLT11PaymentRequest.Parse(lightningInvoice.BOLT11, network.NBitcoinNetwork).PaymentHash, InvoiceId = lightningInvoice.Id, - NodeInfo = nodeInfo.First().ToString() + NodeInfo = (await nodeInfo).FirstOrDefault()?.ToString() }; } - public async Task GetNodeInfo(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, bool? preferOnion = null) + public async Task GetNodeInfo(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceLogs invoiceLogs, bool? preferOnion = null) { if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary)) throw new PaymentMethodUnavailableException("Full node not available"); - using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT)) + try { - var client = supportedPaymentMethod.CreateLightningClient(network, Options.Value, _lightningClientFactory); - LightningNodeInformation info; - try + using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT)) { - info = await client.GetInfo(cts.Token); - } - catch (OperationCanceledException) when (cts.IsCancellationRequested) - { - throw new PaymentMethodUnavailableException("The lightning node did not reply in a timely manner"); - } - catch (Exception ex) - { - throw new PaymentMethodUnavailableException($"Error while connecting to the API: {ex.Message}" + - (!string.IsNullOrEmpty(ex.InnerException?.Message) ? $" ({ex.InnerException.Message})" : "")); - } + var client = CreateLightningClient(supportedPaymentMethod, network); + LightningNodeInformation info; + try + { + info = await client.GetInfo(cts.Token); + } + catch (OperationCanceledException) when (cts.IsCancellationRequested) + { + throw new PaymentMethodUnavailableException("The lightning node did not reply in a timely manner"); + } + catch (Exception ex) + { + throw new PaymentMethodUnavailableException($"Error while connecting to the API: {ex.Message}" + + (!string.IsNullOrEmpty(ex.InnerException?.Message) ? $" ({ex.InnerException.Message})" : "")); + } - var nodeInfo = preferOnion != null && info.NodeInfoList.Any(i => i.IsTor == preferOnion) - ? info.NodeInfoList.Where(i => i.IsTor == preferOnion.Value).ToArray() - : info.NodeInfoList.Select(i => i).ToArray(); + var nodeInfo = preferOnion != null && info.NodeInfoList.Any(i => i.IsTor == preferOnion) + ? info.NodeInfoList.Where(i => i.IsTor == preferOnion.Value).ToArray() + : info.NodeInfoList.Select(i => i).ToArray(); - if (!nodeInfo.Any()) - { - throw new PaymentMethodUnavailableException("No lightning node public address has been configured"); - } + // Maybe the user does not have an easily accessible ln node. Node info should be optional. The UI also supports this. + // if (!nodeInfo.Any()) + // { + // throw new PaymentMethodUnavailableException("No lightning node public address has been configured"); + // } - var blocksGap = summary.Status.ChainHeight - info.BlockHeight; - if (blocksGap > 10) - { - throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)"); - } + var blocksGap = summary.Status.ChainHeight - info.BlockHeight; + if (blocksGap > 10) + { + throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)"); + } - return nodeInfo; + return nodeInfo; + } + } + catch(Exception e) + { + invoiceLogs.Write($"NodeInfo failed to be fetched: {e.Message}", InvoiceEventData.EventSeverity.Error); + } + + return Array.Empty(); + } + + public ILightningClient CreateLightningClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network) + { + var external = supportedPaymentMethod.GetExternalLightningUrl(); + if (external != null) + { + return _lightningClientFactory.Create(external, network); + } + else + { + if (!Options.Value.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode, out var connectionString)) + throw new PaymentMethodUnavailableException("No internal node configured"); + return _lightningClientFactory.Create(connectionString, network); } } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs index 13cb1b4d4..306e64dab 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs @@ -10,7 +10,7 @@ namespace BTCPayServer.Payments.Lightning public string InvoiceId { get; set; } public string NodeInfo { get; set; } - public string GetPaymentDestination() + public virtual string GetPaymentDestination() { return BOLT11; } @@ -20,7 +20,7 @@ namespace BTCPayServer.Payments.Lightning return PaymentHash ?? BOLT11PaymentRequest.Parse(BOLT11, network).PaymentHash; } - public PaymentType GetPaymentType() + public virtual PaymentType GetPaymentType() { return PaymentTypes.LightningLike; } @@ -35,5 +35,10 @@ namespace BTCPayServer.Payments.Lightning return 0.0m; } public bool Activated { get; set; } + + public virtual string GetAdditionalDataPartialName() + { + return null; + } } } diff --git a/BTCPayServer/Payments/Lightning/LightningListener.cs b/BTCPayServer/Payments/Lightning/LightningListener.cs index 3d9e4c995..b8eeb64f1 100644 --- a/BTCPayServer/Payments/Lightning/LightningListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningListener.cs @@ -104,21 +104,42 @@ namespace BTCPayServer.Payments.Lightning } } } + + private string GetCacheKey(string invoiceId) + { + return $"{nameof(GetListenedInvoices)}-{invoiceId}"; + } private Task> GetListenedInvoices(string invoiceId) { - return _memoryCache.GetOrCreateAsync($"{nameof(GetListenedInvoices)}-{invoiceId}", async (cacheEntry) => + return _memoryCache.GetOrCreateAsync( GetCacheKey(invoiceId), async (cacheEntry) => { var listenedInvoices = new List(); var invoice = await _InvoiceRepository.GetInvoice(invoiceId); foreach (var paymentMethod in invoice.GetPaymentMethods() - .Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike)) + .Where(c => new []{PaymentTypes.LightningLike, LNURLPayPaymentType.Instance }.Contains(c.GetId().PaymentType))) { - var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails; - if (lightningMethod == null || !lightningMethod.Activated) - continue; - var lightningSupportedMethod = invoice.GetSupportedPaymentMethod() - .FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode); - if (lightningSupportedMethod == null) + LightningLikePaymentMethodDetails lightningMethod; + LightningSupportedPaymentMethod lightningSupportedMethod; + switch (paymentMethod.GetPaymentMethodDetails()) + { + case LNURLPayPaymentMethodDetails lnurlPayPaymentMethodDetails: + + lightningMethod = lnurlPayPaymentMethodDetails; + + lightningSupportedMethod = lnurlPayPaymentMethodDetails.LightningSupportedPaymentMethod; + + break; + case LightningLikePaymentMethodDetails { Activated: true } lightningLikePaymentMethodDetails: + lightningMethod = lightningLikePaymentMethodDetails; + lightningSupportedMethod = invoice.GetSupportedPaymentMethod() + .FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode); + + break; + default: + continue; + } + + if (lightningSupportedMethod == null || string.IsNullOrEmpty(lightningMethod.InvoiceId)) continue; var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode); @@ -164,7 +185,6 @@ namespace BTCPayServer.Payments.Lightning if (inv.State.Status == InvoiceStatusLegacy.New && inv.State.ExceptionStatus == InvoiceExceptionStatus.PaidPartial) { - var invoice = await _InvoiceRepository.GetInvoice(inv.InvoiceId); await CreateNewLNInvoiceForBTCPayInvoice(invoice); } @@ -174,6 +194,15 @@ namespace BTCPayServer.Payments.Lightning { if (inv.PaymentMethodId.PaymentType == LightningPaymentType.Instance) { + _memoryCache.Remove(GetCacheKey(inv.InvoiceId)); + _CheckInvoices.Writer.TryWrite(inv.InvoiceId); + } + })); + leases.Add(_Aggregator.Subscribe(inv => + { + if (inv.PaymentMethodId.PaymentType == LNURLPayPaymentType.Instance) + { + _memoryCache.Remove(GetCacheKey(inv.InvoiceId)); _CheckInvoices.Writer.TryWrite(inv.InvoiceId); } })); @@ -196,7 +225,7 @@ namespace BTCPayServer.Payments.Lightning private async Task CreateNewLNInvoiceForBTCPayInvoice(InvoiceEntity invoice) { var paymentMethods = invoice.GetPaymentMethods() - .Where(method => method.GetId().PaymentType == PaymentTypes.LightningLike) + .Where(method => new []{PaymentTypes.LightningLike, LNURLPayPaymentType.Instance}.Contains(method.GetId().PaymentType)) .ToArray(); var store = await _storeRepository.FindStore(invoice.StoreId); if (paymentMethods.Any()) @@ -209,8 +238,60 @@ namespace BTCPayServer.Payments.Lightning { try { - var supportedMethod = invoice + var oldDetails = (LightningLikePaymentMethodDetails) paymentMethod.GetPaymentMethodDetails(); + if (!oldDetails.Activated) + { + continue; + } + + + if (oldDetails is LNURLPayPaymentMethodDetails lnurlPayPaymentMethodDetails && !string.IsNullOrEmpty(lnurlPayPaymentMethodDetails.BOLT11)) + { + try + { + var client = _lightningLikePaymentHandler.CreateLightningClient(lnurlPayPaymentMethodDetails.LightningSupportedPaymentMethod, + (BTCPayNetwork)paymentMethod.Network); + await client.CancelInvoice(oldDetails.InvoiceId); + } + catch + { + //not a fully supported option + } + + lnurlPayPaymentMethodDetails = new LNURLPayPaymentMethodDetails() + { + Activated = lnurlPayPaymentMethodDetails.Activated, + Bech32Mode = lnurlPayPaymentMethodDetails.Bech32Mode, + InvoiceId = null, + NodeInfo = lnurlPayPaymentMethodDetails.NodeInfo, + GeneratedBoltAmount = null, + BOLT11 = null, + LightningSupportedPaymentMethod = lnurlPayPaymentMethodDetails.LightningSupportedPaymentMethod, + BTCPayInvoiceId = lnurlPayPaymentMethodDetails.BTCPayInvoiceId + }; + await _InvoiceRepository.NewPaymentDetails(invoice.Id, lnurlPayPaymentMethodDetails, + paymentMethod.Network); + + _Aggregator.Publish(new Events.InvoiceNewPaymentDetailsEvent(invoice.Id, + lnurlPayPaymentMethodDetails, paymentMethod.GetId())); + + continue; + } + + LightningSupportedPaymentMethod supportedMethod = invoice .GetSupportedPaymentMethod(paymentMethod.GetId()).First(); + + try + { + var client = _lightningLikePaymentHandler.CreateLightningClient(supportedMethod, + (BTCPayNetwork)paymentMethod.Network); + await client.CancelInvoice(oldDetails.InvoiceId); + } + catch + { + //not a fully supported option + } + var prepObj = _lightningLikePaymentHandler.PreparePayment(supportedMethod, store, paymentMethod.Network); var newPaymentMethodDetails = @@ -346,7 +427,7 @@ namespace BTCPayServer.Payments.Lightning var client = _lightningClientFactory.Create(ConnectionString, _network); LightningInvoice lightningInvoice = await client.GetInvoice(listenedInvoice.PaymentMethodDetails.InvoiceId, cancellation); if (lightningInvoice?.Status is LightningInvoiceStatus.Paid && - await AddPayment(lightningInvoice, listenedInvoice.InvoiceId)) + await AddPayment(lightningInvoice, listenedInvoice.InvoiceId,listenedInvoice.PaymentMethod.GetId().PaymentType)) { Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Payment detected via polling on {listenedInvoice.InvoiceId}"); } @@ -392,7 +473,7 @@ namespace BTCPayServer.Payments.Lightning if (notification.Status == LightningInvoiceStatus.Paid && notification.PaidAt.HasValue && notification.Amount != null) { - if (await AddPayment(notification, listenedInvoice.InvoiceId)) + if (await AddPayment(notification, listenedInvoice.InvoiceId, listenedInvoice.PaymentMethod.GetId().PaymentType)) { Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Payment detected via notification ({listenedInvoice.InvoiceId})"); } @@ -439,13 +520,14 @@ namespace BTCPayServer.Payments.Lightning bool _ErrorAlreadyLogged = false; readonly ConcurrentDictionary _ListenedInvoices = new ConcurrentDictionary(); - public async Task AddPayment(LightningInvoice notification, string invoiceId) + public async Task AddPayment(LightningInvoice notification, string invoiceId, PaymentType paymentType) { var payment = await _paymentService.AddPayment(invoiceId, notification.PaidAt.Value, new LightningLikePaymentData() { BOLT11 = notification.BOLT11, PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, _network.NBitcoinNetwork).PaymentHash, - Amount = notification.AmountReceived ?? notification.Amount, // if running old version amount received might be unavailable + Amount = notification.AmountReceived ?? notification.Amount, // if running old version amount received might be unavailable, + PaymentType = paymentType.ToString() }, _network, accounted: true); if (payment != null) { diff --git a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs index d050e51f8..8dddc3e97 100644 --- a/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs +++ b/BTCPayServer/Payments/PaymentTypes.Bitcoin.cs @@ -2,12 +2,13 @@ using System; using System.Globalization; using System.Linq; using BTCPayServer.Payments.Bitcoin; -using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using NBitcoin; using BTCPayServer.BIP78.Sender; using BTCPayServer.Client.Models; +using NBitpayClient; using Newtonsoft.Json.Linq; +using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo; namespace BTCPayServer.Payments { @@ -101,5 +102,14 @@ namespace BTCPayServer.Payments { return string.IsNullOrEmpty(paymentType) || base.IsPaymentType(paymentType); } + + public override void PopulateCryptoInfo(PaymentMethod details, InvoiceCryptoInfo cryptoInfo, + string serverUrl) + { + cryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls() + { + BIP21 = GetPaymentLink(details.Network, details.GetPaymentMethodDetails(), cryptoInfo.Due, serverUrl), + }; + } } } diff --git a/BTCPayServer/Payments/PaymentTypes.Lightning.cs b/BTCPayServer/Payments/PaymentTypes.Lightning.cs index 6b8be3b9f..be82ba7f7 100644 --- a/BTCPayServer/Payments/PaymentTypes.Lightning.cs +++ b/BTCPayServer/Payments/PaymentTypes.Lightning.cs @@ -13,7 +13,7 @@ namespace BTCPayServer.Payments { public static LightningPaymentType Instance { get; } = new LightningPaymentType(); - private LightningPaymentType() { } + private protected LightningPaymentType() { } public override string ToPrettyString() => "Off-Chain"; public override string GetId() => "LightningLike"; @@ -87,5 +87,14 @@ namespace BTCPayServer.Payments { return paymentType?.Equals("offchain", StringComparison.InvariantCultureIgnoreCase) is true || base.IsPaymentType(paymentType); } + + public override void PopulateCryptoInfo(PaymentMethod details, InvoiceCryptoInfo invoiceCryptoInfo, string serverUrl) + { + invoiceCryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls() + { + BOLT11 = GetPaymentLink(details.Network, details.GetPaymentMethodDetails(), invoiceCryptoInfo.Due, + serverUrl) + }; + } } } diff --git a/BTCPayServer/Payments/PaymentTypes.cs b/BTCPayServer/Payments/PaymentTypes.cs index 742dec6f8..77a1d6aef 100644 --- a/BTCPayServer/Payments/PaymentTypes.cs +++ b/BTCPayServer/Payments/PaymentTypes.cs @@ -17,7 +17,7 @@ namespace BTCPayServer.Payments { private static PaymentType[] _paymentTypes = { - BTCLike, LightningLike, + BTCLike, LightningLike, LNURLPay, #if ALTCOINS MoneroLike, EthereumPaymentType.Instance @@ -31,6 +31,10 @@ namespace BTCPayServer.Payments /// Lightning payment /// public static LightningPaymentType LightningLike => LightningPaymentType.Instance; + /// + /// Lightning payment + /// + public static LNURLPayPaymentType LNURLPay => LNURLPayPaymentType.Instance; #if ALTCOINS /// @@ -84,6 +88,11 @@ namespace BTCPayServer.Payments public abstract object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore); public virtual bool IsPaymentType(string paymentType) + { + return IsPaymentTypeBase(paymentType); + } + + protected bool IsPaymentTypeBase(string paymentType) { paymentType = paymentType?.ToLowerInvariant(); return new[] @@ -94,5 +103,8 @@ namespace BTCPayServer.Payments paymentType, StringComparer.InvariantCultureIgnoreCase); } + + public abstract void PopulateCryptoInfo(PaymentMethod details, Services.Invoices.InvoiceCryptoInfo invoiceCryptoInfo, + string serverUrl); } } diff --git a/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumPaymentType.cs b/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumPaymentType.cs index 211810c7c..8807f0606 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumPaymentType.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/Payments/EthereumPaymentType.cs @@ -66,6 +66,10 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Payments return null; } + + public override void PopulateCryptoInfo(PaymentMethod details, InvoiceCryptoInfo invoiceCryptoInfo, string serverUrl) + { + } } } #endif diff --git a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs index abce04128..057c799d5 100644 --- a/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs +++ b/BTCPayServer/Services/Altcoins/Monero/Payments/MoneroPaymentType.cs @@ -69,6 +69,11 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments return null; } + + public override void PopulateCryptoInfo(PaymentMethod details, InvoiceCryptoInfo invoiceCryptoInfo, string serverUrl) + { + + } } } #endif diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 063547b41..39c69460f 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -19,7 +19,15 @@ using Newtonsoft.Json.Serialization; namespace BTCPayServer.Services.Invoices { - + public class InvoiceCryptoInfo : NBitpayClient.InvoiceCryptoInfo + { + [JsonProperty("paymentUrls")] + public new InvoicePaymentUrls PaymentUrls { get; set; } + public class InvoicePaymentUrls : NBitpayClient.InvoicePaymentUrls + { + [JsonExtensionData] public Dictionary AdditionalData { get; set; } + } + } public class InvoiceMetadata { public static readonly JsonSerializer MetadataSerializer; @@ -443,19 +451,19 @@ namespace BTCPayServer.Services.Invoices Flags = new Flags() { Refundable = Refundable }, PaymentSubtotals = new Dictionary(), PaymentTotals = new Dictionary(), - SupportedTransactionCurrencies = new Dictionary(), + SupportedTransactionCurrencies = new Dictionary(), Addresses = new Dictionary(), - PaymentCodes = new Dictionary(), + PaymentCodes = new Dictionary(), ExchangeRates = new Dictionary>() }; dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id; - dto.CryptoInfo = new List(); + dto.CryptoInfo = new List(); dto.MinerFees = new Dictionary(); foreach (var info in this.GetPaymentMethods()) { var accounting = info.Calculate(); - var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo(); + var cryptoInfo = new InvoiceCryptoInfo(); var subtotalPrice = accounting.TotalDue - accounting.NetworkFee; var cryptoCode = info.GetId().CryptoCode; var details = info.GetPaymentMethodDetails(); @@ -500,39 +508,31 @@ namespace BTCPayServer.Services.Invoices }).ToList(); - if (details?.Activated is true && paymentId.PaymentType == PaymentTypes.LightningLike) + if (details?.Activated is true) { - cryptoInfo.PaymentUrls = new InvoicePaymentUrls() + + paymentId.PaymentType.PopulateCryptoInfo(info, cryptoInfo, ServerUrl); + if (paymentId.PaymentType == PaymentTypes.BTCLike) { - BOLT11 = paymentId.PaymentType.GetPaymentLink(info.Network, details, cryptoInfo.Due, - ServerUrl) - }; - } - else if (details?.Activated is true && paymentId.PaymentType == PaymentTypes.BTCLike) - { - var minerInfo = new MinerFeeInfo(); - minerInfo.TotalFee = accounting.NetworkFee.Satoshi; - minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate - .GetFee(1).Satoshi; - dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo); - cryptoInfo.PaymentUrls = new InvoicePaymentUrls() - { - BIP21 = paymentId.PaymentType.GetPaymentLink(info.Network, details, cryptoInfo.Due, - ServerUrl) - }; + var minerInfo = new MinerFeeInfo(); + minerInfo.TotalFee = accounting.NetworkFee.Satoshi; + minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate + .GetFee(1).Satoshi; + dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo); #pragma warning disable 618 - if (info.CryptoCode == "BTC") - { - dto.BTCPrice = cryptoInfo.Price; - dto.Rate = cryptoInfo.Rate; - dto.ExRates = cryptoInfo.ExRates; - dto.BitcoinAddress = cryptoInfo.Address; - dto.BTCPaid = cryptoInfo.Paid; - dto.BTCDue = cryptoInfo.Due; - dto.PaymentUrls = cryptoInfo.PaymentUrls; - } + if (info.CryptoCode == "BTC") + { + dto.BTCPrice = cryptoInfo.Price; + dto.Rate = cryptoInfo.Rate; + dto.ExRates = cryptoInfo.ExRates; + dto.BitcoinAddress = cryptoInfo.Address; + dto.BTCPaid = cryptoInfo.Paid; + dto.BTCDue = cryptoInfo.Due; + dto.PaymentUrls = cryptoInfo.PaymentUrls; + } #pragma warning restore 618 + } } dto.CryptoInfo.Add(cryptoInfo); diff --git a/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml b/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml index 04ce4c7c9..e908e063f 100644 --- a/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoicesPaymentsPartial.cshtml @@ -30,9 +30,9 @@ @payment.PaymentMethod @if (Model.ShowAddress) { - - @payment.Address - + + @payment.Address + } @payment.Rate @payment.Paid @@ -42,6 +42,12 @@ @payment.Overpaid } + var details = payment.PaymentMethodRaw.GetPaymentMethodDetails(); + var name = details.GetAdditionalDataPartialName(); + if (!string.IsNullOrEmpty(name)) + { + + } } diff --git a/BTCPayServer/Views/Shared/LNURL/AdditionalPaymentMethodDetails.cshtml b/BTCPayServer/Views/Shared/LNURL/AdditionalPaymentMethodDetails.cshtml new file mode 100644 index 000000000..d11584488 --- /dev/null +++ b/BTCPayServer/Views/Shared/LNURL/AdditionalPaymentMethodDetails.cshtml @@ -0,0 +1,12 @@ +@model BTCPayServer.Payments.LNURLPayPaymentMethodDetails + +@if (!string.IsNullOrEmpty(Model.ProvidedComment)) +{ + + + + + LNURL Comment: @Model.ProvidedComment + + +} diff --git a/BTCPayServer/Views/Shared/Lightning/LightningLikeMethodCheckout.cshtml b/BTCPayServer/Views/Shared/Lightning/LightningLikeMethodCheckout.cshtml index 85ce594e9..58d33f58e 100644 --- a/BTCPayServer/Views/Shared/Lightning/LightningLikeMethodCheckout.cshtml +++ b/BTCPayServer/Views/Shared/Lightning/LightningLikeMethodCheckout.cshtml @@ -5,7 +5,8 @@
-
+
-
-
+
+
diff --git a/BTCPayServer/Views/Shared/Lightning/LightningLikeMethodCheckoutNoScript.cshtml b/BTCPayServer/Views/Shared/Lightning/LightningLikeMethodCheckoutNoScript.cshtml index fd70cc2c1..cb8095fbe 100644 --- a/BTCPayServer/Views/Shared/Lightning/LightningLikeMethodCheckoutNoScript.cshtml +++ b/BTCPayServer/Views/Shared/Lightning/LightningLikeMethodCheckoutNoScript.cshtml @@ -5,5 +5,8 @@

@Model.InvoiceBitcoinUrl

-

Peer Info: @Model.PeerInfo

+ @if (!string.IsNullOrEmpty(Model.PeerInfo)) + { +

Peer Info: @Model.PeerInfo

+ }
diff --git a/BTCPayServer/Views/Shared/Lightning/ViewLightningLikePaymentData.cshtml b/BTCPayServer/Views/Shared/Lightning/ViewLightningLikePaymentData.cshtml index 07f44f17f..b58c52af4 100644 --- a/BTCPayServer/Views/Shared/Lightning/ViewLightningLikePaymentData.cshtml +++ b/BTCPayServer/Views/Shared/Lightning/ViewLightningLikePaymentData.cshtml @@ -3,7 +3,7 @@ @model IEnumerable @{ - var offchainPayments = Model.Where(entity => entity.GetPaymentMethodId()?.PaymentType == LightningPaymentType.Instance).Select(payment => + var offchainPayments = Model.Where(entity => entity.GetPaymentMethodId()?.PaymentType == LightningPaymentType.Instance || entity.GetPaymentMethodId()?.PaymentType == LNURLPayPaymentType.Instance).Select(payment => { var offChainPaymentData = payment.GetCryptoPaymentData() as LightningLikePaymentData; if (offChainPaymentData is null) @@ -13,7 +13,8 @@ return new OffChainPaymentViewModel() { Crypto = payment.Network.CryptoCode, - BOLT11 = offChainPaymentData.BOLT11 + BOLT11 = offChainPaymentData.BOLT11, + Type = payment.GetCryptoPaymentData().GetPaymentType() }; }).Where(model => model != null); } @@ -28,6 +29,7 @@ Crypto + Type BOLT11 @@ -36,6 +38,7 @@ { @payment.Crypto + @payment.Type.ToPrettyString()
@payment.BOLT11
} diff --git a/BTCPayServer/Views/Stores/CheckoutExperience.cshtml b/BTCPayServer/Views/Stores/CheckoutExperience.cshtml index ac0e11e64..be62b6601 100644 --- a/BTCPayServer/Views/Stores/CheckoutExperience.cshtml +++ b/BTCPayServer/Views/Stores/CheckoutExperience.cshtml @@ -12,7 +12,7 @@ {
} -

Payment

+

Invoice Settings

@if (Model.PaymentMethods.Any()) {
diff --git a/BTCPayServer/Views/Stores/Payment.cshtml b/BTCPayServer/Views/Stores/Payment.cshtml new file mode 100644 index 000000000..e82947e4b --- /dev/null +++ b/BTCPayServer/Views/Stores/Payment.cshtml @@ -0,0 +1,161 @@ +@model PaymentViewModel +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData.SetActivePageAndTitle(StoreNavPages.Payment, "Payment", Context.GetStoreData().StoreName); +} + +
+
+

Payment

+ @if (!ViewContext.ModelState.IsValid) + { +
+ } + @if (Model.IsOnchainSetup || Model.IsLightningSetup) + { +
+
+ + + +
+
+ + + + + +
+
+ + + + + +
+
+ + + + +
+ + minutes +
+ +
+
+ + + + +
+ + percent +
+ +
+
+ + + +
+ @if (Model.IsOnchainSetup) + { +
On-Chain
+ @if (Model.CanUsePayJoin) + { +
+
+ + + + + +
+ +
+ } +
+ + + + +
+ + minutes +
+ +
+
+ + + + + + + +
+
+ + +

Fee will be shown for BTC and LTC onchain payments only.

+
+
+ + + +
+ } + + @if (Model.IsLightningSetup) + { +
Lightning
+
+ + +
+
+ + +
+
+ + +
+
+ + + +

+ Available placeholders: + {StoreName} {ItemDescription} {OrderId} +

+
+ } + +
+ } + else + { +

+ Please configure either an on-chain wallet or Lightning node first. +

+ } +
+
+ +@section PageFootContent { + +} diff --git a/BTCPayServer/Views/Stores/SetupLightningNode.cshtml b/BTCPayServer/Views/Stores/SetupLightningNode.cshtml index 0f6f05651..cb3499f1b 100644 --- a/BTCPayServer/Views/Stores/SetupLightningNode.cshtml +++ b/BTCPayServer/Views/Stores/SetupLightningNode.cshtml @@ -7,7 +7,7 @@

@ViewData["Title"]

- +

Please understand that the Lightning Network is still under active development and considered experimental. Before you proceed, take time to familiarize yourself with the risks. @@ -178,11 +178,50 @@

+ +
+
+ + +
+
+
LNURL settings
+
+
+ + + +
+

For wallet compatibility: Bech32 encoded (classic) vs. cleartext URL (upcoming)

+
+
+
+ + +
+

Required for Lightning Address, the pay button and apps.

+
+
+
+ + +
+

Performance: Turn it off if users should pay only via LNURL.

+
+
+
+ + +
+
+
+
+
@section PageFootContent { - + } diff --git a/BTCPayServer/Views/Stores/StoreNavPages.cs b/BTCPayServer/Views/Stores/StoreNavPages.cs index 4fc87f5ca..fecec960e 100644 --- a/BTCPayServer/Views/Stores/StoreNavPages.cs +++ b/BTCPayServer/Views/Stores/StoreNavPages.cs @@ -2,8 +2,6 @@ namespace BTCPayServer.Views.Stores { public enum StoreNavPages { - Index, Create, Rates, Checkout, Tokens, Users, PayButton, Integrations, Wallet, Webhooks, ActivePage, - PullPayments, - Payouts + Index, Create, Rates, Payment, Checkout, Tokens, Users, PayButton, Integrations, Wallet, Webhooks, ActivePage, PullPayments, Payouts } } diff --git a/BTCPayServer/Views/Stores/UpdateStore.cshtml b/BTCPayServer/Views/Stores/UpdateStore.cshtml index b81f8e443..9a9b3b510 100644 --- a/BTCPayServer/Views/Stores/UpdateStore.cshtml +++ b/BTCPayServer/Views/Stores/UpdateStore.cshtml @@ -182,136 +182,7 @@
-
- - - -
- - @if (Model.IsOnchainSetup || Model.IsLightningSetup) - { -

Payment

-
- - - - - -
-
- - - - - -
-
- - - - -
- - minutes -
- -
-
- - - - -
- - percent -
- -
- @if (Model.IsOnchainSetup) - { -
On-Chain
- @if (Model.CanUsePayJoin) - { -
-
- - - - - -
- -
- } -
- - - - -
- - minutes -
- -
-
- - - - - - - -
-
- - -

Fee will be shown for BTC and LTC onchain payments only.

-
-
- - - -
- } - - @if (Model.IsLightningSetup) - { -
Lightning
-
- - -
-
- - -
-
- - -
-
- - - -

- Available placeholders: - {StoreName} {ItemDescription} {OrderId} -

-
- } - } + @@ -356,9 +227,4 @@ @section PageFootContent { - } diff --git a/BTCPayServer/Views/Stores/_Nav.cshtml b/BTCPayServer/Views/Stores/_Nav.cshtml index f67d5e3c2..797bff77c 100644 --- a/BTCPayServer/Views/Stores/_Nav.cshtml +++ b/BTCPayServer/Views/Stores/_Nav.cshtml @@ -1,13 +1,14 @@ diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-payment-methods.lnurl.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-payment-methods.lnurl.json new file mode 100644 index 000000000..2ccd4f849 --- /dev/null +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores-payment-methods.lnurl.json @@ -0,0 +1,291 @@ +{ + "paths": { + "/api/v1/stores/{storeId}/payment-methods/LNURL": { + "get": { + "tags": [ + "Store Payment Methods (LNURL)" + ], + "summary": "Get store LNURL payment methods", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to fetch", + "schema": { + "type": "string" + } + }, + { + "name": "enabled", + "in": "query", + "required": false, + "description": "Fetch payment methods that are enabled/disabled only", + "schema": { + "type": "boolean" + } + } + ], + "description": "View information about the stores' configured LNURL payment methods", + "operationId": "StoreLNURLPayPaymentMethods_GetLNURLPayPaymentMethods", + "responses": { + "200": { + "description": "list of payment methods", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LNURLPayPaymentMethodDataList" + } + } + } + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + } + }, + "/api/v1/stores/{storeId}/payment-methods/LNURL/{cryptoCode}": { + "get": { + "tags": [ + "Store Payment Methods (LNURL Pay)" + ], + "summary": "Get store LNURL Pay payment method", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to fetch", + "schema": { + "type": "string" + } + }, + { + "name": "cryptoCode", + "in": "path", + "required": true, + "description": "The crypto code of the payment method to fetch", + "schema": { + "type": "string" + } + } + ], + "description": "View information about the specified payment method", + "operationId": "StoreLNURLPayPaymentMethods_GetLNURLPayPaymentMethod", + "responses": { + "200": { + "description": "specified payment method", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LNURLPayPaymentMethodData" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to view the specified store" + }, + "404": { + "description": "The key is not found for this store/payment method" + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + }, + "put": { + "tags": [ + "Store Payment Methods (LNURL Pay)" + ], + "summary": "Update store LNURL Pay payment method", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to fetch", + "schema": { + "type": "string" + } + }, + { + "name": "cryptoCode", + "in": "path", + "required": true, + "description": "The crypto code of the payment method to update", + "schema": { + "type": "string" + } + } + ], + "description": "Update the specified store's payment method", + "requestBody": { + "x-name": "request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LNURLPayPaymentMethodData" + } + } + }, + "required": true, + "x-position": 1 + }, + "operationId": "StoreLNURLPayPaymentMethods_UpdateLNURLPayPaymentMethod", + "responses": { + "200": { + "description": "updated specified payment method", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LNURLPayPaymentMethodData" + } + } + } + }, + "400": { + "description": "A list of errors that occurred when updating the store payment method", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to update the specified store" + }, + "404": { + "description": "The key is not found for this store" + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + }, + "delete": { + "tags": [ + "Store Payment Methods (LNURL Pay)" + ], + "summary": "Remove store LNURL Pay payment method", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store to fetch", + "schema": { + "type": "string" + } + }, + { + "name": "cryptoCode", + "in": "path", + "required": true, + "description": "The crypto code of the payment method to update", + "schema": { + "type": "string" + } + } + ], + "description": "Removes the specified store payment method.", + "responses": { + "200": { + "description": "The payment method has been removed" + }, + "400": { + "description": "A list of errors that occurred when removing the payment method", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationProblemDetails" + } + } + } + }, + "403": { + "description": "If you are authenticated but forbidden to remove the specified payment method" + }, + "404": { + "description": "The key is not found for this store/payment-method" + } + }, + "security": [ + { + "API Key": [ + "btcpay.store.canmodifystoresettings" + ], + "Basic": [] + } + ] + } + } + }, + "components": { + "schemas": { + "LNURLPayPaymentMethodDataList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LNURLPayPaymentMethodData" + } + }, + "LNURLPayPaymentMethodBaseData": { + "type": "object", + "additionalProperties": false, + "properties": { + "useBech32Scheme": { + "type": "boolean", + "description": "Whether to use [LUD-01](https://github.com/fiatjaf/lnurl-rfc/blob/luds/01.md)'s bech32 format or to use [LUD-17](https://github.com/fiatjaf/lnurl-rfc/blob/luds/17.md) url formatting. " + }, + "enableForStandardInvoices": { + "type": "boolean", + "description": "Whether to allow this payment method to also be used for standard invoices and not just topup invoices." + }, + "lud12Enabled": { + "type": "boolean", + "description": "Allow comments to be passed on via lnurl." + } + } + }, + "LNURLPayPaymentMethodData": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/LNURLPayPaymentMethodBaseData" + }, + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether the payment method is enabled. Note that this can only enabled when a Lightning Network payment method is available and enabled" + }, + "cryptoCode": { + "type": "string", + "description": "Crypto code of the payment method" + } + } + } + } + }, + "tags": [ + { + "name": "Store Payment Methods (LNURL Pay)" + } + ] +}