mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 22:44:29 +01:00
Wallet transactions export (#3744)
* Wallet transactions export The exported data needs some more work. * Fix transactions export policy * Add test cases * Fix Selenium warnings * Finalize export format * Test export download * Remove CSV download check * Try to fix test
This commit is contained in:
@@ -522,7 +522,7 @@ namespace BTCPayServer.Tests
|
|||||||
walletId ??= WalletId;
|
walletId ??= WalletId;
|
||||||
GoToWallet(walletId, WalletsNavPages.Receive);
|
GoToWallet(walletId, WalletsNavPages.Receive);
|
||||||
Driver.FindElement(By.Id("generateButton")).Click();
|
Driver.FindElement(By.Id("generateButton")).Click();
|
||||||
var addressStr = Driver.FindElement(By.Id("address")).GetProperty("value");
|
var addressStr = Driver.FindElement(By.Id("address")).GetAttribute("value");
|
||||||
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
||||||
for (var i = 0; i < coins; i++)
|
for (var i = 0; i < coins; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -809,7 +809,7 @@ namespace BTCPayServer.Tests
|
|||||||
var walletId = new WalletId(storeId, "BTC");
|
var walletId = new WalletId(storeId, "BTC");
|
||||||
s.GoToWallet(walletId, WalletsNavPages.Receive);
|
s.GoToWallet(walletId, WalletsNavPages.Receive);
|
||||||
s.Driver.FindElement(By.Id("generateButton")).Click();
|
s.Driver.FindElement(By.Id("generateButton")).Click();
|
||||||
var addressStr = s.Driver.FindElement(By.Id("address")).GetProperty("value");
|
var addressStr = s.Driver.FindElement(By.Id("address")).GetAttribute("value");
|
||||||
var address = BitcoinAddress.Create(addressStr,
|
var address = BitcoinAddress.Create(addressStr,
|
||||||
((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
|
((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
|
||||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||||
@@ -1149,6 +1149,25 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.Empty(s.Driver.FindElements(By.Id("confirm")));
|
Assert.Empty(s.Driver.FindElements(By.Id("confirm")));
|
||||||
s.Driver.FindElement(By.Id("proceed")).Click();
|
s.Driver.FindElement(By.Id("proceed")).Click();
|
||||||
Assert.Equal(settingsUrl, s.Driver.Url);
|
Assert.Equal(settingsUrl, s.Driver.Url);
|
||||||
|
|
||||||
|
// Transactions list contains export and action, ensure functions are present.
|
||||||
|
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("BumpFee"));
|
||||||
|
|
||||||
|
// JSON export
|
||||||
|
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("ExportJSON")).Click();
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||||
|
Assert.Contains(s.WalletId.ToString(), s.Driver.Url);
|
||||||
|
Assert.EndsWith("export?format=json", s.Driver.Url);
|
||||||
|
Assert.Contains("\"Amount\": \"3.00000000\"", s.Driver.PageSource);
|
||||||
|
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||||
|
|
||||||
|
// CSV export
|
||||||
|
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("ExportCSV")).Click();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Timeout = TestTimeout)]
|
[Fact(Timeout = TestTimeout)]
|
||||||
@@ -1167,6 +1186,12 @@ namespace BTCPayServer.Tests
|
|||||||
s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).GetAttribute("value"));
|
s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).GetAttribute("value"));
|
||||||
Assert.Contains("m/84'/1'/0'",
|
Assert.Contains("m/84'/1'/0'",
|
||||||
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
|
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
|
||||||
|
|
||||||
|
// Transactions list is empty
|
||||||
|
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||||
|
Assert.Contains("There are no transactions yet.", s.Driver.PageSource);
|
||||||
|
s.Driver.AssertElementNotFound(By.Id("ExportDropdownToggle"));
|
||||||
|
s.Driver.AssertElementNotFound(By.Id("ActionsDropdownToggle"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Mime;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
@@ -29,6 +30,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Logging;
|
using BTCPayServer.Logging;
|
||||||
|
using BTCPayServer.Services.Wallets.Export;
|
||||||
using NBXplorer;
|
using NBXplorer;
|
||||||
using NBXplorer.Client;
|
using NBXplorer.Client;
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
@@ -579,7 +581,6 @@ namespace BTCPayServer.Controllers
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("{walletId}/send")]
|
[HttpPost("{walletId}/send")]
|
||||||
public async Task<IActionResult> WalletSend(
|
public async Task<IActionResult> WalletSend(
|
||||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||||
@@ -1278,6 +1279,36 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("{walletId}/export")]
|
||||||
|
public async Task<IActionResult> Export(
|
||||||
|
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
|
||||||
|
string format, string labelFilter = null)
|
||||||
|
{
|
||||||
|
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
|
||||||
|
if (paymentMethod == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||||
|
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId);
|
||||||
|
var transactions = await wallet.FetchTransactions(paymentMethod.AccountDerivation);
|
||||||
|
var walletTransactionsInfo = await walletTransactionsInfoAsync;
|
||||||
|
var input = transactions.UnconfirmedTransactions.Transactions
|
||||||
|
.Concat(transactions.ConfirmedTransactions.Transactions)
|
||||||
|
.OrderByDescending(t => t.Timestamp)
|
||||||
|
.ToList();
|
||||||
|
var export = new TransactionsExport(wallet, walletTransactionsInfo);
|
||||||
|
var res = export.Process(input, format);
|
||||||
|
|
||||||
|
var cd = new ContentDisposition
|
||||||
|
{
|
||||||
|
FileName = $"btcpay-{walletId}-{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)}.{format}",
|
||||||
|
Inline = true
|
||||||
|
};
|
||||||
|
Response.Headers.Add("Content-Disposition", cd.ToString());
|
||||||
|
Response.Headers.Add("X-Content-Type-Options", "nosniff");
|
||||||
|
return Content(res, "application/" + format);
|
||||||
|
}
|
||||||
|
|
||||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||||
{
|
{
|
||||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
|
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
|
||||||
|
|||||||
100
BTCPayServer/Services/Wallets/Export/TransactionsExport.cs
Normal file
100
BTCPayServer/Services/Wallets/Export/TransactionsExport.cs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Models.WalletViewModels;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
|
using CsvHelper.Configuration;
|
||||||
|
using CsvHelper.Configuration.Attributes;
|
||||||
|
using NBXplorer.Models;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services.Wallets.Export
|
||||||
|
{
|
||||||
|
public class TransactionsExport
|
||||||
|
{
|
||||||
|
private readonly BTCPayWallet _wallet;
|
||||||
|
private readonly Dictionary<string, WalletTransactionInfo> _walletTransactionsInfo;
|
||||||
|
|
||||||
|
public TransactionsExport(BTCPayWallet wallet, Dictionary<string, WalletTransactionInfo> walletTransactionsInfo)
|
||||||
|
{
|
||||||
|
_wallet = wallet;
|
||||||
|
_walletTransactionsInfo = walletTransactionsInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Process(IEnumerable<TransactionInformation> inputList, string fileFormat)
|
||||||
|
{
|
||||||
|
var list = inputList.Select(tx =>
|
||||||
|
{
|
||||||
|
var model = new ExportTransaction
|
||||||
|
{
|
||||||
|
TransactionId = tx.TransactionId.ToString(),
|
||||||
|
Timestamp = tx.Timestamp,
|
||||||
|
Amount = tx.BalanceChange.ShowMoney(_wallet.Network),
|
||||||
|
Currency = _wallet.Network.CryptoCode,
|
||||||
|
IsConfirmed = tx.Confirmations != 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
|
||||||
|
{
|
||||||
|
model.Labels = transactionInfo.Labels?.Select(l => l.Value.Text).ToList();
|
||||||
|
model.Comment = transactionInfo.Comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
if (string.Equals(fileFormat, "json", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return ProcessJson(list);
|
||||||
|
if (string.Equals(fileFormat, "csv", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return ProcessCsv(list);
|
||||||
|
throw new Exception("Export format not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ProcessJson(List<ExportTransaction> invoices)
|
||||||
|
{
|
||||||
|
var serializerSett = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
|
||||||
|
var json = JsonConvert.SerializeObject(invoices, Formatting.Indented, serializerSett);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ProcessCsv(IEnumerable<ExportTransaction> invoices)
|
||||||
|
{
|
||||||
|
using StringWriter writer = new();
|
||||||
|
using var csvWriter = new CsvHelper.CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture), true);
|
||||||
|
csvWriter.Configuration.RegisterClassMap<ExportTransactionMap>();
|
||||||
|
csvWriter.WriteHeader<ExportTransaction>();
|
||||||
|
csvWriter.NextRecord();
|
||||||
|
csvWriter.WriteRecords(invoices);
|
||||||
|
csvWriter.Flush();
|
||||||
|
return writer.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ExportTransactionMap : ClassMap<ExportTransaction>
|
||||||
|
{
|
||||||
|
public ExportTransactionMap()
|
||||||
|
{
|
||||||
|
AutoMap(CultureInfo.InvariantCulture);
|
||||||
|
Map(m => m.Labels).ConvertUsing(row => row.Labels == null ? string.Empty : string.Join(", ", row.Labels));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ExportTransaction
|
||||||
|
{
|
||||||
|
[Name("Transaction Id")]
|
||||||
|
public string TransactionId { get; set; }
|
||||||
|
public DateTimeOffset Timestamp { get; set; }
|
||||||
|
public string Amount { get; set; }
|
||||||
|
public string Currency { get; set; }
|
||||||
|
|
||||||
|
[Name("Is Confirmed")]
|
||||||
|
public bool IsConfirmed { get; set; }
|
||||||
|
public string Comment { get; set; }
|
||||||
|
public List<string> Labels { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -283,8 +283,8 @@
|
|||||||
<form method="post" id="MassAction" asp-action="MassAction" class="">
|
<form method="post" id="MassAction" asp-action="MassAction" class="">
|
||||||
<div class="d-inline-flex align-items-center pb-2 float-xl-end mb-2 gap-3">
|
<div class="d-inline-flex align-items-center pb-2 float-xl-end mb-2 gap-3">
|
||||||
<input type="hidden" name="storeId" value="@Model.StoreId" />
|
<input type="hidden" name="storeId" value="@Model.StoreId" />
|
||||||
<span class="order-xl-1">
|
<div class="dropdown order-xl-1">
|
||||||
<button class="btn btn-secondary dropdown-toggle mb-1" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<button class="btn btn-secondary dropdown-toggle" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
Actions
|
Actions
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-xl-end" aria-labelledby="ActionsDropdownToggle">
|
<div class="dropdown-menu dropdown-menu-xl-end" aria-labelledby="ActionsDropdownToggle">
|
||||||
@@ -295,11 +295,11 @@
|
|||||||
}
|
}
|
||||||
<button id="BumpFee" type="submit" permission="@Policies.CanModifyStoreSettings" class="dropdown-item" name="command" value="cpfp">Bump fee</button>
|
<button id="BumpFee" type="submit" permission="@Policies.CanModifyStoreSettings" class="dropdown-item" name="command" value="cpfp">Bump fee</button>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
<span class="d-inline-flex align-items-center gap-3">
|
<div class="dropdown d-inline-flex align-items-center gap-3">
|
||||||
<a class="btn btn-secondary dropdown-toggle mb-1 order-xl-1" href="#" role="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<button class="btn btn-secondary dropdown-toggle order-xl-1" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
Export
|
Export
|
||||||
</a>
|
</button>
|
||||||
<div class="dropdown-menu" aria-labelledby="ExportDropdownToggle">
|
<div class="dropdown-menu" aria-labelledby="ExportDropdownToggle">
|
||||||
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">CSV</a>
|
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="csv" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">CSV</a>
|
||||||
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">JSON</a>
|
<a asp-action="Export" asp-route-timezoneoffset="0" asp-route-format="json" asp-route-searchTerm="@Model.SearchTerm" class="dropdown-item export-link" target="_blank">JSON</a>
|
||||||
@@ -307,7 +307,7 @@
|
|||||||
<a href="https://docs.btcpayserver.org/Accounting/" target="_blank" rel="noreferrer noopener">
|
<a href="https://docs.btcpayserver.org/Accounting/" target="_blank" rel="noreferrer noopener">
|
||||||
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
|
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="clear:both"></div>
|
<div style="clear:both"></div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@model ListTransactionsViewModel
|
@model ListTransactionsViewModel
|
||||||
@{
|
@{
|
||||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||||
|
var labelFilter = Context.Request.Query["labelFilter"].ToString();
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
ViewData.SetActivePage(WalletsNavPages.Transactions, $"{Model.CryptoCode} Transactions", walletId);
|
ViewData.SetActivePage(WalletsNavPages.Transactions, $"{Model.CryptoCode} Transactions", walletId);
|
||||||
}
|
}
|
||||||
@@ -47,6 +48,13 @@
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* pull actions area, so that it is besides the search form */
|
||||||
|
@@media (min-width: 1200px) {
|
||||||
|
#Actions {
|
||||||
|
margin-top: -4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,10 +75,10 @@
|
|||||||
|
|
||||||
@if (Model.Transactions.Any())
|
@if (Model.Transactions.Any())
|
||||||
{
|
{
|
||||||
<div class="d-sm-flex align-items-center gap-3 mb-2">
|
@if (Model.Labels.Any())
|
||||||
@if (Model.Labels.Any())
|
{
|
||||||
{
|
<div class="col-xl-7 col-xxl-8 mb-4">
|
||||||
<div class="input-group mb-4 mb-sm-0">
|
<div class="input-group">
|
||||||
<span class="input-group-text">Filter</span>
|
<span class="input-group-text">Filter</span>
|
||||||
<div class="form-control d-flex flex-wrap gap-2 align-items-center">
|
<div class="form-control d-flex flex-wrap gap-2 align-items-center">
|
||||||
@foreach (var label in Model.Labels)
|
@foreach (var label in Model.Labels)
|
||||||
@@ -78,13 +86,15 @@
|
|||||||
<a asp-route-labelFilter="@label.Text" class="badge position-relative text-white flex-grow-0" style="background-color:@label.Color;color:@label.TextColor !important;">@label.Text</a>
|
<a asp-route-labelFilter="@label.Text" class="badge position-relative text-white flex-grow-0" style="background-color:@label.Color;color:@label.TextColor !important;">@label.Text</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (Context.Request.Query.ContainsKey("labelFilter"))
|
@if (labelFilter != null)
|
||||||
{
|
{
|
||||||
<a asp-route-labelFilter="" class="btn btn-secondary d-flex align-items-center">Clear filter</a>
|
<a asp-route-labelFilter="" class="btn btn-secondary d-flex align-items-center">Clear filter</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
<div class="dropdown ms-auto">
|
}
|
||||||
|
<div class="d-inline-flex align-items-center pb-2 float-xl-end mb-2 gap-3" id="Actions">
|
||||||
|
<div class="dropdown order-xl-1 ms-auto">
|
||||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<button class="btn btn-secondary dropdown-toggle" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
Actions
|
Actions
|
||||||
</button>
|
</button>
|
||||||
@@ -94,7 +104,20 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="dropdown d-inline-flex align-items-center gap-3">
|
||||||
|
<button class="btn btn-secondary dropdown-toggle order-xl-1" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
Export
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="ExportDropdownToggle">
|
||||||
|
<a asp-action="Export" asp-route-walletId="@walletId" asp-route-format="csv" asp-route-labelFilter="@labelFilter" class="dropdown-item export-link" target="_blank" id="ExportCSV">CSV</a>
|
||||||
|
<a asp-action="Export" asp-route-walletId="@walletId" asp-route-format="json" asp-route-labelFilter="@labelFilter" class="dropdown-item export-link" target="_blank" id="ExportJSON">JSON</a>
|
||||||
|
</div>
|
||||||
|
<a href="https://docs.btcpayserver.org/Accounting/" target="_blank" rel="noreferrer noopener">
|
||||||
|
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="clear:both"></div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col table-responsive-md">
|
<div class="col table-responsive-md">
|
||||||
|
|||||||
Reference in New Issue
Block a user