mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-06-19 21:31:33 +02:00
Merge bitcoin-core/gui#555: Restore Send button when using external signer
2efdfb88aab6496dcf2b98e0de30635bc6bade85 gui: restore Send for external signer (Sjors Provoost) 4b5a6cd14967b8ec3cb525e4cb18628de6c15091 refactor: helper function signWithExternalSigner() (Sjors Provoost) 026b5b4523317fdefc69cf5cec55f76f18ad0c0a 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 2efdfb88aab6496dcf2b98e0de30635bc6bade85 diff review since my last review, code re-review, rebased to current master, verified clean debug build of each commit luke-jr: utACK 2efdfb88aab6496dcf2b98e0de30635bc6bade85 Tree-SHA512: e8731a0ef9e87564b2676c7b022b742d9621bba964c19dba9fd9f6961eb608737a9e1a22c0a3c8b2f2f6d583bba067606ee8392422e82082deefb20ea7b88c7c
This commit is contained in:
commit
aece566249
@ -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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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
|
// 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…
x
Reference in New Issue
Block a user