Greenfield: Add lightning histogram

This commit is contained in:
Dennis Reimann 2024-08-05 15:47:59 +02:00
parent 380f9d5ec5
commit e6afc487df
No known key found for this signature in database
GPG key ID: 5009E1797F03F8D0
13 changed files with 176 additions and 37 deletions

View file

@ -21,6 +21,13 @@ public partial class BTCPayServerClient
return await SendHttpRequest<LightningNodeBalanceData>($"api/v1/server/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token);
}
public virtual async Task<HistogramData> GetLightningNodeHistogram(string cryptoCode, HistogramType? type = null,
CancellationToken token = default)
{
var queryPayload = type == null ? null : new Dictionary<string, string> { ["type"] = type.ToString() };
return await SendHttpRequest<HistogramData>($"api/v1/server/lightning/{cryptoCode}/histogram", queryPayload, HttpMethod.Get, token);
}
public virtual async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
CancellationToken token = default)
{

View file

@ -21,6 +21,13 @@ public partial class BTCPayServerClient
return await SendHttpRequest<LightningNodeBalanceData>($"api/v1/stores/{storeId}/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token);
}
public virtual async Task<HistogramData> GetLightningNodeHistogram(string storeId, string cryptoCode, HistogramType? type = null,
CancellationToken token = default)
{
var queryPayload = type == null ? null : new Dictionary<string, string> { ["type"] = type.ToString() };
return await SendHttpRequest<HistogramData>($"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)
{

View file

@ -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<decimal> Series { get; set; }
public List<DateTimeOffset> Labels { get; set; }
[JsonConverter(typeof(JsonConverters.MoneyJsonConverter))]
public Money Balance { get; set; }
[JsonConverter(typeof(NumericStringJsonConverter))]
public decimal Balance { get; set; }
}

View file

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

View file

@ -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> 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<IActionResult> 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")]

View file

@ -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> 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<IActionResult> 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")]

View file

@ -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<IActionResult> GetInfo(string cryptoCode, CancellationToken cancellationToken = default)
@ -87,6 +91,22 @@ namespace BTCPayServer.Controllers.Greenfield
: null
});
}
public virtual async Task<IActionResult> GetHistogram(string cryptoCode, HistogramType? type = null, CancellationToken cancellationToken = default)
{
Enum.TryParse<WalletHistogramType>(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<HistogramType>(data.Type.ToString(), true),
Balance = data.Balance,
Series = data.Series,
Labels = data.Labels
});
}
public virtual async Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request, CancellationToken cancellationToken = default)
{

View file

@ -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<IActionResult> GetOnChainWalletHistogram(string storeId, string cryptoCode, [FromQuery] string type)
public async Task<IActionResult> 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<WalletHistogramType>(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<HistogramType>(data.Type.ToString(), true),
Balance = data.Balance,
Series = data.Series,
Labels = data.Labels
});
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]

View file

@ -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<GreenfieldStoreLightningNodeApiController>().GetBalance(cryptoCode, token));
}
public override async Task<HistogramData> GetLightningNodeHistogram(string storeId, string cryptoCode, HistogramType? type = null,
CancellationToken token = default)
{
return GetFromActionResult<HistogramData>(
await GetController<GreenfieldStoreLightningNodeApiController>().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<GreenfieldInternalLightningNodeApiController>().GetBalance(cryptoCode));
}
public override async Task<HistogramData> GetLightningNodeHistogram(string cryptoCode, HistogramType? type = null,
CancellationToken token = default)
{
return GetFromActionResult<HistogramData>(
await GetController<GreenfieldInternalLightningNodeApiController>().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<GreenfieldStoreOnChainWalletsController>().ShowOnChainWalletOverview(storeId, cryptoCode));
}
public override async Task<HistogramData> GetOnChainWalletHistogram(string storeId, string cryptoCode, string type, CancellationToken token = default)
public override async Task<HistogramData> GetOnChainWalletHistogram(string storeId, string cryptoCode, HistogramType? type = null, CancellationToken token = default)
{
return GetFromActionResult<HistogramData>(
await GetController<GreenfieldStoreOnChainWalletsController>().GetOnChainWalletHistogram(storeId, cryptoCode, type));
await GetController<GreenfieldStoreOnChainWalletsController>().GetOnChainWalletHistogram(storeId, cryptoCode, type?.ToString()));
}
public override async Task<OnChainWalletAddressData> GetOnChainWalletReceiveAddress(string storeId,

View file

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

View file

@ -182,6 +182,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<UserService>();
services.TryAddSingleton<UriResolver>();
services.TryAddSingleton<WalletHistogramService>();
services.TryAddSingleton<LightningHistogramService>();
services.AddSingleton<ApplicationDbContextFactory>();
services.AddOptions<BTCPayServerOptions>().Configure(
(options) =>

View file

@ -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<WalletHistogramData> 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<decimal>(pointCount);
var labels = new List<DateTimeOffset>(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; }
}
}

View file

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