feather/src/MainWindow.cpp
2024-10-01 22:17:54 +02:00

1940 lines
No EOL
73 KiB
C++

// SPDX-License-Identifier: BSD-3-Clause
// SPDX-FileCopyrightText: 2020-2024 The Monero Project
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QCheckBox>
#include "constants.h"
#include "dialog/AddressCheckerIndexDialog.h"
#include "dialog/BalanceDialog.h"
#include "dialog/DebugInfoDialog.h"
#include "dialog/PasswordDialog.h"
#include "dialog/TxBroadcastDialog.h"
#include "dialog/TxConfAdvDialog.h"
#include "dialog/TxConfDialog.h"
#include "dialog/TxImportDialog.h"
#include "dialog/TxInfoDialog.h"
#include "dialog/TxPoolViewerDialog.h"
#include "dialog/ViewOnlyDialog.h"
#include "dialog/WalletInfoDialog.h"
#include "dialog/WalletCacheDebugDialog.h"
#include "libwalletqt/AddressBook.h"
#include "libwalletqt/rows/CoinsInfo.h"
#include "libwalletqt/Transfer.h"
#include "libwalletqt/TransactionHistory.h"
#include "model/AddressBookModel.h"
#include "plugins/PluginRegistry.h"
#include "utils/AppData.h"
#include "utils/AsyncTask.h"
#include "utils/ColorScheme.h"
#include "utils/Icons.h"
#include "utils/TorManager.h"
#include "utils/WebsocketNotifier.h"
#include "wallet/wallet_errors.h"
#ifdef WITH_SCANNER
#include "wizard/offline_tx_signing/OfflineTxSigningWizard.h"
#include "qrcode/scanner/URDialog.h"
#endif
#ifdef CHECK_UPDATES
#include "utils/updater/UpdateDialog.h"
#endif
MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, m_windowManager(windowManager)
, m_wallet(wallet)
, m_nodes(new Nodes(this, wallet))
, m_rpc(new DaemonRpc(this, ""))
{
ui->setupUi(this);
// Ensure the destructor is called after closeEvent()
setAttribute(Qt::WA_DeleteOnClose);
m_splashDialog = new SplashDialog(this);
m_accountSwitcherDialog = new AccountSwitcherDialog(m_wallet, this);
#ifdef CHECK_UPDATES
m_updater = QSharedPointer<Updater>(new Updater(this));
#endif
this->restoreGeo();
this->initStatusBar();
this->initPlugins();
this->initWidgets();
this->initMenu();
this->initOffline();
this->initWalletContext();
emit uiSetup();
this->onOfflineMode(conf()->get(Config::offlineMode).toBool());
conf()->set(Config::restartRequired, false);
// Websocket notifier
#ifdef CHECK_UPDATES
connect(websocketNotifier(), &WebsocketNotifier::UpdatesReceived, m_updater.data(), &Updater::wsUpdatesReceived);
#endif
websocketNotifier()->emitCache(); // Get cached data
connect(m_windowManager, &WindowManager::websocketStatusChanged, this, &MainWindow::onWebsocketStatusChanged);
this->onWebsocketStatusChanged(!conf()->get(Config::disableWebsocket).toBool());
connect(m_windowManager, &WindowManager::proxySettingsChanged, this, &MainWindow::onProxySettingsChangedConnect);
connect(m_windowManager, &WindowManager::updateBalance, m_wallet, &Wallet::updateBalance);
connect(m_windowManager, &WindowManager::offlineMode, this, &MainWindow::onOfflineMode);
connect(m_windowManager, &WindowManager::manualFeeSelectionEnabled, this, &MainWindow::onManualFeeSelectionEnabled);
connect(m_windowManager, &WindowManager::subtractFeeFromAmountEnabled, this, &MainWindow::onSubtractFeeFromAmountEnabled);
connect(torManager(), &TorManager::connectionStateChanged, this, &MainWindow::onTorConnectionStateChanged);
this->onTorConnectionStateChanged(torManager()->torConnected);
#ifdef CHECK_UPDATES
connect(m_updater.data(), &Updater::updateAvailable, this, &MainWindow::showUpdateNotification);
#endif
ColorScheme::updateFromWidget(this);
QTimer::singleShot(1, [this]{this->updateWidgetIcons();});
// Timers
connect(&m_updateBytes, &QTimer::timeout, this, &MainWindow::updateNetStats);
connect(&m_txTimer, &QTimer::timeout, [this]{
m_statusLabelStatus->setText("Constructing transaction" + this->statusDots());
});
conf()->set(Config::firstRun, false);
this->onWalletOpened();
#ifdef DONATE_BEG
this->donationNag();
#endif
connect(m_windowManager->eventFilter, &EventFilter::userActivity, this, &MainWindow::userActivity);
connect(&m_checkUserActivity, &QTimer::timeout, this, &MainWindow::checkUserActivity);
m_checkUserActivity.setInterval(5000);
m_checkUserActivity.start();
}
void MainWindow::initStatusBar() {
#if defined(Q_OS_WIN)
// No separators between statusbar widgets
this->statusBar()->setStyleSheet("QStatusBar::item {border: None;}");
#endif
this->statusBar()->setFixedHeight(30);
m_statusLabelStatus = new QLabel("Idle", this);
m_statusLabelStatus->setTextInteractionFlags(Qt::TextSelectableByMouse);
this->statusBar()->addWidget(m_statusLabelStatus);
m_statusLabelNetStats = new QLabel("", this);
m_statusLabelNetStats->setTextInteractionFlags(Qt::TextSelectableByMouse);
this->statusBar()->addWidget(m_statusLabelNetStats);
m_statusUpdateAvailable = new QPushButton(this);
m_statusUpdateAvailable->setFlat(true);
m_statusUpdateAvailable->setCursor(Qt::PointingHandCursor);
m_statusUpdateAvailable->setIcon(icons()->icon("tab_party.png"));
m_statusUpdateAvailable->hide();
this->statusBar()->addPermanentWidget(m_statusUpdateAvailable);
m_statusLabelBalance = new ClickableLabel(this);
m_statusLabelBalance->setText("Balance: 0 XMR");
m_statusLabelBalance->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_statusLabelBalance->setCursor(Qt::PointingHandCursor);
this->statusBar()->addPermanentWidget(m_statusLabelBalance);
connect(m_statusLabelBalance, &ClickableLabel::clicked, this, &MainWindow::showBalanceDialog);
m_statusBtnConnectionStatusIndicator = new StatusBarButton(icons()->icon("status_disconnected.svg"), "Connection status", this);
connect(m_statusBtnConnectionStatusIndicator, &StatusBarButton::clicked, [this](){
this->onShowSettingsPage(Settings::Pages::NETWORK);
});
this->statusBar()->addPermanentWidget(m_statusBtnConnectionStatusIndicator);
this->onConnectionStatusChanged(Wallet::ConnectionStatus_Disconnected);
m_statusAccountSwitcher = new StatusBarButton(icons()->icon("change_account.png"), "Account switcher", this);
connect(m_statusAccountSwitcher, &StatusBarButton::clicked, this, &MainWindow::showAccountSwitcherDialog);
this->statusBar()->addPermanentWidget(m_statusAccountSwitcher);
m_statusBtnPassword = new StatusBarButton(icons()->icon("lock.svg"), "Password", this);
connect(m_statusBtnPassword, &StatusBarButton::clicked, this, &MainWindow::showPasswordDialog);
this->statusBar()->addPermanentWidget(m_statusBtnPassword);
m_statusBtnPreferences = new StatusBarButton(icons()->icon("preferences.svg"), "Settings", this);
connect(m_statusBtnPreferences, &StatusBarButton::clicked, this, &MainWindow::menuSettingsClicked);
this->statusBar()->addPermanentWidget(m_statusBtnPreferences);
m_statusBtnSeed = new StatusBarButton(icons()->icon("seed.png"), "Seed", this);
connect(m_statusBtnSeed, &StatusBarButton::clicked, this, &MainWindow::showSeedDialog);
this->statusBar()->addPermanentWidget(m_statusBtnSeed);
m_statusBtnProxySettings = new StatusBarButton(icons()->icon("tor_logo_disabled.png"), "Proxy settings", this);
connect(m_statusBtnProxySettings, &StatusBarButton::clicked, this, &MainWindow::menuProxySettingsClicked);
this->statusBar()->addPermanentWidget(m_statusBtnProxySettings);
this->onProxySettingsChanged();
m_statusBtnHwDevice = new StatusBarButton(this->hardwareDevicePairedIcon(), this->getHardwareDevice(), this);
connect(m_statusBtnHwDevice, &StatusBarButton::clicked, this, &MainWindow::menuHwDeviceClicked);
this->statusBar()->addPermanentWidget(m_statusBtnHwDevice);
m_statusBtnHwDevice->hide();
}
void MainWindow::initPlugins() {
const QStringList enabledPlugins = conf()->get(Config::enabledPlugins).toStringList();
for (const auto& plugin_creator : PluginRegistry::getPluginCreators()) {
Plugin* plugin = plugin_creator();
if (!PluginRegistry::getInstance().isPluginEnabled(plugin->id())) {
continue;
}
qDebug() << "Initializing plugin: " << plugin->id();
plugin->initialize(m_wallet, this);
connect(plugin, &Plugin::setStatusText, this, &MainWindow::setStatusText);
connect(plugin, &Plugin::fillSendTab, this, &MainWindow::fillSendTab);
connect(this, &MainWindow::updateIcons, plugin, &Plugin::skinChanged);
connect(this, &MainWindow::aboutToQuit, plugin, &Plugin::aboutToQuit);
connect(this, &MainWindow::uiSetup, plugin, &Plugin::uiSetup);
m_plugins.append(plugin);
}
std::sort(m_plugins.begin(), m_plugins.end(), [](Plugin *a, Plugin *b) {
return a->idx() < b->idx();
});
}
void MainWindow::initWidgets() {
// [History]
m_historyWidget = new HistoryWidget(m_wallet, this);
ui->historyWidgetLayout->addWidget(m_historyWidget);
connect(m_historyWidget, &HistoryWidget::viewOnBlockExplorer, this, &MainWindow::onViewOnBlockExplorer);
connect(m_historyWidget, &HistoryWidget::resendTransaction, this, &MainWindow::onResendTransaction);
// [Send]
m_sendWidget = new SendWidget(m_wallet, this);
ui->sendWidgetLayout->addWidget(m_sendWidget);
// --------------
m_contactsWidget = new ContactsWidget(m_wallet, this);
ui->contactsWidgetLayout->addWidget(m_contactsWidget);
// [Receive]
m_receiveWidget = new ReceiveWidget(m_wallet, this);
ui->receiveWidgetLayout->addWidget(m_receiveWidget);
connect(m_receiveWidget, &ReceiveWidget::showTransactions, [this](const QString &text) {
m_historyWidget->setSearchText(text);
ui->tabWidget->setCurrentIndex(this->findTab("History"));
});
connect(m_contactsWidget, &ContactsWidget::fill, [this](const QString &address, const QString &description){
m_sendWidget->fill(address, description, 0, true);
});
// [Coins]
m_coinsWidget = new CoinsWidget(m_wallet, this);
ui->coinsWidgetLayout->addWidget(m_coinsWidget);
// [Plugins..]
for (auto* plugin : m_plugins) {
if (!plugin->hasParent()) {
qDebug() << "Adding tab: " << plugin->displayName();
if (plugin->insertFirst()) {
ui->tabWidget->insertTab(0, plugin->tab(), icons()->icon(plugin->icon()), plugin->displayName());
} else {
ui->tabWidget->addTab(plugin->tab(), icons()->icon(plugin->icon()), plugin->displayName());
}
for (auto* child : m_plugins) {
if (child->hasParent() && child->parent() == plugin->id()) {
plugin->addSubPlugin(child);
}
}
}
}
ui->frame_coinControl->setVisible(false);
connect(ui->btn_resetCoinControl, &QPushButton::clicked, [this]{
m_wallet->setSelectedInputs({});
});
m_walletUnlockWidget = new WalletUnlockWidget(this, m_wallet);
m_walletUnlockWidget->setWalletName(this->walletName());
ui->walletUnlockLayout->addWidget(m_walletUnlockWidget);
connect(m_walletUnlockWidget, &WalletUnlockWidget::closeWallet, this, &MainWindow::close);
connect(m_walletUnlockWidget, &WalletUnlockWidget::unlockWallet, this, &MainWindow::unlockWallet);
ui->tabWidget->setCurrentIndex(0);
ui->stackedWidget->setCurrentIndex(0);
}
void MainWindow::initMenu() {
// TODO: Rename actions to follow style
// [File]
connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::menuOpenClicked);
connect(ui->actionNew_Restore, &QAction::triggered, this, &MainWindow::menuNewRestoreClicked);
connect(ui->actionLock, &QAction::triggered, this, &MainWindow::lockWallet);
connect(ui->actionClose, &QAction::triggered, this, &MainWindow::menuWalletCloseClicked); // Close current wallet
connect(ui->actionQuit, &QAction::triggered, this, &MainWindow::menuQuitClicked); // Quit application
connect(ui->actionSettings, &QAction::triggered, this, &MainWindow::menuSettingsClicked);
// [File] -> [Recently open]
m_clearRecentlyOpenAction = new QAction("Clear history", ui->menuFile);
connect(m_clearRecentlyOpenAction, &QAction::triggered, this, &MainWindow::menuClearHistoryClicked);
// [Wallet]
connect(ui->actionInformation, &QAction::triggered, this, &MainWindow::showWalletInfoDialog);
connect(ui->actionAccount, &QAction::triggered, this, &MainWindow::showAccountSwitcherDialog);
connect(ui->actionPassword, &QAction::triggered, this, &MainWindow::showPasswordDialog);
connect(ui->actionSeed, &QAction::triggered, this, &MainWindow::showSeedDialog);
connect(ui->actionKeys, &QAction::triggered, this, &MainWindow::showKeysDialog);
connect(ui->actionViewOnly, &QAction::triggered, this, &MainWindow::showViewOnlyDialog);
// [Wallet] -> [Advanced]
connect(ui->actionStore_wallet, &QAction::triggered, this, &MainWindow::tryStoreWallet);
connect(ui->actionUpdate_balance, &QAction::triggered, [this]{m_wallet->updateBalance();});
connect(ui->actionRefresh_tabs, &QAction::triggered, [this]{m_wallet->refreshModels();});
connect(ui->actionRescan_spent, &QAction::triggered, this, &MainWindow::rescanSpent);
connect(ui->actionWallet_cache_debug, &QAction::triggered, this, &MainWindow::showWalletCacheDebugDialog);
connect(ui->actionTxPoolViewer, &QAction::triggered, this, &MainWindow::showTxPoolViewerDialog);
// [Wallet] -> [History]
connect(ui->actionExport_CSV, &QAction::triggered, this, &MainWindow::onExportHistoryCSV);
// [Wallet] -> [Contacts]
connect(ui->actionExportContactsCSV, &QAction::triggered, this, &MainWindow::onExportContactsCSV);
connect(ui->actionImportContactsCSV, &QAction::triggered, this, &MainWindow::importContacts);
// [View]
m_tabShowHideSignalMapper = new QSignalMapper(this);
connect(ui->actionShow_Searchbar, &QAction::toggled, this, &MainWindow::toggleSearchbar);
ui->actionShow_Searchbar->setChecked(conf()->get(Config::showSearchbar).toBool());
// Show/Hide Coins
connect(ui->actionShow_Coins, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map));
m_tabShowHideMapper["Coins"] = new ToggleTab(ui->tabCoins, "Coins", "Coins", ui->actionShow_Coins, this);
m_tabShowHideSignalMapper->setMapping(ui->actionShow_Coins, "Coins");
// Show/Hide Plugins..
for (const auto &plugin : m_plugins) {
if (plugin->parent() != "") {
continue;
}
auto* pluginAction = new QAction(QString("Show %1").arg(plugin->displayName()), this);
ui->menuView->insertAction(plugin->insertFirst() ? ui->actionPlaceholderBegin : ui->actionPlaceholderEnd, pluginAction);
connect(pluginAction, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map));
m_tabShowHideMapper[plugin->displayName()] = new ToggleTab(plugin->tab(), plugin->displayName(), plugin->displayName(), pluginAction, this);
m_tabShowHideSignalMapper->setMapping(pluginAction, plugin->displayName());
}
ui->actionPlaceholderBegin->setVisible(false);
ui->actionPlaceholderEnd->setVisible(false);
QStringList enabledTabs = conf()->get(Config::enabledTabs).toStringList();
for (const auto &key: m_tabShowHideMapper.keys()) {
const auto toggleTab = m_tabShowHideMapper.value(key);
bool show = enabledTabs.contains(key);
toggleTab->menuAction->setText((show ? QString("Hide ") : QString("Show ")) + toggleTab->name);
ui->tabWidget->setTabVisible(ui->tabWidget->indexOf(toggleTab->tab), show);
}
connect(m_tabShowHideSignalMapper, &QSignalMapper::mappedString, this, &MainWindow::menuToggleTabVisible);
// [Tools]
connect(ui->actionSignVerify, &QAction::triggered, this, &MainWindow::menuSignVerifyClicked);
connect(ui->actionVerifyTxProof, &QAction::triggered, this, &MainWindow::menuVerifyTxProof);
connect(ui->actionKeyImageSync, &QAction::triggered, this, &MainWindow::showKeyImageSyncWizard);
connect(ui->actionLoadSignedTxFromFile, &QAction::triggered, this, &MainWindow::loadSignedTx);
connect(ui->actionLoadSignedTxFromText, &QAction::triggered, this, &MainWindow::loadSignedTxFromText);
connect(ui->actionImport_transaction, &QAction::triggered, this, &MainWindow::importTransaction);
connect(ui->actionTransmitOverUR, &QAction::triggered, this, &MainWindow::showURDialog);
connect(ui->actionPay_to_many, &QAction::triggered, this, &MainWindow::payToMany);
connect(ui->actionAddress_checker, &QAction::triggered, this, &MainWindow::showAddressChecker);
connect(ui->actionCreateDesktopEntry, &QAction::triggered, this, &MainWindow::onCreateDesktopEntry);
if (m_wallet->viewOnly()) {
ui->actionKeyImageSync->setText("Key image sync");
}
// TODO: Allow creating desktop entry on Windows and Mac
#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
ui->actionCreateDesktopEntry->setDisabled(true);
#endif
#ifndef SELF_CONTAINED
ui->actionCreateDesktopEntry->setVisible(false);
#endif
// [Help]
connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::menuAboutClicked);
#if defined(CHECK_UPDATES)
connect(ui->actionCheckForUpdates, &QAction::triggered, this, &MainWindow::showUpdateDialog);
#else
ui->actionCheckForUpdates->setVisible(false);
#endif
connect(ui->actionOfficialWebsite, &QAction::triggered, [this](){Utils::externalLinkWarning(this, "https://featherwallet.org");});
connect(ui->actionDonate_to_Feather, &QAction::triggered, this, &MainWindow::donateButtonClicked);
connect(ui->actionDocumentation, &QAction::triggered, this, &MainWindow::onShowDocumentation);
connect(ui->actionReport_bug, &QAction::triggered, this, &MainWindow::onReportBug);
connect(ui->actionShow_debug_info, &QAction::triggered, this, &MainWindow::showDebugInfo);
// Setup shortcuts
ui->actionStore_wallet->setShortcut(QKeySequence("Ctrl+S"));
ui->actionRefresh_tabs->setShortcut(QKeySequence("Ctrl+R"));
ui->actionOpen->setShortcut(QKeySequence("Ctrl+O"));
ui->actionNew_Restore->setShortcut(QKeySequence("Ctrl+N"));
ui->actionLock->setShortcut(QKeySequence("Ctrl+L"));
ui->actionClose->setShortcut(QKeySequence("Ctrl+W"));
ui->actionShow_debug_info->setShortcut(QKeySequence("Ctrl+D"));
ui->actionSettings->setShortcut(QKeySequence("Ctrl+Alt+S"));
ui->actionUpdate_balance->setShortcut(QKeySequence("Ctrl+U"));
ui->actionShow_Searchbar->setShortcut(QKeySequence("Ctrl+F"));
ui->actionDocumentation->setShortcut(QKeySequence("F1"));
}
void MainWindow::initOffline() {
// TODO: check if we have any cameras available
ui->btn_help->setFocusPolicy(Qt::NoFocus);
ui->btn_viewOnlyDetails->setFocusPolicy(Qt::NoFocus);
ui->btn_checkAddress->setFocusPolicy(Qt::NoFocus);
ui->btn_signTransaction->setFocusPolicy(Qt::StrongFocus);
ui->btn_signTransaction->setFocus();
connect(ui->btn_help, &QPushButton::clicked, [this] {
windowManager()->showDocs(this, "offline_tx_signing");
});
connect(ui->btn_viewOnlyDetails, &QPushButton::clicked, [this] {
this->showViewOnlyDialog();
});
connect(ui->btn_checkAddress, &QPushButton::clicked, [this]{
AddressCheckerIndexDialog dialog{m_wallet, this};
dialog.exec();
});
connect(ui->btn_signTransaction, &QPushButton::clicked, [this] {
this->showKeyImageSyncWizard();
});
switch (conf()->get(Config::offlineTxSigningMethod).toInt()) {
case Config::OTSMethod::FileTransfer:
ui->radio_airgapFiles->setChecked(true);
break;
default:
ui->radio_airgapUR->setChecked(true);
}
// We can't use rich text for radio buttons
connect(ui->label_airgapUR, &ClickableLabel::clicked, [this] {
ui->radio_airgapUR->setChecked(true);
});
connect(ui->label_airgapFiles, &ClickableLabel::clicked, [this] {
ui->radio_airgapFiles->setChecked(true);
});
connect(ui->radio_airgapFiles, &QCheckBox::toggled, [this] (bool checked){
if (checked) {
conf()->set(Config::offlineTxSigningMethod, Config::OTSMethod::FileTransfer);
}
});
connect(ui->radio_airgapUR, &QCheckBox::toggled, [this](bool checked) {
if (checked) {
conf()->set(Config::offlineTxSigningMethod, Config::OTSMethod::UnifiedResources);
}
});
}
void MainWindow::initWalletContext() {
connect(m_wallet, &Wallet::balanceUpdated, this, &MainWindow::onBalanceUpdated);
connect(m_wallet, &Wallet::syncStatus, this, &MainWindow::onSyncStatus);
connect(m_wallet, &Wallet::transactionCreated, this, &MainWindow::onTransactionCreated);
connect(m_wallet, &Wallet::transactionCommitted, this, &MainWindow::onTransactionCommitted);
connect(m_wallet, &Wallet::initiateTransaction, this, &MainWindow::onInitiateTransaction);
connect(m_wallet, &Wallet::keysCorrupted, this, &MainWindow::onKeysCorrupted);
connect(m_wallet, &Wallet::selectedInputsChanged, this, &MainWindow::onSelectedInputsChanged);
connect(m_wallet, &Wallet::txPoolBacklog, this, &MainWindow::onTxPoolBacklog);
// Wallet
connect(m_wallet, &Wallet::connectionStatusChanged, [this](int status){
// Order is important, first inform UI about a potential disconnect, then reconnect
this->onConnectionStatusChanged(status);
m_nodes->autoConnect();
});
connect(m_wallet, &Wallet::currentSubaddressAccountChanged, this, &MainWindow::updateTitle);
connect(m_wallet, &Wallet::walletPassphraseNeeded, this, &MainWindow::onWalletPassphraseNeeded);
connect(m_wallet, &Wallet::unconfirmedMoneyReceived, this, [this](const QString &txId, uint64_t amount){
if (m_wallet->isSynchronized() && !m_locked) {
auto notify = QString("%1 XMR (pending)").arg(WalletManager::displayAmount(amount, false));
m_windowManager->notify("Payment received", notify, 5000);
}
});
// Device
connect(m_wallet, &Wallet::deviceButtonRequest, this, &MainWindow::onDeviceButtonRequest);
connect(m_wallet, &Wallet::deviceButtonPressed, this, &MainWindow::onDeviceButtonPressed);
connect(m_wallet, &Wallet::deviceError, this, &MainWindow::onDeviceError);
connect(m_wallet, &Wallet::donationSent, this, []{
conf()->set(Config::donateBeg, -1);
});
connect(m_wallet, &Wallet::multiBroadcast, this, &MainWindow::onMultiBroadcast);
}
void MainWindow::menuToggleTabVisible(const QString &key){
const auto toggleTab = m_tabShowHideMapper[key];
QStringList enabledTabs = conf()->get(Config::enabledTabs).toStringList();
bool show = enabledTabs.contains(key);
show = !show;
if (show) {
enabledTabs.append(key);
} else {
enabledTabs.removeAll(key);
}
conf()->set(Config::enabledTabs, enabledTabs);
ui->tabWidget->setTabVisible(ui->tabWidget->indexOf(toggleTab->tab), show);
toggleTab->menuAction->setText((show ? QString("Hide ") : QString("Show ")) + toggleTab->name);
}
void MainWindow::menuClearHistoryClicked() {
conf()->remove(Config::recentlyOpenedWallets);
this->updateRecentlyOpenedMenu();
}
QString MainWindow::walletName() {
return QFileInfo(m_wallet->cachePath()).fileName();
}
QString MainWindow::walletCachePath() {
return m_wallet->cachePath();
}
QString MainWindow::walletKeysPath() {
return m_wallet->keysPath();
}
void MainWindow::onWalletOpened() {
qDebug() << Q_FUNC_INFO;
m_splashDialog->hide();
m_wallet->setRingDatabase(Utils::ringDatabasePath());
m_wallet->updateBalance();
if (m_wallet->isHwBacked()) {
m_statusBtnHwDevice->show();
}
this->bringToFront();
this->setEnabled(true);
// receive page
m_wallet->subaddress()->refresh(m_wallet->currentSubaddressAccount());
if (m_wallet->subaddress()->count() == 1) {
for (int i = 0; i < 10; i++) {
m_wallet->subaddress()->addRow(m_wallet->currentSubaddressAccount(), "");
}
}
m_wallet->subaddressModel()->setCurrentSubaddressAccount(m_wallet->currentSubaddressAccount());
// history page
m_wallet->history()->refresh();
// coins page
m_wallet->coins()->refresh();
m_coinsWidget->setModel(m_wallet->coinsModel(), m_wallet->coins());
m_wallet->coinsModel()->setCurrentSubaddressAccount(m_wallet->currentSubaddressAccount());
// Coin labeling uses set_tx_note, so we need to refresh history too
connect(m_wallet->coins(), &Coins::descriptionChanged, [this] {
m_wallet->history()->refresh();
});
// Vice versa
connect(m_wallet->history(), &TransactionHistory::txNoteChanged, [this] {
m_wallet->coins()->refresh();
});
this->updatePasswordIcon();
this->updateTitle();
m_nodes->allowConnection();
m_nodes->connectToNode();
m_updateBytes.start(250);
if (conf()->get(Config::writeRecentlyOpenedWallets).toBool()) {
this->addToRecentlyOpened(m_wallet->cachePath());
}
}
void MainWindow::onBalanceUpdated(quint64 balance, quint64 spendable) {
bool hide = conf()->get(Config::hideBalance).toBool();
int displaySetting = conf()->get(Config::balanceDisplay).toInt();
int decimals = conf()->get(Config::amountPrecision).toInt();
QString balance_str = "Balance: ";
if (hide) {
balance_str += "HIDDEN";
}
else if (displaySetting == Config::totalBalance) {
balance_str += QString("%1 XMR").arg(WalletManager::displayAmount(balance, false, decimals));
}
else if (displaySetting == Config::spendable || displaySetting == Config::spendablePlusUnconfirmed) {
balance_str += QString("%1 XMR").arg(WalletManager::displayAmount(spendable, false, decimals));
if (displaySetting == Config::spendablePlusUnconfirmed && balance > spendable) {
balance_str += QString(" (+%1 XMR unconfirmed)").arg(WalletManager::displayAmount(balance - spendable, false, decimals));
}
}
m_statusLabelBalance->setToolTip("Click for details");
m_statusLabelBalance->setText(balance_str);
}
void MainWindow::setStatusText(const QString &text, bool override, int timeout) {
if (override) {
m_statusOverrideActive = true;
m_statusLabelStatus->setText(text);
QTimer::singleShot(timeout, [this]{
m_statusOverrideActive = false;
this->setStatusText(m_statusText);
});
return;
}
m_statusText = text;
if (!m_statusOverrideActive && !m_constructingTransaction) {
m_statusLabelStatus->setText(text);
}
}
void MainWindow::tryStoreWallet() {
if (m_wallet->connectionStatus() == Wallet::ConnectionStatus::ConnectionStatus_Synchronizing) {
Utils::showError(this, "Unable to save wallet", "Can't save wallet during synchronization", {"Wait until synchronization is finished and try again"}, "synchronization");
return;
}
m_wallet->store();
}
void MainWindow::onWebsocketStatusChanged(bool enabled) {
ui->actionShow_Home->setVisible(enabled);
QStringList enabledTabs = conf()->get(Config::enabledTabs).toStringList();
for (const auto &plugin : m_plugins) {
if (plugin->hasParent()) {
continue;
}
if (plugin->requiresWebsocket()) {
// TODO: unload plugins
ui->tabWidget->setTabVisible(this->findTab(plugin->displayName()), enabled && enabledTabs.contains(plugin->displayName()));
}
}
m_historyWidget->setWebsocketEnabled(enabled);
m_sendWidget->setWebsocketEnabled(enabled);
}
void MainWindow::onProxySettingsChangedConnect() {
m_nodes->connectToNode();
this->onProxySettingsChanged();
}
void MainWindow::onProxySettingsChanged() {
int proxy = conf()->get(Config::proxy).toInt();
if (proxy == Config::Proxy::Tor) {
this->onTorConnectionStateChanged(torManager()->torConnected);
m_statusBtnProxySettings->show();
return;
}
if (proxy == Config::Proxy::i2p) {
m_statusBtnProxySettings->setIcon(icons()->icon("i2p.png"));
m_statusBtnProxySettings->show();
return;
}
m_statusBtnProxySettings->hide();
}
void MainWindow::onOfflineMode(bool offline) {
m_wallet->setOffline(offline);
if (m_wallet->viewOnly()) {
return;
}
if (ui->stackedWidget->currentIndex() != Stack::LOCKED) {
ui->stackedWidget->setCurrentIndex(offline ? Stack::OFFLINE: Stack::WALLET);
}
ui->actionPay_to_many->setVisible(!offline);
ui->menuView->setDisabled(offline);
m_statusLabelBalance->setVisible(!offline);
m_statusBtnProxySettings->setVisible(!offline);
}
void MainWindow::onManualFeeSelectionEnabled(bool enabled) {
m_sendWidget->setManualFeeSelectionEnabled(enabled);
}
void MainWindow::onSubtractFeeFromAmountEnabled(bool enabled) {
m_sendWidget->setSubtractFeeFromAmountEnabled(enabled);
}
void MainWindow::onMultiBroadcast(const QMap<QString, QString> &txHexMap) {
QMapIterator<QString, QString> i(txHexMap);
while (i.hasNext()) {
i.next();
for (const auto& node: m_nodes->nodes()) {
QString address = node.toURL();
qDebug() << QString("Relaying %1 to: %2").arg(i.key(), address);
m_rpc->setDaemonAddress(address);
m_rpc->sendRawTransaction(i.value());
}
}
}
void MainWindow::onSyncStatus(quint64 height, quint64 target, bool daemonSync) {
if (height >= (target - 1)) {
this->updateNetStats();
}
this->setStatusText(Utils::formatSyncStatus(height, target, daemonSync));
m_statusLabelStatus->setToolTip(QString("Wallet height: %1").arg(QString::number(height)));
}
void MainWindow::onConnectionStatusChanged(int status)
{
// Note: Wallet does not emit this signal unless status is changed, so calling this function from MainWindow may
// result in the wrong connection status being displayed.
qDebug() << "Wallet connection status changed " << Utils::QtEnumToString(static_cast<Wallet::ConnectionStatus>(status));
// Update connection info in status bar.
QIcon icon;
if (conf()->get(Config::offlineMode).toBool()) {
icon = icons()->icon("status_offline.svg");
this->setStatusText("Offline mode");
} else {
switch(status){
case Wallet::ConnectionStatus_Disconnected:
icon = icons()->icon("status_disconnected.svg");
this->setStatusText("Disconnected");
break;
case Wallet::ConnectionStatus_Connecting:
icon = icons()->icon("status_lagging.svg");
this->setStatusText("Connecting to node");
break;
case Wallet::ConnectionStatus_WrongVersion:
icon = icons()->icon("status_disconnected.svg");
this->setStatusText("Incompatible node");
break;
case Wallet::ConnectionStatus_Synchronizing:
icon = icons()->icon("status_waiting.svg");
break;
case Wallet::ConnectionStatus_Synchronized:
icon = icons()->icon("status_connected.svg");
break;
default:
icon = icons()->icon("status_disconnected.svg");
break;
}
}
m_statusBtnConnectionStatusIndicator->setIcon(icon);
}
void MainWindow::onTransactionCreated(PendingTransaction *tx, const QVector<QString> &address) {
// Clean up some UI
m_constructingTransaction = false;
m_txTimer.stop();
this->setStatusText(m_statusText);
if (m_wallet->isHwBacked()) {
m_splashDialog->hide();
}
if (tx->status() != PendingTransaction::Status_Ok) {
if (m_showDeviceError) {
// The hardware devices has disconnected during tx construction.
// Due to a macOS-specific Qt bug, we have to prevent it from stacking two QMessageBoxes, otherwise
// the UI becomes unresponsive. The reconnect dialog should take priority.
m_wallet->disposeTransaction(tx);
return;
}
QString errMsg = tx->errorString();
Utils::Message message{this, Utils::ERROR, "Failed to construct transaction", errMsg};
if (tx->getException()) {
try
{
std::rethrow_exception(tx->getException());
}
catch (const tools::error::daemon_busy &e) {
message.description = QString("Node was unable to respond. Failed request: %1").arg(QString::fromStdString(e.request()));
message.helpItems = {"Try sending the transaction again.", "If this keeps happening, connect to a different node."};
}
catch (const tools::error::no_connection_to_daemon &e) {
message.description = QString("Connection to node lost. Failed request: %1").arg(QString::fromStdString(e.request()));
message.helpItems = {"Try sending the transaction again.", "If this keeps happening, connect to a different node."};
}
catch (const tools::error::wallet_rpc_error &e) {
message.description = QString("RPC error: %1").arg(QString::fromStdString(e.to_string()));
message.helpItems = {"Try sending the transaction again.", "If this keeps happening, connect to a different node."};
}
catch (const tools::error::get_outs_error &e) {
message.description = "Failed to get enough decoy outputs from node";
message.helpItems = {"Your transaction has too many inputs. Try sending a lower amount."};
}
catch (const tools::error::not_enough_unlocked_money &e) {
QString error;
if (e.fee() > e.available()) {
error = QString("Transaction fee exceeds spendable balance.\n\nSpendable balance: %1\nTransaction fee: %2").arg(WalletManager::displayAmount(e.available()), WalletManager::displayAmount(e.fee()));
}
else {
error = QString("Spendable balance insufficient to pay for transaction.\n\nSpendable balance: %1\nTransaction needs: %2").arg(WalletManager::displayAmount(e.available()), WalletManager::displayAmount(e.tx_amount() + e.fee()));
}
message.description = error;
message.helpItems = {"Wait for more balance to unlock.", "Click 'Help' to learn more about how balance works."};
message.doc = "balance";
}
catch (const tools::error::not_enough_money &e) {
message.description = QString("Not enough money to transfer\n\nTotal balance: %1\nTransaction amount: %2").arg(WalletManager::displayAmount(e.available()), WalletManager::displayAmount(e.tx_amount()));
message.helpItems = {"If you are trying to send your entire balance, click 'Max'."};
message.doc = "balance";
}
catch (const tools::error::tx_not_possible &e) {
message.description = QString("Not enough money to transfer. Transaction amount + fee exceeds available balance.\n\n"
"Spendable balance: %1\n"
"Transaction needs: %2").arg(WalletManager::displayAmount(e.available()), WalletManager::displayAmount(e.tx_amount() + e.fee()));
message.helpItems = {"If you're trying to send your entire balance, click 'Max'."};
message.doc = "balance";
}
catch (const tools::error::not_enough_outs_to_mix &e) {
message.description = "Not enough outputs for specified ring size.";
}
catch (const tools::error::tx_not_constructed&) {
message.description = "Transaction was not constructed";
message.helpItems = {"You have found a bug. Please contact the developers."};
message.doc = "report_an_issue";
}
catch (const tools::error::tx_rejected &e) {
// TODO: provide helptext
message.description = QString("Transaction was rejected by node. Reason: %1.").arg(QString::fromStdString(e.status()));
}
catch (const tools::error::tx_sum_overflow &e) {
message.description = "Transaction tries to spend an unrealistic amount of XMR";
message.helpItems = {"You have found a bug. Please contact the developers."};
message.doc = "report_an_issue";
}
catch (const tools::error::zero_amount&) {
message.description = "Destination amount is zero";
message.helpItems = {"You have found a bug. Please contact the developers."};
message.doc = "report_an_issue";
}
catch (const tools::error::zero_destination&) {
message.description = "Transaction has no destination";
message.helpItems = {"You have found a bug. Please contact the developers."};
message.doc = "report_an_issue";
}
catch (const tools::error::tx_too_big &e) {
message.description = "Transaction too big";
message.helpItems = {"Try sending a smaller amount."};
}
catch (const tools::error::transfer_error &e) {
message.description = QString("Unknown transfer error: %1").arg(QString::fromStdString(e.what()));
message.helpItems = {"You have found a bug. Please contact the developers."};
message.doc = "report_an_issue";
}
catch (const tools::error::wallet_internal_error &e) {
bool bug = true;
QString msg = e.what();
message.description = QString("Internal error: %1").arg(QString::fromStdString(e.what()));
if (msg.contains("Daemon response did not include the requested real output")) {
QString currentNode = m_nodes->connection().toAddress();
message.description += QString("\nYou are currently connected to: %1\n\n"
"This node may be acting maliciously. You are strongly recommended to disconnect from this node."
"Please report this incident to the developers.").arg(currentNode);
message.doc = "report_an_issue";
}
if (msg.startsWith("No unlocked balance")) {
// TODO: We're sending ALL, but fractional outputs got ignored
message.description = "Spendable balance insufficient to pay for transaction fee.";
bug = false;
}
if (msg.contains("Failed to get height") || msg.contains("Failed to get earliest fork height")) {
message.description = QString("RPC error: %1").arg(QString::fromStdString(e.to_string()));
message.helpItems = {"Try sending the transaction again.", "If this keeps happening, connect to a different node."};
bug = false;
}
if (bug) {
message.helpItems = {"You have found a bug. Please contact the developers."};
message.doc = "report_an_issue";
}
}
catch (const std::exception &e) {
message.description = QString::fromStdString(e.what());
}
}
Utils::showMsg(message);
m_wallet->disposeTransaction(tx);
return;
}
else if (tx->txCount() == 0) {
Utils::showError(this, "Failed to construct transaction", "No transactions were constructed", {"You have found a bug. Please contact the developers."}, "report_an_issue");
m_wallet->disposeTransaction(tx);
return;
}
else if (tx->txCount() > 1) {
Utils::showError(this, "Failed to construct transaction", "Transaction tries to spend too many inputs", {"Send a smaller amount of XMR to yourself first."});
m_wallet->disposeTransaction(tx);
return;
}
// This is a weak check to see if we send to all specified destination addresses
// This is here to catch rare memory corruption errors during transaction construction
// TODO: also check that amounts match
tx->refresh();
QSet<QString> outputAddresses;
for (const auto &output : tx->transaction(0)->outputs()) {
outputAddresses.insert(WalletManager::baseAddressFromIntegratedAddress(output->address(), constants::networkType));
}
QSet<QString> destAddresses;
for (const auto &addr : address) {
// TODO: Monero core bug, integrated address is not added to dests for transactions spending ALL
destAddresses.insert(WalletManager::baseAddressFromIntegratedAddress(addr, constants::networkType));
}
if (!outputAddresses.contains(destAddresses)) {
Utils::showError(this, "Transaction fails sanity check", "Constructed transaction doesn't appear to send to (all) specified destination address(es). Try creating the transaction again.");
m_wallet->disposeTransaction(tx);
return;
}
// Offline transaction signing
if (m_wallet->viewOnly()) {
#ifdef WITH_SCANNER
OfflineTxSigningWizard wizard(this, m_wallet, tx);
wizard.exec();
if (!wizard.readyToCommit()) {
return;
} else {
tx = wizard.signedTx();
}
if (tx->txCount() == 0) {
Utils::showError(this, "Failed to load transaction", "No transactions were found", {"You have found a bug. Please contact the developers."}, "report_an_issue");
m_wallet->disposeTransaction(tx);
return;
}
#else
Utils::showError(this, "Can't open offline transaction signing wizard", "Feather was built without webcam QR scanner support");
return;
#endif
}
m_wallet->addCacheTransaction(tx->txid()[0], tx->signedTxToHex(0));
// Show advanced dialog on multi-destination transactions
if (address.size() > 1) {
TxConfAdvDialog dialog_adv{m_wallet, m_wallet->tmpTxDescription, this};
dialog_adv.setTransaction(tx, !m_wallet->viewOnly());
dialog_adv.exec();
return;
}
TxConfDialog dialog{m_wallet, tx, address[0], m_wallet->tmpTxDescription, this};
switch (dialog.exec()) {
case QDialog::Rejected:
{
if (!dialog.showAdvanced) {
m_wallet->disposeTransaction(tx);
}
break;
}
case QDialog::Accepted:
m_wallet->commitTransaction(tx, m_wallet->tmpTxDescription);
break;
}
if (dialog.showAdvanced) {
TxConfAdvDialog dialog_adv{m_wallet, m_wallet->tmpTxDescription, this};
dialog_adv.setTransaction(tx);
dialog_adv.exec();
}
}
void MainWindow::onTransactionCommitted(bool success, PendingTransaction *tx, const QStringList& txid) {
if (!success) {
QString error = tx->errorString();
if (m_wallet->viewOnly() && error.contains("double spend")) {
m_wallet->setForceKeyImageSync(true);
}
if (error.contains("no connection to daemon")) {
QMessageBox box(this);
box.setWindowTitle("Question");
box.setText("Unable to send transaction");
box.setInformativeText("No connection to node. Retry sending transaction?");
QPushButton *manual = box.addButton("Broadcast manually", QMessageBox::HelpRole);
box.addButton(QMessageBox::No);
box.addButton(QMessageBox::Yes);
box.exec();
if (box.clickedButton() == manual) {
if (txid.empty()) {
Utils::showError(this, "Unable to open tx broadcaster", "Cached transaction not found");
return;
}
this->onResendTransaction(txid[0]);
}
else if (box.result() == QMessageBox::Yes) {
m_wallet->commitTransaction(tx, m_wallet->tmpTxDescription);
}
return;
}
Utils::showError(this, "Failed to send transaction", error);
return;
}
QMessageBox msgBox{this};
QPushButton *showDetailsButton = msgBox.addButton("Show details", QMessageBox::ActionRole);
msgBox.addButton(QMessageBox::Ok);
QString body = QString("Successfully sent %1 transaction(s).").arg(txid.count());
msgBox.setText(body);
msgBox.setWindowTitle("Transaction sent");
msgBox.setIcon(QMessageBox::Icon::Information);
msgBox.exec();
if (msgBox.clickedButton() == showDetailsButton) {
this->showHistoryTab();
TransactionRow *txInfo = m_wallet->history()->transaction(txid.first());
auto *dialog = new TxInfoDialog(m_wallet, txInfo, this);
connect(dialog, &TxInfoDialog::resendTranscation, this, &MainWindow::onResendTransaction);
dialog->show();
dialog->setAttribute(Qt::WA_DeleteOnClose);
}
m_sendWidget->clearFields();
}
void MainWindow::showWalletInfoDialog() {
WalletInfoDialog dialog{m_wallet, this};
dialog.exec();
}
void MainWindow::showSeedDialog() {
if (m_wallet->isHwBacked()) {
Utils::showInfo(this, "Seed unavailable", "Wallet keys are stored on a hardware device", {}, "show_wallet_seed");
return;
}
if (m_wallet->viewOnly()) {
Utils::showInfo(this, "Seed unavailable", "Wallet is view-only", {"To obtain your private spendkey go to Wallet -> Keys"}, "show_wallet_seed");
return;
}
if (!m_wallet->isDeterministic()) {
Utils::showInfo(this, "Seed unavailable", "Wallet is non-deterministic and has no seed",
{"To obtain wallet keys go to Wallet -> Keys"}, "show_wallet_seed");
return;
}
if (!this->verifyPassword()) {
return;
}
SeedDialog dialog{m_wallet, this};
dialog.exec();
}
void MainWindow::showPasswordDialog() {
PasswordChangeDialog dialog{this, m_wallet};
dialog.exec();
this->updatePasswordIcon();
}
void MainWindow::updatePasswordIcon() {
bool emptyPassword = m_wallet->verifyPassword("");
QIcon icon = emptyPassword ? icons()->icon("unlock.svg") : icons()->icon("lock.svg");
m_statusBtnPassword->setIcon(icon);
}
void MainWindow::showKeysDialog() {
if (!this->verifyPassword()) {
return;
}
KeysDialog dialog{m_wallet, this};
dialog.exec();
}
void MainWindow::showViewOnlyDialog() {
if (!this->verifyPassword()) {
return;
}
ViewOnlyDialog dialog{m_wallet, this};
dialog.exec();
}
void MainWindow::showKeyImageSyncWizard() {
#ifdef WITH_SCANNER
OfflineTxSigningWizard wizard{this, m_wallet};
wizard.exec();
if (wizard.readyToSign()) {
TxConfAdvDialog dialog{m_wallet, "", this, true};
dialog.setUnsignedTransaction(wizard.unsignedTransaction());
auto r = dialog.exec();
if (r != QDialog::Accepted) {
return;
}
wizard.setStartId(OfflineTxSigningWizard::Page_ExportSignedTx);
wizard.restart();
wizard.exec();
}
#else
Utils::showError(this, "Can't open offline transaction signing wizard", "Feather was built without webcam QR scanner support");
#endif
}
void MainWindow::menuHwDeviceClicked() {
Utils::showInfo(this, "Hardware device", QString("This wallet is backed by a %1 hardware device.").arg(this->getHardwareDevice()));
}
void MainWindow::menuOpenClicked() {
m_windowManager->wizardOpenWallet();
}
void MainWindow::menuNewRestoreClicked() {
m_windowManager->showWizard(WalletWizard::Page_Menu);
}
void MainWindow::menuQuitClicked() {
this->close();
}
void MainWindow::menuWalletCloseClicked() {
m_windowManager->showWizard(WalletWizard::Page_Menu);
this->close();
}
void MainWindow::menuProxySettingsClicked() {
this->menuSettingsClicked(true);
}
void MainWindow::menuAboutClicked() {
AboutDialog dialog{this};
dialog.exec();
}
void MainWindow::menuSettingsClicked(bool showProxyTab) {
m_windowManager->showSettings(m_nodes, this, showProxyTab);
}
void MainWindow::menuSignVerifyClicked() {
SignVerifyDialog dialog{m_wallet, this};
dialog.exec();
}
void MainWindow::menuVerifyTxProof() {
VerifyProofDialog dialog{m_wallet, this};
dialog.exec();
}
void MainWindow::onShowSettingsPage(int page) {
conf()->set(Config::lastSettingsPage, page);
this->menuSettingsClicked();
}
void MainWindow::skinChanged(const QString &skinName) {
ColorScheme::updateFromWidget(this);
this->updateWidgetIcons();
}
void MainWindow::updateWidgetIcons() {
m_sendWidget->skinChanged();
emit updateIcons();
m_statusBtnHwDevice->setIcon(this->hardwareDevicePairedIcon());
}
QIcon MainWindow::hardwareDevicePairedIcon() {
QString filename;
if (m_wallet->isLedger())
filename = "ledger.png";
else if (m_wallet->isTrezor())
filename = ColorScheme::darkScheme ? "trezor_white.png" : "trezor.png";
return icons()->icon(filename);
}
QIcon MainWindow::hardwareDeviceUnpairedIcon() {
QString filename;
if (m_wallet->isLedger())
filename = "ledger_unpaired.png";
else if (m_wallet->isTrezor())
filename = ColorScheme::darkScheme ? "trezor_unpaired_white.png" : "trezor_unpaired.png";
return icons()->icon(filename);
}
void MainWindow::closeEvent(QCloseEvent *event) {
qDebug() << Q_FUNC_INFO;
if (!this->cleanedUp) {
qDebug() << "MainWindow: cleaning up";
this->cleanedUp = true;
emit aboutToQuit();
m_historyWidget->resetModel();
m_updateBytes.stop();
m_txTimer.stop();
// Wallet signal may fire after AppContext is gone, causing segv
m_wallet->disconnect();
this->disconnect();
this->saveGeo();
m_windowManager->closeWindow(this);
}
event->accept();
}
void MainWindow::changeEvent(QEvent* event)
{
if ((event->type() == QEvent::WindowStateChange) && this->isMinimized()) {
if (conf()->get(Config::lockOnMinimize).toBool()) {
this->lockWallet();
}
} else {
QMainWindow::changeEvent(event);
}
}
void MainWindow::donateButtonClicked() {
m_sendWidget->fill(constants::donationAddress, constants::donationDescription);
ui->tabWidget->setCurrentIndex(this->findTab("Send"));
}
void MainWindow::showHistoryTab() {
this->raise();
ui->tabWidget->setCurrentIndex(this->findTab("History"));
}
void MainWindow::fillSendTab(const QString &address, const QString &description) {
m_sendWidget->fill(address, description);
ui->tabWidget->setCurrentIndex(this->findTab("Send"));
}
void MainWindow::payToMany() {
ui->tabWidget->setCurrentIndex(this->findTab("Send"));
m_sendWidget->payToMany();
Utils::showInfo(this, "Pay to many", "Enter a list of outputs in the 'Pay to' field.\n"
"One output per line.\n"
"Format: address, amount\n"
"A maximum of 16 addresses may be specified.");
}
void MainWindow::onViewOnBlockExplorer(const QString &txid) {
QString blockExplorerLink = Utils::blockExplorerLink(txid);
if (blockExplorerLink.isEmpty()) {
Utils::showError(this, "Unable to open block explorer", "No block explorer configured", {"Go to Settings -> Misc -> Block explorer"});
return;
}
Utils::externalLinkWarning(this, blockExplorerLink);
}
void MainWindow::onResendTransaction(const QString &txid) {
QString txHex = m_wallet->getCacheTransaction(txid);
if (txHex.isEmpty()) {
Utils::showError(this, "Unable to resend transaction", "Transaction was not found in the transaction cache.");
return;
}
// Connect to a different node so chances of successful relay are higher
m_nodes->autoConnect(true);
TxBroadcastDialog dialog{this, m_nodes, txHex};
dialog.exec();
}
void MainWindow::importContacts() {
const QString targetFile = QFileDialog::getOpenFileName(this, "Import CSV file", QDir::homePath(), "CSV Files (*.csv)");
if(targetFile.isEmpty()) return;
auto *model = m_wallet->addressBookModel();
QMapIterator<QString, QString> i(model->readCSV(targetFile));
int inserts = 0;
while (i.hasNext()) {
i.next();
bool addressValid = WalletManager::addressValid(i.value(), m_wallet->nettype());
if(addressValid) {
m_wallet->addressBook()->addRow(i.value(), i.key());
inserts++;
}
}
Utils::showInfo(this, "Contacts imported", QString("Total contacts imported: %1").arg(inserts));
}
void MainWindow::saveGeo() {
conf()->set(Config::geometry, QString(saveGeometry().toBase64()));
conf()->set(Config::windowState, QString(saveState().toBase64()));
}
void MainWindow::restoreGeo() {
bool geo = this->restoreGeometry(QByteArray::fromBase64(conf()->get(Config::geometry).toByteArray()));
bool windowState = this->restoreState(QByteArray::fromBase64(conf()->get(Config::windowState).toByteArray()));
qDebug() << "Restored window state: " << geo << " " << windowState;
}
void MainWindow::showDebugInfo() {
DebugInfoDialog dialog{m_wallet, m_nodes, this};
dialog.exec();
}
void MainWindow::showWalletCacheDebugDialog() {
if (!this->verifyPassword()) {
return;
}
WalletCacheDebugDialog dialog{m_wallet, this};
dialog.exec();
}
void MainWindow::showTxPoolViewerDialog() {
if (!m_txPoolViewerDialog) {
m_txPoolViewerDialog = new TxPoolViewerDialog{this, m_wallet};
}
m_txPoolViewerDialog->show();
}
void MainWindow::showAccountSwitcherDialog() {
m_accountSwitcherDialog->show();
m_accountSwitcherDialog->update();
}
void MainWindow::showAddressChecker() {
QString address = QInputDialog::getText(this, "Address Checker", "Address: ");
if (address.isEmpty()) {
return;
}
if (!WalletManager::addressValid(address, constants::networkType)) {
Utils::showInfo(this, "Invalid address", "The address you entered is not a valid XMR address for the current network type.");
return;
}
SubaddressIndex index = m_wallet->subaddressIndex(address);
if (!index.isValid()) {
// TODO: probably mention lookahead here
Utils::showInfo(this, "This address does not belong to this wallet", "");
return;
} else {
Utils::showInfo(this, QString("This address belongs to Account #%1").arg(index.major));
}
}
void MainWindow::showURDialog() {
#ifdef WITH_SCANNER
URDialog dialog{this};
dialog.exec();
#else
Utils::showError(this, "Unable to open UR dialog", "Feather was built without webcam scanner support");
#endif
}
void MainWindow::loadSignedTx() {
QString fn = QFileDialog::getOpenFileName(this, "Select transaction to load", QDir::homePath(), "Transaction (*signed_monero_tx);;All Files (*)");
if (fn.isEmpty()) return;
PendingTransaction *tx = m_wallet->loadSignedTxFile(fn);
auto err = m_wallet->errorString();
if (!err.isEmpty()) {
Utils::showError(this, "Unable to load signed transaction", err);
return;
}
TxConfAdvDialog dialog{m_wallet, "", this, true};
dialog.setTransaction(tx);
dialog.exec();
}
void MainWindow::loadSignedTxFromText() {
TxBroadcastDialog dialog{this, m_nodes};
dialog.exec();
}
void MainWindow::importTransaction() {
if (conf()->get(Config::torPrivacyLevel).toInt() == Config::allTorExceptNode) {
// TODO: don't show if connected to local node
auto result = QMessageBox::warning(this, "Warning", "Using this feature may allow a remote node to associate the transaction with your IP address.\n"
"\n"
"Connect to a trusted node or run Feather over Tor if network level metadata leakage is included in your threat model.",
QMessageBox::Ok | QMessageBox::Cancel);
if (result != QMessageBox::Ok) {
return;
}
}
TxImportDialog dialog(this, m_wallet);
dialog.exec();
}
void MainWindow::onDeviceError(const QString &error) {
qCritical() << "Device error: " << error;
if (m_showDeviceError) {
return;
}
m_statusBtnHwDevice->setIcon(this->hardwareDeviceUnpairedIcon());
while (true) {
m_showDeviceError = true;
auto result = QMessageBox::question(this, "Hardware device", "Lost connection to hardware device. Attempt to reconnect?");
if (result == QMessageBox::Yes) {
bool r = m_wallet->reconnectDevice();
if (r) {
break;
}
}
if (result == QMessageBox::No) {
this->menuWalletCloseClicked();
return;
}
}
m_statusBtnHwDevice->setIcon(this->hardwareDevicePairedIcon());
m_wallet->startRefresh();
m_showDeviceError = false;
}
void MainWindow::onDeviceButtonRequest(quint64 code) {
qDebug() << "DeviceButtonRequest, code: " << code;
if (m_wallet->isTrezor()) {
switch (code) {
case 1:
{
m_splashDialog->setMessage("Action required on device: Enter your PIN to continue");
m_splashDialog->setIcon(QPixmap(":/assets/images/key.png"));
m_splashDialog->show();
m_splashDialog->setEnabled(true);
break;
}
case 8:
default:
{
// Annoyingly, this code is used for a variety of actions, including:
// Confirm refresh: Do you really want to start refresh?
// Confirm export: Do you really want to export tx_key?
if (m_constructingTransaction) { // This code is also used when signing a tx, we handle this elsewhere
break;
}
m_splashDialog->setMessage("Confirm action on device to proceed");
m_splashDialog->setIcon(QPixmap(":/assets/images/confirmed.svg"));
m_splashDialog->show();
m_splashDialog->setEnabled(true);
break;
}
}
}
}
void MainWindow::onDeviceButtonPressed() {
if (m_constructingTransaction) {
return;
}
m_splashDialog->hide();
}
void MainWindow::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_wallet->onPassphraseEntered("", true, false);
return;
}
bool ok;
QString passphrase = QInputDialog::getText(nullptr, "Wallet Passphrase Needed", "Enter passphrase:", QLineEdit::EchoMode::Password, "", &ok);
if (ok) {
m_wallet->onPassphraseEntered(passphrase, false, false);
} else {
m_wallet->onPassphraseEntered(passphrase, false, true);
}
}
void MainWindow::updateNetStats() {
if (!m_wallet || m_wallet->connectionStatus() == Wallet::ConnectionStatus_Disconnected
|| m_wallet->connectionStatus() == Wallet::ConnectionStatus_Synchronized)
{
m_statusLabelNetStats->hide();
return;
}
m_statusLabelNetStats->show();
m_statusLabelNetStats->setText(QString("(D: %1)").arg(Utils::formatBytes(m_wallet->getBytesReceived())));
}
void MainWindow::rescanSpent() {
QMessageBox warning{this};
warning.setWindowTitle("Warning");
warning.setText("Rescanning spent outputs reveals which outputs you own to the node. "
"Make sure you are connected to a trusted node.\n\n"
"Do you want to proceed?");
warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
auto r = warning.exec();
if (r == QMessageBox::No) {
return;
}
if (!m_wallet->rescanSpent()) {
Utils::showError(this, "Failed to rescan spent outputs", m_wallet->errorString());
} else {
Utils::showInfo(this, "Successfully rescanned spent outputs");
}
}
void MainWindow::showBalanceDialog() {
BalanceDialog dialog{this, m_wallet};
dialog.exec();
}
QString MainWindow::statusDots() {
m_statusDots++;
m_statusDots = m_statusDots % 4;
return QString(".").repeated(m_statusDots);
}
void MainWindow::showOrHide() {
if (this->isHidden())
this->bringToFront();
else
this->hide();
}
void MainWindow::bringToFront() {
ensurePolished();
setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive);
show();
raise();
activateWindow();
}
void MainWindow::onPreferredFiatCurrencyChanged() {
m_sendWidget->onPreferredFiatCurrencyChanged();
}
void MainWindow::onHideUpdateNotifications(bool hidden) {
if (hidden) {
m_statusUpdateAvailable->hide();
}
#ifdef CHECK_UPDATES
else if (m_updater->state == Updater::State::UPDATE_AVAILABLE) {
m_statusUpdateAvailable->show();
}
#endif
}
void MainWindow::onTorConnectionStateChanged(bool connected) {
if (conf()->get(Config::proxy).toInt() != Config::Proxy::Tor) {
return;
}
if (connected)
m_statusBtnProxySettings->setIcon(icons()->icon("tor_logo.png"));
else
m_statusBtnProxySettings->setIcon(icons()->icon("tor_logo_disabled.png"));
}
void MainWindow::showUpdateNotification() {
#ifdef CHECK_UPDATES
if (conf()->get(Config::hideUpdateNotifications).toBool()) {
return;
}
QString versionDisplay{m_updater->version};
versionDisplay.replace("beta", "Beta");
QString updateText = QString("Update to Feather %1 is available").arg(versionDisplay);
m_statusUpdateAvailable->setText(updateText);
m_statusUpdateAvailable->setToolTip("Click to Download update.");
m_statusUpdateAvailable->show();
m_statusUpdateAvailable->disconnect();
connect(m_statusUpdateAvailable, &StatusBarButton::clicked, this, &MainWindow::showUpdateDialog);
#endif
}
void MainWindow::showUpdateDialog() {
#ifdef CHECK_UPDATES
UpdateDialog updateDialog{this, m_updater};
connect(&updateDialog, &UpdateDialog::restartWallet, m_windowManager, &WindowManager::restartApplication);
updateDialog.exec();
#endif
}
void MainWindow::onInitiateTransaction() {
m_statusDots = 0;
m_constructingTransaction = true;
m_txTimer.start(1000);
if (m_wallet->isHwBacked()) {
QString message = "Constructing transaction: action may be required on device.";
m_splashDialog->setMessage(message);
m_splashDialog->setIcon(QPixmap(":/assets/images/unconfirmed.png"));
m_splashDialog->show();
m_splashDialog->setEnabled(true);
}
}
void MainWindow::onKeysCorrupted() {
if (!m_criticalWarningShown) {
m_criticalWarningShown = true;
Utils::showError(this, "Potential wallet file corruption detected",
"WARNING!\n\n"
"To prevent LOSS OF FUNDS do NOT continue to use this wallet file.\n\n"
"Restore your wallet from seed, keys, or device.\n\n"
"Please report this incident to the Feather developers.\n\n"
"WARNING!", {}, "report_an_issue");
m_sendWidget->disallowSending();
}
}
void MainWindow::onSelectedInputsChanged(const QStringList &selectedInputs) {
int numInputs = selectedInputs.size();
ui->frame_coinControl->setStyleSheet(ColorScheme::GREEN.asStylesheet(true));
ui->frame_coinControl->setVisible(numInputs > 0);
if (numInputs > 0) {
quint64 totalAmount = 0;
auto coins = m_wallet->coins()->coinsFromKeyImage(selectedInputs);
for (const auto coin : coins) {
totalAmount += coin->amount();
}
QString text = QString("Coin control active: %1 selected outputs, %2 XMR").arg(QString::number(numInputs), WalletManager::displayAmount(totalAmount));
ui->label_coinControl->setText(text);
}
}
void MainWindow::onTxPoolBacklog(const QVector<quint64> &backlog, quint64 originalFeeLevel, quint64 automaticFeeLevel) {
bool automatic = (originalFeeLevel == 0);
if (automaticFeeLevel == 0) {
qWarning() << "Automatic fee level wasn't adjusted";
automaticFeeLevel = 2;
}
quint64 feeLevel = automatic ? automaticFeeLevel : originalFeeLevel;
for (int i = 0; i < backlog.size(); i++) {
qDebug() << QString("Fee level: %1, backlog: %2").arg(QString::number(i), QString::number(backlog[i]));
}
if (automatic) {
if (backlog.size() > 1 && backlog[1] >= 2) {
auto button = QMessageBox::question(this, "Transaction Pool Backlog",
QString("There is a backlog of %1 blocks (≈ %2 minutes) in the transaction pool "
"at the maximum automatic fee level.\n\n"
"Do you want to increase the fee for this transaction?")
.arg(QString::number(backlog[1]), QString::number(backlog[1] * 2)));
if (button == QMessageBox::Yes) {
feeLevel = 3;
}
}
}
m_wallet->confirmPreTransactionChecks(feeLevel);
}
void MainWindow::onExportHistoryCSV() {
QString fn = QFileDialog::getSaveFileName(this, "Save CSV file", QDir::homePath(), "CSV (*.csv)");
if (fn.isEmpty())
return;
if (!fn.endsWith(".csv"))
fn += ".csv";
m_wallet->history()->writeCSV(fn);
Utils::showInfo(this, "CSV export", QString("Transaction history exported to %1").arg(fn));
}
void MainWindow::onExportContactsCSV() {
auto *model = m_wallet->addressBookModel();
if (model->rowCount() <= 0){
Utils::showInfo(this, "Unable to export contacts", "No contacts to export");
return;
}
const QString targetDir = QFileDialog::getExistingDirectory(this, "Select CSV output directory ", QDir::homePath(), QFileDialog::ShowDirsOnly);
if(targetDir.isEmpty()) return;
qint64 now = QDateTime::currentMSecsSinceEpoch();
QString fn = QString("%1/monero-contacts_%2.csv").arg(targetDir, QString::number(now / 1000));
if (model->writeCSV(fn)) {
Utils::showInfo(this, "Contacts exported successfully", QString("Exported to: %1").arg(fn));
}
}
void MainWindow::onCreateDesktopEntry() {
auto msg = Utils::xdgDesktopEntryRegister() ? "Desktop entry created" : "Desktop entry not created due to an error.";
QMessageBox::information(this, "Desktop entry", msg);
}
void MainWindow::onShowDocumentation() {
// TODO: welcome page
m_windowManager->showDocs(this);
}
void MainWindow::onReportBug() {
m_windowManager->showDocs(this, "report_an_issue");
}
QString MainWindow::getHardwareDevice() {
if (!m_wallet->isHwBacked())
return "";
if (m_wallet->isTrezor())
return "Trezor";
if (m_wallet->isLedger())
return "Ledger";
return "Unknown";
}
void MainWindow::updateTitle() {
QString title = QString("%1 (#%2)").arg(this->walletName(), QString::number(m_wallet->currentSubaddressAccount()));
if (m_wallet->viewOnly()) {
title += " [view-only]";
}
title += " - Feather";
this->setWindowTitle(title);
}
void MainWindow::donationNag() {
if (m_wallet->nettype() != NetworkType::Type::MAINNET)
return;
if (m_wallet->viewOnly())
return;
if (m_wallet->balanceAll() == 0)
return;
auto donationCounter = conf()->get(Config::donateBeg).toInt();
if (donationCounter == -1)
return;
donationCounter++;
if (donationCounter % constants::donationBoundary == 0) {
auto msg = "Feather is a 100% community-sponsored endeavor. Please consider supporting "
"the project financially. Get rid of this message by donating any amount.";
int ret = QMessageBox::information(this, "Donate to Feather", msg, QMessageBox::Yes, QMessageBox::No);
if (ret == QMessageBox::Yes) {
this->donateButtonClicked();
}
}
conf()->set(Config::donateBeg, donationCounter);
}
void MainWindow::addToRecentlyOpened(QString keysFile) {
auto recent = conf()->get(Config::recentlyOpenedWallets).toList();
if (Utils::isPortableMode()) {
QDir appPath{Utils::applicationPath()};
keysFile = appPath.relativeFilePath(keysFile);
}
if (recent.contains(keysFile)) {
recent.removeOne(keysFile);
}
recent.insert(0, keysFile);
QList<QVariant> recent_;
int count = 0;
for (const auto &file : recent) {
if (Utils::fileExists(file.toString())) {
recent_.append(file);
count++;
}
if (count >= 5) {
break;
}
}
conf()->set(Config::recentlyOpenedWallets, recent_);
this->updateRecentlyOpenedMenu();
}
void MainWindow::updateRecentlyOpenedMenu() {
ui->menuRecently_open->clear();
const QStringList recentWallets = conf()->get(Config::recentlyOpenedWallets).toStringList();
for (const auto &walletPath : recentWallets) {
QFileInfo fileInfo{walletPath};
ui->menuRecently_open->addAction(fileInfo.fileName(), m_windowManager, std::bind(&WindowManager::tryOpenWallet, m_windowManager, fileInfo.absoluteFilePath(), ""));
}
ui->menuRecently_open->addSeparator();
ui->menuRecently_open->addAction(m_clearRecentlyOpenAction);
}
bool MainWindow::verifyPassword(bool sensitive) {
bool incorrectPassword = false;
while (true) {
PasswordDialog passwordDialog{this->walletName(), incorrectPassword, sensitive, this};
int ret = passwordDialog.exec();
if (ret == QDialog::Rejected) {
return false;
}
if (!m_wallet->verifyPassword(passwordDialog.password)) {
incorrectPassword = true;
continue;
}
break;
}
return true;
}
void MainWindow::userActivity() {
m_userLastActive = QDateTime::currentSecsSinceEpoch();
}
void MainWindow::closeQDialogChildren(QObject *object) {
for (QObject *child : object->children()) {
if (auto *childDlg = dynamic_cast<QDialog*>(child)) {
qDebug() << "Closing dialog: " << childDlg->objectName();
childDlg->close();
}
this->closeQDialogChildren(child);
}
}
void MainWindow::checkUserActivity() {
if (!conf()->get(Config::inactivityLockEnabled).toBool()) {
return;
}
if (m_constructingTransaction) {
return;
}
if ((m_userLastActive + (conf()->get(Config::inactivityLockTimeout).toInt()*60)) < QDateTime::currentSecsSinceEpoch()) {
qInfo() << "Locking wallet for inactivity";
this->lockWallet();
}
}
void MainWindow::lockWallet() {
if (m_locked) {
return;
}
if (m_constructingTransaction) {
Utils::showError(this, "Unable to lock wallet", "Can't lock wallet during transaction construction");
return;
}
m_walletUnlockWidget->reset();
// Close all open QDialogs
this->closeQDialogChildren(this);
ui->tabWidget->hide();
this->statusBar()->hide();
this->menuBar()->hide();
ui->stackedWidget->setCurrentIndex(1);
m_checkUserActivity.stop();
m_locked = true;
}
void MainWindow::unlockWallet(const QString &password) {
if (!m_locked) {
return;
}
if (!m_wallet->verifyPassword(password)) {
m_walletUnlockWidget->incorrectPassword();
return;
}
m_walletUnlockWidget->reset();
ui->tabWidget->show();
this->statusBar()->show();
this->menuBar()->show();
ui->stackedWidget->setCurrentIndex(0);
this->onOfflineMode(conf()->get(Config::offlineMode).toBool());
m_checkUserActivity.start();
m_locked = false;
}
void MainWindow::toggleSearchbar(bool visible) {
conf()->set(Config::showSearchbar, visible);
m_historyWidget->setSearchbarVisible(visible);
m_receiveWidget->setSearchbarVisible(visible);
m_contactsWidget->setSearchbarVisible(visible);
m_coinsWidget->setSearchbarVisible(visible);
int currentTab = ui->tabWidget->currentIndex();
if (currentTab == this->findTab("History"))
m_historyWidget->focusSearchbar();
else if (currentTab == this->findTab("Send"))
m_contactsWidget->focusSearchbar();
else if (currentTab == this->findTab("Receive"))
m_receiveWidget->focusSearchbar();
else if (currentTab == this->findTab("Coins"))
m_coinsWidget->focusSearchbar();
}
int MainWindow::findTab(const QString &title) {
for (int i = 0; i < ui->tabWidget->count(); i++) {
if (ui->tabWidget->tabText(i) == title) {
return i;
}
}
return -1;
}
MainWindow::~MainWindow() {
qDebug() << "~MainWindow" << QThread::currentThreadId();
}