Refactor label implementation (Fix #2090) (#2123)

This commit is contained in:
Nicolas Dorier
2020-12-12 14:10:47 +09:00
committed by GitHub
parent b2855e74ef
commit 0d144d088e
13 changed files with 385 additions and 167 deletions

View File

@@ -26,11 +26,4 @@ namespace BTCPayServer.Data
.WithMany(w => w.WalletTransactions).OnDelete(DeleteBehavior.Cascade); .WithMany(w => w.WalletTransactions).OnDelete(DeleteBehavior.Cascade);
} }
} }
public class WalletTransactionInfo
{
public string Comment { get; set; } = string.Empty;
[JsonIgnore]
public HashSet<string> Labels { get; set; } = new HashSet<string>();
}
} }

View File

@@ -35,6 +35,7 @@ using BTCPayServer.Security.Bitpay;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Mails; using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Tests.Logging; using BTCPayServer.Tests.Logging;
@@ -585,6 +586,95 @@ namespace BTCPayServer.Tests
} }
} }
[Fact]
[Trait("Fast", "Fast")]
public void CanParseLegacyLabels()
{
static void AssertContainsRawLabel(WalletTransactionInfo info)
{
foreach (var item in new[] { "blah", "lol", "hello" })
{
Assert.True(info.Labels.ContainsKey(item));
var rawLabel = Assert.IsType<RawLabel>(info.Labels[item]);
Assert.Equal("raw", rawLabel.Type);
Assert.Equal(item, rawLabel.Text);
}
}
var data = new WalletTransactionData();
data.Labels = "blah,lol,hello,lol";
var info = data.GetBlobInfo();
Assert.Equal(3, info.Labels.Count);
AssertContainsRawLabel(info);
data.SetBlobInfo(info);
Assert.Contains("raw", data.Labels);
Assert.Contains("{", data.Labels);
Assert.Contains("[", data.Labels);
info = data.GetBlobInfo();
AssertContainsRawLabel(info);
data = new WalletTransactionData()
{
Labels = "pos",
Blob = Encoders.Hex.DecodeData("1f8b08000000000000037abf7b7fb592737e6e6e6a5e89929592522d000000ffff030036bc6ad911000000")
};
info = data.GetBlobInfo();
var label = Assert.Single(info.Labels);
Assert.Equal("raw", label.Value.Type);
Assert.Equal("pos", label.Value.Text);
Assert.Equal("pos", label.Key);
static void AssertContainsLabel(WalletTransactionInfo info)
{
Assert.Equal(2, info.Labels.Count);
var invoiceLabel = Assert.IsType<ReferenceLabel>(info.Labels["invoice"]);
Assert.Equal("BFm1MCJPBCDeRoWXvPcwnM", invoiceLabel.Reference);
Assert.Equal("invoice", invoiceLabel.Text);
Assert.Equal("invoice", invoiceLabel.Type);
var appLabel = Assert.IsType<ReferenceLabel>(info.Labels["app"]);
Assert.Equal("87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe", appLabel.Reference);
Assert.Equal("app", appLabel.Text);
Assert.Equal("app", appLabel.Type);
}
data = new WalletTransactionData()
{
Labels = "[\"{\\n \\\"value\\\": \\\"invoice\\\",\\n \\\"id\\\": \\\"BFm1MCJPBCDeRoWXvPcwnM\\\"\\n}\",\"{\\n \\\"value\\\": \\\"app\\\",\\n \\\"id\\\": \\\"87kj5yKay8mB4UUZcJhZH5TqDKMD3CznjwLjiu1oYZXe\\\"\\n}\"]",
};
info = data.GetBlobInfo();
AssertContainsLabel(info);
data.SetBlobInfo(info);
info = data.GetBlobInfo();
AssertContainsLabel(info);
static void AssertPayoutLabel(WalletTransactionInfo info)
{
Assert.Single(info.Labels);
var l = Assert.IsType<PayoutLabel>(info.Labels["payout"]);
Assert.Equal("pullPaymentId", l.PullPaymentId);
Assert.Equal("walletId", l.WalletId);
Assert.Equal("payoutId", l.PayoutId);
}
var payoutId = "payoutId";
var pullPaymentId = "pullPaymentId";
var walletId = "walletId";
// How it was serialized before
data = new WalletTransactionData()
{
Labels = new JArray(JObject.FromObject(new { value = "payout", id = payoutId, pullPaymentId, walletId })).ToString()
};
info = data.GetBlobInfo();
AssertPayoutLabel(info);
data.SetBlobInfo(info);
info = data.GetBlobInfo();
AssertPayoutLabel(info);
}
[Fact] [Fact]
[Trait("Fast", "Fast")] [Trait("Fast", "Fast")]
public void DeterministicUTXOSorter() public void DeterministicUTXOSorter()
@@ -1234,8 +1324,8 @@ namespace BTCPayServer.Tests
tx = Assert.Single(transactions.Transactions); tx = Assert.Single(transactions.Transactions);
Assert.Equal("hello", tx.Comment); Assert.Equal("hello", tx.Comment);
Assert.Contains("test", tx.Labels.Select(l => l.Value)); Assert.Contains("test", tx.Labels.Select(l => l.Text));
Assert.Contains("test2", tx.Labels.Select(l => l.Value)); Assert.Contains("test2", tx.Labels.Select(l => l.Text));
Assert.Equal(2, tx.Labels.GroupBy(l => l.Color).Count()); Assert.Equal(2, tx.Labels.GroupBy(l => l.Color).Count());
Assert.IsType<RedirectToActionResult>( Assert.IsType<RedirectToActionResult>(
@@ -1246,8 +1336,8 @@ namespace BTCPayServer.Tests
tx = Assert.Single(transactions.Transactions); tx = Assert.Single(transactions.Transactions);
Assert.Equal("hello", tx.Comment); Assert.Equal("hello", tx.Comment);
Assert.Contains("test", tx.Labels.Select(l => l.Value)); Assert.Contains("test", tx.Labels.Select(l => l.Text));
Assert.DoesNotContain("test2", tx.Labels.Select(l => l.Value)); Assert.DoesNotContain("test2", tx.Labels.Select(l => l.Text));
Assert.Single(tx.Labels.GroupBy(l => l.Color)); Assert.Single(tx.Labels.GroupBy(l => l.Color));
var walletInfo = await tester.PayTester.GetService<WalletRepository>().GetWalletInfo(walletId); var walletInfo = await tester.PayTester.GetService<WalletRepository>().GetWalletInfo(walletId);

View File

@@ -159,12 +159,12 @@ namespace BTCPayServer.Controllers
if (addlabel != null) if (addlabel != null)
{ {
addlabel = addlabel.Trim().TrimStart('{').ToLowerInvariant().Replace(',', ' ').Truncate(MaxLabelSize); addlabel = addlabel.Trim().TrimStart('{').ToLowerInvariant().Replace(',', ' ').Truncate(MaxLabelSize);
var labels = _labelFactory.GetLabels(walletBlobInfo, Request); var labels = _labelFactory.GetWalletColoredLabels(walletBlobInfo, Request);
if (!walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo)) if (!walletTransactionsInfo.TryGetValue(transactionId, out var walletTransactionInfo))
{ {
walletTransactionInfo = new WalletTransactionInfo(); walletTransactionInfo = new WalletTransactionInfo();
} }
if (!labels.Any(l => l.Value.Equals(addlabel, StringComparison.OrdinalIgnoreCase))) if (!labels.Any(l => l.Text.Equals(addlabel, StringComparison.OrdinalIgnoreCase)))
{ {
List<string> allColors = new List<string>(); List<string> allColors = new List<string>();
allColors.AddRange(LabelColorScheme); allColors.AddRange(LabelColorScheme);
@@ -183,7 +183,8 @@ namespace BTCPayServer.Controllers
walletBlobInfo.LabelColors.Add(addlabel, chosenColor); walletBlobInfo.LabelColors.Add(addlabel, chosenColor);
await WalletRepository.SetWalletInfo(walletId, walletBlobInfo); await WalletRepository.SetWalletInfo(walletId, walletBlobInfo);
} }
if (walletTransactionInfo.Labels.Add(addlabel)) var rawLabel = new RawLabel(addlabel);
if (walletTransactionInfo.Labels.TryAdd(rawLabel.Text, rawLabel))
{ {
await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo); await WalletRepository.SetWalletTransactionInfo(walletId, transactionId, walletTransactionInfo);
} }
@@ -195,8 +196,8 @@ namespace BTCPayServer.Controllers
{ {
if (walletTransactionInfo.Labels.Remove(removelabel)) if (walletTransactionInfo.Labels.Remove(removelabel))
{ {
var canDelete = !walletTransactionsInfo.SelectMany(txi => txi.Value.Labels).Any(l => l == removelabel); var canDeleteColor = !walletTransactionsInfo.Any(txi => txi.Value.Labels.ContainsKey(removelabel));
if (canDelete) if (canDeleteColor)
{ {
walletBlobInfo.LabelColors.Remove(removelabel); walletBlobInfo.LabelColors.Remove(removelabel);
await WalletRepository.SetWalletInfo(walletId, walletBlobInfo); await WalletRepository.SetWalletInfo(walletId, walletBlobInfo);
@@ -315,14 +316,14 @@ namespace BTCPayServer.Controllers
if (walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo)) if (walletTransactionsInfo.TryGetValue(tx.TransactionId.ToString(), out var transactionInfo))
{ {
var labels = _labelFactory.GetLabels(walletBlob, transactionInfo, Request); var labels = _labelFactory.ColorizeTransactionLabels(walletBlob, transactionInfo, Request);
vm.Labels.AddRange(labels); vm.Labels.AddRange(labels);
model.Labels.AddRange(labels); model.Labels.AddRange(labels);
vm.Comment = transactionInfo.Comment; vm.Comment = transactionInfo.Comment;
} }
if (labelFilter == null || if (labelFilter == null ||
vm.Labels.Any(l => l.Value.Equals(labelFilter, StringComparison.OrdinalIgnoreCase))) vm.Labels.Any(l => l.Text.Equals(labelFilter, StringComparison.OrdinalIgnoreCase)))
model.Transactions.Add(vm); model.Transactions.Add(vm);
} }
@@ -547,7 +548,7 @@ namespace BTCPayServer.Controllers
Outpoint = coin.OutPoint.ToString(), Outpoint = coin.OutPoint.ToString(),
Amount = coin.Value.GetValue(network), Amount = coin.Value.GetValue(network),
Comment = info?.Comment, Comment = info?.Comment,
Labels = info == null ? null : _labelFactory.GetLabels(walletBlobAsync, info, Request), Labels = info == null ? null : _labelFactory.ColorizeTransactionLabels(walletBlobAsync, info, Request),
Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, coin.OutPoint.Hash.ToString()) Link = string.Format(CultureInfo.InvariantCulture, network.BlockExplorerLink, coin.OutPoint.Hash.ToString())
}; };
}).ToArray(); }).ToArray();

View File

@@ -1,32 +1,57 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Services.Labels;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
public class WalletTransactionInfo
{
public string Comment { get; set; } = string.Empty;
[JsonIgnore]
public Dictionary<string, Label> Labels { get; set; } = new Dictionary<string, Label>();
}
public static class WalletTransactionDataExtensions public static class WalletTransactionDataExtensions
{ {
public static WalletTransactionInfo GetBlobInfo(this WalletTransactionData walletTransactionData) public static WalletTransactionInfo GetBlobInfo(this WalletTransactionData walletTransactionData)
{ {
WalletTransactionInfo blobInfo;
if (walletTransactionData.Blob == null || walletTransactionData.Blob.Length == 0) if (walletTransactionData.Blob == null || walletTransactionData.Blob.Length == 0)
{ blobInfo = new WalletTransactionInfo();
return new WalletTransactionInfo(); else
} blobInfo = JsonConvert.DeserializeObject<WalletTransactionInfo>(ZipUtils.Unzip(walletTransactionData.Blob));
var blobInfo = JsonConvert.DeserializeObject<WalletTransactionInfo>(ZipUtils.Unzip(walletTransactionData.Blob));
if (!string.IsNullOrEmpty(walletTransactionData.Labels)) if (!string.IsNullOrEmpty(walletTransactionData.Labels))
{ {
if (walletTransactionData.Labels.StartsWith('[')) if (walletTransactionData.Labels.StartsWith('['))
{ {
blobInfo.Labels.AddRange(JArray.Parse(walletTransactionData.Labels).Values<string>()); foreach (var jtoken in JArray.Parse(walletTransactionData.Labels))
{
var l = jtoken.Type == JTokenType.String ? Label.Parse(jtoken.Value<string>())
: Label.Parse(jtoken.ToString());
blobInfo.Labels.TryAdd(l.Text, l);
}
} }
else else
{ {
blobInfo.Labels.AddRange(walletTransactionData.Labels.Split(',', // Legacy path
StringSplitOptions.RemoveEmptyEntries)); foreach (var token in walletTransactionData.Labels.Split(',',
StringSplitOptions.RemoveEmptyEntries))
{
var l = Label.Parse(token);
blobInfo.Labels.TryAdd(l.Text, l);
}
} }
} }
return blobInfo; return blobInfo;
} }
static JsonSerializerSettings LabelSerializerSettings = new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Formatting = Formatting.None
};
public static void SetBlobInfo(this WalletTransactionData walletTransactionData, WalletTransactionInfo blobInfo) public static void SetBlobInfo(this WalletTransactionData walletTransactionData, WalletTransactionInfo blobInfo)
{ {
if (blobInfo == null) if (blobInfo == null)
@@ -35,8 +60,11 @@ namespace BTCPayServer.Data
walletTransactionData.Blob = Array.Empty<byte>(); walletTransactionData.Blob = Array.Empty<byte>();
return; return;
} }
walletTransactionData.Labels = new JArray(
walletTransactionData.Labels = JArray.FromObject(blobInfo.Labels).ToString(); blobInfo.Labels.Select(l => JsonConvert.SerializeObject(l.Value, LabelSerializerSettings))
.Select(l => JObject.Parse(l))
.OfType<JToken>()
.ToArray()).ToString();
walletTransactionData.Blob = ZipUtils.Zip(JsonConvert.SerializeObject(blobInfo)); walletTransactionData.Blob = ZipUtils.Zip(JsonConvert.SerializeObject(blobInfo));
} }
} }

View File

@@ -9,6 +9,7 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.PaymentRequests; using BTCPayServer.Services.PaymentRequests;
using NBitcoin; using NBitcoin;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@@ -40,7 +41,7 @@ namespace BTCPayServer.HostedServices
{ {
var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode()); var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode());
var transactionId = bitcoinLikePaymentData.Outpoint.Hash; var transactionId = bitcoinLikePaymentData.Outpoint.Hash;
var labels = new List<(string color, string label)> var labels = new List<(string color, Label label)>
{ {
UpdateTransactionLabel.InvoiceLabelTemplate(invoiceEvent.Invoice.Id) UpdateTransactionLabel.InvoiceLabelTemplate(invoiceEvent.Invoice.Id)
}; };
@@ -79,14 +80,14 @@ namespace BTCPayServer.HostedServices
foreach (var label in pair.Value) foreach (var label in pair.Value)
{ {
walletBlobInfo.LabelColors.TryAdd(label.label, label.color); walletBlobInfo.LabelColors.TryAdd(label.label.Text, label.color);
} }
await _walletRepository.SetWalletInfo(updateTransactionLabel.WalletId, walletBlobInfo); await _walletRepository.SetWalletInfo(updateTransactionLabel.WalletId, walletBlobInfo);
var update = false; var update = false;
foreach (var label in pair.Value) foreach (var label in pair.Value)
{ {
if (walletTransactionInfo.Labels.Add(label.label)) if (walletTransactionInfo.Labels.TryAdd(label.label.Text, label.label))
{ {
update = true; update = true;
} }
@@ -108,47 +109,52 @@ namespace BTCPayServer.HostedServices
{ {
} }
public UpdateTransactionLabel(WalletId walletId, uint256 txId, (string color, string label) colorLabel) public UpdateTransactionLabel(WalletId walletId, uint256 txId, (string color, Label label) colorLabel)
{ {
WalletId = walletId; WalletId = walletId;
TransactionLabels = new Dictionary<uint256, List<(string color, string label)>>(); TransactionLabels = new Dictionary<uint256, List<(string color, Label label)>>();
TransactionLabels.Add(txId, new List<(string color, string label)>() { colorLabel }); TransactionLabels.Add(txId, new List<(string color, Label label)>() { colorLabel });
} }
public UpdateTransactionLabel(WalletId walletId, uint256 txId, List<(string color, string label)> colorLabels) public UpdateTransactionLabel(WalletId walletId, uint256 txId, List<(string color, Label label)> colorLabels)
{ {
WalletId = walletId; WalletId = walletId;
TransactionLabels = new Dictionary<uint256, List<(string color, string label)>>(); TransactionLabels = new Dictionary<uint256, List<(string color, Label label)>>();
TransactionLabels.Add(txId, colorLabels); TransactionLabels.Add(txId, colorLabels);
} }
public static (string color, string label) PayjoinLabelTemplate() public static (string color, Label label) PayjoinLabelTemplate()
{ {
return ("#51b13e", "payjoin"); return ("#51b13e", new RawLabel("payjoin"));
} }
public static (string color, string label) InvoiceLabelTemplate(string invoice) public static (string color, Label label) InvoiceLabelTemplate(string invoice)
{ {
return ("#cedc21", JObject.FromObject(new { value = "invoice", id = invoice }).ToString()); return ("#cedc21", new ReferenceLabel("invoice", invoice));
} }
public static (string color, string label) PaymentRequestLabelTemplate(string paymentRequestId) public static (string color, Label label) PaymentRequestLabelTemplate(string paymentRequestId)
{ {
return ("#489D77", JObject.FromObject(new { value = "payment-request", id = paymentRequestId }).ToString()); return ("#489D77", new ReferenceLabel("payment-request", paymentRequestId));
} }
public static (string color, string label) AppLabelTemplate(string appId) public static (string color, Label label) AppLabelTemplate(string appId)
{ {
return ("#5093B6", JObject.FromObject(new { value = "app", id = appId }).ToString()); return ("#5093B6", new ReferenceLabel("app", appId));
} }
public static (string color, string label) PayjoinExposedLabelTemplate(string invoice) public static (string color, Label label) PayjoinExposedLabelTemplate(string invoice)
{ {
return ("#51b13e", JObject.FromObject(new { value = "pj-exposed", id = invoice }).ToString()); return ("#51b13e", new ReferenceLabel("pj-exposed", invoice));
} }
public static (string color, string label) PayoutTemplate(string payoutId, string pullPaymentId, string walletId) public static (string color, Label label) PayoutTemplate(string payoutId, string pullPaymentId, string walletId)
{ {
return ("#3F88AF", JObject.FromObject(new { value = "payout", id = payoutId, pullPaymentId, walletId }).ToString()); return ("#3F88AF", new PayoutLabel()
{
PayoutId = payoutId,
PullPaymentId = pullPaymentId,
WalletId = walletId
});
} }
public WalletId WalletId { get; set; } public WalletId WalletId { get; set; }
public Dictionary<uint256, List<(string color, string label)>> TransactionLabels { get; set; } public Dictionary<uint256, List<(string color, Label label)>> TransactionLabels { get; set; }
public override string ToString() public override string ToString()
{ {
var result = new StringBuilder(); var result = new StringBuilder();

View File

@@ -15,9 +15,9 @@ namespace BTCPayServer.Models.WalletViewModels
public string Link { get; set; } public string Link { get; set; }
public bool Positive { get; set; } public bool Positive { get; set; }
public string Balance { get; set; } public string Balance { get; set; }
public HashSet<Label> Labels { get; set; } = new HashSet<Label>(); public HashSet<ColoredLabel> Labels { get; set; } = new HashSet<ColoredLabel>();
} }
public HashSet<Label> Labels { get; set; } = new HashSet<Label>(); public HashSet<ColoredLabel> Labels { get; set; } = new HashSet<ColoredLabel>();
public List<TransactionViewModel> Transactions { get; set; } = new List<TransactionViewModel>(); public List<TransactionViewModel> Transactions { get; set; } = new List<TransactionViewModel>();
public int Skip { get; set; } public int Skip { get; set; }
public int Count { get; set; } public int Count { get; set; }

View File

@@ -68,7 +68,7 @@ namespace BTCPayServer.Models.WalletViewModels
public class InputSelectionOption public class InputSelectionOption
{ {
public IEnumerable<Label> Labels { get; set; } public IEnumerable<ColoredLabel> Labels { get; set; }
public string Comment { get; set; } public string Comment { get; set; }
public decimal Amount { get; set; } public decimal Amount { get; set; }
public string Outpoint { get; set; } public string Outpoint { get; set; }

View File

@@ -12,6 +12,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Labels;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets; using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
@@ -462,8 +463,8 @@ namespace BTCPayServer.Payments.PayJoin
{ {
WalletId = new WalletId(invoice.StoreId, network.CryptoCode), WalletId = new WalletId(invoice.StoreId, network.CryptoCode),
TransactionLabels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo => TransactionLabels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo =>
new KeyValuePair<uint256, List<(string color, string label)>>(utxo.Key, new KeyValuePair<uint256, List<(string color, Label label)>>(utxo.Key,
new List<(string color, string label)>() new List<(string color, Label label)>()
{ {
UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice.Id) UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice.Id)
})) }))

View File

@@ -0,0 +1,43 @@
using System;
namespace BTCPayServer.Services.Labels
{
public class ColoredLabel
{
internal ColoredLabel()
{
}
public string Text { get; internal set; }
public string Color { get; internal set; }
public string Link { get; internal set; }
public string Tooltip { get; internal set; }
public override bool Equals(object obj)
{
ColoredLabel item = obj as ColoredLabel;
if (item == null)
return false;
return Text.Equals(item.Text, StringComparison.OrdinalIgnoreCase);
}
public static bool operator ==(ColoredLabel a, ColoredLabel b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.Text == b.Text;
}
public static bool operator !=(ColoredLabel a, ColoredLabel b)
{
return !(a == b);
}
public override int GetHashCode()
{
return Text.GetHashCode(StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@@ -1,44 +1,124 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Labels namespace BTCPayServer.Services.Labels
{ {
public class Label public abstract class Label
{ {
internal Label() public string Type { get; set; }
public string Text { get; set; }
static void FixLegacy(JObject jObj, ReferenceLabel refLabel)
{ {
if (refLabel.Reference is null)
refLabel.Reference = jObj["id"].Value<string>();
FixLegacy(jObj, (Label)refLabel);
} }
static void FixLegacy(JObject jObj, PayoutLabel payoutLabel)
public string Value { get; internal set; }
public string RawValue { get; internal set; }
public string Color { get; internal set; }
public string Link { get; internal set; }
public string Tooltip { get; internal set; }
public override bool Equals(object obj)
{ {
Label item = obj as Label; if (payoutLabel.PayoutId is null)
if (item == null) payoutLabel.PayoutId = jObj["id"].Value<string>();
return false; FixLegacy(jObj, (Label)payoutLabel);
return Value.Equals(item.Value, StringComparison.OrdinalIgnoreCase);
} }
static void FixLegacy(JObject jObj, Label label)
public static bool operator ==(Label a, Label b)
{ {
if (System.Object.ReferenceEquals(a, b)) if (label.Type is null)
return true; label.Type = jObj["value"].Value<string>();
if (((object)a == null) || ((object)b == null)) if (label.Text is null)
return false; label.Text = label.Type;
return a.Value == b.Value;
} }
static void FixLegacy(JObject jObj, RawLabel rawLabel)
public static bool operator !=(Label a, Label b)
{ {
return !(a == b); rawLabel.Type = "raw";
FixLegacy(jObj, (Label)rawLabel);
} }
public static Label Parse(string str)
public override int GetHashCode()
{ {
return Value.GetHashCode(StringComparison.OrdinalIgnoreCase); if (str == null)
throw new ArgumentNullException(nameof(str));
if (str.StartsWith("{", StringComparison.InvariantCultureIgnoreCase))
{
var jObj = JObject.Parse(str);
string type = null;
// Legacy label
if (!jObj.ContainsKey("type"))
{
type = jObj["value"].Value<string>();
}
else
{
type = jObj["type"].Value<string>();
}
switch (type)
{
case "raw":
var rawLabel = JsonConvert.DeserializeObject<RawLabel>(str);
FixLegacy(jObj, rawLabel);
return rawLabel;
case "invoice":
case "payment-request":
case "app":
case "pj-exposed":
var refLabel = JsonConvert.DeserializeObject<ReferenceLabel>(str);
FixLegacy(jObj, refLabel);
return refLabel;
case "payout":
var payoutLabel = JsonConvert.DeserializeObject<PayoutLabel>(str);
FixLegacy(jObj, payoutLabel);
return payoutLabel;
default:
// Legacy
return new RawLabel(jObj["value"].Value<string>());
}
}
else
{
return new RawLabel(str);
}
}
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; }
}
public class RawLabel : Label
{
public RawLabel()
{
Type = "raw";
}
public RawLabel(string text) : this()
{
Text = text;
} }
} }
public class ReferenceLabel : Label
{
public ReferenceLabel()
{
}
public ReferenceLabel(string type, string reference)
{
Text = type;
Reference = reference;
Type = type;
}
[JsonProperty("ref")]
public string Reference { get; set; }
}
public class PayoutLabel : Label
{
public PayoutLabel()
{
Type = "payout";
Text = "payout";
}
public string PayoutId { get; set; }
public string WalletId { get; set; }
public string PullPaymentId { get; set; }
}
} }

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Amazon.Util.Internal.PlatformServices;
using BTCPayServer.Data; using BTCPayServer.Data;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -17,104 +18,79 @@ namespace BTCPayServer.Services.Labels
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
} }
public IEnumerable<Label> GetLabels(WalletBlobInfo walletBlobInfo, WalletTransactionInfo transactionInfo, public IEnumerable<ColoredLabel> ColorizeTransactionLabels(WalletBlobInfo walletBlobInfo, WalletTransactionInfo transactionInfo,
HttpRequest request) HttpRequest request)
{ {
foreach (var label in transactionInfo.Labels) foreach (var label in transactionInfo.Labels)
{ {
if (walletBlobInfo.LabelColors.TryGetValue(label, out var color)) if (walletBlobInfo.LabelColors.TryGetValue(label.Value.Text, out var color))
{ {
yield return CreateLabel(label, color, request); yield return CreateLabel(label.Value, color, request);
} }
} }
} }
public IEnumerable<Label> GetLabels(WalletBlobInfo walletBlobInfo, HttpRequest request) public IEnumerable<ColoredLabel> GetWalletColoredLabels(WalletBlobInfo walletBlobInfo, HttpRequest request)
{ {
foreach (var kv in walletBlobInfo.LabelColors) foreach (var kv in walletBlobInfo.LabelColors)
{ {
yield return CreateLabel(kv.Key, kv.Value, request); yield return CreateLabel(new RawLabel() { Text = kv.Key }, kv.Value, request);
} }
} }
private Label CreateLabel(string value, string color, HttpRequest request) private ColoredLabel CreateLabel(Label uncoloredLabel, string color, HttpRequest request)
{ {
if (value == null) if (uncoloredLabel == null)
throw new ArgumentNullException(nameof(value)); throw new ArgumentNullException(nameof(uncoloredLabel));
if (color == null) if (color == null)
throw new ArgumentNullException(nameof(color)); throw new ArgumentNullException(nameof(color));
if (value.StartsWith("{", StringComparison.InvariantCultureIgnoreCase))
ColoredLabel coloredLabel = new ColoredLabel()
{ {
var jObj = JObject.Parse(value); Text = uncoloredLabel.Text,
if (jObj.ContainsKey("value")) Color = color
};
if (uncoloredLabel is ReferenceLabel refLabel)
{
var refInLabel = string.IsNullOrEmpty(refLabel.Reference) ? string.Empty : $"({refLabel.Reference})";
switch (uncoloredLabel.Type)
{ {
var id = jObj.ContainsKey("id") ? jObj["id"].Value<string>() : string.Empty; case "invoice":
var idInLabel = string.IsNullOrEmpty(id) ? string.Empty : $"({id})"; coloredLabel.Tooltip = $"Received through an invoice {refInLabel}";
coloredLabel.Link = string.IsNullOrEmpty(refLabel.Reference)
switch (jObj["value"].Value<string>()) ? null
{ : _linkGenerator.InvoiceLink(refLabel.Reference, request.Scheme, request.Host, request.PathBase);
case "invoice": break;
return new Label() case "payment-request":
{ coloredLabel.Tooltip = $"Received through a payment request {refInLabel}";
RawValue = value, coloredLabel.Link = string.IsNullOrEmpty(refLabel.Reference)
Value = "invoice", ? null
Color = color, : _linkGenerator.PaymentRequestLink(refLabel.Reference, request.Scheme, request.Host, request.PathBase);
Tooltip = $"Received through an invoice {idInLabel}", break;
Link = string.IsNullOrEmpty(id) case "app":
? null coloredLabel.Tooltip = $"Received through an app {refInLabel}";
: _linkGenerator.InvoiceLink(id, request.Scheme, request.Host, request.PathBase) coloredLabel.Link = string.IsNullOrEmpty(refLabel.Reference)
}; ? null
case "payment-request": : _linkGenerator.AppLink(refLabel.Reference, request.Scheme, request.Host, request.PathBase);
return new Label() break;
{ case "pj-exposed":
RawValue = value, coloredLabel.Tooltip = $"This utxo was exposed through a payjoin proposal for an invoice {refInLabel}";
Value = "payment-request", coloredLabel.Link = string.IsNullOrEmpty(refLabel.Reference)
Color = color, ? null
Tooltip = $"Received through a payment request {idInLabel}", : _linkGenerator.InvoiceLink(refLabel.Reference, request.Scheme, request.Host, request.PathBase);
Link = string.IsNullOrEmpty(id) break;
? null
: _linkGenerator.PaymentRequestLink(id, request.Scheme, request.Host, request.PathBase)
};
case "app":
return new Label()
{
RawValue = value,
Value = "app",
Color = color,
Tooltip = $"Received through an app {idInLabel}",
Link = string.IsNullOrEmpty(id)
? null
: _linkGenerator.AppLink(id, request.Scheme, request.Host, request.PathBase)
};
case "pj-exposed":
return new Label()
{
RawValue = value,
Value = "payjoin-exposed",
Color = color,
Tooltip = $"This utxo was exposed through a payjoin proposal for an invoice {idInLabel}",
Link = string.IsNullOrEmpty(id)
? null
: _linkGenerator.InvoiceLink(id, request.Scheme, request.Host, request.PathBase)
};
case "payout":
return new Label()
{
RawValue = value,
Value = "payout",
Color = color,
Tooltip = $"Paid a payout of a pull payment ({jObj["pullPaymentId"].Value<string>()})",
Link = string.IsNullOrEmpty(id)
? null
: _linkGenerator.PayoutLink(jObj["walletId"].Value<string>(),
jObj["pullPaymentId"].Value<string>(), request.Scheme, request.Host,
request.PathBase)
};
}
} }
} }
else if (uncoloredLabel is PayoutLabel payoutLabel)
return new Label() { RawValue = value, Value = value, Color = color }; {
coloredLabel.Tooltip = $"Paid a payout of a pull payment ({payoutLabel.PullPaymentId})";
coloredLabel.Link = string.IsNullOrEmpty(payoutLabel.PullPaymentId) || string.IsNullOrEmpty(payoutLabel.WalletId)
? null
: _linkGenerator.PayoutLink(payoutLabel.WalletId,
payoutLabel.PullPaymentId, request.Scheme, request.Host,
request.PathBase);
}
return coloredLabel;
} }
} }
} }

View File

@@ -48,14 +48,14 @@
class="badge badge-primary badge-pill ml-2" class="badge badge-primary badge-pill ml-2"
v-for="label of item.labels" v-for="label of item.labels"
v-bind:style="{ 'background-color': label.color}" v-bind:style="{ 'background-color': label.color}"
key="label.value"> key="label.text">
<span v-if="!label.link" data-toggle="tooltip" v-tooltip="label.tooltip"> <span v-if="!label.link" data-toggle="tooltip" v-tooltip="label.tooltip">
{{label.value}} {{label.text}}
</span> </span>
<a :href="label.link" target="_blank"v-if="label.link" data-toggle="tooltip" v-tooltip="label.tooltip"> <a :href="label.link" target="_blank" v-if="label.link" data-toggle="tooltip" v-tooltip="label.tooltip">
{{label.value}} {{label.text}}
<i class="fa fa-info-circle"></i> <i class="fa fa-info-circle"></i>
</a> </a>
</span> </span>

View File

@@ -81,8 +81,8 @@
<span class="mr-2">Filter by label:</span> <span class="mr-2">Filter by label:</span>
@foreach (var label in Model.Labels) @foreach (var label in Model.Labels)
{ {
<a asp-route-labelFilter="@label.Value" class="badge mr-2 my-1 position-relative text-white d-block" <a asp-route-labelFilter="@label.Text" class="badge mr-2 my-1 position-relative text-white d-block"
style="background-color: @label.Color;"><span class="text-white">@label.Value</span></a> style="background-color: @label.Color;"><span class="text-white">@label.Text</span></a>
} }
</div> </div>
</div> </div>
@@ -123,7 +123,7 @@
style="background-color: @label.Color; padding-right: 16px; z-index: 1;" style="background-color: @label.Color; padding-right: 16px; z-index: 1;"
data-toggle="tooltip" data-toggle="tooltip"
title="@label.Tooltip"> title="@label.Tooltip">
<a asp-route-labelFilter="@label.Value" class="text-white">@label.Value</a> <a asp-route-labelFilter="@label.Text" class="text-white">@label.Text</a>
<form <form
asp-route-walletId="@this.Context.GetRouteValue("walletId")" asp-route-walletId="@this.Context.GetRouteValue("walletId")"
@@ -135,7 +135,7 @@
name="removelabel" name="removelabel"
style="color: @label.Color; filter: brightness(0.5);" style="color: @label.Color; filter: brightness(0.5);"
type="submit" type="submit"
value="@label.RawValue"> value="@label.Text">
<span class="fa fa-close"></span> <span class="fa fa-close"></span>
</button> </button>
</form> </form>
@@ -184,11 +184,11 @@
{ {
@if (transaction.Labels.Contains(label)) @if (transaction.Labels.Contains(label))
{ {
<button name="removelabel" class="btn btn-sm" type="submit" value="@label.Value"><span class="badge" style="display:block;background-color:@label.Color;color:white;text-align:left;"><span class="fa fa-check"></span> @label.Value</span></button> <button name="removelabel" class="btn btn-sm" type="submit" value="@label.Text"><span class="badge" style="display:block;background-color:@label.Color;color:white;text-align:left;"><span class="fa fa-check"></span> @label.Text</span></button>
} }
else else
{ {
<button name="addlabelclick" class="btn btn-sm" type="submit" value="@label.Value"><span class="badge" style="display:block;background-color:@label.Color;color:white;text-align:left;">@label.Value</span></button> <button name="addlabelclick" class="btn btn-sm" type="submit" value="@label.Text"><span class="badge" style="display:block;background-color:@label.Color;color:white;text-align:left;">@label.Text</span></button>
} }
} }
} }