diff --git a/BTCPayServer.Data/Data/WalletObjectData.cs b/BTCPayServer.Data/Data/WalletObjectData.cs index 1cc0ea954..79e447149 100644 --- a/BTCPayServer.Data/Data/WalletObjectData.cs +++ b/BTCPayServer.Data/Data/WalletObjectData.cs @@ -12,6 +12,13 @@ namespace BTCPayServer.Data { public class Types { + public static readonly HashSet AllTypes; + static Types() + { + AllTypes = typeof(Types).GetFields() + .Where(f => f.FieldType == typeof(string)) + .Select(f => (string)f.GetValue(null)).ToHashSet(StringComparer.OrdinalIgnoreCase); + } public const string Label = "label"; public const string Tx = "tx"; public const string Payjoin = "payjoin"; diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 63b70e234..cd21324f0 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -555,7 +555,7 @@ namespace BTCPayServer.Tests walletId ??= WalletId; GoToWallet(walletId, WalletsNavPages.Receive); Driver.FindElement(By.Id("generateButton")).Click(); - var addressStr = Driver.FindElement(By.Id("address")).GetAttribute("value"); + var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("value"); var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork); for (var i = 0; i < coins; i++) { diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 40cbc83fd..df3c926cc 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -1053,7 +1053,7 @@ namespace BTCPayServer.Tests var walletId = new WalletId(storeId, "BTC"); s.GoToWallet(walletId, WalletsNavPages.Receive); s.Driver.FindElement(By.Id("generateButton")).Click(); - var addressStr = s.Driver.FindElement(By.Id("address")).GetAttribute("value"); + var addressStr = s.Driver.FindElement(By.Id("Address")).GetAttribute("value"); var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork); await s.Server.ExplorerNode.GenerateAsync(1); @@ -1269,14 +1269,48 @@ namespace BTCPayServer.Tests Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed); // no previous page in the wizard, hence no back button Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack"))); - var receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value"); + var receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("value"); + + // Can add a label? + await TestUtils.EventuallyAsync(async () => + { + s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).Click(); + await Task.Delay(500); + s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).SendKeys("test-label" + Keys.Enter); + await Task.Delay(500); + s.Driver.WaitForElement(By.CssSelector("div.label-manager input ")).SendKeys("label2" + Keys.Enter); + }); + + TestUtils.Eventually(() => + { + s.Driver.Navigate().Refresh(); + Assert.NotNull(s.Driver.FindElement(By.CssSelector("[data-value='test-label']"))); + }); //unreserve s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click(); //generate it again, should be the same one as before as nothing got used in the meantime s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed); - Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value")); + Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("value")); + TestUtils.Eventually(() => + { + Assert.Contains("test-label", s.Driver.PageSource); + }); + + // Let's try to remove a label + await TestUtils.EventuallyAsync(async () => + { + s.Driver.WaitForElement(By.CssSelector("[data-value='test-label']")).Click(); + await Task.Delay(500); + s.Driver.ExecuteJavaScript("document.querySelector('[data-value=\"test-label\"]').nextSibling.dispatchEvent(new KeyboardEvent('keydown', {'key': 'Delete', keyCode: 46}));"); + + }); + TestUtils.Eventually(() => + { + s.Driver.Navigate().Refresh(); + Assert.DoesNotContain("test-label", s.Driver.PageSource); + }); Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack"))); //send money to addr and ensure it changed @@ -1289,15 +1323,19 @@ namespace BTCPayServer.Tests await Task.Delay(200); s.Driver.Navigate().Refresh(); s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); - Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value")); - receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value"); + Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("value")); + receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("value"); s.Driver.FindElement(By.Id("CancelWizard")).Click(); + // Check the label is applied to the tx + + Assert.Equal("label2", s.Driver.FindElement(By.XPath("//*[@id=\"WalletTransactionsList\"]//*[contains(@class, 'transactionLabel')]")).Text); + //change the wallet and ensure old address is not there and generating a new one does not result in the prev one s.GenerateWallet(cryptoCode, "", true); s.GoToWallet(null, WalletsNavPages.Receive); s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); - Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value")); + Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("value")); var invoiceId = s.CreateInvoice(storeId); var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); diff --git a/BTCPayServer.Tests/ThirdPartyTests.cs b/BTCPayServer.Tests/ThirdPartyTests.cs index f5a7163ff..02852ea0e 100644 --- a/BTCPayServer.Tests/ThirdPartyTests.cs +++ b/BTCPayServer.Tests/ThirdPartyTests.cs @@ -349,6 +349,10 @@ retry: version = Regex.Match(actual, "vue-qrcode v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value; expected = (await (await client.GetAsync($"https://unpkg.com/@chenfengyuan/vue-qrcode@{version}/dist/vue-qrcode.min.js")).Content.ReadAsStringAsync()).Trim(); Assert.Equal(expected, actual); + + actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim(); + expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim(); + Assert.Equal(expected, actual); } string GetFileContent(params string[] path) diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index c76f8dd45..8369ca56c 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -123,6 +123,7 @@ + diff --git a/BTCPayServer/Components/LabelManager/Default.cshtml b/BTCPayServer/Components/LabelManager/Default.cshtml new file mode 100644 index 000000000..949ab6e34 --- /dev/null +++ b/BTCPayServer/Components/LabelManager/Default.cshtml @@ -0,0 +1,104 @@ +@using NBitcoin.DataEncoders +@using NBitcoin +@using BTCPayServer.Abstractions.TagHelpers +@model BTCPayServer.Components.LabelManager.LabelViewModel +@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery +@{ + var commonCall = Model.ObjectId.Type + Model.ObjectId.Id; + var elementId = "a" + Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); +} + + + + + + diff --git a/BTCPayServer/Components/LabelManager/LabelManager.cs b/BTCPayServer/Components/LabelManager/LabelManager.cs new file mode 100644 index 000000000..af0607ce2 --- /dev/null +++ b/BTCPayServer/Components/LabelManager/LabelManager.cs @@ -0,0 +1,18 @@ +using BTCPayServer.Services; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Components.LabelManager +{ + public class LabelManager : ViewComponent + { + public IViewComponentResult Invoke(WalletObjectId walletObjectId, string[] selectedLabels) + { + var vm = new LabelViewModel + { + ObjectId = walletObjectId, + SelectedLabels = selectedLabels + }; + return View(vm); + } + } +} diff --git a/BTCPayServer/Components/LabelManager/LabelViewModel.cs b/BTCPayServer/Components/LabelManager/LabelViewModel.cs new file mode 100644 index 000000000..139af2d0f --- /dev/null +++ b/BTCPayServer/Components/LabelManager/LabelViewModel.cs @@ -0,0 +1,10 @@ +using BTCPayServer.Services; + +namespace BTCPayServer.Components.LabelManager +{ + public class LabelViewModel + { + public string[] SelectedLabels { get; set; } + public WalletObjectId ObjectId { get; set; } + } +} diff --git a/BTCPayServer/Components/QRCode/QRCode.cs b/BTCPayServer/Components/QRCode/QRCode.cs index f44bbb74d..472c37978 100644 --- a/BTCPayServer/Components/QRCode/QRCode.cs +++ b/BTCPayServer/Components/QRCode/QRCode.cs @@ -9,16 +9,15 @@ namespace BTCPayServer.Components.QRCode { public class QRCode : ViewComponent { - private static QRCodeGenerator qrGenerator = new QRCodeGenerator(); - - + private static QRCodeGenerator _qrGenerator = new (); + public IViewComponentResult Invoke(string data) { - QRCodeData qrCodeData = qrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q); - PngByteQRCode qrCode = new PngByteQRCode(qrCodeData); - var bytes = qrCode.GetGraphic(5, new byte[] { 0, 0, 0, 255 }, new byte[] { 0xf5, 0xf5, 0xf7, 255 }, true); + var qrCodeData = _qrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q); + var qrCode = new PngByteQRCode(qrCodeData); + var bytes = qrCode.GetGraphic(5, new byte[] { 0, 0, 0, 255 }, new byte[] { 0xf5, 0xf5, 0xf7, 255 }); var b64 = Convert.ToBase64String(bytes); - return new HtmlContentViewComponentResult(new HtmlString($"")); + return new HtmlContentViewComponentResult(new HtmlString($"")); } } } diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs index 0646d866a..4b88795c2 100644 --- a/BTCPayServer/Controllers/UIWalletsController.cs +++ b/BTCPayServer/Controllers/UIWalletsController.cs @@ -307,7 +307,7 @@ namespace BTCPayServer.Controllers } [HttpGet("{walletId}/receive")] - public IActionResult WalletReceive([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, + public async Task WalletReceive([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, [FromQuery] string? returnUrl = null) { if (walletId?.StoreId == null) @@ -328,13 +328,23 @@ namespace BTCPayServer.Controllers Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint", new { walletId.CryptoCode }))); } + + string[]? labels = null; + if (address is not null) + { + var info = await WalletRepository.GetWalletObject(new WalletObjectId(walletId, WalletObjectData.Types.Address, + address.ToString())); + labels = info?.GetNeighbours().Where(data => data.Type == WalletObjectData.Types.Label) + .Select(data => data.Id).ToArray(); + } return View(new WalletReceiveViewModel { CryptoCode = walletId.CryptoCode, Address = address?.ToString(), CryptoImage = GetImage(paymentMethod.PaymentId, network), PaymentLink = bip21.ToString(), - ReturnUrl = returnUrl ?? HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath + ReturnUrl = returnUrl ?? HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath, + SelectedLabels = labels?? Array.Empty() }); } @@ -1311,6 +1321,52 @@ namespace BTCPayServer.Controllers return Content(res, "application/" + format); } + + public class UpdateLabelsRequest + { + public string Address { get; set; } + public string[]? Labels { get; set; } + } + + [HttpPost("{walletId}/update-labels")] + [IgnoreAntiforgeryToken] + public async Task UpdateLabels( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, [FromBody] UpdateLabelsRequest request) + { + if (request.Address is null || request.Labels is null) + return BadRequest(); + + var objid = new WalletObjectId(walletId, WalletObjectData.Types.Address, request.Address); + var obj = await WalletRepository.GetWalletObject(objid); + if (obj is null) + { + await WalletRepository.EnsureWalletObject(objid); + } + else + { + var currentLabels = obj.GetNeighbours().Where(data => data.Type == WalletObjectData.Types.Label).ToArray(); + var toRemove = currentLabels.Where(data => !request.Labels.Contains(data.Id)).Select(data => data.Id).ToArray(); + await WalletRepository.RemoveWalletObjectLabels(objid, toRemove); + } + await + WalletRepository.AddWalletObjectLabels(objid, request.Labels); + return Ok(); + } + + [HttpGet("{walletId}/labels")] + [IgnoreAntiforgeryToken] + public async Task GetLabels( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, bool excludeTypes) + { + + return Ok(( await WalletRepository.GetWalletLabels(walletId)) + .Where(l => !excludeTypes || !WalletObjectData.Types.AllTypes.Contains(l.Label)) + .Select(tuple => new + { + label = tuple.Label, + color = tuple.Color, + textColor = ColorPalette.Default.TextColor(tuple.Color) + })); + } + private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network) { var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike @@ -1450,6 +1506,7 @@ namespace BTCPayServer.Controllers public string? Address { get; set; } public string? PaymentLink { get; set; } public string? ReturnUrl { get; set; } + public string[]? SelectedLabels { get; set; } } public class SendToAddressResult diff --git a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs index 95e2eb338..97afc27f1 100644 --- a/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs +++ b/BTCPayServer/HostedServices/TransactionLabelMarkerHostedService.cs @@ -77,6 +77,17 @@ namespace BTCPayServer.HostedServices foreach (var walletObjectData in walletObjectDatas) { await _walletRepository.EnsureWalletObjectLink(txWalletObject, walletObjectData.Key); + //if the object is an address, we also link the labels to the tx + if(walletObjectData.Value.Type == WalletObjectData.Types.Address) + { + var labels = walletObjectData.Value.GetNeighbours() + .Where(data => data.Type == WalletObjectData.Types.Label).Select(data => + new WalletObjectId(walletObjectDatas.Key, data.Type, data.Id)); + foreach (var label in labels) + { + await _walletRepository.EnsureWalletObjectLink(label, txWalletObject); + } + } } } diff --git a/BTCPayServer/Views/UIWallets/WalletReceive.cshtml b/BTCPayServer/Views/UIWallets/WalletReceive.cshtml index 184725eb3..443573c9c 100644 --- a/BTCPayServer/Views/UIWallets/WalletReceive.cshtml +++ b/BTCPayServer/Views/UIWallets/WalletReceive.cshtml @@ -1,6 +1,7 @@ -@inject BTCPayServer.Services.BTCPayServerEnvironment env +@inject BTCPayServerEnvironment env @using BTCPayServer.Controllers @using BTCPayServer.Components.QRCode +@using BTCPayServer.Services @model BTCPayServer.Controllers.WalletReceiveViewModel @{ var walletId = Context.GetRouteValue("walletId").ToString(); @@ -21,10 +22,10 @@ }
-

@ViewData["Title"]

+

@ViewData["Title"]

-
+ @if (string.IsNullOrEmpty(Model.Address)) { @@ -54,46 +55,56 @@ +
-