Exemple #1
0
        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));
            }
        }
Exemple #2
0
        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));
            }
        }
Exemple #3
0
        public async Task <PSBT> RequestPayjoin(PSBT originalTx, IHDKey accountKey, RootedKeyPath rootedKeyPath, HdPubKey changeHdPubKey, CancellationToken cancellationToken)
        {
            if (originalTx.IsAllFinalized())
            {
                throw new InvalidOperationException("The original PSBT should not be finalized.");
            }

            var optionalParameters = new PayjoinClientParameters();

            if (changeHdPubKey is { })
        public static ScriptPubKeyType?GetInputsScriptPubKeyType(this PSBT psbt)
        {
            if (!psbt.IsAllFinalized() || psbt.Inputs.Any(i => i.WitnessUtxo == null))
            {
                throw new InvalidOperationException("The psbt should be finalized with witness information");
            }
            var coinsPerTypes = psbt.Inputs.Select(i =>
            {
                return((PSBTCoin)i, i.GetInputScriptPubKeyType());
            }).GroupBy(o => o.Item2, o => o.Item1).ToArray();

            if (coinsPerTypes.Length != 1)
            {
                return(default);
Exemple #5
0
        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);
        }
Exemple #6
0
        public async Task <PSBT> RequestPayjoin(PSBT originalTx, IHDKey accountKey, RootedKeyPath rootedKeyPath, CancellationToken cancellationToken)
        {
            Guard.NotNull(nameof(originalTx), originalTx);
            if (originalTx.IsAllFinalized())
            {
                throw new InvalidOperationException("The original PSBT should not be finalized.");
            }

            var sentBefore  = -originalTx.GetBalance(ScriptPubKeyType.Segwit, accountKey, rootedKeyPath);
            var oldGlobalTx = originalTx.GetGlobalTransaction();

            if (!originalTx.TryGetEstimatedFeeRate(out var originalFeeRate) || !originalTx.TryGetVirtualSize(out var oldVirtualSize))
            {
                throw new ArgumentException("originalTx should have utxo information", nameof(originalTx));
            }

            var originalFee = originalTx.GetFee();
            var cloned      = originalTx.Clone();

            if (!cloned.TryFinalize(out var _))
            {
                return(null);
            }

            // We make sure we don't send unnecessary information to the receiver
            foreach (var finalized in cloned.Inputs.Where(i => i.IsFinalized()))
            {
                finalized.ClearForFinalize();
            }

            foreach (var output in cloned.Outputs)
            {
                output.HDKeyPaths.Clear();
            }

            cloned.GlobalXPubs.Clear();

            var request = new HttpRequestMessage(HttpMethod.Post, PaymentUrl)
            {
                Content = new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain")
            };

            HttpResponseMessage bpuResponse = await TorHttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);

            if (!bpuResponse.IsSuccessStatusCode)
            {
                var errorStr = await bpuResponse.Content.ReadAsStringAsync().ConfigureAwait(false);

                try
                {
                    var error = JObject.Parse(errorStr);
                    throw new PayjoinReceiverException((int)bpuResponse.StatusCode,
                                                       error["errorCode"].Value <string>(),
                                                       error["message"].Value <string>());
                }
                catch (JsonReaderException)
                {
                    // will throw
                    bpuResponse.EnsureSuccessStatusCode();
                    throw;
                }
            }

            var hexOrBase64 = await bpuResponse.Content.ReadAsStringAsync().ConfigureAwait(false);

            var newPSBT = PSBT.Parse(hexOrBase64, originalTx.Network);

            // Checking that the PSBT of the receiver is clean
            if (newPSBT.GlobalXPubs.Any())
            {
                throw new PayjoinSenderException("GlobalXPubs should not be included in the receiver's PSBT");
            }

            if (newPSBT.Outputs.Any(o => o.HDKeyPaths.Count != 0) || newPSBT.Inputs.Any(o => o.HDKeyPaths.Count != 0))
            {
                throw new PayjoinSenderException("Keypath information should not be included in the receiver's PSBT");
            }

            if (newPSBT.CheckSanity() is IList <PSBTError> errors2 && errors2.Count != 0)
            {
                throw new PayjoinSenderException($"The PSBT of the receiver is insane ({errors2[0]})");
            }

            // Do not trust on inputs order because the payjoin server should shuffle them.
            foreach (var input in originalTx.Inputs)
            {
                var newInput = newPSBT.Inputs.FindIndexedInput(input.PrevOut);
                if (newInput is { })
Exemple #7
0
        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);
            });
        }
Exemple #9
0
        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));
            }
        }