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,6 +401,80 @@ bool SendCoinsDialog::PrepareSendText(QString& question_string, QString& informa
return true; return true;
} }
void SendCoinsDialog::presentPSBT(PartiallySignedTransaction& psbtx)
{
// Serialize the PSBT
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
QMessageBox msgBox;
msgBox.setText("Unsigned Transaction");
msgBox.setInformativeText("The PSBT has been copied to the clipboard. You can also save it.");
msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard);
msgBox.setDefaultButton(QMessageBox::Discard);
switch (msgBox.exec()) {
case QMessageBox::Save: {
QString selectedFilter;
QString fileNameSuggestion = "";
bool first = true;
for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients()) {
if (!first) {
fileNameSuggestion.append(" - ");
}
QString labelOrAddress = rcp.label.isEmpty() ? rcp.address : rcp.label;
QString amount = BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount);
fileNameSuggestion.append(labelOrAddress + "-" + amount);
first = false;
}
fileNameSuggestion.append(".psbt");
QString filename = GUIUtil::getSaveFileName(this,
tr("Save Transaction Data"), fileNameSuggestion,
//: Expanded name of the binary PSBT file format. See: BIP 174.
tr("Partially Signed Transaction (Binary)") + QLatin1String(" (*.psbt)"), &selectedFilter);
if (filename.isEmpty()) {
return;
}
std::ofstream out{filename.toLocal8Bit().data(), std::ofstream::out | std::ofstream::binary};
out << ssTx.str();
out.close();
Q_EMIT message(tr("PSBT saved"), "PSBT saved to disk", CClientUIInterface::MSG_INFORMATION);
break;
}
case QMessageBox::Discard:
break;
default:
assert(false);
} // 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) void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
{ {
if(!model || !model->getOptionsModel()) if(!model || !model->getOptionsModel())
@ -411,7 +485,9 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
assert(m_current_transaction); assert(m_current_transaction);
const QString confirmation = tr("Confirm send coins"); 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); 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); confirmationDialog->setAttribute(Qt::WA_DeleteOnClose);
// TODO: Replace QDialog::exec() with safer QDialog::show(). // TODO: Replace QDialog::exec() with safer QDialog::show().
const auto retval = static_cast<QMessageBox::StandardButton>(confirmationDialog->exec()); const auto retval = static_cast<QMessageBox::StandardButton>(confirmationDialog->exec());
@ -424,49 +500,50 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
bool send_failure = false; bool send_failure = false;
if (retval == QMessageBox::Save) { if (retval == QMessageBox::Save) {
// "Create Unsigned" clicked
CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())}; CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
PartiallySignedTransaction psbtx(mtx); PartiallySignedTransaction psbtx(mtx);
bool complete = false; bool complete = false;
// Always fill without signing first. This prevents an external signer // Fill without signing
// from being called prematurely and is not expensive. TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete);
TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, nullptr, psbtx, complete);
assert(!complete); assert(!complete);
assert(err == TransactionError::OK); assert(err == TransactionError::OK);
// Copy PSBT to clipboard and offer to save
presentPSBT(psbtx);
} else {
// "Send" clicked
assert(!model->wallet().privateKeysDisabled() || model->wallet().hasExternalSigner());
bool broadcast = true;
if (model->wallet().hasExternalSigner()) { if (model->wallet().hasExternalSigner()) {
try { CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())};
err = model->wallet().fillPSBT(SIGHASH_ALL, true /* sign */, true /* bip32derivs */, nullptr, psbtx, complete); PartiallySignedTransaction psbtx(mtx);
} catch (const std::runtime_error& e) { bool complete = false;
QMessageBox::critical(nullptr, tr("Sign failed"), e.what()); // Always fill without signing first. This prevents an external signer
send_failure = true; // from being called prematurely and is not expensive.
return; TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, /*sign=*/false, /*bip32derivs=*/true, /*n_signed=*/nullptr, psbtx, complete);
} assert(!complete);
if (err == TransactionError::EXTERNAL_SIGNER_NOT_FOUND) { assert(err == TransactionError::OK);
//: "External signer" means using devices such as hardware wallets. send_failure = !signWithExternalSigner(psbtx, mtx, complete);
QMessageBox::critical(nullptr, tr("External signer not found"), "External signer not found"); // Don't broadcast when user rejects it on the device or there's a failure:
send_failure = true; broadcast = complete && !send_failure;
return; if (!send_failure) {
} // A transaction signed with an external signer is not always complete,
if (err == TransactionError::EXTERNAL_SIGNER_FAILED) { // e.g. in a multisig wallet.
//: "External signer" means using devices such as hardware wallets. if (complete) {
QMessageBox::critical(nullptr, tr("External signer failure"), "External signer failure"); // Prepare transaction for broadcast transaction if complete
send_failure = true; const CTransactionRef tx = MakeTransactionRef(mtx);
return; m_current_transaction->setWtx(tx);
} } else {
if (err != TransactionError::OK) { presentPSBT(psbtx);
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 // Broadcast the transaction, unless an external signer was used and it
// is not always the case, e.g. in a multisig wallet). // failed, or more signatures are needed.
if (complete) { if (broadcast) {
const CTransactionRef tx = MakeTransactionRef(mtx); // now send the prepared transaction
m_current_transaction->setWtx(tx);
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
processSendCoinsReturn(sendStatus); processSendCoinsReturn(sendStatus);
@ -476,64 +553,6 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked)
} else { } else {
send_failure = true; send_failure = true;
} }
return;
}
// Copy PSBT to clipboard and offer to save
assert(!complete);
// Serialize the PSBT
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << psbtx;
GUIUtil::setClipboard(EncodeBase64(ssTx.str()).c_str());
QMessageBox msgBox;
msgBox.setText("Unsigned Transaction");
msgBox.setInformativeText("The PSBT has been copied to the clipboard. You can also save it.");
msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard);
msgBox.setDefaultButton(QMessageBox::Discard);
switch (msgBox.exec()) {
case QMessageBox::Save: {
QString selectedFilter;
QString fileNameSuggestion = "";
bool first = true;
for (const SendCoinsRecipient &rcp : m_current_transaction->getRecipients()) {
if (!first) {
fileNameSuggestion.append(" - ");
}
QString labelOrAddress = rcp.label.isEmpty() ? rcp.address : rcp.label;
QString amount = BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), rcp.amount);
fileNameSuggestion.append(labelOrAddress + "-" + amount);
first = false;
}
fileNameSuggestion.append(".psbt");
QString filename = GUIUtil::getSaveFileName(this,
tr("Save Transaction Data"), fileNameSuggestion,
//: Expanded name of the binary PSBT file format. See: BIP 174.
tr("Partially Signed Transaction (Binary)") + QLatin1String(" (*.psbt)"), &selectedFilter);
if (filename.isEmpty()) {
return;
}
std::ofstream out{filename.toLocal8Bit().data(), std::ofstream::out | std::ofstream::binary};
out << ssTx.str();
out.close();
Q_EMIT message(tr("PSBT saved"), "PSBT saved to disk", CClientUIInterface::MSG_INFORMATION);
break;
}
case QMessageBox::Discard:
break;
default:
assert(false);
} // msgBox.exec()
} else {
assert(!model->wallet().privateKeysDisabled());
// now send the prepared transaction
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;
} }
} }
if (!send_failure) { if (!send_failure) {

@ -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