Merge bitcoin-core/gui#555: Restore Send button when using external signer

2efdfb88aa gui: restore Send for external signer (Sjors Provoost)
4b5a6cd149 refactor: helper function signWithExternalSigner() (Sjors Provoost)
026b5b4523 move-only: helper function to present PSBT (Sjors Provoost)

Pull request description:

  Fixes #551

  For the simplest use case of a wallet with one external signer and "PSBT Controls" disabled in settings (the default), the send dialog will behave the same as when using a wallet with private keys. I.e. there's no "Create Unsigned" button.

  When PSBT controls are turned on, you can now actually make a PSBT with signing it; before this PR that button would trigger a sign event and would broadcast the transaction.

  In case a multisig, the Send button will sign on the device, and then fall back to presenting a PSBT (same behavior as before #441).

  This PR starts with two refactoring commits to move some stuff into a helper function for improved readability in general, and to make the main commit easier to review.

ACKs for top commit:
  jonatack:
    utACK 2efdfb88aa diff review since my last review, code re-review, rebased to current master, verified clean debug build of each commit
  luke-jr:
    utACK 2efdfb88aa

Tree-SHA512: e8731a0ef9e87564b2676c7b022b742d9621bba964c19dba9fd9f6961eb608737a9e1a22c0a3c8b2f2f6d583bba067606ee8392422e82082deefb20ea7b88c7c
24.x
Hennadii Stepanov 3 years ago
commit aece566249
No known key found for this signature in database
GPG Key ID: 410108112E7EA81F

@ -401,86 +401,8 @@ bool SendCoinsDialog::PrepareSendText(QString& question_string, QString& informa
return true; return true;
} }
void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked) void SendCoinsDialog::presentPSBT(PartiallySignedTransaction& psbtx)
{ {
if(!model || !model->getOptionsModel())
return;
QString question_string, informative_text, detailed_text;
if (!PrepareSendText(question_string, informative_text, detailed_text)) return;
assert(m_current_transaction);
const QString confirmation = tr("Confirm send coins");
auto confirmationDialog = new SendConfirmationDialog(confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, !model->wallet().privateKeysDisabled(), model->getOptionsModel()->getEnablePSBTControls(), this);
confirmationDialog->setAttribute(Qt::WA_DeleteOnClose);
// TODO: Replace QDialog::exec() with safer QDialog::show().
const auto retval = static_cast<QMessageBox::StandardButton>(confirmationDialog->exec());
if(retval != QMessageBox::Yes && retval != QMessageBox::Save)
{
fNewRecipientAllowed = true;
return;
}
bool send_failure = false;
if (retval == QMessageBox::Save) {
CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
PartiallySignedTransaction psbtx(mtx);
bool complete = false;
// Always fill without signing first. This prevents an external signer
// from being called prematurely and is not expensive.
TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, nullptr, psbtx, complete);
assert(!complete);
assert(err == TransactionError::OK);
if (model->wallet().hasExternalSigner()) {
try {
err = model->wallet().fillPSBT(SIGHASH_ALL, true /* sign */, true /* bip32derivs */, nullptr, psbtx, complete);
} catch (const std::runtime_error& e) {
QMessageBox::critical(nullptr, tr("Sign failed"), e.what());
send_failure = true;
return;
}
if (err == TransactionError::EXTERNAL_SIGNER_NOT_FOUND) {
//: "External signer" means using devices such as hardware wallets.
QMessageBox::critical(nullptr, tr("External signer not found"), "External signer not found");
send_failure = true;
return;
}
if (err == TransactionError::EXTERNAL_SIGNER_FAILED) {
//: "External signer" means using devices such as hardware wallets.
QMessageBox::critical(nullptr, tr("External signer failure"), "External signer failure");
send_failure = true;
return;
}
if (err != TransactionError::OK) {
tfm::format(std::cerr, "Failed to sign PSBT");
processSendCoinsReturn(WalletModel::TransactionCreationFailed);
send_failure = true;
return;
}
// fillPSBT does not always properly finalize
complete = FinalizeAndExtractPSBT(psbtx, mtx);
}
// Broadcast transaction if complete (even with an external signer this
// is not always the case, e.g. in a multisig wallet).
if (complete) {
const CTransactionRef tx = MakeTransactionRef(mtx);
m_current_transaction->setWtx(tx);
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(*m_current_transaction);
// process sendStatus and on error generate message shown to user
processSendCoinsReturn(sendStatus);
if (sendStatus.status == WalletModel::OK) {
Q_EMIT coinsSent(m_current_transaction->getWtx()->GetHash());
} else {
send_failure = true;
}
return;
}
// Copy PSBT to clipboard and offer to save
assert(!complete);
// Serialize the PSBT // Serialize the PSBT
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx; ssTx << psbtx;
@ -523,8 +445,104 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
default: default:
assert(false); assert(false);
} // msgBox.exec() } // msgBox.exec()
}
bool SendCoinsDialog::signWithExternalSigner(PartiallySignedTransaction& psbtx, CMutableTransaction& mtx, bool& complete) {
TransactionError err;
try {
err = model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/true, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete);
} catch (const std::runtime_error& e) {
QMessageBox::critical(nullptr, tr("Sign failed"), e.what());
return false;
}
if (err == TransactionError::EXTERNAL_SIGNER_NOT_FOUND) {
//: "External signer" means using devices such as hardware wallets.
QMessageBox::critical(nullptr, tr("External signer not found"), "External signer not found");
return false;
}
if (err == TransactionError::EXTERNAL_SIGNER_FAILED) {
//: "External signer" means using devices such as hardware wallets.
QMessageBox::critical(nullptr, tr("External signer failure"), "External signer failure");
return false;
}
if (err != TransactionError::OK) {
tfm::format(std::cerr, "Failed to sign PSBT");
processSendCoinsReturn(WalletModel::TransactionCreationFailed);
return false;
}
// fillPSBT does not always properly finalize
complete = FinalizeAndExtractPSBT(psbtx, mtx);
return true;
}
void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
{
if(!model || !model->getOptionsModel())
return;
QString question_string, informative_text, detailed_text;
if (!PrepareSendText(question_string, informative_text, detailed_text)) return;
assert(m_current_transaction);
const QString confirmation = tr("Confirm send coins");
const bool enable_send{!model->wallet().privateKeysDisabled() || model->wallet().hasExternalSigner()};
const bool always_show_unsigned{model->getOptionsModel()->getEnablePSBTControls()};
auto confirmationDialog = new SendConfirmationDialog(confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, enable_send, always_show_unsigned, this);
confirmationDialog->setAttribute(Qt::WA_DeleteOnClose);
// TODO: Replace QDialog::exec() with safer QDialog::show().
const auto retval = static_cast<QMessageBox::StandardButton>(confirmationDialog->exec());
if(retval != QMessageBox::Yes && retval != QMessageBox::Save)
{
fNewRecipientAllowed = true;
return;
}
bool send_failure = false;
if (retval == QMessageBox::Save) {
// "Create Unsigned" clicked
CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
PartiallySignedTransaction psbtx(mtx);
bool complete = false;
// Fill without signing
TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete);
assert(!complete);
assert(err == TransactionError::OK);
// Copy PSBT to clipboard and offer to save
presentPSBT(psbtx);
} else { } else {
assert(!model->wallet().privateKeysDisabled()); // "Send" clicked
assert(!model->wallet().privateKeysDisabled() || model->wallet().hasExternalSigner());
bool broadcast = true;
if (model->wallet().hasExternalSigner()) {
CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
PartiallySignedTransaction psbtx(mtx);
bool complete = false;
// Always fill without signing first. This prevents an external signer
// from being called prematurely and is not expensive.
TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete);
assert(!complete);
assert(err == TransactionError::OK);
send_failure = !signWithExternalSigner(psbtx, mtx, complete);
// Don't broadcast when user rejects it on the device or there's a failure:
broadcast = complete && !send_failure;
if (!send_failure) {
// A transaction signed with an external signer is not always complete,
// e.g. in a multisig wallet.
if (complete) {
// Prepare transaction for broadcast transaction if complete
const CTransactionRef tx = MakeTransactionRef(mtx);
m_current_transaction->setWtx(tx);
} else {
presentPSBT(psbtx);
}
}
}
// Broadcast the transaction, unless an external signer was used and it
// failed, or more signatures are needed.
if (broadcast) {
// now send the prepared transaction // now send the prepared transaction
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(*m_current_transaction); WalletModel::SendCoinsReturn sendStatus = model->sendCoins(*m_current_transaction);
// process sendStatus and on error generate message shown to user // process sendStatus and on error generate message shown to user
@ -536,6 +554,7 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
send_failure = true; send_failure = true;
} }
} }
}
if (!send_failure) { if (!send_failure) {
accept(); accept();
m_coin_control->UnSelectAll(); m_coin_control->UnSelectAll();

@ -70,6 +70,8 @@ private:
bool fFeeMinimized; bool fFeeMinimized;
const PlatformStyle *platformStyle; const PlatformStyle *platformStyle;
// Copy PSBT to clipboard and offer to save it.
void presentPSBT(PartiallySignedTransaction& psbt);
// Process WalletModel::SendCoinsReturn and generate a pair consisting // Process WalletModel::SendCoinsReturn and generate a pair consisting
// of a message and message flags for use in Q_EMIT message(). // of a message and message flags for use in Q_EMIT message().
// Additional parameter msgArg can be used via .arg(msgArg). // Additional parameter msgArg can be used via .arg(msgArg).
@ -77,6 +79,15 @@ private:
void minimizeFeeSection(bool fMinimize); void minimizeFeeSection(bool fMinimize);
// Format confirmation message // Format confirmation message
bool PrepareSendText(QString& question_string, QString& informative_text, QString& detailed_text); bool PrepareSendText(QString& question_string, QString& informative_text, QString& detailed_text);
/* Sign PSBT using external signer.
*
* @param[in,out] psbtx the PSBT to sign
* @param[in,out] mtx needed to attempt to finalize
* @param[in,out] complete whether the PSBT is complete (a successfully signed multisig transaction may not be complete)
*
* @returns false if any failure occurred, which may include the user rejection of a transaction on the device.
*/
bool signWithExternalSigner(PartiallySignedTransaction& psbt, CMutableTransaction& mtx, bool& complete);
void updateFeeMinimizedLabel(); void updateFeeMinimizedLabel();
void updateCoinControlState(); void updateCoinControlState();
@ -117,6 +128,8 @@ class SendConfirmationDialog : public QMessageBox
public: public:
SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text = "", const QString& detailed_text = "", int secDelay = SEND_CONFIRM_DELAY, bool enable_send = true, bool always_show_unsigned = true, QWidget* parent = nullptr); SendConfirmationDialog(const QString& title, const QString& text, const QString& informative_text = "", const QString& detailed_text = "", int secDelay = SEND_CONFIRM_DELAY, bool enable_send = true, bool always_show_unsigned = true, QWidget* parent = nullptr);
/* Returns QMessageBox::Cancel, QMessageBox::Yes when "Send" is
clicked and QMessageBox::Save when "Create Unsigned" is clicked. */
int exec() override; int exec() override;
private Q_SLOTS: private Q_SLOTS:

Loading…
Cancel
Save