mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +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:
@@ -18,6 +18,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Lightning;
|
using BTCPayServer.Lightning;
|
||||||
using BTCPayServer.Lightning.CLightning;
|
using BTCPayServer.Lightning.CLightning;
|
||||||
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Views.Stores;
|
using BTCPayServer.Views.Stores;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
@@ -70,18 +71,18 @@ namespace BTCPayServer.Tests
|
|||||||
Driver.AssertNoError();
|
Driver.AssertNoError();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AssertHappyMessage()
|
internal void AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
|
||||||
{
|
{
|
||||||
using var cts = new CancellationTokenSource(20_000);
|
using var cts = new CancellationTokenSource(20_000);
|
||||||
while (!cts.IsCancellationRequested)
|
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)
|
if (success)
|
||||||
return;
|
return;
|
||||||
Thread.Sleep(100);
|
Thread.Sleep(100);
|
||||||
}
|
}
|
||||||
Logs.Tester.LogInformation(this.Driver.PageSource);
|
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);
|
public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(10);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ using System.Linq;
|
|||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using BTCPayServer.Models;
|
||||||
|
using NBitcoin.Payment;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
@@ -569,6 +571,21 @@ namespace BTCPayServer.Tests
|
|||||||
Assert.EndsWith("psbt", s.Driver.Url);
|
Assert.EndsWith("psbt", s.Driver.Url);
|
||||||
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
|
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
|
||||||
Assert.Equal(walletTransactionLink, s.Driver.Url);
|
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);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("{walletId}/send")]
|
[Route("{walletId}/send")]
|
||||||
public async Task<IActionResult> WalletSend(
|
public async Task<IActionResult> WalletSend(
|
||||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
[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)
|
if (walletId?.StoreId == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
@@ -462,6 +463,13 @@ namespace BTCPayServer.Controllers
|
|||||||
if (network == null || network.ReadonlyWallet)
|
if (network == null || network.ReadonlyWallet)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
vm.SupportRBF = network.SupportRBF;
|
vm.SupportRBF = network.SupportRBF;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(bip21))
|
||||||
|
{
|
||||||
|
LoadFromBIP21(vm, bip21, network);
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
decimal transactionAmountSum = 0;
|
decimal transactionAmountSum = 0;
|
||||||
|
|
||||||
if (command == "add-output")
|
if (command == "add-output")
|
||||||
@@ -477,7 +485,6 @@ namespace BTCPayServer.Controllers
|
|||||||
vm.Outputs.RemoveAt(index);
|
vm.Outputs.RemoveAt(index);
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!vm.Outputs.Any())
|
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)
|
private IActionResult ViewVault(WalletId walletId, PSBT psbt)
|
||||||
{
|
{
|
||||||
return View("WalletSendVault", new WalletSendVaultModel()
|
return View("WalletSendVault", new WalletSendVaultModel()
|
||||||
|
|||||||
@@ -15,26 +15,8 @@ namespace BTCPayServer.Models
|
|||||||
public StatusSeverity Severity { get; set; }
|
public StatusSeverity Severity { get; set; }
|
||||||
public bool AllowDismiss { get; set; } = true;
|
public bool AllowDismiss { get; set; } = true;
|
||||||
|
|
||||||
public string SeverityCSS
|
public string SeverityCSS => ToString(Severity);
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ParseNonJsonStatus(string s)
|
private void ParseNonJsonStatus(string s)
|
||||||
{
|
{
|
||||||
Message = s;
|
Message = s;
|
||||||
@@ -43,6 +25,23 @@ namespace BTCPayServer.Models
|
|||||||
: StatusSeverity.Success;
|
: 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
|
public enum StatusSeverity
|
||||||
{
|
{
|
||||||
Info,
|
Info,
|
||||||
|
|||||||
@@ -5,7 +5,14 @@
|
|||||||
ViewData["Title"] = "Manage wallet";
|
ViewData["Title"] = "Manage wallet";
|
||||||
ViewData.SetActivePageAndTitle(WalletsNavPages.Send);
|
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="row">
|
||||||
<div class="@(Model.Outputs.Count==1? "col-lg-6 transaction-output-form": "col-lg-8")">
|
<div class="@(Model.Outputs.Count==1? "col-lg-6 transaction-output-form": "col-lg-8")">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@@ -16,6 +23,7 @@
|
|||||||
<input type="hidden" asp-for="CurrentBalance" />
|
<input type="hidden" asp-for="CurrentBalance" />
|
||||||
<input type="hidden" asp-for="RecommendedSatoshiPerByte" />
|
<input type="hidden" asp-for="RecommendedSatoshiPerByte" />
|
||||||
<input type="hidden" asp-for="CryptoCode" />
|
<input type="hidden" asp-for="CryptoCode" />
|
||||||
|
<input type="hidden" name="BIP21" id="BIP21" />
|
||||||
<ul class="text-danger">
|
<ul class="text-danger">
|
||||||
@foreach (var errors in ViewData.ModelState.Where(pair => pair.Key == string.Empty && pair.Value.ValidationState == ModelValidationState.Invalid))
|
@foreach (var errors in ViewData.ModelState.Where(pair => pair.Key == string.Empty && pair.Value.ValidationState == ModelValidationState.Invalid))
|
||||||
{
|
{
|
||||||
@@ -118,6 +126,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="card">
|
<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">
|
<button class="btn btn-light collapsed" type="button" data-toggle="collapse" data-target="#accordian-advanced" aria-expanded="false" aria-controls="accordian-advanced">
|
||||||
Advanced settings
|
Advanced settings
|
||||||
@@ -163,6 +172,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" name="command" value="add-output" class="ml-1 btn btn-secondary">Add another destination </button>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,4 +44,12 @@ $(function () {
|
|||||||
updateFiatValue(outputAmountElement);
|
updateFiatValue(outputAmountElement);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#bip21parse").on("click", function(){
|
||||||
|
var bip21 = prompt("Paste BIP21 here");
|
||||||
|
if(bip21){
|
||||||
|
$("#BIP21").val(bip21);
|
||||||
|
$("form").submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user