diff --git a/BTCPayServer.Data/Data/WalletTransactionData.cs b/BTCPayServer.Data/Data/WalletTransactionData.cs index ea6168716..7f7982acb 100644 --- a/BTCPayServer.Data/Data/WalletTransactionData.cs +++ b/BTCPayServer.Data/Data/WalletTransactionData.cs @@ -26,11 +26,4 @@ namespace BTCPayServer.Data .WithMany(w => w.WalletTransactions).OnDelete(DeleteBehavior.Cascade); } } - - public class WalletTransactionInfo - { - public string Comment { get; set; } = string.Empty; - [JsonIgnore] - public HashSet Labels { get; set; } = new HashSet(); - } } diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 369317366..297e40a9f 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -35,6 +35,7 @@ using BTCPayServer.Security.Bitpay; using BTCPayServer.Services; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Labels; using BTCPayServer.Services.Mails; using BTCPayServer.Services.Rates; 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(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(info.Labels["invoice"]); + Assert.Equal("BFm1MCJPBCDeRoWXvPcwnM", invoiceLabel.Reference); + Assert.Equal("invoice", invoiceLabel.Text); + Assert.Equal("invoice", invoiceLabel.Type); + + var appLabel = Assert.IsType(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(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] [Trait("Fast", "Fast")] public void DeterministicUTXOSorter() @@ -1234,8 +1324,8 @@ namespace BTCPayServer.Tests tx = Assert.Single(transactions.Transactions); Assert.Equal("hello", tx.Comment); - Assert.Contains("test", tx.Labels.Select(l => l.Value)); - Assert.Contains("test2", tx.Labels.Select(l => l.Value)); + Assert.Contains("test", tx.Labels.Select(l => l.Text)); + Assert.Contains("test2", tx.Labels.Select(l => l.Text)); Assert.Equal(2, tx.Labels.GroupBy(l => l.Color).Count()); Assert.IsType( @@ -1246,8 +1336,8 @@ namespace BTCPayServer.Tests tx = Assert.Single(transactions.Transactions); Assert.Equal("hello", tx.Comment); - Assert.Contains("test", tx.Labels.Select(l => l.Value)); - Assert.DoesNotContain("test2", tx.Labels.Select(l => l.Value)); + Assert.Contains("test", tx.Labels.Select(l => l.Text)); + Assert.DoesNotContain("test2", tx.Labels.Select(l => l.Text)); Assert.Single(tx.Labels.GroupBy(l => l.Color)); var walletInfo = await tester.PayTester.GetService().GetWalletInfo(walletId); diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 045af6e72..03d7c0d08 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -159,12 +159,12 @@ namespace BTCPayServer.Controllers if (addlabel != null) { 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)) { walletTransactionInfo = new WalletTransactionInfo(); } - if (!labels.Any(l => l.Value.Equals(addlabel, StringComparison.OrdinalIgnoreCase))) + if (!labels.Any(l => l.Text.Equals(addlabel, StringComparison.OrdinalIgnoreCase))) { List allColors = new List(); allColors.AddRange(LabelColorScheme); @@ -183,7 +183,8 @@ namespace BTCPayServer.Controllers walletBlobInfo.LabelColors.Add(addlabel, chosenColor); 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); } @@ -195,8 +196,8 @@ namespace BTCPayServer.Controllers { if (walletTransactionInfo.Labels.Remove(removelabel)) { - var canDelete = !walletTransactionsInfo.SelectMany(txi => txi.Value.Labels).Any(l => l == removelabel); - if (canDelete) + var canDeleteColor = !walletTransactionsInfo.Any(txi => txi.Value.Labels.ContainsKey(removelabel)); + if (canDeleteColor) { walletBlobInfo.LabelColors.Remove(removelabel); await WalletRepository.SetWalletInfo(walletId, walletBlobInfo); @@ -315,14 +316,14 @@ namespace BTCPayServer.Controllers 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); model.Labels.AddRange(labels); vm.Comment = transactionInfo.Comment; } 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); } @@ -547,7 +548,7 @@ namespace BTCPayServer.Controllers Outpoint = coin.OutPoint.ToString(), Amount = coin.Value.GetValue(network), 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()) }; }).ToArray(); diff --git a/BTCPayServer/Data/WalletTransactionDataExtensions.cs b/BTCPayServer/Data/WalletTransactionDataExtensions.cs index 5544e917d..fbdf5d64f 100644 --- a/BTCPayServer/Data/WalletTransactionDataExtensions.cs +++ b/BTCPayServer/Data/WalletTransactionDataExtensions.cs @@ -1,32 +1,57 @@ using System; +using System.Collections.Generic; +using System.Linq; +using BTCPayServer.Services.Labels; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; namespace BTCPayServer.Data { + public class WalletTransactionInfo + { + public string Comment { get; set; } = string.Empty; + [JsonIgnore] + public Dictionary Labels { get; set; } = new Dictionary(); + } public static class WalletTransactionDataExtensions { public static WalletTransactionInfo GetBlobInfo(this WalletTransactionData walletTransactionData) { + WalletTransactionInfo blobInfo; if (walletTransactionData.Blob == null || walletTransactionData.Blob.Length == 0) - { - return new WalletTransactionInfo(); - } - var blobInfo = JsonConvert.DeserializeObject(ZipUtils.Unzip(walletTransactionData.Blob)); + blobInfo = new WalletTransactionInfo(); + else + blobInfo = JsonConvert.DeserializeObject(ZipUtils.Unzip(walletTransactionData.Blob)); if (!string.IsNullOrEmpty(walletTransactionData.Labels)) { if (walletTransactionData.Labels.StartsWith('[')) { - blobInfo.Labels.AddRange(JArray.Parse(walletTransactionData.Labels).Values()); + foreach (var jtoken in JArray.Parse(walletTransactionData.Labels)) + { + var l = jtoken.Type == JTokenType.String ? Label.Parse(jtoken.Value()) + : Label.Parse(jtoken.ToString()); + blobInfo.Labels.TryAdd(l.Text, l); + } } else { - blobInfo.Labels.AddRange(walletTransactionData.Labels.Split(',', - StringSplitOptions.RemoveEmptyEntries)); + // Legacy path + foreach (var token in walletTransactionData.Labels.Split(',', + StringSplitOptions.RemoveEmptyEntries)) + { + var l = Label.Parse(token); + blobInfo.Labels.TryAdd(l.Text, l); + } } } return blobInfo; } + static JsonSerializerSettings LabelSerializerSettings = new JsonSerializerSettings() + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Formatting = Formatting.None + }; public static void SetBlobInfo(this WalletTransactionData walletTransactionData, WalletTransactionInfo blobInfo) { if (blobInfo == null) @@ -35,8 +60,11 @@ namespace BTCPayServer.Data walletTransactionData.Blob = Array.Empty(); return; } - - walletTransactionData.Labels = JArray.FromObject(blobInfo.Labels).ToString(); + walletTransactionData.Labels = new JArray( + blobInfo.Labels.Select(l => JsonConvert.SerializeObject(l.Value, LabelSerializerSettings)) + .Select(l => JObject.Parse(l)) + .OfType() + .ToArray()).ToString(); walletTransactionData.Blob = ZipUtils.Zip(JsonConvert.SerializeObject(blobInfo)); } } diff --git a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs index ef33d75f1..8b44bcc27 100644 --- a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs +++ b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs @@ -9,6 +9,7 @@ using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Services; using BTCPayServer.Services.Apps; +using BTCPayServer.Services.Labels; using BTCPayServer.Services.PaymentRequests; using NBitcoin; using Newtonsoft.Json.Linq; @@ -40,7 +41,7 @@ namespace BTCPayServer.HostedServices { var walletId = new WalletId(invoiceEvent.Invoice.StoreId, invoiceEvent.Payment.GetCryptoCode()); 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) }; @@ -79,14 +80,14 @@ namespace BTCPayServer.HostedServices 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); var update = false; foreach (var label in pair.Value) { - if (walletTransactionInfo.Labels.Add(label.label)) + if (walletTransactionInfo.Labels.TryAdd(label.label.Text, label.label)) { 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; - TransactionLabels = new Dictionary>(); - TransactionLabels.Add(txId, new List<(string color, string label)>() { colorLabel }); + TransactionLabels = new Dictionary>(); + 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; - TransactionLabels = new Dictionary>(); + TransactionLabels = new Dictionary>(); 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 Dictionary> TransactionLabels { get; set; } + public Dictionary> TransactionLabels { get; set; } public override string ToString() { var result = new StringBuilder(); diff --git a/BTCPayServer/Models/WalletViewModels/ListTransactionsViewModel.cs b/BTCPayServer/Models/WalletViewModels/ListTransactionsViewModel.cs index d63640ddd..087504672 100644 --- a/BTCPayServer/Models/WalletViewModels/ListTransactionsViewModel.cs +++ b/BTCPayServer/Models/WalletViewModels/ListTransactionsViewModel.cs @@ -15,9 +15,9 @@ namespace BTCPayServer.Models.WalletViewModels public string Link { get; set; } public bool Positive { get; set; } public string Balance { get; set; } - public HashSet