mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-21 14:04:12 +01:00
Improve Labeling further (#4849)
* If loading addresses into the send wallet page using bip21 or address, (or clicking on "Send selected payouts" from the payotus page), existing labels will be pre-populated. * Add the payout label to the address when the payoutis created instead of to the transaction when it is paid. * Add the label attachments when adding labels from an address to the transaction.
This commit is contained in:
parent
91faf5756d
commit
892b3e273f
10 changed files with 87 additions and 37 deletions
|
@ -459,14 +459,17 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ix is null) continue;
|
if (ix is null) continue;
|
||||||
|
|
||||||
|
var labels = _labelService.CreateTransactionTagModels(ix, Request);
|
||||||
var input = vm.Inputs.First(model => model.Index == inputToObject.Key);
|
var input = vm.Inputs.First(model => model.Index == inputToObject.Key);
|
||||||
input.Labels = ix.LabelColors;
|
input.Labels = labels;
|
||||||
}
|
}
|
||||||
foreach (var outputToObject in outputToObjects)
|
foreach (var outputToObject in outputToObjects)
|
||||||
{
|
{
|
||||||
if (!labelInfo.TryGetValue(outputToObject.Value.Id, out var ix)) continue;
|
if (!labelInfo.TryGetValue(outputToObject.Value.Id, out var ix)) continue;
|
||||||
|
var labels = _labelService.CreateTransactionTagModels(ix, Request);
|
||||||
var destination = vm.Destinations.First(model => model.Destination == outputToObject.Key);
|
var destination = vm.Destinations.First(model => model.Destination == outputToObject.Key);
|
||||||
destination.Labels = ix.LabelColors;
|
destination.Labels = labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -453,7 +453,7 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(link))
|
if (!string.IsNullOrEmpty(link))
|
||||||
{
|
{
|
||||||
LoadFromBIP21(model, link, network);
|
await LoadFromBIP21(walletId, model, link, network);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -568,7 +568,7 @@ namespace BTCPayServer.Controllers
|
||||||
if (!string.IsNullOrEmpty(bip21))
|
if (!string.IsNullOrEmpty(bip21))
|
||||||
{
|
{
|
||||||
vm.Outputs?.Clear();
|
vm.Outputs?.Clear();
|
||||||
LoadFromBIP21(vm, bip21, network);
|
await LoadFromBIP21(walletId, vm, bip21, network);
|
||||||
}
|
}
|
||||||
|
|
||||||
decimal transactionAmountSum = 0;
|
decimal transactionAmountSum = 0;
|
||||||
|
@ -586,7 +586,7 @@ namespace BTCPayServer.Controllers
|
||||||
.GetUnspentCoins(schemeSettings.AccountDerivation, false, cancellation);
|
.GetUnspentCoins(schemeSettings.AccountDerivation, false, cancellation);
|
||||||
|
|
||||||
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId,
|
var walletTransactionsInfoAsync = await this.WalletRepository.GetWalletTransactionsInfo(walletId,
|
||||||
utxos.SelectMany(u => GetWalletObjectsQuery.Get(u)).Distinct().ToArray());
|
utxos.SelectMany(GetWalletObjectsQuery.Get).Distinct().ToArray());
|
||||||
vm.InputsAvailable = utxos.Select(coin =>
|
vm.InputsAvailable = utxos.Select(coin =>
|
||||||
{
|
{
|
||||||
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info1);
|
walletTransactionsInfoAsync.TryGetValue(coin.OutPoint.Hash.ToString(), out var info1);
|
||||||
|
@ -863,8 +863,10 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void LoadFromBIP21(WalletSendModel vm, string bip21, BTCPayNetwork network)
|
private async Task LoadFromBIP21(WalletId walletId, WalletSendModel vm, string bip21,
|
||||||
|
BTCPayNetwork network)
|
||||||
{
|
{
|
||||||
|
BitcoinAddress? address = null;
|
||||||
vm.Outputs ??= new();
|
vm.Outputs ??= new();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -879,6 +881,7 @@ namespace BTCPayServer.Controllers
|
||||||
? uriBuilder.UnknownParameters["payout"]
|
? uriBuilder.UnknownParameters["payout"]
|
||||||
: null
|
: null
|
||||||
});
|
});
|
||||||
|
address = uriBuilder.Address;
|
||||||
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
|
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
|
||||||
{
|
{
|
||||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||||
|
@ -896,9 +899,10 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
address = BitcoinAddress.Create(bip21, network.NBitcoinNetwork);
|
||||||
vm.Outputs.Add(new WalletSendModel.TransactionOutput()
|
vm.Outputs.Add(new WalletSendModel.TransactionOutput()
|
||||||
{
|
{
|
||||||
DestinationAddress = BitcoinAddress.Create(bip21, network.NBitcoinNetwork).ToString()
|
DestinationAddress = address.ToString()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -913,6 +917,11 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
ModelState.Clear();
|
ModelState.Clear();
|
||||||
|
if (address is not null)
|
||||||
|
{
|
||||||
|
var addressLabels = await WalletRepository.GetWalletLabels(new WalletObjectId(walletId, WalletObjectData.Types.Address, address.ToString()));
|
||||||
|
vm.Outputs.Last().Labels = addressLabels.Select(tuple => tuple.Label).ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IActionResult ViewVault(WalletId walletId, WalletPSBTViewModel vm)
|
private IActionResult ViewVault(WalletId walletId, WalletPSBTViewModel vm)
|
||||||
|
|
|
@ -64,12 +64,19 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
|
||||||
_btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.CryptoCode)?.ReadonlyWallet is false;
|
_btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.CryptoCode)?.ReadonlyWallet is false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task TrackClaim(PaymentMethodId paymentMethodId, IClaimDestination claimDestination)
|
public async Task TrackClaim(ClaimRequest claimRequest, PayoutData payoutData)
|
||||||
{
|
{
|
||||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
|
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(claimRequest.PaymentMethodId.CryptoCode);
|
||||||
var explorerClient = _explorerClientProvider.GetExplorerClient(network);
|
var explorerClient = _explorerClientProvider.GetExplorerClient(network);
|
||||||
if (claimDestination is IBitcoinLikeClaimDestination bitcoinLikeClaimDestination)
|
if (claimRequest.Destination is IBitcoinLikeClaimDestination bitcoinLikeClaimDestination)
|
||||||
|
{
|
||||||
|
|
||||||
await explorerClient.TrackAsync(TrackedSource.Create(bitcoinLikeClaimDestination.Address));
|
await explorerClient.TrackAsync(TrackedSource.Create(bitcoinLikeClaimDestination.Address));
|
||||||
|
await WalletRepository.AddWalletTransactionAttachment(
|
||||||
|
new WalletId(claimRequest.StoreId, claimRequest.PaymentMethodId.CryptoCode),
|
||||||
|
bitcoinLikeClaimDestination.Address.ToString(),
|
||||||
|
Attachment.Payout(payoutData.PullPaymentDataId, payoutData.Id), WalletObjectData.Types.Address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, CancellationToken cancellationToken)
|
public Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, CancellationToken cancellationToken)
|
||||||
|
|
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||||
|
@ -14,7 +15,7 @@ using StoreData = BTCPayServer.Data.StoreData;
|
||||||
public interface IPayoutHandler
|
public interface IPayoutHandler
|
||||||
{
|
{
|
||||||
public bool CanHandle(PaymentMethodId paymentMethod);
|
public bool CanHandle(PaymentMethodId paymentMethod);
|
||||||
public Task TrackClaim(PaymentMethodId paymentMethodId, IClaimDestination claimDestination);
|
public Task TrackClaim(ClaimRequest claimRequest, PayoutData payoutData);
|
||||||
//Allows payout handler to parse payout destinations on its own
|
//Allows payout handler to parse payout destinations on its own
|
||||||
public Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, CancellationToken cancellationToken);
|
public Task<(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination, CancellationToken cancellationToken);
|
||||||
public (bool valid, string? error) ValidateClaimDestination(IClaimDestination claimDestination, PullPaymentBlob? pullPaymentBlob);
|
public (bool valid, string? error) ValidateClaimDestination(IClaimDestination claimDestination, PullPaymentBlob? pullPaymentBlob);
|
||||||
|
|
|
@ -6,6 +6,7 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Models;
|
using BTCPayServer.Abstractions.Models;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Lightning;
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
@ -45,7 +46,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||||
_btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.CryptoCode)?.SupportLightning is true;
|
_btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.CryptoCode)?.SupportLightning is true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task TrackClaim(PaymentMethodId paymentMethodId, IClaimDestination claimDestination)
|
public Task TrackClaim(ClaimRequest claimRequest, PayoutData payoutData)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
|
@ -592,7 +592,7 @@ namespace BTCPayServer.HostedServices
|
||||||
await ctx.Payouts.AddAsync(payout);
|
await ctx.Payouts.AddAsync(payout);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await payoutHandler.TrackClaim(req.ClaimRequest.PaymentMethodId, req.ClaimRequest.Destination);
|
await payoutHandler.TrackClaim(req.ClaimRequest, payout);
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
if (req.ClaimRequest.PreApprove.GetValueOrDefault(ppBlob?.AutoApproveClaims is true))
|
if (req.ClaimRequest.PreApprove.GetValueOrDefault(ppBlob?.AutoApproveClaims is true))
|
||||||
{
|
{
|
||||||
|
|
|
@ -80,12 +80,18 @@ namespace BTCPayServer.HostedServices
|
||||||
//if the object is an address, we also link the labels to the tx
|
//if the object is an address, we also link the labels to the tx
|
||||||
if(walletObjectData.Value.Type == WalletObjectData.Types.Address)
|
if(walletObjectData.Value.Type == WalletObjectData.Types.Address)
|
||||||
{
|
{
|
||||||
var labels = walletObjectData.Value.GetNeighbours()
|
var neighbours = walletObjectData.Value.GetNeighbours().ToArray();
|
||||||
|
var labels = neighbours
|
||||||
.Where(data => data.Type == WalletObjectData.Types.Label).Select(data =>
|
.Where(data => data.Type == WalletObjectData.Types.Label).Select(data =>
|
||||||
new WalletObjectId(walletObjectDatas.Key, data.Type, data.Id));
|
new WalletObjectId(walletObjectDatas.Key, data.Type, data.Id));
|
||||||
foreach (var label in labels)
|
foreach (var label in labels)
|
||||||
{
|
{
|
||||||
await _walletRepository.EnsureWalletObjectLink(label, txWalletObject);
|
await _walletRepository.EnsureWalletObjectLink(label, txWalletObject);
|
||||||
|
var attachments = neighbours.Where(data => data.Type == label.Id);
|
||||||
|
foreach (var attachment in attachments)
|
||||||
|
{
|
||||||
|
await _walletRepository.EnsureWalletObjectLink(new WalletObjectId(walletObjectDatas.Key, attachment.Type, attachment.Id), txWalletObject);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,14 +109,8 @@ namespace BTCPayServer.HostedServices
|
||||||
{
|
{
|
||||||
Attachment.Invoice(invoiceEvent.Invoice.Id)
|
Attachment.Invoice(invoiceEvent.Invoice.Id)
|
||||||
};
|
};
|
||||||
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
|
labels.AddRange(PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice).Select(Attachment.PaymentRequest));
|
||||||
{
|
labels.AddRange(AppService.GetAppInternalTags(invoiceEvent.Invoice).Select(Attachment.App));
|
||||||
labels.Add(Attachment.PaymentRequest(paymentId));
|
|
||||||
}
|
|
||||||
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice))
|
|
||||||
{
|
|
||||||
labels.Add(Attachment.App(appId));
|
|
||||||
}
|
|
||||||
|
|
||||||
await _walletRepository.AddWalletTransactionAttachment(walletId, transactionId, labels);
|
await _walletRepository.AddWalletTransactionAttachment(walletId, transactionId, labels);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -16,7 +16,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||||
public bool Positive { get; set; }
|
public bool Positive { get; set; }
|
||||||
public string Destination { get; set; }
|
public string Destination { get; set; }
|
||||||
public string Balance { get; set; }
|
public string Balance { get; set; }
|
||||||
public Dictionary<string, string> Labels { get; set; } = new();
|
public IEnumerable<TransactionTagModel> Labels { get; set; } = new List<TransactionTagModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class InputViewModel
|
public class InputViewModel
|
||||||
|
@ -25,7 +25,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||||
public string Error { get; set; }
|
public string Error { get; set; }
|
||||||
public bool Positive { get; set; }
|
public bool Positive { get; set; }
|
||||||
public string BalanceChange { get; set; }
|
public string BalanceChange { get; set; }
|
||||||
public Dictionary<string, string> Labels { get; set; } = new();
|
public IEnumerable<TransactionTagModel> Labels { get; set; } = new List<TransactionTagModel>();
|
||||||
}
|
}
|
||||||
public bool HasErrors => Inputs.Count == 0 || Inputs.Any(i => !string.IsNullOrEmpty(i.Error));
|
public bool HasErrors => Inputs.Count == 0 || Inputs.Any(i => !string.IsNullOrEmpty(i.Error));
|
||||||
public string BalanceChange { get; set; }
|
public string BalanceChange { get; set; }
|
||||||
|
|
|
@ -318,10 +318,10 @@ namespace BTCPayServer.Services
|
||||||
|
|
||||||
public async Task<(string Label, string Color)[]> GetWalletLabels(WalletObjectId objectId)
|
public async Task<(string Label, string Color)[]> GetWalletLabels(WalletObjectId objectId)
|
||||||
{
|
{
|
||||||
return await GetWalletLabels(w =>
|
|
||||||
w.WalletId == objectId.WalletId.ToString() &&
|
await using var ctx = _ContextFactory.CreateContext();
|
||||||
w.Type == objectId.Type &&
|
var obj = await GetWalletObject(objectId, true);
|
||||||
w.Id == objectId.Id);
|
return obj is null ? Array.Empty<(string Label, string Color)>() : obj.GetNeighbours().Where(data => data.Type == WalletObjectData.Types.Label).Select(FormatToLabel).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(string Label, string Color)[]> GetWalletLabels(Expression<Func<WalletObjectData, bool>> predicate)
|
private async Task<(string Label, string Color)[]> GetWalletLabels(Expression<Func<WalletObjectData, bool>> predicate)
|
||||||
|
|
|
@ -35,10 +35,25 @@
|
||||||
{
|
{
|
||||||
<td>@input.Index</td>
|
<td>@input.Index</td>
|
||||||
}<td>
|
}<td>
|
||||||
@foreach (var label in input.Labels)
|
@if (input.Labels.Any())
|
||||||
{
|
{
|
||||||
<div class="transaction-label" style="--label-bg:@label.Value;--label-fg:@ColorPalette.Default.TextColor(label.Value)">@label.Key</div>
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||||
}
|
@foreach (var label in input.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>
|
</td>
|
||||||
<td class="text-end text-@(input.Positive ? "success" : "danger")">@input.BalanceChange</td>
|
<td class="text-end text-@(input.Positive ? "success" : "danger")">@input.BalanceChange</td>
|
||||||
|
|
||||||
|
@ -64,11 +79,25 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-break">@destination.Destination</td>
|
<td class="text-break">@destination.Destination</td>
|
||||||
<td>
|
<td>
|
||||||
|
@if (destination.Labels.Any())
|
||||||
@foreach (var label in destination.Labels)
|
{
|
||||||
{
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||||
<div class="transaction-label" style="--label-bg:@label.Value;--label-fg:@ColorPalette.Default.TextColor(label.Value)">@label.Key</div>
|
@foreach (var label in destination.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>
|
</td>
|
||||||
<td class="text-end text-@(destination.Positive ? "success" : "danger")">@destination.Balance</td>
|
<td class="text-end text-@(destination.Positive ? "success" : "danger")">@destination.Balance</td>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue