From 76985838c4c780bf474e172d289fcecf40ac858a Mon Sep 17 00:00:00 2001 From: d11n Date: Wed, 31 Mar 2021 13:23:36 +0200 Subject: [PATCH] Improve Lightning setup page (#2348) * Redesign Lightning setup page * Improve Lightning setup page Closes #1811. * Test fix * Fix LightningNetworkPaymentMethodAPITests * Bootstrap customization fix --- BTCPayServer.Tests/CheckoutUITests.cs | 2 +- BTCPayServer.Tests/SeleniumTester.cs | 47 +++++--- BTCPayServer.Tests/SeleniumTests.cs | 34 +++--- BTCPayServer.Tests/TestAccount.cs | 8 +- .../StoresController.LightningLike.cs | 94 ++++++++------- .../StoreViewModels/LightningNodeViewModel.cs | 25 ++-- .../Views/Stores/AddLightningNode.cshtml | 114 ++++++++---------- 7 files changed, 164 insertions(+), 160 deletions(-) diff --git a/BTCPayServer.Tests/CheckoutUITests.cs b/BTCPayServer.Tests/CheckoutUITests.cs index 13ad0850a..3f92e8913 100644 --- a/BTCPayServer.Tests/CheckoutUITests.cs +++ b/BTCPayServer.Tests/CheckoutUITests.cs @@ -111,7 +111,7 @@ namespace BTCPayServer.Tests s.GoToRegister(); s.RegisterNewUser(true); var store = s.CreateNewStore(); - s.AddInternalLightningNode("BTC"); + s.AddLightningNode(); s.GoToStore(store.storeId, StoreNavPages.Checkout); s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true); var command = s.Driver.FindElement(By.Name("command")); diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index fedef984d..2bb61b37b 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -188,28 +188,39 @@ namespace BTCPayServer.Tests FindAlertMessage(); } - public void AddLightningNode(string cryptoCode, LightningConnectionType connectionType) + public void AddLightningNode(string cryptoCode = "BTC", LightningConnectionType? connectionType = null) { - string connectionString; - if (connectionType == LightningConnectionType.Charge) - connectionString = $"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true"; - else if (connectionType == LightningConnectionType.CLightning) - connectionString = "type=clightning;server=" + ((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri; - else if (connectionType == LightningConnectionType.LndREST) - connectionString = $"type=lnd-rest;server={Server.MerchantLnd.Swagger.BaseUrl};allowinsecure=true"; + Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click(); + + var connectionString = connectionType switch + { + LightningConnectionType.Charge => + $"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true", + LightningConnectionType.CLightning => + $"type=clightning;server={((CLightningClient) Server.MerchantLightningD).Address.AbsoluteUri}", + LightningConnectionType.LndREST => + $"type=lnd-rest;server={Server.MerchantLnd.Swagger.BaseUrl};allowinsecure=true", + _ => null + }; + + if (connectionString == null) + { + Assert.True(Driver.FindElement(By.Id("LightningNodeType-Internal")).Enabled, "Usage of the internal Lightning node is disabled."); + Driver.FindElement(By.CssSelector("label[for=\"LightningNodeType-Internal\"]")).Click(); + } else - throw new NotSupportedException(connectionType.ToString()); + { + Driver.FindElement(By.CssSelector("label[for=\"LightningNodeType-Custom\"]")).Click(); + Driver.FindElement(By.Id("ConnectionString")).SendKeys(connectionString); + } - Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click(); - Driver.FindElement(By.Name($"ConnectionString")).SendKeys(connectionString); - Driver.FindElement(By.Id($"save")).Click(); - } + var enabled = Driver.FindElement(By.Id("Enabled")); + if (!enabled.Selected) enabled.Click(); - public void AddInternalLightningNode(string cryptoCode) - { - Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click(); - Driver.FindElement(By.Id($"internal-ln-node-setter")).Click(); - Driver.FindElement(By.Id($"save")).Click(); + Driver.FindElement(By.Id("test")).Click(); + Assert.Contains("Connection to the Lightning node succeeded.", FindAlertMessage().Text); + + Driver.FindElement(By.Id("save")).Click(); } public void ClickOnAllSideMenus() diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index b3c18db61..945f7e08c 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -313,13 +313,15 @@ namespace BTCPayServer.Tests } [Fact(Timeout = TestTimeout)] + [Trait("Lightning", "Lightning")] public async Task CanCreateStores() { using (var s = SeleniumTester.Create()) { + s.Server.ActivateLightning(); await s.StartAsync(); - var alice = s.RegisterNewUser(); - var storeData = s.CreateNewStore(); + var alice = s.RegisterNewUser(true); + var (storeName, storeId) = s.CreateNewStore(); var onchainHint = "Set up your wallet to receive payments at your store."; var offchainHint = "A connection to a Lightning node is required to receive Lightning payments."; @@ -328,23 +330,31 @@ namespace BTCPayServer.Tests Assert.True(s.Driver.PageSource.Contains(offchainHint), "Lightning hint not present"); s.GoToStores(); - Assert.True(s.Driver.PageSource.Contains("warninghint_" + storeData.storeId), - "Warning hint on list not present"); + Assert.True(s.Driver.PageSource.Contains($"warninghint_{storeId}"), "Warning hint on list not present"); - s.GoToStore(storeData.storeId); + s.GoToStore(storeId); + Assert.Contains(storeName, s.Driver.PageSource); Assert.True(s.Driver.PageSource.Contains(onchainHint), "Wallet hint should be present at this point"); Assert.True(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be present at this point"); - s.AddDerivationScheme(); // wallet hint should be dismissed + // setup onchain wallet + s.GoToStore(storeId); + s.AddDerivationScheme(); s.Driver.AssertNoError(); - Assert.False(s.Driver.PageSource.Contains(onchainHint), - "Wallet hint not dismissed on derivation scheme add");// dismiss lightning hint + Assert.False(s.Driver.PageSource.Contains(onchainHint), "Wallet hint not dismissed on derivation scheme add"); + + // setup offchain wallet + s.GoToStore(storeId); + s.AddLightningNode(); + s.Driver.AssertNoError(); + var successAlert = s.FindAlertMessage(); + Assert.Contains("BTC Lightning node modified.", successAlert.Text); + Assert.False(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be dismissed at this point"); - Assert.Contains(storeData.storeName, s.Driver.PageSource); var storeUrl = s.Driver.Url; s.ClickOnAllSideMenus(); s.GoToInvoices(); - var invoiceId = s.CreateInvoice(storeData.storeName); + var invoiceId = s.CreateInvoice(storeName); s.FindAlertMessage(); s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); var invoiceUrl = s.Driver.Url; @@ -399,10 +409,6 @@ namespace BTCPayServer.Tests s.Logout(); s.LogIn(alice); s.Driver.FindElement(By.Id("Stores")).Click(); - - // there shouldn't be any hints now - Assert.False(s.Driver.PageSource.Contains(offchainHint), "Lightning hint should be dismissed at this point"); - s.Driver.FindElement(By.LinkText("Remove")).Click(); s.Driver.FindElement(By.Id("continue")).Click(); s.Driver.FindElement(By.Id("Stores")).Click(); diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 731b2be06..9c062a26a 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -24,6 +24,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis.Operations; using NBitcoin; using BTCPayServer.BIP78.Sender; +using BTCPayServer.Payments.Lightning; using NBitcoin.Payment; using NBitpayClient; using NBXplorer.DerivationStrategy; @@ -267,12 +268,13 @@ namespace BTCPayServer.Tests } public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType? connectionType, bool isMerchant = true, string storeId = null) { - var storeController = this.GetController(); + var storeController = GetController(); - string connectionString = parent.GetLightningConnectionString(connectionType, isMerchant); + var connectionString = parent.GetLightningConnectionString(connectionType, isMerchant); + var nodeType = connectionString == LightningSupportedPaymentMethod.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom; await storeController.AddLightningNode(storeId ?? StoreId, - new LightningNodeViewModel() { ConnectionString = connectionString, SkipPortTest = true }, "save", "BTC"); + new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true }, "save", "BTC"); if (storeController.ModelState.ErrorCount != 0) Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage); } diff --git a/BTCPayServer/Controllers/StoresController.LightningLike.cs b/BTCPayServer/Controllers/StoresController.LightningLike.cs index c352a1275..ed93635e9 100644 --- a/BTCPayServer/Controllers/StoresController.LightningLike.cs +++ b/BTCPayServer/Controllers/StoresController.LightningLike.cs @@ -14,15 +14,14 @@ namespace BTCPayServer.Controllers { public partial class StoresController { - - [HttpGet] - [Route("{storeId}/lightning/{cryptoCode}")] + [HttpGet("{storeId}/lightning/{cryptoCode}")] public IActionResult AddLightningNode(string storeId, string cryptoCode) { var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); - LightningNodeViewModel vm = new LightningNodeViewModel + + var vm = new LightningNodeViewModel { CryptoCode = cryptoCode, StoreId = storeId @@ -31,62 +30,50 @@ namespace BTCPayServer.Controllers return View(vm); } - private void SetExistingValues(StoreData store, LightningNodeViewModel vm) - { - if (GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store) is LightningSupportedPaymentMethod paymentMethod) - { - vm.ConnectionString = paymentMethod.GetDisplayableConnectionString(); - } - vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike)); - vm.CanUseInternalNode = CanUseInternalLightning(); - } - private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store) - { - var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); - var existing = store.GetSupportedPaymentMethods(_NetworkProvider) - .OfType() - .FirstOrDefault(d => d.PaymentId == id); - return existing; - } - [HttpPost] - [Route("{storeId}/lightning/{cryptoCode}")] + [HttpPost("{storeId}/lightning/{cryptoCode}")] public async Task AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode) { vm.CryptoCode = cryptoCode; var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); - var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode); + vm.CanUseInternalNode = CanUseInternalLightning(); + + var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode); if (network == null) { ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network"); return View(vm); } - PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike); - Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null; - if (vm.ConnectionString == LightningSupportedPaymentMethod.InternalNode) + var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike); + LightningSupportedPaymentMethod paymentMethod = null; + if (vm.LightningNodeType == LightningNodeType.Internal) { if (!CanUseInternalLightning()) { - ModelState.AddModelError(nameof(vm.ConnectionString), $"You are not authorized to use the internal lightning node"); + ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use the internal lightning node"); return View(vm); } - paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod() + paymentMethod = new LightningSupportedPaymentMethod { CryptoCode = paymentMethodId.CryptoCode }; paymentMethod.SetInternalNode(); } - else if (!string.IsNullOrEmpty(vm.ConnectionString)) + else { + if (string.IsNullOrEmpty(vm.ConnectionString)) + { + ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string"); + return View(vm); + } if (!LightningConnectionString.TryParse(vm.ConnectionString, false, out var connectionString, out var error)) { ModelState.AddModelError(nameof(vm.ConnectionString), $"Invalid URL ({error})"); return View(vm); } - if (connectionString.ConnectionType == LightningConnectionType.LndGRPC) { ModelState.AddModelError(nameof(vm.ConnectionString), $"BTCPay does not support gRPC connections"); @@ -94,11 +81,11 @@ namespace BTCPayServer.Controllers } if (!User.IsInRole(Roles.ServerAdmin) && !connectionString.IsSafe()) { - ModelState.AddModelError(nameof(vm.ConnectionString), $"You are not a server admin, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'."); + ModelState.AddModelError(nameof(vm.ConnectionString), "You are not a server admin, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'."); return View(vm); } - paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod() + paymentMethod = new LightningSupportedPaymentMethod { CryptoCode = paymentMethodId.CryptoCode }; @@ -114,24 +101,20 @@ namespace BTCPayServer.Controllers store.SetStoreBlob(storeBlob); store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod); await _Repo.UpdateStore(store); - TempData[WellKnownTempData.SuccessMessage] = $"Lightning node modified ({network.CryptoCode})"; - return RedirectToAction(nameof(UpdateStore), new { storeId = storeId }); - case "test" when paymentMethod == null: - ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter"); - return View(vm); + TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node modified."; + return RedirectToAction(nameof(UpdateStore), new { storeId }); + case "test": var handler = _ServiceProvider.GetRequiredService(); try { - var info = await handler.GetNodeInfo(this.Request.IsOnion(), paymentMethod, network); + var info = await handler.GetNodeInfo(Request.IsOnion(), paymentMethod, network); if (!vm.SkipPortTest) { - using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20))) - { - await handler.TestConnection(info, cts.Token); - } + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + await handler.TestConnection(info, cts.Token); } - TempData[WellKnownTempData.SuccessMessage] = $"Connection to the lightning node succeeded. Your node address: {info}"; + TempData[WellKnownTempData.SuccessMessage] = $"Connection to the Lightning node succeeded. Your node address: {info}"; } catch (Exception ex) { @@ -139,13 +122,36 @@ namespace BTCPayServer.Controllers return View(vm); } return View(vm); + default: return View(vm); } } + private bool CanUseInternalLightning() { return User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowLightningInternalNodeForAll; } + + private void SetExistingValues(StoreData store, LightningNodeViewModel vm) + { + var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store); + if (lightning != null) + { + vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom; + vm.ConnectionString = lightning.GetDisplayableConnectionString(); + } + vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike)) && lightning != null; + vm.CanUseInternalNode = CanUseInternalLightning(); + } + + private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store) + { + var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); + var existing = store.GetSupportedPaymentMethods(_NetworkProvider) + .OfType() + .FirstOrDefault(d => d.PaymentId == id); + return existing; + } } } diff --git a/BTCPayServer/Models/StoreViewModels/LightningNodeViewModel.cs b/BTCPayServer/Models/StoreViewModels/LightningNodeViewModel.cs index aa99784dd..10c94d023 100644 --- a/BTCPayServer/Models/StoreViewModels/LightningNodeViewModel.cs +++ b/BTCPayServer/Models/StoreViewModels/LightningNodeViewModel.cs @@ -2,27 +2,22 @@ using System.ComponentModel.DataAnnotations; namespace BTCPayServer.Models.StoreViewModels { + public enum LightningNodeType + { + None, + Internal, + Custom + } + public class LightningNodeViewModel { + public LightningNodeType LightningNodeType { get; set; } [Display(Name = "Connection string")] - public string ConnectionString - { - get; - set; - } - - public string CryptoCode - { - get; - set; - } + public string ConnectionString { get; set; } + public string CryptoCode { get; set; } public bool CanUseInternalNode { get; set; } - public bool SkipPortTest { get; set; } - - [Display(Name="Lightning enabled")] public bool Enabled { get; set; } = true; - public string StoreId { get; set; } } } diff --git a/BTCPayServer/Views/Stores/AddLightningNode.cshtml b/BTCPayServer/Views/Stores/AddLightningNode.cshtml index dbf16ef9e..ee14bffd9 100644 --- a/BTCPayServer/Views/Stores/AddLightningNode.cshtml +++ b/BTCPayServer/Views/Stores/AddLightningNode.cshtml @@ -7,49 +7,40 @@ -
-
-

Lightning Node Connection

-
-
-

The connection string encapsulates the configuration for connecting to your lightning node. BTCPay Server currently supports:

+ + + +
+
+ + +
+
+
+
+ + +
+
+ + + +
+

BTCPay Server currently supports:

    -
  • - Internal node, if you are administrator of the server: -
      -
    • - Internal Node -
    • -
    -
  • c-lightning via TCP or unix domain socket connection:
      @@ -105,37 +96,30 @@
+
-
- - - - @if (Model.CanUseInternalNode) - { -

- Use the internal lightning node of this BTCPay Server instance by - clicking here. -

- } -
-
- - -
- - - - - Open Public Node Info Page - -
-
+ +
+ + +
+ +
+ + + + + + Open Public Node Info Page + +
+ @section Scripts { @await Html.PartialAsync("_ValidationScriptsPartial")