/ src / qt / walletcontroller.cpp
walletcontroller.cpp
  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  
  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 wallet::WALLET_FLAG_BLANK_WALLET;
 34  using wallet::WALLET_FLAG_DESCRIPTORS;
 35  using wallet::WALLET_FLAG_DISABLE_PRIVATE_KEYS;
 36  using wallet::WALLET_FLAG_EXTERNAL_SIGNER;
 37  
 38  WalletController::WalletController(ClientModel& client_model, const PlatformStyle* platform_style, QObject* parent)
 39      : QObject(parent)
 40      , m_activity_thread(new QThread(this))
 41      , m_activity_worker(new QObject)
 42      , m_client_model(client_model)
 43      , m_node(client_model.node())
 44      , m_platform_style(platform_style)
 45      , m_options_model(client_model.getOptionsModel())
 46  {
 47      m_handler_load_wallet = m_node.walletLoader().handleLoadWallet([this](std::unique_ptr<interfaces::Wallet> wallet) {
 48          getOrCreateWallet(std::move(wallet));
 49      });
 50  
 51      m_activity_worker->moveToThread(m_activity_thread);
 52      m_activity_thread->start();
 53      QTimer::singleShot(0, m_activity_worker, []() {
 54          util::ThreadRename("qt-walletctrl");
 55      });
 56  }
 57  
 58  // Not using the default destructor because not all member types definitions are
 59  // available in the header, just forward declared.
 60  WalletController::~WalletController()
 61  {
 62      m_activity_thread->quit();
 63      m_activity_thread->wait();
 64      delete m_activity_worker;
 65  }
 66  
 67  std::map<std::string, bool> WalletController::listWalletDir() const
 68  {
 69      QMutexLocker locker(&m_mutex);
 70      std::map<std::string, bool> wallets;
 71      for (const std::string& name : m_node.walletLoader().listWalletDir()) {
 72          wallets[name] = false;
 73      }
 74      for (WalletModel* wallet_model : m_wallets) {
 75          auto it = wallets.find(wallet_model->wallet().getWalletName());
 76          if (it != wallets.end()) it->second = true;
 77      }
 78      return wallets;
 79  }
 80  
 81  void WalletController::closeWallet(WalletModel* wallet_model, QWidget* parent)
 82  {
 83      QMessageBox box(parent);
 84      box.setWindowTitle(tr("Close wallet"));
 85      box.setText(tr("Are you sure you wish to close the wallet <i>%1</i>?").arg(GUIUtil::HtmlEscape(wallet_model->getDisplayName())));
 86      box.setInformativeText(tr("Closing the wallet for too long can result in having to resync the entire chain if pruning is enabled."));
 87      box.setStandardButtons(QMessageBox::Yes|QMessageBox::Cancel);
 88      box.setDefaultButton(QMessageBox::Yes);
 89      if (box.exec() != QMessageBox::Yes) return;
 90  
 91      // First remove wallet from node.
 92      wallet_model->wallet().remove();
 93      // Now release the model.
 94      removeAndDeleteWallet(wallet_model);
 95  }
 96  
 97  void WalletController::closeAllWallets(QWidget* parent)
 98  {
 99      QMessageBox::StandardButton button = QMessageBox::question(parent, tr("Close all wallets"),
100          tr("Are you sure you wish to close all wallets?"),
101          QMessageBox::Yes|QMessageBox::Cancel,
102          QMessageBox::Yes);
103      if (button != QMessageBox::Yes) return;
104  
105      QMutexLocker locker(&m_mutex);
106      for (WalletModel* wallet_model : m_wallets) {
107          wallet_model->wallet().remove();
108          Q_EMIT walletRemoved(wallet_model);
109          delete wallet_model;
110      }
111      m_wallets.clear();
112  }
113  
114  WalletModel* WalletController::getOrCreateWallet(std::unique_ptr<interfaces::Wallet> wallet)
115  {
116      QMutexLocker locker(&m_mutex);
117  
118      // Return model instance if exists.
119      if (!m_wallets.empty()) {
120          std::string name = wallet->getWalletName();
121          for (WalletModel* wallet_model : m_wallets) {
122              if (wallet_model->wallet().getWalletName() == name) {
123                  return wallet_model;
124              }
125          }
126      }
127  
128      // Instantiate model and register it.
129      WalletModel* wallet_model = new WalletModel(std::move(wallet), m_client_model, m_platform_style,
130                                                  nullptr /* required for the following moveToThread() call */);
131  
132      // Move WalletModel object to the thread that created the WalletController
133      // object (GUI main thread), instead of the current thread, which could be
134      // an outside wallet thread or RPC thread sending a LoadWallet notification.
135      // This ensures queued signals sent to the WalletModel object will be
136      // handled on the GUI event loop.
137      wallet_model->moveToThread(thread());
138      // setParent(parent) must be called in the thread which created the parent object. More details in #18948.
139      QMetaObject::invokeMethod(this, [wallet_model, this] {
140          wallet_model->setParent(this);
141      }, GUIUtil::blockingGUIThreadConnection());
142  
143      m_wallets.push_back(wallet_model);
144  
145      // WalletModel::startPollBalance needs to be called in a thread managed by
146      // Qt because of startTimer. Considering the current thread can be a RPC
147      // thread, better delegate the calling to Qt with Qt::AutoConnection.
148      const bool called = QMetaObject::invokeMethod(wallet_model, "startPollBalance");
149      assert(called);
150  
151      connect(wallet_model, &WalletModel::unload, this, [this, wallet_model] {
152          // Defer removeAndDeleteWallet when no modal widget is active.
153          // TODO: remove this workaround by removing usage of QDialog::exec.
154          if (QApplication::activeModalWidget()) {
155              connect(qApp, &QApplication::focusWindowChanged, wallet_model, [this, wallet_model]() {
156                  if (!QApplication::activeModalWidget()) {
157                      removeAndDeleteWallet(wallet_model);
158                  }
159              }, Qt::QueuedConnection);
160          } else {
161              removeAndDeleteWallet(wallet_model);
162          }
163      }, Qt::QueuedConnection);
164  
165      // Re-emit coinsSent signal from wallet model.
166      connect(wallet_model, &WalletModel::coinsSent, this, &WalletController::coinsSent);
167  
168      Q_EMIT walletAdded(wallet_model);
169  
170      return wallet_model;
171  }
172  
173  void WalletController::removeAndDeleteWallet(WalletModel* wallet_model)
174  {
175      // Unregister wallet model.
176      {
177          QMutexLocker locker(&m_mutex);
178          m_wallets.erase(std::remove(m_wallets.begin(), m_wallets.end(), wallet_model));
179      }
180      Q_EMIT walletRemoved(wallet_model);
181      // Currently this can trigger the unload since the model can hold the last
182      // CWallet shared pointer.
183      delete wallet_model;
184  }
185  
186  WalletControllerActivity::WalletControllerActivity(WalletController* wallet_controller, QWidget* parent_widget)
187      : QObject(wallet_controller)
188      , m_wallet_controller(wallet_controller)
189      , m_parent_widget(parent_widget)
190  {
191      connect(this, &WalletControllerActivity::finished, this, &QObject::deleteLater);
192  }
193  
194  void WalletControllerActivity::showProgressDialog(const QString& title_text, const QString& label_text, bool show_minimized)
195  {
196      auto progress_dialog = new QProgressDialog(m_parent_widget);
197      progress_dialog->setAttribute(Qt::WA_DeleteOnClose);
198      connect(this, &WalletControllerActivity::finished, progress_dialog, &QWidget::close);
199  
200      progress_dialog->setWindowTitle(title_text);
201      progress_dialog->setLabelText(label_text);
202      progress_dialog->setRange(0, 0);
203      progress_dialog->setCancelButton(nullptr);
204      progress_dialog->setWindowModality(Qt::ApplicationModal);
205      GUIUtil::PolishProgressDialog(progress_dialog);
206      // The setValue call forces QProgressDialog to start the internal duration estimation.
207      // See details in https://bugreports.qt.io/browse/QTBUG-47042.
208      progress_dialog->setValue(0);
209      // When requested, launch dialog minimized
210      if (show_minimized) progress_dialog->showMinimized();
211  }
212  
213  CreateWalletActivity::CreateWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
214      : WalletControllerActivity(wallet_controller, parent_widget)
215  {
216      m_passphrase.reserve(MAX_PASSPHRASE_SIZE);
217  }
218  
219  CreateWalletActivity::~CreateWalletActivity()
220  {
221      delete m_create_wallet_dialog;
222      delete m_passphrase_dialog;
223  }
224  
225  void CreateWalletActivity::askPassphrase()
226  {
227      m_passphrase_dialog = new AskPassphraseDialog(AskPassphraseDialog::Encrypt, m_parent_widget, &m_passphrase);
228      m_passphrase_dialog->setWindowModality(Qt::ApplicationModal);
229      m_passphrase_dialog->show();
230  
231      connect(m_passphrase_dialog, &QObject::destroyed, [this] {
232          m_passphrase_dialog = nullptr;
233      });
234      connect(m_passphrase_dialog, &QDialog::accepted, [this] {
235          createWallet();
236      });
237      connect(m_passphrase_dialog, &QDialog::rejected, [this] {
238          Q_EMIT finished();
239      });
240  }
241  
242  void CreateWalletActivity::createWallet()
243  {
244      showProgressDialog(
245          //: Title of window indicating the progress of creation of a new wallet.
246          tr("Create Wallet"),
247          /*: Descriptive text of the create wallet progress window which indicates
248              to the user which wallet is currently being created. */
249          tr("Creating Wallet <b>%1</b>…").arg(m_create_wallet_dialog->walletName().toHtmlEscaped()));
250  
251      std::string name = m_create_wallet_dialog->walletName().toStdString();
252      uint64_t flags = 0;
253      // Enable descriptors by default.
254      flags |= WALLET_FLAG_DESCRIPTORS;
255      if (m_create_wallet_dialog->isDisablePrivateKeysChecked()) {
256          flags |= WALLET_FLAG_DISABLE_PRIVATE_KEYS;
257      }
258      if (m_create_wallet_dialog->isMakeBlankWalletChecked()) {
259          flags |= WALLET_FLAG_BLANK_WALLET;
260      }
261      if (m_create_wallet_dialog->isExternalSignerChecked()) {
262          flags |= WALLET_FLAG_EXTERNAL_SIGNER;
263      }
264  
265      QTimer::singleShot(500ms, worker(), [this, name, flags] {
266          auto wallet{node().walletLoader().createWallet(name, m_passphrase, flags, m_warning_message)};
267  
268          if (wallet) {
269              m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(*wallet));
270          } else {
271              m_error_message = util::ErrorString(wallet);
272          }
273  
274          QTimer::singleShot(500ms, this, &CreateWalletActivity::finish);
275      });
276  }
277  
278  void CreateWalletActivity::finish()
279  {
280      if (!m_error_message.empty()) {
281          QMessageBox::critical(m_parent_widget, tr("Create wallet failed"), QString::fromStdString(m_error_message.translated));
282      } else if (!m_warning_message.empty()) {
283          QMessageBox::warning(m_parent_widget, tr("Create wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
284      }
285  
286      if (m_wallet_model) Q_EMIT created(m_wallet_model);
287  
288      Q_EMIT finished();
289  }
290  
291  void CreateWalletActivity::create()
292  {
293      m_create_wallet_dialog = new CreateWalletDialog(m_parent_widget);
294  
295      std::vector<std::unique_ptr<interfaces::ExternalSigner>> signers;
296      try {
297          signers = node().listExternalSigners();
298      } catch (const std::runtime_error& e) {
299          QMessageBox::critical(nullptr, tr("Can't list signers"), e.what());
300      }
301      if (signers.size() > 1) {
302          QMessageBox::critical(nullptr, tr("Too many external signers found"), QString::fromStdString("More than one external signer found. Please connect only one at a time."));
303          signers.clear();
304      }
305      m_create_wallet_dialog->setSigners(signers);
306  
307      m_create_wallet_dialog->setWindowModality(Qt::ApplicationModal);
308      m_create_wallet_dialog->show();
309  
310      connect(m_create_wallet_dialog, &QObject::destroyed, [this] {
311          m_create_wallet_dialog = nullptr;
312      });
313      connect(m_create_wallet_dialog, &QDialog::rejected, [this] {
314          Q_EMIT finished();
315      });
316      connect(m_create_wallet_dialog, &QDialog::accepted, [this] {
317          if (m_create_wallet_dialog->isEncryptWalletChecked()) {
318              askPassphrase();
319          } else {
320              createWallet();
321          }
322      });
323  }
324  
325  OpenWalletActivity::OpenWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
326      : WalletControllerActivity(wallet_controller, parent_widget)
327  {
328  }
329  
330  void OpenWalletActivity::finish()
331  {
332      if (!m_error_message.empty()) {
333          QMessageBox::critical(m_parent_widget, tr("Open wallet failed"), QString::fromStdString(m_error_message.translated));
334      } else if (!m_warning_message.empty()) {
335          QMessageBox::warning(m_parent_widget, tr("Open wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
336      }
337  
338      if (m_wallet_model) Q_EMIT opened(m_wallet_model);
339  
340      Q_EMIT finished();
341  }
342  
343  void OpenWalletActivity::open(const std::string& path)
344  {
345      QString name = path.empty() ? QString("["+tr("default wallet")+"]") : QString::fromStdString(path);
346  
347      showProgressDialog(
348          //: Title of window indicating the progress of opening of a wallet.
349          tr("Open Wallet"),
350          /*: Descriptive text of the open wallet progress window which indicates
351              to the user which wallet is currently being opened. */
352          tr("Opening Wallet <b>%1</b>…").arg(name.toHtmlEscaped()));
353  
354      QTimer::singleShot(0, worker(), [this, path] {
355          auto wallet{node().walletLoader().loadWallet(path, m_warning_message)};
356  
357          if (wallet) {
358              m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(*wallet));
359          } else {
360              m_error_message = util::ErrorString(wallet);
361          }
362  
363          QTimer::singleShot(0, this, &OpenWalletActivity::finish);
364      });
365  }
366  
367  LoadWalletsActivity::LoadWalletsActivity(WalletController* wallet_controller, QWidget* parent_widget)
368      : WalletControllerActivity(wallet_controller, parent_widget)
369  {
370  }
371  
372  void LoadWalletsActivity::load(bool show_loading_minimized)
373  {
374      showProgressDialog(
375          //: Title of progress window which is displayed when wallets are being loaded.
376          tr("Load Wallets"),
377          /*: Descriptive text of the load wallets progress window which indicates to
378              the user that wallets are currently being loaded.*/
379          tr("Loading wallets…"),
380          /*show_minimized=*/show_loading_minimized);
381  
382      QTimer::singleShot(0, worker(), [this] {
383          for (auto& wallet : node().walletLoader().getWallets()) {
384              m_wallet_controller->getOrCreateWallet(std::move(wallet));
385          }
386  
387          QTimer::singleShot(0, this, [this] { Q_EMIT finished(); });
388      });
389  }
390  
391  RestoreWalletActivity::RestoreWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
392      : WalletControllerActivity(wallet_controller, parent_widget)
393  {
394  }
395  
396  void RestoreWalletActivity::restore(const fs::path& backup_file, const std::string& wallet_name)
397  {
398      QString name = QString::fromStdString(wallet_name);
399  
400      showProgressDialog(
401          //: Title of progress window which is displayed when wallets are being restored.
402          tr("Restore Wallet"),
403          /*: Descriptive text of the restore wallets progress window which indicates to
404              the user that wallets are currently being restored.*/
405          tr("Restoring Wallet <b>%1</b>…").arg(name.toHtmlEscaped()));
406  
407      QTimer::singleShot(0, worker(), [this, backup_file, wallet_name] {
408          auto wallet{node().walletLoader().restoreWallet(backup_file, wallet_name, m_warning_message)};
409  
410          if (wallet) {
411              m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(*wallet));
412          } else {
413              m_error_message = util::ErrorString(wallet);
414          }
415  
416          QTimer::singleShot(0, this, &RestoreWalletActivity::finish);
417      });
418  }
419  
420  void RestoreWalletActivity::finish()
421  {
422      if (!m_error_message.empty()) {
423          //: Title of message box which is displayed when the wallet could not be restored.
424          QMessageBox::critical(m_parent_widget, tr("Restore wallet failed"), QString::fromStdString(m_error_message.translated));
425      } else if (!m_warning_message.empty()) {
426          //: Title of message box which is displayed when the wallet is restored with some warning.
427          QMessageBox::warning(m_parent_widget, tr("Restore wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
428      } else {
429          //: Title of message box which is displayed when the wallet is successfully restored.
430          QMessageBox::information(m_parent_widget, tr("Restore wallet message"), QString::fromStdString(Untranslated("Wallet restored successfully \n").translated));
431      }
432  
433      if (m_wallet_model) Q_EMIT restored(m_wallet_model);
434  
435      Q_EMIT finished();
436  }
437  
438  void MigrateWalletActivity::migrate(WalletModel* wallet_model)
439  {
440      // Warn the user about migration
441      QMessageBox box(m_parent_widget);
442      box.setWindowTitle(tr("Migrate wallet"));
443      box.setText(tr("Are you sure you wish to migrate the wallet <i>%1</i>?").arg(GUIUtil::HtmlEscape(wallet_model->getDisplayName())));
444      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"
445                  "If this wallet contains any watchonly scripts, a new wallet will be created which contains those watchonly scripts.\n"
446                  "If this wallet contains any solvable but not watched scripts, a different and new wallet will be created which contains those scripts.\n\n"
447                  "The migration process will create a backup of the wallet before migrating. This backup file will be named "
448                  "<wallet name>-<timestamp>.legacy.bak and can be found in the directory for this wallet. In the event of "
449                  "an incorrect migration, the backup can be restored with the \"Restore Wallet\" functionality."));
450      box.setStandardButtons(QMessageBox::Yes|QMessageBox::Cancel);
451      box.setDefaultButton(QMessageBox::Yes);
452      if (box.exec() != QMessageBox::Yes) return;
453  
454      // Get the passphrase if it is encrypted regardless of it is locked or unlocked. We need the passphrase itself.
455      SecureString passphrase;
456      WalletModel::EncryptionStatus enc_status = wallet_model->getEncryptionStatus();
457      if (enc_status == WalletModel::EncryptionStatus::Locked || enc_status == WalletModel::EncryptionStatus::Unlocked) {
458          AskPassphraseDialog dlg(AskPassphraseDialog::Unlock, m_parent_widget, &passphrase);
459          dlg.setModel(wallet_model);
460          dlg.exec();
461      }
462  
463      // GUI needs to remove the wallet so that it can actually be unloaded by migration
464      const std::string name = wallet_model->wallet().getWalletName();
465      m_wallet_controller->removeAndDeleteWallet(wallet_model);
466  
467      showProgressDialog(tr("Migrate Wallet"), tr("Migrating Wallet <b>%1</b>…").arg(GUIUtil::HtmlEscape(name)));
468  
469      QTimer::singleShot(0, worker(), [this, name, passphrase] {
470          auto res{node().walletLoader().migrateWallet(name, passphrase)};
471  
472          if (res) {
473              m_success_message = tr("The wallet '%1' was migrated successfully.").arg(GUIUtil::HtmlEscape(res->wallet->getWalletName()));
474              if (res->watchonly_wallet_name) {
475                  m_success_message += QChar(' ') + tr("Watchonly scripts have been migrated to a new wallet named '%1'.").arg(GUIUtil::HtmlEscape(res->watchonly_wallet_name.value()));
476              }
477              if (res->solvables_wallet_name) {
478                  m_success_message += QChar(' ') + tr("Solvable but not watched scripts have been migrated to a new wallet named '%1'.").arg(GUIUtil::HtmlEscape(res->solvables_wallet_name.value()));
479              }
480              m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(res->wallet));
481          } else {
482              m_error_message = util::ErrorString(res);
483          }
484  
485          QTimer::singleShot(0, this, &MigrateWalletActivity::finish);
486      });
487  }
488  
489  void MigrateWalletActivity::finish()
490  {
491      if (!m_error_message.empty()) {
492          QMessageBox::critical(m_parent_widget, tr("Migration failed"), QString::fromStdString(m_error_message.translated));
493      } else {
494          QMessageBox::information(m_parent_widget, tr("Migration Successful"), m_success_message);
495      }
496  
497      if (m_wallet_model) Q_EMIT migrated(m_wallet_model);
498  
499      Q_EMIT finished();
500  }