Skip to content

Commit 68a89d7

Browse files
committed
Merge #4: UI external signer support (e.g. hardware wallet)
1c4b456 gui: send using external signer (Sjors Provoost) 24815c6 gui: wallet creation detects external signer (Sjors Provoost) 3f845ea node: add externalSigners to interface (Sjors Provoost) 62ac119 gui: display address on external signer (Sjors Provoost) 450cb40 wallet: add displayAddress to interface (Sjors Provoost) eef8d64 gui: create wallet with external signer (Sjors Provoost) 6cdbc83 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 bitcoin/bitcoin#21935. ACKs for top commit: achow101: Code Review ACK 1c4b456 hebasto: ACK 1c4b456, tested on Linux Mint 20.1 (Qt 5.12.8) with HWW `2.0.2-rc.1`. promag: Tested ACK 1c4b456 but rebased with e033ca1, with HWI 2.0.2, with Nano S and Nano X. meshcollider: re-code-review ACK 1c4b456 Tree-SHA512: 3503113c5c69d40adb6ce364d8e7cae23ce82d032a00474ba9aeb6202eb70f496ef4a6bf2e623e5171e524ad31ade7941a4e0e89539c64518aaec74f4562d86b
2 parents 7cac262 + 1c4b456 commit 68a89d7

19 files changed

+284
-11
lines changed

src/interfaces/node.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#define BITCOIN_INTERFACES_NODE_H
77

88
#include <amount.h> // For CAmount
9+
#include <external_signer.h>
910
#include <net.h> // For NodeId
1011
#include <net_types.h> // For banmap_t
1112
#include <netaddress.h> // For Network
@@ -110,6 +111,11 @@ class Node
110111
//! Disconnect node by id.
111112
virtual bool disconnectById(NodeId id) = 0;
112113

114+
#ifdef ENABLE_EXTERNAL_SIGNER
115+
//! List external signers
116+
virtual std::vector<ExternalSigner> externalSigners() = 0;
117+
#endif
118+
113119
//! Get total bytes recv.
114120
virtual int64_t getTotalBytesRecv() = 0;
115121

src/interfaces/wallet.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ class Wallet
118118
//! Save or remove receive request.
119119
virtual bool setAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& value) = 0;
120120

121+
//! Display address on external signer
122+
virtual bool displayAddress(const CTxDestination& dest) = 0;
123+
121124
//! Lock coin.
122125
virtual void lockCoin(const COutPoint& output) = 0;
123126

@@ -252,6 +255,9 @@ class Wallet
252255
// Return whether private keys enabled.
253256
virtual bool privateKeysDisabled() = 0;
254257

258+
// Return whether wallet uses an external signer.
259+
virtual bool hasExternalSigner() = 0;
260+
255261
// Get default address type.
256262
virtual OutputType getDefaultAddressType() = 0;
257263

src/node/interfaces.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,16 @@ class NodeImpl : public Node
170170
}
171171
return false;
172172
}
173+
#ifdef ENABLE_EXTERNAL_SIGNER
174+
std::vector<ExternalSigner> externalSigners() override
175+
{
176+
std::vector<ExternalSigner> signers = {};
177+
const std::string command = gArgs.GetArg("-signer", "");
178+
if (command == "") return signers;
179+
ExternalSigner::Enumerate(command, signers, Params().NetworkIDString());
180+
return signers;
181+
}
182+
#endif
173183
int64_t getTotalBytesRecv() override { return m_context->connman ? m_context->connman->GetTotalBytesRecv() : 0; }
174184
int64_t getTotalBytesSent() override { return m_context->connman ? m_context->connman->GetTotalBytesSent() : 0; }
175185
size_t getMempoolSize() override { return m_context->mempool ? m_context->mempool->size() : 0; }

src/qt/createwalletdialog.cpp

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <config/bitcoin-config.h>
77
#endif
88

9+
#include <external_signer.h>
910
#include <qt/createwalletdialog.h>
1011
#include <qt/forms/ui_createwalletdialog.h>
1112

@@ -27,14 +28,39 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) :
2728
});
2829

2930
connect(ui->encrypt_wallet_checkbox, &QCheckBox::toggled, [this](bool checked) {
30-
// Disable the disable_privkeys_checkbox when isEncryptWalletChecked is
31+
// Disable the disable_privkeys_checkbox and external_signer_checkbox when isEncryptWalletChecked is
3132
// set to true, enable it when isEncryptWalletChecked is false.
3233
ui->disable_privkeys_checkbox->setEnabled(!checked);
34+
ui->external_signer_checkbox->setEnabled(!checked);
3335

3436
// When the disable_privkeys_checkbox is disabled, uncheck it.
3537
if (!ui->disable_privkeys_checkbox->isEnabled()) {
3638
ui->disable_privkeys_checkbox->setChecked(false);
3739
}
40+
41+
// When the external_signer_checkbox box is disabled, uncheck it.
42+
if (!ui->external_signer_checkbox->isEnabled()) {
43+
ui->external_signer_checkbox->setChecked(false);
44+
}
45+
46+
});
47+
48+
connect(ui->external_signer_checkbox, &QCheckBox::toggled, [this](bool checked) {
49+
ui->encrypt_wallet_checkbox->setEnabled(!checked);
50+
ui->blank_wallet_checkbox->setEnabled(!checked);
51+
ui->disable_privkeys_checkbox->setEnabled(!checked);
52+
ui->descriptor_checkbox->setEnabled(!checked);
53+
54+
// The external signer checkbox is only enabled when a device is detected.
55+
// In that case it is checked by default. Toggling it restores the other
56+
// options to their default.
57+
ui->descriptor_checkbox->setChecked(checked);
58+
ui->encrypt_wallet_checkbox->setChecked(false);
59+
ui->disable_privkeys_checkbox->setChecked(checked);
60+
// The blank check box is ambiguous. This flag is always true for a
61+
// watch-only wallet, even though we immedidately fetch keys from the
62+
// external signer.
63+
ui->blank_wallet_checkbox->setChecked(checked);
3864
});
3965

4066
connect(ui->disable_privkeys_checkbox, &QCheckBox::toggled, [this](bool checked) {
@@ -63,18 +89,51 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) :
6389
ui->descriptor_checkbox->setToolTip(tr("Compiled without sqlite support (required for descriptor wallets)"));
6490
ui->descriptor_checkbox->setEnabled(false);
6591
ui->descriptor_checkbox->setChecked(false);
92+
ui->external_signer_checkbox->setEnabled(false);
93+
ui->external_signer_checkbox->setChecked(false);
6694
#endif
95+
6796
#ifndef USE_BDB
6897
ui->descriptor_checkbox->setEnabled(false);
6998
ui->descriptor_checkbox->setChecked(true);
7099
#endif
100+
101+
#ifndef ENABLE_EXTERNAL_SIGNER
102+
//: "External signing" means using devices such as hardware wallets.
103+
ui->external_signer_checkbox->setToolTip(tr("Compiled without external signing support (required for external signing)"));
104+
ui->external_signer_checkbox->setEnabled(false);
105+
ui->external_signer_checkbox->setChecked(false);
106+
#endif
107+
71108
}
72109

73110
CreateWalletDialog::~CreateWalletDialog()
74111
{
75112
delete ui;
76113
}
77114

115+
#ifdef ENABLE_EXTERNAL_SIGNER
116+
void CreateWalletDialog::setSigners(std::vector<ExternalSigner>& signers)
117+
{
118+
if (!signers.empty()) {
119+
ui->external_signer_checkbox->setEnabled(true);
120+
ui->external_signer_checkbox->setChecked(true);
121+
ui->encrypt_wallet_checkbox->setEnabled(false);
122+
ui->encrypt_wallet_checkbox->setChecked(false);
123+
// The order matters, because connect() is called when toggling a checkbox:
124+
ui->blank_wallet_checkbox->setEnabled(false);
125+
ui->blank_wallet_checkbox->setChecked(false);
126+
ui->disable_privkeys_checkbox->setEnabled(false);
127+
ui->disable_privkeys_checkbox->setChecked(true);
128+
const std::string label = signers[0].m_name;
129+
ui->wallet_name_line_edit->setText(QString::fromStdString(label));
130+
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
131+
} else {
132+
ui->external_signer_checkbox->setEnabled(false);
133+
}
134+
}
135+
#endif
136+
78137
QString CreateWalletDialog::walletName() const
79138
{
80139
return ui->wallet_name_line_edit->text();
@@ -99,3 +158,8 @@ bool CreateWalletDialog::isDescriptorWalletChecked() const
99158
{
100159
return ui->descriptor_checkbox->isChecked();
101160
}
161+
162+
bool CreateWalletDialog::isExternalSignerChecked() const
163+
{
164+
return ui->external_signer_checkbox->isChecked();
165+
}

src/qt/createwalletdialog.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
class WalletModel;
1111

12+
#ifdef ENABLE_EXTERNAL_SIGNER
13+
class ExternalSigner;
14+
#endif
15+
1216
namespace Ui {
1317
class CreateWalletDialog;
1418
}
@@ -23,11 +27,16 @@ class CreateWalletDialog : public QDialog
2327
explicit CreateWalletDialog(QWidget* parent);
2428
virtual ~CreateWalletDialog();
2529

30+
#ifdef ENABLE_EXTERNAL_SIGNER
31+
void setSigners(std::vector<ExternalSigner>& signers);
32+
#endif
33+
2634
QString walletName() const;
2735
bool isEncryptWalletChecked() const;
2836
bool isDisablePrivateKeysChecked() const;
2937
bool isMakeBlankWalletChecked() const;
3038
bool isDescriptorWalletChecked() const;
39+
bool isExternalSignerChecked() const;
3140

3241
private:
3342
Ui::CreateWalletDialog *ui;

src/qt/forms/createwalletdialog.ui

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@
109109
</property>
110110
</widget>
111111
</item>
112+
<item>
113+
<widget class="QCheckBox" name="external_signer_checkbox">
114+
<property name="toolTip">
115+
<string>Use an external signing device such as a hardware wallet. Configure the external signer script in wallet preferences first.</string>
116+
</property>
117+
<property name="text">
118+
<string>External signer</string>
119+
</property>
120+
</widget>
121+
</item>
112122
</layout>
113123
</widget>
114124
</item>
@@ -143,6 +153,7 @@
143153
<tabstop>disable_privkeys_checkbox</tabstop>
144154
<tabstop>blank_wallet_checkbox</tabstop>
145155
<tabstop>descriptor_checkbox</tabstop>
156+
<tabstop>external_signer_checkbox</tabstop>
146157
</tabstops>
147158
<resources/>
148159
<connections>

src/qt/forms/optionsdialog.ui

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,36 @@
229229
</layout>
230230
</widget>
231231
</item>
232+
<item>
233+
<widget class="QGroupBox" name="groupBoxHww">
234+
<property name="title">
235+
<string>External Signer (e.g. hardware wallet)</string>
236+
</property>
237+
<layout class="QVBoxLayout" name="verticalLayoutHww">
238+
<item>
239+
<layout class="QHBoxLayout" name="horizontalLayoutHww">
240+
<item>
241+
<widget class="QLabel" name="externalSignerPathLabel">
242+
<property name="text">
243+
<string>&amp;External signer script path</string>
244+
</property>
245+
<property name="buddy">
246+
<cstring>externalSignerPath</cstring>
247+
</property>
248+
</widget>
249+
</item>
250+
<item>
251+
<widget class="QLineEdit" name="externalSignerPath">
252+
<property name="toolTip">
253+
<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>
254+
</property>
255+
</widget>
256+
</item>
257+
</layout>
258+
</item>
259+
</layout>
260+
</widget>
261+
</item>
232262
<item>
233263
<spacer name="verticalSpacer_Wallet">
234264
<property name="orientation">

src/qt/forms/receiverequestdialog.ui

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,19 @@
254254
</property>
255255
</widget>
256256
</item>
257+
<item>
258+
<widget class="QPushButton" name="btnVerify">
259+
<property name="text">
260+
<string>&amp;Verify</string>
261+
</property>
262+
<property name="toolTip">
263+
<string>Verify this address on e.g. a hardware wallet screen</string>
264+
</property>
265+
<property name="autoDefault">
266+
<bool>false</bool>
267+
</property>
268+
</widget>
269+
</item>
257270
<item>
258271
<widget class="QPushButton" name="btnSaveAs">
259272
<property name="text">

src/qt/optionsdialog.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ void OptionsDialog::setModel(OptionsModel *_model)
199199
connect(ui->prune, &QCheckBox::clicked, this, &OptionsDialog::togglePruneWarning);
200200
connect(ui->pruneSize, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning);
201201
connect(ui->databaseCache, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning);
202+
connect(ui->externalSignerPath, &QLineEdit::textChanged, [this]{ showRestartWarning(); });
202203
connect(ui->threadsScriptVerif, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning);
203204
/* Wallet */
204205
connect(ui->spendZeroConfChange, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
@@ -233,6 +234,7 @@ void OptionsDialog::setMapper()
233234
/* Wallet */
234235
mapper->addMapping(ui->spendZeroConfChange, OptionsModel::SpendZeroConfChange);
235236
mapper->addMapping(ui->coinControlFeatures, OptionsModel::CoinControlFeatures);
237+
mapper->addMapping(ui->externalSignerPath, OptionsModel::ExternalSignerPath);
236238

237239
/* Network */
238240
mapper->addMapping(ui->mapPortUpnp, OptionsModel::MapPortUPnP);

src/qt/optionsmodel.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ void OptionsModel::Init(bool resetSettings)
117117
settings.setValue("bSpendZeroConfChange", true);
118118
if (!gArgs.SoftSetBoolArg("-spendzeroconfchange", settings.value("bSpendZeroConfChange").toBool()))
119119
addOverriddenOption("-spendzeroconfchange");
120+
121+
if (!settings.contains("external_signer_path"))
122+
settings.setValue("external_signer_path", "");
123+
124+
if (!gArgs.SoftSetArg("-signer", settings.value("external_signer_path").toString().toStdString())) {
125+
addOverriddenOption("-signer");
126+
}
120127
#endif
121128

122129
// Network
@@ -326,6 +333,8 @@ QVariant OptionsModel::data(const QModelIndex & index, int role) const
326333
#ifdef ENABLE_WALLET
327334
case SpendZeroConfChange:
328335
return settings.value("bSpendZeroConfChange");
336+
case ExternalSignerPath:
337+
return settings.value("external_signer_path");
329338
#endif
330339
case DisplayUnit:
331340
return nDisplayUnit;
@@ -445,6 +454,12 @@ bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, in
445454
setRestartRequired(true);
446455
}
447456
break;
457+
case ExternalSignerPath:
458+
if (settings.value("external_signer_path") != value.toString()) {
459+
settings.setValue("external_signer_path", value.toString());
460+
setRestartRequired(true);
461+
}
462+
break;
448463
#endif
449464
case DisplayUnit:
450465
setDisplayUnit(value);

src/qt/optionsmodel.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class OptionsModel : public QAbstractListModel
6565
Prune, // bool
6666
PruneSize, // int
6767
DatabaseCache, // int
68+
ExternalSignerPath, // QString
6869
SpendZeroConfChange, // bool
6970
Listen, // bool
7071
OptionIDRowCount,

src/qt/receiverequestdialog.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ void ReceiveRequestDialog::setInfo(const SendCoinsRecipient &_info)
8989
ui->wallet_tag->hide();
9090
ui->wallet_content->hide();
9191
}
92+
93+
ui->btnVerify->setVisible(this->model->wallet().hasExternalSigner());
94+
95+
connect(ui->btnVerify, &QPushButton::clicked, [this] {
96+
model->displayAddress(info.address.toStdString());
97+
});
9298
}
9399

94100
void ReceiveRequestDialog::updateDisplayUnit()

0 commit comments

Comments
 (0)