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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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

@ -446,12 +446,13 @@ 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")
@ -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<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,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,

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