mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Fix the PSBT signing flow (#3465)
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Controllers;
|
using BTCPayServer.Controllers;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
@@ -8,12 +9,13 @@ using BTCPayServer.Tests.Logging;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitpayClient;
|
using NBitpayClient;
|
||||||
|
using OpenQA.Selenium;
|
||||||
|
using OpenQA.Selenium.Support.Extensions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
[Collection(nameof(NonParallelizableCollectionDefinition))]
|
|
||||||
public class PSBTTests : UnitTestBase
|
public class PSBTTests : UnitTestBase
|
||||||
{
|
{
|
||||||
public PSBTTests(ITestOutputHelper helper) : base(helper)
|
public PSBTTests(ITestOutputHelper helper) : base(helper)
|
||||||
@@ -23,125 +25,110 @@ namespace BTCPayServer.Tests
|
|||||||
[Trait("Integration", "Integration")]
|
[Trait("Integration", "Integration")]
|
||||||
public async Task CanPlayWithPSBT()
|
public async Task CanPlayWithPSBT()
|
||||||
{
|
{
|
||||||
using var tester = CreateServerTester();
|
using var s = CreateSeleniumTester(newDb: true);
|
||||||
await tester.StartAsync();
|
await s.StartAsync();
|
||||||
var user = tester.NewAccount();
|
|
||||||
user.GrantAccess();
|
|
||||||
user.RegisterDerivationScheme("BTC");
|
|
||||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
|
||||||
{
|
|
||||||
Price = 10,
|
|
||||||
Currency = "USD",
|
|
||||||
PosData = "posData",
|
|
||||||
OrderId = "orderId",
|
|
||||||
ItemDesc = "Some \", description",
|
|
||||||
FullNotifications = true
|
|
||||||
}, Facade.Merchant);
|
|
||||||
var cashCow = tester.ExplorerNode;
|
|
||||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
|
||||||
cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m));
|
|
||||||
TestUtils.Eventually(() =>
|
|
||||||
{
|
|
||||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
|
||||||
Assert.Equal("paid", invoice.Status);
|
|
||||||
});
|
|
||||||
|
|
||||||
var walletController = user.GetController<UIWalletsController>();
|
var u1 = s.RegisterNewUser(true);
|
||||||
var walletId = new WalletId(user.StoreId, "BTC");
|
var hot = s.CreateNewStore();
|
||||||
var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
|
var seed = s.GenerateWallet(isHotWallet: true);
|
||||||
var sendModel = new WalletSendModel()
|
var cold = s.CreateNewStore();
|
||||||
{
|
s.GenerateWallet(isHotWallet: false, seed: seed.ToString());
|
||||||
Outputs = new List<WalletSendModel.TransactionOutput>()
|
|
||||||
{
|
|
||||||
new WalletSendModel.TransactionOutput()
|
|
||||||
{
|
|
||||||
DestinationAddress = sendDestination,
|
|
||||||
Amount = 0.1m,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
FeeSatoshiPerByte = 1,
|
|
||||||
CurrentBalance = 1.5m
|
|
||||||
};
|
|
||||||
|
|
||||||
string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
|
// Scenario 1: one user has two stores sharing same seed
|
||||||
var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync<WalletPSBTViewModel>();
|
// one store is hot wallet, the other not.
|
||||||
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
|
||||||
Assert.NotNull(vmPSBT.Decoded);
|
|
||||||
|
|
||||||
var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
|
// Here, the cold wallet create a PSBT, then we switch to hot wallet to sign
|
||||||
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
|
// the PSBT and broadcast
|
||||||
|
s.GoToStore(cold.storeId);
|
||||||
|
var address = await s.FundStoreWallet();
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send);
|
||||||
|
SendAllTo(s, address);
|
||||||
|
s.Driver.TakeScreenshot().SaveAsFile(@"C:\Users\NicolasDorier\AppData\Local\Temp\1721425323\0--.png");
|
||||||
|
s.Driver.FindElement(By.Id("SignWithPSBT")).Click();
|
||||||
|
|
||||||
var vmPSBT2 = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
|
var psbt = ExtractPSBT(s);
|
||||||
{
|
|
||||||
SigningContext = new SigningContextModel
|
|
||||||
{
|
|
||||||
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
|
|
||||||
}
|
|
||||||
}).AssertViewModelAsync<WalletPSBTViewModel>();
|
|
||||||
Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
|
|
||||||
Assert.Equal(vmPSBT.PSBT, vmPSBT2.SigningContext.PSBT);
|
|
||||||
|
|
||||||
var signedPSBT = unsignedPSBT.Clone();
|
s.GoToStore(hot.storeId);
|
||||||
signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
|
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
|
||||||
vmPSBT.PSBT = signedPSBT.ToBase64();
|
s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbt);
|
||||||
var psbtReady = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
|
s.Driver.FindElement(By.Id("Decode")).Click();
|
||||||
{
|
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||||
SigningContext = new SigningContextModel
|
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
|
||||||
{
|
s.Driver.TakeScreenshot().SaveAsFile(@"C:\Users\NicolasDorier\AppData\Local\Temp\1721425323\0-.png");
|
||||||
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
|
s.FindAlertMessage();
|
||||||
}
|
|
||||||
}).AssertViewModelAsync<WalletPSBTViewModel>();
|
|
||||||
Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
|
|
||||||
Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
|
|
||||||
Assert.Contains(psbtReady.Destinations, d => d.Positive);
|
|
||||||
|
|
||||||
vmPSBT.PSBT = unsignedPSBT.ToBase64();
|
// Scenario 2: Same as scenario 1, except we create a PSBT from hot wallet, then sign by manually
|
||||||
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
|
// entering the seed on the cold wallet.
|
||||||
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
|
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send);
|
||||||
combineVM.PSBT = signedPSBT.ToBase64();
|
SendAllTo(s, address);
|
||||||
var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
|
psbt = ExtractPSBT(s);
|
||||||
|
|
||||||
var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
|
// Let's check it has been signed, then remove the signature.
|
||||||
Assert.True(signedPSBT.TryFinalize(out _));
|
// Also remove the hdkeys so we can test the update later
|
||||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
var psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork);
|
||||||
Assert.Equal(signedPSBT, signedPSBT2);
|
var signedPSBT = psbtParsed.Clone();
|
||||||
|
Assert.True(psbtParsed.Clone().TryFinalize(out _));
|
||||||
|
Assert.Single(psbtParsed.Inputs[0].PartialSigs);
|
||||||
|
psbtParsed.Inputs[0].PartialSigs.Clear();
|
||||||
|
Assert.Single(psbtParsed.Inputs[0].HDKeyPaths);
|
||||||
|
psbtParsed.Inputs[0].HDKeyPaths.Clear();
|
||||||
|
var skeletonPSBT = psbtParsed;
|
||||||
|
|
||||||
// Can use uploaded file?
|
s.GoToStore(cold.storeId);
|
||||||
combineVM.PSBT = null;
|
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
|
||||||
combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
|
s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64());
|
||||||
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
|
s.Driver.FindElement(By.Id("Decode")).Click();
|
||||||
signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
|
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||||
Assert.True(signedPSBT.TryFinalize(out _));
|
s.Driver.FindElement(By.Id("SignWithSeed")).Click();
|
||||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
s.Driver.FindElement(By.Name("SeedOrKey")).SendKeys(seed.ToString());
|
||||||
Assert.Equal(signedPSBT, signedPSBT2);
|
s.Driver.FindElement(By.Id("Submit")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
|
||||||
|
s.FindAlertMessage();
|
||||||
|
|
||||||
var ready = (await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
|
// Let's check if the update feature works
|
||||||
{
|
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
|
||||||
SigningContext = new SigningContextModel(signedPSBT)
|
s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64());
|
||||||
})).AssertViewModel<WalletPSBTViewModel>();
|
s.Driver.FindElement(By.Id("Decode")).Click();
|
||||||
Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT);
|
s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click();
|
||||||
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
|
s.Driver.WaitForElement(By.Id("update-psbt")).Click();
|
||||||
Assert.Equal(signedPSBT.ToBase64(), psbt);
|
|
||||||
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
|
|
||||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
|
||||||
|
|
||||||
//test base64 psbt file
|
psbt = ExtractPSBT(s);
|
||||||
Assert.False(string.IsNullOrEmpty(Assert.IsType<WalletPSBTViewModel>(
|
psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork);
|
||||||
Assert.IsType<ViewResult>(
|
Assert.Single(psbtParsed.Inputs[0].HDKeyPaths);
|
||||||
await walletController.WalletPSBT(walletId,
|
Assert.Empty(psbtParsed.Inputs[0].PartialSigs);
|
||||||
new WalletPSBTViewModel
|
|
||||||
{
|
// Let's if we can combine the updated psbt (which has hdkeys, but no sig)
|
||||||
UploadedPSBTFile = TestUtils.GetFormFile("base64", signedPSBT.ToBase64())
|
// with the signed psbt (which has sig, but no hdkeys)
|
||||||
})).Model).PSBT));
|
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
|
||||||
|
s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbtParsed.ToBase64());
|
||||||
|
s.Driver.FindElement(By.Id("Decode")).Click();
|
||||||
|
s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click();
|
||||||
|
s.Driver.WaitForElement(By.Id("combine-psbt")).Click();
|
||||||
|
signedPSBT.Inputs[0].HDKeyPaths.Clear();
|
||||||
|
s.Driver.FindElement(By.Name("PSBT")).SendKeys(signedPSBT.ToBase64());
|
||||||
|
s.Driver.WaitForElement(By.Id("Submit")).Click();
|
||||||
|
|
||||||
|
psbt = ExtractPSBT(s);
|
||||||
|
psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork);
|
||||||
|
Assert.Single(psbtParsed.Inputs[0].HDKeyPaths);
|
||||||
|
Assert.Single(psbtParsed.Inputs[0].PartialSigs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string AssertRedirectedPSBT(IActionResult view, string actionName)
|
private static void SendAllTo(SeleniumTester s, string address)
|
||||||
{
|
{
|
||||||
var postRedirectView = Assert.IsType<ViewResult>(view);
|
s.Driver.FindElement(By.Name("Outputs[0].DestinationAddress")).SendKeys(address);
|
||||||
var postRedirectViewModel = Assert.IsType<PostRedirectViewModel>(postRedirectView.Model);
|
s.Driver.FindElement(By.ClassName("crypto-balance-link")).Click();
|
||||||
Assert.Equal(actionName, postRedirectViewModel.AspAction);
|
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||||
var redirectedPSBT = postRedirectViewModel.FormParameters.Single(p => p.Key == "psbt" || p.Key == "SigningContext.PSBT").Value?.FirstOrDefault();
|
}
|
||||||
return redirectedPSBT;
|
|
||||||
|
private static string ExtractPSBT(SeleniumTester s)
|
||||||
|
{
|
||||||
|
var pageSource = s.Driver.PageSource;
|
||||||
|
var start = pageSource.IndexOf("id=\"psbt-base64\">");
|
||||||
|
start += "id=\"psbt-base64\">".Length;
|
||||||
|
var end = pageSource.IndexOf("<", start);
|
||||||
|
return pageSource[start..end];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ using NBXplorer.DerivationStrategy;
|
|||||||
using NBXplorer.Models;
|
using NBXplorer.Models;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using OpenQA.Selenium;
|
using OpenQA.Selenium;
|
||||||
|
using OpenQA.Selenium.Support.Extensions;
|
||||||
using OpenQA.Selenium.Support.UI;
|
using OpenQA.Selenium.Support.UI;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
@@ -252,6 +253,7 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
|
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
|
||||||
{
|
{
|
||||||
|
s.Driver.TakeScreenshot().SaveAsFile(@"C:\Users\NicolasDorier\AppData\Local\Temp\1721425323\fwefw.png");
|
||||||
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();
|
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using BTCPayServer.Views.Stores;
|
|||||||
using BTCPayServer.Views.Wallets;
|
using BTCPayServer.Views.Wallets;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
using NBitcoin.RPC;
|
||||||
using OpenQA.Selenium;
|
using OpenQA.Selenium;
|
||||||
using OpenQA.Selenium.Chrome;
|
using OpenQA.Selenium.Chrome;
|
||||||
using OpenQA.Selenium.Support.UI;
|
using OpenQA.Selenium.Support.UI;
|
||||||
@@ -376,6 +377,8 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
GoToUrl($"/stores/{storeId}/");
|
GoToUrl($"/stores/{storeId}/");
|
||||||
StoreId = storeId;
|
StoreId = storeId;
|
||||||
|
if (WalletId != null)
|
||||||
|
WalletId = new WalletId(storeId, WalletId.CryptoCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
|
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
|
||||||
@@ -422,7 +425,7 @@ namespace BTCPayServer.Tests
|
|||||||
Driver.FindElement(By.Id($"StoreSelectorMenuItem-{storeId}")).Click();
|
Driver.FindElement(By.Id($"StoreSelectorMenuItem-{storeId}")).Click();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GoToInvoiceCheckout(string? invoiceId = null)
|
public void GoToInvoiceCheckout(string invoiceId = null)
|
||||||
{
|
{
|
||||||
invoiceId ??= InvoiceId;
|
invoiceId ??= InvoiceId;
|
||||||
Driver.FindElement(By.Id("StoreNav-Invoices")).Click();
|
Driver.FindElement(By.Id("StoreNav-Invoices")).Click();
|
||||||
@@ -507,7 +510,7 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
string InvoiceId;
|
string InvoiceId;
|
||||||
|
|
||||||
public async Task FundStoreWallet(WalletId walletId = null, int coins = 1, decimal denomination = 1m)
|
public async Task<string> FundStoreWallet(WalletId walletId = null, int coins = 1, decimal denomination = 1m)
|
||||||
{
|
{
|
||||||
walletId ??= WalletId;
|
walletId ??= WalletId;
|
||||||
GoToWallet(walletId, WalletsNavPages.Receive);
|
GoToWallet(walletId, WalletsNavPages.Receive);
|
||||||
@@ -516,8 +519,21 @@ namespace BTCPayServer.Tests
|
|||||||
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
||||||
for (var i = 0; i < coins; i++)
|
for (var i = 0; i < coins; i++)
|
||||||
{
|
{
|
||||||
await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination));
|
bool mined = false;
|
||||||
|
retry:
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination));
|
||||||
|
}
|
||||||
|
catch (RPCException) when (!mined)
|
||||||
|
{
|
||||||
|
mined = true;
|
||||||
|
await Server.ExplorerNode.GenerateAsync(1);
|
||||||
|
goto retry;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Driver.Navigate().Refresh();
|
||||||
|
return addressStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckForJSErrors()
|
private void CheckForJSErrors()
|
||||||
|
|||||||
@@ -146,17 +146,22 @@ namespace BTCPayServer.Controllers
|
|||||||
WalletId walletId, WalletPSBTViewModel vm, string returnUrl = null, string command = null)
|
WalletId walletId, WalletPSBTViewModel vm, string returnUrl = null, string command = null)
|
||||||
{
|
{
|
||||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
|
||||||
vm.SigningContext.PSBT ??= psbt.ToBase64();
|
|
||||||
if (returnUrl is null)
|
if (returnUrl is null)
|
||||||
returnUrl = Url.Action(nameof(WalletTransactions), new { walletId });
|
returnUrl = Url.Action(nameof(WalletTransactions), new { walletId });
|
||||||
|
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||||
|
if (psbt is null || vm.InvalidPSBT)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||||
|
return View("WalletSigningOptions", new WalletSigningOptionsModel(vm.SigningContext, returnUrl));
|
||||||
|
}
|
||||||
switch (command)
|
switch (command)
|
||||||
{
|
{
|
||||||
case "vault":
|
case "vault":
|
||||||
return ViewVault(walletId, vm.SigningContext);
|
return ViewVault(walletId, vm.SigningContext);
|
||||||
case "seed":
|
case "seed":
|
||||||
return SignWithSeed(walletId, vm.SigningContext);
|
return SignWithSeed(walletId, vm.SigningContext);
|
||||||
|
case "decode":
|
||||||
|
return await WalletPSBT(walletId, vm, "decode");
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -171,7 +176,7 @@ namespace BTCPayServer.Controllers
|
|||||||
WellknownMetadataKeys.MasterHDKey);
|
WellknownMetadataKeys.MasterHDKey);
|
||||||
if (extKey != null)
|
if (extKey != null)
|
||||||
{
|
{
|
||||||
return SignWithSeed(walletId,
|
return await SignWithSeed(walletId,
|
||||||
new SignWithSeedViewModel { SeedOrKey = extKey, SigningContext = vm.SigningContext });
|
new SignWithSeedViewModel { SeedOrKey = extKey, SigningContext = vm.SigningContext });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,26 +186,16 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
[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)
|
||||||
{
|
{
|
||||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||||
|
var vm = new WalletPSBTViewModel();
|
||||||
vm.CryptoCode = network.CryptoCode;
|
vm.CryptoCode = network.CryptoCode;
|
||||||
|
|
||||||
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
|
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
|
||||||
if (derivationSchemeSettings == null)
|
if (derivationSchemeSettings == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet;
|
vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet;
|
||||||
|
|
||||||
if (await vm.GetPSBT(network.NBitcoinNetwork) is PSBT psbt)
|
|
||||||
{
|
|
||||||
vm.PSBT = vm.SigningContext.PSBT = psbt.ToBase64();
|
|
||||||
vm.PSBTHex = psbt.ToHex();
|
|
||||||
vm.Decoded = psbt.ToString();
|
|
||||||
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
|
||||||
return View("WalletPSBTDecoded", vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,10 +203,8 @@ namespace BTCPayServer.Controllers
|
|||||||
public async Task<IActionResult> WalletPSBT(
|
public async Task<IActionResult> WalletPSBT(
|
||||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||||
WalletId walletId,
|
WalletId walletId,
|
||||||
WalletPSBTViewModel vm, string command = null)
|
WalletPSBTViewModel vm, string command)
|
||||||
{
|
{
|
||||||
if (command == null)
|
|
||||||
return await WalletPSBT(walletId, vm);
|
|
||||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||||
vm.CryptoCode = network.CryptoCode;
|
vm.CryptoCode = network.CryptoCode;
|
||||||
|
|
||||||
@@ -221,25 +214,23 @@ namespace BTCPayServer.Controllers
|
|||||||
|
|
||||||
vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet;
|
vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet;
|
||||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||||
if (psbt == null)
|
if (vm.InvalidPSBT)
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
if (psbt is null)
|
||||||
vm.PSBT = psbt.ToBase64();
|
{
|
||||||
vm.PSBTHex = psbt.ToHex();
|
return View("WalletPSBT", vm);
|
||||||
|
}
|
||||||
switch (command)
|
switch (command)
|
||||||
{
|
{
|
||||||
case "sign":
|
case "sign":
|
||||||
return await WalletSign(walletId, vm, nameof(WalletPSBT));
|
return await WalletSign(walletId, vm);
|
||||||
case "decode":
|
case "decode":
|
||||||
ModelState.Remove(nameof(vm.PSBT));
|
ModelState.Remove(nameof(vm.PSBT));
|
||||||
ModelState.Remove(nameof(vm.FileName));
|
ModelState.Remove(nameof(vm.FileName));
|
||||||
ModelState.Remove(nameof(vm.UploadedPSBTFile));
|
ModelState.Remove(nameof(vm.UploadedPSBTFile));
|
||||||
vm.PSBT = vm.SigningContext.PSBT;
|
|
||||||
vm.PSBTHex = psbt.ToHex();
|
|
||||||
vm.Decoded = psbt.ToString();
|
|
||||||
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
||||||
return View("WalletPSBTDecoded", vm);
|
return View("WalletPSBTDecoded", vm);
|
||||||
|
|
||||||
@@ -273,8 +264,7 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
var viewName = string.IsNullOrEmpty(vm.PSBT) ? "WalletPSBT" : "WalletPSBTDecoded";
|
return View("WalletPSBTDecoded", vm);
|
||||||
return View(viewName, vm);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,28 +383,21 @@ namespace BTCPayServer.Controllers
|
|||||||
[HttpPost("{walletId}/psbt/ready")]
|
[HttpPost("{walletId}/psbt/ready")]
|
||||||
public async Task<IActionResult> WalletPSBTReady(
|
public async Task<IActionResult> WalletPSBTReady(
|
||||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||||
WalletId walletId, WalletPSBTViewModel vm, string command = null, CancellationToken cancellationToken = default)
|
WalletId walletId, WalletPSBTViewModel vm, string command, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (command == null)
|
|
||||||
return await WalletPSBT(walletId, vm);
|
|
||||||
|
|
||||||
PSBT psbt;
|
|
||||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||||
DerivationSchemeSettings derivationSchemeSettings;
|
PSBT psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||||
try
|
if (vm.InvalidPSBT || psbt is null)
|
||||||
{
|
{
|
||||||
psbt = PSBT.Parse(vm.SigningContext.PSBT, network.NBitcoinNetwork);
|
if (vm.InvalidPSBT)
|
||||||
derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
|
vm.GlobalError = "Invalid PSBT";
|
||||||
if (derivationSchemeSettings == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
vm.GlobalError = "Invalid PSBT";
|
|
||||||
return View(nameof(WalletPSBT), vm);
|
return View(nameof(WalletPSBT), vm);
|
||||||
}
|
}
|
||||||
|
DerivationSchemeSettings derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
|
||||||
|
if (derivationSchemeSettings == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
||||||
|
|
||||||
switch (command)
|
switch (command)
|
||||||
{
|
{
|
||||||
@@ -543,6 +526,9 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
PSBT = psbt.ToBase64()
|
PSBT = psbt.ToBase64()
|
||||||
});
|
});
|
||||||
|
case "decode":
|
||||||
|
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
||||||
|
return View("WalletPSBTDecoded", vm);
|
||||||
default:
|
default:
|
||||||
vm.GlobalError = "Unknown command";
|
vm.GlobalError = "Unknown command";
|
||||||
return View(nameof(WalletPSBT), vm);
|
return View(nameof(WalletPSBT), vm);
|
||||||
|
|||||||
@@ -826,7 +826,8 @@ namespace BTCPayServer.Controllers
|
|||||||
FormParameters =
|
FormParameters =
|
||||||
{
|
{
|
||||||
{ "SigningKey", vm.SigningKey },
|
{ "SigningKey", vm.SigningKey },
|
||||||
{ "SigningKeyPath", vm.SigningKeyPath }
|
{ "SigningKeyPath", vm.SigningKeyPath },
|
||||||
|
{ "command", "decode" }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
AddSigningContext(redirectVm, vm.SigningContext);
|
AddSigningContext(redirectVm, vm.SigningContext);
|
||||||
@@ -834,6 +835,7 @@ 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.FormParameters.Remove("command");
|
||||||
redirectVm.FormParameters.Add("command", "broadcast");
|
redirectVm.FormParameters.Add("command", "broadcast");
|
||||||
}
|
}
|
||||||
if (this.HttpContext.Request.Query["returnUrl"].FirstOrDefault() is string returnUrl)
|
if (this.HttpContext.Request.Query["returnUrl"].FirstOrDefault() is string returnUrl)
|
||||||
@@ -864,7 +866,8 @@ namespace BTCPayServer.Controllers
|
|||||||
FormParameters =
|
FormParameters =
|
||||||
{
|
{
|
||||||
{ "psbt", vm.PSBT },
|
{ "psbt", vm.PSBT },
|
||||||
{ "fileName", vm.FileName }
|
{ "fileName", vm.FileName },
|
||||||
|
{ "command", "decode" },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return View("PostRedirect", redirectVm);
|
return View("PostRedirect", redirectVm);
|
||||||
@@ -876,12 +879,12 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
return View(nameof(SignWithSeed), new SignWithSeedViewModel
|
return View(nameof(SignWithSeed), new SignWithSeedViewModel
|
||||||
{
|
{
|
||||||
SigningContext = signingContext,
|
SigningContext = signingContext
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{walletId}/psbt/seed")]
|
[HttpPost("{walletId}/psbt/seed")]
|
||||||
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
|
public async Task<IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
|
||||||
WalletId walletId, SignWithSeedViewModel viewModel)
|
WalletId walletId, SignWithSeedViewModel viewModel)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
@@ -891,7 +894,6 @@ namespace BTCPayServer.Controllers
|
|||||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||||
if (network == null)
|
if (network == null)
|
||||||
throw new FormatException("Invalid value for crypto code");
|
throw new FormatException("Invalid value for crypto code");
|
||||||
|
|
||||||
ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork);
|
ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork);
|
||||||
|
|
||||||
if (extKey == null)
|
if (extKey == null)
|
||||||
@@ -943,8 +945,19 @@ namespace BTCPayServer.Controllers
|
|||||||
var changed = psbt.PSBTChanged(() => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath));
|
var changed = psbt.PSBTChanged(() => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath));
|
||||||
if (!changed)
|
if (!changed)
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable causes: Incorrect account key path in wallet settings or PSBT already signed.");
|
var update = new UpdatePSBTRequest()
|
||||||
return View(nameof(SignWithSeed), viewModel);
|
{
|
||||||
|
PSBT = psbt,
|
||||||
|
DerivationScheme = settings.AccountDerivation
|
||||||
|
};
|
||||||
|
update.RebaseKeyPaths = settings.GetPSBTRebaseKeyRules().ToList();
|
||||||
|
psbt = (await ExplorerClientProvider.GetExplorerClient(network).UpdatePSBTAsync(update))?.PSBT;
|
||||||
|
changed = psbt is not null && psbt.PSBTChanged(() => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath));
|
||||||
|
if (!changed)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable causes: Incorrect account key path in wallet settings or PSBT already signed.");
|
||||||
|
return View(nameof(SignWithSeed), viewModel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ModelState.Remove(nameof(viewModel.SigningContext.PSBT));
|
ModelState.Remove(nameof(viewModel.SigningContext.PSBT));
|
||||||
viewModel.SigningContext.PSBT = psbt.ToBase64();
|
viewModel.SigningContext.PSBT = psbt.ToBase64();
|
||||||
|
|||||||
@@ -34,7 +34,24 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||||||
[Display(Name = "Upload PSBT from file")]
|
[Display(Name = "Upload PSBT from file")]
|
||||||
public IFormFile UploadedPSBTFile { get; set; }
|
public IFormFile UploadedPSBTFile { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public async Task<PSBT> GetPSBT(Network network)
|
public async Task<PSBT> GetPSBT(Network network)
|
||||||
|
{
|
||||||
|
var psbt = await GetPSBTCore(network);
|
||||||
|
if (psbt != null)
|
||||||
|
{
|
||||||
|
Decoded = psbt.ToString();
|
||||||
|
PSBTHex = psbt.ToHex();
|
||||||
|
PSBT = psbt.ToBase64();
|
||||||
|
if (SigningContext is null)
|
||||||
|
SigningContext = new SigningContextModel(psbt);
|
||||||
|
else
|
||||||
|
SigningContext.PSBT = psbt.ToBase64();
|
||||||
|
}
|
||||||
|
return psbt;
|
||||||
|
}
|
||||||
|
public bool InvalidPSBT { get; set; }
|
||||||
|
async Task<PSBT> GetPSBTCore(Network network)
|
||||||
{
|
{
|
||||||
if (UploadedPSBTFile != null)
|
if (UploadedPSBTFile != null)
|
||||||
{
|
{
|
||||||
@@ -54,6 +71,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||||||
{
|
{
|
||||||
using var stream = new StreamReader(UploadedPSBTFile.OpenReadStream());
|
using var stream = new StreamReader(UploadedPSBTFile.OpenReadStream());
|
||||||
PSBT = await stream.ReadToEndAsync();
|
PSBT = await stream.ReadToEndAsync();
|
||||||
|
InvalidPSBT = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (SigningContext != null && !string.IsNullOrEmpty(SigningContext.PSBT))
|
if (SigningContext != null && !string.IsNullOrEmpty(SigningContext.PSBT))
|
||||||
@@ -64,10 +82,11 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
InvalidPSBT = false;
|
||||||
return NBitcoin.PSBT.Parse(PSBT, network);
|
return NBitcoin.PSBT.Parse(PSBT, network);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{ }
|
{ InvalidPSBT = true; }
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.Globalization;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Services.Altcoins.Zcash.Configuration;
|
using BTCPayServer.Services.Altcoins.Zcash.Configuration;
|
||||||
@@ -117,7 +118,7 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
|
|||||||
public bool DaemonAvailable { get; set; }
|
public bool DaemonAvailable { get; set; }
|
||||||
public bool WalletAvailable { get; set; }
|
public bool WalletAvailable { get; set; }
|
||||||
|
|
||||||
public override String ToString() { return String.Format("{0} {1} {2} {3} {4} {5}", Synced, CurrentHeight, TargetHeight, WalletHeight, DaemonAvailable, WalletAvailable); }
|
public override String ToString() { return String.Format(CultureInfo.InvariantCulture, "{0} {1} {2} {3} {4} {5}", Synced, CurrentHeight, TargetHeight, WalletHeight, DaemonAvailable, WalletAvailable); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|||||||
@@ -47,5 +47,5 @@
|
|||||||
<label asp-for="Passphrase" class="form-label"></label>
|
<label asp-for="Passphrase" class="form-label"></label>
|
||||||
<input asp-for="Passphrase" class="form-control"/>
|
<input asp-for="Passphrase" class="form-control"/>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Sign</button>
|
<button id="Submit" type="submit" class="btn btn-primary">Sign</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@model WalletPSBTCombineViewModel
|
@model WalletPSBTCombineViewModel
|
||||||
@{
|
@{
|
||||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||||
Layout = "_LayoutWizard";
|
Layout = "_LayoutWizard";
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<h1>@ViewData["Title"]</h1>
|
<h1>@ViewData["Title"]</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form class="form-group" method="post" asp-action="WalletPSBTCombine" enctype="multipart/form-data">
|
<form class="form-group" method="post" asp-action="WalletPSBTCombine" asp-route-walletId="@Context.GetRouteValue("walletId")" enctype="multipart/form-data">
|
||||||
<input type="hidden" asp-for="OtherPSBT"/>
|
<input type="hidden" asp-for="OtherPSBT"/>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="PSBT" class="form-label"></label>
|
<label asp-for="PSBT" class="form-label"></label>
|
||||||
@@ -26,5 +26,5 @@
|
|||||||
<label asp-for="UploadedPSBTFile" class="form-label"></label>
|
<label asp-for="UploadedPSBTFile" class="form-label"></label>
|
||||||
<input type="file" class="form-control" asp-for="UploadedPSBTFile">
|
<input type="file" class="form-control" asp-for="UploadedPSBTFile">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Combine</button>
|
<button id="Submit" type="submit" class="btn btn-primary">Combine</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
@{
|
@{
|
||||||
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;
|
||||||
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);
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<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="sign" class="btn btn-primary">Sign transaction</button>
|
<button type="submit" id="SignTransaction" name="command" value="sign" asp-route-returnUrl="@returnUrl" class="btn btn-primary">Sign transaction</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ else
|
|||||||
<div class="d-flex flex-column flex-sm-row flex-wrap align-items-sm-center">
|
<div class="d-flex flex-column flex-sm-row flex-wrap align-items-sm-center">
|
||||||
<button name="command" type="submit" class="btn btn-primary mb-3 mb-sm-0 me-sm-2" value="save-psbt">Download PSBT file</button>
|
<button name="command" type="submit" class="btn btn-primary mb-3 mb-sm-0 me-sm-2" value="save-psbt">Download PSBT file</button>
|
||||||
<button name="command" type="button" class="btn btn-primary mb-3 mb-sm-0 me-sm-2 only-for-js" data-bs-toggle="modal" data-bs-target="#scan-qr-modal">Show QR for wallet camera</button>
|
<button name="command" type="button" class="btn btn-primary mb-3 mb-sm-0 me-sm-2 only-for-js" data-bs-toggle="modal" data-bs-target="#scan-qr-modal">Show QR for wallet camera</button>
|
||||||
<a href="#ExportOptions" data-bs-toggle="collapse" class="btn btn-link text-secondary">Show raw versions</a>
|
<a id="ShowRawVersion" href="#ExportOptions" data-bs-toggle="collapse" class="btn btn-link text-secondary">Show raw versions</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div id="ExportOptions" class="collapse">
|
<div id="ExportOptions" class="collapse">
|
||||||
@@ -126,7 +126,7 @@ else
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content" id="export-tabContent">
|
<div class="tab-content" id="export-tabContent">
|
||||||
<div class="tab-pane fade show active" id="export-base64" role="tabpanel" aria-labelledby="export-base64-tab">
|
<div class="tab-pane fade show active" id="export-base64" role="tabpanel" aria-labelledby="export-base64-tab">
|
||||||
<pre class="mb-4 text-wrap"><code class="text">@Model.PSBT</code></pre>
|
<pre class="mb-4 text-wrap"><code class="text" id="psbt-base64">@Model.PSBT</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="export-hex" role="tabpanel" aria-labelledby="export-hex-tab">
|
<div class="tab-pane fade" id="export-hex" role="tabpanel" aria-labelledby="export-hex-tab">
|
||||||
<pre class="mb-4 text-wrap"><code class="text">@Model.PSBTHex</code></pre>
|
<pre class="mb-4 text-wrap"><code class="text">@Model.PSBTHex</code></pre>
|
||||||
@@ -179,14 +179,11 @@ else
|
|||||||
<div id="PSBTOptionsAdvancedContent" class="accordion-collapse collapse" aria-labelledby="PSBTOptionsAdvancedHeader" data-bs-parent="#PSBTOptions">
|
<div id="PSBTOptionsAdvancedContent" class="accordion-collapse collapse" aria-labelledby="PSBTOptionsAdvancedHeader" data-bs-parent="#PSBTOptions">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<form method="post" asp-action="WalletPSBT" asp-route-walletId="@walletId" class="mb-2">
|
<form method="post" asp-action="WalletPSBT" asp-route-walletId="@walletId" class="mb-2">
|
||||||
<input type="hidden" asp-for="CryptoCode"/>
|
|
||||||
<input type="hidden" asp-for="NBXSeedAvailable"/>
|
|
||||||
<input type="hidden" asp-for="PSBT"/>
|
<input type="hidden" asp-for="PSBT"/>
|
||||||
<input type="hidden" asp-for="FileName"/>
|
|
||||||
<p class="mb-2">For exporting the signed PSBT and transaction information to a wallet, update the PSBT.</p>
|
<p class="mb-2">For exporting the signed PSBT and transaction information to a wallet, update the PSBT.</p>
|
||||||
<button type="submit" name="command" value="update" class="btn btn-secondary">Update PSBT</button>
|
<button id="update-psbt" type="submit" name="command" value="update" class="btn btn-secondary">Update PSBT</button>
|
||||||
<p class="mt-4 mb-2">For batching transactions, you can combine this PSBT with another one.</p>
|
<p class="mt-4 mb-2">For batching transactions, you can combine this PSBT with another one.</p>
|
||||||
<button type="submit" name="command" value="combine" class="btn btn-secondary">Combine PSBT</button>
|
<button id="combine-psbt" type="submit" name="command" value="combine" class="btn btn-secondary">Combine PSBT</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user