private static void AssertFullySigned(HwiTester tester, PSBT psbt) { var txbuilder = tester.Network.CreateTransactionBuilder(); txbuilder.AddCoins(psbt.Inputs.Select(i => i.GetCoin())); Assert.True(txbuilder.Verify(psbt.ExtractTransaction()), "The transaction should be fully signed"); }
public async Task <IActionResult> WalletPSBTReady( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletPSBTReadyViewModel vm, string command = null) { PSBT psbt = null; var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode); try { psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork); var derivationSchemeSettings = await GetDerivationSchemeSettings(walletId); if (derivationSchemeSettings == null) { return(NotFound()); } await FetchTransactionDetails(derivationSchemeSettings, vm, network); } catch { vm.GlobalError = "Invalid PSBT"; return(View(vm)); } if (command == "broadcast") { if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors)) { vm.SetErrors(errors); return(View(vm)); } var transaction = psbt.ExtractTransaction(); try { var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction); if (!broadcastResult.Success) { vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"; return(View(vm)); } } catch (Exception ex) { vm.GlobalError = "Error while broadcasting: " + ex.Message; return(View(vm)); } return(await RedirectToWalletTransaction(walletId, transaction)); } else if (command == "analyze-psbt") { return(RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.ToBase64() })); } else { vm.GlobalError = "Unknown command"; return(View(vm)); } }
public async Task <IActionResult> WalletPSBTReady( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletPSBTReadyViewModel vm, string command = null) { PSBT psbt = null; var network = NetworkProvider.GetNetwork(walletId.CryptoCode); try { psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork); await FetchTransactionDetails(walletId, vm, network); } catch { vm.Errors = new List <string>(); vm.Errors.Add("Invalid PSBT"); return(View(vm)); } if (command == "broadcast") { if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors)) { vm.Errors = new List <string>(); vm.Errors.AddRange(errors.Select(e => e.ToString())); return(View(vm)); } var transaction = psbt.ExtractTransaction(); try { var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction); if (!broadcastResult.Success) { vm.Errors = new List <string>(); vm.Errors.Add($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"); return(View(vm)); } } catch (Exception ex) { vm.Errors = new List <string>(); vm.Errors.Add("Error while broadcasting: " + ex.Message); return(View(vm)); } return(await RedirectToWalletTransaction(walletId, transaction)); } else if (command == "analyze-psbt") { return(RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.ToBase64() })); } else { vm.Errors = new List <string>(); vm.Errors.Add("Unknown command"); return(View(vm)); } }
public async Task <IActionResult> WalletPSBTReady( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletPSBTViewModel vm, string command, CancellationToken cancellationToken = default) { var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode); PSBT psbt = await vm.GetPSBT(network.NBitcoinNetwork); if (vm.InvalidPSBT || psbt is null) { 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); switch (command) { case "payjoin": string error; try { var proposedPayjoin = await GetPayjoinProposedTX(new BitcoinUrlBuilder(vm.SigningContext.PayJoinBIP21, network.NBitcoinNetwork), psbt, derivationSchemeSettings, network, cancellationToken); try { proposedPayjoin.Settings.SigningOptions = new SigningOptions { EnforceLowR = !(vm.SigningContext?.EnforceLowR is false) }; var extKey = ExtKey.Parse(vm.SigningKey, network.NBitcoinNetwork); proposedPayjoin = proposedPayjoin.SignAll(derivationSchemeSettings.AccountDerivation, extKey, RootedKeyPath.Parse(vm.SigningKeyPath)); vm.SigningContext.PSBT = proposedPayjoin.ToBase64(); vm.SigningContext.OriginalPSBT = psbt.ToBase64(); proposedPayjoin.Finalize(); var hash = proposedPayjoin.ExtractTransaction().GetHash(); _EventAggregator.Publish(new UpdateTransactionLabel(walletId, hash, UpdateTransactionLabel.PayjoinLabelTemplate())); TempData.SetStatusMessageModel(new StatusMessageModel { Severity = StatusMessageModel.StatusSeverity.Success, AllowDismiss = false, Html = $"The payjoin transaction has been successfully broadcasted ({proposedPayjoin.ExtractTransaction().GetHash()})" }); return(await WalletPSBTReady(walletId, vm, "broadcast")); } catch (Exception) { TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Warning, AllowDismiss = false, Html = "This transaction has been coordinated between the receiver and you to create a <a href='https://en.bitcoin.it/wiki/PayJoin' target='_blank'>payjoin transaction</a> by adding inputs from the receiver.<br/>" + "The amount being sent may appear higher but is in fact almost same.<br/><br/>" + "If you cancel or refuse to sign this transaction, the payment will proceed without payjoin" }); vm.SigningContext.PSBT = proposedPayjoin.ToBase64(); vm.SigningContext.OriginalPSBT = psbt.ToBase64(); return(ViewVault(walletId, vm.SigningContext)); } } catch (PayjoinReceiverException ex) { error = $"The payjoin receiver could not complete the payjoin: {ex.Message}"; } catch (PayjoinSenderException ex) { error = $"We rejected the receiver's payjoin proposal: {ex.Message}"; } catch (Exception ex) { error = $"Unexpected payjoin error: {ex.Message}"; } //we possibly exposed the tx to the receiver, so we need to broadcast straight away psbt.Finalize(); TempData.SetStatusMessageModel(new StatusMessageModel { Severity = StatusMessageModel.StatusSeverity.Warning, AllowDismiss = false, Html = $"The payjoin transaction could not be created.<br/>" + $"The original transaction was broadcasted instead. ({psbt.ExtractTransaction().GetHash()})<br/><br/>" + $"{error}" }); return(await WalletPSBTReady(walletId, vm, "broadcast")); case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors): vm.SetErrors(errors); return(View(nameof(WalletPSBT), vm)); case "broadcast": { var transaction = psbt.ExtractTransaction(); try { var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction); if (!broadcastResult.Success) { if (!string.IsNullOrEmpty(vm.SigningContext.OriginalPSBT)) { TempData.SetStatusMessageModel(new StatusMessageModel { Severity = StatusMessageModel.StatusSeverity.Warning, AllowDismiss = false, Html = $"The payjoin transaction could not be broadcasted.<br/>({broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}).<br/>The transaction has been reverted back to its original format and has been broadcast." }); vm.SigningContext.PSBT = vm.SigningContext.OriginalPSBT; vm.SigningContext.OriginalPSBT = null; return(await WalletPSBTReady(walletId, vm, "broadcast")); } vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"; return(View(nameof(WalletPSBT), vm)); } else { var wallet = _walletProvider.GetWallet(network); var derivationSettings = GetDerivationSchemeSettings(walletId); wallet.InvalidateCache(derivationSettings.AccountDerivation); } } catch (Exception ex) { vm.GlobalError = "Error while broadcasting: " + ex.Message; return(View(nameof(WalletPSBT), vm)); } if (!TempData.HasStatusMessage()) { TempData[WellKnownTempData.SuccessMessage] = $"Transaction broadcasted successfully ({transaction.GetHash()})"; } var returnUrl = this.HttpContext.Request.Query["returnUrl"].FirstOrDefault(); if (returnUrl is not null) { return(Redirect(returnUrl)); } return(RedirectToAction(nameof(WalletTransactions), new { walletId = walletId.ToString() })); } case "analyze-psbt": return(RedirectToWalletPSBT(new WalletPSBTViewModel() { PSBT = psbt.ToBase64() })); case "decode": await FetchTransactionDetails(derivationSchemeSettings, vm, network); return(View("WalletPSBTDecoded", vm)); default: vm.GlobalError = "Unknown command"; return(View(nameof(WalletPSBT), vm)); } }
public async Task <IActionResult> WalletPSBTReady( [ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId, WalletPSBTReadyViewModel vm, string command = null, CancellationToken cancellationToken = default) { if (command == null) { return(await WalletPSBTReady(walletId, vm.PSBT, vm.SigningKey, vm.SigningKeyPath, vm.OriginalPSBT, vm.PayJoinEndpointUrl)); } PSBT psbt = null; var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode); DerivationSchemeSettings derivationSchemeSettings = null; try { psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork); derivationSchemeSettings = GetDerivationSchemeSettings(walletId); if (derivationSchemeSettings == null) { return(NotFound()); } await FetchTransactionDetails(derivationSchemeSettings, vm, network); } catch { vm.GlobalError = "Invalid PSBT"; return(View(nameof(WalletPSBTReady), vm)); } switch (command) { case "payjoin": string error = null; try { var proposedPayjoin = await GetPayjoinProposedTX(vm.PayJoinEndpointUrl, psbt, derivationSchemeSettings, network, cancellationToken); try { var extKey = ExtKey.Parse(vm.SigningKey, network.NBitcoinNetwork); proposedPayjoin = proposedPayjoin.SignAll(derivationSchemeSettings.AccountDerivation, extKey, RootedKeyPath.Parse(vm.SigningKeyPath)); vm.PSBT = proposedPayjoin.ToBase64(); vm.OriginalPSBT = psbt.ToBase64(); proposedPayjoin.Finalize(); TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Success, AllowDismiss = false, Html = $"The payjoin transaction has been successfully broadcasted ({proposedPayjoin.ExtractTransaction().GetHash()})" }); return(await WalletPSBTReady(walletId, vm, "broadcast")); } catch (Exception) { TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Warning, AllowDismiss = false, Html = $"This transaction has been coordinated between the receiver and you to create a <a href='https://en.bitcoin.it/wiki/PayJoin' target='_blank'>payjoin transaction</a> by adding inputs from the receiver.<br/>" + $"The amount being sent may appear higher but is in fact almost same.<br/><br/>" + $"If you cancel refuse to sign this transaction, the payment will proceed without payjoin" }); return(ViewVault(walletId, proposedPayjoin, vm.PayJoinEndpointUrl, psbt)); } } catch (PayjoinReceiverException ex) { error = $"The payjoin receiver could not complete the payjoin: {ex.Message}"; } catch (PayjoinSenderException ex) { error = $"We rejected the receiver's payjoin proposal: {ex.Message}"; } catch (Exception ex) { error = $"Unexpected payjoin error: {ex.Message}"; } //we possibly exposed the tx to the receiver, so we need to broadcast straight away psbt.Finalize(); TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Warning, AllowDismiss = false, Html = $"The payjoin transaction could not be created.<br/>" + $"The original transaction was broadcasted instead. ({psbt.ExtractTransaction().GetHash()})<br/><br/>" + $"{error}" }); return(await WalletPSBTReady(walletId, vm, "broadcast")); case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors): vm.SetErrors(errors); return(View(nameof(WalletPSBTReady), vm)); case "broadcast": { var transaction = psbt.ExtractTransaction(); try { var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction); if (!broadcastResult.Success) { if (!string.IsNullOrEmpty(vm.OriginalPSBT)) { TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Warning, AllowDismiss = false, Html = $"The payjoin transaction could not be broadcasted.<br/>({broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}).<br/>The transaction has been reverted back to its original format and has been broadcast." }); vm.PSBT = vm.OriginalPSBT; vm.OriginalPSBT = null; return(await WalletPSBTReady(walletId, vm, "broadcast")); } vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}"; return(View(nameof(WalletPSBTReady), vm)); } } catch (Exception ex) { vm.GlobalError = "Error while broadcasting: " + ex.Message; return(View(nameof(WalletPSBTReady), vm)); } if (!TempData.HasStatusMessage()) { TempData[WellKnownTempData.SuccessMessage] = $"Transaction broadcasted successfully ({transaction.GetHash()})"; } return(RedirectToWalletTransaction(walletId, transaction)); } case "analyze-psbt": return(RedirectToWalletPSBT(psbt)); default: vm.GlobalError = "Unknown command"; return(View(nameof(WalletPSBTReady), vm)); } }