BIP21 Support for Wallet spending (#1322)

* BIP21 Support for Wallet spending

* extract bip21 loading to method

* add bip21 parsing test
This commit is contained in:
Andrew Camilleri
2020-02-13 09:18:43 +01:00
committed by GitHub
parent 1a409a441d
commit 07f0d95f56
6 changed files with 109 additions and 26 deletions

View File

@@ -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);

View File

@@ -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"));
}
}
}

View File

@@ -447,11 +447,12 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpPost]
[Route("{walletId}/send")]
public async Task<IActionResult> 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")
@@ -478,7 +486,6 @@ namespace BTCPayServer.Controllers
return View(vm);
}
if (!vm.Outputs.Any())
{
ModelState.AddModelError(string.Empty,
@@ -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<WalletSendModel.TransactionOutput>()
{
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()

View File

@@ -15,11 +15,19 @@ namespace BTCPayServer.Models
public StatusSeverity Severity { get; set; }
public bool AllowDismiss { get; set; } = true;
public string SeverityCSS
public string SeverityCSS => ToString(Severity);
private void ParseNonJsonStatus(string s)
{
get
Message = s;
Severity = s.StartsWith("Error", StringComparison.InvariantCultureIgnoreCase)
? StatusSeverity.Error
: StatusSeverity.Success;
}
public static string ToString(StatusSeverity severity)
{
switch (Severity)
switch (severity)
{
case StatusSeverity.Info:
return "info";
@@ -33,15 +41,6 @@ namespace BTCPayServer.Models
throw new ArgumentOutOfRangeException();
}
}
}
private void ParseNonJsonStatus(string s)
{
Message = s;
Severity = s.StartsWith("Error", StringComparison.InvariantCultureIgnoreCase)
? StatusSeverity.Error
: StatusSeverity.Success;
}
public enum StatusSeverity
{

View File

@@ -5,7 +5,14 @@
ViewData["Title"] = "Manage wallet";
ViewData.SetActivePageAndTitle(WalletsNavPages.Send);
}
@if (TempData.HasStatusMessage())
{
<div class="row">
<div class="col-md-10 text-center">
<partial name="_StatusMessage" />
</div>
</div>
}
<div class="row">
<div class="@(Model.Outputs.Count==1? "col-lg-6 transaction-output-form": "col-lg-8")">
<form method="post">
@@ -16,6 +23,7 @@
<input type="hidden" asp-for="CurrentBalance" />
<input type="hidden" asp-for="RecommendedSatoshiPerByte" />
<input type="hidden" asp-for="CryptoCode" />
<input type="hidden" name="BIP21" id="BIP21" />
<ul class="text-danger">
@foreach (var errors in ViewData.ModelState.Where(pair => pair.Key == string.Empty && pair.Value.ValidationState == ModelValidationState.Invalid))
{
@@ -118,6 +126,7 @@
</div>
</div>
}
<div class="card">
<button class="btn btn-light collapsed" type="button" data-toggle="collapse" data-target="#accordian-advanced" aria-expanded="false" aria-controls="accordian-advanced">
Advanced settings
@@ -163,6 +172,7 @@
</div>
</div>
<button type="submit" name="command" value="add-output" class="ml-1 btn btn-secondary">Add another destination </button>
<button type="button" id="bip21parse" class="ml-1 btn btn-secondary">Parse BIP21</button>
</div>
</form>
</div>

View File

@@ -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();
}
});
});