private async Task <PSBT> SignTxImplAsync(HardwareWalletModels?deviceType, string devicePath, HDFingerprint?fingerprint, PSBT psbt, CancellationToken cancel) { var psbtString = psbt.ToBase64(); var response = await SendCommandAsync( options : BuildOptions(deviceType, devicePath, fingerprint), command : HwiCommands.SignTx, commandArguments : psbtString, openConsole : false, cancel).ConfigureAwait(false); PSBT signedPsbt = HwiParser.ParsePsbt(response, Network); if (!signedPsbt.IsAllFinalized()) { signedPsbt.Finalize(); } return(signedPsbt); }
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 TransactionBroadcasterViewModel() : base("Transaction Broadcaster") { Global = Locator.Current.GetService <Global>(); ButtonText = "Broadcast Transaction"; PasteCommand = ReactiveCommand.CreateFromTask(async() => { if (!string.IsNullOrEmpty(TransactionString)) { return; } var textToPaste = await Application.Current.Clipboard.GetTextAsync(); TransactionString = textToPaste; }); IObservable <bool> broadcastTransactionCanExecute = this .WhenAny(x => x.TransactionString, (transactionString) => !string.IsNullOrWhiteSpace(transactionString.Value)) .ObserveOn(RxApp.MainThreadScheduler); BroadcastTransactionCommand = ReactiveCommand.CreateFromTask( async() => await OnDoTransactionBroadcastAsync(), broadcastTransactionCanExecute); ImportTransactionCommand = ReactiveCommand.CreateFromTask( async() => { try { var ofd = new OpenFileDialog { AllowMultiple = false, Title = "Import Transaction" }; if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { var initialDirectory = Path.Combine("/media", Environment.UserName); if (!Directory.Exists(initialDirectory)) { initialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal); } ofd.Directory = initialDirectory; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { ofd.Directory = Environment.GetFolderPath(Environment.SpecialFolder.Personal); } var window = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).MainWindow; var selected = await ofd.ShowAsync(window, fallBack: true); if (selected != null && selected.Any()) { var path = selected.First(); var psbtBytes = await File.ReadAllBytesAsync(path); PSBT psbt = null; Transaction transaction = null; try { psbt = PSBT.Load(psbtBytes, Global.Network); } catch { var text = await File.ReadAllTextAsync(path); text = text.Trim(); try { psbt = PSBT.Parse(text, Global.Network); } catch { transaction = Transaction.Parse(text, Global.Network); } } if (psbt != null) { if (!psbt.IsAllFinalized()) { psbt.Finalize(); } TransactionString = psbt.ToBase64(); } else { TransactionString = transaction.ToHex(); } } } catch (Exception ex) { Logger.LogError(ex); NotificationHelpers.Error(ex.ToUserFriendlyString()); } }, outputScheduler: RxApp.MainThreadScheduler); Observable .Merge(PasteCommand.ThrownExceptions) .Merge(BroadcastTransactionCommand.ThrownExceptions) .Merge(ImportTransactionCommand.ThrownExceptions) .ObserveOn(RxApp.TaskpoolScheduler) .Subscribe(ex => { NotificationHelpers.Error(ex.ToUserFriendlyString()); Logger.LogError(ex); }); }
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)); } }