mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 13:26:47 +01:00
Dashboard (#3530)
* Add dashboard and chart basics * More widgets * Make widgets responsive * Layout dashboard * Prepare ExplorerClient * Switch to Chartist * Dynamic data for store numbers and recent transactions tiles * Dynamic data for recent invoices tile * Improvements * Plug NBXPlorer DB * Properly filter by code * Reorder cheat mode button * AJAX update for graph data * Fix create invoice button * Retry connection on transient issues * App Top Items stats * Design updates * App Sales stats * Add points for weekly histogram, set last point to current balance Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
d58803a058
commit
7ec978fcdb
@ -453,6 +453,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.GoToStore();
|
||||
Assert.Contains(storeName, s.Driver.PageSource);
|
||||
Assert.DoesNotContain("id=\"Dashboard\"", s.Driver.PageSource);
|
||||
|
||||
// verify steps for wallet setup are displayed correctly
|
||||
s.GoToStore(StoreNavPages.Dashboard);
|
||||
@ -466,10 +467,11 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.AssertNoError();
|
||||
|
||||
s.GoToStore(StoreNavPages.Dashboard);
|
||||
Assert.True(s.Driver.FindElement(By.Id("SetupGuide-WalletDone")).Displayed);
|
||||
Assert.DoesNotContain("id=\"SetupGuide\"", s.Driver.PageSource);
|
||||
Assert.True(s.Driver.FindElement(By.Id("Dashboard")).Displayed);
|
||||
|
||||
// setup offchain wallet
|
||||
s.Driver.FindElement(By.Id("SetupGuide-Lightning")).Click();
|
||||
s.Driver.FindElement(By.Id("StoreNav-LightningBTC")).Click();
|
||||
s.AddLightningNode();
|
||||
s.Driver.AssertNoError();
|
||||
var successAlert = s.FindAlertMessage();
|
||||
@ -477,9 +479,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.ClickOnAllSectionLinks();
|
||||
|
||||
s.GoToStore(StoreNavPages.Dashboard);
|
||||
Assert.True(s.Driver.FindElement(By.Id("SetupGuide-LightningDone")).Displayed);
|
||||
|
||||
s.GoToInvoices();
|
||||
Assert.Contains("There are no invoices matching your criteria.", s.Driver.PageSource);
|
||||
var invoiceId = s.CreateInvoice();
|
||||
|
@ -53,6 +53,7 @@
|
||||
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
|
||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Fido2" Version="2.0.1" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
|
34
BTCPayServer/Components/AppSales/AppSales.cs
Normal file
34
BTCPayServer/Components/AppSales/AppSales.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.AppSales;
|
||||
|
||||
public class AppSales : ViewComponent
|
||||
{
|
||||
private readonly AppService _appService;
|
||||
private readonly StoreRepository _storeRepo;
|
||||
|
||||
public AppSales(AppService appService, StoreRepository storeRepo)
|
||||
{
|
||||
_appService = appService;
|
||||
_storeRepo = storeRepo;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(AppData app)
|
||||
{
|
||||
var stats = await _appService.GetSalesStats(app);
|
||||
var vm = new AppSalesViewModel
|
||||
{
|
||||
App = app,
|
||||
SalesCount = stats.SalesCount,
|
||||
Series = stats.Series
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
13
BTCPayServer/Components/AppSales/AppSalesViewModel.cs
Normal file
13
BTCPayServer/Components/AppSales/AppSalesViewModel.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppSales;
|
||||
|
||||
public class AppSalesViewModel
|
||||
{
|
||||
public AppData App { get; set; }
|
||||
public int SalesCount { get; set; }
|
||||
public IEnumerable<SalesStatsItem> Series { get; set; }
|
||||
}
|
30
BTCPayServer/Components/AppSales/Default.cshtml
Normal file
30
BTCPayServer/Components/AppSales/Default.cshtml
Normal file
@ -0,0 +1,30 @@
|
||||
@model BTCPayServer.Components.AppSales.AppSalesViewModel
|
||||
|
||||
@{
|
||||
var action = $"Update{Model.App.AppType}";
|
||||
}
|
||||
|
||||
<div id="AppSales-@Model.App.Id" class="widget app-sales">
|
||||
<header class="mb-3">
|
||||
<h3>@Model.App.Name Sales</h3>
|
||||
<a asp-controller="UIApps" asp-action="@action" asp-route-appId="@Model.App.Id">Manage</a>
|
||||
</header>
|
||||
<p>@Model.SalesCount Total Sales</p>
|
||||
<div class="ct-chart ct-major-octave"></div>
|
||||
<script>
|
||||
(function () {
|
||||
const id = 'AppSales-@Model.App.Id';
|
||||
const labels = @Safe.Json(Model.Series.Select(i => i.Label));
|
||||
const series = @Safe.Json(Model.Series.Select(i => i.SalesCount));
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
|
||||
new Chartist.Bar(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
}, {
|
||||
low,
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
32
BTCPayServer/Components/AppTopItems/AppTopItems.cs
Normal file
32
BTCPayServer/Components/AppTopItems/AppTopItems.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.AppTopItems;
|
||||
|
||||
public class AppTopItems : ViewComponent
|
||||
{
|
||||
private readonly AppService _appService;
|
||||
private readonly StoreRepository _storeRepo;
|
||||
|
||||
public AppTopItems(AppService appService, StoreRepository storeRepo)
|
||||
{
|
||||
_appService = appService;
|
||||
_storeRepo = storeRepo;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(AppData app)
|
||||
{
|
||||
var entries = await _appService.GetPerkStats(app);
|
||||
var vm = new AppTopItemsViewModel
|
||||
{
|
||||
App = app,
|
||||
Entries = entries
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
11
BTCPayServer/Components/AppTopItems/AppTopItemsViewModel.cs
Normal file
11
BTCPayServer/Components/AppTopItems/AppTopItemsViewModel.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.AppTopItems;
|
||||
|
||||
public class AppTopItemsViewModel
|
||||
{
|
||||
public AppData App { get; set; }
|
||||
public IEnumerable<ItemStats> Entries { get; set; }
|
||||
}
|
33
BTCPayServer/Components/AppTopItems/Default.cshtml
Normal file
33
BTCPayServer/Components/AppTopItems/Default.cshtml
Normal file
@ -0,0 +1,33 @@
|
||||
@model BTCPayServer.Components.AppTopItems.AppTopItemsViewModel
|
||||
|
||||
@{
|
||||
var action = $"Update{Model.App.AppType}";
|
||||
}
|
||||
|
||||
<div class="widget app-top-items">
|
||||
<header class="mb-3">
|
||||
<h3>@Model.App.Name Top Items</h3>
|
||||
<a asp-controller="UIApps" asp-action="@action" asp-route-appId="@Model.App.Id">View All</a>
|
||||
</header>
|
||||
@if (Model.Entries.Any())
|
||||
{
|
||||
<div class="app-items">
|
||||
@foreach (var entry in Model.Entries)
|
||||
{
|
||||
<div class="app-item">
|
||||
<span class="app-item-name">@entry.Title</span>
|
||||
<span class="app-item-value">
|
||||
@entry.SalesCount sale@(entry.SalesCount == 1 ? "" : "s"),
|
||||
@entry.TotalFormatted total
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary mt-3">
|
||||
There are no sales yet.
|
||||
</p>
|
||||
}
|
||||
</div>
|
27
BTCPayServer/Components/StoreNumbers/Default.cshtml
Normal file
27
BTCPayServer/Components/StoreNumbers/Default.cshtml
Normal file
@ -0,0 +1,27 @@
|
||||
@model BTCPayServer.Components.StoreNumbers.StoreNumbersViewModel
|
||||
|
||||
<div class="widget store-numbers">
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Payouts Pending</h6>
|
||||
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id">Manage</a>
|
||||
</header>
|
||||
<div class="h3">@Model.PayoutsPending</div>
|
||||
</div>
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>TXs in the last @Model.TransactionDays days</h6>
|
||||
@if (Model.Transactions > 0)
|
||||
{
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
|
||||
}
|
||||
</header>
|
||||
<div class="h3">@Model.Transactions</div>
|
||||
</div>
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Refunds Issued</h6>
|
||||
</header>
|
||||
<div class="h3">@Model.RefundsIssued</div>
|
||||
</div>
|
||||
</div>
|
77
BTCPayServer/Components/StoreNumbers/StoreNumbers.cs
Normal file
77
BTCPayServer/Components/StoreNumbers/StoreNumbers.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Components.StoreRecentTransactions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Components.StoreNumbers;
|
||||
|
||||
public class StoreNumbers : ViewComponent
|
||||
{
|
||||
private const string CryptoCode = "BTC";
|
||||
private const int TransactionDays = 7;
|
||||
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
|
||||
public StoreNumbers(
|
||||
StoreRepository storeRepo,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayWalletProvider walletProvider)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
_walletProvider = walletProvider;
|
||||
_networkProvider = networkProvider;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||
{
|
||||
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var payoutsCount = await ctx.Payouts
|
||||
.Where(p => p.PullPaymentData.StoreId == store.Id && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingApproval)
|
||||
.CountAsync();
|
||||
var refundsCount = await ctx.Invoices
|
||||
.Where(i => i.StoreData.Id == store.Id && !i.Archived && i.CurrentRefundId != null)
|
||||
.CountAsync();
|
||||
|
||||
var walletId = new WalletId(store.Id, CryptoCode);
|
||||
var derivation = store.GetDerivationSchemeSettings(_networkProvider, walletId.CryptoCode);
|
||||
var transactionsCount = 0;
|
||||
if (derivation != null)
|
||||
{
|
||||
var network = derivation.Network;
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var allTransactions = await wallet.FetchTransactions(derivation.AccountDerivation);
|
||||
var afterDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(TransactionDays);
|
||||
transactionsCount = allTransactions.UnconfirmedTransactions.Transactions
|
||||
.Concat(allTransactions.ConfirmedTransactions.Transactions)
|
||||
.Count(t => t.Timestamp > afterDate);
|
||||
}
|
||||
|
||||
var vm = new StoreNumbersViewModel
|
||||
{
|
||||
Store = store,
|
||||
WalletId = walletId,
|
||||
PayoutsPending = payoutsCount,
|
||||
Transactions = transactionsCount,
|
||||
TransactionDays = TransactionDays,
|
||||
RefundsIssued = refundsCount
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
using System.Collections;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Components.StoreNumbers;
|
||||
|
||||
public class StoreNumbersViewModel
|
||||
{
|
||||
public StoreData Store { get; set; }
|
||||
public WalletId WalletId { get; set; }
|
||||
public int PayoutsPending { get; set; }
|
||||
public int Transactions { get; set; }
|
||||
public int RefundsIssued { get; set; }
|
||||
public int TransactionDays { get; set; }
|
||||
}
|
57
BTCPayServer/Components/StoreRecentInvoices/Default.cshtml
Normal file
57
BTCPayServer/Components/StoreRecentInvoices/Default.cshtml
Normal file
@ -0,0 +1,57 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Services.Invoices
|
||||
@model BTCPayServer.Components.StoreRecentInvoices.StoreRecentInvoicesViewModel
|
||||
|
||||
<div class="widget store-recent-transactions">
|
||||
<header>
|
||||
<h3>Recent Invoices</h3>
|
||||
@if (Model.Invoices.Any())
|
||||
{
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id">View All</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.Invoices.Any())
|
||||
{
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th class="text-nowrap">Invoice Id</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<tr>
|
||||
<td>@invoice.Date.ToTimeAgo()</td>
|
||||
<td>
|
||||
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
@invoice.Status.Status.ToModernStatus().ToString()
|
||||
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
{
|
||||
@($"({invoice.Status.ExceptionStatus.ToString()})")
|
||||
}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">@invoice.AmountCurrency</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary my-3">
|
||||
There are no recent invoices.
|
||||
</p>
|
||||
<a asp-controller="UIInvoice" asp-action="CreateInvoice" asp-route-storeId="@Model.Store.Id" class="fw-semibold">
|
||||
Create Invoice
|
||||
</a>
|
||||
}
|
||||
</div>
|
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentInvoices;
|
||||
|
||||
public class StoreRecentInvoiceViewModel
|
||||
{
|
||||
public string InvoiceId { get; set; }
|
||||
public string OrderId { get; set; }
|
||||
public string AmountCurrency { get; set; }
|
||||
public InvoiceState Status { get; set; }
|
||||
public DateTimeOffset Date { get; set; }
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentInvoices;
|
||||
|
||||
public class StoreRecentInvoices : ViewComponent
|
||||
{
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly InvoiceRepository _invoiceRepo;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
|
||||
public StoreRecentInvoices(
|
||||
StoreRepository storeRepo,
|
||||
InvoiceRepository invoiceRepo,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ApplicationDbContextFactory dbContextFactory)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
_invoiceRepo = invoiceRepo;
|
||||
_userManager = userManager;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||
{
|
||||
var userId = _userManager.GetUserId(UserClaimsPrincipal);
|
||||
var invoiceEntities = await _invoiceRepo.GetInvoices(new InvoiceQuery
|
||||
{
|
||||
UserId = userId,
|
||||
StoreId = new [] { store.Id },
|
||||
Take = 5
|
||||
});
|
||||
var invoices = new List<StoreRecentInvoiceViewModel>();
|
||||
foreach (var invoice in invoiceEntities)
|
||||
{
|
||||
var state = invoice.GetInvoiceState();
|
||||
invoices.Add(new StoreRecentInvoiceViewModel
|
||||
{
|
||||
Date = invoice.InvoiceTime,
|
||||
Status = state,
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
AmountCurrency = _currencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
|
||||
});
|
||||
}
|
||||
var vm = new StoreRecentInvoicesViewModel
|
||||
{
|
||||
Store = store,
|
||||
Invoices = invoices
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentInvoices;
|
||||
|
||||
public class StoreRecentInvoicesViewModel
|
||||
{
|
||||
public StoreData Store { get; set; }
|
||||
public IEnumerable<StoreRecentInvoiceViewModel> Invoices { get; set; }
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@model BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel
|
||||
|
||||
<div class="widget store-recent-transactions">
|
||||
<header>
|
||||
<h3>Recent Transactions</h3>
|
||||
@if (Model.Transactions.Any())
|
||||
{
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
|
||||
}
|
||||
</header>
|
||||
@if (Model.Transactions.Any())
|
||||
{
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th>Transaction</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var tx in Model.Transactions)
|
||||
{
|
||||
<tr>
|
||||
<td>@tx.Timestamp.ToTimeAgo()</td>
|
||||
<td>
|
||||
<a href="@tx.Link" target="_blank" rel="noreferrer noopener" class="text-break">
|
||||
@tx.Id
|
||||
</a>
|
||||
</td>
|
||||
@if (tx.Positive)
|
||||
{
|
||||
<td class="text-end text-success">@tx.Balance</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-end text-danger">@tx.Balance</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary mt-3 mb-0">
|
||||
There are no recent transactions.
|
||||
</p>
|
||||
}
|
||||
</div>
|
@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentTransactions;
|
||||
|
||||
public class StoreRecentTransactionViewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Balance { get; set; }
|
||||
public bool Positive { get; set; }
|
||||
public bool IsConfirmed { get; set; }
|
||||
public string Link { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Dapper;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBXplorer.Client;
|
||||
using static BTCPayServer.Components.StoreRecentTransactions.StoreRecentTransactionsViewModel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentTransactions;
|
||||
|
||||
public class StoreRecentTransactions : ViewComponent
|
||||
{
|
||||
private const string CryptoCode = "BTC";
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; }
|
||||
public NBXplorerConnectionFactory ConnectionFactory { get; }
|
||||
|
||||
public StoreRecentTransactions(
|
||||
StoreRepository storeRepo,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
NBXplorerConnectionFactory connectionFactory,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
ApplicationDbContextFactory dbContextFactory)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
NetworkProvider = networkProvider;
|
||||
ConnectionFactory = connectionFactory;
|
||||
_walletProvider = walletProvider;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||
{
|
||||
var walletId = new WalletId(store.Id, CryptoCode);
|
||||
var derivationSettings = store.GetDerivationSchemeSettings(NetworkProvider, walletId.CryptoCode);
|
||||
var transactions = new List<StoreRecentTransactionViewModel>();
|
||||
if (derivationSettings?.AccountDerivation is not null)
|
||||
{
|
||||
if (ConnectionFactory.Available)
|
||||
{
|
||||
var wallet_id = derivationSettings.GetNBXWalletId();
|
||||
await using var conn = await ConnectionFactory.OpenConnection();
|
||||
var rows = await conn.QueryAsync(
|
||||
"SELECT t.tx_id, t.seen_at, to_btc(balance_change::NUMERIC) balance_change, (t.blk_id IS NOT NULL) confirmed " +
|
||||
"FROM get_wallets_recent(@wallet_id, @code, @interval, 5, 0) " +
|
||||
"JOIN txs t USING (code, tx_id) " +
|
||||
"ORDER BY seen_at DESC;",
|
||||
new
|
||||
{
|
||||
wallet_id,
|
||||
code = CryptoCode,
|
||||
interval = TimeSpan.FromDays(31)
|
||||
});
|
||||
var network = derivationSettings.Network;
|
||||
foreach (var r in rows)
|
||||
{
|
||||
var seenAt = new DateTimeOffset(((DateTime)r.seen_at));
|
||||
var balanceChange = new Money((decimal)r.balance_change, MoneyUnit.BTC);
|
||||
transactions.Add(new StoreRecentTransactionViewModel()
|
||||
{
|
||||
Timestamp = seenAt,
|
||||
Id = r.tx_id,
|
||||
Balance = balanceChange.ShowMoney(network),
|
||||
IsConfirmed = r.confirmed,
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, r.tx_id),
|
||||
Positive = balanceChange.GetValue(network) >= 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var network = derivationSettings.Network;
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var allTransactions = await wallet.FetchTransactions(derivationSettings.AccountDerivation);
|
||||
transactions = allTransactions.UnconfirmedTransactions.Transactions
|
||||
.Concat(allTransactions.ConfirmedTransactions.Transactions).ToArray()
|
||||
.OrderByDescending(t => t.Timestamp)
|
||||
.Take(5)
|
||||
.Select(tx => new StoreRecentTransactionViewModel
|
||||
{
|
||||
Id = tx.TransactionId.ToString(),
|
||||
Positive = tx.BalanceChange.GetValue(network) >= 0,
|
||||
Balance = tx.BalanceChange.ShowMoney(network),
|
||||
IsConfirmed = tx.Confirmations != 0,
|
||||
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, tx.TransactionId.ToString()),
|
||||
Timestamp = tx.Timestamp
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var vm = new StoreRecentTransactionsViewModel
|
||||
{
|
||||
Store = store,
|
||||
WalletId = walletId,
|
||||
Transactions = transactions
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentTransactions;
|
||||
|
||||
public class StoreRecentTransactionsViewModel
|
||||
{
|
||||
public StoreData Store { get; set; }
|
||||
public IList<StoreRecentTransactionViewModel> Transactions { get; set; } = new List<StoreRecentTransactionViewModel>();
|
||||
public WalletId WalletId { get; set; }
|
||||
}
|
@ -47,11 +47,7 @@ else
|
||||
@foreach (var option in Model.Options)
|
||||
{
|
||||
<li>
|
||||
@if (option.IsOwner && option.WalletId != null)
|
||||
{
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@option.WalletId" class="dropdown-item@(option.Selected ? " active" : "")" id="StoreSelectorMenuItem-@option.Value">@StoreName(option.Text)</a>
|
||||
}
|
||||
else if (option.IsOwner)
|
||||
@if (option.IsOwner)
|
||||
{
|
||||
<a asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@option.Value" class="dropdown-item@(option.Selected ? " active" : "")" id="StoreSelectorMenuItem-@option.Value">@StoreName(option.Text)</a>
|
||||
}
|
||||
|
57
BTCPayServer/Components/StoreWalletBalance/Default.cshtml
Normal file
57
BTCPayServer/Components/StoreWalletBalance/Default.cshtml
Normal file
@ -0,0 +1,57 @@
|
||||
@using BTCPayServer.Services.Wallets
|
||||
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
|
||||
|
||||
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
|
||||
<h6 class="mb-2">Wallet Balance</h6>
|
||||
<header class="mb-3">
|
||||
<div class="balance">
|
||||
<h3 class="d-inline-block me-1">@Model.Balance</h3>
|
||||
<span class="text-secondary fw-semibold">@Model.CryptoCode</span>
|
||||
</div>
|
||||
<div class="btn-group mt-1" role="group" aria-label="Filter">
|
||||
<input type="radio" class="btn-check" name="filter" id="filter-week" value="week" @(Model.Type == WalletHistogramType.Week ? "checked" : "")>
|
||||
<label class="btn btn-link" for="filter-week">1W</label>
|
||||
<input type="radio" class="btn-check" name="filter" id="filter-month" value="month" @(Model.Type == WalletHistogramType.Month ? "checked" : "")>
|
||||
<label class="btn btn-link" for="filter-month">1M</label>
|
||||
<input type="radio" class="btn-check" name="filter" id="filter-year" value="year" @(Model.Type == WalletHistogramType.Year ? "checked" : "")>
|
||||
<label class="btn btn-link" for="filter-year">1Y</label>
|
||||
</div>
|
||||
</header>
|
||||
<div class="ct-chart ct-major-eleventh"></div>
|
||||
<script>
|
||||
(function () {
|
||||
const id = 'StoreWalletBalance-@Model.Store.Id';
|
||||
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
|
||||
const render = data => {
|
||||
const { series, labels, balance } = data;
|
||||
document.querySelector(`#${id} h3`).innerText = balance;
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = Math.max(min - ((max - min) / 5), 0);
|
||||
new Chartist.Line(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
}, {
|
||||
low,
|
||||
fullWidth: true,
|
||||
showArea: true
|
||||
});
|
||||
};
|
||||
const update = async type => {
|
||||
const url = baseUrl.replace(/\/week$/gi, `/${type}`);
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
render(json);
|
||||
}
|
||||
};
|
||||
render({ series: @Safe.Json(Model.Series), labels: @Safe.Json(Model.Labels), balance: @Safe.Json(Model.Balance) });
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
delegate('change', `#${id} [name="filter"]`, async e => {
|
||||
const type = e.target.value;
|
||||
await update(type);
|
||||
})
|
||||
})
|
||||
})();
|
||||
</script>
|
||||
</div>
|
@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Client;
|
||||
|
||||
namespace BTCPayServer.Components.StoreWalletBalance;
|
||||
|
||||
public class StoreWalletBalance : ViewComponent
|
||||
{
|
||||
private const string CryptoCode = "BTC";
|
||||
private const WalletHistogramType DefaultType = WalletHistogramType.Week;
|
||||
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly WalletHistogramService _walletHistogramService;
|
||||
|
||||
public StoreWalletBalance(StoreRepository storeRepo, WalletHistogramService walletHistogramService)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
_walletHistogramService = walletHistogramService;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||
{
|
||||
var walletId = new WalletId(store.Id, CryptoCode);
|
||||
var data = await _walletHistogramService.GetHistogram(store, walletId, DefaultType);
|
||||
|
||||
var vm = new StoreWalletBalanceViewModel
|
||||
{
|
||||
Store = store,
|
||||
CryptoCode = CryptoCode,
|
||||
WalletId = walletId,
|
||||
Series = data?.Series,
|
||||
Labels = data?.Labels,
|
||||
Balance = data?.Balance ?? 0,
|
||||
Type = DefaultType
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
|
||||
namespace BTCPayServer.Components.StoreWalletBalance;
|
||||
|
||||
public class StoreWalletBalanceViewModel
|
||||
{
|
||||
public decimal Balance { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public StoreData Store { get; set; }
|
||||
public WalletId WalletId { get; set; }
|
||||
public WalletHistogramType Type { get; set; }
|
||||
public IList<string> Labels { get; set; } = new List<string>();
|
||||
public IList<decimal> Series { get; set; } = new List<decimal>();
|
||||
}
|
@ -50,6 +50,8 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--recommended-plugins", "Plugins which would be marked as recommended to be installed. Separated by newline or space", CommandOptionType.MultipleValue);
|
||||
app.Option("--xforwardedproto", "If specified, set X-Forwarded-Proto to the specified value, this may be useful if your reverse proxy handle https but is not configured to add X-Forwarded-Proto (example: --xforwardedproto https)", CommandOptionType.SingleValue);
|
||||
app.Option("--cheatmode", "Add elements in the UI to facilitate dev-time testing (Default false)", CommandOptionType.BoolValue);
|
||||
|
||||
app.Option("--explorerpostgres", $"Connection string to the postgres database of NBXplorer. (optional, used for dashboard and reporting features)", CommandOptionType.SingleValue);
|
||||
foreach (var network in provider.GetAll().OfType<BTCPayNetwork>())
|
||||
{
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
|
@ -9,5 +9,6 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
set;
|
||||
} = new List<NBXplorerConnectionSetting>();
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -134,31 +134,44 @@ namespace BTCPayServer.Controllers
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public StoreData CurrentStore
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.HttpContext.GetStoreData();
|
||||
}
|
||||
}
|
||||
|
||||
public StoreData CurrentStore => HttpContext.GetStoreData();
|
||||
|
||||
[HttpGet("{storeId}")]
|
||||
public IActionResult Dashboard()
|
||||
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 = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled),
|
||||
LightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled),
|
||||
WalletEnabled = walletEnabled,
|
||||
LightningEnabled = lightningEnabled,
|
||||
StoreId = CurrentStore.Id,
|
||||
StoreName = CurrentStore.StoreName
|
||||
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
|
||||
.Where(a => a.AppType == AppType.Crowdfund.ToString())
|
||||
.Select(a =>
|
||||
{
|
||||
var appData = _appService.GetAppDataIfOwner(userId, a.Id, AppType.Crowdfund).Result;
|
||||
appData.StoreData = store;
|
||||
return appData;
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return View("Dashboard", vm);
|
||||
}
|
||||
|
||||
|
@ -22,11 +22,14 @@ using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Client;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
@ -43,7 +46,7 @@ namespace BTCPayServer.Controllers
|
||||
private WalletRepository WalletRepository { get; }
|
||||
private BTCPayNetworkProvider NetworkProvider { get; }
|
||||
private ExplorerClientProvider ExplorerClientProvider { get; }
|
||||
|
||||
public IServiceProvider ServiceProvider { get; }
|
||||
public RateFetcher RateFetcher { get; }
|
||||
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
@ -62,6 +65,8 @@ namespace BTCPayServer.Controllers
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
|
||||
private readonly PullPaymentHostedService _pullPaymentService;
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
private readonly NBXplorerConnectionFactory _connectionFactory;
|
||||
private readonly WalletHistogramService _walletHistogramService;
|
||||
|
||||
readonly CurrencyNameTable _currencyTable;
|
||||
public UIWalletsController(StoreRepository repo,
|
||||
@ -71,6 +76,8 @@ namespace BTCPayServer.Controllers
|
||||
UserManager<ApplicationUser> userManager,
|
||||
MvcNewtonsoftJsonOptions mvcJsonOptions,
|
||||
NBXplorerDashboard dashboard,
|
||||
WalletHistogramService walletHistogramService,
|
||||
NBXplorerConnectionFactory connectionFactory,
|
||||
RateFetcher rateProvider,
|
||||
IAuthorizationService authorizationService,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
@ -85,7 +92,8 @@ namespace BTCPayServer.Controllers
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
|
||||
PullPaymentHostedService pullPaymentService,
|
||||
IEnumerable<IPayoutHandler> payoutHandlers)
|
||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_currencyTable = currencyTable;
|
||||
Repository = repo;
|
||||
@ -109,6 +117,9 @@ namespace BTCPayServer.Controllers
|
||||
_jsonSerializerSettings = jsonSerializerSettings;
|
||||
_pullPaymentService = pullPaymentService;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
ServiceProvider = serviceProvider;
|
||||
_connectionFactory = connectionFactory;
|
||||
_walletHistogramService = walletHistogramService;
|
||||
}
|
||||
|
||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||
@ -351,6 +362,19 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("{walletId}/histogram/{type}")]
|
||||
public async Task<IActionResult> WalletHistogram(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletHistogramType type)
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
var data = await _walletHistogramService.GetHistogram(store, walletId, type);
|
||||
|
||||
return data == null
|
||||
? NotFound()
|
||||
: Json(data);
|
||||
}
|
||||
|
||||
private static string GetLabelTarget(WalletId walletId, uint256 txId)
|
||||
{
|
||||
@ -416,10 +440,48 @@ namespace BTCPayServer.Controllers
|
||||
case "generate-new-address":
|
||||
await _walletReceiveService.GetOrGenerate(walletId, true);
|
||||
break;
|
||||
case "fill-wallet":
|
||||
var cheater = ServiceProvider.GetService<Cheater>();
|
||||
if (cheater != null)
|
||||
await SendFreeMoney(cheater, walletId, paymentMethod);
|
||||
break;
|
||||
}
|
||||
return RedirectToAction(nameof(WalletReceive), new { walletId });
|
||||
}
|
||||
|
||||
private async Task SendFreeMoney(Cheater cheater, WalletId walletId, DerivationSchemeSettings paymentMethod)
|
||||
{
|
||||
var c = this.ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
|
||||
var addresses = Enumerable.Range(0, 200).Select(_ => c.GetUnusedAsync(paymentMethod.AccountDerivation, DerivationFeature.Deposit, reserve: true)).ToArray();
|
||||
await Task.WhenAll(addresses);
|
||||
await cheater.CashCow.GenerateAsync(addresses.Length / 8);
|
||||
var b = cheater.CashCow.PrepareBatch();
|
||||
Random r = new Random();
|
||||
List<Task<uint256>> sending = new List<Task<uint256>>();
|
||||
foreach (var a in addresses)
|
||||
{
|
||||
sending.Add(b.SendToAddressAsync((await a).Address, Money.Coins(0.1m) + Money.Satoshis(r.Next(0, 90_000_000))));
|
||||
}
|
||||
await b.SendBatchAsync();
|
||||
await cheater.CashCow.GenerateAsync(1);
|
||||
|
||||
var factory = ServiceProvider.GetService<NBXplorerConnectionFactory>();
|
||||
|
||||
// Wait it sync...
|
||||
await Task.Delay(1000);
|
||||
await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode).WaitServerStartedAsync();
|
||||
await Task.Delay(1000);
|
||||
await using var conn = await factory.OpenConnection();
|
||||
var wallet_id = paymentMethod.GetNBXWalletId();
|
||||
|
||||
var txIds = sending.Select(s => s.Result.ToString()).ToArray();
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE txs t SET seen_at=(NOW() - (random() * (interval '90 days'))) " +
|
||||
"FROM unnest(@txIds) AS r (tx_id) WHERE r.tx_id=t.tx_id;", new { txIds });
|
||||
await Task.Delay(1000);
|
||||
await conn.ExecuteAsync("REFRESH MATERIALIZED VIEW wallets_history;");
|
||||
}
|
||||
|
||||
private async Task<bool> CanUseHotWallet()
|
||||
{
|
||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||
|
@ -5,6 +5,7 @@ using System.Text;
|
||||
using BTCPayServer.Payments;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer.Client;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -42,6 +43,10 @@ namespace BTCPayServer
|
||||
return strategy != null;
|
||||
}
|
||||
|
||||
public string GetNBXWalletId()
|
||||
{
|
||||
return AccountDerivation is null ? null : DBUtils.nbxv1_get_wallet_id(Network.CryptoCode, AccountDerivation.ToString());
|
||||
}
|
||||
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = true)
|
||||
{
|
||||
if (!electrum)
|
||||
|
@ -117,6 +117,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<PaymentRequestService>();
|
||||
services.TryAddSingleton<UserService>();
|
||||
services.TryAddSingleton<WalletHistogramService>();
|
||||
services.AddSingleton<ApplicationDbContextFactory>();
|
||||
services.AddOptions<BTCPayServerOptions>().Configure(
|
||||
(options) =>
|
||||
@ -176,6 +177,7 @@ namespace BTCPayServer.Hosting
|
||||
btcPayNetwork.NBXplorerNetwork.DefaultSettings.DefaultCookieFile)
|
||||
};
|
||||
options.NBXplorerConnectionSettings.Add(setting);
|
||||
options.ConnectionString = configuration.GetOrDefault<string>("explorer.postgres", null);
|
||||
}
|
||||
});
|
||||
services.AddOptions<LightningNetworkOptions>().Configure<BTCPayNetworkProvider>(
|
||||
@ -310,6 +312,8 @@ namespace BTCPayServer.Hosting
|
||||
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(DerivationStrategyBase)));
|
||||
});
|
||||
|
||||
services.AddSingleton<Services.NBXplorerConnectionFactory>();
|
||||
services.AddSingleton<IHostedService, Services.NBXplorerConnectionFactory>(o => o.GetRequiredService<Services.NBXplorerConnectionFactory>());
|
||||
services.AddSingleton<HostedServices.CheckConfigurationHostedService>();
|
||||
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>(o => o.GetRequiredService<CheckConfigurationHostedService>());
|
||||
services.AddSingleton<HostedServices.WebhookSender>();
|
||||
|
@ -1,3 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels;
|
||||
|
||||
public class StoreDashboardViewModel
|
||||
@ -6,4 +9,6 @@ public class StoreDashboardViewModel
|
||||
public string StoreName { get; set; }
|
||||
public bool WalletEnabled { get; set; }
|
||||
public bool LightningEnabled { get; set; }
|
||||
public bool IsSetUp { get; set; }
|
||||
public List<AppData> Apps { get; set; }
|
||||
}
|
||||
|
@ -29,7 +29,8 @@
|
||||
"BTCPAY_UPDATEURL": "",
|
||||
"BTCPAY_DOCKERDEPLOYMENT": "true",
|
||||
"BTCPAY_RECOMMENDED-PLUGINS": "BTCPayServer.Plugins.Test",
|
||||
"BTCPAY_CHEATMODE": "true"
|
||||
"BTCPAY_CHEATMODE": "true",
|
||||
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
|
||||
},
|
||||
"applicationUrl": "http://127.0.0.1:14142/"
|
||||
},
|
||||
@ -66,7 +67,8 @@
|
||||
"BTCPAY_SOCKSENDPOINT": "localhost:9050",
|
||||
"BTCPAY_DOCKERDEPLOYMENT": "true",
|
||||
"BTCPAY_RECOMMENDED-PLUGINS": "BTCPayServer.Plugins.Test",
|
||||
"BTCPAY_CHEATMODE": "true"
|
||||
"BTCPAY_CHEATMODE": "true",
|
||||
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
|
||||
},
|
||||
"applicationUrl": "https://localhost:14142/"
|
||||
},
|
||||
@ -105,7 +107,8 @@
|
||||
"BTCPAY_SOCKSENDPOINT": "localhost:9050",
|
||||
"BTCPAY_DOCKERDEPLOYMENT": "true",
|
||||
"BTCPAY_RECOMMENDED-PLUGINS": "BTCPayServer.Plugins.Test",
|
||||
"BTCPAY_CHEATMODE": "true"
|
||||
"BTCPAY_CHEATMODE": "true",
|
||||
"BTCPAY_EXPLORERPOSTGRES": "User ID=postgres;Include Error Detail=true;Host=127.0.0.1;Port=39372;Database=nbxplorer"
|
||||
},
|
||||
"applicationUrl": "https://localhost:14142/"
|
||||
}
|
||||
|
@ -90,9 +90,9 @@ namespace BTCPayServer.Services.Apps
|
||||
}
|
||||
|
||||
var invoices = await GetInvoicesForApp(appData, lastResetDate);
|
||||
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed).ToArray();
|
||||
var pendingInvoices = invoices.Where(entity => !(entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed)).ToArray();
|
||||
var paidInvoices = invoices.Where(entity => entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed || entity.Status == InvoiceStatusLegacy.Paid).ToArray();
|
||||
var completeInvoices = invoices.Where(IsComplete).ToArray();
|
||||
var pendingInvoices = invoices.Where(IsPending).ToArray();
|
||||
var paidInvoices = invoices.Where(IsPaid).ToArray();
|
||||
|
||||
var pendingPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, pendingInvoices, !settings.EnforceTargetAmount);
|
||||
var currentPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, completeInvoices, !settings.EnforceTargetAmount);
|
||||
@ -102,11 +102,12 @@ namespace BTCPayServer.Services.Apps
|
||||
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||
.ToDictionary(entities => entities.Key, entities => entities.Count());
|
||||
|
||||
Dictionary<string, decimal> perkValue = new Dictionary<string, decimal>();
|
||||
Dictionary<string, decimal> perkValue = new();
|
||||
if (settings.DisplayPerksValue)
|
||||
{
|
||||
perkValue = paidInvoices
|
||||
.Where(entity => entity.Currency.Equals(settings.TargetCurrency, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(entity.Metadata.ItemCode))
|
||||
.Where(entity => entity.Currency.Equals(settings.TargetCurrency, StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.IsNullOrEmpty(entity.Metadata.ItemCode))
|
||||
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||
.ToDictionary(entities => entities.Key, entities =>
|
||||
entities.Sum(entity => entity.GetPayments(true).Sum(pay =>
|
||||
@ -117,6 +118,7 @@ namespace BTCPayServer.Services.Apps
|
||||
return rate * value;
|
||||
})));
|
||||
}
|
||||
|
||||
var perks = Parse(settings.PerksTemplate, settings.TargetCurrency);
|
||||
if (settings.SortPerksByPopularity)
|
||||
{
|
||||
@ -160,10 +162,9 @@ namespace BTCPayServer.Services.Apps
|
||||
Sounds = settings.Sounds,
|
||||
AnimationColors = settings.AnimationColors,
|
||||
CurrencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true),
|
||||
CurrencyDataPayments = currentPayments.Select(pair => pair.Key)
|
||||
.Concat(pendingPayments.Select(pair => pair.Key))
|
||||
.Select(id => _Currencies.GetCurrencyData(id.CryptoCode, true))
|
||||
.DistinctBy(data => data.Code)
|
||||
CurrencyDataPayments = Enumerable.DistinctBy(currentPayments.Select(pair => pair.Key)
|
||||
.Concat(pendingPayments.Select(pair => pair.Key))
|
||||
.Select(id => _Currencies.GetCurrencyData(id.CryptoCode, true)), data => data.Code)
|
||||
.ToDictionary(data => data.Code, data => data),
|
||||
Info = new CrowdfundInfo
|
||||
{
|
||||
@ -181,12 +182,101 @@ namespace BTCPayServer.Services.Apps
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsPending(InvoiceEntity entity)
|
||||
{
|
||||
return !(entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed);
|
||||
}
|
||||
|
||||
private static bool IsComplete(InvoiceEntity entity)
|
||||
{
|
||||
return entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ItemStats>> GetPerkStats(AppData appData)
|
||||
{
|
||||
var settings = appData.GetSettings<CrowdfundSettings>();
|
||||
var invoices = await GetInvoicesForApp(appData);
|
||||
var paidInvoices = invoices.Where(IsPaid).ToArray();
|
||||
var currencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true);
|
||||
var perks = Parse(settings.PerksTemplate, settings.TargetCurrency);
|
||||
var perkCount = paidInvoices
|
||||
.Where(entity => !string.IsNullOrEmpty(entity.Metadata.ItemCode) &&
|
||||
entity.Currency.Equals(settings.TargetCurrency, StringComparison.OrdinalIgnoreCase))
|
||||
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||
.Select(entities =>
|
||||
{
|
||||
var total = entities
|
||||
.Sum(entity => entity.GetPayments(true)
|
||||
.Sum(pay => {
|
||||
var paymentMethodId = pay.GetPaymentMethodId();
|
||||
var value = pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee;
|
||||
var rate = entity.GetPaymentMethod(paymentMethodId).Rate;
|
||||
return rate * value;
|
||||
}));
|
||||
var itemCode = entities.Key;
|
||||
var perk = perks.First(p => p.Id == itemCode);
|
||||
return new ItemStats
|
||||
{
|
||||
ItemCode = itemCode,
|
||||
Title = perk.Title,
|
||||
SalesCount = entities.Count(),
|
||||
Total = total,
|
||||
TotalFormatted = $"{total.ShowMoney(currencyData.Divisibility)} {settings.TargetCurrency}"
|
||||
};
|
||||
})
|
||||
.OrderByDescending(stats => stats.SalesCount);
|
||||
|
||||
return perkCount;
|
||||
}
|
||||
|
||||
public async Task<SalesStats> GetSalesStats(AppData appData, int numberOfDays = 7)
|
||||
{
|
||||
var invoices = await GetInvoicesForApp(appData);
|
||||
var paidInvoices = invoices.Where(IsPaid).ToArray();
|
||||
var series = paidInvoices
|
||||
.Where(entity => !string.IsNullOrEmpty(entity.Metadata.ItemCode) &&
|
||||
entity.InvoiceTime > DateTimeOffset.UtcNow - TimeSpan.FromDays(numberOfDays))
|
||||
.GroupBy(entity => entity.InvoiceTime.Date)
|
||||
.Select(entities => new SalesStatsItem
|
||||
{
|
||||
Date = entities.Key,
|
||||
Label = entities.Key.ToString("MMM dd", CultureInfo.InvariantCulture),
|
||||
SalesCount = entities.Count()
|
||||
});
|
||||
|
||||
// fill up the gaps
|
||||
foreach (var i in Enumerable.Range(0, numberOfDays))
|
||||
{
|
||||
var date = (DateTimeOffset.UtcNow - TimeSpan.FromDays(i)).Date;
|
||||
if (!series.Any(e => e.Date == date))
|
||||
{
|
||||
series = series.Append(new SalesStatsItem
|
||||
{
|
||||
Date = date,
|
||||
Label = date.ToString("MMM dd", CultureInfo.InvariantCulture)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new SalesStats
|
||||
{
|
||||
SalesCount = paidInvoices.Length,
|
||||
Series = series.OrderBy(i => i.Label)
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsPaid(InvoiceEntity entity)
|
||||
{
|
||||
return entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed || entity.Status == InvoiceStatusLegacy.Paid;
|
||||
}
|
||||
|
||||
public static string GetCrowdfundOrderId(string appId) => $"crowdfund-app_{appId}";
|
||||
public static string GetAppInternalTag(string appId) => $"APP#{appId}";
|
||||
public static string[] GetAppInternalTags(InvoiceEntity invoice)
|
||||
{
|
||||
return invoice.GetInternalTags("APP#");
|
||||
}
|
||||
|
||||
private async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, DateTime? startDate = null)
|
||||
{
|
||||
var invoices = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
@ -572,4 +662,26 @@ namespace BTCPayServer.Services.Apps
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class ItemStats
|
||||
{
|
||||
public string ItemCode { get; set; }
|
||||
public string Title { get; set; }
|
||||
public int SalesCount { get; set; }
|
||||
public decimal Total { get; set; }
|
||||
public string TotalFormatted { get; set; }
|
||||
}
|
||||
|
||||
public class SalesStats
|
||||
{
|
||||
public int SalesCount { get; set; }
|
||||
public IEnumerable<SalesStatsItem> Series { get; set; }
|
||||
}
|
||||
|
||||
public class SalesStatsItem
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public string Label { get; set; }
|
||||
public int SalesCount { get; set; }
|
||||
}
|
||||
}
|
||||
|
70
BTCPayServer/Services/NBXplorerConnectionFactory.cs
Normal file
70
BTCPayServer/Services/NBXplorerConnectionFactory.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using BTCPayServer.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Data.Common;
|
||||
using System;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class NBXplorerConnectionFactory : IHostedService
|
||||
{
|
||||
public NBXplorerConnectionFactory(IOptions<NBXplorerOptions> nbXplorerOptions, Logs logs)
|
||||
{
|
||||
connectionString = nbXplorerOptions.Value.ConnectionString;
|
||||
Logs = logs;
|
||||
}
|
||||
string connectionString;
|
||||
|
||||
public bool Available { get; set; }
|
||||
public Logs Logs { get; }
|
||||
|
||||
async Task IHostedService.StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
Available = true;
|
||||
try
|
||||
{
|
||||
await using var conn = await OpenConnection();
|
||||
Logs.Configuration.LogInformation("Connection to NBXplorer's database successful, dashboard and reporting features activated.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ConfigException("Error while trying to connection to explorer.postgres: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DbConnection> OpenConnection()
|
||||
{
|
||||
int maxRetries = 10;
|
||||
int retries = maxRetries;
|
||||
retry:
|
||||
var conn = new Npgsql.NpgsqlConnection(connectionString);
|
||||
try
|
||||
{
|
||||
await conn.OpenAsync();
|
||||
}
|
||||
catch (PostgresException ex) when (ex.IsTransient && retries > 0)
|
||||
{
|
||||
retries--;
|
||||
await conn.DisposeAsync();
|
||||
await Task.Delay((maxRetries - retries) * 100);
|
||||
goto retry;
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
|
||||
Task IHostedService.StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Npgsql.NpgsqlConnection.ClearAllPools();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
92
BTCPayServer/Services/Wallets/WalletHistogramService.cs
Normal file
92
BTCPayServer/Services/Wallets/WalletHistogramService.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Data;
|
||||
using Dapper;
|
||||
|
||||
namespace BTCPayServer.Services.Wallets;
|
||||
|
||||
public enum WalletHistogramType
|
||||
{
|
||||
Week,
|
||||
Month,
|
||||
Year
|
||||
}
|
||||
|
||||
public class WalletHistogramService
|
||||
{
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly NBXplorerConnectionFactory _connectionFactory;
|
||||
|
||||
public WalletHistogramService(
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
NBXplorerConnectionFactory connectionFactory)
|
||||
{
|
||||
_networkProvider = networkProvider;
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<WalletHistogramData> GetHistogram(StoreData store, WalletId walletId, WalletHistogramType type)
|
||||
{
|
||||
// https://github.com/dgarage/NBXplorer/blob/master/docs/Postgres-Schema.md
|
||||
if (_connectionFactory.Available)
|
||||
{
|
||||
var derivationSettings = store.GetDerivationSchemeSettings(_networkProvider, walletId.CryptoCode);
|
||||
if (derivationSettings != null)
|
||||
{
|
||||
var wallet_id = derivationSettings.GetNBXWalletId();
|
||||
await using var conn = await _connectionFactory.OpenConnection();
|
||||
|
||||
var code = walletId.CryptoCode;
|
||||
var to = DateTimeOffset.UtcNow;
|
||||
var labelCount = 6;
|
||||
(var days, var pointCount) = type switch
|
||||
{
|
||||
WalletHistogramType.Week => (7, 30),
|
||||
WalletHistogramType.Month => (30, 30),
|
||||
WalletHistogramType.Year => (365, 30),
|
||||
_ => throw new ArgumentException($"WalletHistogramType {type} does not exist.")
|
||||
};
|
||||
var from = to - TimeSpan.FromDays(days);
|
||||
var interval = TimeSpan.FromTicks((to - from).Ticks / pointCount);
|
||||
var balance = await conn.ExecuteScalarAsync<decimal>(
|
||||
"SELECT to_btc(available_balance) FROM wallets_balances WHERE wallet_id=@wallet_id AND code=@code AND asset_id=''",
|
||||
new { code, wallet_id });
|
||||
var rows = await conn.QueryAsync("SELECT date, to_btc(balance) balance FROM get_wallets_histogram(@wallet_id, @code, '', @from, @to, @interval)",
|
||||
new { code, wallet_id, from, to, interval });
|
||||
var data = rows.AsList();
|
||||
var series = new List<decimal>(pointCount);
|
||||
var labels = new List<string>(labelCount);
|
||||
var labelEvery = pointCount / labelCount;
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
var r = data[i];
|
||||
series.Add((decimal)r.balance);
|
||||
labels.Add((i % labelEvery == 0)
|
||||
? ((DateTime)r.date).ToString("MMM dd", CultureInfo.InvariantCulture)
|
||||
: null);
|
||||
}
|
||||
series[^1] = balance;
|
||||
return new WalletHistogramData
|
||||
{
|
||||
Series = series,
|
||||
Labels = labels,
|
||||
Balance = balance,
|
||||
Type = type
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
@ -39,31 +39,6 @@
|
||||
.dropdown-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.badge-new {
|
||||
background: #d4edda;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge-expired {
|
||||
background: #eee;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge-invalid {
|
||||
background: #c94a47;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-processing {
|
||||
background: #f1c332;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge-settled {
|
||||
background: #329f80;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* pull mass action form up, so that it is besides the search form */
|
||||
@@media (min-width: 1200px) {
|
||||
|
@ -2,67 +2,150 @@
|
||||
|
||||
@inject BTCPayNetworkProvider networkProvider
|
||||
@{
|
||||
ViewData.SetActivePage(StoreNavPages.Dashboard, Model.StoreName, Model.StoreId);
|
||||
var isReady = Model.WalletEnabled || Model.LightningEnabled;
|
||||
var defaultCryptoCode = networkProvider.DefaultNetwork.CryptoCode;
|
||||
ViewData.SetActivePage(StoreNavPages.Dashboard, Model.StoreName, Model.StoreId);
|
||||
var defaultCryptoCode = networkProvider.DefaultNetwork.CryptoCode;
|
||||
var store = ViewContext.HttpContext.GetStoreData();
|
||||
}
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
<h2 class="mt-1 mb-3">@ViewData["Title"]</h2>
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<h2 class="mb-0">@ViewData["Title"]</h2>
|
||||
<button type="button" class="btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#WhatsNew">What's New</button>
|
||||
</div>
|
||||
|
||||
@if (isReady)
|
||||
<div class="modal fade" id="WhatsNew" tabindex="-1" aria-labelledby="WhatsNewTitle" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="WhatsNewTitle">What's New</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<vc:icon symbol="close"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5 class="alert-heading">Updated in v1.5.0</h5>
|
||||
<p class="mb-0">Stores now have a neat dashboard like the one you see here! 🗠🎉</p>
|
||||
<hr style="height:1px;background-color:var(--btcpay-body-text-muted);margin:var(--btcpay-space-m) 0;" />
|
||||
<h5 class="alert-heading">Updated in v1.4.0</h5>
|
||||
<p class="mb-2">Invoice states have been updated to match the Greenfield API:</p>
|
||||
<ul class="list-unstyled mb-md-0">
|
||||
<li>
|
||||
<span class="badge badge-processing">Paid</span>
|
||||
<span class="mx-1">is now shown as</span>
|
||||
<span class="badge badge-processing">Processing</span>
|
||||
</li>
|
||||
<li class="mt-2">
|
||||
<span class="badge badge-settled">Completed</span>
|
||||
<span class="mx-1">is now shown as</span>
|
||||
<span class="badge badge-settled">Settled</span>
|
||||
</li>
|
||||
<li class="mt-2">
|
||||
<span class="badge badge-settled">Confirmed</span>
|
||||
<span class="mx-1">is now shown as</span>
|
||||
<span class="badge badge-settled">Settled</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.IsSetUp)
|
||||
{
|
||||
<p class="lead text-secondary">This store is ready to accept transactions, good job!</p>
|
||||
/* 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>
|
||||
<div id="Dashboard" class="mt-4">
|
||||
@if (Model.WalletEnabled)
|
||||
{
|
||||
<vc:store-wallet-balance store="@store"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="widget setup-guide">
|
||||
<header>
|
||||
<h5 class="mb-4 text-muted">This store is ready to accept transactions, good job!</h5>
|
||||
</header>
|
||||
<div class="list-group" id="SetupGuide">
|
||||
<div class="list-group-item d-flex align-items-center" id="SetupGuide-LightningDone">
|
||||
<vc:icon symbol="done"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0 text-success">Set up a Lightning node</h5>
|
||||
</div>
|
||||
</div>
|
||||
<a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@defaultCryptoCode" id="SetupGuide-Wallet" class="list-group-item list-group-item-action d-flex align-items-center">
|
||||
<vc:icon symbol="new-wallet"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0">Set up a wallet</h5>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<vc:store-numbers store="@store"/>
|
||||
@if (Model.WalletEnabled)
|
||||
{
|
||||
<vc:store-recent-transactions store="@store"/>
|
||||
}
|
||||
<vc:store-recent-invoices store="@store"/>
|
||||
@foreach (var app in Model.Apps)
|
||||
{
|
||||
<vc:app-sales app="@app"/>
|
||||
<vc:app-top-items app="@app"/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="lead text-secondary">To start accepting payments, set up a wallet or a Lightning node.</p>
|
||||
|
||||
<div class="list-group" id="SetupGuide">
|
||||
<div class="list-group-item d-flex align-items-center" id="SetupGuide-StoreDone">
|
||||
<vc:icon symbol="done"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0 text-success">Create your store</h5>
|
||||
</div>
|
||||
</div>
|
||||
@if (!Model.WalletEnabled)
|
||||
{
|
||||
<a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@defaultCryptoCode" id="SetupGuide-Wallet" class="list-group-item list-group-item-action d-flex align-items-center order-1">
|
||||
<vc:icon symbol="new-wallet"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0">Set up a wallet</h5>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right"/>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="list-group-item d-flex align-items-center" id="SetupGuide-WalletDone">
|
||||
<vc:icon symbol="done"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0 text-success">Set up a wallet</h5>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!Model.LightningEnabled)
|
||||
{
|
||||
<a asp-controller="UIStores" asp-action="SetupLightningNode" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@defaultCryptoCode" id="SetupGuide-Lightning" class="list-group-item list-group-item-action d-flex align-items-center order-1">
|
||||
<vc:icon symbol="new-wallet"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0">Set up a Lightning node</h5>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right"/>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="list-group-item d-flex align-items-center" id="SetupGuide-LightningDone">
|
||||
<vc:icon symbol="done"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0 text-success">Set up a Lightning node</h5>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="list-group" id="SetupGuide">
|
||||
<div class="list-group-item d-flex align-items-center" id="SetupGuide-StoreDone">
|
||||
<vc:icon symbol="done"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0 text-success">Create your store</h5>
|
||||
</div>
|
||||
</div>
|
||||
@if (!Model.WalletEnabled)
|
||||
{
|
||||
<a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@defaultCryptoCode" id="SetupGuide-Wallet" class="list-group-item list-group-item-action d-flex align-items-center order-1">
|
||||
<vc:icon symbol="new-wallet"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0">Set up a wallet</h5>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right"/>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="list-group-item d-flex align-items-center" id="SetupGuide-WalletDone">
|
||||
<vc:icon symbol="done"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0 text-success">Set up a wallet</h5>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!Model.LightningEnabled)
|
||||
{
|
||||
<a asp-controller="UIStores" asp-action="SetupLightningNode" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@defaultCryptoCode" id="SetupGuide-Lightning" class="list-group-item list-group-item-action d-flex align-items-center order-1">
|
||||
<vc:icon symbol="new-wallet"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0">Set up a Lightning node</h5>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right"/>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="list-group-item d-flex align-items-center" id="SetupGuide-LightningDone">
|
||||
<vc:icon symbol="done"/>
|
||||
<div class="content">
|
||||
<h5 class="mb-0 text-success">Set up a Lightning node</h5>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@inject BTCPayServer.Services.BTCPayServerEnvironment env
|
||||
@model BTCPayServer.Controllers.WalletReceiveViewModel
|
||||
@{
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
@ -17,6 +18,10 @@
|
||||
@if (string.IsNullOrEmpty(Model.Address))
|
||||
{
|
||||
<button id="generateButton" class="btn btn-primary" type="submit" name="command" value="generate-new-address">Generate next available @Model.CryptoCode address</button>
|
||||
@if (env.CheatMode)
|
||||
{
|
||||
<button type="submit" name="command" value="fill-wallet" class="btn btn-info ms-3">Cheat Mode: Send transactions to this wallet</button>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -84,7 +89,7 @@
|
||||
<div class="col-12 col-sm-6 mt-4 mt-sm-0">
|
||||
<button type="submit" name="command" value="unreserve-current-address" class="btn btn-secondary w-100">Unreserve this address</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
|
@ -70,6 +70,32 @@ a.unobtrusive-link {
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge-new {
|
||||
background: #d4edda;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge-expired {
|
||||
background: #eee;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge-invalid {
|
||||
background: #c94a47;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-processing {
|
||||
background: #f1c332;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge-settled {
|
||||
background: #329f80;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Info icons in main headline */
|
||||
h2 small .fa-question-circle-o {
|
||||
position: relative;
|
||||
@ -227,3 +253,172 @@ svg.icon-note {
|
||||
flex: 1;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
/* Dashboard */
|
||||
#Dashboard {
|
||||
display: grid;
|
||||
gap: var(--btcpay-space-m);
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
}
|
||||
|
||||
.widget {
|
||||
--widget-padding: var(--btcpay-space-m);
|
||||
--widget-chart-width: 100vw;
|
||||
|
||||
border: 1px solid var(--btcpay-body-border-light);
|
||||
border-radius: var(--btcpay-border-radius);
|
||||
padding: var(--widget-padding);
|
||||
background: var(--btcpay-bg-tile);
|
||||
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 13;
|
||||
}
|
||||
|
||||
.widget header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--btcpay-space-s);
|
||||
gap: var(--btcpay-space-s);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.widget header a,
|
||||
.widget header .btn-link {
|
||||
margin-top: var(--btcpay-space-xs);
|
||||
font-weight: var(--btcpay-font-weight-semibold);
|
||||
}
|
||||
|
||||
.widget h3,
|
||||
.widget .h3 {
|
||||
font-weight: var(--btcpay-font-weight-bold);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.widget h6,
|
||||
.widget .h6 {
|
||||
color: var(--btcpay-body-text-muted);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.widget .btn-group {
|
||||
display: inline-flex;
|
||||
gap: var(--btcpay-space-m);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.widget .btn-link {
|
||||
color: var(--btcpay-body-text-muted);
|
||||
padding: 0;
|
||||
font-weight: var(--btcpay-font-weight-semibold);
|
||||
box-shadow: none !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.widget input:checked + .btn-link {
|
||||
color: var(--btcpay-body-link-accent);
|
||||
}
|
||||
|
||||
.widget.store-numbers {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: var(--btcpay-space-l) var(--btcpay-space-xl);
|
||||
}
|
||||
|
||||
.widget.store-numbers header {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.widget.store-numbers header h6 {
|
||||
margin-right: var(--btcpay-space-s);
|
||||
}
|
||||
|
||||
.widget header a,
|
||||
.widget header .btn-link {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.widget .store-number {
|
||||
flex: 0 1 calc(50% - var(--btcpay-space-xl) / 2);
|
||||
}
|
||||
|
||||
.widget .number {
|
||||
font-weight: var(--btcpay-font-weight-bold);
|
||||
}
|
||||
|
||||
.widget .table {
|
||||
margin-left: -.5rem;
|
||||
margin-right: -.5rem;
|
||||
width: calc(100% + 1rem);
|
||||
}
|
||||
|
||||
.widget .table th {
|
||||
color: var(--btcpay-body-text-muted);
|
||||
font-weight: var(--btcpay-font-weight-semibold);
|
||||
}
|
||||
|
||||
.widget.app-top-items .app-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--btcpay-space-s);
|
||||
}
|
||||
|
||||
.widget.app-top-items .app-item {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.widget.app-top-items .app-item-value {
|
||||
font-weight: var(--btcpay-font-weight-semibold);
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.widget .store-number {
|
||||
flex: 0 1 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.widget {
|
||||
--widget-padding: var(--btcpay-space-l);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.widget.app-sales,
|
||||
.widget.setup-guide,
|
||||
.widget.store-wallet-balance {
|
||||
--widget-chart-width: 80vw;
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 9;
|
||||
}
|
||||
|
||||
.widget.app-top-items,
|
||||
.widget.store-numbers {
|
||||
grid-column-start: 9;
|
||||
grid-column-end: 13;
|
||||
}
|
||||
|
||||
.widget.store-numbers {
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.widget .store-number {
|
||||
flex: 0 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.widget.store-numbers header {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.widget.store-numbers header h6 {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
557
BTCPayServer/wwwroot/vendor/chartist/chartist.css
vendored
Normal file
557
BTCPayServer/wwwroot/vendor/chartist/chartist.css
vendored
Normal file
@ -0,0 +1,557 @@
|
||||
.ct-label {
|
||||
fill: rgba(128, 128, 128, .4);
|
||||
color: var(--btcpay-body-text-muted);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1; }
|
||||
|
||||
.ct-label.ct-horizontal {
|
||||
min-width: 3rem; }
|
||||
|
||||
.ct-chart-line .ct-label,
|
||||
.ct-chart-bar .ct-label {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-flex;
|
||||
display: flex; }
|
||||
|
||||
.ct-chart-pie .ct-label,
|
||||
.ct-chart-donut .ct-label {
|
||||
dominant-baseline: central; }
|
||||
|
||||
.ct-label.ct-horizontal.ct-start {
|
||||
-webkit-box-align: flex-end;
|
||||
-webkit-align-items: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
-webkit-box-pack: flex-start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: flex-start;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
text-anchor: start; }
|
||||
|
||||
.ct-label.ct-horizontal.ct-end {
|
||||
-webkit-box-align: flex-start;
|
||||
-webkit-align-items: flex-start;
|
||||
-ms-flex-align: flex-start;
|
||||
align-items: flex-start;
|
||||
-webkit-box-pack: flex-start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: flex-start;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
text-anchor: start; }
|
||||
|
||||
.ct-label.ct-vertical.ct-start {
|
||||
-webkit-box-align: flex-end;
|
||||
-webkit-align-items: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
-webkit-box-pack: flex-end;
|
||||
-webkit-justify-content: flex-end;
|
||||
-ms-flex-pack: flex-end;
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
text-anchor: end; }
|
||||
|
||||
.ct-label.ct-vertical.ct-end {
|
||||
-webkit-box-align: flex-end;
|
||||
-webkit-align-items: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
-webkit-box-pack: flex-start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: flex-start;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
text-anchor: start; }
|
||||
|
||||
.ct-chart-bar .ct-label.ct-horizontal.ct-start {
|
||||
-webkit-box-align: flex-end;
|
||||
-webkit-align-items: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
text-anchor: start; }
|
||||
|
||||
.ct-chart-bar .ct-label.ct-horizontal.ct-end {
|
||||
-webkit-box-align: flex-start;
|
||||
-webkit-align-items: flex-start;
|
||||
-ms-flex-align: flex-start;
|
||||
align-items: flex-start;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
text-anchor: start; }
|
||||
|
||||
.ct-chart-bar.ct-horizontal-bars .ct-label.ct-horizontal.ct-start {
|
||||
-webkit-box-align: flex-end;
|
||||
-webkit-align-items: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
-webkit-box-pack: flex-start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: flex-start;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
text-anchor: start; }
|
||||
|
||||
.ct-chart-bar.ct-horizontal-bars .ct-label.ct-horizontal.ct-end {
|
||||
-webkit-box-align: flex-start;
|
||||
-webkit-align-items: flex-start;
|
||||
-ms-flex-align: flex-start;
|
||||
align-items: flex-start;
|
||||
-webkit-box-pack: flex-start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: flex-start;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
text-anchor: start; }
|
||||
|
||||
.ct-chart-bar.ct-horizontal-bars .ct-label.ct-vertical.ct-start {
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: flex-end;
|
||||
-webkit-justify-content: flex-end;
|
||||
-ms-flex-pack: flex-end;
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
text-anchor: end; }
|
||||
|
||||
.ct-chart-bar.ct-horizontal-bars .ct-label.ct-vertical.ct-end {
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: flex-start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: flex-start;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
text-anchor: end; }
|
||||
|
||||
.ct-grid {
|
||||
stroke: rgba(128, 128, 128, .125);
|
||||
stroke-width: 1px;
|
||||
stroke-dasharray: 2px; }
|
||||
|
||||
.ct-grid-background {
|
||||
fill: none; }
|
||||
|
||||
.ct-point {
|
||||
stroke-width: 7px;
|
||||
stroke-linecap: round; }
|
||||
|
||||
.ct-line {
|
||||
fill: none;
|
||||
stroke-width: 2px; }
|
||||
|
||||
.ct-area {
|
||||
stroke: none;
|
||||
fill-opacity: 0.1; }
|
||||
|
||||
.ct-bar {
|
||||
fill: none;
|
||||
stroke-width: 1.5rem; }
|
||||
|
||||
.ct-slice-donut {
|
||||
fill: none;
|
||||
stroke-width: 60px; }
|
||||
|
||||
.ct-series-a .ct-point, .ct-series-a .ct-line, .ct-series-a .ct-bar, .ct-series-a .ct-slice-donut {
|
||||
stroke: rgba(68,164,49, 1); }
|
||||
|
||||
.ct-series-a .ct-slice-pie, .ct-series-a .ct-slice-donut-solid, .ct-series-a .ct-area {
|
||||
fill: rgba(68,164,49, 0.75); }
|
||||
|
||||
.ct-series-b .ct-point, .ct-series-b .ct-line, .ct-series-b .ct-bar, .ct-series-b .ct-slice-donut {
|
||||
stroke: #f05b4f; }
|
||||
|
||||
.ct-series-b .ct-slice-pie, .ct-series-b .ct-slice-donut-solid, .ct-series-b .ct-area {
|
||||
fill: #f05b4f; }
|
||||
|
||||
.ct-series-c .ct-point, .ct-series-c .ct-line, .ct-series-c .ct-bar, .ct-series-c .ct-slice-donut {
|
||||
stroke: #f4c63d; }
|
||||
|
||||
.ct-series-c .ct-slice-pie, .ct-series-c .ct-slice-donut-solid, .ct-series-c .ct-area {
|
||||
fill: #f4c63d; }
|
||||
|
||||
.ct-series-d .ct-point, .ct-series-d .ct-line, .ct-series-d .ct-bar, .ct-series-d .ct-slice-donut {
|
||||
stroke: #d17905; }
|
||||
|
||||
.ct-series-d .ct-slice-pie, .ct-series-d .ct-slice-donut-solid, .ct-series-d .ct-area {
|
||||
fill: #d17905; }
|
||||
|
||||
.ct-series-e .ct-point, .ct-series-e .ct-line, .ct-series-e .ct-bar, .ct-series-e .ct-slice-donut {
|
||||
stroke: #453d3f; }
|
||||
|
||||
.ct-series-e .ct-slice-pie, .ct-series-e .ct-slice-donut-solid, .ct-series-e .ct-area {
|
||||
fill: #453d3f; }
|
||||
|
||||
.ct-square {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-square:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 100%; }
|
||||
.ct-square:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-square > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-minor-second {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-minor-second:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 93.75%; }
|
||||
.ct-minor-second:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-minor-second > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-major-second {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-major-second:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 88.8888888889%; }
|
||||
.ct-major-second:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-major-second > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-minor-third {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-minor-third:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 83.3333333333%; }
|
||||
.ct-minor-third:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-minor-third > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-major-third {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-major-third:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 80%; }
|
||||
.ct-major-third:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-major-third > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-perfect-fourth {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-perfect-fourth:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 75%; }
|
||||
.ct-perfect-fourth:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-perfect-fourth > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-perfect-fifth {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-perfect-fifth:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 66.6666666667%; }
|
||||
.ct-perfect-fifth:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-perfect-fifth > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-minor-sixth {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-minor-sixth:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 62.5%; }
|
||||
.ct-minor-sixth:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-minor-sixth > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-golden-section {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-golden-section:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 61.804697157%; }
|
||||
.ct-golden-section:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-golden-section > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-major-sixth {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-major-sixth:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 60%; }
|
||||
.ct-major-sixth:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-major-sixth > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-minor-seventh {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-minor-seventh:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 56.25%; }
|
||||
.ct-minor-seventh:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-minor-seventh > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-major-seventh {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-major-seventh:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 53.3333333333%; }
|
||||
.ct-major-seventh:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-major-seventh > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-octave {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-octave:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 50%; }
|
||||
.ct-octave:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-octave > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-major-tenth {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-major-tenth:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 40%; }
|
||||
.ct-major-tenth:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-major-tenth > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-major-eleventh {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-major-eleventh:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 37.5%; }
|
||||
.ct-major-eleventh:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-major-eleventh > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-major-twelfth {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-major-twelfth:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 33.3333333333%; }
|
||||
.ct-major-twelfth:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-major-twelfth > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
||||
.ct-double-octave {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%; }
|
||||
.ct-double-octave:before {
|
||||
display: block;
|
||||
float: left;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-bottom: 25%; }
|
||||
.ct-double-octave:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
.ct-double-octave > svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0; }
|
||||
|
10
BTCPayServer/wwwroot/vendor/chartist/chartist.min.js
vendored
Normal file
10
BTCPayServer/wwwroot/vendor/chartist/chartist.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BTCPayServer/wwwroot/vendor/chartist/chartist.min.js.map
vendored
Normal file
1
BTCPayServer/wwwroot/vendor/chartist/chartist.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user