mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +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:
@@ -453,6 +453,7 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
s.GoToStore();
|
s.GoToStore();
|
||||||
Assert.Contains(storeName, s.Driver.PageSource);
|
Assert.Contains(storeName, s.Driver.PageSource);
|
||||||
|
Assert.DoesNotContain("id=\"Dashboard\"", s.Driver.PageSource);
|
||||||
|
|
||||||
// verify steps for wallet setup are displayed correctly
|
// verify steps for wallet setup are displayed correctly
|
||||||
s.GoToStore(StoreNavPages.Dashboard);
|
s.GoToStore(StoreNavPages.Dashboard);
|
||||||
@@ -466,10 +467,11 @@ namespace BTCPayServer.Tests
|
|||||||
s.Driver.AssertNoError();
|
s.Driver.AssertNoError();
|
||||||
|
|
||||||
s.GoToStore(StoreNavPages.Dashboard);
|
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
|
// setup offchain wallet
|
||||||
s.Driver.FindElement(By.Id("SetupGuide-Lightning")).Click();
|
s.Driver.FindElement(By.Id("StoreNav-LightningBTC")).Click();
|
||||||
s.AddLightningNode();
|
s.AddLightningNode();
|
||||||
s.Driver.AssertNoError();
|
s.Driver.AssertNoError();
|
||||||
var successAlert = s.FindAlertMessage();
|
var successAlert = s.FindAlertMessage();
|
||||||
@@ -477,9 +479,6 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
s.ClickOnAllSectionLinks();
|
s.ClickOnAllSectionLinks();
|
||||||
|
|
||||||
s.GoToStore(StoreNavPages.Dashboard);
|
|
||||||
Assert.True(s.Driver.FindElement(By.Id("SetupGuide-LightningDone")).Displayed);
|
|
||||||
|
|
||||||
s.GoToInvoices();
|
s.GoToInvoices();
|
||||||
Assert.Contains("There are no invoices matching your criteria.", s.Driver.PageSource);
|
Assert.Contains("There are no invoices matching your criteria.", s.Driver.PageSource);
|
||||||
var invoiceId = s.CreateInvoice();
|
var invoiceId = s.CreateInvoice();
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
|
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
|
||||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
|
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
|
||||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||||
|
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||||
<PackageReference Include="Fido2" Version="2.0.1" />
|
<PackageReference Include="Fido2" Version="2.0.1" />
|
||||||
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
|
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
<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)
|
@foreach (var option in Model.Options)
|
||||||
{
|
{
|
||||||
<li>
|
<li>
|
||||||
@if (option.IsOwner && option.WalletId != null)
|
@if (option.IsOwner)
|
||||||
{
|
|
||||||
<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)
|
|
||||||
{
|
{
|
||||||
<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>
|
<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("--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("--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("--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>())
|
foreach (var network in provider.GetAll().OfType<BTCPayNetwork>())
|
||||||
{
|
{
|
||||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ namespace BTCPayServer.Configuration
|
|||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
} = new List<NBXplorerConnectionSetting>();
|
} = new List<NBXplorerConnectionSetting>();
|
||||||
|
public string ConnectionString { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,31 +134,44 @@ namespace BTCPayServer.Controllers
|
|||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public StoreData CurrentStore
|
public StoreData CurrentStore => HttpContext.GetStoreData();
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return this.HttpContext.GetStoreData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{storeId}")]
|
[HttpGet("{storeId}")]
|
||||||
public IActionResult Dashboard()
|
public async Task<IActionResult> Dashboard()
|
||||||
{
|
{
|
||||||
var store = CurrentStore;
|
var store = CurrentStore;
|
||||||
var storeBlob = store.GetStoreBlob();
|
var storeBlob = store.GetStoreBlob();
|
||||||
|
|
||||||
AddPaymentMethods(store, storeBlob,
|
AddPaymentMethods(store, storeBlob,
|
||||||
out var derivationSchemes, out var lightningNodes);
|
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
|
var vm = new StoreDashboardViewModel
|
||||||
{
|
{
|
||||||
WalletEnabled = derivationSchemes.Any(scheme => !string.IsNullOrEmpty(scheme.Value) && scheme.Enabled),
|
WalletEnabled = walletEnabled,
|
||||||
LightningEnabled = lightningNodes.Any(ln => !string.IsNullOrEmpty(ln.Address) && ln.Enabled),
|
LightningEnabled = lightningEnabled,
|
||||||
StoreId = CurrentStore.Id,
|
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);
|
return View("Dashboard", vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,14 @@ using BTCPayServer.Services.Labels;
|
|||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
|
using Dapper;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
|
using NBXplorer.Client;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
using NBXplorer.Models;
|
using NBXplorer.Models;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -43,7 +46,7 @@ namespace BTCPayServer.Controllers
|
|||||||
private WalletRepository WalletRepository { get; }
|
private WalletRepository WalletRepository { get; }
|
||||||
private BTCPayNetworkProvider NetworkProvider { get; }
|
private BTCPayNetworkProvider NetworkProvider { get; }
|
||||||
private ExplorerClientProvider ExplorerClientProvider { get; }
|
private ExplorerClientProvider ExplorerClientProvider { get; }
|
||||||
|
public IServiceProvider ServiceProvider { get; }
|
||||||
public RateFetcher RateFetcher { get; }
|
public RateFetcher RateFetcher { get; }
|
||||||
|
|
||||||
private readonly UserManager<ApplicationUser> _userManager;
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
@@ -62,6 +65,8 @@ namespace BTCPayServer.Controllers
|
|||||||
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
|
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
|
||||||
private readonly PullPaymentHostedService _pullPaymentService;
|
private readonly PullPaymentHostedService _pullPaymentService;
|
||||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||||
|
private readonly NBXplorerConnectionFactory _connectionFactory;
|
||||||
|
private readonly WalletHistogramService _walletHistogramService;
|
||||||
|
|
||||||
readonly CurrencyNameTable _currencyTable;
|
readonly CurrencyNameTable _currencyTable;
|
||||||
public UIWalletsController(StoreRepository repo,
|
public UIWalletsController(StoreRepository repo,
|
||||||
@@ -71,6 +76,8 @@ namespace BTCPayServer.Controllers
|
|||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
MvcNewtonsoftJsonOptions mvcJsonOptions,
|
MvcNewtonsoftJsonOptions mvcJsonOptions,
|
||||||
NBXplorerDashboard dashboard,
|
NBXplorerDashboard dashboard,
|
||||||
|
WalletHistogramService walletHistogramService,
|
||||||
|
NBXplorerConnectionFactory connectionFactory,
|
||||||
RateFetcher rateProvider,
|
RateFetcher rateProvider,
|
||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
ExplorerClientProvider explorerProvider,
|
ExplorerClientProvider explorerProvider,
|
||||||
@@ -85,7 +92,8 @@ namespace BTCPayServer.Controllers
|
|||||||
ApplicationDbContextFactory dbContextFactory,
|
ApplicationDbContextFactory dbContextFactory,
|
||||||
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
|
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
|
||||||
PullPaymentHostedService pullPaymentService,
|
PullPaymentHostedService pullPaymentService,
|
||||||
IEnumerable<IPayoutHandler> payoutHandlers)
|
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||||
|
IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
_currencyTable = currencyTable;
|
_currencyTable = currencyTable;
|
||||||
Repository = repo;
|
Repository = repo;
|
||||||
@@ -109,6 +117,9 @@ namespace BTCPayServer.Controllers
|
|||||||
_jsonSerializerSettings = jsonSerializerSettings;
|
_jsonSerializerSettings = jsonSerializerSettings;
|
||||||
_pullPaymentService = pullPaymentService;
|
_pullPaymentService = pullPaymentService;
|
||||||
_payoutHandlers = payoutHandlers;
|
_payoutHandlers = payoutHandlers;
|
||||||
|
ServiceProvider = serviceProvider;
|
||||||
|
_connectionFactory = connectionFactory;
|
||||||
|
_walletHistogramService = walletHistogramService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
|
||||||
@@ -351,6 +362,19 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
return View(model);
|
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)
|
private static string GetLabelTarget(WalletId walletId, uint256 txId)
|
||||||
{
|
{
|
||||||
@@ -416,10 +440,48 @@ namespace BTCPayServer.Controllers
|
|||||||
case "generate-new-address":
|
case "generate-new-address":
|
||||||
await _walletReceiveService.GetOrGenerate(walletId, true);
|
await _walletReceiveService.GetOrGenerate(walletId, true);
|
||||||
break;
|
break;
|
||||||
|
case "fill-wallet":
|
||||||
|
var cheater = ServiceProvider.GetService<Cheater>();
|
||||||
|
if (cheater != null)
|
||||||
|
await SendFreeMoney(cheater, walletId, paymentMethod);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return RedirectToAction(nameof(WalletReceive), new { walletId });
|
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()
|
private async Task<bool> CanUseHotWallet()
|
||||||
{
|
{
|
||||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Text;
|
|||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
|
using NBXplorer.Client;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
@@ -42,6 +43,10 @@ namespace BTCPayServer
|
|||||||
return strategy != null;
|
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)
|
private static bool TryParseXpub(string xpub, DerivationSchemeParser derivationSchemeParser, ref DerivationSchemeSettings derivationSchemeSettings, bool electrum = true)
|
||||||
{
|
{
|
||||||
if (!electrum)
|
if (!electrum)
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ namespace BTCPayServer.Hosting
|
|||||||
services.TryAddSingleton<EventAggregator>();
|
services.TryAddSingleton<EventAggregator>();
|
||||||
services.TryAddSingleton<PaymentRequestService>();
|
services.TryAddSingleton<PaymentRequestService>();
|
||||||
services.TryAddSingleton<UserService>();
|
services.TryAddSingleton<UserService>();
|
||||||
|
services.TryAddSingleton<WalletHistogramService>();
|
||||||
services.AddSingleton<ApplicationDbContextFactory>();
|
services.AddSingleton<ApplicationDbContextFactory>();
|
||||||
services.AddOptions<BTCPayServerOptions>().Configure(
|
services.AddOptions<BTCPayServerOptions>().Configure(
|
||||||
(options) =>
|
(options) =>
|
||||||
@@ -176,6 +177,7 @@ namespace BTCPayServer.Hosting
|
|||||||
btcPayNetwork.NBXplorerNetwork.DefaultSettings.DefaultCookieFile)
|
btcPayNetwork.NBXplorerNetwork.DefaultSettings.DefaultCookieFile)
|
||||||
};
|
};
|
||||||
options.NBXplorerConnectionSettings.Add(setting);
|
options.NBXplorerConnectionSettings.Add(setting);
|
||||||
|
options.ConnectionString = configuration.GetOrDefault<string>("explorer.postgres", null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
services.AddOptions<LightningNetworkOptions>().Configure<BTCPayNetworkProvider>(
|
services.AddOptions<LightningNetworkOptions>().Configure<BTCPayNetworkProvider>(
|
||||||
@@ -310,6 +312,8 @@ namespace BTCPayServer.Hosting
|
|||||||
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(DerivationStrategyBase)));
|
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<HostedServices.CheckConfigurationHostedService>();
|
||||||
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>(o => o.GetRequiredService<CheckConfigurationHostedService>());
|
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>(o => o.GetRequiredService<CheckConfigurationHostedService>());
|
||||||
services.AddSingleton<HostedServices.WebhookSender>();
|
services.AddSingleton<HostedServices.WebhookSender>();
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.StoreViewModels;
|
namespace BTCPayServer.Models.StoreViewModels;
|
||||||
|
|
||||||
public class StoreDashboardViewModel
|
public class StoreDashboardViewModel
|
||||||
@@ -6,4 +9,6 @@ public class StoreDashboardViewModel
|
|||||||
public string StoreName { get; set; }
|
public string StoreName { get; set; }
|
||||||
public bool WalletEnabled { get; set; }
|
public bool WalletEnabled { get; set; }
|
||||||
public bool LightningEnabled { 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_UPDATEURL": "",
|
||||||
"BTCPAY_DOCKERDEPLOYMENT": "true",
|
"BTCPAY_DOCKERDEPLOYMENT": "true",
|
||||||
"BTCPAY_RECOMMENDED-PLUGINS": "BTCPayServer.Plugins.Test",
|
"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/"
|
"applicationUrl": "http://127.0.0.1:14142/"
|
||||||
},
|
},
|
||||||
@@ -66,7 +67,8 @@
|
|||||||
"BTCPAY_SOCKSENDPOINT": "localhost:9050",
|
"BTCPAY_SOCKSENDPOINT": "localhost:9050",
|
||||||
"BTCPAY_DOCKERDEPLOYMENT": "true",
|
"BTCPAY_DOCKERDEPLOYMENT": "true",
|
||||||
"BTCPAY_RECOMMENDED-PLUGINS": "BTCPayServer.Plugins.Test",
|
"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/"
|
"applicationUrl": "https://localhost:14142/"
|
||||||
},
|
},
|
||||||
@@ -105,7 +107,8 @@
|
|||||||
"BTCPAY_SOCKSENDPOINT": "localhost:9050",
|
"BTCPAY_SOCKSENDPOINT": "localhost:9050",
|
||||||
"BTCPAY_DOCKERDEPLOYMENT": "true",
|
"BTCPAY_DOCKERDEPLOYMENT": "true",
|
||||||
"BTCPAY_RECOMMENDED-PLUGINS": "BTCPayServer.Plugins.Test",
|
"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/"
|
"applicationUrl": "https://localhost:14142/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,9 +90,9 @@ namespace BTCPayServer.Services.Apps
|
|||||||
}
|
}
|
||||||
|
|
||||||
var invoices = await GetInvoicesForApp(appData, lastResetDate);
|
var invoices = await GetInvoicesForApp(appData, lastResetDate);
|
||||||
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed).ToArray();
|
var completeInvoices = invoices.Where(IsComplete).ToArray();
|
||||||
var pendingInvoices = invoices.Where(entity => !(entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed)).ToArray();
|
var pendingInvoices = invoices.Where(IsPending).ToArray();
|
||||||
var paidInvoices = invoices.Where(entity => entity.Status == InvoiceStatusLegacy.Complete || entity.Status == InvoiceStatusLegacy.Confirmed || entity.Status == InvoiceStatusLegacy.Paid).ToArray();
|
var paidInvoices = invoices.Where(IsPaid).ToArray();
|
||||||
|
|
||||||
var pendingPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, pendingInvoices, !settings.EnforceTargetAmount);
|
var pendingPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, pendingInvoices, !settings.EnforceTargetAmount);
|
||||||
var currentPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, completeInvoices, !settings.EnforceTargetAmount);
|
var currentPayments = GetContributionsByPaymentMethodId(settings.TargetCurrency, completeInvoices, !settings.EnforceTargetAmount);
|
||||||
@@ -102,11 +102,12 @@ namespace BTCPayServer.Services.Apps
|
|||||||
.GroupBy(entity => entity.Metadata.ItemCode)
|
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||||
.ToDictionary(entities => entities.Key, entities => entities.Count());
|
.ToDictionary(entities => entities.Key, entities => entities.Count());
|
||||||
|
|
||||||
Dictionary<string, decimal> perkValue = new Dictionary<string, decimal>();
|
Dictionary<string, decimal> perkValue = new();
|
||||||
if (settings.DisplayPerksValue)
|
if (settings.DisplayPerksValue)
|
||||||
{
|
{
|
||||||
perkValue = paidInvoices
|
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)
|
.GroupBy(entity => entity.Metadata.ItemCode)
|
||||||
.ToDictionary(entities => entities.Key, entities =>
|
.ToDictionary(entities => entities.Key, entities =>
|
||||||
entities.Sum(entity => entity.GetPayments(true).Sum(pay =>
|
entities.Sum(entity => entity.GetPayments(true).Sum(pay =>
|
||||||
@@ -117,6 +118,7 @@ namespace BTCPayServer.Services.Apps
|
|||||||
return rate * value;
|
return rate * value;
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
var perks = Parse(settings.PerksTemplate, settings.TargetCurrency);
|
var perks = Parse(settings.PerksTemplate, settings.TargetCurrency);
|
||||||
if (settings.SortPerksByPopularity)
|
if (settings.SortPerksByPopularity)
|
||||||
{
|
{
|
||||||
@@ -160,10 +162,9 @@ namespace BTCPayServer.Services.Apps
|
|||||||
Sounds = settings.Sounds,
|
Sounds = settings.Sounds,
|
||||||
AnimationColors = settings.AnimationColors,
|
AnimationColors = settings.AnimationColors,
|
||||||
CurrencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true),
|
CurrencyData = _Currencies.GetCurrencyData(settings.TargetCurrency, true),
|
||||||
CurrencyDataPayments = currentPayments.Select(pair => pair.Key)
|
CurrencyDataPayments = Enumerable.DistinctBy(currentPayments.Select(pair => pair.Key)
|
||||||
.Concat(pendingPayments.Select(pair => pair.Key))
|
.Concat(pendingPayments.Select(pair => pair.Key))
|
||||||
.Select(id => _Currencies.GetCurrencyData(id.CryptoCode, true))
|
.Select(id => _Currencies.GetCurrencyData(id.CryptoCode, true)), data => data.Code)
|
||||||
.DistinctBy(data => data.Code)
|
|
||||||
.ToDictionary(data => data.Code, data => data),
|
.ToDictionary(data => data.Code, data => data),
|
||||||
Info = new CrowdfundInfo
|
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 GetCrowdfundOrderId(string appId) => $"crowdfund-app_{appId}";
|
||||||
public static string GetAppInternalTag(string appId) => $"APP#{appId}";
|
public static string GetAppInternalTag(string appId) => $"APP#{appId}";
|
||||||
public static string[] GetAppInternalTags(InvoiceEntity invoice)
|
public static string[] GetAppInternalTags(InvoiceEntity invoice)
|
||||||
{
|
{
|
||||||
return invoice.GetInternalTags("APP#");
|
return invoice.GetInternalTags("APP#");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, DateTime? startDate = null)
|
private async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, DateTime? startDate = null)
|
||||||
{
|
{
|
||||||
var invoices = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
var invoices = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||||
@@ -572,4 +662,26 @@ namespace BTCPayServer.Services.Apps
|
|||||||
return true;
|
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 {
|
.dropdown-item {
|
||||||
cursor: pointer;
|
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 */
|
/* pull mass action form up, so that it is besides the search form */
|
||||||
@@media (min-width: 1200px) {
|
@@media (min-width: 1200px) {
|
||||||
|
|||||||
@@ -2,67 +2,150 @@
|
|||||||
|
|
||||||
@inject BTCPayNetworkProvider networkProvider
|
@inject BTCPayNetworkProvider networkProvider
|
||||||
@{
|
@{
|
||||||
ViewData.SetActivePage(StoreNavPages.Dashboard, Model.StoreName, Model.StoreId);
|
ViewData.SetActivePage(StoreNavPages.Dashboard, Model.StoreName, Model.StoreId);
|
||||||
var isReady = Model.WalletEnabled || Model.LightningEnabled;
|
var defaultCryptoCode = networkProvider.DefaultNetwork.CryptoCode;
|
||||||
var defaultCryptoCode = networkProvider.DefaultNetwork.CryptoCode;
|
var store = ViewContext.HttpContext.GetStoreData();
|
||||||
}
|
}
|
||||||
|
|
||||||
<partial name="_StatusMessage" />
|
<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
|
else
|
||||||
{
|
{
|
||||||
<p class="lead text-secondary">To start accepting payments, set up a wallet or a Lightning node.</p>
|
<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
|
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||||
|
@inject BTCPayServer.Services.BTCPayServerEnvironment env
|
||||||
@model BTCPayServer.Controllers.WalletReceiveViewModel
|
@model BTCPayServer.Controllers.WalletReceiveViewModel
|
||||||
@{
|
@{
|
||||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||||
@@ -17,6 +18,10 @@
|
|||||||
@if (string.IsNullOrEmpty(Model.Address))
|
@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>
|
<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
|
else
|
||||||
{
|
{
|
||||||
@@ -84,7 +89,7 @@
|
|||||||
<div class="col-12 col-sm-6 mt-4 mt-sm-0">
|
<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>
|
<button type="submit" name="command" value="unreserve-current-address" class="btn btn-secondary w-100">Unreserve this address</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -70,6 +70,32 @@ a.unobtrusive-link {
|
|||||||
text-decoration: inherit;
|
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 */
|
/* Info icons in main headline */
|
||||||
h2 small .fa-question-circle-o {
|
h2 small .fa-question-circle-o {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -227,3 +253,172 @@ svg.icon-note {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 1rem 0;
|
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
Reference in New Issue
Block a user