Bitcoin Core 28.99.0
P2P Digital Currency
walletcontroller.cpp
Go to the documentation of this file.
1// Copyright (c) 2019-2022 The Bitcoin Core developers
2// Distributed under the MIT software license, see the accompanying
3// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
6
8#include <qt/clientmodel.h>
10#include <qt/guiconstants.h>
11#include <qt/guiutil.h>
12#include <qt/walletmodel.h>
13
14#include <external_signer.h>
15#include <interfaces/handler.h>
16#include <interfaces/node.h>
17#include <util/string.h>
18#include <util/threadnames.h>
19#include <util/translation.h>
20#include <wallet/wallet.h>
21
22#include <algorithm>
23#include <chrono>
24
25#include <QApplication>
26#include <QMessageBox>
27#include <QMetaObject>
28#include <QMutexLocker>
29#include <QThread>
30#include <QTimer>
31#include <QWindow>
32
33using util::Join;
38
39WalletController::WalletController(ClientModel& client_model, const PlatformStyle* platform_style, QObject* parent)
40 : QObject(parent)
41 , m_activity_thread(new QThread(this))
42 , m_activity_worker(new QObject)
43 , m_client_model(client_model)
44 , m_node(client_model.node())
45 , m_platform_style(platform_style)
46 , m_options_model(client_model.getOptionsModel())
47{
48 m_handler_load_wallet = m_node.walletLoader().handleLoadWallet([this](std::unique_ptr<interfaces::Wallet> wallet) {
49 getOrCreateWallet(std::move(wallet));
50 });
51
53 m_activity_thread->start();
54 QTimer::singleShot(0, m_activity_worker, []() {
55 util::ThreadRename("qt-walletctrl");
56 });
57}
58
59// Not using the default destructor because not all member types definitions are
60// available in the header, just forward declared.
62{
63 m_activity_thread->quit();
64 m_activity_thread->wait();
65 delete m_activity_worker;
66}
67
68std::map<std::string, std::pair<bool, std::string>> WalletController::listWalletDir() const
69{
70 QMutexLocker locker(&m_mutex);
71 std::map<std::string, std::pair<bool, std::string>> wallets;
72 for (const auto& [name, format] : m_node.walletLoader().listWalletDir()) {
73 wallets[name] = std::make_pair(false, format);
74 }
75 for (WalletModel* wallet_model : m_wallets) {
76 auto it = wallets.find(wallet_model->wallet().getWalletName());
77 if (it != wallets.end()) it->second.first = true;
78 }
79 return wallets;
80}
81
83{
84 // Once the wallet is successfully removed from the node, the model will emit the 'WalletModel::unload' signal.
85 // This signal is already connected and will complete the removal of the view from the GUI.
86 // Look at 'WalletController::getOrCreateWallet' for the signal connection.
87 wallet_model->wallet().remove();
88}
89
90void WalletController::closeWallet(WalletModel* wallet_model, QWidget* parent)
91{
92 QMessageBox box(parent);
93 box.setWindowTitle(tr("Close wallet"));
94 box.setText(tr("Are you sure you wish to close the wallet <i>%1</i>?").arg(GUIUtil::HtmlEscape(wallet_model->getDisplayName())));
95 box.setInformativeText(tr("Closing the wallet for too long can result in having to resync the entire chain if pruning is enabled."));
96 box.setStandardButtons(QMessageBox::Yes|QMessageBox::Cancel);
97 box.setDefaultButton(QMessageBox::Yes);
98 if (box.exec() != QMessageBox::Yes) return;
99
100 removeWallet(wallet_model);
101}
102
104{
105 QMessageBox::StandardButton button = QMessageBox::question(parent, tr("Close all wallets"),
106 tr("Are you sure you wish to close all wallets?"),
107 QMessageBox::Yes|QMessageBox::Cancel,
108 QMessageBox::Yes);
109 if (button != QMessageBox::Yes) return;
110
111 QMutexLocker locker(&m_mutex);
112 for (WalletModel* wallet_model : m_wallets) {
113 removeWallet(wallet_model);
114 }
115}
116
117WalletModel* WalletController::getOrCreateWallet(std::unique_ptr<interfaces::Wallet> wallet)
118{
119 QMutexLocker locker(&m_mutex);
120
121 // Return model instance if exists.
122 if (!m_wallets.empty()) {
123 std::string name = wallet->getWalletName();
124 for (WalletModel* wallet_model : m_wallets) {
125 if (wallet_model->wallet().getWalletName() == name) {
126 return wallet_model;
127 }
128 }
129 }
130
131 // Instantiate model and register it.
132 WalletModel* wallet_model = new WalletModel(std::move(wallet), m_client_model, m_platform_style,
133 nullptr /* required for the following moveToThread() call */);
134
135 // Move WalletModel object to the thread that created the WalletController
136 // object (GUI main thread), instead of the current thread, which could be
137 // an outside wallet thread or RPC thread sending a LoadWallet notification.
138 // This ensures queued signals sent to the WalletModel object will be
139 // handled on the GUI event loop.
140 wallet_model->moveToThread(thread());
141 // setParent(parent) must be called in the thread which created the parent object. More details in #18948.
142 QMetaObject::invokeMethod(this, [wallet_model, this] {
143 wallet_model->setParent(this);
145
146 m_wallets.push_back(wallet_model);
147
148 // WalletModel::startPollBalance needs to be called in a thread managed by
149 // Qt because of startTimer. Considering the current thread can be a RPC
150 // thread, better delegate the calling to Qt with Qt::AutoConnection.
151 const bool called = QMetaObject::invokeMethod(wallet_model, "startPollBalance");
152 assert(called);
153
154 connect(wallet_model, &WalletModel::unload, this, [this, wallet_model] {
155 // Defer removeAndDeleteWallet when no modal widget is actively waiting for an action.
156 // TODO: remove this workaround by removing usage of QDialog::exec.
157 QWidget* active_dialog = QApplication::activeModalWidget();
158 if (active_dialog && dynamic_cast<QProgressDialog*>(active_dialog) == nullptr) {
159 connect(qApp, &QApplication::focusWindowChanged, wallet_model, [this, wallet_model]() {
160 if (!QApplication::activeModalWidget()) {
161 removeAndDeleteWallet(wallet_model);
162 }
163 }, Qt::QueuedConnection);
164 } else {
165 removeAndDeleteWallet(wallet_model);
166 }
167 }, Qt::QueuedConnection);
168
169 // Re-emit coinsSent signal from wallet model.
170 connect(wallet_model, &WalletModel::coinsSent, this, &WalletController::coinsSent);
171
172 Q_EMIT walletAdded(wallet_model);
173
174 return wallet_model;
175}
176
178{
179 // Unregister wallet model.
180 {
181 QMutexLocker locker(&m_mutex);
182 m_wallets.erase(std::remove(m_wallets.begin(), m_wallets.end(), wallet_model));
183 }
184 Q_EMIT walletRemoved(wallet_model);
185 // Currently this can trigger the unload since the model can hold the last
186 // CWallet shared pointer.
187 delete wallet_model;
188}
189
191 : QObject(wallet_controller)
192 , m_wallet_controller(wallet_controller)
193 , m_parent_widget(parent_widget)
194{
195 connect(this, &WalletControllerActivity::finished, this, &QObject::deleteLater);
196}
197
198void WalletControllerActivity::showProgressDialog(const QString& title_text, const QString& label_text, bool show_minimized)
199{
200 auto progress_dialog = new QProgressDialog(m_parent_widget);
201 progress_dialog->setAttribute(Qt::WA_DeleteOnClose);
202 connect(this, &WalletControllerActivity::finished, progress_dialog, &QWidget::close);
203
204 progress_dialog->setWindowTitle(title_text);
205 progress_dialog->setLabelText(label_text);
206 progress_dialog->setRange(0, 0);
207 progress_dialog->setCancelButton(nullptr);
208 progress_dialog->setWindowModality(Qt::ApplicationModal);
209 GUIUtil::PolishProgressDialog(progress_dialog);
210 // The setValue call forces QProgressDialog to start the internal duration estimation.
211 // See details in https://bugreports.qt.io/browse/QTBUG-47042.
212 progress_dialog->setValue(0);
213 // When requested, launch dialog minimized
214 if (show_minimized) progress_dialog->showMinimized();
215}
216
217CreateWalletActivity::CreateWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
218 : WalletControllerActivity(wallet_controller, parent_widget)
219{
221}
222
224{
226 delete m_passphrase_dialog;
227}
228
230{
232 m_passphrase_dialog->setWindowModality(Qt::ApplicationModal);
233 m_passphrase_dialog->show();
234
235 connect(m_passphrase_dialog, &QObject::destroyed, [this] {
236 m_passphrase_dialog = nullptr;
237 });
238 connect(m_passphrase_dialog, &QDialog::accepted, [this] {
239 createWallet();
240 });
241 connect(m_passphrase_dialog, &QDialog::rejected, [this] {
242 Q_EMIT finished();
243 });
244}
245
247{
249 //: Title of window indicating the progress of creation of a new wallet.
250 tr("Create Wallet"),
251 /*: Descriptive text of the create wallet progress window which indicates
252 to the user which wallet is currently being created. */
253 tr("Creating Wallet <b>%1</b>…").arg(m_create_wallet_dialog->walletName().toHtmlEscaped()));
254
255 std::string name = m_create_wallet_dialog->walletName().toStdString();
256 uint64_t flags = 0;
257 // Enable descriptors by default.
261 }
264 }
267 }
268
269 QTimer::singleShot(500ms, worker(), [this, name, flags] {
271
272 if (wallet) {
274 } else {
276 }
277
278 QTimer::singleShot(500ms, this, &CreateWalletActivity::finish);
279 });
280}
281
283{
284 if (!m_error_message.empty()) {
285 QMessageBox::critical(m_parent_widget, tr("Create wallet failed"), QString::fromStdString(m_error_message.translated));
286 } else if (!m_warning_message.empty()) {
287 QMessageBox::warning(m_parent_widget, tr("Create wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
288 }
289
291
292 Q_EMIT finished();
293}
294
296{
298
299 std::vector<std::unique_ptr<interfaces::ExternalSigner>> signers;
300 try {
301 signers = node().listExternalSigners();
302 } catch (const std::runtime_error& e) {
303 QMessageBox::critical(nullptr, tr("Can't list signers"), e.what());
304 }
305 if (signers.size() > 1) {
306 QMessageBox::critical(nullptr, tr("Too many external signers found"), QString::fromStdString("More than one external signer found. Please connect only one at a time."));
307 signers.clear();
308 }
310
311 m_create_wallet_dialog->setWindowModality(Qt::ApplicationModal);
313
314 connect(m_create_wallet_dialog, &QObject::destroyed, [this] {
315 m_create_wallet_dialog = nullptr;
316 });
317 connect(m_create_wallet_dialog, &QDialog::rejected, [this] {
318 Q_EMIT finished();
319 });
320 connect(m_create_wallet_dialog, &QDialog::accepted, [this] {
322 askPassphrase();
323 } else {
324 createWallet();
325 }
326 });
327}
328
329OpenWalletActivity::OpenWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
330 : WalletControllerActivity(wallet_controller, parent_widget)
331{
332}
333
335{
336 if (!m_error_message.empty()) {
337 QMessageBox::critical(m_parent_widget, tr("Open wallet failed"), QString::fromStdString(m_error_message.translated));
338 } else if (!m_warning_message.empty()) {
339 QMessageBox::warning(m_parent_widget, tr("Open wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
340 }
341
343
344 Q_EMIT finished();
345}
346
347void OpenWalletActivity::open(const std::string& path)
348{
349 QString name = GUIUtil::WalletDisplayName(path);
350
352 //: Title of window indicating the progress of opening of a wallet.
353 tr("Open Wallet"),
354 /*: Descriptive text of the open wallet progress window which indicates
355 to the user which wallet is currently being opened. */
356 tr("Opening Wallet <b>%1</b>…").arg(name.toHtmlEscaped()));
357
358 QTimer::singleShot(0, worker(), [this, path] {
360
361 if (wallet) {
363 } else {
365 }
366
367 QTimer::singleShot(0, this, &OpenWalletActivity::finish);
368 });
369}
370
371LoadWalletsActivity::LoadWalletsActivity(WalletController* wallet_controller, QWidget* parent_widget)
372 : WalletControllerActivity(wallet_controller, parent_widget)
373{
374}
375
376void LoadWalletsActivity::load(bool show_loading_minimized)
377{
379 //: Title of progress window which is displayed when wallets are being loaded.
380 tr("Load Wallets"),
381 /*: Descriptive text of the load wallets progress window which indicates to
382 the user that wallets are currently being loaded.*/
383 tr("Loading wallets…"),
384 /*show_minimized=*/show_loading_minimized);
385
386 QTimer::singleShot(0, worker(), [this] {
387 for (auto& wallet : node().walletLoader().getWallets()) {
389 }
390
391 QTimer::singleShot(0, this, [this] { Q_EMIT finished(); });
392 });
393}
394
395RestoreWalletActivity::RestoreWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
396 : WalletControllerActivity(wallet_controller, parent_widget)
397{
398}
399
400void RestoreWalletActivity::restore(const fs::path& backup_file, const std::string& wallet_name)
401{
402 QString name = QString::fromStdString(wallet_name);
403
405 //: Title of progress window which is displayed when wallets are being restored.
406 tr("Restore Wallet"),
407 /*: Descriptive text of the restore wallets progress window which indicates to
408 the user that wallets are currently being restored.*/
409 tr("Restoring Wallet <b>%1</b>…").arg(name.toHtmlEscaped()));
410
411 QTimer::singleShot(0, worker(), [this, backup_file, wallet_name] {
412 auto wallet{node().walletLoader().restoreWallet(backup_file, wallet_name, m_warning_message)};
413
414 if (wallet) {
416 } else {
418 }
419
420 QTimer::singleShot(0, this, &RestoreWalletActivity::finish);
421 });
422}
423
425{
426 if (!m_error_message.empty()) {
427 //: Title of message box which is displayed when the wallet could not be restored.
428 QMessageBox::critical(m_parent_widget, tr("Restore wallet failed"), QString::fromStdString(m_error_message.translated));
429 } else if (!m_warning_message.empty()) {
430 //: Title of message box which is displayed when the wallet is restored with some warning.
431 QMessageBox::warning(m_parent_widget, tr("Restore wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
432 } else {
433 //: Title of message box which is displayed when the wallet is successfully restored.
434 QMessageBox::information(m_parent_widget, tr("Restore wallet message"), QString::fromStdString(Untranslated("Wallet restored successfully \n").translated));
435 }
436
438
439 Q_EMIT finished();
440}
441
442void MigrateWalletActivity::migrate(const std::string& name)
443{
444 // Warn the user about migration
445 QMessageBox box(m_parent_widget);
446 box.setWindowTitle(tr("Migrate wallet"));
447 box.setText(tr("Are you sure you wish to migrate the wallet <i>%1</i>?").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(name))));
448 box.setInformativeText(tr("Migrating the wallet will convert this wallet to one or more descriptor wallets. A new wallet backup will need to be made.\n"
449 "If this wallet contains any watchonly scripts, a new wallet will be created which contains those watchonly scripts.\n"
450 "If this wallet contains any solvable but not watched scripts, a different and new wallet will be created which contains those scripts.\n\n"
451 "The migration process will create a backup of the wallet before migrating. This backup file will be named "
452 "<wallet name>-<timestamp>.legacy.bak and can be found in the directory for this wallet. In the event of "
453 "an incorrect migration, the backup can be restored with the \"Restore Wallet\" functionality."));
454 box.setStandardButtons(QMessageBox::Yes|QMessageBox::Cancel);
455 box.setDefaultButton(QMessageBox::Yes);
456 if (box.exec() != QMessageBox::Yes) return;
457
458 SecureString passphrase;
459 if (node().walletLoader().isEncrypted(name)) {
460 // Get the passphrase for the wallet
462 if (dlg.exec() == QDialog::Rejected) return;
463 }
464
465 showProgressDialog(tr("Migrate Wallet"), tr("Migrating Wallet <b>%1</b>…").arg(GUIUtil::HtmlEscape(name)));
466
467 QTimer::singleShot(0, worker(), [this, name, passphrase] {
468 auto res{node().walletLoader().migrateWallet(name, passphrase)};
469
470 if (res) {
471 m_success_message = tr("The wallet '%1' was migrated successfully.").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(res->wallet->getWalletName())));
472 if (res->watchonly_wallet_name) {
473 m_success_message += QChar(' ') + tr("Watchonly scripts have been migrated to a new wallet named '%1'.").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(res->watchonly_wallet_name.value())));
474 }
475 if (res->solvables_wallet_name) {
476 m_success_message += QChar(' ') + tr("Solvable but not watched scripts have been migrated to a new wallet named '%1'.").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(res->solvables_wallet_name.value())));
477 }
478 m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(res->wallet));
479 } else {
481 }
482
483 QTimer::singleShot(0, this, &MigrateWalletActivity::finish);
484 });
485}
486
488{
489 if (!m_error_message.empty()) {
490 QMessageBox::critical(m_parent_widget, tr("Migration failed"), QString::fromStdString(m_error_message.translated));
491 } else {
492 QMessageBox::information(m_parent_widget, tr("Migration Successful"), m_success_message);
493 }
494
496
497 Q_EMIT finished();
498}
node::NodeContext m_node
Definition: bitcoin-gui.cpp:42
int flags
Definition: bitcoin-tx.cpp:536
Multifunctional dialog to ask for passphrases.
@ Encrypt
Ask passphrase twice and encrypt.
@ UnlockMigration
Ask passphrase for unlocking during migration.
Model for Bitcoin network client.
Definition: clientmodel.h:57
AskPassphraseDialog * m_passphrase_dialog
CreateWalletDialog * m_create_wallet_dialog
CreateWalletActivity(WalletController *wallet_controller, QWidget *parent_widget)
void created(WalletModel *wallet_model)
Dialog for creating wallets.
bool isMakeBlankWalletChecked() const
QString walletName() const
bool isDisablePrivateKeysChecked() const
bool isEncryptWalletChecked() const
void setSigners(const std::vector< std::unique_ptr< interfaces::ExternalSigner > > &signers)
bool isExternalSignerChecked() const
void load(bool show_loading_minimized)
LoadWalletsActivity(WalletController *wallet_controller, QWidget *parent_widget)
void migrated(WalletModel *wallet_model)
void migrate(const std::string &path)
void opened(WalletModel *wallet_model)
OpenWalletActivity(WalletController *wallet_controller, QWidget *parent_widget)
void open(const std::string &path)
void restore(const fs::path &backup_file, const std::string &wallet_name)
void restored(WalletModel *wallet_model)
RestoreWalletActivity(WalletController *wallet_controller, QWidget *parent_widget)
std::vector< bilingual_str > m_warning_message
WalletController *const m_wallet_controller
interfaces::Node & node() const
QObject * worker() const
void showProgressDialog(const QString &title_text, const QString &label_text, bool show_minimized=false)
WalletControllerActivity(WalletController *wallet_controller, QWidget *parent_widget)
QWidget *const m_parent_widget
Controller between interfaces::Node, WalletModel instances and the GUI.
WalletController(ClientModel &client_model, const PlatformStyle *platform_style, QObject *parent)
WalletModel * getOrCreateWallet(std::unique_ptr< interfaces::Wallet > wallet)
ClientModel & m_client_model
void removeAndDeleteWallet(WalletModel *wallet_model)
void walletAdded(WalletModel *wallet_model)
void closeAllWallets(QWidget *parent=nullptr)
std::unique_ptr< interfaces::Handler > m_handler_load_wallet
QThread *const m_activity_thread
void coinsSent(WalletModel *wallet_model, SendCoinsRecipient recipient, QByteArray transaction)
QObject *const m_activity_worker
void walletRemoved(WalletModel *wallet_model)
const PlatformStyle *const m_platform_style
interfaces::Node & m_node
void closeWallet(WalletModel *wallet_model, QWidget *parent=nullptr)
std::map< std::string, std::pair< bool, std::string > > listWalletDir() const
Returns all wallet names in the wallet dir mapped to whether the wallet is loaded.
void removeWallet(WalletModel *wallet_model)
Starts the wallet closure procedure.
std::vector< WalletModel * > m_wallets
Interface to Bitcoin wallet from Qt view code.
Definition: walletmodel.h:48
interfaces::Wallet & wallet() const
Definition: walletmodel.h:138
void coinsSent(WalletModel *wallet, SendCoinsRecipient recipient, QByteArray transaction)
QString getDisplayName() const
void unload()
Path class wrapper to block calls to the fs::path(std::string) implicit constructor and the fs::path:...
Definition: fs.h:33
virtual WalletLoader & walletLoader()=0
Get wallet loader.
virtual std::vector< std::unique_ptr< ExternalSigner > > listExternalSigners()=0
Return list of external signers (attached devices which can sign transactions).
virtual void remove()=0
virtual util::Result< std::unique_ptr< Wallet > > restoreWallet(const fs::path &backup_file, const std::string &wallet_name, std::vector< bilingual_str > &warnings)=0
Restore backup wallet.
virtual util::Result< std::unique_ptr< Wallet > > createWallet(const std::string &name, const SecureString &passphrase, uint64_t wallet_creation_flags, std::vector< bilingual_str > &warnings)=0
Create new wallet.
virtual util::Result< WalletMigrationResult > migrateWallet(const std::string &name, const SecureString &passphrase)=0
Migrate a wallet.
virtual std::vector< std::pair< std::string, std::string > > listWalletDir()=0
Return available wallets in wallet directory.
virtual util::Result< std::unique_ptr< Wallet > > loadWallet(const std::string &name, std::vector< bilingual_str > &warnings)=0
Load existing wallet.
virtual std::unique_ptr< Handler > handleLoadWallet(LoadWalletFn fn)=0
static const int MAX_PASSPHRASE_SIZE
Definition: guiconstants.h:20
Qt::ConnectionType blockingGUIThreadConnection()
Get connection type to call object slot in GUI thread with invokeMethod.
Definition: guiutil.cpp:378
QString WalletDisplayName(const QString &name)
Definition: guiutil.cpp:1011
QString HtmlEscape(const QString &str, bool fMultiLine)
Definition: guiutil.cpp:249
void PolishProgressDialog(QProgressDialog *dialog)
Definition: guiutil.cpp:901
Definition: messages.h:20
void format(std::ostream &out, FormatStringCheck< sizeof...(Args)> fmt, const Args &... args)
Format list of arguments to the stream according to given format string.
Definition: tinyformat.h:1072
void ThreadRename(const std::string &)
Rename a thread both in terms of an internal (in-memory) name as well as its system thread name.
Definition: threadnames.cpp:57
bilingual_str ErrorString(const Result< T > &result)
Definition: result.h:93
auto Join(const C &container, const S &separator, UnaryOp unary_op)
Join all container items.
Definition: string.h:192
@ WALLET_FLAG_EXTERNAL_SIGNER
Indicates that the wallet needs an external signer.
Definition: walletutil.h:77
@ WALLET_FLAG_DESCRIPTORS
Indicate that this wallet supports DescriptorScriptPubKeyMan.
Definition: walletutil.h:74
@ WALLET_FLAG_DISABLE_PRIVATE_KEYS
Definition: walletutil.h:51
@ WALLET_FLAG_BLANK_WALLET
Flag set when a wallet contains no HD seed and no private keys, scripts, addresses,...
Definition: walletutil.h:71
const char * name
Definition: rest.cpp:49
std::basic_string< char, std::char_traits< char >, secure_allocator< char > > SecureString
Definition: secure.h:58
bool empty() const
Definition: translation.h:32
std::string translated
Definition: translation.h:23
bilingual_str Untranslated(std::string original)
Mark a bilingual_str as untranslated.
Definition: translation.h:51
assert(!tx.IsCoinBase())