btcpayserver/BTCPayServer/Controllers/UIStoresController.Dashboard.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

180 lines
7.4 KiB
C#

#nullable enable
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;
using BTCPayServer.Components.StoreLightningBalance;
using BTCPayServer.Components.StoreNumbers;
using BTCPayServer.Components.StoreRecentInvoices;
using BTCPayServer.Components.StoreRecentTransactions;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.DataEncoders;
namespace BTCPayServer.Controllers;
public partial class UIStoresController
{
[HttpGet("{storeId}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Dashboard()
{
var store = CurrentStore;
if (store is null)
return NotFound();
var storeBlob = store.GetStoreBlob();
AddPaymentMethods(store, storeBlob,
out var derivationSchemes, out var lightningNodes);
var walletEnabled = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled);
var lightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled);
var cryptoCode = _networkProvider.DefaultNetwork.CryptoCode;
var vm = new StoreDashboardViewModel
{
WalletEnabled = walletEnabled,
LightningEnabled = lightningEnabled,
LightningSupported = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode)?.SupportLightning is true,
StoreId = CurrentStore.Id,
StoreName = CurrentStore.StoreName,
CryptoCode = cryptoCode,
Network = _networkProvider.DefaultNetwork,
IsSetUp = walletEnabled || lightningEnabled
};
// Widget data
if (vm is { WalletEnabled: false, LightningEnabled: false })
return View(vm);
var userId = GetUserId();
if (userId is null)
return NotFound();
var apps = await _appService.GetAllApps(userId, false, store.Id);
foreach (var app in apps)
{
var appData = await _appService.GetAppData(userId, app.Id);
vm.Apps.Add(appData);
}
return View(vm);
}
[HttpGet("{storeId}/dashboard/{cryptoCode}/lightning/balance")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult LightningBalance(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
return ViewComponent("StoreLightningBalance", new { Store = store, CryptoCode = cryptoCode });
}
[HttpGet("{storeId}/dashboard/{cryptoCode}/numbers")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult StoreNumbers(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var vm = new StoreNumbersViewModel { Store = store, CryptoCode = cryptoCode };
return ViewComponent("StoreNumbers", new { vm });
}
[HttpGet("{storeId}/dashboard/{cryptoCode}/recent-transactions")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult RecentTransactions(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var vm = new StoreRecentTransactionsViewModel { Store = store, CryptoCode = cryptoCode };
return ViewComponent("StoreRecentTransactions", new { vm });
}
[HttpGet("{storeId}/dashboard/{cryptoCode}/recent-invoices")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public IActionResult RecentInvoices(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var vm = new StoreRecentInvoicesViewModel { Store = store, CryptoCode = cryptoCode };
return ViewComponent("StoreRecentInvoices", new { vm });
}
internal void AddPaymentMethods(StoreData store, StoreBlob storeBlob,
out List<StoreDerivationScheme> derivationSchemes, out List<StoreLightningNode> lightningNodes)
{
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var derivationByCryptoCode =
store
.GetPaymentMethodConfigs<DerivationSchemeSettings>(_handlers)
.ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => c.Value);
var lightningByCryptoCode = store
.GetPaymentMethodConfigs(_handlers)
.Where(c => c.Value is LightningPaymentMethodConfig)
.ToDictionary(c => ((IHasNetwork)_handlers[c.Key]).Network.CryptoCode, c => (LightningPaymentMethodConfig)c.Value);
derivationSchemes = [];
lightningNodes = [];
foreach (var handler in _handlers)
{
if (handler is BitcoinLikePaymentHandler { Network: var network })
{
var strategy = derivationByCryptoCode.TryGet(network.CryptoCode);
var value = strategy?.ToPrettyString() ?? string.Empty;
derivationSchemes.Add(new StoreDerivationScheme
{
Crypto = network.CryptoCode,
PaymentMethodId = handler.PaymentMethodId,
WalletSupported = network.WalletSupported,
ReadonlyWallet = network.ReadonlyWallet,
Value = value,
WalletId = new WalletId(store.Id, network.CryptoCode),
Enabled = !excludeFilters.Match(handler.PaymentMethodId) && strategy != null,
Collapsed = network is Plugins.Altcoins.ElementsBTCPayNetwork { IsNativeAsset : false } && string.IsNullOrEmpty(value)
});
}
else if (handler is LightningLikePaymentHandler)
{
var lnNetwork = ((IHasNetwork)handler).Network;
var lightning = lightningByCryptoCode.TryGet(lnNetwork.CryptoCode);
var isEnabled = !excludeFilters.Match(handler.PaymentMethodId) && lightning != null;
lightningNodes.Add(new StoreLightningNode
{
CryptoCode = lnNetwork.CryptoCode,
PaymentMethodId = handler.PaymentMethodId,
Address = lightning?.GetDisplayableConnectionString(),
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]);
}
}