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.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models;
|
||||
@@ -8,12 +9,13 @@ using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
[Collection(nameof(NonParallelizableCollectionDefinition))]
|
||||
public class PSBTTests : UnitTestBase
|
||||
{
|
||||
public PSBTTests(ITestOutputHelper helper) : base(helper)
|
||||
@@ -23,125 +25,110 @@ namespace BTCPayServer.Tests
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanPlayWithPSBT()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.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);
|
||||
});
|
||||
using var s = CreateSeleniumTester(newDb: true);
|
||||
await s.StartAsync();
|
||||
|
||||
var walletController = user.GetController<UIWalletsController>();
|
||||
var walletId = new WalletId(user.StoreId, "BTC");
|
||||
var sendDestination = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
|
||||
var sendModel = new WalletSendModel()
|
||||
{
|
||||
Outputs = new List<WalletSendModel.TransactionOutput>()
|
||||
{
|
||||
new WalletSendModel.TransactionOutput()
|
||||
{
|
||||
DestinationAddress = sendDestination,
|
||||
Amount = 0.1m,
|
||||
}
|
||||
},
|
||||
FeeSatoshiPerByte = 1,
|
||||
CurrentBalance = 1.5m
|
||||
};
|
||||
var u1 = s.RegisterNewUser(true);
|
||||
var hot = s.CreateNewStore();
|
||||
var seed = s.GenerateWallet(isHotWallet: true);
|
||||
var cold = s.CreateNewStore();
|
||||
s.GenerateWallet(isHotWallet: false, seed: seed.ToString());
|
||||
|
||||
string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
|
||||
var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmPSBT.Decoded);
|
||||
// Scenario 1: one user has two stores sharing same seed
|
||||
// one store is hot wallet, the other not.
|
||||
|
||||
var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
|
||||
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
|
||||
// Here, the cold wallet create a PSBT, then we switch to hot wallet to sign
|
||||
// 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
|
||||
{
|
||||
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 psbt = ExtractPSBT(s);
|
||||
|
||||
var signedPSBT = unsignedPSBT.Clone();
|
||||
signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
|
||||
vmPSBT.PSBT = signedPSBT.ToBase64();
|
||||
var psbtReady = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
|
||||
{
|
||||
SigningContext = new SigningContextModel
|
||||
{
|
||||
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
|
||||
}
|
||||
}).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);
|
||||
s.GoToStore(hot.storeId);
|
||||
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
|
||||
s.Driver.FindElement(By.Name("PSBT")).SendKeys(psbt);
|
||||
s.Driver.FindElement(By.Id("Decode")).Click();
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
|
||||
s.Driver.TakeScreenshot().SaveAsFile(@"C:\Users\NicolasDorier\AppData\Local\Temp\1721425323\0-.png");
|
||||
s.FindAlertMessage();
|
||||
|
||||
vmPSBT.PSBT = unsignedPSBT.ToBase64();
|
||||
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
|
||||
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
|
||||
combineVM.PSBT = signedPSBT.ToBase64();
|
||||
var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
|
||||
// Scenario 2: Same as scenario 1, except we create a PSBT from hot wallet, then sign by manually
|
||||
// entering the seed on the cold wallet.
|
||||
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.Send);
|
||||
SendAllTo(s, address);
|
||||
psbt = ExtractPSBT(s);
|
||||
|
||||
var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.True(signedPSBT.TryFinalize(out _));
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
// Let's check it has been signed, then remove the signature.
|
||||
// Also remove the hdkeys so we can test the update later
|
||||
var psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork);
|
||||
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?
|
||||
combineVM.PSBT = null;
|
||||
combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
|
||||
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
|
||||
signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.True(signedPSBT.TryFinalize(out _));
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
s.GoToStore(cold.storeId);
|
||||
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
|
||||
s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64());
|
||||
s.Driver.FindElement(By.Id("Decode")).Click();
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
s.Driver.FindElement(By.Id("SignWithSeed")).Click();
|
||||
s.Driver.FindElement(By.Name("SeedOrKey")).SendKeys(seed.ToString());
|
||||
s.Driver.FindElement(By.Id("Submit")).Click();
|
||||
s.Driver.FindElement(By.Id("BroadcastTransaction")).Click();
|
||||
s.FindAlertMessage();
|
||||
|
||||
var ready = (await walletController.WalletPSBT(walletId, new WalletPSBTViewModel
|
||||
{
|
||||
SigningContext = new SigningContextModel(signedPSBT)
|
||||
})).AssertViewModel<WalletPSBTViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT);
|
||||
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
|
||||
Assert.Equal(signedPSBT.ToBase64(), psbt);
|
||||
var redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
|
||||
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
|
||||
// Let's check if the update feature works
|
||||
s.GoToWallet(navPages: Views.Wallets.WalletsNavPages.PSBT);
|
||||
s.Driver.FindElement(By.Name("PSBT")).SendKeys(skeletonPSBT.ToBase64());
|
||||
s.Driver.FindElement(By.Id("Decode")).Click();
|
||||
s.Driver.FindElement(By.Id("PSBTOptionsAdvancedHeader")).Click();
|
||||
s.Driver.WaitForElement(By.Id("update-psbt")).Click();
|
||||
|
||||
//test base64 psbt file
|
||||
Assert.False(string.IsNullOrEmpty(Assert.IsType<WalletPSBTViewModel>(
|
||||
Assert.IsType<ViewResult>(
|
||||
await walletController.WalletPSBT(walletId,
|
||||
new WalletPSBTViewModel
|
||||
{
|
||||
UploadedPSBTFile = TestUtils.GetFormFile("base64", signedPSBT.ToBase64())
|
||||
})).Model).PSBT));
|
||||
psbt = ExtractPSBT(s);
|
||||
psbtParsed = PSBT.Parse(psbt, s.Server.NetworkProvider.BTC.NBitcoinNetwork);
|
||||
Assert.Single(psbtParsed.Inputs[0].HDKeyPaths);
|
||||
Assert.Empty(psbtParsed.Inputs[0].PartialSigs);
|
||||
|
||||
// Let's if we can combine the updated psbt (which has hdkeys, but no sig)
|
||||
// with the signed psbt (which has sig, but no hdkeys)
|
||||
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);
|
||||
var postRedirectViewModel = Assert.IsType<PostRedirectViewModel>(postRedirectView.Model);
|
||||
Assert.Equal(actionName, postRedirectViewModel.AspAction);
|
||||
var redirectedPSBT = postRedirectViewModel.FormParameters.Single(p => p.Key == "psbt" || p.Key == "SigningContext.PSBT").Value?.FirstOrDefault();
|
||||
return redirectedPSBT;
|
||||
s.Driver.FindElement(By.Name("Outputs[0].DestinationAddress")).SendKeys(address);
|
||||
s.Driver.FindElement(By.ClassName("crypto-balance-link")).Click();
|
||||
s.Driver.FindElement(By.Id("SignTransaction")).Click();
|
||||
}
|
||||
|
||||
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 Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@@ -252,6 +253,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
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();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ using BTCPayServer.Views.Stores;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Chrome;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
@@ -376,6 +377,8 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
GoToUrl($"/stores/{storeId}/");
|
||||
StoreId = storeId;
|
||||
if (WalletId != null)
|
||||
WalletId = new WalletId(storeId, WalletId.CryptoCode);
|
||||
}
|
||||
|
||||
Driver.FindElement(By.Id("StoreNav-StoreSettings")).Click();
|
||||
@@ -422,7 +425,7 @@ namespace BTCPayServer.Tests
|
||||
Driver.FindElement(By.Id($"StoreSelectorMenuItem-{storeId}")).Click();
|
||||
}
|
||||
|
||||
public void GoToInvoiceCheckout(string? invoiceId = null)
|
||||
public void GoToInvoiceCheckout(string invoiceId = null)
|
||||
{
|
||||
invoiceId ??= InvoiceId;
|
||||
Driver.FindElement(By.Id("StoreNav-Invoices")).Click();
|
||||
@@ -507,7 +510,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
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;
|
||||
GoToWallet(walletId, WalletsNavPages.Receive);
|
||||
@@ -515,9 +518,22 @@ namespace BTCPayServer.Tests
|
||||
var addressStr = Driver.FindElement(By.Id("address")).GetProperty("value");
|
||||
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
||||
for (var i = 0; i < coins; i++)
|
||||
{
|
||||
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()
|
||||
|
||||
@@ -146,17 +146,22 @@ namespace BTCPayServer.Controllers
|
||||
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 });
|
||||
|
||||
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)
|
||||
{
|
||||
case "vault":
|
||||
return ViewVault(walletId, vm.SigningContext);
|
||||
case "seed":
|
||||
return SignWithSeed(walletId, vm.SigningContext);
|
||||
case "decode":
|
||||
return await WalletPSBT(walletId, vm, "decode");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -171,7 +176,7 @@ namespace BTCPayServer.Controllers
|
||||
WellknownMetadataKeys.MasterHDKey);
|
||||
if (extKey != null)
|
||||
{
|
||||
return SignWithSeed(walletId,
|
||||
return await SignWithSeed(walletId,
|
||||
new SignWithSeedViewModel { SeedOrKey = extKey, SigningContext = vm.SigningContext });
|
||||
}
|
||||
}
|
||||
@@ -181,26 +186,16 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet("{walletId}/psbt")]
|
||||
public async Task<IActionResult> WalletPSBT([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, WalletPSBTViewModel vm)
|
||||
WalletId walletId)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
var vm = new WalletPSBTViewModel();
|
||||
vm.CryptoCode = network.CryptoCode;
|
||||
|
||||
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
|
||||
if (derivationSchemeSettings == null)
|
||||
return NotFound();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -208,10 +203,8 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> WalletPSBT(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
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);
|
||||
vm.CryptoCode = network.CryptoCode;
|
||||
|
||||
@@ -221,25 +214,23 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet;
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
if (psbt == null)
|
||||
if (vm.InvalidPSBT)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
vm.PSBT = psbt.ToBase64();
|
||||
vm.PSBTHex = psbt.ToHex();
|
||||
if (psbt is null)
|
||||
{
|
||||
return View("WalletPSBT", vm);
|
||||
}
|
||||
switch (command)
|
||||
{
|
||||
case "sign":
|
||||
return await WalletSign(walletId, vm, nameof(WalletPSBT));
|
||||
return await WalletSign(walletId, vm);
|
||||
case "decode":
|
||||
ModelState.Remove(nameof(vm.PSBT));
|
||||
ModelState.Remove(nameof(vm.FileName));
|
||||
ModelState.Remove(nameof(vm.UploadedPSBTFile));
|
||||
vm.PSBT = vm.SigningContext.PSBT;
|
||||
vm.PSBTHex = psbt.ToHex();
|
||||
vm.Decoded = psbt.ToString();
|
||||
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
||||
return View("WalletPSBTDecoded", vm);
|
||||
|
||||
@@ -273,8 +264,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
default:
|
||||
var viewName = string.IsNullOrEmpty(vm.PSBT) ? "WalletPSBT" : "WalletPSBTDecoded";
|
||||
return View(viewName, vm);
|
||||
return View("WalletPSBTDecoded", vm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,28 +383,21 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost("{walletId}/psbt/ready")]
|
||||
public async Task<IActionResult> WalletPSBTReady(
|
||||
[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);
|
||||
DerivationSchemeSettings derivationSchemeSettings;
|
||||
try
|
||||
PSBT psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
if (vm.InvalidPSBT || psbt is null)
|
||||
{
|
||||
psbt = PSBT.Parse(vm.SigningContext.PSBT, network.NBitcoinNetwork);
|
||||
derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
|
||||
if (vm.InvalidPSBT)
|
||||
vm.GlobalError = "Invalid PSBT";
|
||||
return View(nameof(WalletPSBT), vm);
|
||||
}
|
||||
DerivationSchemeSettings derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
|
||||
if (derivationSchemeSettings == null)
|
||||
return NotFound();
|
||||
|
||||
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
||||
}
|
||||
catch
|
||||
{
|
||||
vm.GlobalError = "Invalid PSBT";
|
||||
return View(nameof(WalletPSBT), vm);
|
||||
}
|
||||
|
||||
switch (command)
|
||||
{
|
||||
@@ -543,6 +526,9 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
PSBT = psbt.ToBase64()
|
||||
});
|
||||
case "decode":
|
||||
await FetchTransactionDetails(derivationSchemeSettings, vm, network);
|
||||
return View("WalletPSBTDecoded", vm);
|
||||
default:
|
||||
vm.GlobalError = "Unknown command";
|
||||
return View(nameof(WalletPSBT), vm);
|
||||
|
||||
@@ -826,7 +826,8 @@ namespace BTCPayServer.Controllers
|
||||
FormParameters =
|
||||
{
|
||||
{ "SigningKey", vm.SigningKey },
|
||||
{ "SigningKeyPath", vm.SigningKeyPath }
|
||||
{ "SigningKeyPath", vm.SigningKeyPath },
|
||||
{ "command", "decode" }
|
||||
}
|
||||
};
|
||||
AddSigningContext(redirectVm, vm.SigningContext);
|
||||
@@ -834,6 +835,7 @@ namespace BTCPayServer.Controllers
|
||||
!string.IsNullOrEmpty(vm.SigningContext.PSBT))
|
||||
{
|
||||
//if a hw device signed a payjoin, we want it broadcast instantly
|
||||
redirectVm.FormParameters.Remove("command");
|
||||
redirectVm.FormParameters.Add("command", "broadcast");
|
||||
}
|
||||
if (this.HttpContext.Request.Query["returnUrl"].FirstOrDefault() is string returnUrl)
|
||||
@@ -864,7 +866,8 @@ namespace BTCPayServer.Controllers
|
||||
FormParameters =
|
||||
{
|
||||
{ "psbt", vm.PSBT },
|
||||
{ "fileName", vm.FileName }
|
||||
{ "fileName", vm.FileName },
|
||||
{ "command", "decode" },
|
||||
}
|
||||
};
|
||||
return View("PostRedirect", redirectVm);
|
||||
@@ -876,12 +879,12 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(nameof(SignWithSeed), new SignWithSeedViewModel
|
||||
{
|
||||
SigningContext = signingContext,
|
||||
SigningContext = signingContext
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{walletId}/psbt/seed")]
|
||||
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
public async Task<IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, SignWithSeedViewModel viewModel)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
@@ -891,7 +894,6 @@ namespace BTCPayServer.Controllers
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
|
||||
ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork);
|
||||
|
||||
if (extKey == null)
|
||||
@@ -942,10 +944,21 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
var changed = psbt.PSBTChanged(() => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath));
|
||||
if (!changed)
|
||||
{
|
||||
var update = new UpdatePSBTRequest()
|
||||
{
|
||||
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));
|
||||
viewModel.SigningContext.PSBT = psbt.ToBase64();
|
||||
return RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel
|
||||
|
||||
@@ -34,7 +34,24 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
[Display(Name = "Upload PSBT from file")]
|
||||
public IFormFile UploadedPSBTFile { get; set; }
|
||||
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -54,6 +71,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
using var stream = new StreamReader(UploadedPSBTFile.OpenReadStream());
|
||||
PSBT = await stream.ReadToEndAsync();
|
||||
InvalidPSBT = true;
|
||||
}
|
||||
}
|
||||
if (SigningContext != null && !string.IsNullOrEmpty(SigningContext.PSBT))
|
||||
@@ -64,10 +82,11 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
try
|
||||
{
|
||||
InvalidPSBT = false;
|
||||
return NBitcoin.PSBT.Parse(PSBT, network);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
{ InvalidPSBT = true; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Altcoins.Zcash.Configuration;
|
||||
@@ -117,7 +118,7 @@ namespace BTCPayServer.Services.Altcoins.Zcash.Services
|
||||
public bool DaemonAvailable { 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 Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
@@ -47,5 +47,5 @@
|
||||
<label asp-for="Passphrase" class="form-label"></label>
|
||||
<input asp-for="Passphrase" class="form-control"/>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Sign</button>
|
||||
<button id="Submit" type="submit" class="btn btn-primary">Sign</button>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@model WalletPSBTCombineViewModel
|
||||
@model WalletPSBTCombineViewModel
|
||||
@{
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
Layout = "_LayoutWizard";
|
||||
@@ -15,7 +15,7 @@
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
</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"/>
|
||||
<div class="form-group">
|
||||
<label asp-for="PSBT" class="form-label"></label>
|
||||
@@ -26,5 +26,5 @@
|
||||
<label asp-for="UploadedPSBTFile" class="form-label"></label>
|
||||
<input type="file" class="form-control" asp-for="UploadedPSBTFile">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Combine</button>
|
||||
<button id="Submit" type="submit" class="btn btn-primary">Combine</button>
|
||||
</form>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@{
|
||||
var walletId = Context.GetRouteValue("walletId").ToString();
|
||||
var isReady = !Model.HasErrors;
|
||||
var isSignable = !isReady && Model.NBXSeedAvailable;
|
||||
var isSignable = !isReady;
|
||||
var needsExport = !isSignable && !isReady;
|
||||
Layout = "_LayoutWizard";
|
||||
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="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="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>
|
||||
</form>
|
||||
}
|
||||
@@ -108,7 +108,7 @@ else
|
||||
<div class="d-flex flex-column flex-sm-row flex-wrap align-items-sm-center">
|
||||
<button name="command" type="submit" class="btn btn-primary mb-3 mb-sm-0 me-sm-2" value="save-psbt">Download PSBT file</button>
|
||||
<button name="command" type="button" class="btn btn-primary mb-3 mb-sm-0 me-sm-2 only-for-js" data-bs-toggle="modal" data-bs-target="#scan-qr-modal">Show QR for wallet camera</button>
|
||||
<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>
|
||||
</form>
|
||||
<div id="ExportOptions" class="collapse">
|
||||
@@ -126,7 +126,7 @@ else
|
||||
</ul>
|
||||
<div class="tab-content" id="export-tabContent">
|
||||
<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 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>
|
||||
@@ -179,14 +179,11 @@ else
|
||||
<div id="PSBTOptionsAdvancedContent" class="accordion-collapse collapse" aria-labelledby="PSBTOptionsAdvancedHeader" data-bs-parent="#PSBTOptions">
|
||||
<div class="accordion-body">
|
||||
<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="FileName"/>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user