mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-11 01:35:22 +01:00
[UX/UI] Add CPFP (#3395)
* Add CPFP * Sign PSBT should go back to the initial page
This commit is contained in:
parent
efed00f58b
commit
300d84c5d8
26 changed files with 432 additions and 204 deletions
|
@ -127,7 +127,6 @@ namespace BTCPayServer
|
|||
public string BlockExplorerLinkDefault { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
public int Divisibility { get; set; } = 8;
|
||||
[Obsolete("Should not be needed")]
|
||||
public bool IsBTC
|
||||
{
|
||||
get
|
||||
|
|
|
@ -204,8 +204,7 @@ namespace BTCPayServer.Tests
|
|||
{
|
||||
// Local link, this is fine
|
||||
}
|
||||
else if (attributeValue.StartsWith("http://") || attributeValue.StartsWith("https://") ||
|
||||
attributeValue.StartsWith("@"))
|
||||
else if (attributeValue.StartsWith("http://") || attributeValue.StartsWith("https://"))
|
||||
{
|
||||
// This can be an external link. Treating it as such.
|
||||
var rel = GetAttributeValue(node, "rel");
|
||||
|
|
|
@ -140,7 +140,7 @@ namespace BTCPayServer.Tests
|
|||
var postRedirectView = Assert.IsType<ViewResult>(view);
|
||||
var postRedirectViewModel = Assert.IsType<PostRedirectViewModel>(postRedirectView.Model);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,6 +85,11 @@ namespace BTCPayServer.Tests
|
|||
Driver.AssertNoError();
|
||||
}
|
||||
|
||||
public void PayInvoice()
|
||||
{
|
||||
Driver.FindElement(By.Id("FakePayment")).Click();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use this ServerUri when trying to browse with selenium
|
||||
/// 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();
|
||||
var name = "Store" + RandomUtils.GetUInt64();
|
||||
TestLogs.LogInformation($"Created store {name}");
|
||||
Driver.WaitForElement(By.Id("Name")).SendKeys(name);
|
||||
Driver.WaitForElement(By.Id("Create")).Click();
|
||||
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
|
||||
|
@ -161,7 +167,7 @@ namespace BTCPayServer.Tests
|
|||
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);
|
||||
GoToWalletSettings(cryptoCode);
|
||||
|
@ -181,11 +187,11 @@ namespace BTCPayServer.Tests
|
|||
Driver.FindElement(By.Id("ImportWalletOptionsLink")).Click();
|
||||
Driver.FindElement(By.Id("ImportSeedLink")).Click();
|
||||
Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed);
|
||||
Driver.SetCheckbox(By.Id("SavePrivateKeys"), privkeys);
|
||||
Driver.SetCheckbox(By.Id("SavePrivateKeys"), isHotWallet);
|
||||
}
|
||||
else
|
||||
{
|
||||
var option = privkeys ? "Hotwallet" : "Watchonly";
|
||||
var option = isHotWallet ? "Hotwallet" : "Watchonly";
|
||||
TestLogs.LogInformation($"Generating new seed ({option})");
|
||||
Driver.FindElement(By.Id("GenerateWalletLink")).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.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();
|
||||
|
||||
if (isImport)
|
||||
|
@ -366,7 +373,10 @@ namespace BTCPayServer.Tests
|
|||
public void GoToStore(string storeId, StoreNavPages storeNavPage = StoreNavPages.General)
|
||||
{
|
||||
if (storeId is not null)
|
||||
{
|
||||
GoToUrl($"/stores/{storeId}/");
|
||||
StoreId = storeId;
|
||||
}
|
||||
|
||||
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
|
||||
|
||||
|
@ -412,8 +422,9 @@ namespace BTCPayServer.Tests
|
|||
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($"invoice-checkout-{invoiceId}")).Click();
|
||||
CheckForJSErrors();
|
||||
|
@ -433,6 +444,7 @@ namespace BTCPayServer.Tests
|
|||
else
|
||||
{
|
||||
GoToUrl(storeId == null ? "/invoices/" : $"/stores/{storeId}/invoices/");
|
||||
StoreId = storeId;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -473,6 +485,7 @@ namespace BTCPayServer.Tests
|
|||
)
|
||||
{
|
||||
GoToInvoices(storeId);
|
||||
|
||||
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
|
||||
if (amount is decimal v)
|
||||
Driver.FindElement(By.Id("Amount")).SendKeys(v.ToString(CultureInfo.InvariantCulture));
|
||||
|
@ -487,8 +500,12 @@ namespace BTCPayServer.Tests
|
|||
Driver.FindElement(By.Id("Create")).Click();
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
//wait for seleniun update: https://stackoverflow.com/questions/57520296/selenium-webdriver-3-141-0-driver-manage-logs-availablelogtypes-throwing-syste
|
||||
|
|
|
@ -64,6 +64,48 @@ namespace BTCPayServer.Tests
|
|||
Assert.Contains("Starting listening NBXplorer", s.Driver.PageSource);
|
||||
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)]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
|
@ -932,7 +974,7 @@ namespace BTCPayServer.Tests
|
|||
{
|
||||
var cryptoCode = "BTC";
|
||||
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);
|
||||
if (isHotwallet)
|
||||
Assert.Contains("View seed", s.Driver.PageSource);
|
||||
|
|
|
@ -15,6 +15,7 @@ using BTCPayServer.Client.Models;
|
|||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
|
@ -29,6 +30,7 @@ using Microsoft.EntityFrameworkCore;
|
|||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
@ -441,17 +443,61 @@ namespace BTCPayServer.Controllers
|
|||
await _InvoiceRepository.MassArchive(selectedItems);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} archived.";
|
||||
break;
|
||||
|
||||
|
||||
case "unarchive":
|
||||
await _InvoiceRepository.MassArchive(selectedItems, false);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"{selectedItems.Length} invoice{(selectedItems.Length == 1 ? "" : "s")} unarchived.";
|
||||
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 });
|
||||
}
|
||||
|
||||
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}/{paymentMethodId}")]
|
||||
[HttpGet("invoice")]
|
||||
|
|
|
@ -42,6 +42,8 @@ namespace BTCPayServer.Controllers
|
|||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly PullPaymentHostedService _paymentHostedService;
|
||||
private readonly LanguageService _languageService;
|
||||
private readonly ExplorerClientProvider _ExplorerClients;
|
||||
private readonly UIWalletsController _walletsController;
|
||||
|
||||
public WebhookSender WebhookNotificationManager { get; }
|
||||
|
||||
|
@ -58,7 +60,9 @@ namespace BTCPayServer.Controllers
|
|||
ApplicationDbContextFactory dbContextFactory,
|
||||
PullPaymentHostedService paymentHostedService,
|
||||
WebhookSender webhookNotificationManager,
|
||||
LanguageService languageService)
|
||||
LanguageService languageService,
|
||||
ExplorerClientProvider explorerClients,
|
||||
UIWalletsController walletsController)
|
||||
{
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
|
@ -72,6 +76,8 @@ namespace BTCPayServer.Controllers
|
|||
_paymentHostedService = paymentHostedService;
|
||||
WebhookNotificationManager = webhookNotificationManager;
|
||||
_languageService = languageService;
|
||||
this._ExplorerClients = explorerClients;
|
||||
_walletsController = walletsController;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -312,18 +312,16 @@ namespace BTCPayServer.Controllers
|
|||
var redirectVm = new PostRedirectViewModel()
|
||||
{
|
||||
FormUrl = viewModel.RedirectUrl.AbsoluteUri,
|
||||
Parameters =
|
||||
FormParameters =
|
||||
{
|
||||
new KeyValuePair<string, string>("apiKey", key.Id),
|
||||
new KeyValuePair<string, string>("userId", key.UserId)
|
||||
}
|
||||
{ "apiKey", key.Id },
|
||||
{ "userId", key.UserId },
|
||||
},
|
||||
};
|
||||
foreach (var permission in permissions)
|
||||
{
|
||||
redirectVm.Parameters.Add(
|
||||
new KeyValuePair<string, string>("permissions[]", permission));
|
||||
redirectVm.FormParameters.Add("permissions[]", permission);
|
||||
}
|
||||
|
||||
return View("PostRedirect", redirectVm);
|
||||
}
|
||||
|
||||
|
|
|
@ -70,6 +70,115 @@ namespace BTCPayServer.Controllers
|
|||
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")]
|
||||
public async Task<IActionResult> WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletPSBTViewModel vm)
|
||||
|
@ -118,14 +227,12 @@ namespace BTCPayServer.Controllers
|
|||
return View(vm);
|
||||
}
|
||||
|
||||
vm.PSBT = psbt.ToBase64();
|
||||
vm.PSBTHex = psbt.ToHex();
|
||||
var res = await TryHandleSigningCommands(walletId, psbt, command, vm.SigningContext, nameof(WalletPSBT));
|
||||
if (res != null)
|
||||
{
|
||||
return res;
|
||||
}
|
||||
switch (command)
|
||||
{
|
||||
case "sign":
|
||||
return await WalletSign(walletId, vm, nameof(WalletPSBT));
|
||||
case "decode":
|
||||
ModelState.Remove(nameof(vm.PSBT));
|
||||
ModelState.Remove(nameof(vm.FileName));
|
||||
|
@ -407,6 +514,12 @@ namespace BTCPayServer.Controllers
|
|||
vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}";
|
||||
return View(nameof(WalletPSBT), vm);
|
||||
}
|
||||
else
|
||||
{
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var derivationSettings = GetDerivationSchemeSettings(walletId);
|
||||
wallet.InvalidateCache(derivationSettings.AccountDerivation);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -418,7 +531,12 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
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":
|
||||
return RedirectToWalletPSBT(new WalletPSBTViewModel()
|
||||
|
@ -460,45 +578,5 @@ namespace BTCPayServer.Controllers
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -723,17 +723,16 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
PayJoinBIP21 = vm.PayJoinBIP21,
|
||||
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)
|
||||
{
|
||||
case "sign":
|
||||
return await WalletSign(walletId, new WalletPSBTViewModel()
|
||||
{
|
||||
SigningContext = signingContext
|
||||
});
|
||||
case "analyze-psbt":
|
||||
var name =
|
||||
$"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",
|
||||
AspAction = nameof(WalletPSBTReady),
|
||||
Parameters =
|
||||
RouteParameters = { { "walletId", this.RouteData?.Values["walletId"]?.ToString() } },
|
||||
FormParameters =
|
||||
{
|
||||
new KeyValuePair<string, string>("SigningKey", vm.SigningKey),
|
||||
new KeyValuePair<string, string>("SigningKeyPath", vm.SigningKeyPath)
|
||||
{ "SigningKey", vm.SigningKey },
|
||||
{ "SigningKeyPath", vm.SigningKeyPath }
|
||||
}
|
||||
};
|
||||
AddSigningContext(redirectVm, vm.SigningContext);
|
||||
|
@ -834,7 +834,11 @@ namespace BTCPayServer.Controllers
|
|||
!string.IsNullOrEmpty(vm.SigningContext.PSBT))
|
||||
{
|
||||
//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);
|
||||
}
|
||||
|
@ -843,11 +847,11 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
if (signingContext is null)
|
||||
return;
|
||||
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.PSBT", signingContext.PSBT));
|
||||
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.OriginalPSBT", signingContext.OriginalPSBT));
|
||||
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.PayJoinBIP21", signingContext.PayJoinBIP21));
|
||||
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.EnforceLowR", signingContext.EnforceLowR?.ToString(CultureInfo.InvariantCulture)));
|
||||
redirectVm.Parameters.Add(new KeyValuePair<string, string>("SigningContext.ChangeAddress", signingContext.ChangeAddress));
|
||||
redirectVm.FormParameters.Add("SigningContext.PSBT", signingContext.PSBT);
|
||||
redirectVm.FormParameters.Add("SigningContext.OriginalPSBT", signingContext.OriginalPSBT);
|
||||
redirectVm.FormParameters.Add("SigningContext.PayJoinBIP21", signingContext.PayJoinBIP21);
|
||||
redirectVm.FormParameters.Add("SigningContext.EnforceLowR", signingContext.EnforceLowR?.ToString(CultureInfo.InvariantCulture));
|
||||
redirectVm.FormParameters.Add("SigningContext.ChangeAddress", signingContext.ChangeAddress);
|
||||
}
|
||||
|
||||
private IActionResult RedirectToWalletPSBT(WalletPSBTViewModel vm)
|
||||
|
@ -856,10 +860,11 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
AspController = "UIWallets",
|
||||
AspAction = nameof(WalletPSBT),
|
||||
Parameters =
|
||||
RouteParameters = { { "walletId", this.RouteData?.Values["walletId"]?.ToString() } },
|
||||
FormParameters =
|
||||
{
|
||||
new KeyValuePair<string, string>("psbt", vm.PSBT),
|
||||
new KeyValuePair<string, string>("fileName", vm.FileName)
|
||||
{ "psbt", vm.PSBT },
|
||||
{ "fileName", vm.FileName }
|
||||
}
|
||||
};
|
||||
return View("PostRedirect", redirectVm);
|
||||
|
@ -956,18 +961,6 @@ namespace BTCPayServer.Controllers
|
|||
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")]
|
||||
public async Task<IActionResult> WalletRescan(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
|
@ -1067,6 +1060,7 @@ namespace BTCPayServer.Controllers
|
|||
public async Task<IActionResult> WalletActions(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string command,
|
||||
string[] selectedTransactions,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var derivationScheme = GetDerivationSchemeSettings(walletId);
|
||||
|
@ -1075,6 +1069,31 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
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":
|
||||
{
|
||||
var result = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode).PruneAsync(derivationScheme.AccountDerivation, new PruneRequest(), cancellationToken);
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace BTCPayServer.Data
|
|||
addressInvoiceData.Address = address + "#" + paymentMethodId.ToString();
|
||||
return addressInvoiceData;
|
||||
}
|
||||
public static PaymentMethodId GetpaymentMethodId(this AddressInvoiceData addressInvoiceData)
|
||||
public static PaymentMethodId GetPaymentMethodId(this AddressInvoiceData addressInvoiceData)
|
||||
{
|
||||
if (addressInvoiceData.Address == null)
|
||||
return null;
|
||||
|
|
|
@ -498,14 +498,14 @@ namespace BTCPayServer
|
|||
{
|
||||
AspController = "UIHome",
|
||||
AspAction = "RecoverySeedBackup",
|
||||
Parameters =
|
||||
FormParameters =
|
||||
{
|
||||
new KeyValuePair<string, string>("cryptoCode", vm.CryptoCode),
|
||||
new KeyValuePair<string, string>("mnemonic", vm.Mnemonic),
|
||||
new KeyValuePair<string, string>("passphrase", vm.Passphrase),
|
||||
new KeyValuePair<string, string>("isStored", vm.IsStored ? "true" : "false"),
|
||||
new KeyValuePair<string, string>("requireConfirm", vm.RequireConfirm ? "true" : "false"),
|
||||
new KeyValuePair<string, string>("returnUrl", vm.ReturnUrl)
|
||||
{ "cryptoCode", vm.CryptoCode },
|
||||
{ "mnemonic", vm.Mnemonic },
|
||||
{ "passphrase", vm.Passphrase },
|
||||
{ "isStored", vm.IsStored ? "true" : "false" },
|
||||
{ "requireConfirm", vm.RequireConfirm ? "true" : "false" },
|
||||
{ "returnUrl", vm.ReturnUrl }
|
||||
}
|
||||
};
|
||||
return controller.View("PostRedirect", redirectVm);
|
||||
|
|
|
@ -13,6 +13,5 @@ namespace BTCPayServer
|
|||
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == cryptoCode);
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ namespace BTCPayServer.Models
|
|||
public string AspController { 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>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,13 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
{
|
||||
public WalletSigningOptionsModel(
|
||||
SigningContextModel signingContext,
|
||||
IDictionary<string, string> routeDataBack)
|
||||
string returnUrl)
|
||||
{
|
||||
SigningContext = signingContext;
|
||||
RouteDataBack = routeDataBack;
|
||||
ReturnUrl = returnUrl;
|
||||
}
|
||||
|
||||
public SigningContextModel SigningContext { get; }
|
||||
public IDictionary<string, string> RouteDataBack { get; }
|
||||
public string ReturnUrl { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ namespace BTCPayServer.Payments
|
|||
CryptoCode = cryptoCode.ToUpperInvariant();
|
||||
}
|
||||
|
||||
[Obsolete("Should only be used for legacy stuff")]
|
||||
public bool IsBTCOnChain
|
||||
{
|
||||
get
|
||||
|
|
|
@ -585,7 +585,7 @@ namespace BTCPayServer.Services.Invoices
|
|||
}
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -1,46 +1,71 @@
|
|||
@model PostRedirectViewModel
|
||||
@{
|
||||
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);
|
||||
@model PostRedirectViewModel
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<partial name="LayoutHead" />
|
||||
<title>Post Redirect</title>
|
||||
<partial name="LayoutHead" />
|
||||
<title>Post Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
<form method="post" id="postform" action="@action" rel="noreferrer noopener">
|
||||
@Html.AntiForgeryToken()
|
||||
@foreach (var o in Model.Parameters)
|
||||
{
|
||||
<input type="hidden" name="@o.Key" value="@o.Value"/>
|
||||
}
|
||||
<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" />
|
||||
@if (Model.FormUrl is null)
|
||||
{
|
||||
<form method="post" id="postform" asp-action="@Model.AspAction" asp-controller="@Model.AspController" asp-all-route-data="Model.RouteParameters">
|
||||
@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>
|
||||
}
|
||||
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>
|
||||
</html>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@model PaymentModel
|
||||
@model PaymentModel
|
||||
|
||||
<div id="testing">
|
||||
<hr class="my-3" />
|
||||
|
@ -13,7 +13,7 @@
|
|||
<div id="test-payment-crypto-code" class="input-group-addon">@Model.CryptoCode</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>
|
||||
</form>
|
||||
<form id="expire-invoice" action="/i/@Model.InvoiceId/expire" method="post" class="mb-1">
|
||||
|
|
|
@ -310,11 +310,12 @@
|
|||
Actions
|
||||
</button>
|
||||
<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)
|
||||
{
|
||||
<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>
|
||||
</span>
|
||||
<span>
|
||||
|
|
|
@ -3,22 +3,19 @@
|
|||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
Layout = "_LayoutWizard";
|
||||
ViewData.SetActivePage(WalletsNavPages.Send, "Sign PSBT", walletId);
|
||||
var returnUrl = this.Context.Request.Query["returnUrl"].FirstOrDefault();
|
||||
}
|
||||
|
||||
@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" />
|
||||
</a>
|
||||
<a asp-action="WalletSend" asp-route-walletId="@walletId" class="cancel">
|
||||
<a href="@returnUrl" class="cancel">
|
||||
<vc:icon symbol="close" />
|
||||
</a>
|
||||
}
|
||||
|
||||
@section PageFootContent
|
||||
{
|
||||
<script>
|
||||
delegate('click', '#GoBack', () => { history.back(); return false; })
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
|
@ -40,7 +37,7 @@
|
|||
|
||||
<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"/>
|
||||
<div class="form-group">
|
||||
<label asp-for="SeedOrKey" class="form-label"></label>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
@model WalletPSBTViewModel
|
||||
@model WalletPSBTViewModel
|
||||
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||
@{
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
var isReady = !Model.HasErrors;
|
||||
var isSignable = !isReady && Model.NBXSeedAvailable;
|
||||
var needsExport = !isSignable && !isReady;
|
||||
Layout = "_LayoutWizard";
|
||||
ViewData.SetActivePage(WalletsNavPages.PSBT, isReady ? "Confirm broadcasting this transaction" : "Transaction Details", walletId);
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
var isReady = !Model.HasErrors;
|
||||
var isSignable = !isReady && Model.NBXSeedAvailable;
|
||||
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 {
|
||||
|
@ -37,9 +38,12 @@
|
|||
}
|
||||
|
||||
@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" />
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
<header class="text-center mb-3">
|
||||
|
@ -56,13 +60,13 @@
|
|||
<input type="hidden" asp-for="PSBT"/>
|
||||
<input type="hidden" asp-for="FileName"/>
|
||||
<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>
|
||||
</form>
|
||||
}
|
||||
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="SigningKeyPath" />
|
||||
<partial name="SigningContext" for="SigningContext" />
|
||||
|
@ -75,7 +79,7 @@ else if (isReady)
|
|||
}
|
||||
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>
|
||||
</form>
|
||||
|
|
|
@ -230,7 +230,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<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="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>
|
||||
|
|
|
@ -3,15 +3,19 @@
|
|||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
Layout = "_LayoutWizard";
|
||||
ViewData.SetActivePage(WalletsNavPages.Send, "Sign the transaction", walletId);
|
||||
var returnUrl = this.Context.Request.Query["returnUrl"].FirstOrDefault();
|
||||
}
|
||||
|
||||
@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" />
|
||||
</a>
|
||||
<a asp-action="WalletSend" asp-route-walletId="@walletId" class="cancel">
|
||||
<a href="@returnUrl" class="cancel">
|
||||
<vc:icon symbol="close" />
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
<header class="text-center">
|
||||
|
@ -27,7 +31,7 @@
|
|||
</div>
|
||||
|
||||
<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" asp-for="WebsocketPath" />
|
||||
<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.ui.js" type="text/javascript" defer="defer" asp-append-version="true"></script>
|
||||
<script>
|
||||
delegate('click', '#GoBack', () => { history.back(); return false; })
|
||||
|
||||
async function askSign() {
|
||||
var websocketPath = $("#WebsocketPath").val();
|
||||
var loc = window.location, ws_uri;
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
}
|
||||
|
||||
@section Navbar {
|
||||
<a asp-all-route-data="Model.RouteDataBack">
|
||||
<a href="@Model.ReturnUrl">
|
||||
<vc:icon symbol="back" />
|
||||
</a>
|
||||
<a asp-all-route-data="Model.RouteDataBack" class="cancel">
|
||||
<a href="@Model.ReturnUrl" class="cancel">
|
||||
<vc:icon symbol="close" />
|
||||
</a>
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
|||
<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="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" />
|
||||
|
||||
@if (BTCPayNetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode).VaultSupported)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@model ListTransactionsViewModel
|
||||
@model ListTransactionsViewModel
|
||||
@{
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
|
@ -67,18 +67,24 @@
|
|||
|
||||
@* Custom Range Modal *@
|
||||
<script>
|
||||
delegate('click', '#switchTimeFormat', switchTimeFormat)
|
||||
delegate('click', '#switchTimeFormat', switchTimeFormat);
|
||||
delegate('click', '#selectAllCheckbox', e => {
|
||||
document.querySelectorAll(".selector").forEach(checkbox => {
|
||||
checkbox.checked = e.target.checked;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<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">
|
||||
<button class="btn btn-secondary dropdown-toggle mb-1" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<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>
|
||||
<button name="command" type="submit" class="dropdown-item" value="prune">Prune old transactions from history</button>
|
||||
@if (User.IsInRole(Roles.ServerAdmin))
|
||||
|
@ -116,6 +122,9 @@
|
|||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<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">
|
||||
Date
|
||||
<a id="switchTimeFormat" href="#">
|
||||
|
@ -132,6 +141,9 @@
|
|||
@foreach (var transaction in Model.Transactions)
|
||||
{
|
||||
<tr>
|
||||
<td class="only-for-js">
|
||||
<input name="selectedTransactions" type="checkbox" class="selector form-check-input" form="WalletActions" value="@transaction.Id" />
|
||||
</td>
|
||||
<td>
|
||||
<span class="switchTimeFormat" data-switch="@transaction.Timestamp.ToTimeAgo()">
|
||||
@transaction.Timestamp.ToBrowserDate()
|
||||
|
|
Loading…
Add table
Reference in a new issue