feather/src/WindowManager.cpp

570 lines
20 KiB
C++
Raw Normal View History

2021-05-18 15:59:18 +00:00
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2020-2021, The Monero Project.
#include "WindowManager.h"
2021-06-28 17:48:23 +00:00
2021-07-08 00:34:27 +00:00
#include <QInputDialog>
2021-06-28 17:48:23 +00:00
#include <QMessageBox>
2021-05-18 15:59:18 +00:00
#include "constants.h"
2021-06-27 11:46:32 +00:00
#include "dialog/PasswordDialog.h"
#include "dialog/SplashDialog.h"
2021-05-18 15:59:18 +00:00
#include "utils/Icons.h"
#include "utils/NetworkManager.h"
2021-06-28 17:48:23 +00:00
#include "utils/os/tails.h"
2021-05-18 15:59:18 +00:00
#include "utils/TorManager.h"
2021-06-28 17:48:23 +00:00
#include "utils/WebsocketNotifier.h"
2021-05-18 15:59:18 +00:00
WindowManager::WindowManager() {
m_walletManager = WalletManager::instance();
m_splashDialog = new SplashDialog;
m_cleanupThread = new QThread();
2021-05-18 15:59:18 +00:00
connect(m_walletManager, &WalletManager::walletOpened, this, &WindowManager::onWalletOpened);
connect(m_walletManager, &WalletManager::walletCreated, this, &WindowManager::onWalletCreated);
connect(m_walletManager, &WalletManager::deviceButtonRequest, this, &WindowManager::onDeviceButtonRequest);
connect(m_walletManager, &WalletManager::deviceButtonPressed, this, &WindowManager::onDeviceButtonPressed);
2021-05-18 15:59:18 +00:00
connect(m_walletManager, &WalletManager::deviceError, this, &WindowManager::onDeviceError);
2021-07-08 00:34:27 +00:00
connect(m_walletManager, &WalletManager::walletPassphraseNeeded, this, &WindowManager::onWalletPassphraseNeeded);
2021-05-18 15:59:18 +00:00
connect(qApp, &QGuiApplication::lastWindowClosed, this, &WindowManager::quitAfterLastWindow);
m_tray = new QSystemTrayIcon(icons()->icon("appicons/64x64.png"));
m_tray->setToolTip("Feather Wallet");
this->buildTrayMenu();
m_tray->show();
this->initSkins();
if (!config()->get(Config::firstRun).toBool() || TailsOS::detect() || WhonixOS::detect()) {
2021-05-18 15:59:18 +00:00
this->onInitialNetworkConfigured();
}
2021-05-26 23:20:37 +00:00
this->startupWarning();
2021-05-18 15:59:18 +00:00
if (!this->autoOpenWallet()) {
this->initWizard();
}
}
// ######################## APPLICATION LIFECYCLE ########################
void WindowManager::quitAfterLastWindow() {
if (m_windows.length() > 0 || m_openingWallet) {
return;
}
qDebug() << "No wizards in progress and no wallets open, quitting application.";
this->close();
2021-05-18 15:59:18 +00:00
}
void WindowManager::close() {
qDebug() << Q_FUNC_INFO;
for (const auto &window: m_windows) {
window->close();
}
2021-05-25 13:15:19 +00:00
2021-05-18 15:59:18 +00:00
torManager()->stop();
2021-05-25 13:15:19 +00:00
m_tray->hide();
2021-05-18 15:59:18 +00:00
QApplication::quit();
}
void WindowManager::closeWindow(MainWindow *window) {
m_windows.removeOne(window);
// Move Wallet to a different thread for cleanup so it doesn't block GUI thread
window->m_ctx->wallet->moveToThread(m_cleanupThread);
m_cleanupThread->start();
window->m_ctx->wallet->deleteLater();
2021-05-18 15:59:18 +00:00
}
void WindowManager::restartApplication(const QString &binaryFilename) {
QProcess::startDetached(binaryFilename, qApp->arguments());
this->close();
}
2021-05-26 23:20:37 +00:00
void WindowManager::startupWarning() {
// Stagenet / Testnet
auto worthlessWarning = QString("Feather wallet is currently running in %1 mode. This is meant "
"for developers only. Your coins are WORTHLESS.");
if (constants::networkType == NetworkType::STAGENET && config()->get(Config::warnOnStagenet).toBool()) {
this->showWarningMessageBox("Warning", worthlessWarning.arg("stagenet"));
config()->set(Config::warnOnStagenet, false);
}
else if (constants::networkType == NetworkType::TESTNET && config()->get(Config::warnOnTestnet).toBool()){
this->showWarningMessageBox("Warning", worthlessWarning.arg("testnet"));
config()->set(Config::warnOnTestnet, false);
}
// Beta
if (config()->get(Config::warnOnAlpha).toBool()) {
QString warning = "Feather Wallet is currently in beta.\n\nPlease report any bugs "
"you encounter on our Git repository, IRC or on /r/FeatherWallet.";
this->showWarningMessageBox("Beta warning", warning);
config()->set(Config::warnOnAlpha, false);
}
}
void WindowManager::showWarningMessageBox(const QString &title, const QString &message) {
QMessageBox msgBox;
msgBox.setWindowIcon(icons()->icon("appicons/64x64.png"));
msgBox.setIcon(QMessageBox::Warning);
msgBox.setText(message);
msgBox.setWindowTitle(title);
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setDefaultButton(QMessageBox::Ok);
msgBox.exec();
}
2021-05-18 15:59:18 +00:00
// ######################## WALLET OPEN ########################
void WindowManager::tryOpenWallet(const QString &path, const QString &password) {
// Path : path to .keys file
QString absolutePath = path;
if (absolutePath.startsWith("~")) {
absolutePath.replace(0, 1, QDir::homePath());
}
// If the wallet is already open, just bring window to front
for (const auto &window : m_windows) {
if (absolutePath == window->walletKeysPath() || absolutePath == window->walletCachePath()) {
window->bringToFront();
return;
}
}
if (!Utils::fileExists(path)) {
this->handleWalletError(QString("Wallet not found: %1").arg(path));
return;
}
m_openingWallet = true;
m_walletManager->openWalletAsync(path, password, constants::networkType, 1);
}
void WindowManager::onWalletOpened(Wallet *wallet) {
if (wallet->status() != Wallet::Status_Ok) {
QString errMsg = wallet->errorString();
if (wallet->status() == Wallet::Status_BadPassword) {
// Don't show incorrect password when we try with empty password for the first time
bool showIncorrectPassword = m_openWalletTriedOnce;
m_openWalletTriedOnce = true;
this->onWalletOpenPasswordRequired(showIncorrectPassword, wallet->keysPath());
2021-05-18 15:59:18 +00:00
}
else if (errMsg == QString("basic_string::_M_replace_aux") || errMsg == QString("std::bad_alloc")) {
qCritical() << errMsg;
WalletManager::clearWalletCache(wallet->cachePath()); // TODO: check this
errMsg = QString("%1\n\nAttempted to clean wallet cache. Please restart Feather.").arg(errMsg);
this->handleWalletError(errMsg);
} else {
this->handleWalletError(errMsg);
}
return;
}
this->onInitialNetworkConfigured();
2021-05-18 15:59:18 +00:00
// Create new mainwindow with wallet
m_splashDialog->hide();
m_openWalletTriedOnce = false;
auto *window = new MainWindow(this, wallet);
m_windows.append(window);
this->buildTrayMenu();
m_openingWallet = false;
}
void WindowManager::onWalletOpenPasswordRequired(bool invalidPassword, const QString &path) {
QFileInfo fileInfo(path);
PasswordDialog dialog{fileInfo.fileName(), invalidPassword};
switch (dialog.exec()) {
case QDialog::Rejected:
{
m_openWalletTriedOnce = false;
this->showWizard(WalletWizard::Page_OpenWallet);
return;
}
}
this->tryOpenWallet(path, dialog.password);
}
bool WindowManager::autoOpenWallet() {
QString autoPath = config()->get(Config::autoOpenWalletPath).toString();
if (!autoPath.isEmpty() && autoPath.startsWith(QString::number(constants::networkType))) {
autoPath.remove(0, 1);
}
if (!autoPath.isEmpty() && Utils::fileExists(autoPath)) {
this->tryOpenWallet(autoPath, ""); // TODO: get password from --password
return true;
}
return false;
}
// ######################## WALLET CREATION ########################
void WindowManager::tryCreateWallet(FeatherSeed seed, const QString &path, const QString &password,
const QString &seedOffset) {
if(Utils::fileExists(path)) {
auto err = QString("Failed to write wallet to path: \"%1\"; file already exists.").arg(path);
this->handleWalletError(err);
return;
}
if (seed.mnemonic.isEmpty()) {
this->handleWalletError("Mnemonic seed error. Failed to write wallet.");
return;
}
Wallet *wallet = nullptr;
if (seed.seedType == SeedType::TEVADOR) {
wallet = m_walletManager->createDeterministicWalletFromSpendKey(path, password, seed.language, constants::networkType, seed.spendKey, seed.restoreHeight, constants::kdfRounds, seedOffset);
wallet->setCacheAttribute("feather.seed", seed.mnemonic.join(" "));
wallet->setCacheAttribute("feather.seedoffset", seedOffset);
}
if (seed.seedType == SeedType::MONERO) {
wallet = m_walletManager->recoveryWallet(path, password, seed.mnemonic.join(" "), seedOffset, constants::networkType, seed.restoreHeight, constants::kdfRounds);
}
if (!wallet) {
this->handleWalletError("Failed to write wallet");
return;
}
this->onWalletOpened(wallet);
}
2021-07-01 21:00:47 +00:00
void WindowManager::tryCreateWalletFromDevice(const QString &path, const QString &password, const QString &deviceName, int restoreHeight)
2021-05-18 15:59:18 +00:00
{
if (Utils::fileExists(path)) {
auto err = QString("Failed to write wallet to path: \"%1\"; file already exists.").arg(path);
this->handleWalletError(err);
return;
}
m_openingWallet = true;
2021-07-01 21:00:47 +00:00
m_walletManager->createWalletFromDeviceAsync(path, password, constants::networkType, deviceName, restoreHeight);
2021-05-18 15:59:18 +00:00
}
void WindowManager::tryCreateWalletFromKeys(const QString &path, const QString &password, const QString &address,
const QString &viewkey, const QString &spendkey, quint64 restoreHeight) {
if (Utils::fileExists(path)) {
auto err = QString("Failed to write wallet to path: \"%1\"; file already exists.").arg(path);
this->handleWalletError(err);
return;
}
if (!WalletManager::addressValid(address, constants::networkType)) {
auto err = QString("Failed to create wallet. Invalid address provided.").arg(path);
this->handleWalletError(err);
return;
}
if (!WalletManager::keyValid(viewkey, address, true, constants::networkType)) {
auto err = QString("Failed to create wallet. Invalid viewkey provided.").arg(path);
this->handleWalletError(err);
return;
}
if (!spendkey.isEmpty() && !WalletManager::keyValid(spendkey, address, false, constants::networkType)) {
auto err = QString("Failed to create wallet. Invalid spendkey provided.").arg(path);
this->handleWalletError(err);
return;
}
Wallet *wallet = m_walletManager->createWalletFromKeys(path, password, constants::seedLanguage, constants::networkType, address, viewkey, spendkey, restoreHeight);
m_openingWallet = true;
m_walletManager->walletOpened(wallet);
}
void WindowManager::onWalletCreated(Wallet *wallet) {
// Currently only called when a wallet is created from device.
auto state = wallet->status();
if (state != Wallet::Status_Ok) {
qDebug() << Q_FUNC_INFO << QString("Wallet open error: %1").arg(wallet->errorString());
this->displayWalletErrorMessage(wallet->errorString());
m_splashDialog->hide();
this->showWizard(WalletWizard::Page_Menu);
return;
}
this->onWalletOpened(wallet);
}
// ######################## ERROR HANDLING ########################
void WindowManager::handleWalletError(const QString &message) {
qCritical() << message;
this->displayWalletErrorMessage(message);
this->initWizard();
}
void WindowManager::displayWalletErrorMessage(const QString &message) {
2021-07-01 21:00:47 +00:00
QString errMsg = QString("Error: %1").arg(message);
2021-07-07 17:04:31 +00:00
QString link;
2021-07-01 21:00:47 +00:00
// Ledger
2021-05-18 15:59:18 +00:00
if (message.contains("No device found")) {
2021-07-01 21:00:47 +00:00
errMsg += "\n\nThis wallet is backed by a Ledger hardware device. Make sure the Monero app is opened on the device.\n"
2021-05-18 15:59:18 +00:00
"You may need to restart Feather before the device can get detected.";
}
if (message.contains("Unable to open device")) {
errMsg += "\n\nThe device might be in use by a different application.";
2021-05-25 18:00:02 +00:00
#if defined(Q_OS_LINUX)
errMsg += "\n\nNote: On Linux you may need to follow the instructions in the link below before the device can be opened:\n"
2021-07-01 21:00:47 +00:00
"https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues";
2021-07-07 17:04:31 +00:00
link = "https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues";
2021-07-01 21:00:47 +00:00
#endif
}
// TREZOR
if (message.contains("Unable to claim libusb device")) {
errMsg += "\n\nThis wallet is backed by a Trezor hardware device. Feather was unable to access the device. "
"Please make sure it is not opened by another program and try again.";
}
if (message.contains("Cannot get a device address")) {
errMsg += "\n\nRestart the Trezor hardware device and try again.";
}
if (message.contains("Could not connect to the device Trezor") || message.contains("Device connect failed")) {
2021-07-07 17:04:31 +00:00
errMsg += "\n\nThis wallet is backed by a Trezor hardware device. Make sure the device is connected to your computer and unlocked.";
2021-07-01 21:00:47 +00:00
#if defined(Q_OS_LINUX)
errMsg += "\n\nNote: On Linux you may need to follow the instructions in the link below before the device can be opened:\n"
"https://wiki.trezor.io/Udev_rules";
2021-07-07 17:04:31 +00:00
link = "https://wiki.trezor.io/Udev_rules";
2021-05-25 18:00:02 +00:00
#endif
2021-05-18 15:59:18 +00:00
}
if (message.contains("SW_CLIENT_NOT_SUPPORTED")) {
errMsg += "\n\nIncompatible version: you may need to upgrade the Monero app on the Ledger device to the latest version.";
}
else if (message.contains("Wrong Device Status")) {
errMsg += "\n\nThe device may need to be unlocked.";
}
else if (message.contains("Wrong Channel")) {
errMsg += "\n\nRestart the hardware device and try again.";
}
QMessageBox msgBox;
msgBox.setWindowIcon(icons()->icon("appicons/64x64.png"));
msgBox.setIcon(QMessageBox::Warning);
msgBox.setText(errMsg);
msgBox.setWindowTitle("Wallet error");
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setDefaultButton(QMessageBox::Ok);
2021-07-07 17:04:31 +00:00
QPushButton *openLinkButton = nullptr;
if (!link.isEmpty()) {
openLinkButton = msgBox.addButton("Open link", QMessageBox::ActionRole);
}
2021-05-18 15:59:18 +00:00
msgBox.exec();
2021-07-07 17:04:31 +00:00
if (openLinkButton && msgBox.clickedButton() == openLinkButton) {
Utils::externalLinkWarning(nullptr, link);
}
2021-05-18 15:59:18 +00:00
}
// ######################## DEVICE ########################
void WindowManager::onDeviceButtonRequest(quint64 code) {
2021-07-01 21:00:47 +00:00
QString message;
switch (code) {
case 1: // Trezor
message = "Action required on device: enter your PIN to continue.";
break;
case 8: // Trezor
message = "Action required on device: Export watch-only credentials to open the wallet.";
break;
case 19: // Trezor
message = "Action required on device: Enter passphrase to open the wallet.";
break;
2021-07-01 21:00:47 +00:00
default:
message = "Action required on device: Export the view key to open the wallet.";
}
m_splashDialog->setMessage(message);
2021-05-18 15:59:18 +00:00
m_splashDialog->setIcon(QPixmap(":/assets/images/key.png"));
m_splashDialog->show();
m_splashDialog->setEnabled(true);
}
void WindowManager::onDeviceButtonPressed() {
m_splashDialog->hide();
}
2021-05-18 15:59:18 +00:00
void WindowManager::onDeviceError(const QString &errorMessage) {
// TODO: when does this get called?
qCritical() << Q_FUNC_INFO << errorMessage;
}
2021-07-08 00:34:27 +00:00
void WindowManager::onWalletPassphraseNeeded(bool on_device) {
auto button = QMessageBox::question(nullptr, "Wallet Passphrase Needed", "Enter passphrase on hardware wallet?\n\n"
"It is recommended to enter passphrase on "
"the hardware wallet for better security.",
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (button == QMessageBox::Yes) {
m_walletManager->onPassphraseEntered("", true, false);
return;
}
bool ok;
QString passphrase = QInputDialog::getText(nullptr, "Wallet Passphrase Needed", "Enter passphrase:", QLineEdit::EchoMode::Password, "", &ok);
if (ok) {
m_walletManager->onPassphraseEntered(passphrase, false, false);
} else {
m_walletManager->onPassphraseEntered(passphrase, false, true);
}
}
2021-05-18 15:59:18 +00:00
// ######################## TRAY ########################
void WindowManager::buildTrayMenu() {
QMenu *menu;
if (!m_tray->contextMenu()) {
menu = new QMenu();
m_tray->setContextMenu(menu);
} else {
menu = m_tray->contextMenu();
menu->clear();
}
for (const auto &window : m_windows) {
QString name = window->walletName();
QMenu *submenu = menu->addMenu(name);
submenu->addAction("Show/Hide", window, &MainWindow::showOrHide);
submenu->addAction("Close", window, &MainWindow::close);
}
menu->addSeparator();
menu->addAction("Exit Feather", this, &WindowManager::close);
}
// ######################## NETWORKING ########################
void WindowManager::onInitialNetworkConfigured() {
if (!m_initialNetworkConfigured) {
m_initialNetworkConfigured = true;
appData();
this->initTor();
this->initWS();
}
2021-05-18 15:59:18 +00:00
}
void WindowManager::initTor() {
torManager()->init();
torManager()->start();
connect(torManager(), &TorManager::connectionStateChanged, &websocketNotifier()->websocketClient, &WebsocketClient::onToggleConnect);
this->onTorSettingsChanged();
}
void WindowManager::onTorSettingsChanged() {
if (Utils::isTorsocks()) {
return;
}
// use local tor -> bundled tor
QString host = config()->get(Config::socks5Host).toString();
quint16 port = config()->get(Config::socks5Port).toString().toUShort();
if (!torManager()->isLocalTor()) {
host = torManager()->featherTorHost;
port = torManager()->featherTorPort;
}
QNetworkProxy proxy{QNetworkProxy::Socks5Proxy, host, port};
getNetworkTor()->setProxy(proxy);
websocketNotifier()->websocketClient.webSocket.setProxy(proxy);
emit torSettingsChanged();
}
void WindowManager::initWS() {
websocketNotifier()->websocketClient.start();
}
// ######################## WIZARD ########################
WalletWizard* WindowManager::createWizard(WalletWizard::Page startPage) const {
auto *wizard = new WalletWizard;
connect(wizard, &WalletWizard::initialNetworkConfigured, this, &WindowManager::onInitialNetworkConfigured);
connect(wizard, &WalletWizard::skinChanged, this, &WindowManager::changeSkin);
connect(wizard, &WalletWizard::openWallet, this, &WindowManager::tryOpenWallet);
connect(wizard, &WalletWizard::createWallet, this, &WindowManager::tryCreateWallet);
connect(wizard, &WalletWizard::createWalletFromKeys, this, &WindowManager::tryCreateWalletFromKeys);
connect(wizard, &WalletWizard::createWalletFromDevice, this, &WindowManager::tryCreateWalletFromDevice);
return wizard;
}
void WindowManager::initWizard() {
auto startPage = WalletWizard::Page_Menu;
if (config()->get(Config::firstRun).toBool() && !(TailsOS::detect() || WhonixOS::detect())) {
startPage = WalletWizard::Page_Network;
}
this->showWizard(startPage);
}
void WindowManager::showWizard(WalletWizard::Page startPage) {
if (!m_wizard) {
m_wizard = this->createWizard(startPage);
}
m_wizard->setStartId(startPage);
m_wizard->restart();
m_wizard->setEnabled(true);
m_wizard->show();
}
void WindowManager::wizardOpenWallet() {
this->showWizard(WalletWizard::Page_OpenWallet);
}
// ######################## SKINS ########################
void WindowManager::initSkins() {
m_skins.insert("Native", "");
QString qdarkstyle = this->loadStylesheet(":qdarkstyle/style.qss");
if (!qdarkstyle.isEmpty())
m_skins.insert("QDarkStyle", qdarkstyle);
QString breeze_dark = this->loadStylesheet(":/dark.qss");
if (!breeze_dark.isEmpty())
m_skins.insert("Breeze/Dark", breeze_dark);
QString breeze_light = this->loadStylesheet(":/light.qss");
if (!breeze_light.isEmpty())
m_skins.insert("Breeze/Light", breeze_light);
QString skin = config()->get(Config::skin).toString();
qApp->setStyleSheet(m_skins[skin]);
}
QString WindowManager::loadStylesheet(const QString &resource) {
QFile f(resource);
if (!f.exists()) {
printf("Unable to set stylesheet, file not found\n");
f.close();
return "";
}
f.open(QFile::ReadOnly | QFile::Text);
QTextStream ts(&f);
QString data = ts.readAll();
f.close();
return data;
}
void WindowManager::changeSkin(const QString &skinName) {
if (!m_skins.contains(skinName)) {
qWarning() << QString("No such skin %1").arg(skinName);
return;
}
config()->set(Config::skin, skinName);
qApp->setStyleSheet(m_skins[skinName]);
qDebug() << QString("Skin changed to %1").arg(skinName);
}