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 }