mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
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:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
140
BTCPayServer/Services/Labels/LabelService.cs
Normal file
140
BTCPayServer/Services/Labels/LabelService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user