From 6cdbc83e9341d1552faee4ccd8c190babc63e8d1 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 16 Sep 2019 18:51:24 +0200 Subject: [PATCH 1/7] gui: add external signer path to options dialog --- src/qt/forms/optionsdialog.ui | 30 ++++++++++++++++++++++++++++++ src/qt/optionsdialog.cpp | 2 ++ src/qt/optionsmodel.cpp | 15 +++++++++++++++ src/qt/optionsmodel.h | 1 + 4 files changed, 48 insertions(+) diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index f199e8c1a16..bd72328c021 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -229,6 +229,36 @@ + + + + External Signer (e.g. hardware wallet) + + + + + + + + &External signer script path + + + externalSignerPath + + + + + + + Full path to a Bitcoin Core compatible script (e.g. C:\Downloads\hwi.exe or /Users/you/Downloads/hwi.py). Beware: malware can steal your coins! + + + + + + + + diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 8a32994e3f5..6ad8db43480 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -199,6 +199,7 @@ void OptionsDialog::setModel(OptionsModel *_model) connect(ui->prune, &QCheckBox::clicked, this, &OptionsDialog::togglePruneWarning); connect(ui->pruneSize, qOverload(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning); connect(ui->databaseCache, qOverload(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning); + connect(ui->externalSignerPath, &QLineEdit::textChanged, [this]{ showRestartWarning(); }); connect(ui->threadsScriptVerif, qOverload(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning); /* Wallet */ connect(ui->spendZeroConfChange, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); @@ -233,6 +234,7 @@ void OptionsDialog::setMapper() /* Wallet */ mapper->addMapping(ui->spendZeroConfChange, OptionsModel::SpendZeroConfChange); mapper->addMapping(ui->coinControlFeatures, OptionsModel::CoinControlFeatures); + mapper->addMapping(ui->externalSignerPath, OptionsModel::ExternalSignerPath); /* Network */ mapper->addMapping(ui->mapPortUpnp, OptionsModel::MapPortUPnP); diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index abdf9e9ae6f..24a4e9ee968 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -117,6 +117,13 @@ void OptionsModel::Init(bool resetSettings) settings.setValue("bSpendZeroConfChange", true); if (!gArgs.SoftSetBoolArg("-spendzeroconfchange", settings.value("bSpendZeroConfChange").toBool())) addOverriddenOption("-spendzeroconfchange"); + + if (!settings.contains("external_signer_path")) + settings.setValue("external_signer_path", ""); + + if (!gArgs.SoftSetArg("-signer", settings.value("external_signer_path").toString().toStdString())) { + addOverriddenOption("-signer"); + } #endif // Network @@ -326,6 +333,8 @@ QVariant OptionsModel::data(const QModelIndex & index, int role) const #ifdef ENABLE_WALLET case SpendZeroConfChange: return settings.value("bSpendZeroConfChange"); + case ExternalSignerPath: + return settings.value("external_signer_path"); #endif case DisplayUnit: return nDisplayUnit; @@ -445,6 +454,12 @@ bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, in setRestartRequired(true); } break; + case ExternalSignerPath: + if (settings.value("external_signer_path") != value.toString()) { + settings.setValue("external_signer_path", value.toString()); + setRestartRequired(true); + } + break; #endif case DisplayUnit: setDisplayUnit(value); diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index 4d012a9b8f4..535843e8ba9 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -65,6 +65,7 @@ class OptionsModel : public QAbstractListModel Prune, // bool PruneSize, // int DatabaseCache, // int + ExternalSignerPath, // QString SpendZeroConfChange, // bool Listen, // bool OptionIDRowCount, From eef8d6452962cd4a8956d9ad268164715365b9ab Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 21 Feb 2020 21:13:43 +0100 Subject: [PATCH 2/7] gui: create wallet with external signer --- src/qt/createwalletdialog.cpp | 43 +++++++++++++++++++++++++++++- src/qt/createwalletdialog.h | 1 + src/qt/forms/createwalletdialog.ui | 11 ++++++++ src/qt/walletcontroller.cpp | 3 +++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/qt/createwalletdialog.cpp b/src/qt/createwalletdialog.cpp index 113bd30a0c1..6f5ca95d1b7 100644 --- a/src/qt/createwalletdialog.cpp +++ b/src/qt/createwalletdialog.cpp @@ -27,14 +27,39 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) : }); connect(ui->encrypt_wallet_checkbox, &QCheckBox::toggled, [this](bool checked) { - // Disable the disable_privkeys_checkbox when isEncryptWalletChecked is + // Disable the disable_privkeys_checkbox and external_signer_checkbox when isEncryptWalletChecked is // set to true, enable it when isEncryptWalletChecked is false. ui->disable_privkeys_checkbox->setEnabled(!checked); + ui->external_signer_checkbox->setEnabled(!checked); // When the disable_privkeys_checkbox is disabled, uncheck it. if (!ui->disable_privkeys_checkbox->isEnabled()) { ui->disable_privkeys_checkbox->setChecked(false); } + + // When the external_signer_checkbox box is disabled, uncheck it. + if (!ui->external_signer_checkbox->isEnabled()) { + ui->external_signer_checkbox->setChecked(false); + } + + }); + + connect(ui->external_signer_checkbox, &QCheckBox::toggled, [this](bool checked) { + ui->encrypt_wallet_checkbox->setEnabled(!checked); + ui->blank_wallet_checkbox->setEnabled(!checked); + ui->disable_privkeys_checkbox->setEnabled(!checked); + ui->descriptor_checkbox->setEnabled(!checked); + + // The external signer checkbox is only enabled when a device is detected. + // In that case it is checked by default. Toggling it restores the other + // options to their default. + ui->descriptor_checkbox->setChecked(checked); + ui->encrypt_wallet_checkbox->setChecked(false); + ui->disable_privkeys_checkbox->setChecked(checked); + // The blank check box is ambiguous. This flag is always true for a + // watch-only wallet, even though we immedidately fetch keys from the + // external signer. + ui->blank_wallet_checkbox->setChecked(checked); }); connect(ui->disable_privkeys_checkbox, &QCheckBox::toggled, [this](bool checked) { @@ -63,11 +88,22 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) : ui->descriptor_checkbox->setToolTip(tr("Compiled without sqlite support (required for descriptor wallets)")); ui->descriptor_checkbox->setEnabled(false); ui->descriptor_checkbox->setChecked(false); + ui->external_signer_checkbox->setEnabled(false); + ui->external_signer_checkbox->setChecked(false); #endif + #ifndef USE_BDB ui->descriptor_checkbox->setEnabled(false); ui->descriptor_checkbox->setChecked(true); #endif + +#ifndef ENABLE_EXTERNAL_SIGNER + //: "External signing" means using devices such as hardware wallets. + ui->external_signer_checkbox->setToolTip(tr("Compiled without external signing support (required for external signing)")); + ui->external_signer_checkbox->setEnabled(false); + ui->external_signer_checkbox->setChecked(false); +#endif + } CreateWalletDialog::~CreateWalletDialog() @@ -99,3 +135,8 @@ bool CreateWalletDialog::isDescriptorWalletChecked() const { return ui->descriptor_checkbox->isChecked(); } + +bool CreateWalletDialog::isExternalSignerChecked() const +{ + return ui->external_signer_checkbox->isChecked(); +} diff --git a/src/qt/createwalletdialog.h b/src/qt/createwalletdialog.h index 20cce937c83..6eb79bc0603 100644 --- a/src/qt/createwalletdialog.h +++ b/src/qt/createwalletdialog.h @@ -28,6 +28,7 @@ class CreateWalletDialog : public QDialog bool isDisablePrivateKeysChecked() const; bool isMakeBlankWalletChecked() const; bool isDescriptorWalletChecked() const; + bool isExternalSignerChecked() const; private: Ui::CreateWalletDialog *ui; diff --git a/src/qt/forms/createwalletdialog.ui b/src/qt/forms/createwalletdialog.ui index 881869a46cb..b11fb026b0f 100644 --- a/src/qt/forms/createwalletdialog.ui +++ b/src/qt/forms/createwalletdialog.ui @@ -109,6 +109,16 @@ + + + + Use an external signing device such as a hardware wallet. Configure the external signer script in wallet preferences first. + + + External signer + + + @@ -143,6 +153,7 @@ disable_privkeys_checkbox blank_wallet_checkbox descriptor_checkbox + external_signer_checkbox diff --git a/src/qt/walletcontroller.cpp b/src/qt/walletcontroller.cpp index c152689f0be..fc469140ed2 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -260,6 +260,9 @@ void CreateWalletActivity::createWallet() if (m_create_wallet_dialog->isDescriptorWalletChecked()) { flags |= WALLET_FLAG_DESCRIPTORS; } + if (m_create_wallet_dialog->isExternalSignerChecked()) { + flags |= WALLET_FLAG_EXTERNAL_SIGNER; + } QTimer::singleShot(500, worker(), [this, name, flags] { std::unique_ptr wallet = node().walletClient().createWallet(name, m_passphrase, flags, m_error_message, m_warning_message); From 450cb40a344605dda3bcc39495c35869580b9fc2 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 16 Sep 2019 19:00:55 +0200 Subject: [PATCH 3/7] wallet: add displayAddress to interface --- src/interfaces/wallet.h | 3 +++ src/wallet/interfaces.cpp | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 6ccfd7fc205..988e59b65a0 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -121,6 +121,9 @@ class Wallet //! Get dest values with prefix. virtual std::vector getDestValues(const std::string& prefix) = 0; + //! Display address on external signer + virtual bool displayAddress(const CTxDestination& dest) = 0; + //! Lock coin. virtual void lockCoin(const COutPoint& output) = 0; diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 64ce09d1d12..bc39450eb39 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -214,6 +214,11 @@ class WalletImpl : public Wallet LOCK(m_wallet->cs_wallet); return m_wallet->GetDestValues(prefix); } + bool displayAddress(const CTxDestination& dest) override + { + LOCK(m_wallet->cs_wallet); + return m_wallet->DisplayAddress(dest); + } void lockCoin(const COutPoint& output) override { LOCK(m_wallet->cs_wallet); From 62ac119f919ae1160ed67af796f24b78025fa8e3 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 16 Sep 2019 19:01:04 +0200 Subject: [PATCH 4/7] gui: display address on external signer --- src/interfaces/wallet.h | 3 +++ src/qt/forms/receiverequestdialog.ui | 13 +++++++++++++ src/qt/receiverequestdialog.cpp | 6 ++++++ src/qt/walletmodel.cpp | 12 ++++++++++++ src/qt/walletmodel.h | 1 + src/wallet/interfaces.cpp | 1 + 6 files changed, 36 insertions(+) diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 988e59b65a0..f7e02e6ad49 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -258,6 +258,9 @@ class Wallet // Return whether private keys enabled. virtual bool privateKeysDisabled() = 0; + // Return whether wallet uses an external signer. + virtual bool hasExternalSigner() = 0; + // Get default address type. virtual OutputType getDefaultAddressType() = 0; diff --git a/src/qt/forms/receiverequestdialog.ui b/src/qt/forms/receiverequestdialog.ui index 7d95a8bc907..70a7cf71de9 100644 --- a/src/qt/forms/receiverequestdialog.ui +++ b/src/qt/forms/receiverequestdialog.ui @@ -254,6 +254,19 @@ + + + + &Verify + + + Verify this address on e.g. a hardware wallet screen + + + false + + + diff --git a/src/qt/receiverequestdialog.cpp b/src/qt/receiverequestdialog.cpp index 78ae5c07da7..abe7de8f89c 100644 --- a/src/qt/receiverequestdialog.cpp +++ b/src/qt/receiverequestdialog.cpp @@ -89,6 +89,12 @@ void ReceiveRequestDialog::setInfo(const SendCoinsRecipient &_info) ui->wallet_tag->hide(); ui->wallet_content->hide(); } + + ui->btnVerify->setVisible(this->model->wallet().hasExternalSigner()); + + connect(ui->btnVerify, &QPushButton::clicked, [this] { + model->displayAddress(info.address.toStdString()); + }); } void ReceiveRequestDialog::updateDisplayUnit() diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index cc6db8d33ef..2ce378e15b7 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -571,6 +571,18 @@ bool WalletModel::bumpFee(uint256 hash, uint256& new_hash) return true; } +bool WalletModel::displayAddress(std::string sAddress) +{ + CTxDestination dest = DecodeDestination(sAddress); + bool res = false; + try { + res = m_wallet->displayAddress(dest); + } catch (const std::runtime_error& e) { + QMessageBox::critical(nullptr, tr("Can't display address"), e.what()); + } + return res; +} + bool WalletModel::isWalletEnabled() { return !gArgs.GetBoolArg("-disablewallet", DEFAULT_DISABLE_WALLET); diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 4ca8643444d..76b8ac72aff 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -139,6 +139,7 @@ class WalletModel : public QObject bool saveReceiveRequest(const std::string &sAddress, const int64_t nId, const std::string &sRequest); bool bumpFee(uint256 hash, uint256& new_hash); + bool displayAddress(std::string sAddress); static bool isWalletEnabled(); diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index bc39450eb39..05656aec93f 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -459,6 +459,7 @@ class WalletImpl : public Wallet unsigned int getConfirmTarget() override { return m_wallet->m_confirm_target; } bool hdEnabled() override { return m_wallet->IsHDEnabled(); } bool canGetAddresses() override { return m_wallet->CanGetAddresses(); } + bool hasExternalSigner() override { return m_wallet->IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER); } bool privateKeysDisabled() override { return m_wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS); } OutputType getDefaultAddressType() override { return m_wallet->m_default_address_type; } CAmount getDefaultMaxTxFee() override { return m_wallet->m_default_max_tx_fee; } From 3f845ea2994f53e29abeb3fa158c35f1ee56e7e8 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 29 Oct 2019 20:53:49 +0100 Subject: [PATCH 5/7] node: add externalSigners to interface --- src/interfaces/node.h | 6 ++++++ src/node/interfaces.cpp | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/interfaces/node.h b/src/interfaces/node.h index 1dd1e92e2ff..35b6160ceac 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -6,6 +6,7 @@ #define BITCOIN_INTERFACES_NODE_H #include // For CAmount +#include #include // For NodeId #include // For banmap_t #include // For Network @@ -110,6 +111,11 @@ class Node //! Disconnect node by id. virtual bool disconnectById(NodeId id) = 0; +#ifdef ENABLE_EXTERNAL_SIGNER + //! List external signers + virtual std::vector externalSigners() = 0; +#endif + //! Get total bytes recv. virtual int64_t getTotalBytesRecv() = 0; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 8befbf5e307..171f15d4fb9 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -170,6 +170,16 @@ class NodeImpl : public Node } return false; } +#ifdef ENABLE_EXTERNAL_SIGNER + std::vector externalSigners() override + { + std::vector signers = {}; + const std::string command = gArgs.GetArg("-signer", ""); + if (command == "") return signers; + ExternalSigner::Enumerate(command, signers, Params().NetworkIDString()); + return signers; + } +#endif int64_t getTotalBytesRecv() override { return m_context->connman ? m_context->connman->GetTotalBytesRecv() : 0; } int64_t getTotalBytesSent() override { return m_context->connman ? m_context->connman->GetTotalBytesSent() : 0; } size_t getMempoolSize() override { return m_context->mempool ? m_context->mempool->size() : 0; } From 24815c6309431cb0797defaf7add1150bcf4b567 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Thu, 7 Nov 2019 19:06:40 +0100 Subject: [PATCH 6/7] gui: wallet creation detects external signer --- src/qt/createwalletdialog.cpp | 23 +++++++++++++++++++++++ src/qt/createwalletdialog.h | 8 ++++++++ src/qt/walletcontroller.cpp | 11 +++++++++++ 3 files changed, 42 insertions(+) diff --git a/src/qt/createwalletdialog.cpp b/src/qt/createwalletdialog.cpp index 6f5ca95d1b7..e593697b46d 100644 --- a/src/qt/createwalletdialog.cpp +++ b/src/qt/createwalletdialog.cpp @@ -6,6 +6,7 @@ #include #endif +#include #include #include @@ -111,6 +112,28 @@ CreateWalletDialog::~CreateWalletDialog() delete ui; } +#ifdef ENABLE_EXTERNAL_SIGNER +void CreateWalletDialog::setSigners(std::vector& signers) +{ + if (!signers.empty()) { + ui->external_signer_checkbox->setEnabled(true); + ui->external_signer_checkbox->setChecked(true); + ui->encrypt_wallet_checkbox->setEnabled(false); + ui->encrypt_wallet_checkbox->setChecked(false); + // The order matters, because connect() is called when toggling a checkbox: + ui->blank_wallet_checkbox->setEnabled(false); + ui->blank_wallet_checkbox->setChecked(false); + ui->disable_privkeys_checkbox->setEnabled(false); + ui->disable_privkeys_checkbox->setChecked(true); + const std::string label = signers[0].m_name; + ui->wallet_name_line_edit->setText(QString::fromStdString(label)); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + } else { + ui->external_signer_checkbox->setEnabled(false); + } +} +#endif + QString CreateWalletDialog::walletName() const { return ui->wallet_name_line_edit->text(); diff --git a/src/qt/createwalletdialog.h b/src/qt/createwalletdialog.h index 6eb79bc0603..585b1461f7e 100644 --- a/src/qt/createwalletdialog.h +++ b/src/qt/createwalletdialog.h @@ -9,6 +9,10 @@ class WalletModel; +#ifdef ENABLE_EXTERNAL_SIGNER +class ExternalSigner; +#endif + namespace Ui { class CreateWalletDialog; } @@ -23,6 +27,10 @@ class CreateWalletDialog : public QDialog explicit CreateWalletDialog(QWidget* parent); virtual ~CreateWalletDialog(); +#ifdef ENABLE_EXTERNAL_SIGNER + void setSigners(std::vector& signers); +#endif + QString walletName() const; bool isEncryptWalletChecked() const; bool isDisablePrivateKeysChecked() const; diff --git a/src/qt/walletcontroller.cpp b/src/qt/walletcontroller.cpp index fc469140ed2..404eb01f2cc 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -291,6 +291,17 @@ void CreateWalletActivity::finish() void CreateWalletActivity::create() { m_create_wallet_dialog = new CreateWalletDialog(m_parent_widget); + +#ifdef ENABLE_EXTERNAL_SIGNER + std::vector signers; + try { + signers = node().externalSigners(); + } catch (const std::runtime_error& e) { + QMessageBox::critical(nullptr, tr("Can't list signers"), e.what()); + } + m_create_wallet_dialog->setSigners(signers); +#endif + m_create_wallet_dialog->setWindowModality(Qt::ApplicationModal); m_create_wallet_dialog->show(); From 1c4b456e1a0ccf0397d652f8c18201c3224c5c21 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 21 Feb 2020 22:28:40 +0100 Subject: [PATCH 7/7] gui: send using external signer --- src/qt/sendcoinsdialog.cpp | 80 +++++++++++++++++++++++++++---- src/qt/walletmodeltransaction.cpp | 5 ++ src/qt/walletmodeltransaction.h | 2 + 3 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 160b43324f9..e87a2b97bca 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -199,7 +199,16 @@ void SendCoinsDialog::setModel(WalletModel *_model) // set default rbf checkbox state ui->optInRBF->setCheckState(Qt::Checked); - if (model->wallet().privateKeysDisabled()) { + if (model->wallet().hasExternalSigner()) { + ui->sendButton->setText(tr("Sign on device")); + if (gArgs.GetArg("-signer", "") != "") { + ui->sendButton->setEnabled(true); + ui->sendButton->setToolTip(tr("Connect your hardware wallet first.")); + } else { + ui->sendButton->setEnabled(false); + ui->sendButton->setToolTip(tr("Set external signer script path in Options -> Wallet")); + } + } else if (model->wallet().privateKeysDisabled()) { ui->sendButton->setText(tr("Cr&eate Unsigned")); ui->sendButton->setToolTip(tr("Creates a Partially Signed Bitcoin Transaction (PSBT) for use with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); } @@ -313,14 +322,14 @@ bool SendCoinsDialog::PrepareSendText(QString& question_string, QString& informa formatted.append(recipientElement); } - if (model->wallet().privateKeysDisabled()) { + if (model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner()) { question_string.append(tr("Do you want to draft this transaction?")); } else { question_string.append(tr("Are you sure you want to send?")); } question_string.append("
"); - if (model->wallet().privateKeysDisabled()) { + if (model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner()) { question_string.append(tr("Please, review your transaction proposal. This will produce a Partially Signed Bitcoin Transaction (PSBT) which you can save or copy and then sign with e.g. an offline %1 wallet, or a PSBT-compatible hardware wallet.").arg(PACKAGE_NAME)); } else { question_string.append(tr("Please, review your transaction.")); @@ -386,8 +395,8 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked) if (!PrepareSendText(question_string, informative_text, detailed_text)) return; assert(m_current_transaction); - const QString confirmation = model->wallet().privateKeysDisabled() ? tr("Confirm transaction proposal") : tr("Confirm send coins"); - const QString confirmButtonText = model->wallet().privateKeysDisabled() ? tr("Create Unsigned") : tr("Send"); + const QString confirmation = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner() ? tr("Confirm transaction proposal") : tr("Confirm send coins"); + const QString confirmButtonText = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner() ? tr("Create Unsigned") : tr("Sign and send"); SendConfirmationDialog confirmationDialog(confirmation, question_string, informative_text, detailed_text, SEND_CONFIRM_DELAY, confirmButtonText, this); confirmationDialog.exec(); QMessageBox::StandardButton retval = static_cast(confirmationDialog.result()); @@ -403,9 +412,58 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked) CMutableTransaction mtx = CMutableTransaction{*(m_current_transaction->getWtx())}; PartiallySignedTransaction psbtx(mtx); bool complete = false; - const TransactionError err = model->wallet().fillPSBT(SIGHASH_ALL, false /* sign */, true /* bip32derivs */, psbtx, complete, nullptr); + // 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 */, psbtx, complete, nullptr); assert(!complete); assert(err == TransactionError::OK); + if (model->wallet().hasExternalSigner()) { + try { + err = model->wallet().fillPSBT(SIGHASH_ALL, true /* sign */, true /* bip32derivs */, psbtx, complete, nullptr); + } 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) { + QMessageBox::critical(nullptr, tr("External signer not found"), "External signer not found"); + send_failure = true; + return; + } + if (err == TransactionError::EXTERNAL_SIGNER_FAILED) { + 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 CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); ssTx << psbtx; @@ -447,7 +505,7 @@ void SendCoinsDialog::sendButtonClicked([[maybe_unused]] bool checked) break; default: assert(false); - } + } // msgBox.exec() } else { // now send the prepared transaction WalletModel::SendCoinsReturn sendStatus = model->sendCoins(*m_current_transaction); @@ -614,7 +672,9 @@ void SendCoinsDialog::setBalance(const interfaces::WalletBalances& balances) if(model && model->getOptionsModel()) { CAmount balance = balances.balance; - if (model->wallet().privateKeysDisabled()) { + if (model->wallet().hasExternalSigner()) { + ui->labelBalanceName->setText(tr("External balance:")); + } else if (model->wallet().privateKeysDisabled()) { balance = balances.watch_only_balance; ui->labelBalanceName->setText(tr("Watch-only balance:")); } @@ -698,7 +758,7 @@ void SendCoinsDialog::on_buttonMinimizeFee_clicked() void SendCoinsDialog::useAvailableBalance(SendCoinsEntry* entry) { // Include watch-only for wallets without private key - m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled(); + m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner(); // Calculate available amount to send. CAmount amount = model->wallet().getAvailableBalance(*m_coin_control); @@ -753,7 +813,7 @@ void SendCoinsDialog::updateCoinControlState() m_coin_control->m_confirm_target = getConfTargetForIndex(ui->confTargetSelector->currentIndex()); m_coin_control->m_signal_bip125_rbf = ui->optInRBF->isChecked(); // Include watch-only for wallets without private key - m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled(); + m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner(); } void SendCoinsDialog::updateNumberOfBlocks(int count, const QDateTime& blockDate, double nVerificationProgress, bool headers, SynchronizationState sync_state) { diff --git a/src/qt/walletmodeltransaction.cpp b/src/qt/walletmodeltransaction.cpp index 25172e774c6..d185ddb7e85 100644 --- a/src/qt/walletmodeltransaction.cpp +++ b/src/qt/walletmodeltransaction.cpp @@ -26,6 +26,11 @@ CTransactionRef& WalletModelTransaction::getWtx() return wtx; } +void WalletModelTransaction::setWtx(const CTransactionRef& newTx) +{ + wtx = newTx; +} + unsigned int WalletModelTransaction::getTransactionSize() { return wtx ? GetVirtualTransactionSize(*wtx) : 0; diff --git a/src/qt/walletmodeltransaction.h b/src/qt/walletmodeltransaction.h index f9a95362c80..120d240d917 100644 --- a/src/qt/walletmodeltransaction.h +++ b/src/qt/walletmodeltransaction.h @@ -27,6 +27,8 @@ class WalletModelTransaction QList getRecipients() const; CTransactionRef& getWtx(); + void setWtx(const CTransactionRef&); + unsigned int getTransactionSize(); void setTransactionFee(const CAmount& newFee);