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