Dashboard: Load Lightning balance async, display default currency (#3907)

* Dashboard: Load Lightning balance async, display default currency

* Simplify approach, improve views and scripts

* Remove LightMoney converters
This commit is contained in:
d11n 2022-07-04 04:03:16 +02:00 committed by GitHub
parent 2e2c6aef83
commit 2c3b8d8925
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 302 additions and 181 deletions

View File

@ -1,60 +1,78 @@
@using BTCPayServer.Lightning
@using BTCPayServer.TagHelpers
@model BTCPayServer.Components.StoreLightningBalance.StoreLightningBalanceViewModel
<div id="StoreLightningBalance-@Model.Store.Id" class="widget store-lightning-balance">
<header class="mb-3">
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
<h6>Lightning Balance</h6>
<a
asp-controller="UIPublicLightningNodeInfo"
asp-action="ShowLightningNodeInfo"app-top-items
asp-route-cryptoCode="@Model.CryptoCode"
asp-route-storeId="@Model.Store.Id"
target="_blank"
id="PublicNodeInfo">
Node Info
</a>
</header>
@if (Model.Balance == null)
@if (Model.CryptoCode != Model.DefaultCurrency && Model.Balance != null)
{
<div class="btn-group btn-group-sm gap-0 currency-toggle" role="group">
<input type="radio" class="btn-check" name="StoreLightningBalance-currency" id="StoreLightningBalance-currency_@Model.CryptoCode" value="@Model.CryptoCode" autocomplete="off" checked>
<label class="btn btn-outline-secondary px-2 py-1" for="StoreLightningBalance-currency_@Model.CryptoCode">@Model.CryptoCode</label>
<input type="radio" class="btn-check" name="StoreLightningBalance-currency" id="StoreLightningBalance-currency_@Model.DefaultCurrency" value="@Model.DefaultCurrency" autocomplete="off">
<label class="btn btn-outline-secondary px-2 py-1" for="StoreLightningBalance-currency_@Model.DefaultCurrency">@Model.DefaultCurrency</label>
</div>
}
</div>
@if (!string.IsNullOrEmpty(Model.ProblemDescription))
{
<p>@Model.ProblemDescription</p>
}
else
else if (Model.Balance != null)
{
<div class="balances d-flex flex-wrap gap-3">
<div class="balances d-flex flex-wrap">
@if (Model.TotalOffchain != LightMoney.Zero && Model.Balance.OffchainBalance != null)
{
<div class="balance">
<h3 class="d-inline-block me-1">@Model.TotalOffchain</h3>
<span class="text-secondary fw-semibold text-nowrap">@Model.CryptoCode in channels</span>
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain">@Model.TotalOffchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
<span class="currency">@Model.CryptoCode</span> in channels
</span>
<div class="balance-details collapse" id="balanceDetailsOffchain">
@if (Model.Balance.OffchainBalance.Opening != null && Model.Balance.OffchainBalance.Opening != LightMoney.Zero)
{
<div class="mt-2">
<span class="fw-semibold">@Model.Balance.OffchainBalance.Opening</span>
<span class="text-secondary text-nowrap">@Model.CryptoCode opening channels</span>
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Opening">
@Model.Balance.OffchainBalance.Opening
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> opening channels
</span>
</div>
}
@if (Model.Balance.OffchainBalance.Local != null)
{
<div class="mt-2">
<span class="fw-semibold">@Model.Balance.OffchainBalance.Local</span>
<span class="text-secondary text-nowrap">@Model.CryptoCode local balance</span>
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Local">
@Model.Balance.OffchainBalance.Local
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> local balance
</span>
</div>
}
@if (Model.Balance.OffchainBalance.Remote != null)
{
<div class="mt-2">
<span class="fw-semibold">@Model.Balance.OffchainBalance.Remote</span>
<span class="text-secondary text-nowrap">@Model.CryptoCode remote balance</span>
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Remote">
@Model.Balance.OffchainBalance.Remote
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> remote balance
</span>
</div>
}
@if (Model.Balance.OffchainBalance.Closing != null && Model.Balance.OffchainBalance.Closing != LightMoney.Zero)
{
<div class="mt-2">
<span class="fw-semibold">@Model.Balance.OffchainBalance.Closing</span>
<span class="text-secondary text-nowrap">@Model.CryptoCode closing channels</span>
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Closing">
@Model.Balance.OffchainBalance.Closing
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> closing channels
</span>
</div>
}
</div>
@ -63,28 +81,42 @@
@if (Model.TotalOnchain != LightMoney.Zero && Model.Balance.OnchainBalance != null)
{
<div class="balance">
<h3 class="d-inline-block me-1">@Model.TotalOnchain</h3>
<span class="text-secondary fw-semibold text-nowrap">@Model.CryptoCode on-chain</span>
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain">@Model.TotalOnchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
<span class="currency">@Model.CryptoCode</span> on-chain
</span>
<div class="balance-details collapse" id="balanceDetailsOnchain">
@if (Model.Balance.OnchainBalance.Confirmed != null && Model.Balance.OnchainBalance.Confirmed != LightMoney.Zero)
{
<div class="mt-2">
<span class="fw-semibold">@Model.Balance.OnchainBalance.Confirmed</span>
<span class="text-secondary text-nowrap">@Model.CryptoCode confirmed</span>
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Confirmed">
@Model.Balance.OnchainBalance.Confirmed
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> confirmed
</span>
</div>
}
@if (Model.Balance.OnchainBalance.Unconfirmed != null && Model.Balance.OnchainBalance.Unconfirmed != LightMoney.Zero)
{
<div class="mt-2">
<span class="fw-semibold">@Model.Balance.OnchainBalance.Unconfirmed</span>
<span class="text-secondary text-nowrap">@Model.CryptoCode unconfirmed</span>
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Unconfirmed">
@Model.Balance.OnchainBalance.Unconfirmed
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> unconfirmed
</span>
</div>
}
@if (Model.Balance.OnchainBalance.Reserved != null && Model.Balance.OnchainBalance.Reserved != LightMoney.Zero)
{
<div class="mt-2">
<span class="fw-semibold">@Model.Balance.OnchainBalance.Reserved</span>
<span class="text-secondary text-nowrap">@Model.CryptoCode reserved</span>
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Reserved">
@Model.Balance.OnchainBalance.Reserved
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> reserved
</span>
</div>
}
</div>
@ -96,4 +128,55 @@
<a class="d-inline-block mt-3" role="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">Show details</a>
}
}
else
{
<div class="loading d-flex justify-content-center p-3">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<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 response = await fetch(url);
if (response.ok) {
document.getElementById(`StoreLightningBalance-${storeId}`).outerHTML = await response.text();
}
})();
</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>

View File

@ -10,6 +10,7 @@ using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@ -20,6 +21,7 @@ public class StoreLightningBalance : ViewComponent
{
private string _cryptoCode;
private readonly StoreRepository _storeRepo;
private readonly CurrencyNameTable _currencies;
private readonly BTCPayServerOptions _btcpayServerOptions;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly LightningClientFactoryService _lightningClientFactory;
@ -28,6 +30,7 @@ public class StoreLightningBalance : ViewComponent
public StoreLightningBalance(
StoreRepository storeRepo,
CurrencyNameTable currencies,
BTCPayNetworkProvider networkProvider,
BTCPayServerOptions btcpayServerOptions,
LightningClientFactoryService lightningClientFactory,
@ -35,6 +38,7 @@ public class StoreLightningBalance : ViewComponent
IOptions<ExternalServicesOptions> externalServiceOptions)
{
_storeRepo = storeRepo;
_currencies = currencies;
_networkProvider = networkProvider;
_btcpayServerOptions = btcpayServerOptions;
_externalServiceOptions = externalServiceOptions;
@ -43,48 +47,40 @@ public class StoreLightningBalance : ViewComponent
_cryptoCode = _networkProvider.DefaultNetwork.CryptoCode;
}
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
public async Task<IViewComponentResult> InvokeAsync(StoreLightningBalanceViewModel vm)
{
var walletId = new WalletId(store.Id, _cryptoCode);
var lightningClient = GetLightningClient(store);
var vm = new StoreLightningBalanceViewModel
{
Store = store,
CryptoCode = _cryptoCode,
WalletId = walletId
};
if (vm.Store == null) throw new ArgumentNullException(nameof(vm.Store));
if (lightningClient != null)
{
try
{
var balance = await lightningClient.GetBalance();
vm.Balance = balance;
vm.TotalOnchain = balance.OnchainBalance != null
? (balance.OnchainBalance.Confirmed?? 0) + (balance.OnchainBalance.Reserved ?? 0) +
(balance.OnchainBalance.Unconfirmed ?? 0)
: null;
vm.TotalOffchain = balance.OffchainBalance != null
? (balance.OffchainBalance.Opening?? 0) + (balance.OffchainBalance.Local?? 0) +
(balance.OffchainBalance.Closing?? 0)
: null;
}
catch (NotSupportedException)
{
// not all implementations support balance fetching
vm.ProblemDescription = "Your node does not support balance fetching.";
}
catch
{
// general error
vm.ProblemDescription = "Could not fetch Lightning balance.";
}
}
else
{
vm.ProblemDescription = "Cannot instantiate Lightning client.";
}
vm.CryptoCode = _cryptoCode;
vm.DefaultCurrency = vm.Store.GetStoreBlob().DefaultCurrency;
vm.CurrencyData = _currencies.GetCurrencyData(vm.DefaultCurrency, true);
if (vm.InitialRendering) return View(vm);
try
{
var lightningClient = GetLightningClient(vm.Store);
var balance = await lightningClient.GetBalance();
vm.Balance = balance;
vm.TotalOnchain = balance.OnchainBalance != null
? (balance.OnchainBalance.Confirmed?? 0) + (balance.OnchainBalance.Reserved ?? 0) +
(balance.OnchainBalance.Unconfirmed ?? 0)
: null;
vm.TotalOffchain = balance.OffchainBalance != null
? (balance.OffchainBalance.Opening?? 0) + (balance.OffchainBalance.Local?? 0) +
(balance.OffchainBalance.Closing?? 0)
: null;
}
catch (NotSupportedException)
{
// not all implementations support balance fetching
vm.ProblemDescription = "Your node does not support balance fetching.";
}
catch
{
// general error
vm.ProblemDescription = "Could not fetch Lightning balance.";
}
return View(vm);
}

View File

@ -1,15 +1,20 @@
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Services.Rates;
using Newtonsoft.Json;
namespace BTCPayServer.Components.StoreLightningBalance;
public class StoreLightningBalanceViewModel
{
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 LightMoney TotalOnchain { get; set; }
public LightMoney TotalOffchain { get; set; }
public LightningNodeBalance Balance { get; set; }
public string ProblemDescription { get; set; }
public bool InitialRendering { get; set; }
}

View File

@ -6,7 +6,15 @@
<div id="StoreLightningServices-@Model.Store.Id" class="widget store-lightning-services">
<header class="mb-4">
<h6>Lightning Services</h6>
<a class asp-controller="UIStores" asp-action="Lightning" asp-route-storeId="@Model.Store.Id" asp-route-cryptoCode="@Model.CryptoCode">Details</a>
<a
asp-controller="UIPublicLightningNodeInfo"
asp-action="ShowLightningNodeInfo"app-top-items
asp-route-cryptoCode="@Model.CryptoCode"
asp-route-storeId="@Model.Store.Id"
target="_blank"
id="PublicNodeInfo">
Node Info
</a>
</header>
<div id="Services" class="services-list">
@foreach (var service in Model.Services)

View File

@ -1,43 +1,26 @@
@using BTCPayServer.Services.Wallets
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
<style>
#DefaultCurrencyToggle .btn {
background-color: var(--btcpay-bg-tile);
border-color: var(--btcpay-body-border-light);
}
#DefaultCurrencyToggle input:not(:checked) + .btn {
color: var(--btcpay-body-text-muted);
}
</style>
<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">
<h6>Wallet Balance</h6>
@if (Model.CryptoCode != Model.DefaultCurrency)
{
<div class="btn-group btn-group-sm gap-0" role="group" id="DefaultCurrencyToggle">
<input type="radio" class="btn-check" name="currency" id="currency_@Model.CryptoCode" value="@Model.CryptoCode" autocomplete="off" checked>
<label class="btn btn-outline-secondary px-2 py-1" for="currency_@Model.CryptoCode">@Model.CryptoCode</label>
<input type="radio" class="btn-check" name="currency" id="currency_@Model.DefaultCurrency" value="@Model.DefaultCurrency" autocomplete="off">
<label class="btn btn-outline-secondary px-2 py-1" for="currency_@Model.DefaultCurrency">@Model.DefaultCurrency</label>
<div class="btn-group btn-group-sm gap-0 currency-toggle" role="group">
<input type="radio" class="btn-check" name="StoreWalletBalance-currency" id="StoreWalletBalance-currency_@Model.CryptoCode" value="@Model.CryptoCode" autocomplete="off" checked>
<label class="btn btn-outline-secondary px-2 py-1" for="StoreWalletBalance-currency_@Model.CryptoCode">@Model.CryptoCode</label>
<input type="radio" class="btn-check" name="StoreWalletBalance-currency" id="StoreWalletBalance-currency_@Model.DefaultCurrency" value="@Model.DefaultCurrency" autocomplete="off">
<label class="btn btn-outline-secondary px-2 py-1" for="StoreWalletBalance-currency_@Model.DefaultCurrency">@Model.DefaultCurrency</label>
</div>
}
</div>
<header class="mb-3">
@if (Model.Balance != null)
{
<div class="balance" id="Balance-@Model.CryptoCode">
<h3 class="d-inline-block me-1">@Model.Balance</h3>
<span class="text-secondary fw-semibold">@Model.CryptoCode</span>
<div class="balance">
<h3 class="d-inline-block me-1" data-balance="@Model.Balance">@Model.Balance</h3>
<span class="text-secondary fw-semibold currency">@Model.CryptoCode</span>
</div>
@if (Model.CryptoCode != Model.DefaultCurrency)
{
<div class="balance" id="Balance-@Model.DefaultCurrency">
<h3 class="d-inline-block" id="DefaultCurrencyBalance"></h3>
<span class="text-secondary fw-semibold">@Model.DefaultCurrency</span>
</div>
}
}
@if (Model.Series != null)
{
@ -65,19 +48,13 @@
}
<script>
(function () {
const balance = @Safe.Json(Model.Balance);
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 pathBase = @Safe.Json(Context.Request.PathBase);
let data = { series: @Safe.Json(Model.Series), labels: @Safe.Json(Model.Labels), balance: @Safe.Json(Model.Balance) };
let rate = null;
const $cryptoBalance = document.getElementById(`Balance-${cryptoCode}`);
const $defaultBalance = document.getElementById(`Balance-${defaultCurrency}`);
const $defaultCurrencyBalance = document.getElementById('DefaultCurrencyBalance');
const id = `StoreWalletBalance-${storeId}`;
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
const chartOpts = {
@ -85,23 +62,23 @@
showArea: true,
axisY: {
labelInterpolationFnc: value => rate
? displayDefaultCurrency(value, defaultCurrency).toString()
? DashboardUtils.displayDefaultCurrency(value, rate, defaultCurrency, divisibility).toString()
: value
}
};
const render = data => {
let { series, labels, balance } = data;
if (balance)
document.querySelector(`#${id} h3`).innerText = balance;
if (cryptoCode !== defaultCurrency) {
$cryptoBalance.style.display = rate ? 'none' : 'block';
$defaultBalance.style.display = rate ? 'block' : 'none';
}
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 = rate
? DashboardUtils.displayDefaultCurrency(value, rate, currency, divisibility)
: value
});
if (!series) return;
if (rate)
series = data.series.map(i => toDefaultCurrency(i, rate));
const min = Math.min(...series);
const max = Math.max(...series);
const low = Math.max(min - ((max - min) / 5), 0);
@ -110,6 +87,7 @@
labels,
series: [series]
}, renderOpts);
// prevent y-axis labels from getting cut off
window.setTimeout(() => {
const yLabels = [...document.querySelectorAll('.ct-label.ct-vertical.ct-start')];
@ -133,16 +111,6 @@
}
};
const toDefaultCurrency = (value, rate) => {
return Math.round((value * rate) * 100) / 100;
};
const displayDefaultCurrency = (value, currency) => {
const locale = currency === "USD" ? 'en-US' : navigator.language;
const opts = { currency, style: 'decimal', minimumFractionDigits: divisibility };
return new Intl.NumberFormat(locale, opts).format(value);
};
render(data);
document.addEventListener('DOMContentLoaded', () => {
@ -150,20 +118,11 @@
const type = e.target.value;
await update(type);
})
delegate('change', '#DefaultCurrencyToggle input', async e => {
delegate('change', `#${id} .currency-toggle input`, async e => {
const { target } = e;
if (target.value === defaultCurrency) {
const currencyPair = `${cryptoCode}_${defaultCurrency}`;
const response = await fetch(`${pathBase}/api/rates?storeId=${storeId}&currencyPairs=${currencyPair}`);
const json = await response.json();
rate = json[0] && json[0].rate;
if (rate) {
const value = toDefaultCurrency(balance, rate);
$defaultCurrencyBalance.innerText = displayDefaultCurrency(value, defaultCurrency);
render(data);
} else {
console.warn(`Fetching rate for ${currencyPair} failed.`);
}
rate = await DashboardUtils.fetchRate(`${cryptoCode}_${defaultCurrency}`);
if (rate) render(data);
} else {
rate = null;
render(data);

View File

@ -0,0 +1,74 @@
#nullable enable
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Components.StoreLightningBalance;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Controllers
{
public partial class UIStoresController
{
[HttpGet("{storeId}")]
public async Task<IActionResult> Dashboard()
{
var store = CurrentStore;
var storeBlob = store.GetStoreBlob();
AddPaymentMethods(store, storeBlob,
out var derivationSchemes, out var lightningNodes);
var walletEnabled = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled);
var lightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled);
var vm = new StoreDashboardViewModel
{
WalletEnabled = walletEnabled,
LightningEnabled = lightningEnabled,
StoreId = CurrentStore.Id,
StoreName = CurrentStore.StoreName,
IsSetUp = walletEnabled || lightningEnabled
};
// Widget data
if (vm.WalletEnabled || vm.LightningEnabled)
{
var userId = GetUserId();
var apps = await _appService.GetAllApps(userId, false, store.Id);
vm.Apps = apps
.Select(a =>
{
var appData = _appService.GetAppDataIfOwner(userId, a.Id).Result;
appData.StoreData = store;
return appData;
})
.ToList();
}
return View(vm);
}
[HttpGet("{storeId}/lightning/{cryptoCode}/balance")]
public async Task<IActionResult> LightningBalance(string storeId, string cryptoCode)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var vm = new StoreLightningBalanceViewModel { Store = store };
return ViewComponent("StoreLightningBalance", new { vm });
}
}
}

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Components.StoreLightningBalance;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
@ -13,6 +14,7 @@ using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
@ -20,6 +22,7 @@ namespace BTCPayServer.Controllers
{
public partial class UIStoresController
{
[HttpGet("{storeId}/lightning/{cryptoCode}")]
public IActionResult Lightning(string storeId, string cryptoCode)
{

View File

@ -132,44 +132,6 @@ namespace BTCPayServer.Controllers
public StoreData CurrentStore => HttpContext.GetStoreData();
[HttpGet("{storeId}")]
public async Task<IActionResult> Dashboard()
{
var store = CurrentStore;
var storeBlob = store.GetStoreBlob();
AddPaymentMethods(store, storeBlob,
out var derivationSchemes, out var lightningNodes);
var walletEnabled = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled);
var lightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled);
var vm = new StoreDashboardViewModel
{
WalletEnabled = walletEnabled,
LightningEnabled = lightningEnabled,
StoreId = CurrentStore.Id,
StoreName = CurrentStore.StoreName,
IsSetUp = walletEnabled || lightningEnabled
};
// Widget data
if (vm.WalletEnabled || vm.LightningEnabled)
{
var userId = GetUserId();
var apps = await _appService.GetAllApps(userId, false, store.Id);
vm.Apps = apps
.Select(a =>
{
var appData = _appService.GetAppDataIfOwner(userId, a.Id).Result;
appData.StoreData = store;
return appData;
})
.ToList();
}
return View(vm);
}
[HttpPost]
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers(StoreUsersViewModel vm)

View File

@ -348,11 +348,6 @@ namespace BTCPayServer.Controllers
: Json(data);
}
private static string GetLabelTarget(WalletId walletId, uint256 txId)
{
return $"{walletId}:{txId}";
}
[HttpGet("{walletId}/receive")]
public IActionResult WalletReceive([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId)
{

View File

@ -1,3 +1,8 @@
@using BTCPayServer.Components.StoreLightningBalance
@using BTCPayServer.Components.StoreNumbers
@using BTCPayServer.Components.StoreWalletBalance
@using BTCPayServer.TagHelpers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model StoreDashboardViewModel;
@inject BTCPayNetworkProvider networkProvider
@ -56,6 +61,28 @@
/* include chart library inline so that it instantly renders */
<link rel="stylesheet" href="~/vendor/chartist/chartist.css" asp-append-version="true">
<script src="~/vendor/chartist/chartist.min.js" asp-append-version="true"></script>
<script>
const DashboardUtils = {
toDefaultCurrency(amount, rate) {
return Math.round((amount * rate) * 100) / 100;
},
displayDefaultCurrency(amount, rate, currency, divisibility) {
const value = DashboardUtils.toDefaultCurrency(amount, rate);
const locale = currency === 'USD' ? 'en-US' : navigator.language;
const opts = { currency, style: 'decimal', minimumFractionDigits: divisibility };
return new Intl.NumberFormat(locale, opts).format(value);
},
async fetchRate(currencyPair) {
const storeId = @Safe.Json(Context.GetRouteValue("storeId"));
const pathBase = @Safe.Json(Context.Request.PathBase);
const response = await fetch(`${pathBase}/api/rates?storeId=${storeId}&currencyPairs=${currencyPair}`);
const json = await response.json();
const rate = json[0] && json[0].rate;
if (rate) return rate;
else console.warn(`Fetching rate for ${currencyPair} failed.`);
}
};
</script>
<div id="Dashboard" class="mt-4">
@if (Model.WalletEnabled)
{
@ -87,7 +114,7 @@
<vc:store-numbers store="@store"/>
@if (Model.LightningEnabled)
{
<vc:store-lightning-balance store="@store"/>
<vc:store-lightning-balance vm="@(new StoreLightningBalanceViewModel { Store = store, InitialRendering = true })"/>
<vc:store-lightning-services store="@store"/>
}
@if (Model.WalletEnabled)

View File

@ -330,6 +330,15 @@ svg.icon-note {
margin-bottom: 0;
}
.widget .currency-toggle .btn {
background-color: var(--btcpay-bg-tile);
border-color: var(--btcpay-body-border-light);
}
.widget .currency-toggle input:not(:checked) + .btn {
color: var(--btcpay-body-text-muted);
}
.widget .btn-group {
display: inline-flex;
gap: var(--btcpay-space-m);