Show Lightning node availability in navigation (#5951)

* Show Lightning node availability in navigation

Instead of simply communicating the setup state of the store's LN node, this now also checks its availability.

Closes  #5940.

* Cleanups

* Add Selenium test for public node page and status in nav

* Cache the available lightning node result

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
d11n 2024-04-26 08:30:34 +02:00 committed by GitHub
parent d3277306cf
commit 8d429f064b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 121 additions and 26 deletions

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
@ -1965,6 +1966,60 @@ namespace BTCPayServer.Tests
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
}
[Fact]
[Trait("Selenium", "Selenium")]
[Trait("Lightning", "Lightning")]
public async Task CanManageLightningNode()
{
using var s = CreateSeleniumTester();
s.Server.ActivateLightning();
await s.StartAsync();
await s.Server.EnsureChannelsSetup();
s.RegisterNewUser(true);
(string storeName, _) = s.CreateNewStore();
// Check status in navigation
s.Driver.FindElement(By.CssSelector("#StoreNav-LightningBTC .btcpay-status--pending"));
// Set up LN node
s.AddLightningNode();
s.Driver.FindElement(By.CssSelector("#StoreNav-LightningBTC .btcpay-status--enabled"));
// Check public node info for availability
s.Driver.FindElement(By.Id("PublicNodeInfo")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.Equal(storeName, s.Driver.FindElement(By.CssSelector(".store-name")).Text);
Assert.Equal("BTC Lightning Node", s.Driver.FindElement(By.Id("LightningNodeTitle")).Text);
Assert.Equal("Online", s.Driver.FindElement(By.Id("LightningNodeStatus")).Text);
s.Driver.FindElement(By.CssSelector(".btcpay-status--enabled"));
s.Driver.FindElement(By.Id("LightningNodeUrlClearnet"));
s.Driver.Close();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// Set wrong node connection string to simulate offline node
s.GoToLightningSettings();
s.Driver.FindElement(By.Id("SetupLightningNodeLink")).Click();
s.Driver.FindElement(By.CssSelector("label[for=\"LightningNodeType-Custom\"]")).Click();
s.Driver.WaitForElement(By.Id("ConnectionString")).Clear();
s.Driver.FindElement(By.Id("ConnectionString")).SendKeys("type=lnd-rest;server=https://doesnotwork:8080/");
s.Driver.FindElement(By.Id("test")).Click();
Assert.Contains("Error", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text);
s.Driver.FindElement(By.Id("save")).Click();
Assert.Contains("BTC Lightning node updated.", s.FindAlertMessage().Text);
// Check offline state is communicated in nav item
s.Driver.FindElement(By.CssSelector("#StoreNav-LightningBTC .btcpay-status--disabled"));
// Check public node info for availability
s.Driver.FindElement(By.Id("PublicNodeInfo")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.Equal(storeName, s.Driver.FindElement(By.CssSelector(".store-name")).Text);
Assert.Equal("BTC Lightning Node", s.Driver.FindElement(By.Id("LightningNodeTitle")).Text);
Assert.Equal("Unavailable", s.Driver.FindElement(By.Id("LightningNodeStatus")).Text);
s.Driver.FindElement(By.CssSelector(".btcpay-status--disabled"));
s.Driver.AssertElementNotFound(By.Id("LightningNodeUrlClearnet"));
}
[Fact(Timeout = TestTimeout)]
public async Task CanImportWallet()
{
@ -2680,7 +2735,7 @@ namespace BTCPayServer.Tests
items = cartData.FindElements(By.CssSelector("tbody tr"));
sums = cartData.FindElements(By.CssSelector("tfoot tr"));
Assert.Equal(3, items.Count);
Assert.Equal(1, sums.Count);
Assert.Single(sums);
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text);
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector("td")).Text);
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector("th")).Text);

View File

@ -79,8 +79,11 @@
<li class="nav-item">
@if (isSetUp)
{
var status = scheme.Enabled
? scheme.Available ? "enabled" : "disabled"
: "pending";
<a asp-area="" asp-controller="UIStores" asp-action="Lightning" asp-route-cryptoCode="@scheme.CryptoCode" asp-route-storeId="@Model.Store.Id" class="nav-link @ViewData.IsActivePage(StoreNavPages.Lightning) @ViewData.IsActivePage(StoreNavPages.LightningSettings)" id="@($"StoreNav-Lightning{scheme.CryptoCode}")">
<span class="me-2 btcpay-status btcpay-status--@(scheme.Enabled ? "enabled" : "pending")"></span>
<span class="me-2 btcpay-status btcpay-status--@status"></span>
<span>@PrettyName.PrettyName(scheme.PaymentMethodId)</span>
</a>
}

View File

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
@ -14,6 +16,7 @@ using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Caching.Memory;
using NBitcoin;
using NBitcoin.Secp256k1;
@ -28,6 +31,8 @@ namespace BTCPayServer.Components.MainNav
private readonly UserManager<ApplicationUser> _userManager;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
private readonly SettingsRepository _settingsRepository;
private readonly IMemoryCache _cache;
public PoliciesSettings PoliciesSettings { get; }
public MainNav(
@ -38,6 +43,7 @@ namespace BTCPayServer.Components.MainNav
UserManager<ApplicationUser> userManager,
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
SettingsRepository settingsRepository,
IMemoryCache cache,
PoliciesSettings policiesSettings)
{
_storeRepo = storeRepo;
@ -47,6 +53,7 @@ namespace BTCPayServer.Components.MainNav
_storesController = storesController;
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
_settingsRepository = settingsRepository;
_cache = cache;
PoliciesSettings = policiesSettings;
}
@ -69,6 +76,38 @@ namespace BTCPayServer.Components.MainNav
// Wallets
_storesController.AddPaymentMethods(store, storeBlob,
out var derivationSchemes, out var lightningNodes);
foreach (var lnNode in lightningNodes)
{
var pmi = PaymentTypes.LN.GetPaymentMethodId(lnNode.CryptoCode);
if (_paymentMethodHandlerDictionary.TryGet(pmi) is not LightningLikePaymentHandler handler)
continue;
if (lnNode.CacheKey is not null)
{
using var cts = new CancellationTokenSource(5000);
try
{
lnNode.Available = await _cache.GetOrCreateAsync(lnNode.CacheKey, async entry =>
{
entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(5));
try
{
var paymentMethodDetails = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, _paymentMethodHandlerDictionary);
await handler.GetNodeInfo(paymentMethodDetails, null, throws: true);
// if we came here without exception, this means the node is available
return true;
}
catch (Exception)
{
return false;
}
}).WithCancellation(cts.Token);
}
catch when (cts.IsCancellationRequested) { }
}
}
vm.DerivationSchemes = derivationSchemes;
vm.LightningNodes = lightningNodes;

View File

@ -5,7 +5,6 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
@ -55,7 +54,6 @@ namespace BTCPayServer.Controllers
try
{
var paymentMethodDetails = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(pmi, _handlers);
var network = _BtcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var nodeInfo = await handler.GetNodeInfo(paymentMethodDetails, null, throws: true);
vm.Available = true;

View File

@ -1,6 +1,9 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Client;
@ -15,6 +18,7 @@ using BTCPayServer.Payments.Lightning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.DataEncoders;
namespace BTCPayServer.Controllers;
@ -161,10 +165,19 @@ public partial class UIStoresController
CryptoCode = lnNetwork.CryptoCode,
PaymentMethodId = handler.PaymentMethodId,
Address = lightning?.GetDisplayableConnectionString(),
Enabled = isEnabled
});
Enabled = isEnabled,
CacheKey = GetCacheKey(lightning)
});
}
}
}
private string? GetCacheKey(LightningPaymentMethodConfig? lightning)
{
if (lightning is null)
return null;
var connStr = lightning.IsInternalNode ? lightning.InternalNodeRef : lightning.ConnectionString;
connStr ??= string.Empty;
return "LN-INFO-" + Encoders.Hex.EncodeData(SHA256.HashData(Encoding.UTF8.GetBytes(connStr))[0..4]);
}
}

View File

@ -8,5 +8,7 @@ namespace BTCPayServer.Models.StoreViewModels
public PaymentMethodId PaymentMethodId { get; set; }
public string Address { get; set; }
public bool Enabled { get; set; }
public bool Available { get; set; }
public string CacheKey { get; set; }
}
}

View File

@ -1,11 +1,8 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AngleSharp.Dom;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
@ -13,17 +10,12 @@ using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.LndHub;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.Protocol;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -41,7 +33,6 @@ namespace BTCPayServer.Payments.Lightning
private readonly LightningClientFactoryService _lightningClientFactory;
private readonly BTCPayNetwork _Network;
private readonly SocketFactory _socketFactory;
private readonly DisplayFormatter _displayFormatter;
private readonly ISettingsAccessor<PoliciesSettings> _policies;
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
@ -51,7 +42,6 @@ namespace BTCPayServer.Payments.Lightning
LightningClientFactoryService lightningClientFactory,
BTCPayNetwork network,
SocketFactory socketFactory,
DisplayFormatter displayFormatter,
IOptions<LightningNetworkOptions> options,
ISettingsAccessor<PoliciesSettings> policies,
IOptions<LightningNetworkOptions> lightningNetworkOptions)
@ -61,7 +51,6 @@ namespace BTCPayServer.Payments.Lightning
_lightningClientFactory = lightningClientFactory;
_Network = network;
_socketFactory = socketFactory;
_displayFormatter = displayFormatter;
Options = options;
_policies = policies;
_lightningNetworkOptions = lightningNetworkOptions;
@ -155,7 +144,7 @@ namespace BTCPayServer.Payments.Lightning
var synced = _Dashboard.IsFullySynched(_Network.CryptoCode, out var summary);
if (supportedPaymentMethod.IsInternalNode && !synced)
throw new PaymentMethodUnavailableException("Full node not available");
;
try
{
using var cts = new CancellationTokenSource(LightningTimeout);

View File

@ -1,13 +1,8 @@
#nullable enable
using System;
using System.Linq;
#if ALTCOINS
using BTCPayServer.Services.Altcoins.Monero.Payments;
using BTCPayServer.Services.Altcoins.Zcash.Payments;
#endif
using BTCPayServer.Services.Invoices;
using NBitcoin;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments
{

View File

@ -18,13 +18,13 @@
<div class="d-flex flex-column justify-content-center gap-4">
<partial name="_StoreHeader" model="(Model.StoreName, Model.StoreBranding)" />
<section class="tile">
<h2 class="h4 card-subtitle text-center text-secondary mt-1 mb-3">
<h2 class="h4 card-subtitle text-center text-secondary mt-1 mb-3" id="LightningNodeTitle">
<span>@Model.CryptoCode</span>
Lightning Node
</h2>
<h4 class="d-flex align-items-center justify-content-center gap-2 my-4">
<span class="btcpay-status btcpay-status--@(Model.Available ? "enabled" : "disabled")" style="margin-top:.1rem;"></span>
@(Model.Available ? "Online" : "Unavailable")
<span id="LightningNodeStatus">@(Model.Available ? "Online" : "Unavailable")</span>
</h4>
@if (Model.Available)
{
@ -46,6 +46,7 @@
{
var nodeInfo = Model.NodeInfo[i];
var title = nodeInfo.IsTor ? "Tor" : "Clearnet";
var id = $"LightningNodeUrl{title}";
var value = nodeInfo.ToString();
<div class="tab-pane fade @(i == 0 ? "show active" : "")" id="nodeInfo-@i" role="tabpanel" aria-labelledby="nodeInfo-tab-@i">
<div class="payment-box">
@ -58,7 +59,7 @@
</div>
<div class="input-group mt-3">
<div class="form-floating">
<vc:truncate-center text="@value" padding="15" elastic="true" classes="form-control-plaintext" />
<vc:truncate-center text="@value" padding="15" elastic="true" classes="form-control-plaintext" id="@id"/>
<label>@title</label>
</div>
</div>