mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 01:43:50 +01:00
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
This commit is contained in:
parent
b3945d758a
commit
641bdcff31
@ -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, object> { { "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)
|
||||
{
|
||||
|
@ -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, object> { { "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)
|
||||
{
|
||||
|
@ -16,6 +16,14 @@ public partial class BTCPayServerClient
|
||||
{
|
||||
return await SendHttpRequest<OnChainWalletOverviewData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet", null, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<HistogramData> GetOnChainWalletHistogram(string storeId, string cryptoCode, HistogramType? type = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var queryPayload = type == null ? null : new Dictionary<string, object> { { "type", type.ToString() } };
|
||||
return await SendHttpRequest<HistogramData>($"api/v1/stores/{storeId}/payment-methods/{cryptoCode}-CHAIN/wallet/histogram", queryPayload, HttpMethod.Get, token);
|
||||
}
|
||||
|
||||
public virtual async Task<OnChainWalletFeeRateData> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
|
30
BTCPayServer.Client/Models/HistogramData.cs
Normal file
30
BTCPayServer.Client/Models/HistogramData.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models;
|
||||
|
||||
public enum HistogramType
|
||||
{
|
||||
Week,
|
||||
Month,
|
||||
YTD,
|
||||
Year,
|
||||
TwoYears,
|
||||
Day
|
||||
}
|
||||
|
||||
public class HistogramData
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public HistogramType Type { get; set; }
|
||||
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
|
||||
public List<decimal> Series { get; set; }
|
||||
[JsonProperty(ItemConverterType = typeof(DateTimeToUnixTimeConverter))]
|
||||
public List<DateTimeOffset> Labels { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Balance { get; set; }
|
||||
}
|
@ -3002,6 +3002,19 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
|
||||
// balance
|
||||
var balance = await client.GetLightningNodeBalance(user.StoreId, "BTC");
|
||||
Assert.True(LightMoney.Satoshis(1000) <= balance.OffchainBalance.Local);
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var localBalance = balance.OffchainBalance.Local.ToDecimal(LightMoneyUnit.BTC);
|
||||
var histogram = await client.GetLightningNodeHistogram(user.StoreId, "BTC");
|
||||
Assert.Equal(histogram.Balance, histogram.Series.Last());
|
||||
Assert.Equal(localBalance, histogram.Balance);
|
||||
Assert.Equal(localBalance, histogram.Series.Last());
|
||||
});
|
||||
|
||||
// As admin, can use the internal node through our store.
|
||||
await user.MakeAdmin(true);
|
||||
await user.RegisterInternalLightningNodeAsync("BTC");
|
||||
@ -3023,6 +3036,10 @@ namespace BTCPayServer.Tests
|
||||
client = await guest.CreateClient(Policies.CanUseLightningNodeInStore);
|
||||
// Can use lightning node is only granted to store's owner
|
||||
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC"));
|
||||
|
||||
// balance and histogram should not be accessible with view only clients
|
||||
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeBalance(user.StoreId, "BTC"));
|
||||
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeHistogram(user.StoreId, "BTC"));
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 20 * 1000)]
|
||||
@ -3539,7 +3556,6 @@ namespace BTCPayServer.Tests
|
||||
var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
|
||||
Assert.Equal(0m, overview.Balance);
|
||||
|
||||
|
||||
var fee = await client.GetOnChainFeeRate(walletId.StoreId, walletId.CryptoCode);
|
||||
Assert.NotNull(fee.FeeRate);
|
||||
|
||||
@ -3585,6 +3601,17 @@ namespace BTCPayServer.Tests
|
||||
overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
|
||||
Assert.Equal(0.01m, overview.Balance);
|
||||
|
||||
// histogram should not be accessible with view only clients
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnlyClient.GetOnChainWalletHistogram(walletId.StoreId, walletId.CryptoCode);
|
||||
});
|
||||
var histogram = await client.GetOnChainWalletHistogram(walletId.StoreId, walletId.CryptoCode);
|
||||
Assert.Equal(histogram.Balance, histogram.Series.Last());
|
||||
Assert.Equal(0.01m, histogram.Balance);
|
||||
Assert.Equal(0.01m, histogram.Series.Last());
|
||||
Assert.Equal(0, histogram.Series.First());
|
||||
|
||||
//the simplest request:
|
||||
var nodeAddress = await tester.ExplorerNode.GetNewAddressAsync();
|
||||
var createTxRequest = new CreateOnChainTransactionRequest()
|
||||
|
@ -43,10 +43,19 @@ if (!window.appSales) {
|
||||
}
|
||||
};
|
||||
|
||||
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
});
|
||||
function addEventListeners() {
|
||||
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
|
||||
console.log("CHANGED", id)
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
window.addEventListener("DOMContentLoaded", addEventListeners);
|
||||
} else {
|
||||
addEventListeners();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.TagHelpers
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Components.StoreLightningBalance.StoreLightningBalanceViewModel
|
||||
@if(!Model.InitialRendering && Model.Balance == null)
|
||||
@if (!Model.InitialRendering && Model.Balance == null)
|
||||
{
|
||||
return;
|
||||
return;
|
||||
}
|
||||
<div id="StoreLightningBalance-@Model.Store.Id" class="widget store-lightning-balance">
|
||||
<div id="StoreLightningBalance-@Model.StoreId" class="widget store-lightning-balance">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
|
||||
<h6 text-translate="true">Lightning Balance</h6>
|
||||
@if (Model.CryptoCode != Model.DefaultCurrency && Model.Balance != null)
|
||||
@ -128,12 +132,32 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.Balance.OffchainBalance != null && Model.Balance.OnchainBalance != null)
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 @(Model.Series != null ? "my-3" : "mt-3")">
|
||||
@if (Model.Balance.OffchainBalance != null && Model.Balance.OnchainBalance != null)
|
||||
{
|
||||
<button class="d-inline-flex align-items-center btn btn-link text-primary fw-semibold p-0 ms-n1" type="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">
|
||||
<vc:icon symbol="caret-down" />
|
||||
<span class="ms-1" text-translate="true">Details</span>
|
||||
</button>
|
||||
}
|
||||
@if (Model.Series != null)
|
||||
{
|
||||
<div class="btn-group only-for-js mt-1" role="group" aria-label="Period">
|
||||
<input type="radio" class="btn-check" name="StoreLightningBalancePeriod-@Model.StoreId" id="StoreLightningBalancePeriodWeek-@Model.StoreId" value="@HistogramType.Week" @(Model.Type == HistogramType.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreLightningBalancePeriodWeek-@Model.StoreId">1W</label>
|
||||
<input type="radio" class="btn-check" name="StoreLightningBalancePeriod-@Model.StoreId" id="StoreLightningBalancePeriodMonth-@Model.StoreId" value="@HistogramType.Month" @(Model.Type == HistogramType.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreLightningBalancePeriodMonth-@Model.StoreId">1M</label>
|
||||
<input type="radio" class="btn-check" name="StoreLightningBalancePeriod-@Model.StoreId" id="StoreLightningBalancePeriodYear-@Model.StoreId" value="@HistogramType.Year" @(Model.Type == HistogramType.Year ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreLightningBalancePeriodYear-@Model.StoreId">1Y</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.Series != null)
|
||||
{
|
||||
<button class="d-inline-flex align-items-center btn btn-link text-primary fw-semibold p-0 mt-3 ms-n1" type="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">
|
||||
<vc:icon symbol="caret-down"/>
|
||||
<span class="ms-1" text-translate="true">Details</span>
|
||||
</button>
|
||||
<div class="ct-chart"></div>
|
||||
<template>
|
||||
@Safe.Json(Model)
|
||||
</template>
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -143,47 +167,18 @@
|
||||
<span class="visually-hidden" text-translate="true">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script src="~/Components/StoreLightningBalance/Default.cshtml.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
const url = @Safe.Json(Url.Action("LightningBalance", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const url = @Safe.Json(Model.DataUrl);
|
||||
const storeId = @Safe.Json(Model.StoreId);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`StoreLightningBalance-${storeId}`).outerHTML = await response.text();
|
||||
const data = document.querySelector(`#StoreLightningBalance-${storeId} template`);
|
||||
if (data) window.storeLightningBalance.dataLoaded(JSON.parse(data.innerHTML));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const cryptoCode = @Safe.Json(Model.CryptoCode);
|
||||
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
|
||||
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
|
||||
const id = `StoreLightningBalance-${storeId}`;
|
||||
|
||||
const render = rate => {
|
||||
const currency = rate ? defaultCurrency : cryptoCode;
|
||||
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
|
||||
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
|
||||
const value = Number.parseFloat(c.dataset.balance);
|
||||
c.innerText = rate
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, currency, divisibility)
|
||||
: value
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
delegate('change', `#${id} .currency-toggle input`, async e => {
|
||||
const { target } = e;
|
||||
if (target.value === defaultCurrency) {
|
||||
const rate = await DashboardUtils.fetchRate(`${cryptoCode}_${defaultCurrency}`);
|
||||
if (rate) render(rate);
|
||||
} else {
|
||||
render(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -0,0 +1,94 @@
|
||||
if (!window.storeLightningBalance) {
|
||||
window.storeLightningBalance = {
|
||||
dataLoaded (model) {
|
||||
const { storeId, cryptoCode, defaultCurrency, currencyData: { divisibility } } = model;
|
||||
const id = `StoreLightningBalance-${storeId}`;
|
||||
const valueTransform = value => rate ? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility) : value
|
||||
const labelCount = 6
|
||||
const tooltip = Chartist.plugins.tooltip2({
|
||||
template: '<div class="chartist-tooltip-value">{{value}}</div><div class="chartist-tooltip-line"></div>',
|
||||
offset: {
|
||||
x: 0,
|
||||
y: -16
|
||||
},
|
||||
valueTransformFunction(value, label) {
|
||||
return valueTransform(value) + ' ' + (rate ? defaultCurrency : cryptoCode)
|
||||
}
|
||||
})
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
||||
const dateFormatter = new Intl.DateTimeFormat('default', { month: 'short', day: 'numeric' })
|
||||
const chartOpts = {
|
||||
fullWidth: true,
|
||||
showArea: true,
|
||||
axisY: {
|
||||
showLabel: false,
|
||||
offset: 0
|
||||
},
|
||||
plugins: [tooltip]
|
||||
};
|
||||
const baseUrl = model.dataUrl;
|
||||
let data = model;
|
||||
let rate = null;
|
||||
|
||||
const render = data => {
|
||||
let { series, labels } = data;
|
||||
const currency = rate ? defaultCurrency : cryptoCode;
|
||||
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
|
||||
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
|
||||
const value = Number.parseFloat(c.dataset.balance);
|
||||
c.innerText = valueTransform(value)
|
||||
});
|
||||
if (!series) return;
|
||||
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = Math.max(min - ((max - min) / 5), 0);
|
||||
const renderOpts = Object.assign({}, chartOpts, { low, axisX: {
|
||||
labelInterpolationFnc(date, i) {
|
||||
return i % labelEvery == 0 ? dateFormatter.format(new Date(date)) : null
|
||||
}
|
||||
} });
|
||||
const pointCount = series.length;
|
||||
const labelEvery = pointCount / labelCount;
|
||||
const chart = new Chartist.Line(`#${id} .ct-chart`, {
|
||||
labels: labels,
|
||||
series: [series]
|
||||
}, renderOpts);
|
||||
};
|
||||
|
||||
const update = async type => {
|
||||
const url = `${baseUrl}/${type}`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
data = await response.json();
|
||||
render(data);
|
||||
}
|
||||
};
|
||||
|
||||
render(data);
|
||||
|
||||
function addEventListeners() {
|
||||
delegate('change', `#${id} [name="StoreLightningBalancePeriod-${storeId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
})
|
||||
delegate('change', `#${id} .currency-toggle input`, async e => {
|
||||
const { target } = e;
|
||||
if (target.value === defaultCurrency) {
|
||||
rate = await DashboardUtils.fetchRate(`${cryptoCode}_${defaultCurrency}`);
|
||||
if (rate) render(data);
|
||||
} else {
|
||||
rate = null;
|
||||
render(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
window.addEventListener("DOMContentLoaded", addEventListeners);
|
||||
} else {
|
||||
addEventListeners();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -1,13 +1,11 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
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;
|
||||
@ -18,11 +16,14 @@ using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Components.StoreLightningBalance;
|
||||
|
||||
public class StoreLightningBalance : ViewComponent
|
||||
{
|
||||
private const HistogramType DefaultType = HistogramType.Week;
|
||||
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly BTCPayServerOptions _btcpayServerOptions;
|
||||
@ -32,6 +33,7 @@ public class StoreLightningBalance : ViewComponent
|
||||
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
private readonly LightningHistogramService _lnHistogramService;
|
||||
|
||||
public StoreLightningBalance(
|
||||
StoreRepository storeRepo,
|
||||
@ -42,7 +44,8 @@ public class StoreLightningBalance : ViewComponent
|
||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||
IOptions<ExternalServicesOptions> externalServiceOptions,
|
||||
IAuthorizationService authorizationService,
|
||||
PaymentMethodHandlerDictionary handlers)
|
||||
PaymentMethodHandlerDictionary handlers,
|
||||
LightningHistogramService lnHistogramService)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
_currencies = currencies;
|
||||
@ -53,31 +56,32 @@ public class StoreLightningBalance : ViewComponent
|
||||
_handlers = handlers;
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
_lightningNetworkOptions = lightningNetworkOptions;
|
||||
_lnHistogramService = lnHistogramService;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreLightningBalanceViewModel vm)
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode, bool initialRendering)
|
||||
{
|
||||
if (vm.Store == null)
|
||||
throw new ArgumentNullException(nameof(vm.Store));
|
||||
if (vm.CryptoCode == null)
|
||||
throw new ArgumentNullException(nameof(vm.CryptoCode));
|
||||
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
|
||||
var vm = new StoreLightningBalanceViewModel
|
||||
{
|
||||
StoreId = store.Id,
|
||||
CryptoCode = cryptoCode,
|
||||
InitialRendering = initialRendering,
|
||||
DefaultCurrency = defaultCurrency,
|
||||
CurrencyData = _currencies.GetCurrencyData(defaultCurrency, true),
|
||||
DataUrl = Url.Action("LightningBalanceDashboard", "UIStores", new { storeId = store.Id, cryptoCode })
|
||||
};
|
||||
|
||||
vm.DefaultCurrency = vm.Store.GetStoreBlob().DefaultCurrency;
|
||||
vm.CurrencyData = _currencies.GetCurrencyData(vm.DefaultCurrency, true);
|
||||
if (vm.InitialRendering)
|
||||
return View(vm);
|
||||
|
||||
try
|
||||
{
|
||||
var lightningClient = await GetLightningClient(vm.Store, vm.CryptoCode);
|
||||
if (lightningClient == null)
|
||||
{
|
||||
vm.InitialRendering = false;
|
||||
return View(vm);
|
||||
}
|
||||
var lightningClient = await GetLightningClient(store, vm.CryptoCode);
|
||||
|
||||
if (vm.InitialRendering)
|
||||
return View(vm);
|
||||
|
||||
var balance = await lightningClient.GetBalance();
|
||||
// balance
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var balance = await lightningClient.GetBalance(cts.Token);
|
||||
vm.Balance = balance;
|
||||
vm.TotalOnchain = balance.OnchainBalance != null
|
||||
? (balance.OnchainBalance.Confirmed ?? 0L) + (balance.OnchainBalance.Reserved ?? 0L) +
|
||||
@ -87,8 +91,16 @@ public class StoreLightningBalance : ViewComponent
|
||||
? (balance.OffchainBalance.Opening ?? 0) + (balance.OffchainBalance.Local ?? 0) +
|
||||
(balance.OffchainBalance.Closing ?? 0)
|
||||
: null;
|
||||
}
|
||||
|
||||
// histogram
|
||||
var data = await _lnHistogramService.GetHistogram(lightningClient, DefaultType, cts.Token);
|
||||
if (data != null)
|
||||
{
|
||||
vm.Type = data.Type;
|
||||
vm.Series = data.Series;
|
||||
vm.Labels = data.Labels;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is NotImplementedException or NotSupportedException)
|
||||
{
|
||||
// not all implementations support balance fetching
|
||||
@ -102,7 +114,7 @@ public class StoreLightningBalance : ViewComponent
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task<ILightningClient> GetLightningClient(StoreData store, string cryptoCode )
|
||||
private async Task<ILightningClient> GetLightningClient(StoreData store, string cryptoCode)
|
||||
{
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
var id = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
|
||||
|
@ -1,19 +1,26 @@
|
||||
using BTCPayServer.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Components.StoreLightningBalance;
|
||||
|
||||
public class StoreLightningBalanceViewModel
|
||||
{
|
||||
public string StoreId { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string DefaultCurrency { get; set; }
|
||||
public CurrencyData CurrencyData { get; set; }
|
||||
public StoreData Store { get; set; }
|
||||
public Money TotalOnchain { get; set; }
|
||||
public LightMoney TotalOffchain { get; set; }
|
||||
public LightningNodeBalance Balance { get; set; }
|
||||
public string ProblemDescription { get; set; }
|
||||
public bool InitialRendering { get; set; }
|
||||
public bool InitialRendering { get; set; } = true;
|
||||
public HistogramType Type { get; set; }
|
||||
public IList<DateTimeOffset> Labels { get; set; }
|
||||
public IList<decimal> Series { get; set; }
|
||||
public string DataUrl { get; set; }
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
@using BTCPayServer.Services.Wallets
|
||||
@using BTCPayServer.Payments
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.TagHelpers
|
||||
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
|
||||
@inject BTCPayNetworkProvider NetworkProvider
|
||||
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
|
||||
<div id="StoreWalletBalance-@Model.StoreId" class="widget store-wallet-balance">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
|
||||
<h6 text-translate="true">Wallet Balance</h6>
|
||||
@if (Model.CryptoCode != Model.DefaultCurrency)
|
||||
@ -26,12 +26,12 @@
|
||||
@if (Model.Series != null)
|
||||
{
|
||||
<div class="btn-group only-for-js mt-1" role="group" aria-label="Period">
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.Store.Id" id="StoreWalletBalancePeriodWeek-@Model.Store.Id" value="@WalletHistogramType.Week" @(Model.Type == WalletHistogramType.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodWeek-@Model.Store.Id">1W</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.Store.Id" id="StoreWalletBalancePeriodMonth-@Model.Store.Id" value="@WalletHistogramType.Month" @(Model.Type == WalletHistogramType.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodMonth-@Model.Store.Id">1M</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.Store.Id" id="StoreWalletBalancePeriodYear-@Model.Store.Id" value="@WalletHistogramType.Year" @(Model.Type == WalletHistogramType.Year ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodYear-@Model.Store.Id">1Y</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.StoreId" id="StoreWalletBalancePeriodWeek-@Model.StoreId" value="@HistogramType.Week" @(Model.Type == HistogramType.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodWeek-@Model.StoreId">1W</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.StoreId" id="StoreWalletBalancePeriodMonth-@Model.StoreId" value="@HistogramType.Month" @(Model.Type == HistogramType.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodMonth-@Model.StoreId">1M</label>
|
||||
<input type="radio" class="btn-check" name="StoreWalletBalancePeriod-@Model.StoreId" id="StoreWalletBalancePeriodYear-@Model.StoreId" value="@HistogramType.Year" @(Model.Type == HistogramType.Year ? "checked" : "")>
|
||||
<label class="btn btn-link" for="StoreWalletBalancePeriodYear-@Model.StoreId">1Y</label>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
@ -39,10 +39,10 @@
|
||||
{
|
||||
<div class="ct-chart"></div>
|
||||
}
|
||||
else if (Model.Store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(Model.CryptoCode)) is null)
|
||||
else if (Model.MissingWalletConfig)
|
||||
{
|
||||
<p>
|
||||
We would like to show you a chart of your balance but you have not yet <a href="@Url.Action("SetupWallet", "UIStores", new {storeId = Model.Store.Id, cryptoCode = Model.CryptoCode})">configured a wallet</a>.
|
||||
We would like to show you a chart of your balance but you have not yet <a href="@Url.Action("SetupWallet", "UIStores", new { storeId = Model.StoreId, cryptoCode = Model.CryptoCode })">configured a wallet</a>.
|
||||
</p>
|
||||
}
|
||||
else
|
||||
@ -55,7 +55,7 @@
|
||||
}
|
||||
<script>
|
||||
(function () {
|
||||
const storeId = @Safe.Json(Model.Store.Id);
|
||||
const storeId = @Safe.Json(Model.StoreId);
|
||||
const cryptoCode = @Safe.Json(Model.CryptoCode);
|
||||
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
|
||||
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
|
||||
@ -63,16 +63,29 @@
|
||||
let rate = null;
|
||||
|
||||
const id = `StoreWalletBalance-${storeId}`;
|
||||
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
|
||||
const valueTransform = value => rate
|
||||
? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility).toString()
|
||||
: value
|
||||
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = HistogramType.Week }));
|
||||
const valueTransform = value => rate ? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility) : value
|
||||
const labelCount = 6
|
||||
const tooltip = Chartist.plugins.tooltip2({
|
||||
template: '<div class="chartist-tooltip-value">{{value}}</div><div class="chartist-tooltip-line"></div>',
|
||||
offset: {
|
||||
x: 0,
|
||||
y: -16
|
||||
},
|
||||
valueTransformFunction(value, label) {
|
||||
return valueTransform(value) + ' ' + (rate ? defaultCurrency : cryptoCode)
|
||||
}
|
||||
})
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
||||
const dateFormatter = new Intl.DateTimeFormat('default', { month: 'short', day: 'numeric' })
|
||||
const chartOpts = {
|
||||
fullWidth: true,
|
||||
showArea: true,
|
||||
axisY: {
|
||||
labelInterpolationFnc: valueTransform
|
||||
}
|
||||
showLabel: false,
|
||||
offset: 0
|
||||
},
|
||||
plugins: [tooltip]
|
||||
};
|
||||
|
||||
const render = data => {
|
||||
@ -88,31 +101,17 @@
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = Math.max(min - ((max - min) / 5), 0);
|
||||
const tooltip = Chartist.plugins.tooltip2({
|
||||
template: '<div class="chartist-tooltip-value">{{value}}</div><div class="chartist-tooltip-line"></div>',
|
||||
offset: {
|
||||
x: 0,
|
||||
y: -16
|
||||
},
|
||||
valueTransformFunction: valueTransform
|
||||
})
|
||||
const renderOpts = Object.assign({}, chartOpts, { low, plugins: [tooltip] });
|
||||
const chart = new Chartist.Line(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
const renderOpts = Object.assign({}, chartOpts, { low, axisX: {
|
||||
labelInterpolationFnc(date, i) {
|
||||
return i % labelEvery === 0 ? dateFormatter.format(new Date(date)) : null
|
||||
}
|
||||
} });
|
||||
const pointCount = series.length;
|
||||
const labelEvery = pointCount / labelCount;
|
||||
new Chartist.Line(`#${id} .ct-chart`, {
|
||||
labels: labels,
|
||||
series: [series]
|
||||
}, renderOpts);
|
||||
|
||||
// prevent y-axis labels from getting cut off
|
||||
window.setTimeout(() => {
|
||||
const yLabels = [...document.querySelectorAll('.ct-label.ct-vertical.ct-start')];
|
||||
if (yLabels) {
|
||||
const width = Math.max(...(yLabels.map(l => l.innerText.length * 7.5)));
|
||||
const opts = Object.assign({}, renderOpts, {
|
||||
axisY: Object.assign({}, renderOpts.axisY, { offset: width })
|
||||
});
|
||||
chart.update(null, opts);
|
||||
}
|
||||
}, 0)
|
||||
};
|
||||
|
||||
const update = async type => {
|
||||
@ -126,7 +125,7 @@
|
||||
|
||||
render(data);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
function addEventListeners() {
|
||||
delegate('change', `#${id} [name="StoreWalletBalancePeriod-${storeId}"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
@ -141,7 +140,13 @@
|
||||
render(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
window.addEventListener("DOMContentLoaded", addEventListeners);
|
||||
} else {
|
||||
addEventListeners();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
|
@ -1,29 +1,21 @@
|
||||
#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.Client.Models;
|
||||
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;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Components.StoreWalletBalance;
|
||||
|
||||
public class StoreWalletBalance : ViewComponent
|
||||
{
|
||||
private const WalletHistogramType DefaultType = WalletHistogramType.Week;
|
||||
private const HistogramType DefaultType = HistogramType.Week;
|
||||
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
@ -57,7 +49,7 @@ public class StoreWalletBalance : ViewComponent
|
||||
|
||||
var vm = new StoreWalletBalanceViewModel
|
||||
{
|
||||
Store = store,
|
||||
StoreId = store.Id,
|
||||
CryptoCode = cryptoCode,
|
||||
CurrencyData = _currencies.GetCurrencyData(defaultCurrency, true),
|
||||
DefaultCurrency = defaultCurrency,
|
||||
@ -82,6 +74,10 @@ public class StoreWalletBalance : ViewComponent
|
||||
var balance = await wallet.GetBalance(derivation.AccountDerivation, cts.Token);
|
||||
vm.Balance = balance.Available.GetValue(network);
|
||||
}
|
||||
else
|
||||
{
|
||||
vm.MissingWalletConfig = true;
|
||||
}
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
|
@ -1,19 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Components.StoreWalletBalance;
|
||||
|
||||
public class StoreWalletBalanceViewModel
|
||||
{
|
||||
public string StoreId { get; set; }
|
||||
public decimal? Balance { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string DefaultCurrency { get; set; }
|
||||
public CurrencyData CurrencyData { get; set; }
|
||||
public StoreData Store { get; set; }
|
||||
public WalletId WalletId { get; set; }
|
||||
public WalletHistogramType Type { get; set; }
|
||||
public IList<string> Labels { get; set; }
|
||||
public HistogramType Type { get; set; }
|
||||
public IList<DateTimeOffset> Labels { get; set; }
|
||||
public IList<decimal> Series { get; set; }
|
||||
public bool MissingWalletConfig { get; set; }
|
||||
}
|
||||
|
@ -1,13 +1,10 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
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;
|
||||
@ -31,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;
|
||||
@ -55,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")]
|
||||
|
@ -1,8 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
@ -34,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 +56,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)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/connect")]
|
||||
@ -64,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")]
|
||||
@ -71,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")]
|
||||
|
@ -11,6 +11,7 @@ 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;
|
||||
@ -32,15 +33,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
|
||||
});
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetHistogram(string cryptoCode, HistogramType? type = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Enum.TryParse<HistogramType>(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 = data.Type,
|
||||
Balance = data.Balance,
|
||||
Series = data.Series,
|
||||
Labels = data.Labels
|
||||
});
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, true);
|
||||
|
@ -2,7 +2,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -20,7 +19,6 @@ using BTCPayServer.Payments.PayJoin;
|
||||
using BTCPayServer.Payments.PayJoin.Sender;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
@ -58,6 +56,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly IFeeProviderFactory _feeProviderFactory;
|
||||
private readonly UTXOLocker _utxoLocker;
|
||||
private readonly TransactionLinkProviders _transactionLinkProviders;
|
||||
private readonly WalletHistogramService _walletHistogramService;
|
||||
|
||||
public GreenfieldStoreOnChainWalletsController(
|
||||
IAuthorizationService authorizationService,
|
||||
@ -74,6 +73,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
WalletReceiveService walletReceiveService,
|
||||
IFeeProviderFactory feeProviderFactory,
|
||||
UTXOLocker utxoLocker,
|
||||
WalletHistogramService walletHistogramService,
|
||||
TransactionLinkProviders transactionLinkProviders
|
||||
)
|
||||
{
|
||||
@ -91,6 +91,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_walletReceiveService = walletReceiveService;
|
||||
_feeProviderFactory = feeProviderFactory;
|
||||
_utxoLocker = utxoLocker;
|
||||
_walletHistogramService = walletHistogramService;
|
||||
_transactionLinkProviders = transactionLinkProviders;
|
||||
}
|
||||
|
||||
@ -114,6 +115,27 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
});
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/histogram")]
|
||||
public async Task<IActionResult> GetOnChainWalletHistogram(string storeId, string paymentMethodId, [FromQuery] string? type = null)
|
||||
{
|
||||
if (IsInvalidWalletRequest(paymentMethodId, out var network, out var derivationScheme, out var actionResult))
|
||||
return actionResult;
|
||||
|
||||
var walletId = new WalletId(storeId, network.CryptoCode);
|
||||
Enum.TryParse<HistogramType>(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(new HistogramData
|
||||
{
|
||||
Type = data.Type,
|
||||
Balance = data.Balance,
|
||||
Series = data.Series,
|
||||
Labels = data.Labels
|
||||
});
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/feerate")]
|
||||
public async Task<IActionResult> GetOnChainFeeRate(string storeId, string paymentMethodId, int? blockTarget = null)
|
||||
|
@ -14,18 +14,15 @@ using BTCPayServer.Controllers.GreenField;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
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;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
||||
using Language = BTCPayServer.Client.Models.Language;
|
||||
@ -385,6 +382,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 +465,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,6 +714,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
await GetController<GreenfieldStoreOnChainWalletsController>().ShowOnChainWalletOverview(storeId, cryptoCode));
|
||||
}
|
||||
|
||||
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?.ToString()));
|
||||
}
|
||||
|
||||
public override async Task<OnChainWalletAddressData> GetOnChainWalletReceiveAddress(string storeId,
|
||||
string cryptoCode, bool forceGenerate = false,
|
||||
CancellationToken token = default)
|
||||
|
@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
@ -78,8 +77,7 @@ public partial class UIStoresController
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var vm = new StoreLightningBalanceViewModel { Store = store, CryptoCode = cryptoCode };
|
||||
return ViewComponent("StoreLightningBalance", new { vm });
|
||||
return ViewComponent("StoreLightningBalance", new { Store = store, CryptoCode = cryptoCode });
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/numbers")]
|
||||
|
@ -6,15 +6,20 @@ 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;
|
||||
|
||||
@ -82,6 +87,32 @@ public partial class UIStoresController
|
||||
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)
|
||||
@ -321,4 +352,26 @@ public partial class UIStoresController
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
@ -64,7 +63,9 @@ public partial class UIStoresController : Controller
|
||||
SettingsRepository settingsRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
IStringLocalizer stringLocalizer,
|
||||
EventAggregator eventAggregator)
|
||||
EventAggregator eventAggregator,
|
||||
LightningHistogramService lnHistogramService,
|
||||
LightningClientFactoryService lightningClientFactory)
|
||||
{
|
||||
_rateFactory = rateFactory;
|
||||
_storeRepo = storeRepo;
|
||||
@ -95,6 +96,8 @@ public partial class UIStoresController : Controller
|
||||
_dataProtector = dataProtector.CreateProtector("ConfigProtector");
|
||||
_webhookNotificationManager = webhookNotificationManager;
|
||||
_lightningNetworkOptions = lightningNetworkOptions.Value;
|
||||
_lnHistogramService = lnHistogramService;
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
StringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
@ -127,6 +130,8 @@ public partial class UIStoresController : Controller
|
||||
private readonly WebhookSender _webhookNotificationManager;
|
||||
private readonly LightningNetworkOptions _lightningNetworkOptions;
|
||||
private readonly IDataProtector _dataProtector;
|
||||
private readonly LightningHistogramService _lnHistogramService;
|
||||
private readonly LightningClientFactoryService _lightningClientFactory;
|
||||
|
||||
public string? GeneratedPairingCode { get; set; }
|
||||
public IStringLocalizer StringLocalizer { get; }
|
||||
|
@ -307,14 +307,13 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet("{walletId}/histogram/{type}")]
|
||||
public async Task<IActionResult> WalletHistogram(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletHistogramType type)
|
||||
WalletId walletId, HistogramType type)
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
var data = await _walletHistogramService.GetHistogram(store, walletId, type);
|
||||
if (data == null) return NotFound();
|
||||
|
||||
return data == null
|
||||
? NotFound()
|
||||
: Json(data);
|
||||
return Json(data);
|
||||
}
|
||||
|
||||
[HttpGet("{walletId}/receive")]
|
||||
|
@ -173,6 +173,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<UserService>();
|
||||
services.TryAddSingleton<UriResolver>();
|
||||
services.TryAddSingleton<WalletHistogramService>();
|
||||
services.TryAddSingleton<LightningHistogramService>();
|
||||
services.AddSingleton<ApplicationDbContextFactory>();
|
||||
services.AddOptions<BTCPayServerOptions>().Configure(
|
||||
(options) =>
|
||||
|
83
BTCPayServer/Services/LightningHistogramService.cs
Normal file
83
BTCPayServer/Services/LightningHistogramService.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Services;
|
||||
|
||||
public class LightningHistogramService
|
||||
{
|
||||
public async Task<HistogramData> GetHistogram(ILightningClient lightningClient, HistogramType type, CancellationToken cancellationToken)
|
||||
{
|
||||
var (days, pointCount) = type switch
|
||||
{
|
||||
HistogramType.Day => (1, 30),
|
||||
HistogramType.Week => (7, 30),
|
||||
HistogramType.Month => (30, 30),
|
||||
HistogramType.YTD => (DateTimeOffset.Now.DayOfYear - 1, 30),
|
||||
HistogramType.Year => (365, 30),
|
||||
HistogramType.TwoYears => (730, 30),
|
||||
_ => throw new ArgumentException($"HistogramType {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 total = lnBalance.OffchainBalance.Local;
|
||||
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 - 1));
|
||||
}
|
||||
// reverse the lists
|
||||
series.Reverse();
|
||||
labels.Reverse();
|
||||
return new HistogramData
|
||||
{
|
||||
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; }
|
||||
}
|
||||
}
|
@ -1,21 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Dapper;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Services.Wallets;
|
||||
|
||||
public enum WalletHistogramType
|
||||
{
|
||||
Week,
|
||||
Month,
|
||||
Year
|
||||
}
|
||||
|
||||
public class WalletHistogramService
|
||||
{
|
||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||
@ -29,7 +21,7 @@ public class WalletHistogramService
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<WalletHistogramData> GetHistogram(StoreData store, WalletId walletId, WalletHistogramType type)
|
||||
public async Task<HistogramData> GetHistogram(StoreData store, WalletId walletId, HistogramType type)
|
||||
{
|
||||
// https://github.com/dgarage/NBXplorer/blob/master/docs/Postgres-Schema.md
|
||||
if (_connectionFactory.Available)
|
||||
@ -43,13 +35,15 @@ public class WalletHistogramService
|
||||
|
||||
var code = walletId.CryptoCode;
|
||||
var to = DateTimeOffset.UtcNow;
|
||||
var labelCount = 6;
|
||||
(var days, var pointCount) = type switch
|
||||
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.")
|
||||
HistogramType.Day => (1, 30),
|
||||
HistogramType.Week => (7, 30),
|
||||
HistogramType.Month => (30, 30),
|
||||
HistogramType.YTD => (DateTimeOffset.Now.DayOfYear - 1, 30),
|
||||
HistogramType.Year => (365, 30),
|
||||
HistogramType.TwoYears => (730, 30),
|
||||
_ => throw new ArgumentException($"HistogramType {type} does not exist.")
|
||||
};
|
||||
var from = to - TimeSpan.FromDays(days);
|
||||
var interval = TimeSpan.FromTicks((to - from).Ticks / pointCount);
|
||||
@ -60,18 +54,15 @@ public class WalletHistogramService
|
||||
new { code, wallet_id, from, to, interval });
|
||||
var data = rows.AsList();
|
||||
var series = new List<decimal>(pointCount);
|
||||
var labels = new List<string>(labelCount);
|
||||
var labelEvery = pointCount / labelCount;
|
||||
var labels = new List<DateTimeOffset>(pointCount);
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
var r = data[i];
|
||||
series.Add((decimal)r.balance);
|
||||
labels.Add((i % labelEvery == 0)
|
||||
? ((DateTime)r.date).ToString("MMM dd", CultureInfo.InvariantCulture)
|
||||
: null);
|
||||
labels.Add((DateTimeOffset)r.date);
|
||||
}
|
||||
series[^1] = balance;
|
||||
return new WalletHistogramData
|
||||
return new HistogramData
|
||||
{
|
||||
Series = series,
|
||||
Labels = labels,
|
||||
@ -84,11 +75,3 @@ public class WalletHistogramService
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class WalletHistogramData
|
||||
{
|
||||
public WalletHistogramType Type { get; set; }
|
||||
public List<decimal> Series { get; set; }
|
||||
public List<string> Labels { get; set; }
|
||||
public decimal Balance { get; set; }
|
||||
}
|
||||
|
@ -81,7 +81,7 @@
|
||||
<vc:store-numbers vm="@(new StoreNumbersViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })" />
|
||||
@if (Model.LightningEnabled)
|
||||
{
|
||||
<vc:store-lightning-balance vm="@(new StoreLightningBalanceViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })" />
|
||||
<vc:store-lightning-balance store="store" crypto-code="@Model.CryptoCode" initial-rendering="true" />
|
||||
<vc:store-lightning-services vm="@(new StoreLightningServicesViewModel { Store = store, CryptoCode = Model.CryptoCode })" permission="@Policies.CanModifyServerSettings" />
|
||||
}
|
||||
@if (Model.WalletEnabled)
|
||||
|
@ -115,6 +115,49 @@
|
||||
"type": "string",
|
||||
"description": "Payout method IDs. Available payment method IDs for Bitcoin are: \n- `\"BTC-CHAIN\"`: Onchain \n-`\"BTC-LN\"`: Lightning",
|
||||
"example": "BTC-LN"
|
||||
},
|
||||
"HistogramData": {
|
||||
"type": "object",
|
||||
"description": "Histogram data for wallet balances over time",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The timespan of the histogram data",
|
||||
"x-enumNames": [
|
||||
"Week",
|
||||
"Month",
|
||||
"Year"
|
||||
],
|
||||
"enum": [
|
||||
"Week",
|
||||
"Month",
|
||||
"Year"
|
||||
],
|
||||
"default": "Week"
|
||||
},
|
||||
"balance": {
|
||||
"type": "string",
|
||||
"format": "decimal",
|
||||
"description": "The current wallet balance"
|
||||
},
|
||||
"series": {
|
||||
"type": "array",
|
||||
"description": "An array of historic balances of the wallet",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "decimal",
|
||||
"description": "The balance of the wallet at a specific time"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"type": "array",
|
||||
"description": "An array of timestamps associated with the series data",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"description": "UNIX timestamp of the balance snapshot"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
|
@ -96,6 +96,54 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/server/lightning/{cryptoCode}/histogram": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Lightning (Internal Node)"
|
||||
],
|
||||
"summary": "Get node balance histogram",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "BTC"
|
||||
}
|
||||
],
|
||||
"description": "View balance histogram of the lightning node",
|
||||
"operationId": "InternalLightningNodeApi_GetHistogram",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Lightning node balance histogram for off-chain funds",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HistogramData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": [
|
||||
"btcpay.server.canuseinternallightningnode"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/server/lightning/{cryptoCode}/connect": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
@ -114,6 +114,63 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/lightning/{cryptoCode}/histogram": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Lightning (Store)"
|
||||
],
|
||||
"summary": "Get node balance histogram",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cryptoCode",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The cryptoCode of the lightning-node to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "BTC"
|
||||
},
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store id with the lightning-node configuration to query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "View balance histogram of the lightning node",
|
||||
"operationId": "StoreLightningNodeApi_GetHistogram",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Lightning node balance histogram for off-chain funds",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HistogramData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Unable to access the lightning node"
|
||||
},
|
||||
"404": {
|
||||
"description": "The lightning node configuration was not found"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": [
|
||||
"btcpay.store.canuselightningnode"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/lightning/{cryptoCode}/connect": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
@ -50,6 +50,56 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/histogram": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Store Wallet (On Chain)"
|
||||
],
|
||||
"summary": "Get store on-chain wallet balance histogram",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "storeId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "The store to fetch",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/PaymentMethodId"
|
||||
}
|
||||
],
|
||||
"description": "View the balance histogram of the specified wallet",
|
||||
"operationId": "StoreOnChainWallets_ShowOnChainWalletHistogram",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "specified wallet",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HistogramData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to view the specified store"
|
||||
},
|
||||
"404": {
|
||||
"description": "The key is not found for this store/wallet"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"API_Key": [
|
||||
"btcpay.store.canmodifystoresettings"
|
||||
],
|
||||
"Basic": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/feerate": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
Loading…
Reference in New Issue
Block a user