Skip to content

Commit aece566

Browse files
committed
Merge #555: Restore Send button when using external signer
2efdfb8 gui: restore Send for external signer (Sjors Provoost) 4b5a6cd refactor: helper function signWithExternalSigner() (Sjors Provoost) 026b5b4 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 2efdfb8 diff review since my last review, code re-review, rebased to current master, verified clean debug build of each commit luke-jr: utACK 2efdfb8 Tree-SHA512: e8731a0ef9e87564b2676c7b022b742d9621bba964c19dba9fd9f6961eb608737a9e1a22c0a3c8b2f2f6d583bba067606ee8392422e82082deefb20ea7b88c7c
2 parents 74f8c55 + 2efdfb8 commit aece566

File tree

2 files changed

+125
-93
lines changed

2 files changed

+125
-93
lines changed

src/qt/sendcoinsdialog.cpp

Lines changed: 112 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,80 @@ bool SendCoinsDialog::PrepareSendText(QString& question_string, QString& informa
401401
return true;
402402
}
403403

404+
void SendCoinsDialog::presentPSBT(PartiallySignedTransaction& psbtx)
405+
{
406+
// Serialize the PSBT
407+
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
408+
ssTx << psbtx;
409+
GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
410+
QMessageBox msgBox;
411+
msgBox.setText("Unsigned Transaction");
412+
msgBox.setInformativeText("The PSBT has been copied to the clipboard. You can also save it.");
413+
msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard);
414+
msgBox.setDefaultButton(QMessageBox::Discard);
415+
switch (msgBox.exec()) {
416+
case QMessageBox::Save: {
417+
QString selectedFilter;
418+
QString fileNameSuggestion = "";
419+
bool first = true;
420+
for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients()) {
421+
if (!first) {
422+
fileNameSuggestion.append(" - ");
423+
}
424+
QString labelOrAddress = rcp.label.isEmpty() ? rcp.address : rcp.label;
425+
QString amount = BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount);
426+
fileNameSuggestion.append(labelOrAddress + "-" + amount);
427+
first = false;
428+
}
429+
fileNameSuggestion.append(".psbt");
430+
QString filename = GUIUtil::getSaveFileName(this,
431+
tr("Save Transaction Data"), fileNameSuggestion,
432+
//: Expanded name of the binary PSBT file format. See: BIP 174.
433+
tr("Partially Signed Transaction (Binary)") + QLatin1String(" (*.psbt)"), &selectedFilter);
434+
if (filename.isEmpty()) {
435+
return;
436+
}
437+
std::ofstream out{filename.toLocal8Bit().data(), std::ofstream::out | std::ofstream::binary};
438+
out << ssTx.str();
439+
out.close();
440+
Q_EMIT message(tr("PSBT saved"), "PSBT saved to disk", CClientUIInterface::MSG_INFORMATION);
441+
break;
442+
}
443+
case QMessageBox::Discard:
444+
break;
445+
default:
446+
assert(false);
447+
} // msgBox.exec()
448+
}
449+
450+
bool SendCoinsDialog::signWithExternalSigner(PartiallySignedTransaction& psbtx, CMutableTransaction& mtx, bool& complete) {
451+
TransactionError err;
452+
try {
453+
err = model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/true, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete);
454+
} catch (const std::runtime_error& e) {
455+
QMessageBox::critical(nullptr, tr("Sign failed"), e.what());
456+
return false;
457+
}
458+
if (err == TransactionError::EXTERNAL_SIGNER_NOT_FOUND) {
459+
//: "External signer" means using devices such as hardware wallets.
460+
QMessageBox::critical(nullptr, tr("External signer not found"), "External signer not found");
461+
return false;
462+
}
463+
if (err == TransactionError::EXTERNAL_SIGNER_FAILED) {
464+
//: "External signer" means using devices such as hardware wallets.
465+
QMessageBox::critical(nullptr, tr("External signer failure"), "External signer failure");
466+
return false;
467+
}
468+
if (err != TransactionError::OK) {
469+
tfm::format(std::cerr, "Failed to sign PSBT");
470+
processSendCoinsReturn(WalletModel::TransactionCreationFailed);
471+
return false;
472+
}
473+
// fillPSBT does not always properly finalize
474+
complete = FinalizeAndExtractPSBT(psbtx, mtx);
475+
return true;
476+
}
477+
404478
void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
405479
{
406480
if(!model || !model->getOptionsModel())
@@ -411,7 +485,9 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
411485
assert(m_current_transaction);
412486

413487
const QString confirmation = tr("Confirm send coins");
414-
auto confirmationDialog = new SendConfirmationDialog(confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, !model->wallet().privateKeysDisabled(), model->getOptionsModel()->getEnablePSBTControls(), this);
488+
const bool enable_send{!model->wallet().privateKeysDisabled() || model->wallet().hasExternalSigner()};
489+
const bool always_show_unsigned{model->getOptionsModel()->getEnablePSBTControls()};
490+
auto confirmationDialog = new SendConfirmationDialog(confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, enable_send, always_show_unsigned, this);
415491
confirmationDialog->setAttribute(Qt::WA_DeleteOnClose);
416492
// TODO: Replace QDialog::exec() with safer QDialog::show().
417493
const auto retval = static_cast<QMessageBox::StandardButton>(confirmationDialog->exec());
@@ -424,49 +500,50 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
424500

425501
bool send_failure = false;
426502
if (retval == QMessageBox::Save) {
503+
// "Create Unsigned" clicked
427504
CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
428505
PartiallySignedTransaction psbtx(mtx);
429506
bool complete = false;
430-
// Always fill without signing first. This prevents an external signer
431-
// from being called prematurely and is not expensive.
432-
TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, nullptr, psbtx, complete);
507+
// Fill without signing
508+
TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete);
433509
assert(!complete);
434510
assert(err == TransactionError::OK);
511+
512+
// Copy PSBT to clipboard and offer to save
513+
presentPSBT(psbtx);
514+
} else {
515+
// "Send" clicked
516+
assert(!model->wallet().privateKeysDisabled() || model->wallet().hasExternalSigner());
517+
bool broadcast = true;
435518
if (model->wallet().hasExternalSigner()) {
436-
try {
437-
err = model->wallet().fillPSBT(SIGHASH_ALL, true /* sign */, true /* bip32derivs */, nullptr, psbtx, complete);
438-
} catch (const std::runtime_error& e) {
439-
QMessageBox::critical(nullptr, tr("Sign failed"), e.what());
440-
send_failure = true;
441-
return;
442-
}
443-
if (err == TransactionError::EXTERNAL_SIGNER_NOT_FOUND) {
444-
//: "External signer" means using devices such as hardware wallets.
445-
QMessageBox::critical(nullptr, tr("External signer not found"), "External signer not found");
446-
send_failure = true;
447-
return;
448-
}
449-
if (err == TransactionError::EXTERNAL_SIGNER_FAILED) {
450-
//: "External signer" means using devices such as hardware wallets.
451-
QMessageBox::critical(nullptr, tr("External signer failure"), "External signer failure");
452-
send_failure = true;
453-
return;
454-
}
455-
if (err != TransactionError::OK) {
456-
tfm::format(std::cerr, "Failed to sign PSBT");
457-
processSendCoinsReturn(WalletModel::TransactionCreationFailed);
458-
send_failure = true;
459-
return;
519+
CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
520+
PartiallySignedTransaction psbtx(mtx);
521+
bool complete = false;
522+
// Always fill without signing first. This prevents an external signer
523+
// from being called prematurely and is not expensive.
524+
TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete);
525+
assert(!complete);
526+
assert(err == TransactionError::OK);
527+
send_failure = !signWithExternalSigner(psbtx, mtx, complete);
528+
// Don't broadcast when user rejects it on the device or there's a failure:
529+
broadcast = complete && !send_failure;
530+
if (!send_failure) {
531+
// A transaction signed with an external signer is not always complete,
532+
// e.g. in a multisig wallet.
533+
if (complete) {
534+
// Prepare transaction for broadcast transaction if complete
535+
const CTransactionRef tx = MakeTransactionRef(mtx);
536+
m_current_transaction->setWtx(tx);
537+
} else {
538+
presentPSBT(psbtx);
539+
}
460540
}
461-
// fillPSBT does not always properly finalize
462-
complete = FinalizeAndExtractPSBT(psbtx, mtx);
463541
}
464542

465-
// Broadcast transaction if complete (even with an external signer this
466-
// is not always the case, e.g. in a multisig wallet).
467-
if (complete) {
468-
const CTransactionRef tx = MakeTransactionRef(mtx);
469-
m_current_transaction->setWtx(tx);
543+
// Broadcast the transaction, unless an external signer was used and it
544+
// failed, or more signatures are needed.
545+
if (broadcast) {
546+
// now send the prepared transaction
470547
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(*m_current_transaction);
471548
// process sendStatus and on error generate message shown to user
472549
processSendCoinsReturn(sendStatus);
@@ -476,64 +553,6 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
476553
} else {
477554
send_failure = true;
478555
}
479-
return;
480-
}
481-
482-
// Copy PSBT to clipboard and offer to save
483-
assert(!complete);
484-
// Serialize the PSBT
485-
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
486-
ssTx << psbtx;
487-
GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
488-
QMessageBox msgBox;
489-
msgBox.setText("Unsigned Transaction");
490-
msgBox.setInformativeText("The PSBT has been copied to the clipboard. You can also save it.");
491-
msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard);
492-
msgBox.setDefaultButton(QMessageBox::Discard);
493-
switch (msgBox.exec()) {
494-
case QMessageBox::Save: {
495-
QString selectedFilter;
496-
QString fileNameSuggestion = "";
497-
bool first = true;
498-
for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients()) {
499-
if (!first) {
500-
fileNameSuggestion.append(" - ");
501-
}
502-
QString labelOrAddress = rcp.label.isEmpty() ? rcp.address : rcp.label;
503-
QString amount = BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount);
504-
fileNameSuggestion.append(labelOrAddress + "-" + amount);
505-
first = false;
506-
}
507-
fileNameSuggestion.append(".psbt");
508-
QString filename = GUIUtil::getSaveFileName(this,
509-
tr("Save Transaction Data"), fileNameSuggestion,
510-
//: Expanded name of the binary PSBT file format. See: BIP 174.
511-
tr("Partially Signed Transaction (Binary)") + QLatin1String(" (*.psbt)"), &selectedFilter);
512-
if (filename.isEmpty()) {
513-
return;
514-
}
515-
std::ofstream out{filename.toLocal8Bit().data(), std::ofstream::out | std::ofstream::binary};
516-
out << ssTx.str();
517-
out.close();
518-
Q_EMIT message(tr("PSBT saved"), "PSBT saved to disk", CClientUIInterface::MSG_INFORMATION);
519-
break;
520-
}
521-
case QMessageBox::Discard:
522-
break;
523-
default:
524-
assert(false);
525-
} // msgBox.exec()
526-
} else {
527-
assert(!model->wallet().privateKeysDisabled());
528-
// now send the prepared transaction
529-
WalletModel::SendCoinsReturn sendStatus = model->sendCoins(*m_current_transaction);
530-
// process sendStatus and on error generate message shown to user
531-
processSendCoinsReturn(sendStatus);
532-
533-
if (sendStatus.status == WalletModel::OK) {
534-
Q_EMIT coinsSent(m_current_transaction->getWtx()->GetHash());
535-
} else {
536-
send_failure = true;
537556
}
538557
}
539558
if (!send_failure) {

src/qt/sendcoinsdialog.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,24 @@ public Q_SLOTS:
7070
bool fFeeMinimized;
7171
const PlatformStyle *platformStyle;
7272

73+
// Copy PSBT to clipboard and offer to save it.
74+
void presentPSBT(PartiallySignedTransaction& psbt);
7375
// Process WalletModel::SendCoinsReturn and generate a pair consisting
7476
// of a message and message flags for use in Q_EMIT message().
7577
// Additional parameter msgArg can be used via .arg(msgArg).
7678
void processSendCoinsReturn(const WalletModel::SendCoinsReturn &sendCoinsReturn, const QString &msgArg = QString());
7779
void minimizeFeeSection(bool fMinimize);
7880
// Format confirmation message
7981
bool PrepareSendText(QString& question_string, QString& informative_text, QString& detailed_text);
82+
/* Sign PSBT using external signer.
83+
*
84+
* @param[in,out] psbtx the PSBT to sign
85+
* @param[in,out] mtx needed to attempt to finalize
86+
* @param[in,out] complete whether the PSBT is complete (a successfully signed multisig transaction may not be complete)
87+
*
88+
* @returns false if any failure occurred, which may include the user rejection of a transaction on the device.
89+
*/
90+
bool signWithExternalSigner(PartiallySignedTransaction& psbt, CMutableTransaction& mtx, bool& complete);
8091
void updateFeeMinimizedLabel();
8192
void updateCoinControlState();
8293

@@ -117,6 +128,8 @@ class SendConfirmationDialog : public QMessageBox
117128

118129
public:
119130
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);
131+
/* Returns QMessageBox::Cancel, QMessageBox::Yes when "Send" is
132+
clicked and QMessageBox::Save when "Create Unsigned" is clicked. */
120133
int exec() override;
121134

122135
private Q_SLOTS:

0 commit comments

Comments
 (0)