From 320db335d14ae31b9c86c0c3fa15255b0dbabd31 Mon Sep 17 00:00:00 2001 From: Henry Romp <151henry151@gmail.com> Date: Wed, 10 Jun 2026 00:19:43 -0400 Subject: [PATCH] qt: Defer transaction signing until user clicks Send This fixes issue #30070 where creating unsigned PSBTs from the GUI would fail because the transaction was already signed during preparation, causing legacy inputs to have non-empty scriptSig fields. The fix defers signing until the user explicitly clicks 'Send', allowing truly unsigned PSBTs to be created while still supporting fee calculation. --- src/qt/sendcoinsdialog.cpp | 25 ++++++++++++++++++++++++- src/qt/walletmodel.cpp | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 7c8c04366f7..412bd5dfc6b 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -268,6 +268,8 @@ bool SendCoinsDialog::PrepareSendText(QString& question_string, QString& informa } // prepare transaction for getting txFee earlier + // Create unsigned transaction to support creating unsigned PSBTs. + // Signing is deferred until the user clicks "Send". m_current_transaction = std::make_unique(recipients); WalletModel::SendCoinsReturn prepareStatus; @@ -344,7 +346,7 @@ bool SendCoinsDialog::PrepareSendText(QString& question_string, QString& informa // append transaction size //: When reviewing a newly created PSBT (via Send flow), the transaction fee is shown, with "virtual size" of the transaction displayed for context - question_string.append(" (" + tr("%1 kvB", "PSBT transaction creation").arg((double)m_current_transaction->getTransactionSize() / 1000, 0, 'g', 3) + "): "); + question_string.append(" (" + tr("%1 kvB (unsigned)", "PSBT transaction creation").arg((double)m_current_transaction->getTransactionSize() / 1000, 0, 'g', 3) + "): "); // append transaction fee value question_string.append(""); @@ -497,6 +499,12 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked) presentPSBT(psbtx); } else { // "Send" clicked + WalletModel::UnlockContext ctx(model->requestUnlock()); + if (!ctx.isValid()) { + fNewRecipientAllowed = true; + return; + } + assert(!model->wallet().privateKeysDisabled() || model->wallet().hasExternalSigner()); bool broadcast = true; if (model->wallet().hasExternalSigner()) { @@ -522,6 +530,21 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked) presentPSBT(psbtx); } } + } else { + // Sign the transaction now that the user has confirmed they want to send. + CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())}; + PartiallySignedTransaction psbtx(mtx); + bool complete = false; + const auto err{model->wallet().fillPSBT({.sign = true, .bip32_derivs = false}, /*n_signed=*/nullptr, psbtx, complete)}; + if (err || !complete) { + Q_EMIT message(tr("Send Coins"), tr("Failed to sign transaction."), + CClientUIInterface::MSG_ERROR); + send_failure = true; + broadcast = false; + } else { + CHECK_NONFATAL(FinalizeAndExtractPSBT(psbtx, mtx)); + m_current_transaction->setWtx(MakeTransactionRef(mtx)); + } } // Broadcast the transaction, unless an external signer was used and it diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index a9142b930d5..26cf83c8188 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -203,7 +203,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact try { auto& newTx = transaction.getWtx(); - const auto& res = m_wallet->createTransaction(vecSend, coinControl, /*sign=*/!wallet().privateKeysDisabled(), /*change_pos=*/std::nullopt); + const auto& res = m_wallet->createTransaction(vecSend, coinControl, /*sign=*/false, /*change_pos=*/std::nullopt); if (!res) { Q_EMIT message(tr("Send Coins"), QString::fromStdString(util::ErrorString(res).translated), CClientUIInterface::MSG_ERROR);