Improve Lightning setup page (#2348)

* Redesign Lightning setup page

* Improve Lightning setup page

Closes #1811.

* Test fix

* Fix LightningNetworkPaymentMethodAPITests

* Bootstrap customization fix
This commit is contained in:
d11n 2021-03-31 13:23:36 +02:00 committed by GitHub
parent d24964e900
commit 76985838c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 164 additions and 160 deletions

View file

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

View file

@ -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()

View file

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

View file

@ -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<StoresController>();
var storeController = GetController<StoresController>();
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);
}

View file

@ -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<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
[HttpPost]
[Route("{storeId}/lightning/{cryptoCode}")]
[HttpPost("{storeId}/lightning/{cryptoCode}")]
public async Task<IActionResult> 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<LightningLikePaymentHandler>();
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<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
}
}

View file

@ -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; }
}
}

View file

@ -7,49 +7,40 @@
<partial name="_StatusMessage" />
<div class="alert alert-warning alert-dismissible mb-5" role="alert">
<h4 class="alert-heading">Warning</h4>
<p>
<span>Before you proceed, please understand that the Lightning Network is still considered experimental and is under active development.</span>
<p class="mb-0">
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.
<a href="https://docs.btcpayserver.org/LightningNetwork/" class="alert-link">More information</a>
</p>
<p><strong>Do not add money that you can't afford to lose - there's a high risk of loss of funds.</strong></p>
<p class="mb-2">Take time to familiarize yourself with the risks, some of which are:</p>
<ul class="mb-4">
<li>Most BTCPay Server deployments run on a pruned node which, while working, is not officially supported by lightning network implementations.</li>
<li>
Lightning keys are NOT automatically backed up by BTCPay Server. Your keys are in a hot-wallet. This means:
<ul>
<li>If you erase your BTCPay Server virtual machine: you lose all the funds.</li>
<li>If your server gets hacked: a hacker can take all of your funds by accessing your keys.</li>
<li>If there is a bug in a lightning network implementation: you could lose all the funds.</li>
</ul>
</li>
</ul>
<hr>
<p class="mb-2">To proceed, please ensure that:</p>
<ul>
<li>You accept being #reckless and being the sole responsible party for any loss of funds.</li>
<li>You agree to keep on your lightning node only what you can afford to lose.</li>
</ul>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="row">
<div class="col">
<h3 class="mb-3">Lightning Node Connection</h3>
<form method="post">
<div class="form-group">
<p class="mb-2">The connection string encapsulates the configuration for connecting to your lightning node. BTCPay Server currently supports:</p>
<style>
#CustomSetup { display: none; }
#LightningNodeType-Custom:checked + * + #CustomSetup { display: block; }
</style>
<form method="post">
<div class="form-group">
<div class="custom-control custom-radio">
<input asp-for="LightningNodeType" value="@LightningNodeType.Internal" type="radio" id="LightningNodeType-@LightningNodeType.Internal" class="custom-control-input" disabled="@(!Model.CanUseInternalNode)">
<label asp-for="LightningNodeType" for="@($"LightningNodeType-{LightningNodeType.Internal}")" class="custom-control-label">Use the internal Lightning node</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-radio">
<input asp-for="LightningNodeType" value="@LightningNodeType.Custom" type="radio" id="LightningNodeType-@LightningNodeType.Custom" class="custom-control-input">
<label asp-for="LightningNodeType" for="@($"LightningNodeType-{LightningNodeType.Custom}")" class="custom-control-label">Use a custom Lightning node <span class="text-muted">(requires connection string)</span></label>
<div id="CustomSetup">
<div class="form-group my-3">
<label asp-for="ConnectionString">The connection string configuration for your custom Lightning node:</label>
<input asp-for="ConnectionString" class="form-control" placeholder="type=…;server=…;" value="@(Model.LightningNodeType == LightningNodeType.Internal ? "" : Model.ConnectionString)" />
<span asp-validation-for="ConnectionString" class="text-danger"></span>
</div>
<p class="mt-4 mb-2">BTCPay Server currently supports:</p>
<ul>
<li class="mb-2">
<strong>Internal node</strong>, if you are administrator of the server:
<ul>
<li>
<code>Internal Node</code>
</li>
</ul>
</li>
<li class="mb-2">
<strong>c-lightning</strong> via TCP or unix domain socket connection:
<ul>
@ -105,37 +96,30 @@
</li>
</ul>
</div>
</div>
<div class="form-group">
<label asp-for="ConnectionString"></label>
<input asp-for="ConnectionString" class="form-control" />
<span asp-validation-for="ConnectionString" class="text-danger"></span>
@if (Model.CanUseInternalNode)
{
<p class="form-text text-muted">
Use the internal lightning node of this BTCPay Server instance by
<a href="#" id="internal-ln-node-setter" onclick="$('#ConnectionString').val('Internal Node');return false;">clicking here</a>.
</p>
}
</div>
<div class="form-group form-check">
<input asp-for="Enabled" type="checkbox" class="form-check-input" />
<label asp-for="Enabled" class="form-check-label"></label>
</div>
<button id="save" name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
<button name="command" type="submit" value="test" class="btn btn-secondary mr-3">Test connection</button>
<a class="text-secondary"
asp-controller="PublicLightningNodeInfo"
asp-action="ShowLightningNodeInfo"
asp-route-cryptoCode="@Model.CryptoCode"
asp-route-storeId="@Model.StoreId"
target="_blank">
<span class="fa fa-info-circle" title="More information..."></span>
Open Public Node Info Page
</a>
</form>
</div>
</div>
<div class="form-group mt-4 mb-5">
<label asp-for="Enabled" class="form-check-label"></label>
<input asp-for="Enabled" type="checkbox" class="btcpay-toggle ml-2" />
</div>
<div>
<button id="save" name="command" type="submit" value="save" class="btn btn-primary mr-2">Submit</button>
<button id="test" name="command" type="submit" value="test" class="btn btn-secondary mr-3">Test connection</button>
<a class="text-secondary"
asp-controller="PublicLightningNodeInfo"
asp-action="ShowLightningNodeInfo"
asp-route-cryptoCode="@Model.CryptoCode"
asp-route-storeId="@Model.StoreId"
target="_blank">
<span class="fa fa-info-circle" title="More information..."></span>
Open Public Node Info Page
</a>
</div>
</form>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")