btcpayserver/BTCPayServer/Controllers/UIStoresController.LightningLike.cs
d11n 641bdcff31
Histograms: Add Lightning data and API endpoints (#6217)
* Histograms: Add Lightning data and API endpoints

Ported over from the mobile-working-branch.

Adds histogram data for Lightning and exposes the wallet/lightning histogram data via the API. It also add a dashboard graph for the Lightning balance.

Caveat: The Lightning histogram is calculated by using the current channel balance and going backwards through as much invoices and transactions as we have. The "start" of the LN graph data might not be accurate though. That's because we don't track (and not even have) the LN onchain data. It is calculated by using the current channel balance and going backwards through as much invoices and transactions as we have. So the historic graph data for LN is basically a best effort of trying to reconstruct it with what we have: The LN channel transactions.

* More timeframes

* Refactoring: Remove redundant WalletHistogram types

* Remove store property from dashboard tile view models

* JS error fixes
2024-11-05 21:40:37 +09:00

377 lines
15 KiB
C#

#nullable enable
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Components.StoreLightningBalance;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers;
public partial class UIStoresController
{
[HttpGet("{storeId}/lightning/{cryptoCode}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult Lightning(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var vm = new LightningViewModel
{
CryptoCode = cryptoCode,
StoreId = storeId
};
SetExistingValues(store, vm);
if (vm.LightningNodeType == LightningNodeType.Internal)
{
var services = _externalServiceOptions.Value.ExternalServices.ToList()
.Where(service => ExternalServices.LightningServiceTypes.Contains(service.Type))
.Select(async service =>
{
var model = new AdditionalServiceViewModel
{
DisplayName = service.DisplayName,
ServiceName = service.ServiceName,
CryptoCode = service.CryptoCode,
Type = service.Type.ToString()
};
try
{
model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), _btcpayServerOptions.NetworkType);
}
catch (Exception exception)
{
model.Error = exception.Message;
}
return model;
})
.Select(t => t.Result)
.ToList();
// other services
foreach ((string key, Uri value) in _externalServiceOptions.Value.OtherExternalServices)
{
if (ExternalServices.LightningServiceNames.Contains(key))
{
services.Add(new AdditionalServiceViewModel
{
DisplayName = key,
ServiceName = key,
Type = key.Replace(" ", ""),
Link = Request.GetAbsoluteUriNoPathBase(value).AbsoluteUri
});
}
}
vm.Services = services;
}
return View(vm);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{storeId}/lightning/{cryptoCode}/dashboard/balance")]
public IActionResult LightningBalanceDashboard(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
return ViewComponent("StoreLightningBalance", new { Store = store, CryptoCode = cryptoCode });
}
[HttpGet("{storeId}/lightning/{cryptoCode}/dashboard/balance/{type}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> LightningBalanceDashboard(string storeId, string cryptoCode, HistogramType type)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var lightningClient = await GetLightningClient(store, cryptoCode);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var data = await _lnHistogramService.GetHistogram(lightningClient, type, cts.Token);
if (data == null) return NotFound();
return Json(data);
}
[HttpGet("{storeId}/lightning/{cryptoCode}/setup")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult SetupLightningNode(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var vm = new LightningNodeViewModel
{
CryptoCode = cryptoCode,
StoreId = storeId
};
SetExistingValues(store, vm);
return View(vm);
}
[HttpPost("{storeId}/lightning/{cryptoCode}/setup")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> SetupLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
{
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var network = _explorerProvider.GetNetwork(vm.CryptoCode);
var oldConf = _handlers.GetLightningConfig(store, network);
vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode);
if (vm.CryptoCode == null)
{
ModelState.AddModelError(nameof(vm.CryptoCode), StringLocalizer["Invalid network"]);
return View(vm);
}
var paymentMethodId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
LightningPaymentMethodConfig? paymentMethod;
if (vm.LightningNodeType == LightningNodeType.Internal)
{
paymentMethod = new LightningPaymentMethodConfig();
paymentMethod.SetInternalNode();
}
else
{
if (string.IsNullOrEmpty(vm.ConnectionString))
{
ModelState.AddModelError(nameof(vm.ConnectionString), StringLocalizer["Please provide a connection string"]);
return View(vm);
}
paymentMethod = new LightningPaymentMethodConfig { ConnectionString = vm.ConnectionString };
}
var handler = (LightningLikePaymentHandler)_handlers[paymentMethodId];
var ctx = new PaymentMethodConfigValidationContext(_authorizationService, ModelState,
JToken.FromObject(paymentMethod, handler.Serializer), User, oldConf is null ? null : JToken.FromObject(oldConf, handler.Serializer));
await handler.ValidatePaymentMethodConfig(ctx);
if (ctx.MissingPermission is not null)
ModelState.AddModelError(nameof(vm.ConnectionString), StringLocalizer["You do not have the permissions to change this settings"]);
if (!ModelState.IsValid)
return View(vm);
switch (command)
{
case "save":
var lnurl = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
store.SetPaymentMethodConfig(_handlers[paymentMethodId], paymentMethod);
store.SetPaymentMethodConfig(_handlers[lnurl], new LNURLPaymentMethodConfig
{
UseBech32Scheme = true,
LUD12Enabled = false
});
await _storeRepo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["{0} Lightning node updated.", network.CryptoCode].Value;
return RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode });
case "test":
try
{
var info = await handler.GetNodeInfo(paymentMethod, null, Request.IsOnion(), true);
var hasPublicAddress = info.Any();
if (!vm.SkipPortTest && hasPublicAddress)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
await handler.TestConnection(info.First(), cts.Token);
}
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Connection to the Lightning node successful."].Value + " " +
(hasPublicAddress
? StringLocalizer["Your node address: {0}", info.First()].Value
: StringLocalizer["No public address has been configured."].Value);
}
catch (Exception ex)
{
TempData[WellKnownTempData.ErrorMessage] = ex.Message;
return View(vm);
}
return View(vm);
default:
return View(vm);
}
}
[HttpGet("{storeId}/lightning/{cryptoCode}/settings")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult LightningSettings(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob();
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var lnId = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var lightning = GetConfig<LightningPaymentMethodConfig>(lnId, store);
if (lightning == null)
{
TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["You need to connect to a Lightning node before adjusting its settings."].Value;
return RedirectToAction(nameof(SetupLightningNode), new { storeId, cryptoCode });
}
var vm = new LightningSettingsViewModel
{
CryptoCode = cryptoCode,
StoreId = storeId,
Enabled = !excludeFilters.Match(lnId),
LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate,
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback
};
SetExistingValues(store, vm);
var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(vm.CryptoCode);
var lnurl = GetConfig<LNURLPaymentMethodConfig>(lnurlId, store);
if (lnurl != null)
{
vm.LNURLEnabled = !store.GetStoreBlob().GetExcludedPaymentMethods().Match(lnurlId);
vm.LNURLBech32Mode = lnurl.UseBech32Scheme;
vm.LUD12Enabled = lnurl.LUD12Enabled;
}
return View(vm);
}
[HttpPost("{storeId}/lightning/{cryptoCode}/settings")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> LightningSettings(LightningSettingsViewModel vm)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
if (vm.CryptoCode == null)
{
ModelState.AddModelError(nameof(vm.CryptoCode), StringLocalizer["Invalid network"]);
return View(vm);
}
var network = _explorerProvider.GetNetwork(vm.CryptoCode);
var lnId = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
var lnurlId = PaymentTypes.LNURL.GetPaymentMethodId(network.CryptoCode);
var lightning = GetConfig<LightningPaymentMethodConfig>(lnId, store);
if (lightning == null)
return NotFound();
var needUpdate = false;
var blob = store.GetStoreBlob();
blob.LightningDescriptionTemplate = vm.LightningDescriptionTemplate ?? string.Empty;
blob.LightningAmountInSatoshi = vm.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = vm.LightningPrivateRouteHints;
blob.OnChainWithLnInvoiceFallback = vm.OnChainWithLnInvoiceFallback;
// Lightning
blob.SetExcluded(lnId, !vm.Enabled);
// LNURL
blob.SetExcluded(lnurlId, !vm.LNURLEnabled || !vm.Enabled);
var lnurl = GetConfig<LNURLPaymentMethodConfig>(lnurlId, store);
if (lnurl is null || (
lnurl.UseBech32Scheme != vm.LNURLBech32Mode ||
lnurl.LUD12Enabled != vm.LUD12Enabled))
{
needUpdate = true;
}
store.SetPaymentMethodConfig(_handlers[lnurlId], new LNURLPaymentMethodConfig
{
UseBech32Scheme = vm.LNURLBech32Mode,
LUD12Enabled = vm.LUD12Enabled
});
if (store.SetStoreBlob(blob))
{
needUpdate = true;
}
if (needUpdate)
{
await _storeRepo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["{0} Lightning settings successfully updated.", network.CryptoCode].Value;
}
return RedirectToAction(nameof(LightningSettings), new { vm.StoreId, vm.CryptoCode });
}
private bool CanUseInternalLightning(string cryptoCode)
{
return _lightningNetworkOptions.InternalLightningByCryptoCode.ContainsKey(cryptoCode.ToUpperInvariant()) && (User.IsInRole(Roles.ServerAdmin) || _policiesSettings.AllowLightningInternalNodeForAll);
}
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
vm.CanUseInternalNode = CanUseInternalLightning(vm.CryptoCode);
var lightning = GetConfig<LightningPaymentMethodConfig>(PaymentTypes.LN.GetPaymentMethodId(vm.CryptoCode), store);
if (lightning != null)
{
vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
vm.ConnectionString = lightning.GetDisplayableConnectionString();
}
else
{
vm.LightningNodeType = vm.CanUseInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
}
}
private T? GetConfig<T>(PaymentMethodId paymentMethodId, StoreData store) where T: class
{
return store.GetPaymentMethodConfig<T>(paymentMethodId, _handlers);
}
private async Task<ILightningClient?> GetLightningClient(StoreData store, string cryptoCode)
{
var network = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var id = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
var existing = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(id, _handlers);
if (existing == null)
return null;
if (existing.GetExternalLightningUrl() is { } connectionString)
{
return _lightningClientFactory.Create(connectionString, network);
}
if (existing.IsInternalNode && _lightningNetworkOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var internalLightningNode))
{
var result = await _authorizationService.AuthorizeAsync(HttpContext.User, null,
new PolicyRequirement(Policies.CanUseInternalLightningNode));
return result.Succeeded ? internalLightningNode : null;
}
return null;
}
}