mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 09:54:30 +01:00
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:
parent
1a409a441d
commit
07f0d95f56
@ -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);
|
||||
|
@ -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"));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user