Improve wallet nav (#3921)

* Fix rescan nav highlighting

* Wallet send wizard

* Wallet receive wizard

* Consistent wizard back button behaviour
This commit is contained in:
d11n 2022-07-04 06:20:08 +02:00 committed by GitHub
parent f30ddbf175
commit 181d4d5ea4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 606 additions and 429 deletions

View file

@ -544,6 +544,7 @@ namespace BTCPayServer.Tests
}
}
Driver.Navigate().Refresh();
Driver.FindElement(By.Id("CancelWizard")).Click();
return addressStr;
}

View file

@ -11,7 +11,6 @@ using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
@ -94,9 +93,8 @@ namespace BTCPayServer.Tests
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("BumpFee")).Click();
var err = s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
Assert.Contains("any UTXO available", err.Text);
Assert.Contains($"/stores/{s.StoreId}/invoices", s.Driver.Url);
Assert.Contains("any UTXO available", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text);
// But we should be able to bump from the wallet's page
s.GoToWallet(navPages: WalletsNavPages.Transactions);
@ -104,8 +102,8 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("BumpFee")).Click();
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
s.FindAlertMessage();
Assert.Contains($"/wallets/{s.WalletId}", s.Driver.Url);
Assert.Contains("Transaction broadcasted successfully", s.FindAlertMessage().Text);
}
[Fact(Timeout = TestTimeout)]
@ -1046,6 +1044,7 @@ namespace BTCPayServer.Tests
//you cannot use the Sign with NBX option without saving private keys when generating the wallet.
Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
s.Driver.FindElement(By.Id("CancelWizard")).Click();
s.Driver.FindElement(By.Id("WalletNav-Receive")).Click();
//generate a receiving address
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
@ -1070,6 +1069,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value"));
receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value");
s.Driver.FindElement(By.Id("CancelWizard")).Click();
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
s.GenerateWallet(cryptoCode, "", true);
@ -1164,6 +1164,7 @@ namespace BTCPayServer.Tests
Assert.Equal(parsedBip21.Address.ToString(),
s.Driver.FindElement(By.Id("Outputs_0__DestinationAddress")).GetAttribute("value"));
s.Driver.FindElement(By.Id("CancelWizard")).Click();
s.GoToWalletSettings(cryptoCode);
var settingsUrl = s.Driver.Url;
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
@ -1299,6 +1300,7 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
s.FindAlertMessage();
s.GoToWallet(null, WalletsNavPages.Transactions);
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();

View file

@ -13,6 +13,7 @@ using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments.PayJoin.Sender;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.Payment;
@ -23,7 +24,6 @@ namespace BTCPayServer.Controllers
{
public partial class UIWalletsController
{
[NonAction]
public async Task<CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendModel sendModel, CancellationToken cancellationToken)
{
@ -132,16 +132,17 @@ namespace BTCPayServer.Controllers
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIWallets",
AspAction = nameof(UIWalletsController.WalletSign),
AspAction = nameof(WalletSign),
RouteParameters = {
{ "walletId", walletId.ToString() },
{ "returnUrl", returnUrl }
{ "walletId", walletId.ToString() }
},
FormParameters =
{
{ "walletId", walletId.ToString() },
{ "psbt", psbt.ToHex() }
}
{
{ "walletId", walletId.ToString() },
{ "psbt", psbt.ToHex() },
{ "backUrl", returnUrl },
{ "returnUrl", returnUrl }
}
});
} catch (Exception ex) {
TempData[WellKnownTempData.ErrorMessage] = ex.Message;
@ -152,23 +153,29 @@ namespace BTCPayServer.Controllers
[HttpPost("{walletId}/sign")]
public async Task<IActionResult> WalletSign([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTViewModel vm, string returnUrl = null, string command = null)
WalletId walletId, WalletPSBTViewModel vm, string command = null)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
if (returnUrl is null)
returnUrl = Url.Action(nameof(WalletTransactions), new { walletId });
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
vm.BackUrl ??= HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath;
if (psbt is null || vm.InvalidPSBT)
{
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
return View("WalletSigningOptions", new WalletSigningOptionsModel(vm.SigningContext, returnUrl));
return View("WalletSigningOptions", new WalletSigningOptionsModel
{
SigningContext = vm.SigningContext,
ReturnUrl = vm.ReturnUrl,
BackUrl = vm.BackUrl
});
}
switch (command)
{
case "vault":
return ViewVault(walletId, vm.SigningContext);
return ViewVault(walletId, vm);
case "seed":
return SignWithSeed(walletId, vm.SigningContext);
return SignWithSeed(walletId, vm.SigningContext, vm.ReturnUrl, vm.BackUrl);
case "decode":
return await WalletPSBT(walletId, vm, "decode");
default:
@ -185,21 +192,36 @@ namespace BTCPayServer.Controllers
WellknownMetadataKeys.MasterHDKey);
if (extKey != null)
{
return await SignWithSeed(walletId,
new SignWithSeedViewModel { SeedOrKey = extKey, SigningContext = vm.SigningContext });
return await SignWithSeed(walletId, new SignWithSeedViewModel
{
SeedOrKey = extKey,
SigningContext = vm.SigningContext,
ReturnUrl = vm.ReturnUrl,
BackUrl = vm.BackUrl
});
}
}
}
return View("WalletSigningOptions", new WalletSigningOptionsModel(vm.SigningContext, returnUrl));
return View("WalletSigningOptions", new WalletSigningOptionsModel
{
SigningContext = vm.SigningContext,
ReturnUrl = vm.ReturnUrl,
BackUrl = vm.BackUrl
});
}
[HttpGet("{walletId}/psbt")]
public async Task<IActionResult> WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
WalletId walletId, string returnUrl)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
var vm = new WalletPSBTViewModel();
vm.CryptoCode = network.CryptoCode;
var referer = HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath;
var vm = new WalletPSBTViewModel
{
BackUrl = string.IsNullOrEmpty(returnUrl) ? null : referer,
ReturnUrl = returnUrl ?? referer,
CryptoCode = network.CryptoCode
};
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
if (derivationSchemeSettings == null)
@ -222,6 +244,8 @@ namespace BTCPayServer.Controllers
return NotFound();
vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet;
vm.BackUrl ??= HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath;
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
if (vm.InvalidPSBT)
{
@ -257,20 +281,27 @@ namespace BTCPayServer.Controllers
return RedirectToWalletPSBT(new WalletPSBTViewModel
{
PSBT = psbt.ToBase64(),
FileName = vm.FileName
FileName = vm.FileName,
ReturnUrl = vm.ReturnUrl,
BackUrl = vm.BackUrl
});
case "combine":
ModelState.Remove(nameof(vm.PSBT));
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel { OtherPSBT = psbt.ToBase64() });
return View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel
{
OtherPSBT = psbt.ToBase64(),
ReturnUrl = vm.ReturnUrl,
BackUrl = vm.BackUrl
});
case "broadcast":
return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel
{
return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel
{
SigningContext = new SigningContextModel(psbt)
});
}
SigningContext = new SigningContextModel(psbt),
ReturnUrl = vm.ReturnUrl,
BackUrl = vm.BackUrl
});
default:
return View("WalletPSBTDecoded", vm);
@ -452,7 +483,7 @@ namespace BTCPayServer.Controllers
});
vm.SigningContext.PSBT = proposedPayjoin.ToBase64();
vm.SigningContext.OriginalPSBT = psbt.ToBase64();
return ViewVault(walletId, vm.SigningContext);
return ViewVault(walletId, vm);
}
}
catch (PayjoinReceiverException ex)
@ -523,17 +554,18 @@ namespace BTCPayServer.Controllers
{
TempData[WellKnownTempData.SuccessMessage] = $"Transaction broadcasted successfully ({transaction.GetHash()})";
}
var returnUrl = this.HttpContext.Request.Query["returnUrl"].FirstOrDefault();
if (returnUrl is not null)
if (!string.IsNullOrEmpty(vm.ReturnUrl))
{
return LocalRedirect(returnUrl);
return LocalRedirect(vm.ReturnUrl);
}
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
}
case "analyze-psbt":
return RedirectToWalletPSBT(new WalletPSBTViewModel()
return RedirectToWalletPSBT(new WalletPSBTViewModel
{
PSBT = psbt.ToBase64()
PSBT = psbt.ToBase64(),
ReturnUrl = vm.ReturnUrl,
BackUrl = vm.BackUrl
});
case "decode":
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
@ -568,9 +600,10 @@ namespace BTCPayServer.Controllers
}
sourcePSBT = sourcePSBT.Combine(psbt);
TempData[WellKnownTempData.SuccessMessage] = "PSBT Successfully combined!";
return RedirectToWalletPSBT(new WalletPSBTViewModel()
return RedirectToWalletPSBT(new WalletPSBTViewModel
{
PSBT = sourcePSBT.ToBase64()
PSBT = sourcePSBT.ToBase64(),
ReturnUrl = vm.ReturnUrl
});
}
}

View file

@ -31,6 +31,7 @@ using NBitcoin;
using BTCPayServer.Client.Models;
using BTCPayServer.Logging;
using BTCPayServer.Services.Wallets.Export;
using Microsoft.AspNetCore.Http;
using NBXplorer;
using NBXplorer.Client;
using NBXplorer.DerivationStrategy;
@ -349,7 +350,8 @@ namespace BTCPayServer.Controllers
}
[HttpGet("{walletId}/receive")]
public IActionResult WalletReceive([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId)
public IActionResult WalletReceive([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
[FromQuery] string returnUrl = null)
{
if (walletId?.StoreId == null)
return NotFound();
@ -369,26 +371,26 @@ namespace BTCPayServer.Controllers
Request.GetAbsoluteUri(Url.Action(nameof(PayJoinEndpointController.Submit), "PayJoinEndpoint",
new { walletId.CryptoCode })));
}
return View(new WalletReceiveViewModel()
return View(new WalletReceiveViewModel
{
CryptoCode = walletId.CryptoCode,
Address = address?.ToString(),
CryptoImage = GetImage(paymentMethod.PaymentId, network),
PaymentLink = bip21.ToString()
PaymentLink = bip21.ToString(),
ReturnUrl = returnUrl ?? HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath
});
}
[HttpPost]
[Route("{walletId}/receive")]
[HttpPost("{walletId}/receive")]
public async Task<IActionResult> WalletReceive([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
WalletReceiveViewModel viewModel, string command)
WalletReceiveViewModel vm, string command)
{
if (walletId?.StoreId == null)
return NotFound();
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null)
return NotFound();
switch (command)
@ -397,7 +399,7 @@ namespace BTCPayServer.Controllers
var address = await _walletReceiveService.UnReserveAddress(walletId);
if (!string.IsNullOrEmpty(address))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
AllowDismiss = true,
Message = $"Address {address} was unreserved.",
@ -414,7 +416,7 @@ namespace BTCPayServer.Controllers
await SendFreeMoney(cheater, walletId, paymentMethod);
break;
}
return RedirectToAction(nameof(WalletReceive), new { walletId });
return RedirectToAction(nameof(WalletReceive), new { walletId, returnUrl = vm.ReturnUrl });
}
private async Task SendFreeMoney(Cheater cheater, WalletId walletId, DerivationSchemeSettings paymentMethod)
@ -458,8 +460,9 @@ namespace BTCPayServer.Controllers
[HttpGet("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string defaultDestination = null, string defaultAmount = null, string[] bip21 = null)
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
string defaultDestination = null, string defaultAmount = null, string[] bip21 = null,
[FromQuery] string returnUrl = null)
{
if (walletId?.StoreId == null)
return NotFound();
@ -475,7 +478,12 @@ namespace BTCPayServer.Controllers
rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, storeData.DefaultCurrency);
double.TryParse(defaultAmount, out var amount);
var model = new WalletSendModel() { CryptoCode = walletId.CryptoCode };
var model = new WalletSendModel
{
CryptoCode = walletId.CryptoCode,
ReturnUrl = returnUrl ?? HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath
};
if (bip21?.Any() is true)
{
foreach (var link in bip21)
@ -590,6 +598,7 @@ namespace BTCPayServer.Controllers
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null || network.ReadonlyWallet)
return NotFound();
vm.SupportRBF = network.SupportRBF;
vm.NBXSeedAvailable = await GetSeed(walletId, network) != null;
if (!string.IsNullOrEmpty(bip21))
@ -857,7 +866,12 @@ namespace BTCPayServer.Controllers
switch (command)
{
case "sign":
return await WalletSign(walletId, new WalletPSBTViewModel() { SigningContext = signingContext });
return await WalletSign(walletId, new WalletPSBTViewModel
{
SigningContext = signingContext,
ReturnUrl = vm.ReturnUrl,
BackUrl = vm.BackUrl
});
case "analyze-psbt":
var name =
$"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
@ -920,24 +934,30 @@ namespace BTCPayServer.Controllers
ModelState.Clear();
}
private IActionResult ViewVault(WalletId walletId, SigningContextModel signingContext)
private IActionResult ViewVault(WalletId walletId, WalletPSBTViewModel vm)
{
return View(nameof(WalletSendVault),
new WalletSendVaultModel()
new WalletSendVaultModel
{
SigningContext = signingContext,
SigningContext = vm.SigningContext,
WalletId = walletId.ToString(),
WebsocketPath = this.Url.Action(nameof(UIVaultController.VaultBridgeConnection), "UIVault",
new { walletId = walletId.ToString() })
WebsocketPath = Url.Action(nameof(UIVaultController.VaultBridgeConnection), "UIVault",
new { walletId = walletId.ToString() }),
ReturnUrl = vm.ReturnUrl,
BackUrl = vm.BackUrl
});
}
[HttpPost]
[Route("{walletId}/vault")]
[HttpPost("{walletId}/vault")]
public IActionResult WalletSendVault([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
WalletSendVaultModel model)
{
return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel() { SigningContext = model.SigningContext });
return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel
{
SigningContext = model.SigningContext,
ReturnUrl = model.ReturnUrl,
BackUrl = model.BackUrl
});
}
private IActionResult RedirectToWalletPSBTReady(WalletPSBTReadyViewModel vm)
@ -962,9 +982,13 @@ namespace BTCPayServer.Controllers
redirectVm.FormParameters.Remove("command");
redirectVm.FormParameters.Add("command", "broadcast");
}
if (this.HttpContext.Request.Query["returnUrl"].FirstOrDefault() is string returnUrl)
if (vm.ReturnUrl != null)
{
redirectVm.RouteParameters.Add("returnUrl", returnUrl);
redirectVm.FormParameters.Add("returnUrl", vm.ReturnUrl);
}
if (vm.BackUrl != null)
{
redirectVm.FormParameters.Add("backUrl", vm.BackUrl);
}
return View("PostRedirect", redirectVm);
}
@ -987,17 +1011,29 @@ namespace BTCPayServer.Controllers
{
AspController = "UIWallets",
AspAction = nameof(WalletPSBT),
RouteParameters = { { "walletId", this.RouteData?.Values["walletId"]?.ToString() } },
FormParameters = { { "psbt", vm.PSBT }, { "fileName", vm.FileName }, { "command", "decode" }, }
RouteParameters = { { "walletId", RouteData.Values["walletId"]?.ToString() } },
FormParameters =
{
{ "psbt", vm.PSBT },
{ "fileName", vm.FileName },
{ "backUrl", vm.BackUrl },
{ "returnUrl", vm.ReturnUrl },
{ "command", "decode" }
}
};
return View("PostRedirect", redirectVm);
}
[HttpGet("{walletId}/psbt/seed")]
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
SigningContextModel signingContext)
SigningContextModel signingContext, string returnUrl, string backUrl)
{
return View(nameof(SignWithSeed), new SignWithSeedViewModel { SigningContext = signingContext });
return View(nameof(SignWithSeed), new SignWithSeedViewModel
{
SigningContext = signingContext,
ReturnUrl = returnUrl,
BackUrl = backUrl
});
}
[HttpPost("{walletId}/psbt/seed")]
@ -1082,7 +1118,9 @@ namespace BTCPayServer.Controllers
{
SigningKey = signingKey.GetWif(network.NBitcoinNetwork).ToString(),
SigningKeyPath = rootedKeyPath?.ToString(),
SigningContext = viewModel.SigningContext
SigningContext = viewModel.SigningContext,
ReturnUrl = viewModel.ReturnUrl,
BackUrl = viewModel.BackUrl
});
}
@ -1225,12 +1263,14 @@ namespace BTCPayServer.Controllers
i++;
}
parameters.Add("returnUrl", Url.Action(nameof(WalletTransactions), new { walletId }));
var backUrl = Url.Action(nameof(WalletTransactions), new { walletId });
parameters.Add("returnUrl", backUrl);
parameters.Add("backUrl", backUrl);
return View("PostRedirect",
new PostRedirectViewModel
{
AspController = "UIWallets",
AspAction = nameof(UIWalletsController.WalletCPFP),
AspAction = nameof(WalletCPFP),
RouteParameters = { { "walletId", walletId.ToString() } },
FormParameters = parameters
});
@ -1324,6 +1364,7 @@ namespace BTCPayServer.Controllers
public string CryptoCode { get; set; }
public string Address { get; set; }
public string PaymentLink { get; set; }
public string? ReturnUrl { get; set; }
}
public class SendToAddressResult

View file

@ -14,6 +14,9 @@ namespace BTCPayServer.Models.WalletViewModels
[Display(Name = "Optional seed passphrase")]
public string Passphrase { get; set; }
public string BackUrl { get; set; }
public string ReturnUrl { get; set; }
public ExtKey GetExtKey(Network network)
{

View file

@ -1,3 +1,4 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -12,6 +13,9 @@ namespace BTCPayServer.Models.WalletViewModels
public string PSBT { get; set; }
[Display(Name = "Upload PSBT from file...")]
public IFormFile UploadedPSBTFile { get; set; }
public string BackUrl { get; set; }
public string ReturnUrl { get; set; }
public PSBT GetSourcePSBT(Network network)
{

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NBitcoin;
@ -31,6 +32,8 @@ namespace BTCPayServer.Models.WalletViewModels
public List<DestinationViewModel> Destinations { get; set; } = new List<DestinationViewModel>();
public List<InputViewModel> Inputs { get; set; } = new List<InputViewModel>();
public string FeeRate { get; set; }
public string BackUrl { get; set; }
public string ReturnUrl { get; set; }
internal void SetErrors(IList<PSBTError> errors)
{

View file

@ -51,6 +51,7 @@ namespace BTCPayServer.Models.WalletViewModels
return psbt;
}
public bool InvalidPSBT { get; set; }
async Task<PSBT> GetPSBTCore(Network network)
{
if (UploadedPSBTFile != null)

View file

@ -67,6 +67,9 @@ namespace BTCPayServer.Models.WalletViewModels
[Display(Name = "UTXOs to spend from")]
public IEnumerable<string> SelectedInputs { get; set; }
public string BackUrl { get; set; }
public string ReturnUrl { get; set; }
public class InputSelectionOption
{

View file

@ -4,6 +4,8 @@ namespace BTCPayServer.Models.WalletViewModels
{
public string WalletId { get; set; }
public string WebsocketPath { get; set; }
public SigningContextModel SigningContext { get; set; } = new SigningContextModel();
public string BackUrl { get; set; }
public string ReturnUrl { get; set; }
public SigningContextModel SigningContext { get; set; } = new ();
}
}

View file

@ -1,18 +1,11 @@
using System.Collections.Generic;
using System;
namespace BTCPayServer.Models.WalletViewModels
{
public class WalletSigningOptionsModel
{
public WalletSigningOptionsModel(
SigningContextModel signingContext,
string returnUrl)
{
SigningContext = signingContext;
ReturnUrl = returnUrl;
}
public SigningContextModel SigningContext { get; }
public string ReturnUrl { get; }
public SigningContextModel SigningContext { get; set; }
public string BackUrl { get; set; }
public string ReturnUrl { get; set; }
}
}

View file

@ -1,12 +1,12 @@
<script id="VaultConnection" type="text/template">
<div class="vault-feedback vault-feedback1 mb-2 d-flex">
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-1"></span>
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-grow"></span>
</div>
<div class="vault-feedback vault-feedback2 mb-2 d-flex">
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-1"></span>
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-grow"></span>
</div>
<div class="vault-feedback vault-feedback3 mb-2 d-flex">
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-1"></span>
<span class="vault-feedback-icon mt-1 me-2"></span> <span class="vault-feedback-content flex-grow"></span>
</div>
<div id="pin-input" class="mt-4" style="display: none;">
<div class="row">

View file

@ -19,5 +19,3 @@
}
@RenderBody()

View file

@ -1,21 +1,24 @@
@using BTCPayServer.Controllers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model SignWithSeedViewModel
@{
var walletId = Context.GetRouteValue("walletId").ToString();
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
Layout = "_LayoutWizard";
ViewData.SetActivePage(WalletsNavPages.Send, "Sign PSBT", walletId);
var returnUrl = this.Context.Request.Query["returnUrl"].FirstOrDefault();
}
@section Navbar {
@if (returnUrl is string)
{
<a href="@returnUrl" id="GoBack">
<vc:icon symbol="back" />
</a>
<a href="@returnUrl" class="cancel">
@if (backUrl != null)
{
<a href="@backUrl" id="GoBack">
<vc:icon symbol="back" />
</a>
}
<a href="@cancelUrl" id="CancelWizard" class="cancel">
<vc:icon symbol="close" />
</a>
}
}
<header class="text-center">
@ -37,8 +40,10 @@
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post" asp-action="SignWithSeed" asp-route-walletId="@walletId" asp-route-returnUrl="@returnUrl">
<form method="post" asp-action="SignWithSeed" asp-route-walletId="@walletId">
<partial name="SigningContext" for="SigningContext"/>
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="BackUrl" />
<div class="form-group">
<label asp-for="SeedOrKey" class="form-label"></label>
<input asp-for="SeedOrKey" class="form-control" autocomplete="off" autocorrect="off" autocapitalize="off"/>

View file

@ -1,11 +1,29 @@
@model WalletPSBTViewModel
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Controllers
@using BTCPayServer.TagHelpers
@using BundlerMinifier.TagHelpers
@model WalletPSBTViewModel
@addTagHelper *, BundlerMinifier.TagHelpers
@{
var walletId = Context.GetRouteValue("walletId").ToString();
Layout = "../Shared/_NavLayout.cshtml";
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
Layout = "_LayoutWizard";
ViewData.SetActivePage(WalletsNavPages.PSBT, "Decode PSBT", walletId);
}
@section Navbar {
@if (backUrl != null)
{
<a href="@backUrl" id="GoBack">
<vc:icon symbol="back" />
</a>
}
<a href="@cancelUrl" id="CancelWizard" class="cancel">
<vc:icon symbol="close" />
</a>
}
@section PageHeadContent {
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
}
@ -23,42 +41,43 @@
</script>
}
<h3 class="mb-3">@ViewData["Title"]</h3>
<header class="text-center">
<h1>@ViewData["Title"]</h1>
<p class="lead text-secondary mt-3 mx-md-5">You can decode a PSBT by either pasting its content, uploading the file or scanning the wallet QR code.</p>
</header>
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
@if (Model.Errors != null && Model.Errors.Count != 0)
{
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<vc:icon symbol="close" />
</button>
@foreach (var error in Model.Errors)
{
<span>@error</span>
<br/>
}
</div>
}
<div class="my-5">
@if (Model.Errors != null && Model.Errors.Count != 0)
{
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<vc:icon symbol="close" />
</button>
@foreach (var error in Model.Errors)
{
<span>@error</span>
<br/>
}
</div>
}
<p>You can decode a PSBT by either pasting its content, uploading the file or scanning the wallet QR code.</p>
<form class="form-group" method="post" asp-action="WalletPSBT" asp-route-walletId="@walletId" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="PSBT" class="form-label"></label>
<textarea class="form-control" rows="5" asp-for="PSBT"></textarea>
<span asp-validation-for="PSBT" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="UploadedPSBTFile" class="form-label"></label>
<input asp-for="UploadedPSBTFile" type="file" class="form-control">
</div>
<div class="d-flex">
<button type="submit" name="command" value="decode" class="btn btn-primary mt-2" id="Decode">Decode PSBT</button>
<button type="button" id="scanqrcode" class="btn btn-secondary only-for-js ms-3 mt-2" data-bs-toggle="modal" data-bs-target="#scanModal">Scan wallet QR with camera</button>
</div>
</form>
</div>
<form class="form-group" method="post" asp-action="WalletPSBT" asp-route-walletId="@walletId" enctype="multipart/form-data">
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="BackUrl" />
<div class="form-group">
<label asp-for="PSBT" class="form-label"></label>
<textarea class="form-control" rows="5" asp-for="PSBT"></textarea>
<span asp-validation-for="PSBT" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="UploadedPSBTFile" class="form-label"></label>
<input asp-for="UploadedPSBTFile" type="file" class="form-control">
</div>
<div class="d-flex">
<button type="submit" name="command" value="decode" class="btn btn-primary mt-2" id="Decode">Decode PSBT</button>
<button type="button" id="scanqrcode" class="btn btn-secondary only-for-js ms-3 mt-2" data-bs-toggle="modal" data-bs-target="#scanModal">Scan wallet QR with camera</button>
</div>
</form>
</div>
<partial name="ShowQR"/>
<partial name="CameraScanner"/>

View file

@ -1,12 +1,22 @@
@using BTCPayServer.Controllers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model WalletPSBTCombineViewModel
@{
var walletId = Context.GetRouteValue("walletId").ToString();
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
Layout = "_LayoutWizard";
ViewData.SetActivePage(WalletsNavPages.PSBT, "Combine PSBT", walletId);
}
@section Navbar {
<a asp-action="WalletSend" asp-route-walletId="@Context.GetRouteValue("walletId")" class="cancel">
@if (backUrl != null)
{
<a href="@backUrl" id="GoBack">
<vc:icon symbol="back" />
</a>
}
<a href="@cancelUrl" id="CancelWizard" class="cancel">
<vc:icon symbol="close" />
</a>
}
@ -17,6 +27,8 @@
<form class="form-group" method="post" asp-action="WalletPSBTCombine" asp-route-walletId="@Context.GetRouteValue("walletId")" enctype="multipart/form-data">
<input type="hidden" asp-for="OtherPSBT"/>
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="BackUrl" />
<div class="form-group">
<label asp-for="PSBT" class="form-label"></label>
<textarea class="form-control" rows="5" asp-for="PSBT"></textarea>

View file

@ -1,13 +1,18 @@
@using BTCPayServer.Controllers
@using BTCPayServer.TagHelpers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BundlerMinifier.TagHelpers
@model WalletPSBTViewModel
@addTagHelper *, BundlerMinifier.TagHelpers
@{
var walletId = Context.GetRouteValue("walletId").ToString();
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
var isReady = !Model.HasErrors;
var isSignable = !isReady;
var needsExport = !isSignable && !isReady;
Layout = "_LayoutWizard";
ViewData.SetActivePage(WalletsNavPages.PSBT, isReady ? "Confirm broadcasting this transaction" : "Transaction Details", walletId);
var returnUrl = this.Context.Request.Query["returnUrl"].FirstOrDefault();
}
@section PageHeadContent {
@ -38,12 +43,15 @@
}
@section Navbar {
@if (returnUrl is string)
{
<a href="@returnUrl" class="cancel">
@if (backUrl != null)
{
<a href="@backUrl" id="GoBack">
<vc:icon symbol="back" />
</a>
}
<a href="@cancelUrl" id="CancelWizard" class="cancel">
<vc:icon symbol="close" />
</a>
}
}
<header class="text-center mb-3">
@ -59,17 +67,21 @@
<input type="hidden" asp-for="NBXSeedAvailable"/>
<input type="hidden" asp-for="PSBT"/>
<input type="hidden" asp-for="FileName"/>
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="BackUrl" />
<div class="d-flex flex-column flex-sm-row flex-wrap justify-content-center align-items-sm-center">
<button type="submit" id="SignTransaction" name="command" value="sign" asp-route-returnUrl="@returnUrl" class="btn btn-primary">Sign transaction</button>
<button type="submit" id="SignTransaction" name="command" value="sign" class="btn btn-primary">Sign transaction</button>
</div>
</form>
}
else if (isReady)
{
<form method="post" asp-action="WalletPSBTReady" asp-route-walletId="@walletId" asp-route-returnUrl="@returnUrl" class="my-5">
<form method="post" asp-action="WalletPSBTReady" asp-route-walletId="@walletId" class="my-5">
<input type="hidden" asp-for="SigningKey" />
<input type="hidden" asp-for="SigningKeyPath" />
<partial name="SigningContext" for="SigningContext" />
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="BackUrl" />
<div class="d-flex flex-column flex-sm-row flex-wrap justify-content-center align-items-sm-center">
@if (!string.IsNullOrEmpty(Model.SigningContext?.PayJoinBIP21))
{
@ -105,6 +117,8 @@ else
<form method="post" asp-action="WalletPSBT" asp-route-walletId="@walletId" class="mb-2">
<input type="hidden" asp-for="CryptoCode"/>
<input type="hidden" asp-for="PSBT"/>
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="BackUrl" />
<div class="d-flex flex-column flex-sm-row flex-wrap align-items-sm-center">
<button name="command" type="submit" class="btn btn-primary mb-3 mb-sm-0 me-sm-2" value="save-psbt">Download PSBT file</button>
<button name="command" type="button" class="btn btn-primary mb-3 mb-sm-0 me-sm-2 only-for-js" data-bs-toggle="modal" data-bs-target="#scan-qr-modal">Show QR for wallet camera</button>
@ -152,6 +166,8 @@ else
<div id="PSBTOptionsImportContent" class="accordion-collapse collapse" aria-labelledby="PSBTOptionsImportHeader" data-bs-parent="#PSBTOptions">
<div class="accordion-body">
<form method="post" asp-action="WalletPSBT" asp-route-walletId="@walletId" enctype="multipart/form-data" class="mb-2">
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="BackUrl" />
<div class="form-group">
<label for="ImportedPSBT" class="form-label">PSBT content</label>
<textarea id="ImportedPSBT" name="PSBT" class="form-control" rows="5"></textarea>
@ -180,6 +196,8 @@ else
<div class="accordion-body">
<form method="post" asp-action="WalletPSBT" asp-route-walletId="@walletId" class="mb-2">
<input type="hidden" asp-for="PSBT"/>
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="BackUrl" />
<p class="mb-2">For exporting the signed PSBT and transaction information to a wallet, update the PSBT.</p>
<button id="update-psbt" type="submit" name="command" value="update" class="btn btn-secondary">Update PSBT</button>
<p class="mt-4 mb-2">For batching transactions, you can combine this PSBT with another one.</p>

View file

@ -1,9 +1,13 @@
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.Services.BTCPayServerEnvironment env
@using BTCPayServer.Controllers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Components.QRCode
@model BTCPayServer.Controllers.WalletReceiveViewModel
@{
var walletId = Context.GetRouteValue("walletId").ToString();
Layout = "../Shared/_NavLayout.cshtml";
var returnUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
Layout = "_LayoutWizard";
ViewData.SetActivePage(WalletsNavPages.Receive, $"Receive {Model.CryptoCode}", walletId);
}
@ -12,88 +16,86 @@
<link href="~/main/qrcode.css" rel="stylesheet" asp-append-version="true"/>
}
<div class="row no-gutters">
<div class="col-xl-8 col-xxl-constrain">
<form method="post" asp-action="WalletReceive">
@if (string.IsNullOrEmpty(Model.Address))
@section Navbar {
<a href="@returnUrl" id="CancelWizard" class="cancel">
<vc:icon symbol="close" />
</a>
}
<header class="text-center">
<h1>@ViewData["Title"]</h1>
</header>
<form method="post" asp-action="WalletReceive" class="my-5">
<input type="hidden" asp-for="ReturnUrl" />
@if (string.IsNullOrEmpty(Model.Address))
{
<div class="d-grid gap-3 col-sm-10 col-md-8 col-lg-7 col-xxl-6 mx-auto">
<button id="generateButton" class="btn btn-primary" type="submit" name="command" value="generate-new-address">Generate next available @Model.CryptoCode address</button>
@if (env.CheatMode)
{
<div class="d-flex flex-wrap gap-3">
<button id="generateButton" class="btn btn-primary" type="submit" name="command" value="generate-new-address">Generate next available @Model.CryptoCode address</button>
@if (env.CheatMode)
{
<button type="submit" name="command" value="fill-wallet" class="btn btn-info">Cheat Mode: Send transactions to this wallet</button>
}
</div>
<button type="submit" name="command" value="fill-wallet" class="btn btn-info">Cheat Mode: Send transactions to this wallet</button>
}
else
{
<h3 class="mb-4">@Model.CryptoCode&nbsp;Address</h3>
<noscript>
<div class="m-sm-0 p-sm-0">
<div class="form-group">
<input type="text" class="form-control " readonly="readonly" asp-for="Address" id="address"/>
</div>
<div class="form-group">
<input type="text" class="form-control" readonly="readonly" asp-for="PaymentLink" id="payment-link"/>
</div>
<div class="row mt-4">
<div class="col-12 col-sm-6">
<button type="submit" name="command" value="generate-new-address" class="btn btn-primary w-100">Generate another address</button>
</div>
<div class="col-12 col-sm-6 mt-4 mt-sm-0">
<button type="submit" name="command" value="unreserve-current-address" class="btn btn-secondary w-100">Unreserve this address</button>
</div>
</div>
}
else
{
<noscript>
<div class="form-group">
<input type="text" class="form-control " readonly="readonly" asp-for="Address" id="address"/>
</div>
<div class="form-group">
<input type="text" class="form-control" readonly="readonly" asp-for="PaymentLink" id="payment-link"/>
</div>
<div class="row mt-4">
<div class="col-12 col-sm-6">
<button type="submit" name="command" value="generate-new-address" class="btn btn-primary w-100">Generate another address</button>
</div>
<div class="col-12 col-sm-6 mt-4 mt-sm-0">
<button type="submit" name="command" value="unreserve-current-address" class="btn btn-secondary w-100">Unreserve this address</button>
</div>
</div>
</noscript>
<div class="only-for-js col-sm-10 col-xxl-8 mx-auto" id="app">
<div class="tab-content text-center">
<div class="tab-pane" id="link-tab" role="tabpanel">
<div class="qr-container mb-3">
<img src="@Model.CryptoImage" class="qr-icon" alt="@Model.CryptoCode"/>
<vc:qr-code data="@Model.PaymentLink"/>
</div>
<div class="form-group">
<div class="input-group" data-clipboard="@Model.PaymentLink">
<input type="text" class="form-control" style="cursor:copy" readonly="readonly" value="@Model.PaymentLink" id="payment-link"/>
<button type="button" class="btn btn-outline-secondary p-2" style="width:7em;" data-clipboard-confirm>
<vc:icon symbol="copy"/>
</button>
</div>
</div>
</noscript>
<div class="only-for-js m-sm-0 p-sm-0" id="app">
<div class="mb-5">
<div class="tab-content">
<div class="tab-pane" id="link-tab" role="tabpanel">
<div class="qr-container mb-3">
<img src="@Model.CryptoImage" class="qr-icon" alt="@Model.CryptoCode"/>
<vc:qr-code data="@Model.PaymentLink"/>
</div>
<div class="form-group">
<div class="input-group" data-clipboard="@Model.PaymentLink">
<input type="text" class="form-control" style="cursor:copy" readonly="readonly" value="@Model.PaymentLink" id="payment-link"/>
<button type="button" class="btn btn-outline-secondary p-2" style="width:7em;" data-clipboard-confirm>
<vc:icon symbol="copy"/>
</button>
</div>
</div>
</div>
<div class="tab-pane show active" id="address-tab" role="tabpanel">
<div class="qr-container mb-3">
<img src="@Model.CryptoImage" class="qr-icon" alt="@Model.CryptoCode"/>
<vc:qr-code data="@Model.Address"/>
</div>
<div class="form-group">
<div class="input-group" data-clipboard="@Model.Address">
<input type="text" class="form-control" style="cursor:copy" readonly="readonly" value="@Model.Address" id="address"/>
<button type="button" class="input-group-text btn btn-outline-secondary p-2" style="width:7em;" data-clipboard-confirm>
<vc:icon symbol="copy"/>
</button>
</div>
</div>
</div>
</div>
<div class="nav">
<a class="btcpay-pill active" data-bs-toggle="tab" href="#address-tab">Address</a>
<a class="btcpay-pill " data-bs-toggle="tab" href="#link-tab">Link</a>
</div>
<div class="tab-pane show active" id="address-tab" role="tabpanel">
<div class="qr-container mb-3">
<img src="@Model.CryptoImage" class="qr-icon" alt="@Model.CryptoCode"/>
<vc:qr-code data="@Model.Address"/>
</div>
<div class="form-group">
<div class="input-group" data-clipboard="@Model.Address">
<input type="text" class="form-control" style="cursor:copy" readonly="readonly" value="@Model.Address" id="address"/>
<button type="button" class="input-group-text btn btn-outline-secondary p-2" style="width:7em;" data-clipboard-confirm>
<vc:icon symbol="copy"/>
</button>
</div>
</div>
<div class="row">
<div class="col-12 col-sm-6">
<button type="submit" name="command" value="generate-new-address" class="btn btn-primary w-100">Generate another address</button>
</div>
<div class="col-12 col-sm-6 mt-4 mt-sm-0">
<button type="submit" name="command" value="unreserve-current-address" class="btn btn-secondary w-100">Unreserve this address</button>
</div>
</div>
</div>
}
</form>
</div>
</div>
</div>
<div class="nav justify-content-center">
<a class="btcpay-pill active" data-bs-toggle="tab" href="#address-tab">Address</a>
<a class="btcpay-pill " data-bs-toggle="tab" href="#link-tab">Link</a>
</div>
</div>
<div class="d-grid gap-3 col-sm-8 col-sm-6 col-lg-5 mx-auto mt-5">
<button type="submit" name="command" value="generate-new-address" class="btn btn-primary w-100">Generate another address</button>
<button type="submit" name="command" value="unreserve-current-address" class="btn btn-secondary w-100">Unreserve this address</button>
</div>
}
</form>

View file

@ -2,7 +2,7 @@
@{
var walletId = Context.GetRouteValue("walletId").ToString();
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePage(WalletsNavPages.Rescan, "Rescan Wallet", walletId);
ViewData.SetActivePage(WalletsNavPages.Settings, "Rescan Wallet", walletId);
}
<h3 class="mb-3">@ViewData["Title"]</h3>

View file

@ -1,14 +1,32 @@
@addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.Security.ContentSecurityPolicies csp
@using Microsoft.AspNetCore.Mvc.ModelBinding
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.TagHelpers
@using BundlerMinifier.TagHelpers
@using BTCPayServer.Controllers
@model WalletSendModel
@{
var walletId = Context.GetRouteValue("walletId").ToString();
Layout = "../Shared/_NavLayout.cshtml";
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
Layout = "_LayoutWizard";
ViewData.SetActivePage(WalletsNavPages.Send, $"Send {Model.CryptoCode}", walletId);
csp.Add("worker-src", "blob:");
}
@section Navbar {
@if (backUrl != null)
{
<a href="@backUrl" id="GoBack">
<vc:icon symbol="back" />
</a>
}
<a href="@cancelUrl" id="CancelWizard" class="cancel">
<vc:icon symbol="close" />
</a>
}
@section PageHeadContent
{
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
@ -29,214 +47,219 @@
<partial name="CameraScanner"/>
<div class="row">
<div class="col-xl-8 col-xxl-constrain @(!Model.InputSelection && Model.Outputs.Count == 1 ? "transaction-output-form" : "")">
<form method="post" asp-action="WalletSend" asp-route-walletId="@walletId">
<input type="hidden" asp-for="InputSelection" />
<input type="hidden" asp-for="FiatDivisibility" />
<input type="hidden" asp-for="CryptoDivisibility" />
<input type="hidden" asp-for="NBXSeedAvailable" />
<input type="hidden" asp-for="Fiat" />
<input type="hidden" asp-for="Rate" />
<input type="hidden" asp-for="CurrentBalance" />
<input type="hidden" asp-for="ImmatureBalance" />
<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))
{
foreach (var error in errors.Value.Errors)
{
<li>@error.ErrorMessage</li>
}
}
</ul>
<header class="text-center">
<h1>@ViewData["Title"]</h1>
</header>
@if (Model.Outputs.Count == 1)
<form method="post" asp-action="WalletSend" asp-route-walletId="@walletId" class="my-5">
<input type="hidden" asp-for="InputSelection" />
<input type="hidden" asp-for="FiatDivisibility" />
<input type="hidden" asp-for="CryptoDivisibility" />
<input type="hidden" asp-for="NBXSeedAvailable" />
<input type="hidden" asp-for="Fiat" />
<input type="hidden" asp-for="Rate" />
<input type="hidden" asp-for="CurrentBalance" />
<input type="hidden" asp-for="ImmatureBalance" />
<input type="hidden" asp-for="CryptoCode" />
<input type="hidden" asp-for="BackUrl" />
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" name="BIP21" id="BIP21" />
@if (!ViewContext.ModelState.IsValid)
{
<ul class="text-danger">
@foreach (var errors in ViewData.ModelState.Where(pair => pair.Key == string.Empty && pair.Value.ValidationState == ModelValidationState.Invalid))
{
<div class="form-group">
<div class="d-flex align-items-center justify-content-between">
<label asp-for="Outputs[0].DestinationAddress" class="form-label"></label>
<button type="submit" name="command" value="add-output" class="d-inline-block ms-2 btn text-primary btn-link p-0 mb-2">
<span class="fa fa-plus"></span> Add another destination
</button>
</div>
<input asp-for="Outputs[0].DestinationAddress" class="form-control font-monospace" autofocus autocomplete="off" />
<span asp-validation-for="Outputs[0].DestinationAddress" class="text-danger"></span>
foreach (var error in errors.Value.Errors)
{
<li>@error.ErrorMessage</li>
}
}
</ul>
}
@if (Model.Outputs.Count == 1)
{
<div class="form-group">
<div class="d-flex align-items-center justify-content-between">
<label asp-for="Outputs[0].DestinationAddress" class="form-label"></label>
<button type="submit" name="command" value="add-output" class="d-inline-block ms-2 btn text-primary btn-link p-0 mb-2">
<span class="fa fa-plus"></span> Add another destination
</button>
</div>
<input asp-for="Outputs[0].DestinationAddress" class="form-control font-monospace" autofocus autocomplete="off" />
<span asp-validation-for="Outputs[0].DestinationAddress" class="text-danger"></span>
</div>
<div class="form-group">
<div class="d-flex align-items-center justify-content-between">
<label asp-for="Outputs[0].Amount" class="form-label"></label>
<button type="submit" name="command" value="toggle-input-selection" class="d-inline-block ms-2 btn text-primary btn-link p-0 mb-2" id="toggleInputSelection"><span class="fa fa-@(Model.InputSelection ? "eye-slash" : "eye") "></span> @(Model.InputSelection ? "Hide" : "Show") coin selection</button>
</div>
<div class="input-group">
<input asp-for="Outputs[0].Amount" type="number" inputmode="decimal" step="any" min="0" asp-format="{0}" class="form-control output-amount hide-number-spin" />
<div class="input-group-text fiat-value" style="display:none;">
<span class="input-group-text p-0 border-0">=</span>
<input type="number" inputmode="decimal" class="input-group-text fiat-value-edit-input py-0 border-0 hide-number-spin" min="0" step="any" style="max-width:100px" />
<span class="input-group-text p-0 border-0">@Model.Fiat</span>
</div>
<div class="form-group">
<div class="d-flex align-items-center justify-content-between">
<label asp-for="Outputs[0].Amount" class="form-label"></label>
<button type="submit" name="command" value="toggle-input-selection" class="d-inline-block ms-2 btn text-primary btn-link p-0 mb-2" id="toggleInputSelection"><span class="fa fa-@(Model.InputSelection ? "eye-slash" : "eye") "></span> @(Model.InputSelection ? "Hide" : "Show") coin selection</button>
</div>
<div class="input-group">
<input asp-for="Outputs[0].Amount" type="number" inputmode="decimal" step="any" min="0" asp-format="{0}" class="form-control output-amount hide-number-spin" />
<div class="input-group-text fiat-value" style="display:none;">
<span class="input-group-text p-0 border-0">=</span>
<input type="number" inputmode="decimal" class="input-group-text fiat-value-edit-input py-0 border-0 hide-number-spin" min="0" step="any" style="max-width:100px" />
<span class="input-group-text p-0 border-0">@Model.Fiat</span>
</div>
<span asp-validation-for="Outputs[0].Amount" class="text-danger"></span>
<p class="form-text text-secondary mb-0 crypto-info">
Your available balance is
<button type="button" class="crypto-balance-link btn btn-link p-0 align-baseline">@Model.CurrentBalance</button> <span>@Model.CryptoCode</span>.
@if (Model.ImmatureBalance > 0)
{
<span><br><span class="text-warning">⚠</span> @Model.ImmatureBalance @Model.CryptoCode are still immature and require additional confirmations.</span>
}
</p>
</div>
}
else
{
<div class="list-group list-group-flush">
@for (var index = 0; index < Model.Outputs.Count; index++)
{
<input type="hidden" asp-for="Outputs[index].PayoutId" />
<div class="list-group-item d-block px-0 pt-0 pb-3 mb-3">
<div class="form-group">
<div class="d-flex align-items-center justify-content-between">
<label asp-for="Outputs[index].DestinationAddress" class="form-label"></label>
<button type="submit" name="command" value="@($"remove-output:{index}")" class="d-inline-block ms-2 btn text-danger btn-link p-0 mb-2">
<span class="fa fa-times"></span> Remove Destination
</button>
</div>
<input asp-for="Outputs[index].DestinationAddress" class="form-control" autocomplete="off"/>
<span asp-validation-for="Outputs[index].DestinationAddress" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Outputs[index].Amount" class="form-label"></label>
<div class="input-group">
<input asp-for="Outputs[index].Amount" type="number" min="0" step="any" asp-format="{0}" class="form-control output-amount hide-number-spin" />
<div class="input-group-text fiat-value" style="display:none;">
<span class="input-group-text p-0 border-0">=</span>
<input type="number" inputmode="decimal" class="input-group-text fiat-value-edit-input py-0 border-0 hide-number-spin" min="0" step="any" style="max-width:100px" />
<span class="input-group-text p-0 border-0">@Model.Fiat</span>
</div>
</div>
<p class="form-text text-secondary crypto-info mb-2">
Your available balance is
<button type="button" class="crypto-balance-link btn btn-link p-0 align-baseline">@Model.CurrentBalance</button> <span>@Model.CryptoCode</span>.
@if (Model.ImmatureBalance > 0)
{
<span><br>Note: @Model.ImmatureBalance @Model.CryptoCode are still immature and require additional confirmations.</span>
}
</p>
<span asp-validation-for="Outputs[index].Amount" class="text-danger"></span>
</div>
<div class="form-check">
<input type="checkbox" asp-for="Outputs[index].SubtractFeesFromOutput" class="form-check-input subtract-fees" />
<label asp-for="Outputs[index].SubtractFeesFromOutput" class="form-check-label"></label>
<span asp-validation-for="Outputs[index].SubtractFeesFromOutput" class="text-danger"></span>
</div>
<span asp-validation-for="Outputs[0].Amount" class="text-danger"></span>
<p class="form-text text-secondary mb-0 crypto-info">
Your available balance is
<button type="button" class="crypto-balance-link btn btn-link p-0 align-baseline">@Model.CurrentBalance</button> <span>@Model.CryptoCode</span>.
@if (Model.ImmatureBalance > 0)
{
<span><br><span class="text-warning">⚠</span> @Model.ImmatureBalance @Model.CryptoCode are still immature and require additional confirmations.</span>
}
</p>
</div>
}
else
{
<div class="list-group list-group-flush">
@for (var index = 0; index < Model.Outputs.Count; index++)
</div>
<div class="d-grid gap-3 d-md-block mt-n2">
<button type="submit" name="command" value="add-output" class="btn btn-secondary me-md-1"><span class="fa fa-plus"></span> Add another destination</button>
<button type="submit" name="command" value="toggle-input-selection" class="btn btn-secondary" id="toggleInputSelection"><span class="fa fa-@(Model.InputSelection ? "eye-slash" : "eye") "></span> @(Model.InputSelection ? "Hide" : "Show") coin selection</button>
</div>
}
@if (Model.InputSelection)
{
<partial name="CoinSelection" />
}
<div class="form-group my-4">
<label asp-for="FeeSatoshiPerByte" class="form-label"></label>
<input asp-for="FeeSatoshiPerByte" type="number" inputmode="numeric" min="0" step="any" class="form-control" style="max-width:14ch;" />
<span asp-validation-for="FeeSatoshiPerByte" class="text-danger"></span>
<span id="FeeRate-Error" class="text-danger"></span>
@if (Model.RecommendedSatoshiPerByte.Any())
{
<div class="text-start mt-4 d-flex align-items-sm-center flex-column flex-sm-row">
<span class="text-secondary me-3">
Confirm in the next
</span>
<div class="btn-group btn-group-toggle feerate-options mt-2 mt-sm-0" role="group" data-bs-toggle="buttons">
@for (var index = 0; index < Model.RecommendedSatoshiPerByte.Count; index++)
{
<input type="hidden" asp-for="Outputs[index].PayoutId" />
<div class="list-group-item transaction-output-form px-0 pt-0 pb-3 mb-3">
<div class="form-group">
<div class="d-flex align-items-center justify-content-between">
<label asp-for="Outputs[index].DestinationAddress" class="form-label"></label>
<button type="submit" name="command" value="@($"remove-output:{index}")" class="d-inline-block ms-2 btn text-danger btn-link p-0 mb-2">
<span class="fa fa-times"></span> Remove Destination
</button>
</div>
<input asp-for="Outputs[index].DestinationAddress" class="form-control" autocomplete="off"/>
<span asp-validation-for="Outputs[index].DestinationAddress" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Outputs[index].Amount" class="form-label"></label>
<div class="input-group">
<input asp-for="Outputs[index].Amount" type="number" min="0" step="any" asp-format="{0}" class="form-control output-amount hide-number-spin" />
<div class="input-group-text fiat-value" style="display:none;">
<span class="input-group-text p-0 border-0">=</span>
<input type="number" inputmode="decimal" class="input-group-text fiat-value-edit-input py-0 border-0 hide-number-spin" min="0" step="any" style="max-width:100px" />
<span class="input-group-text p-0 border-0">@Model.Fiat</span>
</div>
</div>
<p class="form-text text-secondary crypto-info mb-2">
Your available balance is
<button type="button" class="crypto-balance-link btn btn-link p-0 align-baseline">@Model.CurrentBalance</button> <span>@Model.CryptoCode</span>.
@if (Model.ImmatureBalance > 0)
{
<span><br>Note: @Model.ImmatureBalance @Model.CryptoCode are still immature and require additional confirmations.</span>
}
</p>
<span asp-validation-for="Outputs[index].Amount" class="text-danger"></span>
</div>
<div class="form-check">
<input type="checkbox" asp-for="Outputs[index].SubtractFeesFromOutput" class="form-check-input subtract-fees" />
<label asp-for="Outputs[index].SubtractFeesFromOutput" class="form-check-label"></label>
<span asp-validation-for="Outputs[index].SubtractFeesFromOutput" class="text-danger"></span>
</div>
</div>
var feeRateOption = Model.RecommendedSatoshiPerByte[index];
<button type="button" class="btn btn-sm btn-secondary crypto-fee-link" value="@feeRateOption.FeeRate" data-bs-toggle="tooltip" title="@feeRateOption.FeeRate sat/b">
@feeRateOption.Target.TimeString()
</button>
<input type="hidden" asp-for="RecommendedSatoshiPerByte[index].Target" />
<input type="hidden" asp-for="RecommendedSatoshiPerByte[index].FeeRate" />
}
</div>
<div class="d-grid gap-3 d-md-block mt-n2">
<button type="submit" name="command" value="add-output" class="btn btn-secondary me-md-1"><span class="fa fa-plus"></span> Add another destination</button>
<button type="submit" name="command" value="toggle-input-selection" class="btn btn-secondary" id="toggleInputSelection"><span class="fa fa-@(Model.InputSelection ? "eye-slash" : "eye") "></span> @(Model.InputSelection ? "Hide" : "Show") coin selection</button>
</div>
}
@if (Model.InputSelection)
{
<partial name="CoinSelection" />
}
<div class="form-group my-4">
<label asp-for="FeeSatoshiPerByte" class="form-label"></label>
<input asp-for="FeeSatoshiPerByte" type="number" inputmode="numeric" min="0" step="any" class="form-control" style="max-width:14ch;" />
<span asp-validation-for="FeeSatoshiPerByte" class="text-danger"></span>
<span id="FeeRate-Error" class="text-danger"></span>
@if (Model.RecommendedSatoshiPerByte.Any())
{
<div class="text-start mt-4 d-flex align-items-sm-center flex-column flex-sm-row">
<span class="text-secondary me-3">
Confirm in the next
</span>
<div class="btn-group btn-group-toggle feerate-options mt-2 mt-sm-0" role="group" data-bs-toggle="buttons">
@for (var index = 0; index < Model.RecommendedSatoshiPerByte.Count; index++)
{
var feeRateOption = Model.RecommendedSatoshiPerByte[index];
<button type="button" class="btn btn-sm btn-secondary crypto-fee-link" value="@feeRateOption.FeeRate" data-bs-toggle="tooltip" title="@feeRateOption.FeeRate sat/b">
@feeRateOption.Target.TimeString()
</button>
<input type="hidden" asp-for="RecommendedSatoshiPerByte[index].Target" />
<input type="hidden" asp-for="RecommendedSatoshiPerByte[index].FeeRate" />
}
</div>
</div>
}
</div>
@if (Model.Outputs.Count == 1)
{
}
</div>
@if (Model.Outputs.Count == 1)
{
<div class="form-group">
<div class="form-check">
<input type="checkbox" asp-for="Outputs[0].SubtractFeesFromOutput" class="form-check-input subtract-fees" />
<label asp-for="Outputs[0].SubtractFeesFromOutput" class="form-check-label"></label>
<span asp-validation-for="Outputs[0].SubtractFeesFromOutput" class="text-danger"></span>
</div>
</div>
}
<div class="my-4">
<button class="btn btn-link text-primary p-0" type="button" id="AdvancedSettingsButton" data-bs-toggle="collapse" data-bs-target="#AdvancedSettings" aria-expanded="false" aria-controls="AdvancedSettings">
Advanced settings
</button>
<div id="AdvancedSettings" class="collapse">
<div class="pt-3 pb-1">
<div class="form-group">
<div class="form-check">
<input type="checkbox" asp-for="Outputs[0].SubtractFeesFromOutput" class="form-check-input subtract-fees" />
<label asp-for="Outputs[0].SubtractFeesFromOutput" class="form-check-label"></label>
<span asp-validation-for="Outputs[0].SubtractFeesFromOutput" class="text-danger"></span>
<input asp-for="NoChange" class="form-check-input" />
<label asp-for="NoChange" class="form-check-label"></label>
<a href="https://docs.btcpayserver.org/Wallet/#dont-create-utxo-change" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</div>
</div>
}
<div class="my-4">
<button class="btn btn-link text-primary p-0" type="button" id="AdvancedSettingsButton" data-bs-toggle="collapse" data-bs-target="#AdvancedSettings" aria-expanded="false" aria-controls="AdvancedSettings">
Advanced settings
</button>
<div id="AdvancedSettings" class="collapse">
<div class="pt-3 pb-1">
<div class="form-group">
<div class="form-check">
<input asp-for="NoChange" class="form-check-input" />
<label asp-for="NoChange" class="form-check-label"></label>
<a href="https://docs.btcpayserver.org/Wallet/#dont-create-utxo-change" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="AlwaysIncludeNonWitnessUTXO" class="form-check-input"/>
<label asp-for="AlwaysIncludeNonWitnessUTXO" class="form-check-label"></label>
<a href="https://medium.com/@@jmacato/wasabi-wallets-advisory-for-trezor-users-7d942c727f92" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</div>
</div>
@if (Model.SupportRBF)
{
<div class="form-group">
<label asp-for="AllowFeeBump" class="form-label"></label>
<a href="https://docs.btcpayserver.org/Wallet/#rbf-replace-by-fee" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<select asp-for="AllowFeeBump" class="form-select w-auto">
<option value="Maybe">Randomize for higher privacy</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
</div>
}
@if (!string.IsNullOrEmpty(Model.PayJoinBIP21))
{
<div class="form-group">
<label asp-for="PayJoinBIP21" class="form-label"></label>
<input asp-for="PayJoinBIP21" class="form-control" />
<span asp-validation-for="PayJoinBIP21" class="text-danger"></span>
</div>
}
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="AlwaysIncludeNonWitnessUTXO" class="form-check-input"/>
<label asp-for="AlwaysIncludeNonWitnessUTXO" class="form-check-label"></label>
<a href="https://medium.com/@@jmacato/wasabi-wallets-advisory-for-trezor-users-7d942c727f92" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</div>
</div>
</div>
@if (Model.SupportRBF)
{
<div class="form-group">
<label asp-for="AllowFeeBump" class="form-label"></label>
<a href="https://docs.btcpayserver.org/Wallet/#rbf-replace-by-fee" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<select asp-for="AllowFeeBump" class="form-select w-auto">
<option value="Maybe">Randomize for higher privacy</option>
<option value="Yes">Yes</option>
<option value="No">No</option>
</select>
</div>
}
@if (!string.IsNullOrEmpty(Model.PayJoinBIP21))
{
<div class="form-group">
<label asp-for="PayJoinBIP21" class="form-label"></label>
<input asp-for="PayJoinBIP21" class="form-control" />
<span asp-validation-for="PayJoinBIP21" class="text-danger"></span>
</div>
}
</div>
<div class="form-group d-flex gap-3 mt-2">
<button type="submit" id="SignTransaction" name="command" value="sign" class="btn btn-primary">Sign transaction</button>
<button type="submit" id="ScheduleTransaction" name="command" value="schedule" class="btn btn-secondary">Schedule transaction</button>
<a class="btn btn-secondary" asp-controller="UIWallets" asp-action="WalletPSBT" asp-route-walletId="@walletId" id="PSBT">PSBT</a>
<button type="button" id="bip21parse" class="btn btn-secondary" title="Paste BIP21/Address"><i class="fa fa-paste"></i></button>
<button type="button" id="scanqrcode" class="btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan BIP21/Address with camera"><i class="fa fa-camera"></i></button>
</div>
</form>
</div>
</div>
</div>
<div class="form-group d-grid d-sm-flex flex-wrap gap-3 mt-2">
<button type="submit" id="SignTransaction" name="command" value="sign" class="btn btn-primary">Sign transaction</button>
<button type="submit" id="ScheduleTransaction" name="command" value="schedule" class="btn btn-secondary">Schedule transaction</button>
<a class="btn btn-secondary" asp-controller="UIWallets" asp-action="WalletPSBT" asp-route-walletId="@walletId" asp-route-returnUrl="@Model.ReturnUrl" id="PSBT">PSBT</a>
<button type="button" id="bip21parse" class="btn btn-secondary" title="Paste BIP21/Address"><i class="fa fa-paste"></i></button>
<button type="button" id="scanqrcode" class="btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan BIP21/Address with camera"><i class="fa fa-camera"></i></button>
</div>
</form>

View file

@ -1,21 +1,25 @@
@using BTCPayServer.Controllers
@using BTCPayServer.TagHelpers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model WalletSendVaultModel
@{
var walletId = Context.GetRouteValue("walletId").ToString();
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
Layout = "_LayoutWizard";
ViewData.SetActivePage(WalletsNavPages.Send, "Sign the transaction", walletId);
var returnUrl = this.Context.Request.Query["returnUrl"].FirstOrDefault();
}
@section Navbar {
@if (returnUrl is string)
{
<a href="@returnUrl" id="GoBack">
<vc:icon symbol="back" />
</a>
<a href="@returnUrl" class="cancel">
@if (backUrl != null)
{
<a href="@backUrl" id="GoBack">
<vc:icon symbol="back" />
</a>
}
<a href="@cancelUrl" id="CancelWizard" class="cancel">
<vc:icon symbol="close" />
</a>
}
}
<header class="text-center">
@ -31,9 +35,11 @@
</div>
<div id="body" class="my-4">
<form id="broadcastForm" asp-action="WalletSendVault" asp-route-walletId="@walletId" asp-route-returnUrl="@returnUrl" method="post" style="display:none;">
<form id="broadcastForm" asp-action="WalletSendVault" asp-route-walletId="@walletId" method="post" style="display:none;">
<input type="hidden" id="WalletId" asp-for="WalletId" />
<input type="hidden" asp-for="WebsocketPath" />
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="BackUrl" />
<partial name="SigningContext" for="SigningContext" />
</form>
<div id="vaultPlaceholder"></div>

View file

@ -1,17 +1,24 @@
@using BTCPayServer.Controllers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model WalletSigningOptionsModel
@inject BTCPayNetworkProvider BTCPayNetworkProvider
@addTagHelper *, BundlerMinifier.TagHelpers
@{
var walletId = WalletId.Parse(Context.GetRouteValue("walletId").ToString());
var cancelUrl = Model.ReturnUrl ?? Url.Action(nameof(UIWalletsController.WalletTransactions), new { walletId });
var backUrl = Model.BackUrl != null ? $"{Model.BackUrl}?returnUrl={Model.ReturnUrl}" : null;
Layout = "_LayoutWizard";
ViewData.SetActivePage(WalletsNavPages.Send, "Sign the transaction", walletId.ToString());
}
@section Navbar {
<a href="@Model.ReturnUrl">
<vc:icon symbol="back" />
</a>
<a href="@Model.ReturnUrl" class="cancel">
@if (backUrl != null)
{
<a href="@backUrl" id="GoBack">
<vc:icon symbol="back" />
</a>
}
<a href="@cancelUrl" id="CancelWizard" class="cancel">
<vc:icon symbol="close" />
</a>
}
@ -21,8 +28,10 @@
<p class="lead text-secondary mt-3">You can sign the transaction using one of the following methods.</p>
</header>
<form method="post" asp-action="WalletSign" asp-route-walletId="@walletId" asp-route-returnUrl="@Model.ReturnUrl">
<form method="post" asp-action="WalletSign" asp-route-walletId="@walletId">
<partial name="SigningContext" for="SigningContext" />
<input type="hidden" asp-for="ReturnUrl" />
<input type="hidden" asp-for="BackUrl" />
@if (BTCPayNetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode).VaultSupported)
{

View file

@ -5,7 +5,6 @@ namespace BTCPayServer.Views.Wallets
Index,
Send,
Transactions,
Rescan,
PSBT,
Receive,
Settings