Wallet Transactions Export: Add BIP-329 support (#4799)

* Wallet Transactions Export: Add BIP-329 support

* Adjust wording

* Export one line per label

* Join labels, fix type

* Rewrite the ProcessBip329 function to be more performant

* Add nullable on all TransactionsExport

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
d11n 2023-03-27 06:59:33 +02:00 committed by GitHub
parent 18c78192ec
commit c53d5272d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 72 additions and 23 deletions

View File

@ -1570,9 +1570,20 @@ namespace BTCPayServer.Tests
Assert.Contains("\"Amount\": \"3.00000000\"", s.Driver.PageSource);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// BIP-329 export
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ExportBIP329")).Click();
Thread.Sleep(1000);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.Contains(s.WalletId.ToString(), s.Driver.Url);
Assert.EndsWith("export?format=bip329", s.Driver.Url);
Assert.Contains("{\"type\":\"tx\",\"ref\":\"", 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();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
}
[Fact(Timeout = TestTimeout)]

View File

@ -14,6 +14,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using ExchangeSharp;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
@ -250,7 +251,6 @@ retry:
{
// 1. Generate an API Token on https://www.transifex.com/user/settings/api/
// 2. Run "dotnet user-secrets set TransifexAPIToken <youapitoken>"
await PullTransifexTranslationsCore(TranslationFolder.CheckoutV1);
await PullTransifexTranslationsCore(TranslationFolder.CheckoutV2);

View File

@ -1317,22 +1317,34 @@ namespace BTCPayServer.Controllers
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId, (string[]?)null);
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, null, null);
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation);
var walletTransactionsInfo = await walletTransactionsInfoAsync;
var export = new TransactionsExport(wallet, walletTransactionsInfo);
var res = export.Process(input, format);
var fileType = format switch
{
"csv" => "csv",
"json" => "json",
"bip329" => "jsonl",
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null)
};
var mimeType = format switch
{
"csv" => "text/csv",
"json" => "application/json",
"bip329" => "text/jsonl", // https://stackoverflow.com/questions/59938644/what-is-the-mime-type-of-jsonl-files
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null)
};
var cd = new ContentDisposition
{
FileName = $"btcpay-{walletId}-{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)}.{format}",
FileName = $"btcpay-{walletId}-{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)}.{fileType}",
Inline = true
};
Response.Headers.Add("Content-Disposition", cd.ToString());
Response.Headers.Add("X-Content-Type-Options", "nosniff");
return Content(res, "application/" + format);
return Content(res, mimeType);
}
public class UpdateLabelsRequest
{
public string? Id { get; set; }

View File

@ -1,16 +1,13 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using BTCPayServer.Client.Models;
using System.Text;
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
@ -41,18 +38,46 @@ namespace BTCPayServer.Services.Wallets.Export
if (_walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
{
model.Labels = transactionInfo.LabelColors?.Select(l => l.Key).ToList();
model.Labels = transactionInfo.LabelColors.Select(l => l.Key).ToList();
model.Comment = transactionInfo.Comment;
}
return model;
}).ToList();
return fileFormat switch
{
"bip329" => ProcessBip329(list),
"json" => ProcessJson(list),
"csv" => ProcessCsv(list),
_ => throw new Exception("Export format not supported")
};
}
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");
// https://github.com/bitcoin/bips/blob/master/bip-0329.mediawiki
private static string ProcessBip329(List<ExportTransaction> txs)
{
var sw = new StringWriter();
var jsonw = new JsonTextWriter(sw);
foreach (var tx in txs)
{
if (tx.Labels is null)
continue;
foreach (var label in tx.Labels)
{
jsonw.WriteStartObject();
jsonw.WritePropertyName("type");
jsonw.WriteValue("tx");
jsonw.WritePropertyName("ref");
jsonw.WriteValue(tx.TransactionId);
jsonw.WritePropertyName("label");
jsonw.WriteValue(label);
jsonw.WriteEndObject();
jsonw.WriteWhitespace("\n");
}
}
jsonw.Flush();
return sw.ToString();
}
private static string ProcessJson(List<ExportTransaction> invoices)
@ -87,14 +112,14 @@ namespace BTCPayServer.Services.Wallets.Export
public class ExportTransaction
{
[Name("Transaction Id")]
public string TransactionId { get; set; }
public string TransactionId { get; set; } = string.Empty;
public DateTimeOffset Timestamp { get; set; }
public string Amount { get; set; }
public string Currency { get; set; }
public string Amount { get; set; } = string.Empty;
public string Currency { get; set; } = string.Empty;
[Name("Is Confirmed")]
public bool IsConfirmed { get; set; }
public string Comment { get; set; }
public List<string> Labels { get; set; }
public string? Comment { get; set; }
public List<string>? Labels { get; set; }
}
}

View File

@ -167,6 +167,7 @@
<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>
<a asp-action="Export" asp-route-walletId="@walletId" asp-route-format="bip329" asp-route-labelFilter="@labelFilter" class="dropdown-item export-link" target="_blank" id="ExportBIP329">Wallet Labels (BIP-329)</a>
</div>
</div>
</div>