Fix the PSBT signing flow (#3465)

This commit is contained in:
Nicolas Dorier
2022-02-17 17:58:56 +09:00
committed by GitHub
parent dcdab5b218
commit 6efeb60c41
11 changed files with 199 additions and 179 deletions

View File

@@ -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];
}
}
}

View File

@@ -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;
});

View File

@@ -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()

View File

@@ -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);

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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); }
}
}
}

View File

@@ -1,4 +1,3 @@
#nullable enable
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>