[UX/UI] Add CPFP (#3395)

* Add CPFP

* Sign PSBT should go back to the initial page
This commit is contained in:
Nicolas Dorier 2022-02-10 12:24:28 +09:00 committed by GitHub
parent efed00f58b
commit 300d84c5d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 432 additions and 204 deletions

View file

@ -127,7 +127,6 @@ namespace BTCPayServer
public string BlockExplorerLinkDefault { get; set; } public string BlockExplorerLinkDefault { get; set; }
public string DisplayName { get; set; } public string DisplayName { get; set; }
public int Divisibility { get; set; } = 8; public int Divisibility { get; set; } = 8;
[Obsolete("Should not be needed")]
public bool IsBTC public bool IsBTC
{ {
get get

View file

@ -204,8 +204,7 @@ namespace BTCPayServer.Tests
{ {
// Local link, this is fine // Local link, this is fine
} }
else if (attributeValue.StartsWith("http://") || attributeValue.StartsWith("https://") || else if (attributeValue.StartsWith("http://") || attributeValue.StartsWith("https://"))
attributeValue.StartsWith("@"))
{ {
// This can be an external link. Treating it as such. // This can be an external link. Treating it as such.
var rel = GetAttributeValue(node, "rel"); var rel = GetAttributeValue(node, "rel");

View file

@ -140,7 +140,7 @@ namespace BTCPayServer.Tests
var postRedirectView = Assert.IsType<ViewResult>(view); var postRedirectView = Assert.IsType<ViewResult>(view);
var postRedirectViewModel = Assert.IsType<PostRedirectViewModel>(postRedirectView.Model); var postRedirectViewModel = Assert.IsType<PostRedirectViewModel>(postRedirectView.Model);
Assert.Equal(actionName, postRedirectViewModel.AspAction); Assert.Equal(actionName, postRedirectViewModel.AspAction);
var redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt" || p.Key == "SigningContext.PSBT").Value; var redirectedPSBT = postRedirectViewModel.FormParameters.Single(p => p.Key == "psbt" || p.Key == "SigningContext.PSBT").Value?.FirstOrDefault();
return redirectedPSBT; return redirectedPSBT;
} }
} }

View file

@ -85,6 +85,11 @@ namespace BTCPayServer.Tests
Driver.AssertNoError(); Driver.AssertNoError();
} }
public void PayInvoice()
{
Driver.FindElement(By.Id("FakePayment")).Click();
}
/// <summary> /// <summary>
/// Use this ServerUri when trying to browse with selenium /// Use this ServerUri when trying to browse with selenium
/// Because for some reason, the selenium container can't resolve the tests container domain name /// Because for some reason, the selenium container can't resolve the tests container domain name
@ -151,6 +156,7 @@ namespace BTCPayServer.Tests
} }
Driver.WaitForElement(By.Id("StoreSelectorCreate")).Click(); Driver.WaitForElement(By.Id("StoreSelectorCreate")).Click();
var name = "Store" + RandomUtils.GetUInt64(); var name = "Store" + RandomUtils.GetUInt64();
TestLogs.LogInformation($"Created store {name}");
Driver.WaitForElement(By.Id("Name")).SendKeys(name); Driver.WaitForElement(By.Id("Name")).SendKeys(name);
Driver.WaitForElement(By.Id("Create")).Click(); Driver.WaitForElement(By.Id("Create")).Click();
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click(); Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
@ -161,7 +167,7 @@ namespace BTCPayServer.Tests
return (name, storeId); return (name, storeId);
} }
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit) public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool? importkeys = null, bool isHotWallet = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
{ {
var isImport = !string.IsNullOrEmpty(seed); var isImport = !string.IsNullOrEmpty(seed);
GoToWalletSettings(cryptoCode); GoToWalletSettings(cryptoCode);
@ -181,11 +187,11 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click(); Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
Driver.FindElement(By.Id("ImportSeedLink")).Click(); Driver.FindElement(By.Id("ImportSeedLink")).Click();
Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed); Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed);
Driver.SetCheckbox(By.Id("SavePrivateKeys"), privkeys); Driver.SetCheckbox(By.Id("SavePrivateKeys"), isHotWallet);
} }
else else
{ {
var option = privkeys ? "Hotwallet" : "Watchonly"; var option = isHotWallet ? "Hotwallet" : "Watchonly";
TestLogs.LogInformation($"Generating new seed ({option})"); TestLogs.LogInformation($"Generating new seed ({option})");
Driver.FindElement(By.Id("GenerateWalletLink")).Click(); Driver.FindElement(By.Id("GenerateWalletLink")).Click();
Driver.FindElement(By.Id($"Generate{option}Link")).Click(); Driver.FindElement(By.Id($"Generate{option}Link")).Click();
@ -195,7 +201,8 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click(); Driver.FindElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click();
Driver.ToggleCollapse("AdvancedSettings"); Driver.ToggleCollapse("AdvancedSettings");
Driver.SetCheckbox(By.Id("ImportKeysToRPC"), importkeys); if (importkeys is bool v)
Driver.SetCheckbox(By.Id("ImportKeysToRPC"), v);
Driver.FindElement(By.Id("Continue")).Click(); Driver.FindElement(By.Id("Continue")).Click();
if (isImport) if (isImport)
@ -366,7 +373,10 @@ namespace BTCPayServer.Tests
public void GoToStore(string storeId, StoreNavPages storeNavPage = StoreNavPages.General) public void GoToStore(string storeId, StoreNavPages storeNavPage = StoreNavPages.General)
{ {
if (storeId is not null) if (storeId is not null)
{
GoToUrl($"/stores/{storeId}/"); GoToUrl($"/stores/{storeId}/");
StoreId = storeId;
}
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click(); Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
@ -412,8 +422,9 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id($"StoreSelectorMenuItem-{storeId}")).Click(); Driver.FindElement(By.Id($"StoreSelectorMenuItem-{storeId}")).Click();
} }
public void GoToInvoiceCheckout(string invoiceId) public void GoToInvoiceCheckout(string? invoiceId = null)
{ {
invoiceId ??= InvoiceId;
Driver.FindElement(By.Id("StoreNav-Invoices")).Click(); Driver.FindElement(By.Id("StoreNav-Invoices")).Click();
Driver.FindElement(By.Id($"invoice-checkout-{invoiceId}")).Click(); Driver.FindElement(By.Id($"invoice-checkout-{invoiceId}")).Click();
CheckForJSErrors(); CheckForJSErrors();
@ -433,6 +444,7 @@ namespace BTCPayServer.Tests
else else
{ {
GoToUrl(storeId == null ? "/invoices/" : $"/stores/{storeId}/invoices/"); GoToUrl(storeId == null ? "/invoices/" : $"/stores/{storeId}/invoices/");
StoreId = storeId;
} }
} }
@ -473,6 +485,7 @@ namespace BTCPayServer.Tests
) )
{ {
GoToInvoices(storeId); GoToInvoices(storeId);
Driver.FindElement(By.Id("CreateNewInvoice")).Click(); Driver.FindElement(By.Id("CreateNewInvoice")).Click();
if (amount is decimal v) if (amount is decimal v)
Driver.FindElement(By.Id("Amount")).SendKeys(v.ToString(CultureInfo.InvariantCulture)); Driver.FindElement(By.Id("Amount")).SendKeys(v.ToString(CultureInfo.InvariantCulture));
@ -487,8 +500,12 @@ namespace BTCPayServer.Tests
Driver.FindElement(By.Id("Create")).Click(); Driver.FindElement(By.Id("Create")).Click();
var statusElement = FindAlertMessage(expectedSeverity); var statusElement = FindAlertMessage(expectedSeverity);
return expectedSeverity == StatusMessageModel.StatusSeverity.Success ? statusElement.Text.Split(" ")[1] : null; var inv = expectedSeverity == StatusMessageModel.StatusSeverity.Success ? statusElement.Text.Split(" ")[1] : null;
InvoiceId = inv;
TestLogs.LogInformation($"Created invoice {inv}");
return inv;
} }
string InvoiceId;
public async Task FundStoreWallet(WalletId walletId = null, int coins = 1, decimal denomination = 1m) public async Task FundStoreWallet(WalletId walletId = null, int coins = 1, decimal denomination = 1m)
{ {
@ -503,22 +520,6 @@ namespace BTCPayServer.Tests
} }
} }
public void PayInvoice(WalletId walletId, string invoiceId)
{
GoToInvoiceCheckout(invoiceId);
var bip21 = Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
.GetAttribute("href");
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
GoToWallet(walletId);
Driver.FindElement(By.Id("bip21parse")).Click();
Driver.SwitchTo().Alert().SendKeys(bip21);
Driver.SwitchTo().Alert().Accept();
Driver.FindElement(By.Id("SignTransaction")).Click();
Driver.FindElement(By.Id("SignWithSeed")).Click();
Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
}
private void CheckForJSErrors() private void CheckForJSErrors()
{ {
//wait for seleniun update: https://stackoverflow.com/questions/57520296/selenium-webdriver-3-141-0-driver-manage-logs-availablelogtypes-throwing-syste //wait for seleniun update: https://stackoverflow.com/questions/57520296/selenium-webdriver-3-141-0-driver-manage-logs-availablelogtypes-throwing-syste

View file

@ -64,6 +64,48 @@ namespace BTCPayServer.Tests
Assert.Contains("Starting listening NBXplorer", s.Driver.PageSource); Assert.Contains("Starting listening NBXplorer", s.Driver.PageSource);
s.Driver.Quit(); s.Driver.Quit();
} }
[Fact(Timeout = TestTimeout)]
public async Task CanUseCPFP()
{
using var s = CreateSeleniumTester();
await s.StartAsync();
s.RegisterNewUser(true);
s.CreateNewStore();
s.GenerateWallet(isHotWallet: true);
await s.FundStoreWallet();
for (int i = 0; i < 3; i++)
{
s.CreateInvoice();
s.GoToInvoiceCheckout();
s.PayInvoice();
s.GoToInvoices(s.StoreId);
}
// Let's CPFP from the invoices page
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
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($"/stores/{s.StoreId}/invoices", s.Driver.Url);
// CPFP again should fail because all invoices got bumped
s.GoToInvoices();
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);
// But we should be able to bump from the wallet's page
s.GoToWallet(navPages: WalletsNavPages.Transactions);
s.Driver.SetCheckbox(By.Id("selectAllCheckbox"), true);
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);
}
[Fact(Timeout = TestTimeout)] [Fact(Timeout = TestTimeout)]
[Trait("Lightning", "Lightning")] [Trait("Lightning", "Lightning")]
@ -932,7 +974,7 @@ namespace BTCPayServer.Tests
{ {
var cryptoCode = "BTC"; var cryptoCode = "BTC";
s.CreateNewStore(); s.CreateNewStore();
s.GenerateWallet(cryptoCode, "melody lizard phrase voice unique car opinion merge degree evil swift cargo", privkeys: isHotwallet); s.GenerateWallet(cryptoCode, "melody lizard phrase voice unique car opinion merge degree evil swift cargo", isHotWallet: isHotwallet);
s.GoToWalletSettings(cryptoCode); s.GoToWalletSettings(cryptoCode);
if (isHotwallet) if (isHotwallet)
Assert.Contains("View seed", s.Driver.PageSource); Assert.Contains("View seed", s.Driver.PageSource);

View file

@ -15,6 +15,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Filters; using BTCPayServer.Filters;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels; using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Rating; using BTCPayServer.Rating;
@ -29,6 +30,7 @@ using Microsoft.EntityFrameworkCore;
using NBitcoin; using NBitcoin;
using NBitpayClient; using NBitpayClient;
using NBXplorer; using NBXplorer;
using NBXplorer.Models;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest; using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
using StoreData = BTCPayServer.Data.StoreData; using StoreData = BTCPayServer.Data.StoreData;
@ -446,12 +448,56 @@ namespace BTCPayServer.Controllers
await _InvoiceRepository.MassArchive(selectedItems, false); await _InvoiceRepository.MassArchive(selectedItems, false);
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived."; TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
break; break;
case "cpfp":
if (selectedItems.Length == 0)
return NotSupported("No invoice has been selected");
var network = _NetworkProvider.BTC;
var explorer = _ExplorerClients.GetExplorerClient(_NetworkProvider.BTC);
IActionResult NotSupported(string err)
{
TempData[WellKnownTempData.ErrorMessage] = err;
return RedirectToAction(nameof(ListInvoices), new { storeId });
}
if (explorer is null)
return NotSupported("This feature is only available to BTC wallets");
if (this.GetCurrentStore().Role != StoreRoles.Owner)
return Forbid();
var settings = (this.GetCurrentStore().GetDerivationSchemeSettings(_NetworkProvider, network.CryptoCode));
var derivationScheme = settings.AccountDerivation;
if (derivationScheme is null)
return NotSupported("This feature is only available to BTC wallets");
var bumpableAddresses = (await GetAddresses(selectedItems))
.Where(p => p.GetPaymentMethodId().IsBTCOnChain)
.Select(p => p.GetAddress()).ToHashSet();
var utxos = await explorer.GetUTXOsAsync(derivationScheme);
var bumpableUTXOs = utxos.GetUnspentUTXOs().Where(u => u.Confirmations == 0 && bumpableAddresses.Contains(u.ScriptPubKey.Hash.ToString())).ToArray();
var parameters = new MultiValueDictionary<string, string>();
foreach (var utxo in bumpableUTXOs)
{
parameters.Add($"outpoints[]", utxo.Outpoint.ToString());
}
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIWallets",
AspAction = nameof(UIWalletsController.WalletCPFP),
RouteParameters = {
{ "walletId", new WalletId(storeId, network.CryptoCode).ToString() },
{ "returnUrl", Url.Action(nameof(ListInvoices), new { storeId }) }
},
FormParameters = parameters,
});
} }
} }
return RedirectToAction(nameof(ListInvoices), new { storeId }); return RedirectToAction(nameof(ListInvoices), new { storeId });
} }
private async Task<AddressInvoiceData[]> GetAddresses(string[] selectedItems)
{
using var ctx = _dbContextFactory.CreateContext();
return await ctx.AddressInvoices.Where(i => selectedItems.Contains(i.InvoiceDataId)).ToArrayAsync();
}
[HttpGet("i/{invoiceId}")] [HttpGet("i/{invoiceId}")]
[HttpGet("i/{invoiceId}/{paymentMethodId}")] [HttpGet("i/{invoiceId}/{paymentMethodId}")]
[HttpGet("invoice")] [HttpGet("invoice")]

View file

@ -42,6 +42,8 @@ namespace BTCPayServer.Controllers
private readonly ApplicationDbContextFactory _dbContextFactory; private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly PullPaymentHostedService _paymentHostedService; private readonly PullPaymentHostedService _paymentHostedService;
private readonly LanguageService _languageService; private readonly LanguageService _languageService;
private readonly ExplorerClientProvider _ExplorerClients;
private readonly UIWalletsController _walletsController;
public WebhookSender WebhookNotificationManager { get; } public WebhookSender WebhookNotificationManager { get; }
@ -58,7 +60,9 @@ namespace BTCPayServer.Controllers
ApplicationDbContextFactory dbContextFactory, ApplicationDbContextFactory dbContextFactory,
PullPaymentHostedService paymentHostedService, PullPaymentHostedService paymentHostedService,
WebhookSender webhookNotificationManager, WebhookSender webhookNotificationManager,
LanguageService languageService) LanguageService languageService,
ExplorerClientProvider explorerClients,
UIWalletsController walletsController)
{ {
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable)); _CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository)); _StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
@ -72,6 +76,8 @@ namespace BTCPayServer.Controllers
_paymentHostedService = paymentHostedService; _paymentHostedService = paymentHostedService;
WebhookNotificationManager = webhookNotificationManager; WebhookNotificationManager = webhookNotificationManager;
_languageService = languageService; _languageService = languageService;
this._ExplorerClients = explorerClients;
_walletsController = walletsController;
} }

View file

@ -312,18 +312,16 @@ namespace BTCPayServer.Controllers
var redirectVm = new PostRedirectViewModel() var redirectVm = new PostRedirectViewModel()
{ {
FormUrl = viewModel.RedirectUrl.AbsoluteUri, FormUrl = viewModel.RedirectUrl.AbsoluteUri,
Parameters = FormParameters =
{ {
new KeyValuePair<string, string>("apiKey", key.Id), { "apiKey", key.Id },
new KeyValuePair<string, string>("userId", key.UserId) { "userId", key.UserId },
} },
}; };
foreach (var permission in permissions) foreach (var permission in permissions)
{ {
redirectVm.Parameters.Add( redirectVm.FormParameters.Add("permissions[]", permission);
new KeyValuePair<string, string>("permissions[]", permission));
} }
return View("PostRedirect", redirectVm); return View("PostRedirect", redirectVm);
} }

View file

@ -70,6 +70,115 @@ namespace BTCPayServer.Controllers
return psbt; return psbt;
} }
[HttpPost("{walletId}/cpfp")]
public async Task<IActionResult> WalletCPFP([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string[] outpoints, string[] transactionHashes, string returnUrl)
{
outpoints ??= Array.Empty<string>();
transactionHashes ??= Array.Empty<string>();
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
var explorer = ExplorerClientProvider.GetExplorerClient(network);
var fr = _feeRateProvider.CreateFeeProvider(network);
var targetFeeRate = await fr.GetFeeRateAsync(1);
// Since we don't know the actual fee rate paid by a tx from NBX
// we just assume that it is 20 blocks
var assumedFeeRate = await fr.GetFeeRateAsync(20);
var settings = (this.GetCurrentStore().GetDerivationSchemeSettings(NetworkProvider, network.CryptoCode));
var derivationScheme = settings.AccountDerivation;
if (derivationScheme is null)
return NotFound();
var utxos = await explorer.GetUTXOsAsync(derivationScheme);
var outpointsHashet = outpoints.ToHashSet();
var transactionHashesSet = transactionHashes.ToHashSet();
var bumpableUTXOs = utxos.GetUnspentUTXOs().Where(u => u.Confirmations == 0 &&
(outpointsHashet.Contains(u.Outpoint.ToString()) ||
transactionHashesSet.Contains(u.Outpoint.Hash.ToString()))).ToArray();
if (bumpableUTXOs.Length == 0)
{
TempData[WellKnownTempData.ErrorMessage] = "There isn't any UTXO available to bump fee";
return Redirect(returnUrl);
}
Money bumpFee = Money.Zero;
foreach (var txid in bumpableUTXOs.Select(u => u.TransactionHash).ToHashSet())
{
var tx = await explorer.GetTransactionAsync(txid);
var vsize = tx.Transaction.GetVirtualSize();
var assumedFeePaid = assumedFeeRate.GetFee(vsize);
var expectedFeePaid = targetFeeRate.GetFee(vsize);
bumpFee += Money.Max(Money.Zero, expectedFeePaid - assumedFeePaid);
}
var returnAddress = (await explorer.GetUnusedAsync(derivationScheme, NBXplorer.DerivationStrategy.DerivationFeature.Deposit)).Address;
TransactionBuilder builder = explorer.Network.NBitcoinNetwork.CreateTransactionBuilder();
builder.AddCoins(bumpableUTXOs.Select(utxo => utxo.AsCoin(derivationScheme)));
// The fee of the bumped transaction should pay for both, the fee
// of the bump transaction and those that are being bumped
builder.SendEstimatedFees(targetFeeRate);
builder.SendFees(bumpFee);
builder.SendAll(returnAddress);
var psbt = builder.BuildPSBT(false);
psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest()
{
PSBT = psbt,
DerivationScheme = derivationScheme
})).PSBT;
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIWallets",
AspAction = nameof(UIWalletsController.WalletSign),
RouteParameters = {
{ "walletId", walletId.ToString() },
{ "returnUrl", returnUrl }
},
FormParameters =
{
{ "walletId", walletId.ToString() },
{ "psbt", psbt.ToHex() }
}
});
}
[HttpPost("{walletId}/sign")]
public async Task<IActionResult> WalletSign([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTViewModel vm, string returnUrl = null, string command = null)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
vm.SigningContext.PSBT ??= psbt.ToBase64();
if (returnUrl is null)
returnUrl = Url.Action(nameof(WalletTransactions), new { walletId });
switch (command)
{
case "vault":
return ViewVault(walletId, vm.SigningContext);
case "seed":
return SignWithSeed(walletId, vm.SigningContext);
default:
break;
}
if (await CanUseHotWallet())
{
var derivationScheme = GetDerivationSchemeSettings(walletId);
if (derivationScheme.IsHotWallet)
{
var extKey = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
WellknownMetadataKeys.MasterHDKey);
if (extKey != null)
{
return SignWithSeed(walletId,
new SignWithSeedViewModel { SeedOrKey = extKey, SigningContext = vm.SigningContext });
}
}
}
return View("WalletSigningOptions", new WalletSigningOptionsModel(vm.SigningContext, returnUrl));
}
[HttpGet("{walletId}/psbt")] [HttpGet("{walletId}/psbt")]
public async Task<IActionResult> WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))] public async Task<IActionResult> WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletPSBTViewModel vm) WalletId walletId, WalletPSBTViewModel vm)
@ -118,14 +227,12 @@ namespace BTCPayServer.Controllers
return View(vm); return View(vm);
} }
vm.PSBT = psbt.ToBase64();
vm.PSBTHex = psbt.ToHex(); vm.PSBTHex = psbt.ToHex();
var res = await TryHandleSigningCommands(walletId, psbt, command, vm.SigningContext, nameof(WalletPSBT));
if (res != null)
{
return res;
}
switch (command) switch (command)
{ {
case "sign":
return await WalletSign(walletId, vm, nameof(WalletPSBT));
case "decode": case "decode":
ModelState.Remove(nameof(vm.PSBT)); ModelState.Remove(nameof(vm.PSBT));
ModelState.Remove(nameof(vm.FileName)); ModelState.Remove(nameof(vm.FileName));
@ -407,6 +514,12 @@ namespace BTCPayServer.Controllers
vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"; vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}";
return View(nameof(WalletPSBT), vm); return View(nameof(WalletPSBT), vm);
} }
else
{
var wallet = _walletProvider.GetWallet(network);
var derivationSettings = GetDerivationSchemeSettings(walletId);
wallet.InvalidateCache(derivationSettings.AccountDerivation);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -418,7 +531,12 @@ namespace BTCPayServer.Controllers
{ {
TempData[WellKnownTempData.SuccessMessage] = $"Transaction broadcasted successfully ({transaction.GetHash()})"; TempData[WellKnownTempData.SuccessMessage] = $"Transaction broadcasted successfully ({transaction.GetHash()})";
} }
return RedirectToWalletTransaction(walletId, transaction); var returnUrl = this.HttpContext.Request.Query["returnUrl"].FirstOrDefault();
if (returnUrl is not null)
{
return Redirect(returnUrl);
}
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
} }
case "analyze-psbt": case "analyze-psbt":
return RedirectToWalletPSBT(new WalletPSBTViewModel() return RedirectToWalletPSBT(new WalletPSBTViewModel()
@ -460,45 +578,5 @@ namespace BTCPayServer.Controllers
PSBT = sourcePSBT.ToBase64() PSBT = sourcePSBT.ToBase64()
}); });
} }
private async Task<IActionResult> TryHandleSigningCommands(WalletId walletId, PSBT psbt, string command,
SigningContextModel signingContext, string actionBack)
{
signingContext.PSBT = psbt.ToBase64();
switch (command)
{
case "sign":
var routeBack = new Dictionary<string, string>
{
{"action", actionBack }, {"walletId", walletId.ToString()}
};
return View("WalletSigningOptions", new WalletSigningOptionsModel(signingContext, routeBack));
case "vault":
return ViewVault(walletId, signingContext);
case "seed":
return SignWithSeed(walletId, signingContext);
case "nbx-seed":
if (await CanUseHotWallet())
{
var derivationScheme = GetDerivationSchemeSettings(walletId);
if (derivationScheme.IsHotWallet)
{
var extKey = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
WellknownMetadataKeys.MasterHDKey);
return SignWithSeed(walletId,
new SignWithSeedViewModel { SeedOrKey = extKey, SigningContext = signingContext });
}
}
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "NBX seed functionality is not available"
});
break;
}
return null;
}
} }
} }

View file

@ -723,17 +723,16 @@ namespace BTCPayServer.Controllers
{ {
PayJoinBIP21 = vm.PayJoinBIP21, PayJoinBIP21 = vm.PayJoinBIP21,
EnforceLowR = psbtResponse.Suggestions?.ShouldEnforceLowR, EnforceLowR = psbtResponse.Suggestions?.ShouldEnforceLowR,
ChangeAddress = psbtResponse.ChangeAddress?.ToString() ChangeAddress = psbtResponse.ChangeAddress?.ToString(),
PSBT = psbt.ToHex()
}; };
var res = await TryHandleSigningCommands(walletId, psbt, command, signingContext, nameof(WalletSend));
if (res != null)
{
return res;
}
switch (command) switch (command)
{ {
case "sign":
return await WalletSign(walletId, new WalletPSBTViewModel()
{
SigningContext = signingContext
});
case "analyze-psbt": case "analyze-psbt":
var name = var name =
$"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt"; $"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
@ -823,10 +822,11 @@ namespace BTCPayServer.Controllers
{ {
AspController = "UIWallets", AspController = "UIWallets",
AspAction = nameof(WalletPSBTReady), AspAction = nameof(WalletPSBTReady),
Parameters = RouteParameters = { { "walletId", this.RouteData?.Values["walletId"]?.ToString() } },
FormParameters =
{ {
new KeyValuePair<string, string>("SigningKey", vm.SigningKey), { "SigningKey", vm.SigningKey },
new KeyValuePair<string, string>("SigningKeyPath", vm.SigningKeyPath) { "SigningKeyPath", vm.SigningKeyPath }
} }
}; };
AddSigningContext(redirectVm, vm.SigningContext); AddSigningContext(redirectVm, vm.SigningContext);
@ -834,7 +834,11 @@ namespace BTCPayServer.Controllers
!string.IsNullOrEmpty(vm.SigningContext.PSBT)) !string.IsNullOrEmpty(vm.SigningContext.PSBT))
{ {
//if a hw device signed a payjoin, we want it broadcast instantly //if a hw device signed a payjoin, we want it broadcast instantly
redirectVm.Parameters.Add(new KeyValuePair<string, string>("command", "broadcast")); redirectVm.FormParameters.Add("command", "broadcast");
}
if (this.HttpContext.Request.Query["returnUrl"].FirstOrDefault() is string returnUrl)
{
redirectVm.RouteParameters.Add("returnUrl", returnUrl);
} }
return View("PostRedirect", redirectVm); return View("PostRedirect", redirectVm);
} }
@ -843,11 +847,11 @@ namespace BTCPayServer.Controllers
{ {
if (signingContext is null) if (signingContext is null)
return; return;
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.PSBT", signingContext.PSBT)); redirectVm.FormParameters.Add("SigningContext.PSBT", signingContext.PSBT);
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.OriginalPSBT", signingContext.OriginalPSBT)); redirectVm.FormParameters.Add("SigningContext.OriginalPSBT", signingContext.OriginalPSBT);
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.PayJoinBIP21", signingContext.PayJoinBIP21)); redirectVm.FormParameters.Add("SigningContext.PayJoinBIP21", signingContext.PayJoinBIP21);
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.EnforceLowR", signingContext.EnforceLowR?.ToString(CultureInfo.InvariantCulture))); redirectVm.FormParameters.Add("SigningContext.EnforceLowR", signingContext.EnforceLowR?.ToString(CultureInfo.InvariantCulture));
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.ChangeAddress", signingContext.ChangeAddress)); redirectVm.FormParameters.Add("SigningContext.ChangeAddress", signingContext.ChangeAddress);
} }
private IActionResult RedirectToWalletPSBT(WalletPSBTViewModel vm) private IActionResult RedirectToWalletPSBT(WalletPSBTViewModel vm)
@ -856,10 +860,11 @@ namespace BTCPayServer.Controllers
{ {
AspController = "UIWallets", AspController = "UIWallets",
AspAction = nameof(WalletPSBT), AspAction = nameof(WalletPSBT),
Parameters = RouteParameters = { { "walletId", this.RouteData?.Values["walletId"]?.ToString() } },
FormParameters =
{ {
new KeyValuePair<string, string>("psbt", vm.PSBT), { "psbt", vm.PSBT },
new KeyValuePair<string, string>("fileName", vm.FileName) { "fileName", vm.FileName }
} }
}; };
return View("PostRedirect", redirectVm); return View("PostRedirect", redirectVm);
@ -956,18 +961,6 @@ namespace BTCPayServer.Controllers
return v.ToString() + " " + network.CryptoCode; return v.ToString() + " " + network.CryptoCode;
} }
private IActionResult RedirectToWalletTransaction(WalletId walletId, Transaction transaction)
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
if (transaction != null)
{
var wallet = _walletProvider.GetWallet(network);
var derivationSettings = GetDerivationSchemeSettings(walletId);
wallet.InvalidateCache(derivationSettings.AccountDerivation);
}
return RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() });
}
[HttpGet("{walletId}/rescan")] [HttpGet("{walletId}/rescan")]
public async Task<IActionResult> WalletRescan( public async Task<IActionResult> WalletRescan(
[ModelBinder(typeof(WalletIdModelBinder))] [ModelBinder(typeof(WalletIdModelBinder))]
@ -1067,6 +1060,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> WalletActions( public async Task<IActionResult> WalletActions(
[ModelBinder(typeof(WalletIdModelBinder))] [ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string command, WalletId walletId, string command,
string[] selectedTransactions,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var derivationScheme = GetDerivationSchemeSettings(walletId); var derivationScheme = GetDerivationSchemeSettings(walletId);
@ -1075,6 +1069,31 @@ namespace BTCPayServer.Controllers
switch (command) switch (command)
{ {
case "cpfp":
{
selectedTransactions ??= Array.Empty<string>();
if (selectedTransactions.Length == 0)
{
TempData[WellKnownTempData.ErrorMessage] = $"No transaction selected";
return RedirectToAction(nameof(WalletTransactions), new { walletId });
}
var parameters = new MultiValueDictionary<string, string>();
parameters.Add("walletId", walletId.ToString());
int i = 0;
foreach (var tx in selectedTransactions)
{
parameters.Add($"transactionHashes[{i}]", tx);
i++;
}
parameters.Add("returnUrl", Url.Action(nameof(WalletTransactions), new { walletId }));
return View("PostRedirect", new PostRedirectViewModel
{
AspController = "UIWallets",
AspAction = nameof(UIWalletsController.WalletCPFP),
RouteParameters = { { "walletId", walletId.ToString() } },
FormParameters = parameters
});
}
case "prune": case "prune":
{ {
var result = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode).PruneAsync(derivationScheme.AccountDerivation, new PruneRequest(), cancellationToken); var result = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode).PruneAsync(derivationScheme.AccountDerivation, new PruneRequest(), cancellationToken);

View file

@ -20,7 +20,7 @@ namespace BTCPayServer.Data
addressInvoiceData.Address = address + "#" + paymentMethodId.ToString(); addressInvoiceData.Address = address + "#" + paymentMethodId.ToString();
return addressInvoiceData; return addressInvoiceData;
} }
public static PaymentMethodId GetpaymentMethodId(this AddressInvoiceData addressInvoiceData) public static PaymentMethodId GetPaymentMethodId(this AddressInvoiceData addressInvoiceData)
{ {
if (addressInvoiceData.Address == null) if (addressInvoiceData.Address == null)
return null; return null;

View file

@ -498,14 +498,14 @@ namespace BTCPayServer
{ {
AspController = "UIHome", AspController = "UIHome",
AspAction = "RecoverySeedBackup", AspAction = "RecoverySeedBackup",
Parameters = FormParameters =
{ {
new KeyValuePair<string, string>("cryptoCode", vm.CryptoCode), { "cryptoCode", vm.CryptoCode },
new KeyValuePair<string, string>("mnemonic", vm.Mnemonic), { "mnemonic", vm.Mnemonic },
new KeyValuePair<string, string>("passphrase", vm.Passphrase), { "passphrase", vm.Passphrase },
new KeyValuePair<string, string>("isStored", vm.IsStored ? "true" : "false"), { "isStored", vm.IsStored ? "true" : "false" },
new KeyValuePair<string, string>("requireConfirm", vm.RequireConfirm ? "true" : "false"), { "requireConfirm", vm.RequireConfirm ? "true" : "false" },
new KeyValuePair<string, string>("returnUrl", vm.ReturnUrl) { "returnUrl", vm.ReturnUrl }
} }
}; };
return controller.View("PostRedirect", redirectVm); return controller.View("PostRedirect", redirectVm);

View file

@ -13,6 +13,5 @@ namespace BTCPayServer
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == cryptoCode); .FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == cryptoCode);
return paymentMethod; return paymentMethod;
} }
} }
} }

View file

@ -8,6 +8,7 @@ namespace BTCPayServer.Models
public string AspController { get; set; } public string AspController { get; set; }
public string FormUrl { get; set; } public string FormUrl { get; set; }
public List<KeyValuePair<string, string>> Parameters { get; set; } = new List<KeyValuePair<string, string>>(); public MultiValueDictionary<string, string> FormParameters { get; set; } = new MultiValueDictionary<string, string>();
public Dictionary<string, string> RouteParameters { get; set; } = new Dictionary<string, string>();
} }
} }

View file

@ -6,13 +6,13 @@ namespace BTCPayServer.Models.WalletViewModels
{ {
public WalletSigningOptionsModel( public WalletSigningOptionsModel(
SigningContextModel signingContext, SigningContextModel signingContext,
IDictionary<string, string> routeDataBack) string returnUrl)
{ {
SigningContext = signingContext; SigningContext = signingContext;
RouteDataBack = routeDataBack; ReturnUrl = returnUrl;
} }
public SigningContextModel SigningContext { get; } public SigningContextModel SigningContext { get; }
public IDictionary<string, string> RouteDataBack { get; } public string ReturnUrl { get; }
} }
} }

View file

@ -25,7 +25,6 @@ namespace BTCPayServer.Payments
CryptoCode = cryptoCode.ToUpperInvariant(); CryptoCode = cryptoCode.ToUpperInvariant();
} }
[Obsolete("Should only be used for legacy stuff")]
public bool IsBTCOnChain public bool IsBTCOnChain
{ {
get get

View file

@ -585,7 +585,7 @@ namespace BTCPayServer.Services.Invoices
} }
if (invoice.AddressInvoices != null) if (invoice.AddressInvoices != null)
{ {
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetAddress() + a.GetpaymentMethodId().ToString()).ToHashSet(); entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetAddress() + a.GetPaymentMethodId().ToString()).ToHashSet();
} }
if (invoice.Events != null) if (invoice.Events != null)
{ {

View file

@ -1,46 +1,71 @@
@model PostRedirectViewModel @model PostRedirectViewModel
@{ @{
Layout = null; Layout = null;
var routeData = Context.GetRouteData();
var routeParams = new Dictionary<string, string>();
if (routeData != null)
{
routeParams["walletId"] = routeData.Values["walletId"]?.ToString();
}
var action = Model.FormUrl ?? Url.Action(Model.AspAction, Model.AspController, routeParams);
} }
<html lang="en"> <html lang="en">
<head> <head>
<partial name="LayoutHead" /> <partial name="LayoutHead" />
<title>Post Redirect</title> <title>Post Redirect</title>
</head> </head>
<body> <body>
<form method="post" id="postform" action="@action" rel="noreferrer noopener"> @if (Model.FormUrl is null)
@Html.AntiForgeryToken() {
@foreach (var o in Model.Parameters) <form method="post" id="postform" asp-action="@Model.AspAction" asp-controller="@Model.AspController" asp-all-route-data="Model.RouteParameters">
{ @Html.AntiForgeryToken()
<input type="hidden" name="@o.Key" value="@o.Value"/> @foreach (var o in Model.FormParameters)
} {
<noscript> foreach (var v in o.Value)
<div class="modal-dialog modal-dialog-centered min-vh-100"> {
<div class="modal-content"> <input type="hidden" name="@o.Key" value="@v" />
<div class="modal-body text-center my-3"> }
<p> }
This redirection page is supposed to be submitted automatically. <noscript>
<br> <div class="modal-dialog modal-dialog-centered min-vh-100">
Since you have not enabled JavaScript, please submit manually. <div class="modal-content">
</p> <div class="modal-body text-center my-3">
<button class="btn btn-primary" type="submit">Submit</button> <p>
</div> This redirection page is supposed to be submitted automatically.
</div> <br>
</div> Since you have not enabled JavaScript, please submit manually.
</noscript> </p>
</form> <button class="btn btn-primary" type="submit">Submit</button>
<script type="text/javascript"> </div>
document.forms.item(0).submit(); </div>
</script> </div>
<partial name="LayoutFoot" /> </noscript>
</form>
}
else
{
<form method="post" id="postform" action="@Model.FormUrl" rel="noreferrer noopener">
@Html.AntiForgeryToken()
@foreach (var o in Model.FormParameters)
{
foreach (var v in o.Value)
{
<input type="hidden" name="@o.Key" value="@v" />
}
}
<noscript>
<div class="modal-dialog modal-dialog-centered min-vh-100">
<div class="modal-content">
<div class="modal-body text-center my-3">
<p>
This redirection page is supposed to be submitted automatically.
<br>
Since you have not enabled JavaScript, please submit manually.
</p>
<button class="btn btn-primary" type="submit">Submit</button>
</div>
</div>
</div>
</noscript>
</form>
}
<script type="text/javascript">
document.forms.item(0).submit();
</script>
<partial name="LayoutFoot" />
</body> </body>
</html> </html>

View file

@ -1,4 +1,4 @@
@model PaymentModel @model PaymentModel
<div id="testing"> <div id="testing">
<hr class="my-3" /> <hr class="my-3" />
@ -13,7 +13,7 @@
<div id="test-payment-crypto-code" class="input-group-addon">@Model.CryptoCode</div> <div id="test-payment-crypto-code" class="input-group-addon">@Model.CryptoCode</div>
</div> </div>
</div> </div>
<button class="btn btn-primary" type="submit">{{$t("Fake Payment")}}</button> <button id="FakePayment" class="btn btn-primary" type="submit">{{$t("Fake Payment")}}</button>
<p class="text-muted mt-1">{{$t("This is the same as running bitcoin-cli.sh sendtoaddress xxx")}}</p> <p class="text-muted mt-1">{{$t("This is the same as running bitcoin-cli.sh sendtoaddress xxx")}}</p>
</form> </form>
<form id="expire-invoice" action="/i/@Model.InvoiceId/expire" method="post" class="mb-1"> <form id="expire-invoice" action="/i/@Model.InvoiceId/expire" method="post" class="mb-1">

View file

@ -310,11 +310,12 @@
Actions Actions
</button> </button>
<div class="dropdown-menu" aria-labelledby="ActionsDropdownToggle"> <div class="dropdown-menu" aria-labelledby="ActionsDropdownToggle">
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="archive" id="ActionsDropdownArchive"><i class="fa fa-archive"></i> Archive</button> <button type="submit" class="dropdown-item" name="command" value="archive" id="ActionsDropdownArchive"><i class="fa fa-archive"></i> Archive</button>
@if (Model.IncludeArchived) @if (Model.IncludeArchived)
{ {
<button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="unarchive" id="ActionsDropdownUnarchive"><i class="fa fa-archive"></i> Unarchive</button> <button type="submit" asp-action="MassAction" class="dropdown-item" name="command" value="unarchive" id="ActionsDropdownUnarchive"><i class="fa fa-archive"></i> Unarchive</button>
} }
<button id="BumpFee" type="submit" class="dropdown-item" name="command" value="cpfp">Bump fee</button>
</div> </div>
</span> </span>
<span> <span>

View file

@ -3,22 +3,19 @@
var walletId = Context.GetRouteValue("walletId").ToString(); var walletId = Context.GetRouteValue("walletId").ToString();
Layout = "_LayoutWizard"; Layout = "_LayoutWizard";
ViewData.SetActivePage(WalletsNavPages.Send, "Sign PSBT", walletId); ViewData.SetActivePage(WalletsNavPages.Send, "Sign PSBT", walletId);
var returnUrl = this.Context.Request.Query["returnUrl"].FirstOrDefault();
} }
@section Navbar { @section Navbar {
<a asp-action="WalletPSBT" asp-route-walletId="@walletId" id="GoBack"> @if (returnUrl is string)
{
<a href="@returnUrl" id="GoBack">
<vc:icon symbol="back" /> <vc:icon symbol="back" />
</a> </a>
<a asp-action="WalletSend" asp-route-walletId="@walletId" class="cancel"> <a href="@returnUrl" class="cancel">
<vc:icon symbol="close" /> <vc:icon symbol="close" />
</a> </a>
} }
@section PageFootContent
{
<script>
delegate('click', '#GoBack', () => { history.back(); return false; })
</script>
} }
<header class="text-center"> <header class="text-center">
@ -40,7 +37,7 @@
<div asp-validation-summary="All" class="text-danger"></div> <div asp-validation-summary="All" class="text-danger"></div>
<form method="post" asp-action="SignWithSeed" asp-route-walletId="@walletId"> <form method="post" asp-action="SignWithSeed" asp-route-walletId="@walletId" asp-route-returnUrl="@returnUrl">
<partial name="SigningContext" for="SigningContext"/> <partial name="SigningContext" for="SigningContext"/>
<div class="form-group"> <div class="form-group">
<label asp-for="SeedOrKey" class="form-label"></label> <label asp-for="SeedOrKey" class="form-label"></label>

View file

@ -1,12 +1,13 @@
@model WalletPSBTViewModel @model WalletPSBTViewModel
@addTagHelper *, BundlerMinifier.TagHelpers @addTagHelper *, BundlerMinifier.TagHelpers
@{ @{
var walletId = Context.GetRouteValue("walletId").ToString(); var walletId = Context.GetRouteValue("walletId").ToString();
var isReady = !Model.HasErrors; var isReady = !Model.HasErrors;
var isSignable = !isReady && Model.NBXSeedAvailable; var isSignable = !isReady && Model.NBXSeedAvailable;
var needsExport = !isSignable && !isReady; var needsExport = !isSignable && !isReady;
Layout = "_LayoutWizard"; Layout = "_LayoutWizard";
ViewData.SetActivePage(WalletsNavPages.PSBT, isReady ? "Confirm broadcasting this transaction" : "Transaction Details", walletId); ViewData.SetActivePage(WalletsNavPages.PSBT, isReady ? "Confirm broadcasting this transaction" : "Transaction Details", walletId);
var returnUrl = this.Context.Request.Query["returnUrl"].FirstOrDefault();
} }
@section PageHeadContent { @section PageHeadContent {
@ -37,9 +38,12 @@
} }
@section Navbar { @section Navbar {
<a asp-action="WalletSend" asp-route-walletId="@Context.GetRouteValue("walletId")" class="cancel"> @if (returnUrl is string)
{
<a href="@returnUrl" class="cancel">
<vc:icon symbol="close" /> <vc:icon symbol="close" />
</a> </a>
}
} }
<header class="text-center mb-3"> <header class="text-center mb-3">
@ -56,13 +60,13 @@
<input type="hidden" asp-for="PSBT"/> <input type="hidden" asp-for="PSBT"/>
<input type="hidden" asp-for="FileName"/> <input type="hidden" asp-for="FileName"/>
<div class="d-flex flex-column flex-sm-row flex-wrap justify-content-center align-items-sm-center"> <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="nbx-seed" class="btn btn-primary">Sign transaction</button> <button type="submit" id="SignTransaction" name="command" value="sign" class="btn btn-primary">Sign transaction</button>
</div> </div>
</form> </form>
} }
else if (isReady) else if (isReady)
{ {
<form method="post" asp-action="WalletPSBTReady" asp-route-walletId="@walletId" class="my-5"> <form method="post" asp-action="WalletPSBTReady" asp-route-walletId="@walletId" asp-route-returnUrl="@returnUrl" class="my-5">
<input type="hidden" asp-for="SigningKey" /> <input type="hidden" asp-for="SigningKey" />
<input type="hidden" asp-for="SigningKeyPath" /> <input type="hidden" asp-for="SigningKeyPath" />
<partial name="SigningContext" for="SigningContext" /> <partial name="SigningContext" for="SigningContext" />
@ -75,7 +79,7 @@ else if (isReady)
} }
else else
{ {
<button type="submit" class="btn btn-primary" name="command" value="broadcast">Broadcast transaction</button> <button id="BroadcastTransaction" type="submit" class="btn btn-primary" name="command" value="broadcast">Broadcast transaction</button>
} }
</div> </div>
</form> </form>

View file

@ -230,7 +230,7 @@
</div> </div>
</div> </div>
<div class="form-group d-flex mt-2"> <div class="form-group d-flex mt-2">
<button type="submit" id="SignTransaction" name="command" value="@(Model.NBXSeedAvailable ? "nbx-seed" : "sign")" class="btn btn-primary">Sign transaction</button> <button type="submit" id="SignTransaction" name="command" value="sign" class="btn btn-primary">Sign transaction</button>
<button type="button" id="bip21parse" class="ms-3 btn btn-secondary" title="Paste BIP21/Address"><i class="fa fa-paste"></i></button> <button type="button" id="bip21parse" class="ms-3 btn btn-secondary" title="Paste BIP21/Address"><i class="fa fa-paste"></i></button>
<button type="button" id="scanqrcode" class="ms-3 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> <button type="button" id="scanqrcode" class="ms-3 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> </div>

View file

@ -3,15 +3,19 @@
var walletId = Context.GetRouteValue("walletId").ToString(); var walletId = Context.GetRouteValue("walletId").ToString();
Layout = "_LayoutWizard"; Layout = "_LayoutWizard";
ViewData.SetActivePage(WalletsNavPages.Send, "Sign the transaction", walletId); ViewData.SetActivePage(WalletsNavPages.Send, "Sign the transaction", walletId);
var returnUrl = this.Context.Request.Query["returnUrl"].FirstOrDefault();
} }
@section Navbar { @section Navbar {
<a asp-action="WalletPSBT" asp-route-walletId="@walletId" id="GoBack"> @if (returnUrl is string)
{
<a href="@returnUrl" id="GoBack">
<vc:icon symbol="back" /> <vc:icon symbol="back" />
</a> </a>
<a asp-action="WalletSend" asp-route-walletId="@walletId" class="cancel"> <a href="@returnUrl" class="cancel">
<vc:icon symbol="close" /> <vc:icon symbol="close" />
</a> </a>
}
} }
<header class="text-center"> <header class="text-center">
@ -27,7 +31,7 @@
</div> </div>
<div id="body" class="my-4"> <div id="body" class="my-4">
<form id="broadcastForm" asp-action="WalletSendVault" asp-route-walletId="@walletId" method="post" style="display:none;"> <form id="broadcastForm" asp-action="WalletSendVault" asp-route-walletId="@walletId" asp-route-returnUrl="@returnUrl" method="post" style="display:none;">
<input type="hidden" id="WalletId" asp-for="WalletId" /> <input type="hidden" id="WalletId" asp-for="WalletId" />
<input type="hidden" asp-for="WebsocketPath" /> <input type="hidden" asp-for="WebsocketPath" />
<partial name="SigningContext" for="SigningContext" /> <partial name="SigningContext" for="SigningContext" />
@ -44,8 +48,6 @@
<script src="~/js/vaultbridge.js" type="text/javascript" defer="defer" asp-append-version="true"></script> <script src="~/js/vaultbridge.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
<script src="~/js/vaultbridge.ui.js" type="text/javascript" defer="defer" asp-append-version="true"></script> <script src="~/js/vaultbridge.ui.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
<script> <script>
delegate('click', '#GoBack', () => { history.back(); return false; })
async function askSign() { async function askSign() {
var websocketPath = $("#WebsocketPath").val(); var websocketPath = $("#WebsocketPath").val();
var loc = window.location, ws_uri; var loc = window.location, ws_uri;

View file

@ -8,10 +8,10 @@
} }
@section Navbar { @section Navbar {
<a asp-all-route-data="Model.RouteDataBack"> <a href="@Model.ReturnUrl">
<vc:icon symbol="back" /> <vc:icon symbol="back" />
</a> </a>
<a asp-all-route-data="Model.RouteDataBack" class="cancel"> <a href="@Model.ReturnUrl" class="cancel">
<vc:icon symbol="close" /> <vc:icon symbol="close" />
</a> </a>
} }
@ -21,7 +21,7 @@
<p class="lead text-secondary mt-3">You can sign the transaction using one of the following methods.</p> <p class="lead text-secondary mt-3">You can sign the transaction using one of the following methods.</p>
</header> </header>
<form method="post" asp-action="WalletPSBT" asp-route-walletId="@walletId"> <form method="post" asp-action="WalletSign" asp-route-walletId="@walletId" asp-route-returnUrl="@Model.ReturnUrl">
<partial name="SigningContext" for="SigningContext" /> <partial name="SigningContext" for="SigningContext" />
@if (BTCPayNetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode).VaultSupported) @if (BTCPayNetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode).VaultSupported)

View file

@ -1,4 +1,4 @@
@model ListTransactionsViewModel @model ListTransactionsViewModel
@{ @{
var walletId = Context.GetRouteValue("walletId").ToString(); var walletId = Context.GetRouteValue("walletId").ToString();
Layout = "../Shared/_NavLayout.cshtml"; Layout = "../Shared/_NavLayout.cshtml";
@ -67,18 +67,24 @@
@* Custom Range Modal *@ @* Custom Range Modal *@
<script> <script>
delegate('click', '#switchTimeFormat', switchTimeFormat) delegate('click', '#switchTimeFormat', switchTimeFormat);
delegate('click', '#selectAllCheckbox', e => {
document.querySelectorAll(".selector").forEach(checkbox => {
checkbox.checked = e.target.checked;
});
});
</script> </script>
} }
<div class="d-flex align-items-center justify-content-between mb-3"> <div class="d-flex align-items-center justify-content-between mb-3">
<h3 class="mb-0">@ViewData["Title"]</h3> <h3 class="mb-0">@ViewData["Title"]</h3>
<form method="post" asp-action="WalletActions" asp-route-walletId="@Context.GetRouteValue("walletId")"> <form id="WalletActions" method="post" asp-action="WalletActions" asp-route-walletId="@Context.GetRouteValue("walletId")">
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-secondary dropdown-toggle mb-1" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button class="btn btn-secondary dropdown-toggle mb-1" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions Actions
</button> </button>
<div class="dropdown-menu" aria-labelledby="ActionsDropdownToggle"> <div class="dropdown-menu" aria-labelledby="ActionsDropdownToggle">
<button id="BumpFee" name="command" type="submit" class="dropdown-item" value="cpfp">Bump fee (CPFP)</button>
<a asp-action="WalletRescan" asp-route-walletId="@Context.GetRouteValue("walletId")" class="dropdown-item">Rescan wallet for missing transactions</a> <a asp-action="WalletRescan" asp-route-walletId="@Context.GetRouteValue("walletId")" class="dropdown-item">Rescan wallet for missing transactions</a>
<button name="command" type="submit" class="dropdown-item" value="prune">Prune old transactions from history</button> <button name="command" type="submit" class="dropdown-item" value="prune">Prune old transactions from history</button>
@if (User.IsInRole(Roles.ServerAdmin)) @if (User.IsInRole(Roles.ServerAdmin))
@ -116,6 +122,9 @@
<table class="table table-hover"> <table class="table table-hover">
<thead class="thead-inverse"> <thead class="thead-inverse">
<tr> <tr>
<th style="width:2rem;" class="only-for-js">
<input id="selectAllCheckbox" type="checkbox" class="form-check-input" />
</th>
<th style="min-width: 90px;" class="col-md-auto"> <th style="min-width: 90px;" class="col-md-auto">
Date Date
<a id="switchTimeFormat" href="#"> <a id="switchTimeFormat" href="#">
@ -132,6 +141,9 @@
@foreach (var transaction in Model.Transactions) @foreach (var transaction in Model.Transactions)
{ {
<tr> <tr>
<td class="only-for-js">
<input name="selectedTransactions" type="checkbox" class="selector form-check-input" form="WalletActions" value="@transaction.Id" />
</td>
<td> <td>
<span class="switchTimeFormat" data-switch="@transaction.Timestamp.ToTimeAgo()"> <span class="switchTimeFormat" data-switch="@transaction.Timestamp.ToTimeAgo()">
@transaction.Timestamp.ToBrowserDate() @transaction.Timestamp.ToBrowserDate()