mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 05:54:26 +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:
@@ -21,6 +21,13 @@ public partial class BTCPayServerClient
|
|||||||
return await SendHttpRequest<LightningNodeBalanceData>($"api/v1/server/lightning/{cryptoCode}/balance", null, HttpMethod.Get, token);
|
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,
|
public virtual async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
|
||||||
CancellationToken token = default)
|
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);
|
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,
|
public virtual async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request,
|
||||||
CancellationToken token = default)
|
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);
|
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,
|
public virtual async Task<OnChainWalletFeeRateData> GetOnChainFeeRate(string storeId, string cryptoCode, int? blockTarget = null,
|
||||||
CancellationToken token = default)
|
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; }
|
||||||
|
}
|
||||||
@@ -3001,6 +3001,19 @@ namespace BTCPayServer.Tests
|
|||||||
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
||||||
Assert.Single(info.NodeURIs);
|
Assert.Single(info.NodeURIs);
|
||||||
Assert.NotEqual(0, info.BlockHeight);
|
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.
|
// As admin, can use the internal node through our store.
|
||||||
await user.MakeAdmin(true);
|
await user.MakeAdmin(true);
|
||||||
@@ -3023,6 +3036,10 @@ namespace BTCPayServer.Tests
|
|||||||
client = await guest.CreateClient(Policies.CanUseLightningNodeInStore);
|
client = await guest.CreateClient(Policies.CanUseLightningNodeInStore);
|
||||||
// Can use lightning node is only granted to store's owner
|
// Can use lightning node is only granted to store's owner
|
||||||
await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC"));
|
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)]
|
[Fact(Timeout = 60 * 20 * 1000)]
|
||||||
@@ -3538,8 +3555,7 @@ namespace BTCPayServer.Tests
|
|||||||
});
|
});
|
||||||
var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
|
var overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
|
||||||
Assert.Equal(0m, overview.Balance);
|
Assert.Equal(0m, overview.Balance);
|
||||||
|
|
||||||
|
|
||||||
var fee = await client.GetOnChainFeeRate(walletId.StoreId, walletId.CryptoCode);
|
var fee = await client.GetOnChainFeeRate(walletId.StoreId, walletId.CryptoCode);
|
||||||
Assert.NotNull(fee.FeeRate);
|
Assert.NotNull(fee.FeeRate);
|
||||||
|
|
||||||
@@ -3585,6 +3601,17 @@ namespace BTCPayServer.Tests
|
|||||||
overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
|
overview = await client.ShowOnChainWalletOverview(walletId.StoreId, walletId.CryptoCode);
|
||||||
Assert.Equal(0.01m, overview.Balance);
|
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:
|
//the simplest request:
|
||||||
var nodeAddress = await tester.ExplorerNode.GetNewAddressAsync();
|
var nodeAddress = await tester.ExplorerNode.GetNewAddressAsync();
|
||||||
var createTxRequest = new CreateOnChainTransactionRequest()
|
var createTxRequest = new CreateOnChainTransactionRequest()
|
||||||
|
|||||||
@@ -42,11 +42,20 @@ if (!window.appSales) {
|
|||||||
render(data, period);
|
render(data, period);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function addEventListeners() {
|
||||||
|
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
|
||||||
|
console.log("CHANGED", id)
|
||||||
|
const type = e.target.value;
|
||||||
|
await update(type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
delegate('change', `#${id} [name="AppSalesPeriod-${appId}"]`, async e => {
|
if (document.readyState === "loading") {
|
||||||
const type = e.target.value;
|
window.addEventListener("DOMContentLoaded", addEventListeners);
|
||||||
await update(type);
|
} 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
|
@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">
|
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
|
||||||
<h6 text-translate="true">Lightning Balance</h6>
|
<h6 text-translate="true">Lightning Balance</h6>
|
||||||
@if (Model.CryptoCode != Model.DefaultCurrency && Model.Balance != null)
|
@if (Model.CryptoCode != Model.DefaultCurrency && Model.Balance != null)
|
||||||
@@ -128,12 +132,32 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</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">
|
<div class="ct-chart"></div>
|
||||||
<vc:icon symbol="caret-down"/>
|
<template>
|
||||||
<span class="ms-1" text-translate="true">Details</span>
|
@Safe.Json(Model)
|
||||||
</button>
|
</template>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -143,47 +167,18 @@
|
|||||||
<span class="visually-hidden" text-translate="true">Loading...</span>
|
<span class="visually-hidden" text-translate="true">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="~/Components/StoreLightningBalance/Default.cshtml.js" asp-append-version="true"></script>
|
||||||
<script>
|
<script>
|
||||||
(async () => {
|
(async () => {
|
||||||
const url = @Safe.Json(Url.Action("LightningBalance", "UIStores", new { storeId = Model.Store.Id, cryptoCode = Model.CryptoCode }));
|
const url = @Safe.Json(Model.DataUrl);
|
||||||
const storeId = @Safe.Json(Model.Store.Id);
|
const storeId = @Safe.Json(Model.StoreId);
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
document.getElementById(`StoreLightningBalance-${storeId}`).outerHTML = await response.text();
|
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>
|
</script>
|
||||||
}
|
}
|
||||||
</div>
|
</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;
|
||||||
using System.Linq;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Models;
|
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
@@ -18,11 +16,14 @@ using BTCPayServer.Services.Stores;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
namespace BTCPayServer.Components.StoreLightningBalance;
|
namespace BTCPayServer.Components.StoreLightningBalance;
|
||||||
|
|
||||||
public class StoreLightningBalance : ViewComponent
|
public class StoreLightningBalance : ViewComponent
|
||||||
{
|
{
|
||||||
|
private const HistogramType DefaultType = HistogramType.Week;
|
||||||
|
|
||||||
private readonly StoreRepository _storeRepo;
|
private readonly StoreRepository _storeRepo;
|
||||||
private readonly CurrencyNameTable _currencies;
|
private readonly CurrencyNameTable _currencies;
|
||||||
private readonly BTCPayServerOptions _btcpayServerOptions;
|
private readonly BTCPayServerOptions _btcpayServerOptions;
|
||||||
@@ -32,6 +33,7 @@ public class StoreLightningBalance : ViewComponent
|
|||||||
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
|
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
|
||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||||
|
private readonly LightningHistogramService _lnHistogramService;
|
||||||
|
|
||||||
public StoreLightningBalance(
|
public StoreLightningBalance(
|
||||||
StoreRepository storeRepo,
|
StoreRepository storeRepo,
|
||||||
@@ -42,7 +44,8 @@ public class StoreLightningBalance : ViewComponent
|
|||||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||||
IOptions<ExternalServicesOptions> externalServiceOptions,
|
IOptions<ExternalServicesOptions> externalServiceOptions,
|
||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
PaymentMethodHandlerDictionary handlers)
|
PaymentMethodHandlerDictionary handlers,
|
||||||
|
LightningHistogramService lnHistogramService)
|
||||||
{
|
{
|
||||||
_storeRepo = storeRepo;
|
_storeRepo = storeRepo;
|
||||||
_currencies = currencies;
|
_currencies = currencies;
|
||||||
@@ -53,31 +56,32 @@ public class StoreLightningBalance : ViewComponent
|
|||||||
_handlers = handlers;
|
_handlers = handlers;
|
||||||
_lightningClientFactory = lightningClientFactory;
|
_lightningClientFactory = lightningClientFactory;
|
||||||
_lightningNetworkOptions = lightningNetworkOptions;
|
_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)
|
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
|
||||||
throw new ArgumentNullException(nameof(vm.Store));
|
var vm = new StoreLightningBalanceViewModel
|
||||||
if (vm.CryptoCode == null)
|
{
|
||||||
throw new ArgumentNullException(nameof(vm.CryptoCode));
|
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;
|
if (vm.InitialRendering)
|
||||||
vm.CurrencyData = _currencies.GetCurrencyData(vm.DefaultCurrency, true);
|
return View(vm);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var lightningClient = await GetLightningClient(vm.Store, vm.CryptoCode);
|
var lightningClient = await GetLightningClient(store, vm.CryptoCode);
|
||||||
if (lightningClient == null)
|
|
||||||
{
|
|
||||||
vm.InitialRendering = false;
|
|
||||||
return View(vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vm.InitialRendering)
|
// balance
|
||||||
return View(vm);
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||||
|
var balance = await lightningClient.GetBalance(cts.Token);
|
||||||
var balance = await lightningClient.GetBalance();
|
|
||||||
vm.Balance = balance;
|
vm.Balance = balance;
|
||||||
vm.TotalOnchain = balance.OnchainBalance != null
|
vm.TotalOnchain = balance.OnchainBalance != null
|
||||||
? (balance.OnchainBalance.Confirmed ?? 0L) + (balance.OnchainBalance.Reserved ?? 0L) +
|
? (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.Opening ?? 0) + (balance.OffchainBalance.Local ?? 0) +
|
||||||
(balance.OffchainBalance.Closing ?? 0)
|
(balance.OffchainBalance.Closing ?? 0)
|
||||||
: null;
|
: 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)
|
catch (Exception ex) when (ex is NotImplementedException or NotSupportedException)
|
||||||
{
|
{
|
||||||
// not all implementations support balance fetching
|
// not all implementations support balance fetching
|
||||||
@@ -102,7 +114,7 @@ public class StoreLightningBalance : ViewComponent
|
|||||||
return View(vm);
|
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 network = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||||
var id = PaymentTypes.LN.GetPaymentMethodId(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.Lightning;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
namespace BTCPayServer.Components.StoreLightningBalance;
|
namespace BTCPayServer.Components.StoreLightningBalance;
|
||||||
|
|
||||||
public class StoreLightningBalanceViewModel
|
public class StoreLightningBalanceViewModel
|
||||||
{
|
{
|
||||||
|
public string StoreId { get; set; }
|
||||||
public string CryptoCode { get; set; }
|
public string CryptoCode { get; set; }
|
||||||
public string DefaultCurrency { get; set; }
|
public string DefaultCurrency { get; set; }
|
||||||
public CurrencyData CurrencyData { get; set; }
|
public CurrencyData CurrencyData { get; set; }
|
||||||
public StoreData Store { get; set; }
|
|
||||||
public Money TotalOnchain { get; set; }
|
public Money TotalOnchain { get; set; }
|
||||||
public LightMoney TotalOffchain { get; set; }
|
public LightMoney TotalOffchain { get; set; }
|
||||||
public LightningNodeBalance Balance { get; set; }
|
public LightningNodeBalance Balance { get; set; }
|
||||||
public string ProblemDescription { 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.Abstractions.TagHelpers
|
||||||
@using BTCPayServer.Payments
|
@using BTCPayServer.Client.Models
|
||||||
|
@using BTCPayServer.TagHelpers
|
||||||
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
|
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
|
||||||
@inject BTCPayNetworkProvider NetworkProvider
|
<div id="StoreWalletBalance-@Model.StoreId" class="widget store-wallet-balance">
|
||||||
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
|
|
||||||
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
|
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
|
||||||
<h6 text-translate="true">Wallet Balance</h6>
|
<h6 text-translate="true">Wallet Balance</h6>
|
||||||
@if (Model.CryptoCode != Model.DefaultCurrency)
|
@if (Model.CryptoCode != Model.DefaultCurrency)
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
@if (Model.Series != null)
|
@if (Model.Series != null)
|
||||||
{
|
{
|
||||||
<div class="btn-group only-for-js mt-1" role="group" aria-label="Period">
|
<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" : "")>
|
<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.Store.Id">1W</label>
|
<label class="btn btn-link" for="StoreWalletBalancePeriodWeek-@Model.StoreId">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" : "")>
|
<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.Store.Id">1M</label>
|
<label class="btn btn-link" for="StoreWalletBalancePeriodMonth-@Model.StoreId">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" : "")>
|
<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.Store.Id">1Y</label>
|
<label class="btn btn-link" for="StoreWalletBalancePeriodYear-@Model.StoreId">1Y</label>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</header>
|
</header>
|
||||||
@@ -39,10 +39,10 @@
|
|||||||
{
|
{
|
||||||
<div class="ct-chart"></div>
|
<div class="ct-chart"></div>
|
||||||
}
|
}
|
||||||
else if (Model.Store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(Model.CryptoCode)) is null)
|
else if (Model.MissingWalletConfig)
|
||||||
{
|
{
|
||||||
<p>
|
<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>
|
</p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -55,66 +55,65 @@
|
|||||||
}
|
}
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const storeId = @Safe.Json(Model.Store.Id);
|
const storeId = @Safe.Json(Model.StoreId);
|
||||||
const cryptoCode = @Safe.Json(Model.CryptoCode);
|
const cryptoCode = @Safe.Json(Model.CryptoCode);
|
||||||
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
|
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
|
||||||
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
|
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
|
||||||
let data = { series: @Safe.Json(Model.Series), labels: @Safe.Json(Model.Labels), balance: @Safe.Json(Model.Balance) };
|
let data = { series: @Safe.Json(Model.Series), labels: @Safe.Json(Model.Labels), balance: @Safe.Json(Model.Balance) };
|
||||||
let rate = null;
|
let rate = null;
|
||||||
|
|
||||||
const id = `StoreWalletBalance-${storeId}`;
|
const id = `StoreWalletBalance-${storeId}`;
|
||||||
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
|
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = HistogramType.Week }));
|
||||||
const valueTransform = value => rate
|
const valueTransform = value => rate ? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility) : value
|
||||||
? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility).toString()
|
const labelCount = 6
|
||||||
: value
|
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 = {
|
const chartOpts = {
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
showArea: true,
|
showArea: true,
|
||||||
axisY: {
|
axisY: {
|
||||||
labelInterpolationFnc: valueTransform
|
showLabel: false,
|
||||||
}
|
offset: 0
|
||||||
|
},
|
||||||
|
plugins: [tooltip]
|
||||||
};
|
};
|
||||||
|
|
||||||
const render = data => {
|
const render = data => {
|
||||||
let { series, labels } = data;
|
let { series, labels } = data;
|
||||||
const currency = rate ? defaultCurrency : cryptoCode;
|
const currency = rate ? defaultCurrency : cryptoCode;
|
||||||
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
|
document.querySelectorAll(`#${id} .currency`).forEach(c => c.innerText = currency)
|
||||||
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
|
document.querySelectorAll(`#${id} [data-balance]`).forEach(c => {
|
||||||
const value = Number.parseFloat(c.dataset.balance);
|
const value = Number.parseFloat(c.dataset.balance);
|
||||||
c.innerText = valueTransform(value)
|
c.innerText = valueTransform(value)
|
||||||
});
|
});
|
||||||
if (!series) return;
|
if (!series) return;
|
||||||
|
|
||||||
const min = Math.min(...series);
|
const min = Math.min(...series);
|
||||||
const max = Math.max(...series);
|
const max = Math.max(...series);
|
||||||
const low = Math.max(min - ((max - min) / 5), 0);
|
const low = Math.max(min - ((max - min) / 5), 0);
|
||||||
const tooltip = Chartist.plugins.tooltip2({
|
const renderOpts = Object.assign({}, chartOpts, { low, axisX: {
|
||||||
template: '<div class="chartist-tooltip-value">{{value}}</div><div class="chartist-tooltip-line"></div>',
|
labelInterpolationFnc(date, i) {
|
||||||
offset: {
|
return i % labelEvery === 0 ? dateFormatter.format(new Date(date)) : null
|
||||||
x: 0,
|
}
|
||||||
y: -16
|
} });
|
||||||
},
|
const pointCount = series.length;
|
||||||
valueTransformFunction: valueTransform
|
const labelEvery = pointCount / labelCount;
|
||||||
})
|
new Chartist.Line(`#${id} .ct-chart`, {
|
||||||
const renderOpts = Object.assign({}, chartOpts, { low, plugins: [tooltip] });
|
labels: labels,
|
||||||
const chart = new Chartist.Line(`#${id} .ct-chart`, {
|
|
||||||
labels,
|
|
||||||
series: [series]
|
series: [series]
|
||||||
}, renderOpts);
|
}, 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 => {
|
const update = async type => {
|
||||||
const url = baseUrl.replace(/\/week$/gi, `/${type}`);
|
const url = baseUrl.replace(/\/week$/gi, `/${type}`);
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
@@ -123,10 +122,10 @@
|
|||||||
render(data);
|
render(data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render(data);
|
render(data);
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
function addEventListeners() {
|
||||||
delegate('change', `#${id} [name="StoreWalletBalancePeriod-${storeId}"]`, async e => {
|
delegate('change', `#${id} [name="StoreWalletBalancePeriod-${storeId}"]`, async e => {
|
||||||
const type = e.target.value;
|
const type = e.target.value;
|
||||||
await update(type);
|
await update(type);
|
||||||
@@ -141,7 +140,13 @@
|
|||||||
render(data);
|
render(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
window.addEventListener("DOMContentLoaded", addEventListeners);
|
||||||
|
} else {
|
||||||
|
addEventListeners();
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,21 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data.Common;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Services;
|
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
using Dapper;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NBitcoin;
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
using NBXplorer;
|
|
||||||
using NBXplorer.Client;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Components.StoreWalletBalance;
|
namespace BTCPayServer.Components.StoreWalletBalance;
|
||||||
|
|
||||||
public class StoreWalletBalance : ViewComponent
|
public class StoreWalletBalance : ViewComponent
|
||||||
{
|
{
|
||||||
private const WalletHistogramType DefaultType = WalletHistogramType.Week;
|
private const HistogramType DefaultType = HistogramType.Week;
|
||||||
|
|
||||||
private readonly StoreRepository _storeRepo;
|
private readonly StoreRepository _storeRepo;
|
||||||
private readonly CurrencyNameTable _currencies;
|
private readonly CurrencyNameTable _currencies;
|
||||||
@@ -57,7 +49,7 @@ public class StoreWalletBalance : ViewComponent
|
|||||||
|
|
||||||
var vm = new StoreWalletBalanceViewModel
|
var vm = new StoreWalletBalanceViewModel
|
||||||
{
|
{
|
||||||
Store = store,
|
StoreId = store.Id,
|
||||||
CryptoCode = cryptoCode,
|
CryptoCode = cryptoCode,
|
||||||
CurrencyData = _currencies.GetCurrencyData(defaultCurrency, true),
|
CurrencyData = _currencies.GetCurrencyData(defaultCurrency, true),
|
||||||
DefaultCurrency = defaultCurrency,
|
DefaultCurrency = defaultCurrency,
|
||||||
@@ -82,6 +74,10 @@ public class StoreWalletBalance : ViewComponent
|
|||||||
var balance = await wallet.GetBalance(derivation.AccountDerivation, cts.Token);
|
var balance = await wallet.GetBalance(derivation.AccountDerivation, cts.Token);
|
||||||
vm.Balance = balance.Available.GetValue(network);
|
vm.Balance = balance.Available.GetValue(network);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
vm.MissingWalletConfig = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return View(vm);
|
return View(vm);
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Wallets;
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
namespace BTCPayServer.Components.StoreWalletBalance;
|
namespace BTCPayServer.Components.StoreWalletBalance;
|
||||||
|
|
||||||
public class StoreWalletBalanceViewModel
|
public class StoreWalletBalanceViewModel
|
||||||
{
|
{
|
||||||
|
public string StoreId { get; set; }
|
||||||
public decimal? Balance { get; set; }
|
public decimal? Balance { get; set; }
|
||||||
public string CryptoCode { get; set; }
|
public string CryptoCode { get; set; }
|
||||||
public string DefaultCurrency { get; set; }
|
public string DefaultCurrency { get; set; }
|
||||||
public CurrencyData CurrencyData { get; set; }
|
public CurrencyData CurrencyData { get; set; }
|
||||||
public StoreData Store { get; set; }
|
|
||||||
public WalletId WalletId { get; set; }
|
public WalletId WalletId { get; set; }
|
||||||
public WalletHistogramType Type { get; set; }
|
public HistogramType Type { get; set; }
|
||||||
public IList<string> Labels { get; set; }
|
public IList<DateTimeOffset> Labels { get; set; }
|
||||||
public IList<decimal> Series { get; set; }
|
public IList<decimal> Series { get; set; }
|
||||||
|
public bool MissingWalletConfig { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.HostedServices;
|
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Security;
|
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -31,8 +28,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
PoliciesSettings policiesSettings, LightningClientFactoryService lightningClientFactory,
|
PoliciesSettings policiesSettings, LightningClientFactoryService lightningClientFactory,
|
||||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
PaymentMethodHandlerDictionary handlers
|
PaymentMethodHandlerDictionary handlers,
|
||||||
) : base(policiesSettings, authorizationService, handlers)
|
LightningHistogramService lnHistogramService
|
||||||
|
) : base(policiesSettings, authorizationService, handlers, lnHistogramService)
|
||||||
{
|
{
|
||||||
_lightningClientFactory = lightningClientFactory;
|
_lightningClientFactory = lightningClientFactory;
|
||||||
_lightningNetworkOptions = lightningNetworkOptions;
|
_lightningNetworkOptions = lightningNetworkOptions;
|
||||||
@@ -55,6 +53,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
return base.GetBalance(cryptoCode, cancellationToken);
|
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,
|
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/connect")]
|
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/connect")]
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Abstractions.Contracts;
|
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
@@ -34,7 +32,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
IOptions<LightningNetworkOptions> lightningNetworkOptions,
|
||||||
LightningClientFactoryService lightningClientFactory, PaymentMethodHandlerDictionary handlers,
|
LightningClientFactoryService lightningClientFactory, PaymentMethodHandlerDictionary handlers,
|
||||||
PoliciesSettings policiesSettings,
|
PoliciesSettings policiesSettings,
|
||||||
IAuthorizationService authorizationService) : base(policiesSettings, authorizationService, handlers)
|
IAuthorizationService authorizationService,
|
||||||
|
LightningHistogramService lnHistogramService) : base(policiesSettings, authorizationService, handlers, lnHistogramService)
|
||||||
{
|
{
|
||||||
_lightningNetworkOptions = lightningNetworkOptions;
|
_lightningNetworkOptions = lightningNetworkOptions;
|
||||||
_lightningClientFactory = lightningClientFactory;
|
_lightningClientFactory = lightningClientFactory;
|
||||||
@@ -56,6 +55,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
{
|
{
|
||||||
return base.GetBalance(cryptoCode, cancellationToken);
|
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,
|
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
@@ -64,6 +70,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
{
|
{
|
||||||
return base.ConnectToNode(cryptoCode, request, cancellationToken);
|
return base.ConnectToNode(cryptoCode, request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
|
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
|
||||||
@@ -71,6 +78,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
{
|
{
|
||||||
return base.GetChannels(cryptoCode, cancellationToken);
|
return base.GetChannels(cryptoCode, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
|
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using BTCPayServer.Payments.Bitcoin;
|
|||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Wallets;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
@@ -32,15 +33,18 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
private readonly PoliciesSettings _policiesSettings;
|
private readonly PoliciesSettings _policiesSettings;
|
||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||||
|
private readonly LightningHistogramService _lnHistogramService;
|
||||||
|
|
||||||
protected GreenfieldLightningNodeApiController(
|
protected GreenfieldLightningNodeApiController(
|
||||||
PoliciesSettings policiesSettings,
|
PoliciesSettings policiesSettings,
|
||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
PaymentMethodHandlerDictionary handlers)
|
PaymentMethodHandlerDictionary handlers,
|
||||||
|
LightningHistogramService lnHistogramService)
|
||||||
{
|
{
|
||||||
_policiesSettings = policiesSettings;
|
_policiesSettings = policiesSettings;
|
||||||
_authorizationService = authorizationService;
|
_authorizationService = authorizationService;
|
||||||
_handlers = handlers;
|
_handlers = handlers;
|
||||||
|
_lnHistogramService = lnHistogramService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<IActionResult> GetInfo(string cryptoCode, CancellationToken cancellationToken = default)
|
public virtual async Task<IActionResult> GetInfo(string cryptoCode, CancellationToken cancellationToken = default)
|
||||||
@@ -86,6 +90,22 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
: null
|
: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
public virtual async Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -20,7 +19,6 @@ using BTCPayServer.Payments.PayJoin;
|
|||||||
using BTCPayServer.Payments.PayJoin.Sender;
|
using BTCPayServer.Payments.PayJoin.Sender;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Labels;
|
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Cors;
|
using Microsoft.AspNetCore.Cors;
|
||||||
@@ -58,6 +56,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
private readonly IFeeProviderFactory _feeProviderFactory;
|
private readonly IFeeProviderFactory _feeProviderFactory;
|
||||||
private readonly UTXOLocker _utxoLocker;
|
private readonly UTXOLocker _utxoLocker;
|
||||||
private readonly TransactionLinkProviders _transactionLinkProviders;
|
private readonly TransactionLinkProviders _transactionLinkProviders;
|
||||||
|
private readonly WalletHistogramService _walletHistogramService;
|
||||||
|
|
||||||
public GreenfieldStoreOnChainWalletsController(
|
public GreenfieldStoreOnChainWalletsController(
|
||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
@@ -74,6 +73,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
WalletReceiveService walletReceiveService,
|
WalletReceiveService walletReceiveService,
|
||||||
IFeeProviderFactory feeProviderFactory,
|
IFeeProviderFactory feeProviderFactory,
|
||||||
UTXOLocker utxoLocker,
|
UTXOLocker utxoLocker,
|
||||||
|
WalletHistogramService walletHistogramService,
|
||||||
TransactionLinkProviders transactionLinkProviders
|
TransactionLinkProviders transactionLinkProviders
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -91,6 +91,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
_walletReceiveService = walletReceiveService;
|
_walletReceiveService = walletReceiveService;
|
||||||
_feeProviderFactory = feeProviderFactory;
|
_feeProviderFactory = feeProviderFactory;
|
||||||
_utxoLocker = utxoLocker;
|
_utxoLocker = utxoLocker;
|
||||||
|
_walletHistogramService = walletHistogramService;
|
||||||
_transactionLinkProviders = transactionLinkProviders;
|
_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)]
|
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||||
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/feerate")]
|
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/feerate")]
|
||||||
public async Task<IActionResult> GetOnChainFeeRate(string storeId, string paymentMethodId, int? blockTarget = null)
|
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.Data;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Security.Greenfield;
|
using BTCPayServer.Security.Greenfield;
|
||||||
using BTCPayServer.Services.Apps;
|
|
||||||
using BTCPayServer.Services.Mails;
|
using BTCPayServer.Services.Mails;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBXplorer.Models;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
||||||
using Language = BTCPayServer.Client.Models.Language;
|
using Language = BTCPayServer.Client.Models.Language;
|
||||||
@@ -385,6 +382,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
await GetController<GreenfieldStoreLightningNodeApiController>().GetBalance(cryptoCode, token));
|
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,
|
public override async Task ConnectToLightningNode(string storeId, string cryptoCode,
|
||||||
ConnectToNodeRequest request, CancellationToken token = default)
|
ConnectToNodeRequest request, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
@@ -461,6 +465,13 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
await GetController<GreenfieldInternalLightningNodeApiController>().GetBalance(cryptoCode));
|
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,
|
public override async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
@@ -702,6 +713,12 @@ namespace BTCPayServer.Controllers.Greenfield
|
|||||||
return GetFromActionResult<OnChainWalletOverviewData>(
|
return GetFromActionResult<OnChainWalletOverviewData>(
|
||||||
await GetController<GreenfieldStoreOnChainWalletsController>().ShowOnChainWalletOverview(storeId, cryptoCode));
|
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,
|
public override async Task<OnChainWalletAddressData> GetOnChainWalletReceiveAddress(string storeId,
|
||||||
string cryptoCode, bool forceGenerate = false,
|
string cryptoCode, bool forceGenerate = false,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
@@ -78,8 +77,7 @@ public partial class UIStoresController
|
|||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
var vm = new StoreLightningBalanceViewModel { Store = store, CryptoCode = cryptoCode };
|
return ViewComponent("StoreLightningBalance", new { Store = store, CryptoCode = cryptoCode });
|
||||||
return ViewComponent("StoreLightningBalance", new { vm });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{storeId}/dashboard/{cryptoCode}/numbers")]
|
[HttpGet("{storeId}/dashboard/{cryptoCode}/numbers")]
|
||||||
|
|||||||
@@ -6,15 +6,20 @@ using System.Threading.Tasks;
|
|||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
using BTCPayServer.Client;
|
using BTCPayServer.Client;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Components.StoreLightningBalance;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using BTCPayServer.Security;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
namespace BTCPayServer.Controllers;
|
namespace BTCPayServer.Controllers;
|
||||||
|
|
||||||
@@ -82,6 +87,32 @@ public partial class UIStoresController
|
|||||||
return View(vm);
|
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")]
|
[HttpGet("{storeId}/lightning/{cryptoCode}/setup")]
|
||||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||||
public IActionResult SetupLightningNode(string storeId, string cryptoCode)
|
public IActionResult SetupLightningNode(string storeId, string cryptoCode)
|
||||||
@@ -321,4 +352,26 @@ public partial class UIStoresController
|
|||||||
{
|
{
|
||||||
return store.GetPaymentMethodConfig<T>(paymentMethodId, _handlers);
|
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
|
#nullable enable
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
@@ -64,7 +63,9 @@ public partial class UIStoresController : Controller
|
|||||||
SettingsRepository settingsRepository,
|
SettingsRepository settingsRepository,
|
||||||
CurrencyNameTable currencyNameTable,
|
CurrencyNameTable currencyNameTable,
|
||||||
IStringLocalizer stringLocalizer,
|
IStringLocalizer stringLocalizer,
|
||||||
EventAggregator eventAggregator)
|
EventAggregator eventAggregator,
|
||||||
|
LightningHistogramService lnHistogramService,
|
||||||
|
LightningClientFactoryService lightningClientFactory)
|
||||||
{
|
{
|
||||||
_rateFactory = rateFactory;
|
_rateFactory = rateFactory;
|
||||||
_storeRepo = storeRepo;
|
_storeRepo = storeRepo;
|
||||||
@@ -95,6 +96,8 @@ public partial class UIStoresController : Controller
|
|||||||
_dataProtector = dataProtector.CreateProtector("ConfigProtector");
|
_dataProtector = dataProtector.CreateProtector("ConfigProtector");
|
||||||
_webhookNotificationManager = webhookNotificationManager;
|
_webhookNotificationManager = webhookNotificationManager;
|
||||||
_lightningNetworkOptions = lightningNetworkOptions.Value;
|
_lightningNetworkOptions = lightningNetworkOptions.Value;
|
||||||
|
_lnHistogramService = lnHistogramService;
|
||||||
|
_lightningClientFactory = lightningClientFactory;
|
||||||
StringLocalizer = stringLocalizer;
|
StringLocalizer = stringLocalizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +130,8 @@ public partial class UIStoresController : Controller
|
|||||||
private readonly WebhookSender _webhookNotificationManager;
|
private readonly WebhookSender _webhookNotificationManager;
|
||||||
private readonly LightningNetworkOptions _lightningNetworkOptions;
|
private readonly LightningNetworkOptions _lightningNetworkOptions;
|
||||||
private readonly IDataProtector _dataProtector;
|
private readonly IDataProtector _dataProtector;
|
||||||
|
private readonly LightningHistogramService _lnHistogramService;
|
||||||
|
private readonly LightningClientFactoryService _lightningClientFactory;
|
||||||
|
|
||||||
public string? GeneratedPairingCode { get; set; }
|
public string? GeneratedPairingCode { get; set; }
|
||||||
public IStringLocalizer StringLocalizer { get; }
|
public IStringLocalizer StringLocalizer { get; }
|
||||||
|
|||||||
@@ -307,14 +307,13 @@ namespace BTCPayServer.Controllers
|
|||||||
[HttpGet("{walletId}/histogram/{type}")]
|
[HttpGet("{walletId}/histogram/{type}")]
|
||||||
public async Task<IActionResult> WalletHistogram(
|
public async Task<IActionResult> WalletHistogram(
|
||||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||||
WalletId walletId, WalletHistogramType type)
|
WalletId walletId, HistogramType type)
|
||||||
{
|
{
|
||||||
var store = GetCurrentStore();
|
var store = GetCurrentStore();
|
||||||
var data = await _walletHistogramService.GetHistogram(store, walletId, type);
|
var data = await _walletHistogramService.GetHistogram(store, walletId, type);
|
||||||
|
if (data == null) return NotFound();
|
||||||
|
|
||||||
return data == null
|
return Json(data);
|
||||||
? NotFound()
|
|
||||||
: Json(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{walletId}/receive")]
|
[HttpGet("{walletId}/receive")]
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ namespace BTCPayServer.Hosting
|
|||||||
services.TryAddSingleton<UserService>();
|
services.TryAddSingleton<UserService>();
|
||||||
services.TryAddSingleton<UriResolver>();
|
services.TryAddSingleton<UriResolver>();
|
||||||
services.TryAddSingleton<WalletHistogramService>();
|
services.TryAddSingleton<WalletHistogramService>();
|
||||||
|
services.TryAddSingleton<LightningHistogramService>();
|
||||||
services.AddSingleton<ApplicationDbContextFactory>();
|
services.AddSingleton<ApplicationDbContextFactory>();
|
||||||
services.AddOptions<BTCPayServerOptions>().Configure(
|
services.AddOptions<BTCPayServerOptions>().Configure(
|
||||||
(options) =>
|
(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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Client.JsonConverters;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
|
using StoreData = BTCPayServer.Data.StoreData;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Wallets;
|
namespace BTCPayServer.Services.Wallets;
|
||||||
|
|
||||||
public enum WalletHistogramType
|
|
||||||
{
|
|
||||||
Week,
|
|
||||||
Month,
|
|
||||||
Year
|
|
||||||
}
|
|
||||||
|
|
||||||
public class WalletHistogramService
|
public class WalletHistogramService
|
||||||
{
|
{
|
||||||
private readonly PaymentMethodHandlerDictionary _handlers;
|
private readonly PaymentMethodHandlerDictionary _handlers;
|
||||||
@@ -29,7 +21,7 @@ public class WalletHistogramService
|
|||||||
_connectionFactory = connectionFactory;
|
_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
|
// https://github.com/dgarage/NBXplorer/blob/master/docs/Postgres-Schema.md
|
||||||
if (_connectionFactory.Available)
|
if (_connectionFactory.Available)
|
||||||
@@ -43,13 +35,15 @@ public class WalletHistogramService
|
|||||||
|
|
||||||
var code = walletId.CryptoCode;
|
var code = walletId.CryptoCode;
|
||||||
var to = DateTimeOffset.UtcNow;
|
var to = DateTimeOffset.UtcNow;
|
||||||
var labelCount = 6;
|
var (days, pointCount) = type switch
|
||||||
(var days, var pointCount) = type switch
|
|
||||||
{
|
{
|
||||||
WalletHistogramType.Week => (7, 30),
|
HistogramType.Day => (1, 30),
|
||||||
WalletHistogramType.Month => (30, 30),
|
HistogramType.Week => (7, 30),
|
||||||
WalletHistogramType.Year => (365, 30),
|
HistogramType.Month => (30, 30),
|
||||||
_ => throw new ArgumentException($"WalletHistogramType {type} does not exist.")
|
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 from = to - TimeSpan.FromDays(days);
|
||||||
var interval = TimeSpan.FromTicks((to - from).Ticks / pointCount);
|
var interval = TimeSpan.FromTicks((to - from).Ticks / pointCount);
|
||||||
@@ -60,18 +54,15 @@ public class WalletHistogramService
|
|||||||
new { code, wallet_id, from, to, interval });
|
new { code, wallet_id, from, to, interval });
|
||||||
var data = rows.AsList();
|
var data = rows.AsList();
|
||||||
var series = new List<decimal>(pointCount);
|
var series = new List<decimal>(pointCount);
|
||||||
var labels = new List<string>(labelCount);
|
var labels = new List<DateTimeOffset>(pointCount);
|
||||||
var labelEvery = pointCount / labelCount;
|
|
||||||
for (int i = 0; i < data.Count; i++)
|
for (int i = 0; i < data.Count; i++)
|
||||||
{
|
{
|
||||||
var r = data[i];
|
var r = data[i];
|
||||||
series.Add((decimal)r.balance);
|
series.Add((decimal)r.balance);
|
||||||
labels.Add((i % labelEvery == 0)
|
labels.Add((DateTimeOffset)r.date);
|
||||||
? ((DateTime)r.date).ToString("MMM dd", CultureInfo.InvariantCulture)
|
|
||||||
: null);
|
|
||||||
}
|
}
|
||||||
series[^1] = balance;
|
series[^1] = balance;
|
||||||
return new WalletHistogramData
|
return new HistogramData
|
||||||
{
|
{
|
||||||
Series = series,
|
Series = series,
|
||||||
Labels = labels,
|
Labels = labels,
|
||||||
@@ -84,11 +75,3 @@ public class WalletHistogramService
|
|||||||
return null;
|
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 })" />
|
<vc:store-numbers vm="@(new StoreNumbersViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })" />
|
||||||
@if (Model.LightningEnabled)
|
@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" />
|
<vc:store-lightning-services vm="@(new StoreLightningServicesViewModel { Store = store, CryptoCode = Model.CryptoCode })" permission="@Policies.CanModifyServerSettings" />
|
||||||
}
|
}
|
||||||
@if (Model.WalletEnabled)
|
@if (Model.WalletEnabled)
|
||||||
|
|||||||
@@ -115,6 +115,49 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Payout method IDs. Available payment method IDs for Bitcoin are: \n- `\"BTC-CHAIN\"`: Onchain \n-`\"BTC-LN\"`: Lightning",
|
"description": "Payout method IDs. Available payment method IDs for Bitcoin are: \n- `\"BTC-CHAIN\"`: Onchain \n-`\"BTC-LN\"`: Lightning",
|
||||||
"example": "BTC-LN"
|
"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": {
|
"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": {
|
"/api/v1/server/lightning/{cryptoCode}/connect": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"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": {
|
"/api/v1/stores/{storeId}/lightning/{cryptoCode}/connect": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"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": {
|
"/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}/wallet/feerate": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|||||||
Reference in New Issue
Block a user