Add labels for recent txs dashboard widget (#4831)

* Add labels for recent txs dashboard widget

It is not with the rich data for now, but a good start.

* Turn labels into links

* Add rich info to dashboard labels

* Use truncate-center component for recent transactions

---------

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
Andrew Camilleri
2023-03-30 19:54:11 +02:00
committed by GitHub
parent 98d8ef8e1a
commit eece001376
7 changed files with 198 additions and 140 deletions

View File

@@ -36,6 +36,7 @@
<tr>
<th class="w-125px">Date</th>
<th>Transaction</th>
<th>Labels</th>
<th class="text-end">Amount</th>
</tr>
</thead>
@@ -45,9 +46,29 @@
<tr>
<td>@tx.Timestamp.ToTimeAgo()</td>
<td>
<a href="@tx.Link" target="_blank" rel="noreferrer noopener" class="text-break">
@tx.Id
</a>
<vc:truncate-center text="@tx.Id" link="tx.Link" classes="truncate-center-id" />
</td>
<td>
@if (tx.Labels.Any())
{
<div class="d-flex flex-wrap gap-2 align-items-center">
@foreach (var label in tx.Labels)
{
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
<span>@label.Text</span>
@if (!string.IsNullOrEmpty(label.Link))
{
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
<vc:icon symbol="info" />
</a>
}
</div>
}
</div>
}
</td>
@if (tx.Positive)
{

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Models.WalletViewModels;
namespace BTCPayServer.Components.StoreRecentTransactions;
@@ -11,4 +13,5 @@ public class StoreRecentTransactionViewModel
public bool IsConfirmed { get; set; }
public string Link { get; set; }
public DateTimeOffset Timestamp { get; set; }
public IEnumerable<TransactionTagModel> Labels { get; set; }
}

View File

@@ -8,6 +8,7 @@ using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Data;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Dapper;
@@ -23,14 +24,20 @@ namespace BTCPayServer.Components.StoreRecentTransactions;
public class StoreRecentTransactions : ViewComponent
{
private readonly BTCPayWalletProvider _walletProvider;
private readonly WalletRepository _walletRepository;
private readonly LabelService _labelService;
public BTCPayNetworkProvider NetworkProvider { get; }
public StoreRecentTransactions(
BTCPayNetworkProvider networkProvider,
BTCPayWalletProvider walletProvider)
BTCPayWalletProvider walletProvider,
WalletRepository walletRepository,
LabelService labelService)
{
NetworkProvider = networkProvider;
_walletProvider = walletProvider;
_walletRepository = walletRepository;
_labelService = labelService;
}
public async Task<IViewComponentResult> InvokeAsync(StoreRecentTransactionsViewModel vm)
@@ -52,16 +59,25 @@ public class StoreRecentTransactions : ViewComponent
var network = derivationSettings.Network;
var wallet = _walletProvider.GetWallet(network);
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0));
var walletTransactionsInfo = await _walletRepository.GetWalletTransactionsInfo( vm.WalletId , allTransactions.Select(t => t.TransactionId.ToString()).ToArray());
transactions = allTransactions
.Select(tx => new StoreRecentTransactionViewModel
.Select(tx =>
{
Id = tx.TransactionId.ToString(),
Positive = tx.BalanceChange.GetValue(network) >= 0,
Balance = tx.BalanceChange.ShowMoney(network),
Currency = vm.CryptoCode,
IsConfirmed = tx.Confirmations != 0,
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, tx.TransactionId.ToString()),
Timestamp = tx.SeenAt
walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo);
var labels = _labelService.CreateTransactionTagModels(transactionInfo, Request);
return new StoreRecentTransactionViewModel
{
Id = tx.TransactionId.ToString(),
Positive = tx.BalanceChange.GetValue(network) >= 0,
Balance = tx.BalanceChange.ShowMoney(network),
Currency = vm.CryptoCode,
IsConfirmed = tx.Confirmations != 0,
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
tx.TransactionId.ToString()),
Timestamp = tx.SeenAt,
Labels = labels
};
})
.ToList();
}

View File

@@ -65,9 +65,8 @@ namespace BTCPayServer.Controllers
private readonly SettingsRepository _settingsRepository;
private readonly DelayedTransactionBroadcaster _broadcaster;
private readonly PayjoinClient _payjoinClient;
private readonly LinkGenerator _linkGenerator;
private readonly LabelService _labelService;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly UTXOLocker _utxoLocker;
private readonly WalletHistogramService _walletHistogramService;
readonly CurrencyNameTable _currencyTable;
@@ -90,11 +89,10 @@ namespace BTCPayServer.Controllers
PayjoinClient payjoinClient,
IServiceProvider serviceProvider,
PullPaymentHostedService pullPaymentHostedService,
UTXOLocker utxoLocker,
LinkGenerator linkGenerator)
LabelService labelService)
{
_currencyTable = currencyTable;
_linkGenerator = linkGenerator;
_labelService = labelService;
Repository = repo;
WalletRepository = walletRepository;
RateFetcher = rateProvider;
@@ -110,7 +108,6 @@ namespace BTCPayServer.Controllers
_broadcaster = broadcaster;
_payjoinClient = payjoinClient;
_pullPaymentHostedService = pullPaymentHostedService;
_utxoLocker = utxoLocker;
ServiceProvider = serviceProvider;
_walletHistogramService = walletHistogramService;
}
@@ -266,7 +263,7 @@ namespace BTCPayServer.Controllers
if (walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
{
var labels = CreateTransactionTagModels(transactionInfo);
var labels = _labelService.CreateTransactionTagModels(transactionInfo, Request);
vm.Tags.AddRange(labels);
vm.Comment = transactionInfo.Comment;
}
@@ -601,7 +598,7 @@ namespace BTCPayServer.Controllers
Outpoint = coin.OutPoint.ToString(),
Amount = coin.Value.GetValue(network),
Comment = info?.Comment,
Labels = CreateTransactionTagModels(info),
Labels = _labelService.CreateTransactionTagModels(info, Request),
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink,
coin.OutPoint.Hash.ToString()),
Confirmations = coin.Confirmations
@@ -1412,124 +1409,6 @@ namespace BTCPayServer.Controllers
private string GetUserId() => _userManager.GetUserId(User);
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
public IEnumerable<TransactionTagModel> CreateTransactionTagModels(WalletTransactionInfo? transactionInfo)
{
if (transactionInfo is null)
return Array.Empty<TransactionTagModel>();
string PayoutTooltip(IGrouping<string, string>? payoutsByPullPaymentId = null)
{
if (payoutsByPullPaymentId is null)
{
return "Paid a payout";
}
else if (payoutsByPullPaymentId.Count() == 1)
{
var pp = payoutsByPullPaymentId.Key;
var payout = payoutsByPullPaymentId.First();
if (!string.IsNullOrEmpty(pp))
return $"Paid a payout ({payout}) of a pull payment ({pp})";
else
return $"Paid a payout {payout}";
}
else
{
var pp = payoutsByPullPaymentId.Key;
if (!string.IsNullOrEmpty(pp))
return $"Paid {payoutsByPullPaymentId.Count()} payouts of a pull payment ({pp})";
else
return $"Paid {payoutsByPullPaymentId.Count()} payouts";
}
}
var models = new Dictionary<string, TransactionTagModel>();
foreach (var tag in transactionInfo.Attachments)
{
if (models.ContainsKey(tag.Type))
continue;
if (!transactionInfo.LabelColors.TryGetValue(tag.Type, out var color))
continue;
var model = new TransactionTagModel
{
Text = tag.Type,
Color = color,
TextColor = ColorPalette.Default.TextColor(color)
};
models.Add(tag.Type, model);
if (tag.Type == WalletObjectData.Types.Payout)
{
var payoutsByPullPaymentId =
transactionInfo.Attachments.Where(t => t.Type == "payout")
.GroupBy(t => t.Data?["pullPaymentId"]?.Value<string>() ?? "",
k => k.Id).ToList();
model.Tooltip = payoutsByPullPaymentId.Count switch
{
0 => PayoutTooltip(),
1 => PayoutTooltip(payoutsByPullPaymentId.First()),
_ => string.Join(", ", payoutsByPullPaymentId.Select(PayoutTooltip))
};
model.Link = _linkGenerator.PayoutLink(transactionInfo.WalletId.ToString(), null, PayoutState.Completed, Request.Scheme, Request.Host,
Request.PathBase);
}
else if (tag.Type == WalletObjectData.Types.Payjoin)
{
model.Tooltip = "This UTXO was part of a PayJoin transaction.";
}
else if (tag.Type == WalletObjectData.Types.Invoice)
{
model.Tooltip = $"Received through an invoice {tag.Id}";
model.Link = string.IsNullOrEmpty(tag.Id)
? null
: _linkGenerator.InvoiceLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
}
else if (tag.Type == WalletObjectData.Types.PaymentRequest)
{
model.Tooltip = $"Received through a payment request {tag.Id}";
model.Link = _linkGenerator.PaymentRequestLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
}
else if (tag.Type == WalletObjectData.Types.App)
{
model.Tooltip = $"Received through an app {tag.Id}";
model.Link = _linkGenerator.AppLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
}
else if (tag.Type == WalletObjectData.Types.PayjoinExposed)
{
if (tag.Id.Length != 0)
{
model.Tooltip = $"This UTXO was exposed through a PayJoin proposal for an invoice ({tag.Id})";
model.Link = _linkGenerator.InvoiceLink(tag.Id, Request.Scheme, Request.Host, Request.PathBase);
}
else
{
model.Tooltip = $"This UTXO was exposed through a PayJoin proposal";
}
}
else if (tag.Type == WalletObjectData.Types.Payjoin)
{
model.Tooltip = $"This UTXO was part of a PayJoin transaction.";
}
else
{
model.Tooltip = tag.Data?.TryGetValue("tooltip", StringComparison.InvariantCultureIgnoreCase, out var tooltip) is true ? tooltip.ToString() : tag.Id;
if (tag.Data?.TryGetValue("link", StringComparison.InvariantCultureIgnoreCase, out var link) is true)
{
model.Link = link.ToString();
}
}
}
foreach (var label in transactionInfo.LabelColors)
models.TryAdd(label.Key, new TransactionTagModel
{
Text = label.Key,
Color = label.Value,
TextColor = ColorPalette.Default.TextColor(label.Value)
});
return models.Values.OrderBy(v => v.Text);
}
}
public class WalletReceiveViewModel

View File

@@ -433,6 +433,7 @@ namespace BTCPayServer.Hosting
services.AddTransient<BitpayAccessTokenController>();
services.AddTransient<UIInvoiceController>();
services.AddTransient<UIPaymentRequestController>();
services.AddSingleton<LabelService>();
// Add application services.
services.AddSingleton<EmailSenderFactory>();
services.AddSingleton<InvoiceActivator>();

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Client.Models;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

View File

@@ -0,0 +1,140 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Models.WalletViewModels;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Labels;
public class LabelService
{
private readonly LinkGenerator _linkGenerator;
public LabelService(LinkGenerator linkGenerator)
{
_linkGenerator = linkGenerator;
}
public IEnumerable<TransactionTagModel> CreateTransactionTagModels(WalletTransactionInfo? transactionInfo, HttpRequest req)
{
if (transactionInfo is null)
return Array.Empty<TransactionTagModel>();
string PayoutTooltip(IGrouping<string, string>? payoutsByPullPaymentId = null)
{
if (payoutsByPullPaymentId is null)
{
return "Paid a payout";
}
if (payoutsByPullPaymentId.Count() == 1)
{
var pp = payoutsByPullPaymentId.Key;
var payout = payoutsByPullPaymentId.First();
return !string.IsNullOrEmpty(pp)
? $"Paid a payout ({payout}) of a pull payment ({pp})"
: $"Paid a payout {payout}";
}
else
{
var pp = payoutsByPullPaymentId.Key;
return !string.IsNullOrEmpty(pp)
? $"Paid {payoutsByPullPaymentId.Count()} payouts of a pull payment ({pp})"
: $"Paid {payoutsByPullPaymentId.Count()} payouts";
}
}
var models = new Dictionary<string, TransactionTagModel>();
foreach (var tag in transactionInfo.Attachments)
{
if (models.ContainsKey(tag.Type))
continue;
if (!transactionInfo.LabelColors.TryGetValue(tag.Type, out var color))
continue;
var model = new TransactionTagModel
{
Text = tag.Type,
Color = color,
TextColor = ColorPalette.Default.TextColor(color)
};
models.Add(tag.Type, model);
if (tag.Type == WalletObjectData.Types.Payout)
{
var payoutsByPullPaymentId =
transactionInfo.Attachments.Where(t => t.Type == "payout")
.GroupBy(t => t.Data?["pullPaymentId"]?.Value<string>() ?? "",
k => k.Id).ToList();
model.Tooltip = payoutsByPullPaymentId.Count switch
{
0 => PayoutTooltip(),
1 => PayoutTooltip(payoutsByPullPaymentId.First()),
_ => string.Join(", ", payoutsByPullPaymentId.Select(PayoutTooltip))
};
model.Link = _linkGenerator.PayoutLink(transactionInfo.WalletId.ToString(), null,
PayoutState.Completed, req.Scheme, req.Host, req.PathBase);
}
else if (tag.Type == WalletObjectData.Types.Payjoin)
{
model.Tooltip = "This UTXO was part of a PayJoin transaction.";
}
else if (tag.Type == WalletObjectData.Types.Invoice)
{
model.Tooltip = $"Received through an invoice {tag.Id}";
model.Link = string.IsNullOrEmpty(tag.Id)
? null
: _linkGenerator.InvoiceLink(tag.Id, req.Scheme, req.Host, req.PathBase);
}
else if (tag.Type == WalletObjectData.Types.PaymentRequest)
{
model.Tooltip = $"Received through a payment request {tag.Id}";
model.Link = _linkGenerator.PaymentRequestLink(tag.Id, req.Scheme, req.Host, req.PathBase);
}
else if (tag.Type == WalletObjectData.Types.App)
{
model.Tooltip = $"Received through an app {tag.Id}";
model.Link = _linkGenerator.AppLink(tag.Id, req.Scheme, req.Host, req.PathBase);
}
else if (tag.Type == WalletObjectData.Types.PayjoinExposed)
{
if (tag.Id.Length != 0)
{
model.Tooltip = $"This UTXO was exposed through a PayJoin proposal for an invoice ({tag.Id})";
model.Link = _linkGenerator.InvoiceLink(tag.Id, req.Scheme, req.Host, req.PathBase);
}
else
{
model.Tooltip = $"This UTXO was exposed through a PayJoin proposal";
}
}
else if (tag.Type == WalletObjectData.Types.Payjoin)
{
model.Tooltip = $"This UTXO was part of a PayJoin transaction.";
}
else
{
model.Tooltip = tag.Data?.TryGetValue("tooltip", StringComparison.InvariantCultureIgnoreCase, out var tooltip) is true ? tooltip.ToString() : tag.Id;
if (tag.Data?.TryGetValue("link", StringComparison.InvariantCultureIgnoreCase, out var link) is true)
{
model.Link = link.ToString();
}
}
}
foreach (var label in transactionInfo.LabelColors)
models.TryAdd(label.Key, new TransactionTagModel
{
Text = label.Key,
Color = label.Value,
TextColor = ColorPalette.Default.TextColor(label.Value)
});
return models.Values.OrderBy(v => v.Text);
}
}