walletcontroller.cpp raw

   1  // Copyright (c) 2019-2022 The Limenka 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      if (m_create_wallet_dialog->isDisablePrivateKeysChecked()) {
 258          flags |= WALLET_FLAG_DISABLE_PRIVATE_KEYS;
 259      }
 260      if (m_create_wallet_dialog->isMakeBlankWalletChecked()) {
 261          flags |= WALLET_FLAG_BLANK_WALLET;
 262      }
 263      if (m_create_wallet_dialog->isDescriptorWalletChecked()) {
 264          flags |= WALLET_FLAG_DESCRIPTORS;
 265      }
 266      if (m_create_wallet_dialog->isExternalSignerChecked()) {
 267          flags |= WALLET_FLAG_EXTERNAL_SIGNER;
 268      }
 269  
 270      QTimer::singleShot(500ms, worker(), [this, name, flags] {
 271          auto wallet{node().walletLoader().createWallet(name, m_passphrase, flags, m_warning_message)};
 272  
 273          if (wallet) {
 274              m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(*wallet));
 275          } else {
 276              m_error_message = util::ErrorString(wallet);
 277          }
 278  
 279          QTimer::singleShot(500ms, this, &CreateWalletActivity::finish);
 280      });
 281  }
 282  
 283  void CreateWalletActivity::finish()
 284  {
 285      if (!m_error_message.empty()) {
 286          QMessageBox::critical(m_parent_widget, tr("Create wallet failed"), QString::fromStdString(m_error_message.translated));
 287      } else if (!m_warning_message.empty()) {
 288          QMessageBox::warning(m_parent_widget, tr("Create wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
 289      }
 290  
 291      if (m_wallet_model) Q_EMIT created(m_wallet_model);
 292  
 293      Q_EMIT finished();
 294  }
 295  
 296  void CreateWalletActivity::create()
 297  {
 298      m_create_wallet_dialog = new CreateWalletDialog(m_parent_widget);
 299  
 300      std::vector<std::unique_ptr<interfaces::ExternalSigner>> signers;
 301      try {
 302          signers = node().listExternalSigners();
 303      } catch (const std::runtime_error& e) {
 304          QMessageBox msgBox;
 305          msgBox.setIcon(QMessageBox::Critical);
 306          msgBox.setWindowTitle(tr("Can't list signers"));
 307          msgBox.setText(tr("Unable to execute external signer script. Please check that the script signer path is correct and that the script is functional."));
 308          msgBox.setDetailedText(QString::fromStdString(e.what()));
 309          msgBox.exec();
 310      }
 311      if (signers.size() > 1) {
 312          QMessageBox::critical(nullptr, tr("Too many external signers found"), QString::fromStdString("More than one external signer found. Please connect only one at a time."));
 313          signers.clear();
 314      }
 315      m_create_wallet_dialog->setSigners(signers);
 316  
 317      m_create_wallet_dialog->setWindowModality(Qt::ApplicationModal);
 318      m_create_wallet_dialog->show();
 319  
 320      connect(m_create_wallet_dialog, &QObject::destroyed, [this] {
 321          m_create_wallet_dialog = nullptr;
 322      });
 323      connect(m_create_wallet_dialog, &QDialog::rejected, [this] {
 324          Q_EMIT finished();
 325      });
 326      connect(m_create_wallet_dialog, &QDialog::accepted, [this] {
 327          if (m_create_wallet_dialog->isEncryptWalletChecked()) {
 328              askPassphrase();
 329          } else {
 330              createWallet();
 331          }
 332      });
 333  }
 334  
 335  OpenWalletActivity::OpenWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
 336      : WalletControllerActivity(wallet_controller, parent_widget)
 337  {
 338  }
 339  
 340  void OpenWalletActivity::finish()
 341  {
 342      if (!m_error_message.empty()) {
 343          QMessageBox::critical(m_parent_widget, tr("Open wallet failed"), QString::fromStdString(m_error_message.translated));
 344      } else if (!m_warning_message.empty()) {
 345          QMessageBox::warning(m_parent_widget, tr("Open wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
 346      }
 347  
 348      if (m_wallet_model) Q_EMIT opened(m_wallet_model);
 349  
 350      Q_EMIT finished();
 351  }
 352  
 353  void OpenWalletActivity::open(const std::string& path)
 354  {
 355      QString name = GUIUtil::WalletDisplayName(path);
 356  
 357      showProgressDialog(
 358          //: Title of window indicating the progress of opening of a wallet.
 359          tr("Open Wallet"),
 360          /*: Descriptive text of the open wallet progress window which indicates
 361              to the user which wallet is currently being opened. */
 362          tr("Opening Wallet <b>%1</b>…").arg(name.toHtmlEscaped()));
 363  
 364      QTimer::singleShot(0, worker(), [this, path] {
 365          auto wallet{node().walletLoader().loadWallet(path, m_warning_message)};
 366  
 367          if (wallet) {
 368              m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(*wallet));
 369          } else {
 370              m_error_message = util::ErrorString(wallet);
 371          }
 372  
 373          QTimer::singleShot(0, this, &OpenWalletActivity::finish);
 374      });
 375  }
 376  
 377  LoadWalletsActivity::LoadWalletsActivity(WalletController* wallet_controller, QWidget* parent_widget)
 378      : WalletControllerActivity(wallet_controller, parent_widget)
 379  {
 380  }
 381  
 382  void LoadWalletsActivity::load(bool show_loading_minimized)
 383  {
 384      showProgressDialog(
 385          //: Title of progress window which is displayed when wallets are being loaded.
 386          tr("Load Wallets"),
 387          /*: Descriptive text of the load wallets progress window which indicates to
 388              the user that wallets are currently being loaded.*/
 389          tr("Loading wallets…"),
 390          /*show_minimized=*/show_loading_minimized);
 391  
 392      QTimer::singleShot(0, worker(), [this] {
 393          for (auto& wallet : node().walletLoader().getWallets()) {
 394              m_wallet_controller->getOrCreateWallet(std::move(wallet));
 395          }
 396  
 397          QTimer::singleShot(0, this, [this] { Q_EMIT finished(); });
 398      });
 399  }
 400  
 401  RestoreWalletActivity::RestoreWalletActivity(WalletController* wallet_controller, QWidget* parent_widget)
 402      : WalletControllerActivity(wallet_controller, parent_widget)
 403  {
 404  }
 405  
 406  void RestoreWalletActivity::restore(const fs::path& backup_file, const std::string& wallet_name)
 407  {
 408      QString name = QString::fromStdString(wallet_name);
 409  
 410      showProgressDialog(
 411          //: Title of progress window which is displayed when wallets are being restored.
 412          tr("Restore Wallet"),
 413          /*: Descriptive text of the restore wallets progress window which indicates to
 414              the user that wallets are currently being restored.*/
 415          tr("Restoring Wallet <b>%1</b>…").arg(name.toHtmlEscaped()));
 416  
 417      QTimer::singleShot(0, worker(), [this, backup_file, wallet_name] {
 418          auto wallet{node().walletLoader().restoreWallet(backup_file, wallet_name, m_warning_message)};
 419  
 420          if (wallet) {
 421              m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(*wallet));
 422          } else {
 423              m_error_message = util::ErrorString(wallet);
 424          }
 425  
 426          QTimer::singleShot(0, this, &RestoreWalletActivity::finish);
 427      });
 428  }
 429  
 430  void RestoreWalletActivity::finish()
 431  {
 432      if (!m_error_message.empty()) {
 433          //: Title of message box which is displayed when the wallet could not be restored.
 434          QMessageBox::critical(m_parent_widget, tr("Restore wallet failed"), QString::fromStdString(m_error_message.translated));
 435      } else if (!m_warning_message.empty()) {
 436          //: Title of message box which is displayed when the wallet is restored with some warning.
 437          QMessageBox::warning(m_parent_widget, tr("Restore wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated));
 438      } else {
 439          //: Title of message box which is displayed when the wallet is successfully restored.
 440          QMessageBox::information(m_parent_widget, tr("Restore wallet message"), QString::fromStdString(Untranslated("Wallet restored successfully \n").translated));
 441      }
 442  
 443      if (m_wallet_model) Q_EMIT restored(m_wallet_model);
 444  
 445      Q_EMIT finished();
 446  }
 447  
 448  void MigrateWalletActivity::migrate(const std::string& name)
 449  {
 450      // Warn the user about migration
 451      QMessageBox box(m_parent_widget);
 452      box.setWindowTitle(tr("Migrate wallet"));
 453      box.setText(tr("Are you sure you wish to migrate the wallet <i>%1</i>?").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(name))));
 454      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"
 455                  "If this wallet contains any watchonly scripts, a new wallet will be created which contains those watchonly scripts.\n"
 456                  "If this wallet contains any solvable but not watched scripts, a different and new wallet will be created which contains those scripts.\n\n"
 457                  "The migration process will create a backup of the wallet before migrating. This backup file will be named "
 458                  "<wallet name>-<timestamp>.legacy.bak and can be found in the directory for this wallet. In the event of "
 459                  "an incorrect migration, the backup can be restored with the \"Restore Wallet\" functionality."));
 460      box.setStandardButtons(QMessageBox::Yes|QMessageBox::Cancel);
 461      box.setDefaultButton(QMessageBox::Yes);
 462      if (box.exec() != QMessageBox::Yes) return;
 463  
 464      SecureString passphrase;
 465      if (node().walletLoader().isEncrypted(name)) {
 466          // Get the passphrase for the wallet
 467          AskPassphraseDialog dlg(AskPassphraseDialog::UnlockMigration, m_parent_widget, &passphrase);
 468          if (dlg.exec() == QDialog::Rejected) return;
 469      }
 470  
 471      showProgressDialog(tr("Migrate Wallet"), tr("Migrating Wallet <b>%1</b>…").arg(GUIUtil::HtmlEscape(name)));
 472  
 473      QTimer::singleShot(0, worker(), [this, name, passphrase] {
 474          auto res{node().walletLoader().migrateWallet(name, passphrase)};
 475  
 476          if (res) {
 477              m_success_message = tr("The wallet '%1' was migrated successfully.").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(name)));
 478              if (res->watchonly_wallet_name) {
 479                  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())));
 480              }
 481              if (res->solvables_wallet_name) {
 482                  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())));
 483              }
 484              m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(res->wallet));
 485          } else {
 486              m_error_message = util::ErrorString(res);
 487          }
 488  
 489          QTimer::singleShot(0, this, &MigrateWalletActivity::finish);
 490      });
 491  }
 492  
 493  void MigrateWalletActivity::finish()
 494  {
 495      if (!m_error_message.empty()) {
 496          QMessageBox::critical(m_parent_widget, tr("Migration failed"), QString::fromStdString(m_error_message.translated));
 497      } else {
 498          QMessageBox::information(m_parent_widget, tr("Migration Successful"), m_success_message);
 499      }
 500  
 501      if (m_wallet_model) Q_EMIT migrated(m_wallet_model);
 502  
 503      Q_EMIT finished();
 504  }
 505