1 // Copyright (c) 2019-present 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 5 #include <qt/walletcontroller.h>
6 7 #include <qt/askpassphrasedialog.h>
8 #include <qt/clientmodel.h>
9 #include <qt/createwalletdialog.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 33 using util::Join;
34 using wallet::WALLET_FLAG_BLANK_WALLET;
35 using wallet::WALLET_FLAG_DESCRIPTORS;
36 using wallet::WALLET_FLAG_DISABLE_PRIVATE_KEYS;
37 using wallet::WALLET_FLAG_EXTERNAL_SIGNER;
38 39 WalletController::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 52 m_activity_worker->moveToThread(m_activity_thread);
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.
61 WalletController::~WalletController()
62 {
63 m_activity_thread->quit();
64 m_activity_thread->wait();
65 delete m_activity_worker;
66 }
67 68 std::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 82 void WalletController::removeWallet(WalletModel* wallet_model)
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 90 void 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 103 void WalletController::closeAllWallets(QWidget* parent)
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 117 WalletModel* 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);
144 }, GUIUtil::blockingGUIThreadConnection());
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 177 void WalletController::removeAndDeleteWallet(WalletModel* wallet_model)
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 190 WalletControllerActivity::WalletControllerActivity(WalletController* wallet_controller, QWidget* parent_widget)
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 198 void 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 217 CreateWalletActivity::CreateWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
218 : WalletControllerActivity(wallet_controller, parent_widget)
219 {
220 m_passphrase.reserve(MAX_PASSPHRASE_SIZE);
221 }
222 223 CreateWalletActivity::~CreateWalletActivity()
224 {
225 delete m_create_wallet_dialog;
226 delete m_passphrase_dialog;
227 }
228 229 void CreateWalletActivity::askPassphrase()
230 {
231 m_passphrase_dialog = new AskPassphraseDialog(AskPassphraseDialog::Encrypt, m_parent_widget, &m_passphrase);
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 246 void CreateWalletActivity::createWallet()
247 {
248 showProgressDialog(
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.
258 flags |= WALLET_FLAG_DESCRIPTORS;
259 if (m_create_wallet_dialog->isDisablePrivateKeysChecked()) {
260 flags |= WALLET_FLAG_DISABLE_PRIVATE_KEYS;
261 }
262 if (m_create_wallet_dialog->isMakeBlankWalletChecked()) {
263 flags |= WALLET_FLAG_BLANK_WALLET;
264 }
265 if (m_create_wallet_dialog->isExternalSignerChecked()) {
266 flags |= WALLET_FLAG_EXTERNAL_SIGNER;
267 }
268 269 QTimer::singleShot(500ms, worker(), [this, name, flags] {
270 auto wallet{node().walletLoader().createWallet(name, m_passphrase, flags, m_warning_message)};
271 272 if (wallet) {
273 m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(*wallet));
274 } else {
275 m_error_message = util::ErrorString(wallet);
276 }
277 278 QTimer::singleShot(500ms, this, &CreateWalletActivity::finish);
279 });
280 }
281 282 void CreateWalletActivity::finish()
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 290 if (m_wallet_model) Q_EMIT created(m_wallet_model);
291 292 Q_EMIT finished();
293 }
294 295 void CreateWalletActivity::create()
296 {
297 m_create_wallet_dialog = new CreateWalletDialog(m_parent_widget);
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 }
309 m_create_wallet_dialog->setSigners(signers);
310 311 m_create_wallet_dialog->setWindowModality(Qt::ApplicationModal);
312 m_create_wallet_dialog->show();
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] {
321 if (m_create_wallet_dialog->isEncryptWalletChecked()) {
322 askPassphrase();
323 } else {
324 createWallet();
325 }
326 });
327 }
328 329 OpenWalletActivity::OpenWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
330 : WalletControllerActivity(wallet_controller, parent_widget)
331 {
332 }
333 334 void OpenWalletActivity::finish()
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 342 if (m_wallet_model) Q_EMIT opened(m_wallet_model);
343 344 Q_EMIT finished();
345 }
346 347 void OpenWalletActivity::open(const std::string& path)
348 {
349 QString name = GUIUtil::WalletDisplayName(path);
350 351 showProgressDialog(
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] {
359 auto wallet{node().walletLoader().loadWallet(path, m_warning_message)};
360 361 if (wallet) {
362 m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(*wallet));
363 } else {
364 m_error_message = util::ErrorString(wallet);
365 }
366 367 QTimer::singleShot(0, this, &OpenWalletActivity::finish);
368 });
369 }
370 371 LoadWalletsActivity::LoadWalletsActivity(WalletController* wallet_controller, QWidget* parent_widget)
372 : WalletControllerActivity(wallet_controller, parent_widget)
373 {
374 }
375 376 void LoadWalletsActivity::load(bool show_loading_minimized)
377 {
378 showProgressDialog(
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()) {
388 m_wallet_controller->getOrCreateWallet(std::move(wallet));
389 }
390 391 QTimer::singleShot(0, this, [this] { Q_EMIT finished(); });
392 });
393 }
394 395 RestoreWalletActivity::RestoreWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
396 : WalletControllerActivity(wallet_controller, parent_widget)
397 {
398 }
399 400 void RestoreWalletActivity::restore(const fs::path& backup_file, const std::string& wallet_name)
401 {
402 QString name = QString::fromStdString(wallet_name);
403 404 showProgressDialog(
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, /*load_after_restore=*/true)};
413 414 if (wallet) {
415 m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(*wallet));
416 } else {
417 m_error_message = util::ErrorString(wallet);
418 }
419 420 QTimer::singleShot(0, this, &RestoreWalletActivity::finish);
421 });
422 }
423 424 void RestoreWalletActivity::finish()
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 437 if (m_wallet_model) Q_EMIT restored(m_wallet_model);
438 439 Q_EMIT finished();
440 }
441 442 void MigrateWalletActivity::do_migrate(const std::string& name)
443 {
444 SecureString passphrase;
445 if (node().walletLoader().isEncrypted(name)) {
446 // Get the passphrase for the wallet
447 AskPassphraseDialog dlg(AskPassphraseDialog::UnlockMigration, m_parent_widget, &passphrase);
448 if (dlg.exec() == QDialog::Rejected) return;
449 }
450 451 showProgressDialog(tr("Migrate Wallet"), tr("Migrating Wallet <b>%1</b>…").arg(GUIUtil::HtmlEscape(name)));
452 453 QTimer::singleShot(0, worker(), [this, name, passphrase] {
454 auto res{node().walletLoader().migrateWallet(name, passphrase)};
455 456 if (res) {
457 m_success_message = tr("The wallet '%1' was migrated successfully.").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(name)));
458 if (res->watchonly_wallet_name) {
459 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())));
460 }
461 if (res->solvables_wallet_name) {
462 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())));
463 }
464 m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(res->wallet));
465 } else {
466 m_error_message = util::ErrorString(res);
467 }
468 469 QTimer::singleShot(0, this, &MigrateWalletActivity::finish);
470 });
471 }
472 473 void MigrateWalletActivity::migrate(const std::string& name)
474 {
475 // Warn the user about migration
476 QMessageBox box(m_parent_widget);
477 box.setWindowTitle(tr("Migrate wallet"));
478 box.setText(tr("Are you sure you wish to migrate the wallet <i>%1</i>?").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(name))));
479 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"
480 "If this wallet contains any watchonly scripts, a new wallet will be created which contains those watchonly scripts.\n"
481 "If this wallet contains any solvable but not watched scripts, a different and new wallet will be created which contains those scripts.\n\n"
482 "The migration process will create a backup of the wallet before migrating. This backup file will be named "
483 "<wallet name>-<timestamp>.legacy.bak and can be found in the directory for this wallet. In the event of "
484 "an incorrect migration, the backup can be restored with the \"Restore Wallet\" functionality."));
485 box.setStandardButtons(QMessageBox::Yes|QMessageBox::Cancel);
486 box.setDefaultButton(QMessageBox::Yes);
487 if (box.exec() != QMessageBox::Yes) return;
488 489 do_migrate(name);
490 }
491 492 void MigrateWalletActivity::restore_and_migrate(const fs::path& path, const std::string& wallet_name)
493 {
494 // Warn the user about migration
495 QMessageBox box(m_parent_widget);
496 box.setWindowTitle(tr("Restore and Migrate wallet"));
497 box.setText(tr("Are you sure you wish to restore the wallet file <i>%1</i> to <i>%2</i> and migrate it?").arg(GUIUtil::HtmlEscape(fs::PathToString(path)), GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(wallet_name))));
498 box.setInformativeText(tr("Restoring the wallet will copy the backup file to the wallets directory and place it in the standard "
499 "wallet directory layout. The original file will not be modified.\n\n"
500 "Migrating the wallet will convert the restored wallet to one or more descriptor wallets. A new wallet backup will need to be made.\n"
501 "If this wallet contains any watchonly scripts, a new wallet will be created which contains those watchonly scripts.\n"
502 "If this wallet contains any solvable but not watched scripts, a different and new wallet will be created which contains those scripts.\n\n"
503 "The migration process will create a backup of the wallet before migrating. This backup file will be named "
504 "<wallet name>-<timestamp>.legacy.bak and can be found in the directory for this wallet. In the event of "
505 "an incorrect migration, the backup can be restored with the \"Restore Wallet\" functionality."));
506 box.setStandardButtons(QMessageBox::Yes|QMessageBox::Cancel);
507 box.setDefaultButton(QMessageBox::Yes);
508 if (box.exec() != QMessageBox::Yes) return;
509 510 showProgressDialog(
511 //: Title of progress window which is displayed when wallets are being restored.
512 tr("Restore Wallet"),
513 /*: Descriptive text of the restore wallets progress window which indicates to
514 the user that wallets are currently being restored.*/
515 tr("Restoring Wallet <b>%1</b>…").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(wallet_name))));
516 517 QTimer::singleShot(0, worker(), [this, path, wallet_name] {
518 auto res{node().walletLoader().restoreWallet(path, wallet_name, m_warning_message, /*load_after_restore=*/false)};
519 520 if (!res) {
521 m_error_message = util::ErrorString(res);
522 QTimer::singleShot(0, this, &MigrateWalletActivity::finish);
523 return;
524 }
525 QTimer::singleShot(0, this, [this, wallet_name] {
526 do_migrate(wallet_name);
527 });
528 });
529 }
530 531 void MigrateWalletActivity::finish()
532 {
533 if (!m_error_message.empty()) {
534 QMessageBox::critical(m_parent_widget, tr("Migration failed"), QString::fromStdString(m_error_message.translated));
535 } else {
536 QMessageBox::information(m_parent_widget, tr("Migration Successful"), m_success_message);
537 }
538 539 if (m_wallet_model) Q_EMIT migrated(m_wallet_model);
540 541 Q_EMIT finished();
542 }
543