diff options
author | Samuel Dobson <dobsonsa68@gmail.com> | 2021-06-09 18:17:26 +1200 |
---|---|---|
committer | Samuel Dobson <dobsonsa68@gmail.com> | 2021-06-09 18:59:59 +1200 |
commit | 68a89d7a46bf61c2156da3ad68d20095397b943b (patch) | |
tree | aa836280e5ecaa850427873daf8fdbdceaa59f9d /src | |
parent | 7cac26246a23b2843cb368fe8a68c54dde04d5d8 (diff) | |
parent | 1c4b456e1a0ccf0397d652f8c18201c3224c5c21 (diff) |
Merge bitcoin-core/gui#4: UI external signer support (e.g. hardware wallet)
1c4b456e1a0ccf0397d652f8c18201c3224c5c21 gui: send using external signer (Sjors Provoost)
24815c6309431cb0797defaf7add1150bcf4b567 gui: wallet creation detects external signer (Sjors Provoost)
3f845ea2994f53e29abeb3fa158c35f1ee56e7e8 node: add externalSigners to interface (Sjors Provoost)
62ac119f919ae1160ed67af796f24b78025fa8e3 gui: display address on external signer (Sjors Provoost)
450cb40a344605dda3bcc39495c35869580b9fc2 wallet: add displayAddress to interface (Sjors Provoost)
eef8d6452962cd4a8956d9ad268164715365b9ab gui: create wallet with external signer (Sjors Provoost)
6cdbc83e9341d1552faee4ccd8c190babc63e8d1 gui: add external signer path to options dialog (Sjors Provoost)
Pull request description:
Big picture overview in [this gist](https://gist.github.com/Sjors/29d06728c685e6182828c1ce9b74483d).
This PR adds GUI support for external signers, based on the since merged bitcoin/bitcoin#16546 (RPC).
The UX isn't amazing - especially the blocking calls - but it works.
First we adds a GUI setting for the signer script (e.g. path to HWI):
<img width="625" alt="Schermafbeelding 2019-08-05 om 19 32 59" src="https://user-images.githubusercontent.com/10217/62483415-e1ff1680-b7b7-11e9-97ca-8d2ce54ca1cb.png">
Then we add an external signer checkbox to the wallet creation dialog:
<img width="374" alt="Schermafbeelding 2019-11-07 om 19 17 23" src="https://user-images.githubusercontent.com/10217/68416387-b57ee000-0194-11ea-9730-127d60273008.png">
It's checked by default if HWI detects a device. It also grabs the name. It then creates a fresh wallet and imports the keys.
You can verify an address on the device (blocking...):
<img width="673" alt="Schermafbeelding 2019-08-05 om 19 29 22" src="https://user-images.githubusercontent.com/10217/62483560-43bf8080-b7b8-11e9-9902-8a036116dc4b.png">
Sending, including coin selection, Just Works(tm) as long the device is present.
~External signer support is enabled by default when the GUI is configured and Boost::Process is present.~
External signer support remains disabled by default, see https://github.com/bitcoin/bitcoin/pull/21935.
ACKs for top commit:
achow101:
Code Review ACK 1c4b456e1a0ccf0397d652f8c18201c3224c5c21
hebasto:
ACK 1c4b456e1a0ccf0397d652f8c18201c3224c5c21, tested on Linux Mint 20.1 (Qt 5.12.8) with HWW `2.0.2-rc.1`.
promag:
Tested ACK 1c4b456e1a0ccf0397d652f8c18201c3224c5c21 but rebased with e033ca1379, with HWI 2.0.2, with Nano S and Nano X.
meshcollider:
re-code-review ACK 1c4b456e1a0ccf0397d652f8c18201c3224c5c21
Tree-SHA512: 3503113c5c69d40adb6ce364d8e7cae23ce82d032a00474ba9aeb6202eb70f496ef4a6bf2e623e5171e524ad31ade7941a4e0e89539c64518aaec74f4562d86b
Diffstat (limited to 'src')
-rw-r--r-- | src/interfaces/node.h | 6 | ||||
-rw-r--r-- | src/interfaces/wallet.h | 6 | ||||
-rw-r--r-- | src/node/interfaces.cpp | 10 | ||||
-rw-r--r-- | src/qt/createwalletdialog.cpp | 66 | ||||
-rw-r--r-- | src/qt/createwalletdialog.h | 9 | ||||
-rw-r--r-- | src/qt/forms/createwalletdialog.ui | 11 | ||||
-rw-r--r-- | src/qt/forms/optionsdialog.ui | 30 | ||||
-rw-r--r-- | src/qt/forms/receiverequestdialog.ui | 13 | ||||
-rw-r--r-- | src/qt/optionsdialog.cpp | 2 | ||||
-rw-r--r-- | src/qt/optionsmodel.cpp | 15 | ||||
-rw-r--r-- | src/qt/optionsmodel.h | 1 | ||||
-rw-r--r-- | src/qt/receiverequestdialog.cpp | 6 | ||||
-rw-r--r-- | src/qt/sendcoinsdialog.cpp | 80 | ||||
-rw-r--r-- | src/qt/walletcontroller.cpp | 14 | ||||
-rw-r--r-- | src/qt/walletmodel.cpp | 12 | ||||
-rw-r--r-- | src/qt/walletmodel.h | 1 | ||||
-rw-r--r-- | src/qt/walletmodeltransaction.cpp | 5 | ||||
-rw-r--r-- | src/qt/walletmodeltransaction.h | 2 | ||||
-rw-r--r-- | src/wallet/interfaces.cpp | 6 |
19 files changed, 284 insertions, 11 deletions
diff --git a/src/interfaces/node.h b/src/interfaces/node.h index 1dd1e92e2f..35b6160cea 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -6,6 +6,7 @@ #define BITCOIN_INTERFACES_NODE_H #include <amount.h> // For CAmount +#include <external_signer.h> #include <net.h> // For NodeId #include <net_types.h> // For banmap_t #include <netaddress.h> // For Network @@ -110,6 +111,11 @@ public: //! Disconnect node by id. virtual bool disconnectById(NodeId id) = 0; +#ifdef ENABLE_EXTERNAL_SIGNER + //! List external signers + virtual std::vector<ExternalSigner> externalSigners() = 0; +#endif + //! Get total bytes recv. virtual int64_t getTotalBytesRecv() = 0; diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index 88f93321f9..a0cb2787b7 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -118,6 +118,9 @@ public: //! Save or remove receive request. virtual bool setAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& value) = 0; + //! Display address on external signer + virtual bool displayAddress(const CTxDestination& dest) = 0; + //! Lock coin. virtual void lockCoin(const COutPoint& output) = 0; @@ -252,6 +255,9 @@ public: // 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/node/interfaces.cpp b/src/node/interfaces.cpp index 8befbf5e30..171f15d4fb 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -170,6 +170,16 @@ public: } return false; } +#ifdef ENABLE_EXTERNAL_SIGNER + std::vector<ExternalSigner> externalSigners() override + { + std::vector<ExternalSigner> 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; } diff --git a/src/qt/createwalletdialog.cpp b/src/qt/createwalletdialog.cpp index 113bd30a0c..e593697b46 100644 --- a/src/qt/createwalletdialog.cpp +++ b/src/qt/createwalletdialog.cpp @@ -6,6 +6,7 @@ #include <config/bitcoin-config.h> #endif +#include <external_signer.h> #include <qt/createwalletdialog.h> #include <qt/forms/ui_createwalletdialog.h> @@ -27,14 +28,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 +89,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() @@ -75,6 +112,28 @@ CreateWalletDialog::~CreateWalletDialog() delete ui; } +#ifdef ENABLE_EXTERNAL_SIGNER +void CreateWalletDialog::setSigners(std::vector<ExternalSigner>& 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(); @@ -99,3 +158,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 20cce937c8..585b1461f7 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,11 +27,16 @@ public: explicit CreateWalletDialog(QWidget* parent); virtual ~CreateWalletDialog(); +#ifdef ENABLE_EXTERNAL_SIGNER + void setSigners(std::vector<ExternalSigner>& signers); +#endif + QString walletName() const; bool isEncryptWalletChecked() const; 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 881869a46c..b11fb026b0 100644 --- a/src/qt/forms/createwalletdialog.ui +++ b/src/qt/forms/createwalletdialog.ui @@ -109,6 +109,16 @@ </property> </widget> </item> + <item> + <widget class="QCheckBox" name="external_signer_checkbox"> + <property name="toolTip"> + <string>Use an external signing device such as a hardware wallet. Configure the external signer script in wallet preferences first.</string> + </property> + <property name="text"> + <string>External signer</string> + </property> + </widget> + </item> </layout> </widget> </item> @@ -143,6 +153,7 @@ <tabstop>disable_privkeys_checkbox</tabstop> <tabstop>blank_wallet_checkbox</tabstop> <tabstop>descriptor_checkbox</tabstop> + <tabstop>external_signer_checkbox</tabstop> </tabstops> <resources/> <connections> diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index f199e8c1a1..bd72328c02 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -230,6 +230,36 @@ </widget> </item> <item> + <widget class="QGroupBox" name="groupBoxHww"> + <property name="title"> + <string>External Signer (e.g. hardware wallet)</string> + </property> + <layout class="QVBoxLayout" name="verticalLayoutHww"> + <item> + <layout class="QHBoxLayout" name="horizontalLayoutHww"> + <item> + <widget class="QLabel" name="externalSignerPathLabel"> + <property name="text"> + <string>&External signer script path</string> + </property> + <property name="buddy"> + <cstring>externalSignerPath</cstring> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="externalSignerPath"> + <property name="toolTip"> + <string>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!</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> <spacer name="verticalSpacer_Wallet"> <property name="orientation"> <enum>Qt::Vertical</enum> diff --git a/src/qt/forms/receiverequestdialog.ui b/src/qt/forms/receiverequestdialog.ui index 7d95a8bc90..70a7cf71de 100644 --- a/src/qt/forms/receiverequestdialog.ui +++ b/src/qt/forms/receiverequestdialog.ui @@ -255,6 +255,19 @@ </widget> </item> <item> + <widget class="QPushButton" name="btnVerify"> + <property name="text"> + <string>&Verify</string> + </property> + <property name="toolTip"> + <string>Verify this address on e.g. a hardware wallet screen</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> <widget class="QPushButton" name="btnSaveAs"> <property name="text"> <string>&Save Imageā¦</string> diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 8a32994e3f..6ad8db4348 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<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning); connect(ui->databaseCache, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning); + connect(ui->externalSignerPath, &QLineEdit::textChanged, [this]{ showRestartWarning(); }); connect(ui->threadsScriptVerif, qOverload<int>(&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 abdf9e9ae6..24a4e9ee96 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 4d012a9b8f..535843e8ba 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -65,6 +65,7 @@ public: Prune, // bool PruneSize, // int DatabaseCache, // int + ExternalSignerPath, // QString SpendZeroConfChange, // bool Listen, // bool OptionIDRowCount, diff --git a/src/qt/receiverequestdialog.cpp b/src/qt/receiverequestdialog.cpp index 78ae5c07da..abe7de8f89 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/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 160b43324f..e87a2b97bc 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("<br /><span style='font-size:10pt;'>"); - 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<QMessageBox::StandardButton>(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/walletcontroller.cpp b/src/qt/walletcontroller.cpp index aa26a01541..7e5790fd87 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -263,6 +263,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<interfaces::Wallet> wallet = node().walletClient().createWallet(name, m_passphrase, flags, m_error_message, m_warning_message); @@ -291,6 +294,17 @@ void CreateWalletActivity::finish() void CreateWalletActivity::create() { m_create_wallet_dialog = new CreateWalletDialog(m_parent_widget); + +#ifdef ENABLE_EXTERNAL_SIGNER + std::vector<ExternalSigner> 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(); diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 7c58b8afd2..e32b7c2807 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -552,6 +552,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 b2ce5d69fb..47a21bcfcf 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -136,6 +136,7 @@ public: UnlockContext requestUnlock(); bool bumpFee(uint256 hash, uint256& new_hash); + bool displayAddress(std::string sAddress); static bool isWalletEnabled(); diff --git a/src/qt/walletmodeltransaction.cpp b/src/qt/walletmodeltransaction.cpp index 25172e774c..d185ddb7e8 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 f9a95362c8..120d240d91 100644 --- a/src/qt/walletmodeltransaction.h +++ b/src/qt/walletmodeltransaction.h @@ -27,6 +27,8 @@ public: QList<SendCoinsRecipient> getRecipients() const; CTransactionRef& getWtx(); + void setWtx(const CTransactionRef&); + unsigned int getTransactionSize(); void setTransactionFee(const CAmount& newFee); diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index aca52964ee..ee92316b89 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -206,6 +206,11 @@ public: WalletBatch batch{m_wallet->GetDatabase()}; return m_wallet->SetAddressReceiveRequest(batch, dest, id, value); } + 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); @@ -446,6 +451,7 @@ public: 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; } |