From 07f0d95f56c3b8da07bedaf8f192bd0498e4b087 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 13 Feb 2020 09:18:43 +0100 Subject: [PATCH] BIP21 Support for Wallet spending (#1322) * BIP21 Support for Wallet spending * extract bip21 loading to method * add bip21 parsing test --- BTCPayServer.Tests/SeleniumTester.cs | 7 +-- BTCPayServer.Tests/SeleniumTests.cs | 17 ++++++ BTCPayServer/Controllers/WalletsController.cs | 52 ++++++++++++++++++- BTCPayServer/Models/StatusMessageModel.cs | 39 +++++++------- BTCPayServer/Views/Wallets/WalletSend.cshtml | 12 ++++- BTCPayServer/wwwroot/js/WalletSend.js | 8 +++ 6 files changed, 109 insertions(+), 26 deletions(-) diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index e97b958d5..46c2d8767 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -18,6 +18,7 @@ using System.Threading; using System.Threading.Tasks; using BTCPayServer.Lightning; using BTCPayServer.Lightning.CLightning; +using BTCPayServer.Models; using BTCPayServer.Views.Stores; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -70,18 +71,18 @@ namespace BTCPayServer.Tests Driver.AssertNoError(); } - internal void AssertHappyMessage() + internal void AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success) { using var cts = new CancellationTokenSource(20_000); while (!cts.IsCancellationRequested) { - var success = Driver.FindElements(By.ClassName("alert-success")).Where(el => el.Displayed).Any(); + var success = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).Any(el => el.Displayed); if (success) return; Thread.Sleep(100); } Logs.Tester.LogInformation(this.Driver.PageSource); - Assert.True(false, "Should have shown happy message"); + Assert.True(false, $"Should have shown {severity} message"); } public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(10); diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 52ac31ab4..ff01ad6b9 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -9,6 +9,8 @@ using System.Linq; using NBitcoin; using System.Threading.Tasks; using System.Text.RegularExpressions; +using BTCPayServer.Models; +using NBitcoin.Payment; namespace BTCPayServer.Tests { @@ -569,6 +571,21 @@ namespace BTCPayServer.Tests Assert.EndsWith("psbt", s.Driver.Url); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); Assert.Equal(walletTransactionLink, s.Driver.Url); + + var bip21 = invoice.EntityToDTO().CryptoInfo.First().PaymentUrls.BIP21; + //let's make bip21 more interesting + bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!"; + var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest); + s.Driver.FindElement(By.Id("Wallets")).Click(); + s.Driver.FindElement(By.LinkText("Manage")).Click(); + s.Driver.FindElement(By.Id("WalletSend")).Click(); + s.Driver.FindElement(By.Id("bip21parse")).Click(); + s.Driver.SwitchTo().Alert().SendKeys(bip21); + s.Driver.SwitchTo().Alert().Accept(); + s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Info); + Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value")); + Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value")); + } } } diff --git a/BTCPayServer/Controllers/WalletsController.cs b/BTCPayServer/Controllers/WalletsController.cs index 69205c390..a5330c8df 100644 --- a/BTCPayServer/Controllers/WalletsController.cs +++ b/BTCPayServer/Controllers/WalletsController.cs @@ -446,12 +446,13 @@ namespace BTCPayServer.Controllers } return View(model); } + [HttpPost] [Route("{walletId}/send")] public async Task WalletSend( [ModelBinder(typeof(WalletIdModelBinder))] - WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default) + WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default, string bip21 = "") { if (walletId?.StoreId == null) return NotFound(); @@ -462,6 +463,13 @@ namespace BTCPayServer.Controllers if (network == null || network.ReadonlyWallet) return NotFound(); vm.SupportRBF = network.SupportRBF; + + if (!string.IsNullOrEmpty(bip21)) + { + LoadFromBIP21(vm, bip21, network); + return View(vm); + } + decimal transactionAmountSum = 0; if (command == "add-output") @@ -477,7 +485,6 @@ namespace BTCPayServer.Controllers vm.Outputs.RemoveAt(index); return View(vm); } - if (!vm.Outputs.Any()) { @@ -593,6 +600,47 @@ namespace BTCPayServer.Controllers } + private void LoadFromBIP21(WalletSendModel vm, string bip21, BTCPayNetwork network) + { + try + { + if (bip21.StartsWith(network.UriScheme, StringComparison.InvariantCultureIgnoreCase)) + { + bip21 = $"bitcoin{bip21.Substring(network.UriScheme.Length)}"; + } + + var uriBuilder = new NBitcoin.Payment.BitcoinUrlBuilder(bip21, network.NBitcoinNetwork); + vm.Outputs = new List() + { + new WalletSendModel.TransactionOutput() + { + Amount = uriBuilder.Amount.ToDecimal(MoneyUnit.BTC), + DestinationAddress = uriBuilder.Address.ToString(), + SubtractFeesFromOutput = false + } + }; + if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message)) + { + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Info, + Html = + $"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to {uriBuilder.Label}")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for {uriBuilder.Message}")}" + }); + } + } + catch (Exception) + { + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Severity = StatusMessageModel.StatusSeverity.Error, + Message = "The provided BIP21 payment URI was malformed" + }); + } + + ModelState.Clear(); + } + private IActionResult ViewVault(WalletId walletId, PSBT psbt) { return View("WalletSendVault", new WalletSendVaultModel() diff --git a/BTCPayServer/Models/StatusMessageModel.cs b/BTCPayServer/Models/StatusMessageModel.cs index 22cccbfaf..0a34008bf 100644 --- a/BTCPayServer/Models/StatusMessageModel.cs +++ b/BTCPayServer/Models/StatusMessageModel.cs @@ -15,26 +15,8 @@ namespace BTCPayServer.Models public StatusSeverity Severity { get; set; } public bool AllowDismiss { get; set; } = true; - public string SeverityCSS - { - get - { - switch (Severity) - { - case StatusSeverity.Info: - return "info"; - case StatusSeverity.Error: - return "danger"; - case StatusSeverity.Success: - return "success"; - case StatusSeverity.Warning: - return "warning"; - default: - throw new ArgumentOutOfRangeException(); - } - } - } - + public string SeverityCSS => ToString(Severity); + private void ParseNonJsonStatus(string s) { Message = s; @@ -43,6 +25,23 @@ namespace BTCPayServer.Models : StatusSeverity.Success; } + public static string ToString(StatusSeverity severity) + { + switch (severity) + { + case StatusSeverity.Info: + return "info"; + case StatusSeverity.Error: + return "danger"; + case StatusSeverity.Success: + return "success"; + case StatusSeverity.Warning: + return "warning"; + default: + throw new ArgumentOutOfRangeException(); + } + } + public enum StatusSeverity { Info, diff --git a/BTCPayServer/Views/Wallets/WalletSend.cshtml b/BTCPayServer/Views/Wallets/WalletSend.cshtml index 7df3bace6..fbfe61dd0 100644 --- a/BTCPayServer/Views/Wallets/WalletSend.cshtml +++ b/BTCPayServer/Views/Wallets/WalletSend.cshtml @@ -5,7 +5,14 @@ ViewData["Title"] = "Manage wallet"; ViewData.SetActivePageAndTitle(WalletsNavPages.Send); } - +@if (TempData.HasStatusMessage()) +{ +
+
+ +
+
+}
@@ -16,6 +23,7 @@ +
    @foreach (var errors in ViewData.ModelState.Where(pair => pair.Key == string.Empty && pair.Value.ValidationState == ModelValidationState.Invalid)) { @@ -118,6 +126,7 @@
} +
+ diff --git a/BTCPayServer/wwwroot/js/WalletSend.js b/BTCPayServer/wwwroot/js/WalletSend.js index dcfd49f89..7976ae3b7 100644 --- a/BTCPayServer/wwwroot/js/WalletSend.js +++ b/BTCPayServer/wwwroot/js/WalletSend.js @@ -44,4 +44,12 @@ $(function () { updateFiatValue(outputAmountElement); return false; }); + + $("#bip21parse").on("click", function(){ + var bip21 = prompt("Paste BIP21 here"); + if(bip21){ + $("#BIP21").val(bip21); + $("form").submit(); + } + }); });