From e6afc487df39743e53e68c6d1a3e9b4fa52b5e7a Mon Sep 17 00:00:00 2001 From: Dennis Reimann Date: Mon, 5 Aug 2024 15:47:59 +0200 Subject: [PATCH] Greenfield: Add lightning histogram --- .../BTCPayServerClient.Lightning.Internal.cs | 7 ++ .../BTCPayServerClient.Lightning.Store.cs | 7 ++ .../Models/WalletHistogramData.cs | 6 +- .../StoreWalletBalance/StoreWalletBalance.cs | 10 --- ...ieldLightningNodeApiController.Internal.cs | 17 ++-- ...enfieldLightningNodeApiController.Store.cs | 15 +++- .../GreenfieldLightningNodeApiController.cs | 24 ++++- ...GreenfieldStoreOnChainWalletsController.cs | 11 ++- .../GreenField/LocalBTCPayServerClient.cs | 20 ++++- .../Controllers/UIWalletsController.cs | 5 +- BTCPayServer/Hosting/BTCPayServerServices.cs | 1 + .../Services/LightningHistogramService.cs | 88 +++++++++++++++++++ .../Wallets/WalletHistogramService.cs | 2 - 13 files changed, 176 insertions(+), 37 deletions(-) create mode 100644 BTCPayServer/Services/LightningHistogramService.cs diff --git a/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs b/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs index 979dd014a..54cc96e8f 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs @@ -21,6 +21,13 @@ public partial class BTCPayServerClient return await SendHttpRequest($"api/v1/server/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token); } + public virtual async Task GetLightningNodeHistogram(string cryptoCode, HistogramType? type = null, + CancellationToken token = default) + { + var queryPayload = type == null ? null : new Dictionary { ["type"] = type.ToString() }; + return await SendHttpRequest($"api/v1/server/lightning/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token); + } + public virtual async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request, CancellationToken token = default) { diff --git a/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs b/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs index eeefeb570..28bafbb72 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs @@ -21,6 +21,13 @@ public partial class BTCPayServerClient return await SendHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token); } + public virtual async Task GetLightningNodeHistogram(string storeId, string cryptoCode, HistogramType? type = null, + CancellationToken token = default) + { + var queryPayload = type == null ? null : new Dictionary { ["type"] = type.ToString() }; + return await SendHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token); + } + public virtual async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request, CancellationToken token = default) { diff --git a/BTCPayServer.Client/Models/WalletHistogramData.cs b/BTCPayServer.Client/Models/WalletHistogramData.cs index 2f9d83bfa..26c9ba498 100644 --- a/BTCPayServer.Client/Models/WalletHistogramData.cs +++ b/BTCPayServer.Client/Models/WalletHistogramData.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using NBitcoin; +using BTCPayServer.JsonConverters; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -19,6 +19,6 @@ public class HistogramData public HistogramType Type { get; set; } public List Series { get; set; } public List Labels { get; set; } - [JsonConverter(typeof(JsonConverters.MoneyJsonConverter))] - public Money Balance { get; set; } + [JsonConverter(typeof(NumericStringJsonConverter))] + public decimal Balance { get; set; } } diff --git a/BTCPayServer/Components/StoreWalletBalance/StoreWalletBalance.cs b/BTCPayServer/Components/StoreWalletBalance/StoreWalletBalance.cs index 94f2ff2a5..c9104b2de 100644 --- a/BTCPayServer/Components/StoreWalletBalance/StoreWalletBalance.cs +++ b/BTCPayServer/Components/StoreWalletBalance/StoreWalletBalance.cs @@ -1,23 +1,13 @@ #nullable enable using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Globalization; -using System.Linq; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Data; -using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; using BTCPayServer.Services.Wallets; -using Dapper; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using NBitcoin; -using NBXplorer; -using NBXplorer.Client; namespace BTCPayServer.Components.StoreWalletBalance; diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Internal.cs b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Internal.cs index 62b59c424..ea768de3d 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Internal.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Internal.cs @@ -1,14 +1,10 @@ using System.Threading; using System.Threading.Tasks; using BTCPayApp.CommonServer; -using BTCPayServer.Abstractions.Constants; -using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Configuration; -using BTCPayServer.HostedServices; using BTCPayServer.Lightning; -using BTCPayServer.Security; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; using Microsoft.AspNetCore.Authorization; @@ -32,8 +28,9 @@ namespace BTCPayServer.Controllers.Greenfield PoliciesSettings policiesSettings, LightningClientFactoryService lightningClientFactory, IOptions lightningNetworkOptions, IAuthorizationService authorizationService, - PaymentMethodHandlerDictionary handlers - ) : base(policiesSettings, authorizationService, handlers) + PaymentMethodHandlerDictionary handlers, + LightningHistogramService lnHistogramService + ) : base(policiesSettings, authorizationService, handlers, lnHistogramService) { _lightningClientFactory = lightningClientFactory; _lightningNetworkOptions = lightningNetworkOptions; @@ -56,6 +53,14 @@ namespace BTCPayServer.Controllers.Greenfield return base.GetBalance(cryptoCode, cancellationToken); } + [Authorize(Policy = Policies.CanUseInternalLightningNode, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/server/lightning/{cryptoCode}/histogram")] + public override Task GetHistogram(string cryptoCode, [FromQuery] HistogramType? type = null, CancellationToken cancellationToken = default) + { + return base.GetHistogram(cryptoCode, type, cancellationToken); + } + [Authorize(Policy = Policies.CanUseInternalLightningNode, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpPost("~/api/v1/server/lightning/{cryptoCode}/connect")] diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Store.cs b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Store.cs index c36d3fe4e..c73c42c23 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Store.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Store.cs @@ -1,9 +1,6 @@ -using System.Linq; using System.Threading; using System.Threading.Tasks; using BTCPayApp.CommonServer; -using BTCPayServer.Abstractions.Constants; -using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Client; using BTCPayServer.Client.Models; @@ -35,7 +32,8 @@ namespace BTCPayServer.Controllers.Greenfield IOptions lightningNetworkOptions, LightningClientFactoryService lightningClientFactory, PaymentMethodHandlerDictionary handlers, PoliciesSettings policiesSettings, - IAuthorizationService authorizationService) : base(policiesSettings, authorizationService, handlers) + IAuthorizationService authorizationService, + LightningHistogramService lnHistogramService) : base(policiesSettings, authorizationService, handlers, lnHistogramService) { _lightningNetworkOptions = lightningNetworkOptions; _lightningClientFactory = lightningClientFactory; @@ -57,6 +55,13 @@ namespace BTCPayServer.Controllers.Greenfield { return base.GetBalance(cryptoCode, cancellationToken); } + + [Authorize(Policy = Policies.CanUseLightningNodeInStore, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/histogram")] + public override Task GetHistogram(string cryptoCode, [FromQuery] HistogramType? type = null, CancellationToken cancellationToken = default) + { + return base.GetHistogram(cryptoCode, type, cancellationToken); + } [Authorize(Policy = Policies.CanUseLightningNodeInStore, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] @@ -65,6 +70,7 @@ namespace BTCPayServer.Controllers.Greenfield { return base.ConnectToNode(cryptoCode, request, cancellationToken); } + [Authorize(Policy = Policies.CanUseLightningNodeInStore, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")] @@ -72,6 +78,7 @@ namespace BTCPayServer.Controllers.Greenfield { return base.GetChannels(cryptoCode, cancellationToken); } + [Authorize(Policy = Policies.CanUseLightningNodeInStore, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")] diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs index eb63f72f4..5e7290e4b 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs @@ -9,12 +9,13 @@ using BTCPayServer.Client.Models; using BTCPayServer.Lightning; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; -using BTCPayServer.Security; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using NBitcoin; using Newtonsoft.Json.Linq; namespace BTCPayServer.Controllers.Greenfield @@ -33,15 +34,18 @@ namespace BTCPayServer.Controllers.Greenfield private readonly PoliciesSettings _policiesSettings; private readonly IAuthorizationService _authorizationService; private readonly PaymentMethodHandlerDictionary _handlers; + private readonly LightningHistogramService _lnHistogramService; protected GreenfieldLightningNodeApiController( PoliciesSettings policiesSettings, IAuthorizationService authorizationService, - PaymentMethodHandlerDictionary handlers) + PaymentMethodHandlerDictionary handlers, + LightningHistogramService lnHistogramService) { _policiesSettings = policiesSettings; _authorizationService = authorizationService; _handlers = handlers; + _lnHistogramService = lnHistogramService; } public virtual async Task GetInfo(string cryptoCode, CancellationToken cancellationToken = default) @@ -87,6 +91,22 @@ namespace BTCPayServer.Controllers.Greenfield : null }); } + + public virtual async Task GetHistogram(string cryptoCode, HistogramType? type = null, CancellationToken cancellationToken = default) + { + Enum.TryParse(type.ToString(), true, out var histType); + var lightningClient = await GetLightningClient(cryptoCode, true); + var data = await _lnHistogramService.GetHistogram(lightningClient, histType, cancellationToken); + if (data == null) return this.CreateAPIError(404, "histogram-not-found", "The lightning histogram was not found."); + + return Ok(new HistogramData + { + Type = Enum.Parse(data.Type.ToString(), true), + Balance = data.Balance, + Series = data.Series, + Labels = data.Labels + }); + } public virtual async Task ConnectToNode(string cryptoCode, ConnectToNodeRequest request, CancellationToken cancellationToken = default) { diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs index 2988102cd..afb5078f0 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs @@ -116,7 +116,7 @@ namespace BTCPayServer.Controllers.Greenfield [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpGet("~/api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/histogram")] - public async Task GetOnChainWalletHistogram(string storeId, string cryptoCode, [FromQuery] string type) + public async Task GetOnChainWalletHistogram(string storeId, string cryptoCode, [FromQuery] string? type = null) { if (IsInvalidWalletRequest(cryptoCode, out _, out _, out var actionResult)) return actionResult; @@ -124,8 +124,15 @@ namespace BTCPayServer.Controllers.Greenfield var walletId = new WalletId(storeId, cryptoCode); Enum.TryParse(type, true, out var histType); var data = await _walletHistogramService.GetHistogram(Store, walletId, histType); + if (data == null) return this.CreateAPIError(404, "histogram-not-found", "The wallet histogram was not found."); - return Ok(data); + return Ok(new HistogramData + { + Type = Enum.Parse(data.Type.ToString(), true), + Balance = data.Balance, + Series = data.Series, + Labels = data.Labels + }); } [Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index 00de044fb..364dcd674 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -13,7 +13,6 @@ using BTCPayServer.Client; using BTCPayServer.Client.Models; using BTCPayServer.Controllers.GreenField; using BTCPayServer.Data; -using BTCPayServer.Security; using BTCPayServer.Security.Greenfield; using BTCPayServer.Services.Mails; using BTCPayServer.Services.Stores; @@ -21,7 +20,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NBitcoin; @@ -385,6 +383,13 @@ namespace BTCPayServer.Controllers.Greenfield await GetController().GetBalance(cryptoCode, token)); } + public override async Task GetLightningNodeHistogram(string storeId, string cryptoCode, HistogramType? type = null, + CancellationToken token = default) + { + return GetFromActionResult( + await GetController().GetHistogram(cryptoCode, type, token)); + } + public override async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request, CancellationToken token = default) { @@ -461,6 +466,13 @@ namespace BTCPayServer.Controllers.Greenfield await GetController().GetBalance(cryptoCode)); } + public override async Task GetLightningNodeHistogram(string cryptoCode, HistogramType? type = null, + CancellationToken token = default) + { + return GetFromActionResult( + await GetController().GetHistogram(cryptoCode, type, token)); + } + public override async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request, CancellationToken token = default) { @@ -703,10 +715,10 @@ namespace BTCPayServer.Controllers.Greenfield await GetController().ShowOnChainWalletOverview(storeId, cryptoCode)); } - public override async Task GetOnChainWalletHistogram(string storeId, string cryptoCode, string type, CancellationToken token = default) + public override async Task GetOnChainWalletHistogram(string storeId, string cryptoCode, HistogramType? type = null, CancellationToken token = default) { return GetFromActionResult( - await GetController().GetOnChainWalletHistogram(storeId, cryptoCode, type)); + await GetController().GetOnChainWalletHistogram(storeId, cryptoCode, type?.ToString())); } public override async Task GetOnChainWalletReceiveAddress(string storeId, diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs index 13dfd2608..549b566e6 100644 --- a/BTCPayServer/Controllers/UIWalletsController.cs +++ b/BTCPayServer/Controllers/UIWalletsController.cs @@ -46,6 +46,7 @@ using NBXplorer.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using StoreData = BTCPayServer.Data.StoreData; +using WalletHistogramType = BTCPayServer.Services.Wallets.WalletHistogramType; namespace BTCPayServer.Controllers { @@ -313,10 +314,6 @@ namespace BTCPayServer.Controllers var store = GetCurrentStore(); var data = await _walletHistogramService.GetHistogram(store, walletId, type); if (data == null) return NotFound(); - - const int labelCount = 6; - var pointCount = data.Series.Count; - var labelEvery = pointCount / labelCount; return Json(data); } diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 73724e779..4f1ad78ca 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -182,6 +182,7 @@ namespace BTCPayServer.Hosting services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.AddSingleton(); services.AddOptions().Configure( (options) => diff --git a/BTCPayServer/Services/LightningHistogramService.cs b/BTCPayServer/Services/LightningHistogramService.cs new file mode 100644 index 000000000..8ce4d1e5f --- /dev/null +++ b/BTCPayServer/Services/LightningHistogramService.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Lightning; +using BTCPayServer.Services.Wallets; + +namespace BTCPayServer.Services; + +public class LightningHistogramService +{ + public async Task GetHistogram(ILightningClient lightningClient, WalletHistogramType type, CancellationToken cancellationToken) + { + var (days, pointCount) = type switch + { + WalletHistogramType.Week => (7, 30), + WalletHistogramType.Month => (30, 30), + WalletHistogramType.Year => (365, 30), + _ => throw new ArgumentException($"WalletHistogramType {type} does not exist.") + }; + var to = DateTimeOffset.UtcNow; + var from = to - TimeSpan.FromDays(days); + var ticks = (to - from).Ticks; + var interval = TimeSpan.FromTicks(ticks / pointCount); + + try + { + // general balance + var lnBalance = await lightningClient.GetBalance(cancellationToken); + /*var totalOnchain = lnBalance.OnchainBalance != null + ? (lnBalance.OnchainBalance.Confirmed ?? 0L) + (lnBalance.OnchainBalance.Reserved ?? 0L) + + (lnBalance.OnchainBalance.Unconfirmed ?? 0L) + : new Money(0L); + var totalOffchain = lnBalance.OffchainBalance != null + ? (lnBalance.OffchainBalance.Opening ?? 0) + (lnBalance.OffchainBalance.Local ?? 0) + + (lnBalance.OffchainBalance.Closing ?? 0) + : null;*/ + var total = lnBalance.OffchainBalance.Local;//(totalOnchain + new Money(totalOffchain?.ToDecimal(LightMoneyUnit.Satoshi) ?? 0, MoneyUnit.Satoshi)).ToDecimal(MoneyUnit.Satoshi); + var totalBtc = total.ToDecimal(LightMoneyUnit.BTC); + // prepare transaction data + var lnInvoices = await lightningClient.ListInvoices(cancellationToken); + var lnPayments = await lightningClient.ListPayments(cancellationToken); + var lnTransactions = lnInvoices + .Where(inv => inv.Status == LightningInvoiceStatus.Paid && inv.PaidAt >= from) + .Select(inv => new LnTx { Amount = inv.Amount.ToDecimal(LightMoneyUnit.BTC), Settled = inv.PaidAt.GetValueOrDefault() }) + .Concat(lnPayments + .Where(pay => pay.Status == LightningPaymentStatus.Complete && pay.CreatedAt >= from) + .Select(pay => new LnTx { Amount = pay.Amount.ToDecimal(LightMoneyUnit.BTC) * -1, Settled = pay.CreatedAt.GetValueOrDefault() })) + .OrderByDescending(tx => tx.Settled) + .ToList(); + // assemble graph data going backwards + var series = new List(pointCount); + var labels = new List(pointCount); + var balance = totalBtc; + for (var i = pointCount; i > 0; i--) + { + var txs = lnTransactions.Where(t => + t.Settled.Ticks >= from.Ticks + interval.Ticks * i && + t.Settled.Ticks < from.Ticks + interval.Ticks * (i + 1)); + var sum = txs.Sum(tx => tx.Amount); + balance -= sum; + series.Add(balance); + labels.Add(from + interval * i); + } + // reverse the lists + series.Reverse(); + labels.Reverse(); + return new WalletHistogramData + { + Type = type, + Balance = totalBtc, + Series = series, + Labels = labels + }; + } + catch (Exception) + { + return null; + } + } + + private class LnTx + { + public DateTimeOffset Settled { get; set; } + public decimal Amount { get; set; } + } +} diff --git a/BTCPayServer/Services/Wallets/WalletHistogramService.cs b/BTCPayServer/Services/Wallets/WalletHistogramService.cs index 058de04e2..53eebd1d5 100644 --- a/BTCPayServer/Services/Wallets/WalletHistogramService.cs +++ b/BTCPayServer/Services/Wallets/WalletHistogramService.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Threading.Tasks; -using BTCPayServer.Client.JsonConverters; using BTCPayServer.Data; using BTCPayServer.Services.Invoices; using Dapper;