diff --git a/CMakeLists.txt b/CMakeLists.txt index abb160f..c9806b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,7 @@ if(DEBUG) set(CMAKE_VERBOSE_MAKEFILE ON) endif() -set(MONERO_HEAD "2390030d10b69c357165f82aaf417391a9e11019") +set(MONERO_HEAD "2fc0c6355d7f3756f9cc01f1165aeec42bc52201") set(BUILD_GUI_DEPS ON) set(ARCH "x86-64") set(BUILD_64 ON) diff --git a/monero b/monero index 2390030..2fc0c63 160000 --- a/monero +++ b/monero @@ -1 +1 @@ -Subproject commit 2390030d10b69c357165f82aaf417391a9e11019 +Subproject commit 2fc0c6355d7f3756f9cc01f1165aeec42bc52201 diff --git a/src/appcontext.cpp b/src/appcontext.cpp index 5017164..f8ce281 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -113,7 +113,6 @@ AppContext::AppContext(QCommandLineParser *cmdargs) { this->nodes = new Nodes(this, this->networkClearnet); connect(this, &AppContext::nodeSourceChanged, this->nodes, &Nodes::onNodeSourceChanged); connect(this, &AppContext::setCustomNodes, this->nodes, &Nodes::setCustomNodes); - connect(this, &AppContext::walletClosing, this->nodes, &Nodes::onWalletClosing); // Tor & socks proxy this->ws = new WSClient(this, m_wsUrl); @@ -261,7 +260,6 @@ void AppContext::onCreateTransactionError(const QString &msg) { } void AppContext::walletClose(bool emitClosedSignal) { - this->nodes->stopTimer(); if(this->currentWallet == nullptr) return; emit walletClosing(); //ctx->currentWallet->store(); @TODO: uncomment to store on wallet close @@ -341,6 +339,9 @@ void AppContext::onWalletOpened(Wallet *wallet) { emit walletOpened(); + connect(this->currentWallet, &Wallet::connectionStatusChanged, [this]{ + this->nodes->autoConnect(); + }); this->nodes->connectToNode(); this->updateBalance(); @@ -723,13 +724,15 @@ void AppContext::onWalletUpdate() { this->storeWallet(); } -void AppContext::onWalletRefreshed() { +void AppContext::onWalletRefreshed(bool success) { if (!this->refreshed) { refreshModels(); this->refreshed = true; this->storeWallet(); } + qDebug() << "Wallet refresh status: " << success; + this->currentWallet->refreshHeightAsync(); } @@ -746,7 +749,7 @@ void AppContext::onWalletNewBlock(quint64 blockheight, quint64 targetHeight) { void AppContext::onHeightRefreshed(quint64 walletHeight, quint64 daemonHeight, quint64 targetHeight) { qDebug() << Q_FUNC_INFO << walletHeight << daemonHeight << targetHeight; - if (!this->currentWallet->connected()) + if (this->currentWallet->connectionStatus() == Wallet::ConnectionStatus_Disconnected) return; if (daemonHeight < targetHeight) { diff --git a/src/appcontext.h b/src/appcontext.h index f4d346d..b2c58ad 100644 --- a/src/appcontext.h +++ b/src/appcontext.h @@ -126,7 +126,7 @@ private slots: void onMoneyReceived(const QString &txId, quint64 amount); void onUnconfirmedMoneyReceived(const QString &txId, quint64 amount); void onWalletUpdate(); - void onWalletRefreshed(); + void onWalletRefreshed(bool success); void onWalletOpened(Wallet *wallet); void onWalletNewBlock(quint64 blockheight, quint64 targetHeight); void onHeightRefreshed(quint64 walletHeight, quint64 daemonHeight, quint64 targetHeight); diff --git a/src/dialog/debuginfodialog.cpp b/src/dialog/debuginfodialog.cpp index 7363387..d3b00a7 100644 --- a/src/dialog/debuginfodialog.cpp +++ b/src/dialog/debuginfodialog.cpp @@ -12,15 +12,26 @@ DebugInfoDialog::DebugInfoDialog(AppContext *ctx, QWidget *parent) : QDialog(parent) , ui(new Ui::DebugInfoDialog) + , m_ctx(ctx) { ui->setupUi(this); + connect(ui->btn_Copy, &QPushButton::clicked, this, &DebugInfoDialog::copyToClipboad); + + m_updateTimer.start(5000); + connect(&m_updateTimer, &QTimer::timeout, this, &DebugInfoDialog::updateInfo); + this->updateInfo(); + + this->adjustSize(); +} + +void DebugInfoDialog::updateInfo() { QString torStatus; - if(ctx->isTorSocks) + if(m_ctx->isTorSocks) torStatus = "Torsocks"; - else if(ctx->tor->localTor) + else if(m_ctx->tor->localTor) torStatus = "Local (assumed to be running)"; - else if(ctx->tor->torConnected) + else if(m_ctx->tor->torConnected) torStatus = "Running"; else torStatus = "Unknown"; @@ -28,32 +39,28 @@ DebugInfoDialog::DebugInfoDialog(AppContext *ctx, QWidget *parent) ui->label_featherVersion->setText(QString("%1-%2").arg(FEATHER_VERSION, FEATHER_BRANCH)); ui->label_moneroVersion->setText(QString("%1-%2").arg(MONERO_VERSION, MONERO_BRANCH)); - ui->label_walletHeight->setText(QString::number(ctx->currentWallet->blockChainHeight())); - ui->label_daemonHeight->setText(QString::number(ctx->currentWallet->daemonBlockChainHeight())); - ui->label_targetHeight->setText(QString::number(ctx->currentWallet->daemonBlockChainTargetHeight())); - ui->label_restoreHeight->setText(QString::number(ctx->currentWallet->getWalletCreationHeight())); - ui->label_synchronized->setText(ctx->currentWallet->synchronized() ? "True" : "False"); + ui->label_walletHeight->setText(QString::number(m_ctx->currentWallet->blockChainHeight())); + ui->label_daemonHeight->setText(QString::number(m_ctx->currentWallet->daemonBlockChainHeight())); + ui->label_targetHeight->setText(QString::number(m_ctx->currentWallet->daemonBlockChainTargetHeight())); + ui->label_restoreHeight->setText(QString::number(m_ctx->currentWallet->getWalletCreationHeight())); + ui->label_synchronized->setText(m_ctx->currentWallet->synchronized() ? "True" : "False"); - auto node = ctx->nodes->connection(); + auto node = m_ctx->nodes->connection(); ui->label_remoteNode->setText(node.full); - ui->label_walletStatus->setText(this->statusToString(ctx->currentWallet->connected())); + ui->label_walletStatus->setText(this->statusToString(m_ctx->currentWallet->connectionStatus())); ui->label_torStatus->setText(torStatus); - ui->label_websocketStatus->setText(Utils::QtEnumToString(ctx->ws->webSocket.state())); + ui->label_websocketStatus->setText(Utils::QtEnumToString(m_ctx->ws->webSocket.state())); - ui->label_netType->setText(Utils::QtEnumToString(ctx->currentWallet->nettype())); - ui->label_seedType->setText(ctx->currentWallet->getCacheAttribute("feather.seed").isEmpty() ? "25 word" : "14 word"); - ui->label_viewOnly->setText(ctx->currentWallet->viewOnly() ? "True" : "False"); + ui->label_netType->setText(Utils::QtEnumToString(m_ctx->currentWallet->nettype())); + ui->label_seedType->setText(m_ctx->currentWallet->getCacheAttribute("feather.seed").isEmpty() ? "25 word" : "14 word"); + ui->label_viewOnly->setText(m_ctx->currentWallet->viewOnly() ? "True" : "False"); QString os = QSysInfo::prettyProductName(); - if (ctx->isTails) { + if (m_ctx->isTails) { os = QString("Tails %1").arg(TailsOS::version()); } ui->label_OS->setText(os); ui->label_timestamp->setText(QString::number(QDateTime::currentSecsSinceEpoch())); - - connect(ui->btn_Copy, &QPushButton::clicked, this, &DebugInfoDialog::copyToClipboad); - - this->adjustSize(); } QString DebugInfoDialog::statusToString(Wallet::ConnectionStatus status) { @@ -72,27 +79,28 @@ QString DebugInfoDialog::statusToString(Wallet::ConnectionStatus status) { } void DebugInfoDialog::copyToClipboad() { + // Two spaces at the end of each line are for newlines in Markdown QString text = ""; - text += QString("Feather version: %1\n").arg(ui->label_featherVersion->text()); - text += QString("Monero version: %1\n").arg(ui->label_moneroVersion->text()); - - text += QString("Wallet height: %1\n").arg(ui->label_walletHeight->text()); - text += QString("Daemon height: %1\n").arg(ui->label_daemonHeight->text()); - text += QString("Target height: %1\n").arg(ui->label_targetHeight->text()); - text += QString("Restore height: %1\n").arg(ui->label_restoreHeight->text()); - text += QString("Synchronized: %1\n").arg(ui->label_synchronized->text()); - - text += QString("Remote node: %1\n").arg(ui->label_remoteNode->text()); - text += QString("Wallet status: %1\n").arg(ui->label_walletStatus->text()); - text += QString("Tor status: %1\n").arg(ui->label_torStatus->text()); - text += QString("Websocket status: %1\n").arg(ui->label_websocketStatus->text()); - - text += QString("Network type: %1\n").arg(ui->label_netType->text()); - text += QString("Seed type: %1\n").arg(ui->label_seedType->text()); - text += QString("View only: %1\n").arg(ui->label_viewOnly->text()); - - text += QString("Operating system: %1\n").arg(ui->label_OS->text()); - text += QString("Timestamp: %1\n").arg(ui->label_timestamp->text()); + text += QString("Feather version: %1 \n").arg(ui->label_featherVersion->text()); + text += QString("Monero version: %1 \n").arg(ui->label_moneroVersion->text()); + + text += QString("Wallet height: %1 \n").arg(ui->label_walletHeight->text()); + text += QString("Daemon height: %1 \n").arg(ui->label_daemonHeight->text()); + text += QString("Target height: %1 \n").arg(ui->label_targetHeight->text()); + text += QString("Restore height: %1 \n").arg(ui->label_restoreHeight->text()); + text += QString("Synchronized: %1 \n").arg(ui->label_synchronized->text()); + + text += QString("Remote node: %1 \n").arg(ui->label_remoteNode->text()); + text += QString("Wallet status: %1 \n").arg(ui->label_walletStatus->text()); + text += QString("Tor status: %1 \n").arg(ui->label_torStatus->text()); + text += QString("Websocket status: %1 \n").arg(ui->label_websocketStatus->text()); + + text += QString("Network type: %1 \n").arg(ui->label_netType->text()); + text += QString("Seed type: %1 \n").arg(ui->label_seedType->text()); + text += QString("View only: %1 \n").arg(ui->label_viewOnly->text()); + + text += QString("Operating system: %1 \n").arg(ui->label_OS->text()); + text += QString("Timestamp: %1 \n").arg(ui->label_timestamp->text()); Utils::copyToClipboard(text); } diff --git a/src/dialog/debuginfodialog.h b/src/dialog/debuginfodialog.h index 2443df1..d9a6e4a 100644 --- a/src/dialog/debuginfodialog.h +++ b/src/dialog/debuginfodialog.h @@ -23,6 +23,10 @@ public: private: QString statusToString(Wallet::ConnectionStatus status); void copyToClipboad(); + void updateInfo(); + + QTimer m_updateTimer; + AppContext *m_ctx; Ui::DebugInfoDialog *ui; }; diff --git a/src/dialog/debuginfodialog.ui b/src/dialog/debuginfodialog.ui index a613e6c..ecace2c 100644 --- a/src/dialog/debuginfodialog.ui +++ b/src/dialog/debuginfodialog.ui @@ -7,7 +7,7 @@ 0 0 693 - 580 + 612 @@ -311,6 +311,9 @@ TextLabel + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 31ab716..9e1abc6 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -47,6 +47,11 @@ Wallet::Wallet(QObject * parent) { } +Wallet::ConnectionStatus Wallet::connectionStatus() const +{ + return m_connectionStatus; +} + QString Wallet::getSeed() const { return QString::fromStdString(m_walletImpl->seed()); @@ -72,36 +77,6 @@ NetworkType::Type Wallet::nettype() const return static_cast(m_walletImpl->nettype()); } - -void Wallet::updateConnectionStatusAsync() -{ - m_scheduler.run([this] { - if (m_connectionStatus == Wallet::ConnectionStatus_Disconnected) - { - setConnectionStatus(ConnectionStatus_Connecting); - } - ConnectionStatus newStatus = static_cast(m_walletImpl->connected()); - if (newStatus != m_connectionStatus || !m_initialized) { - m_initialized = true; - setConnectionStatus(newStatus); - } - // Release lock - m_connectionStatusRunning = false; - }); -} - -Wallet::ConnectionStatus Wallet::connected(bool forceCheck) -{ - // cache connection status - if (forceCheck || !m_initialized || (m_connectionStatusTime.elapsed() / 1000 > m_connectionStatusTtl && !m_connectionStatusRunning) || m_connectionStatusTime.elapsed() > 30000) { - m_connectionStatusRunning = true; - m_connectionStatusTime.restart(); - updateConnectionStatusAsync(); - } - - return m_connectionStatus; -} - bool Wallet::disconnected() const { return m_disconnected; @@ -120,6 +95,9 @@ void Wallet::refreshingSet(bool value) } } +void Wallet::setConnectionTimeout(int timeout) { + m_connectionTimeout = timeout; +} void Wallet::setConnectionStatus(ConnectionStatus value) { @@ -232,16 +210,16 @@ bool Wallet::init(const QString &daemonAddress, bool trustedDaemon, quint64 uppe { QMutexLocker locker(&m_proxyMutex); - if (!m_walletImpl->init(daemonAddress.toStdString(), upperTransactionLimit, m_daemonUsername.toStdString(), m_daemonPassword.toStdString(), false, false, proxyAddress.toStdString())) + if (!m_walletImpl->init(daemonAddress.toStdString(), upperTransactionLimit, m_daemonUsername.toStdString(), m_daemonPassword.toStdString(), m_useSSL, false, proxyAddress.toStdString())) { return false; } - m_proxyAddress = proxyAddress; } emit proxyAddressChanged(); + setTrustedDaemon(trustedDaemon); setTrustedDaemon(trustedDaemon); return true; } @@ -269,7 +247,7 @@ void Wallet::initAsync( { emit walletCreationHeightChanged(); qDebug() << "init async finished - starting refresh"; - connected(true); + refreshHeightAsync(); startRefresh(); } }); @@ -313,6 +291,11 @@ void Wallet::setTrustedDaemon(bool arg) m_walletImpl->setTrustedDaemon(arg); } +void Wallet::setUseSSL(bool ssl) +{ + m_useSSL = ssl; +} + bool Wallet::viewOnly() const { return m_walletImpl->watchOnly(); @@ -425,6 +408,8 @@ void Wallet::refreshHeightAsync() daemonHeightFuture.second.waitForFinished(); targetHeightFuture.second.waitForFinished(); + setConnectionStatus(ConnectionStatus_Connected); + emit heightRefreshed(walletHeight, daemonHeight, targetHeight); }); } @@ -439,7 +424,8 @@ quint64 Wallet::daemonBlockChainHeight() const // cache daemon blockchain height for some time (60 seconds by default) if (m_daemonBlockChainHeight == 0 - || m_daemonBlockChainHeightTime.elapsed() / 1000 > m_daemonBlockChainHeightTtl) { + || m_daemonBlockChainHeightTime.elapsed() / 1000 > m_daemonBlockChainHeightTtl) + { m_daemonBlockChainHeight = m_walletImpl->daemonBlockChainHeight(); m_daemonBlockChainHeightTime.restart(); } @@ -449,7 +435,8 @@ quint64 Wallet::daemonBlockChainHeight() const quint64 Wallet::daemonBlockChainTargetHeight() const { if (m_daemonBlockChainTargetHeight <= 1 - || m_daemonBlockChainTargetHeightTime.elapsed() / 1000 > m_daemonBlockChainTargetHeightTtl) { + || m_daemonBlockChainTargetHeightTime.elapsed() / 1000 > m_daemonBlockChainTargetHeightTtl) + { m_daemonBlockChainTargetHeight = m_walletImpl->daemonBlockChainTargetHeight(); // Target height is set to 0 if daemon is synced. @@ -501,6 +488,7 @@ bool Wallet::importTransaction(const QString& txid, const QVector& outp void Wallet::startRefresh() { m_refreshEnabled = true; + m_refreshNow = true; } void Wallet::pauseRefresh() @@ -1089,6 +1077,14 @@ void Wallet::onWalletPassphraseNeeded(bool on_device) emit this->walletPassphraseNeeded(on_device); } +quint64 Wallet::getBytesReceived() const { + return m_walletImpl->getBytesReceived(); +} + +quint64 Wallet::getBytesSent() const { + return m_walletImpl->getBytesSent(); +} + void Wallet::onPassphraseEntered(const QString &passphrase, bool enter_on_device, bool entry_abort) { if (m_walletListener != nullptr) @@ -1117,9 +1113,11 @@ Wallet::Wallet(Monero::Wallet *w, QObject *parent) , m_subaddressAccount(nullptr) , m_subaddressAccountModel(nullptr) , m_coinsModel(nullptr) + , m_refreshNow(false) , m_refreshEnabled(false) , m_refreshing(false) , m_scheduler(this) + , m_useSSL(true) { m_history = new TransactionHistory(m_walletImpl->history(), this); m_addressBook = new AddressBook(m_walletImpl->addressBook(), this); @@ -1191,8 +1189,9 @@ void Wallet::startRefreshThread() { const auto now = std::chrono::steady_clock::now(); const auto elapsed = now - last; - if (elapsed >= refreshInterval) + if (elapsed >= refreshInterval || m_refreshNow) { + m_refreshNow = false; refresh(false); last = std::chrono::steady_clock::now(); } @@ -1205,4 +1204,12 @@ void Wallet::startRefreshThread() { throw std::runtime_error("failed to start auto refresh thread"); } +} + +void Wallet::onRefreshed(bool success) { + if (success) { + setConnectionStatus(ConnectionStatus_Connected); + } else { + setConnectionStatus(ConnectionStatus_Disconnected); + } } \ No newline at end of file diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index a9c88ad..f44edc4 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -62,7 +62,7 @@ Q_OBJECT Q_PROPERTY(QString seedLanguage READ getSeedLanguage) Q_PROPERTY(Status status READ status) Q_PROPERTY(NetworkType::Type nettype READ nettype) -// Q_PROPERTY(ConnectionStatus connected READ connected) + Q_PROPERTY(ConnectionStatus connectionStatus READ connectionStatus) Q_PROPERTY(quint32 currentSubaddressAccount READ currentSubaddressAccount NOTIFY currentSubaddressAccountChanged) Q_PROPERTY(bool synchronized READ synchronized) Q_PROPERTY(QString errorString READ errorString) @@ -105,6 +105,9 @@ public: Q_ENUM(ConnectionStatus) + //! return connection status + ConnectionStatus connectionStatus() const; + //! returns mnemonic seed QString getSeed() const; @@ -120,10 +123,6 @@ public: //! returns network type of the wallet. NetworkType::Type nettype() const; - //! returns whether the wallet is connected, and version status - Q_INVOKABLE ConnectionStatus connected(bool forceCheck = false); - void updateConnectionStatusAsync(); - //! returns true if wallet was ever synchronized bool synchronized() const; @@ -167,9 +166,15 @@ public: //! connects to daemon Q_INVOKABLE bool connectToDaemon(); - //! indicates id daemon is trusted + //! set connect to daemon timeout + Q_INVOKABLE void setConnectionTimeout(int timeout); + + //! indicates if daemon is trusted Q_INVOKABLE void setTrustedDaemon(bool arg); + //! indicates if ssl should be used to connect to daemon + Q_INVOKABLE void setUseSSL(bool ssl); + //! returns balance Q_INVOKABLE quint64 balance() const; Q_INVOKABLE quint64 balance(quint32 accountIndex) const; @@ -394,6 +399,9 @@ public: Q_INVOKABLE void onPassphraseEntered(const QString &passphrase, bool enter_on_device, bool entry_abort=false); virtual void onWalletPassphraseNeeded(bool on_device) override; + Q_INVOKABLE quint64 getBytesReceived() const; + Q_INVOKABLE quint64 getBytesSent() const; + // TODO: setListenter() when it implemented in API signals: // emitted on every event happened with wallet @@ -402,7 +410,7 @@ signals: // emitted when refresh process finished (could take a long time) // signalling only after we - void refreshed(); + void refreshed(bool success); void moneySpent(const QString &txId, quint64 amount); void moneyReceived(const QString &txId, quint64 amount); @@ -445,6 +453,7 @@ private: bool disconnected() const; bool refreshing() const; void refreshingSet(bool value); + void onRefreshed(bool success); void setConnectionStatus(ConnectionStatus value); QString getProxyAddress() const; @@ -489,10 +498,13 @@ private: QString m_daemonPassword; QString m_proxyAddress; mutable QMutex m_proxyMutex; + std::atomic m_refreshNow; std::atomic m_refreshEnabled; std::atomic m_refreshing; WalletListenerImpl *m_walletListener; FutureScheduler m_scheduler; + int m_connectionTimeout = 30; + bool m_useSSL; }; diff --git a/src/libwalletqt/WalletListenerImpl.cpp b/src/libwalletqt/WalletListenerImpl.cpp index d857b9e..2a025c8 100644 --- a/src/libwalletqt/WalletListenerImpl.cpp +++ b/src/libwalletqt/WalletListenerImpl.cpp @@ -41,10 +41,11 @@ void WalletListenerImpl::updated() } // called when wallet refreshed by background thread or explicitly -void WalletListenerImpl::refreshed() +void WalletListenerImpl::refreshed(bool success) { qDebug() << __FUNCTION__; - emit m_wallet->refreshed(); + m_wallet->onRefreshed(success); + emit m_wallet->refreshed(success); } void WalletListenerImpl::onDeviceButtonRequest(uint64_t code) diff --git a/src/libwalletqt/WalletListenerImpl.h b/src/libwalletqt/WalletListenerImpl.h index 120ddf4..109ae59 100644 --- a/src/libwalletqt/WalletListenerImpl.h +++ b/src/libwalletqt/WalletListenerImpl.h @@ -25,7 +25,7 @@ public: virtual void updated() override; // called when wallet refreshed by background thread or explicitly - virtual void refreshed() override; + virtual void refreshed(bool success) override; virtual void onDeviceButtonRequest(uint64_t code) override; diff --git a/src/libwalletqt/WalletManager.cpp b/src/libwalletqt/WalletManager.cpp index d0caa6e..17cf353 100644 --- a/src/libwalletqt/WalletManager.cpp +++ b/src/libwalletqt/WalletManager.cpp @@ -26,7 +26,7 @@ public: virtual void unconfirmedMoneyReceived(const std::string &txId, uint64_t amount) override { (void)txId; (void)amount; }; virtual void newBlock(uint64_t height) override { (void) height; }; virtual void updated() override {}; - virtual void refreshed() override {}; + virtual void refreshed(bool success) override {}; virtual void onPassphraseEntered(const QString &passphrase, bool enter_on_device, bool entry_abort) override { @@ -335,7 +335,7 @@ bool WalletManager::isMining() const { { QMutexLocker locker(&m_mutex); - if (m_currentWallet == nullptr || !m_currentWallet->connected()) + if (m_currentWallet == nullptr || !m_currentWallet->connectionStatus()) { return false; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f377784..5037b02 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -366,6 +366,8 @@ MainWindow::MainWindow(AppContext *ctx, QWidget *parent) : this->initMain(); this->initWidgets(); this->initMenu(); + + connect(&m_updateBytes, &QTimer::timeout, this, &MainWindow::updateNetStats); } void MainWindow::initMain() { @@ -669,6 +671,8 @@ void MainWindow::onWalletOpened() { this->touchbarShowWallet(); this->updatePasswordIcon(); + + m_updateBytes.start(1000); } void MainWindow::onBalanceUpdated(double balance, double unlocked, const QString &balance_str, const QString &unlocked_str) { @@ -688,6 +692,7 @@ void MainWindow::onBalanceUpdated(double balance, double unlocked, const QString } void MainWindow::onSynchronized() { + this->updateNetStats(); m_statusLabelStatus->setText("Synchronized"); this->onConnectionStatusChanged(Wallet::ConnectionStatus_Connected); } @@ -700,11 +705,12 @@ void MainWindow::onBlockchainSync(int height, int target) { void MainWindow::onRefreshSync(int height, int target) { QString heightText = QString("Wallet refresh: %1/%2").arg(height).arg(target); m_statusLabelStatus->setText(heightText); + this->updateNetStats(); } void MainWindow::onConnectionStatusChanged(int status) { - qDebug() << "Wallet connection status changed " << status; + qDebug() << "Wallet connection status changed " << Utils::QtEnumToString(static_cast(status)); // Update connection info in status bar. @@ -746,7 +752,7 @@ void MainWindow::onCreateTransactionSuccess(PendingTransaction *tx, const QStrin auto tx_err = tx->errorString(); qCritical() << tx_err; - if(m_ctx->currentWallet->connected() == Wallet::ConnectionStatus_WrongVersion) + if (m_ctx->currentWallet->connectionStatus() == Wallet::ConnectionStatus_WrongVersion) err = QString("%1 Wrong daemon version: %2").arg(err).arg(tx_err); else err = QString("%1 %2").arg(err).arg(tx_err); @@ -826,6 +832,10 @@ void MainWindow::create_status_bar() { 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_statusLabelBalance = new QLabel("Balance: 0.00 XMR", this); m_statusLabelBalance->setTextInteractionFlags(Qt::TextSelectableByMouse); this->statusBar()->addPermanentWidget(m_statusLabelBalance); @@ -864,7 +874,7 @@ void MainWindow::showSeedDialog() { } void MainWindow::showConnectionStatusDialog() { - auto status = m_ctx->currentWallet->connected(true); + auto status = m_ctx->currentWallet->connectionStatus(); bool synchronized = m_ctx->currentWallet->synchronized(); QString statusMsg; @@ -891,6 +901,9 @@ void MainWindow::showConnectionStatusDialog() { statusMsg = "Unknown connection status (this should never happen)."; } + statusMsg += QString("\n\nTx: %1, Rx: %2").arg(Utils::formatBytes(m_ctx->currentWallet->getBytesSent()), + Utils::formatBytes(m_ctx->currentWallet->getBytesReceived())); + QMessageBox::information(this, "Connection Status", statusMsg); } @@ -1304,6 +1317,20 @@ void MainWindow::importTransaction() { } } +void MainWindow::updateNetStats() { + if (!m_ctx->currentWallet) { + m_statusLabelNetStats->setText(""); + return; + } + + if (m_ctx->currentWallet->connectionStatus() == Wallet::ConnectionStatus_Connected && m_ctx->currentWallet->synchronized()) { + m_statusLabelNetStats->setText(""); + return; + } + + m_statusLabelNetStats->setText(QString("(D: %1)").arg(Utils::formatBytes(m_ctx->currentWallet->getBytesReceived()))); +} + MainWindow::~MainWindow() { delete ui; } diff --git a/src/mainwindow.h b/src/mainwindow.h index 7a2ae36..2bc86c6 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -163,6 +163,7 @@ private: void touchbarShowWizard(); void touchbarShowWallet(); void updatePasswordIcon(); + void updateNetStats(); WalletWizard *createWizard(WalletWizard::Page startPage); @@ -192,6 +193,7 @@ private: // lower status bar QLabel *m_statusLabelBalance; QLabel *m_statusLabelStatus; + QLabel *m_statusLabelNetStats; StatusBarButton *m_statusBtnConnectionStatusIndicator; StatusBarButton *m_statusBtnPassword; StatusBarButton *m_statusBtnPreferences; @@ -213,6 +215,8 @@ private: QMap m_skins; + QTimer m_updateBytes; + private slots: void menuToggleTabVisible(const QString &key); }; diff --git a/src/model/NodeModel.cpp b/src/model/NodeModel.cpp index 6b465b5..60ff93e 100644 --- a/src/model/NodeModel.cpp +++ b/src/model/NodeModel.cpp @@ -70,9 +70,9 @@ QVariant NodeModel::data(const QModelIndex &index, int role) const { } else if(role == Qt::BackgroundRole) { if (node.isConnecting) - return QBrush(QColor(186, 247, 255)); + return QBrush(QColor("#A9DEF9")); else if (node.isActive) - return QBrush(QColor(158, 250, 158)); + return QBrush(QColor("#78BC61")); } return QVariant(); } diff --git a/src/settings.cpp b/src/settings.cpp index 19c8b77..4e787fc 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -32,7 +32,7 @@ Settings::Settings(QWidget *parent) : // nodes ui->nodeWidget->setupUI(m_ctx); connect(ui->nodeWidget, &NodeWidget::nodeSourceChanged, m_ctx->nodes, &Nodes::onNodeSourceChanged); - connect(ui->nodeWidget, &NodeWidget::connectToNode, m_ctx->nodes, QOverload::of(&Nodes::connectToNode)); + connect(ui->nodeWidget, &NodeWidget::connectToNode, m_ctx->nodes, QOverload::of(&Nodes::connectToNode)); // setup checkboxes ui->checkBox_externalLink->setChecked(config()->get(Config::warnOnExternalLink).toBool()); diff --git a/src/ui/qdarkstyle/style.qss b/src/ui/qdarkstyle/style.qss index 4605041..46a592f 100644 --- a/src/ui/qdarkstyle/style.qss +++ b/src/ui/qdarkstyle/style.qss @@ -1831,6 +1831,7 @@ QColumnView:selected { color: #32414B; } + QTreeView:hover, QListView:hover, QTableView:hover, @@ -1838,6 +1839,7 @@ QColumnView:hover { background-color: #19232D; } + QTreeView::item:pressed, QListView::item:pressed, QTableView::item:pressed, diff --git a/src/utils/nodes.cpp b/src/utils/nodes.cpp index 0dd07d9..e071a55 100644 --- a/src/utils/nodes.cpp +++ b/src/utils/nodes.cpp @@ -4,11 +4,9 @@ #include #include #include -#include #include "nodes.h" #include "utils/utils.h" -#include "utils/networking.h" #include "appcontext.h" Nodes::Nodes(AppContext *ctx, QNetworkAccessManager *networkAccessManager, QObject *parent) : @@ -19,23 +17,20 @@ Nodes::Nodes(AppContext *ctx, QNetworkAccessManager *networkAccessManager, QObje modelWebsocket(new NodeModel(NodeSource::websocket, this)), modelCustom(new NodeModel(NodeSource::custom, this)) { this->loadConfig(); - - connect(m_connectionTimer, &QTimer::timeout, this, &Nodes::onConnectionTimer); } void Nodes::loadConfig() { - QString msg; auto configNodes = config()->get(Config::nodes).toByteArray(); auto key = QString::number(m_ctx->networkType); if (!Utils::validateJSON(configNodes)) { m_configJson[key] = QJsonObject(); - qCritical() << "fixed malformed config key \"nodes\""; + qCritical() << "Fixed malformed config key \"nodes\""; } QJsonDocument doc = QJsonDocument::fromJson(configNodes); m_configJson = doc.object(); - if(!m_configJson.contains(key)) + if (!m_configJson.contains(key)) m_configJson[key] = QJsonObject(); auto obj = m_configJson.value(key).toObject(); @@ -46,7 +41,7 @@ void Nodes::loadConfig() { // load custom nodes auto nodes = obj.value("custom").toArray(); - foreach (const QJsonValue &value, nodes) { + for (auto value: nodes) { auto customNode = FeatherNode(value.toString()); customNode.custom = true; @@ -62,15 +57,15 @@ void Nodes::loadConfig() { // load cached websocket nodes auto ws = obj.value("ws").toArray(); - foreach (const QJsonValue &value, ws) { + for (auto value: ws) { auto wsNode = FeatherNode(value.toString()); wsNode.custom = false; wsNode.online = true; // assume online - if(m_connection == wsNode) { - if(m_connection.isActive) + if (m_connection == wsNode) { + if (m_connection.isActive) wsNode.isActive = true; - else if(m_connection.isConnecting) + else if (m_connection.isConnecting) wsNode.isConnecting = true; } @@ -81,14 +76,12 @@ void Nodes::loadConfig() { obj["source"] = NodeSource::websocket; m_source = static_cast(obj.value("source").toInt()); - if(m_websocketNodes.count() > 0){ - msg = QString("Loaded %1 cached websocket nodes from config").arg(m_websocketNodes.count()); - activityLog.append(msg); + if (m_websocketNodes.count() > 0) { + qDebug() << QString("Loaded %1 cached websocket nodes from config").arg(m_websocketNodes.count()); } - if(m_customNodes.count() > 0){ - msg = QString("Loaded %1 custom nodes from config").arg(m_customNodes.count()); - activityLog.append(msg); + if (m_customNodes.count() > 0) { + qDebug() << QString("Loaded %1 custom nodes from config").arg(m_customNodes.count()); } m_configJson[key] = obj; @@ -101,33 +94,30 @@ void Nodes::writeConfig() { QString output(doc.toJson(QJsonDocument::Compact)); config()->set(Config::nodes, output); - auto msg = QString("Saved node config."); - activityLog.append(msg); + qDebug() << "Saved node config."; } void Nodes::connectToNode() { // auto connect - m_connectionAttempts.clear(); m_wsExhaustedWarningEmitted = false; m_customExhaustedWarningEmitted = false; - m_connectionTimer->start(2000); - this->onConnectionTimer(); + this->autoConnect(); } -void Nodes::connectToNode(FeatherNode node) { - if(node.address.isEmpty()) +void Nodes::connectToNode(const FeatherNode &node) { + if (node.address.isEmpty()) return; emit updateStatus(QString("Connecting to %1").arg(node.address)); - auto msg = QString("Attempting to connect to %1 (%2)") - .arg(node.address).arg(node.custom ? "custom" : "ws"); - qInfo() << msg; - activityLog.append(msg); + qInfo() << QString("Attempting to connect to %1 (%2)").arg(node.address).arg(node.custom ? "custom" : "ws"); if (!node.username.isEmpty() && !node.password.isEmpty()) m_ctx->currentWallet->setDaemonLogin(node.username, node.password); + + // Don't use SSL over Tor + m_ctx->currentWallet->setUseSSL(!node.tor); + m_ctx->currentWallet->initAsync(node.address, true, 0, false, false, 0); - m_connectionAttemptTime = std::time(nullptr); m_connection = node; m_connection.isActive = false; @@ -135,88 +125,55 @@ void Nodes::connectToNode(FeatherNode node) { this->resetLocalState(); this->updateModels(); - - m_connectionTimer->start(1000); } -void Nodes::onConnectionTimer() { +void Nodes::autoConnect(bool forceReconnect) { // this function is responsible for automatically connecting to a daemon. - if (m_ctx->currentWallet == nullptr) { - m_connectionTimer->stop(); + if (m_ctx->currentWallet == nullptr || !m_enableAutoconnect) { return; } - QString msg; - Wallet::ConnectionStatus status = m_ctx->currentWallet->connected(true); - NodeSource nodeSource = this->source(); - auto wsMode = (nodeSource == NodeSource::websocket); + Wallet::ConnectionStatus status = m_ctx->currentWallet->connectionStatus(); + bool wsMode = (this->source() == NodeSource::websocket); auto nodes = wsMode ? m_customNodes : m_websocketNodes; if (wsMode && !m_wsNodesReceived && m_websocketNodes.count() == 0) { // this situation should rarely occur due to the usage of the websocket node cache on startup. - msg = QString("Feather is in websocket connection mode but was not able to receive any nodes (yet)."); - qInfo() << msg; - activityLog.append(msg); + qInfo() << "Feather is in websocket connection mode but was not able to receive any nodes (yet)."; return; } - if (status == Wallet::ConnectionStatus::ConnectionStatus_Disconnected) { + if (status == Wallet::ConnectionStatus_Disconnected || forceReconnect) { + if (!m_connection.address.isEmpty() && !forceReconnect) { + m_recentFailures << m_connection.address; + } + // try a connect auto node = this->pickEligibleNode(); this->connectToNode(node); return; - } else if (status == Wallet::ConnectionStatus::ConnectionStatus_Connecting){ - if (!m_connection.isConnecting) { - // Weirdly enough, status == connecting directly after a wallet is opened. - auto node = this->pickEligibleNode(); - this->connectToNode(node); - return; - } - - // determine timeout - unsigned int nodeConnectionTimeout = 6; - if(m_connection.tor) - nodeConnectionTimeout = 25; - - auto connectionTimeout = static_cast(std::time(nullptr) - m_connectionAttemptTime); - if(connectionTimeout < nodeConnectionTimeout) - return; // timeout not reached yet - - msg = QString("Node connection attempt stale after %1 seconds, picking new node").arg(nodeConnectionTimeout); - activityLog.append(msg); - qInfo() << msg; - - auto newNode = this->pickEligibleNode(); - this->connectToNode(newNode); - return; - } else if(status == Wallet::ConnectionStatus::ConnectionStatus_Connected) { - // wallet is connected to daemon successfully, poll status every 3 seconds - if(!m_connection.isConnecting) - return; - - msg = QString("Node connected to %1").arg(m_connection.address); - qInfo() << msg; - activityLog.append(msg); + } + else if (status == Wallet::ConnectionStatus_Connected && m_connection.isConnecting) { + qInfo() << QString("Node connected to %1").arg(m_connection.address); // set current connection object m_connection.isConnecting = false; m_connection.isActive = true; - this->resetLocalState(); - this->updateModels(); // reset node exhaustion state - m_connectionAttempts.clear(); m_wsExhaustedWarningEmitted = false; m_customExhaustedWarningEmitted = false; - m_connectionTimer->setInterval(3000); + m_recentFailures.clear(); } + + this->resetLocalState(); + this->updateModels(); } FeatherNode Nodes::pickEligibleNode() { // Pick a node at random to connect to auto rtn = FeatherNode(); - NodeSource nodeSource = this->source(); - auto wsMode = nodeSource == NodeSource::websocket; + auto wsMode = (this->source() == NodeSource::websocket); auto nodes = wsMode ? m_websocketNodes : m_customNodes; if (nodes.count() == 0) { @@ -224,51 +181,23 @@ FeatherNode Nodes::pickEligibleNode() { return rtn; } - QVector heights; + QVector node_indeces; + int i = 0; for (const auto &node: nodes) { - heights.push_back(node.height); + node_indeces.push_back(i); + i++; } + unsigned seed = std::chrono::system_clock::now().time_since_epoch().count(); + std::shuffle(node_indeces.begin(), node_indeces.end(), std::default_random_engine(seed)); - std::sort(heights.begin(), heights.end()); + // Pick random eligible node + int mode_height = this->modeHeight(nodes); + for (int index : node_indeces) { + const FeatherNode &node = nodes.at(index); - // Calculate mode of node heights - int max_count = 1, mode_height = heights[0], count = 1; - for (int i = 1; i < heights.count(); i++) { - if (heights[i] == 0) { // Don't consider 0 height nodes - continue; - } - - if (heights[i] == heights[i - 1]) - count++; - else { - if (count > max_count) { - max_count = count; - mode_height = heights[i - 1]; - } - count = 1; - } - } - if (count > max_count) - { - max_count = count; - mode_height = heights[heights.count() - 1]; - } - - while(true) { - // keep track of nodes we have previously tried to connect to - if (m_connectionAttempts.count() == nodes.count()) { - this->exhausted(); - m_connectionTimer->stop(); - return rtn; - } - - int random = QRandomGenerator::global()->bounded(nodes.count()); - FeatherNode node = nodes.at(random); - if (m_connectionAttempts.contains(node.full)) - continue; - m_connectionAttempts.append(node.full); - - if (wsMode) { + // This may fail to detect bad nodes if cached nodes are used + // Todo: wait on websocket before connecting, only use cache if websocket is unavailable + if (wsMode && m_wsNodesReceived) { // Ignore offline nodes if (!node.online) continue; @@ -282,19 +211,28 @@ FeatherNode Nodes::pickEligibleNode() { continue; } + // Don't connect to nodes that failed to connect recently + if (m_recentFailures.contains(node.address)) { + continue; + } + return node; } + + // All nodes tried, and none eligible + this->exhausted(); + return rtn; } void Nodes::onWSNodesReceived(const QList> &nodes) { m_websocketNodes.clear(); m_wsNodesReceived = true; - for(auto &node: nodes) { - if(m_connection == *node) { - if(m_connection.isActive) + for (auto &node: nodes) { + if (m_connection == *node) { + if (m_connection.isActive) node->isActive = true; - else if(m_connection.isConnecting) + else if (m_connection.isConnecting) node->isConnecting = true; } m_websocketNodes.push_back(*node); @@ -304,7 +242,7 @@ void Nodes::onWSNodesReceived(const QList> &nodes) { auto key = QString::number(m_ctx->networkType); auto obj = m_configJson.value(key).toObject(); auto ws = QJsonArray(); - for(auto const &node: m_websocketNodes) + for (auto const &node: m_websocketNodes) ws.push_back(node.address); obj["ws"] = ws; @@ -315,7 +253,10 @@ void Nodes::onWSNodesReceived(const QList> &nodes) { } void Nodes::onNodeSourceChanged(NodeSource nodeSource) { - if(nodeSource == this->source()) return; + if (nodeSource == this->source()) + return; + m_source = nodeSource; + auto key = QString::number(m_ctx->networkType); auto obj = m_configJson.value(key).toObject(); obj["source"] = nodeSource; @@ -324,16 +265,18 @@ void Nodes::onNodeSourceChanged(NodeSource nodeSource) { this->writeConfig(); this->resetLocalState(); this->updateModels(); + + this->autoConnect(true); } -void Nodes::setCustomNodes(QList nodes) { +void Nodes::setCustomNodes(const QList &nodes) { m_customNodes.clear(); auto key = QString::number(m_ctx->networkType); auto obj = m_configJson.value(key).toObject(); QStringList nodesList; - for(auto const &node: nodes) { - if(nodesList.contains(node.full)) continue; + for (auto const &node: nodes) { + if (nodesList.contains(node.full)) continue; nodesList.append(node.full); m_customNodes.append(node); } @@ -352,56 +295,47 @@ void Nodes::updateModels() { } void Nodes::resetLocalState() { - QList*> models = {&m_customNodes, &m_websocketNodes}; - - for(QList *model: models) { - for (FeatherNode &_node: *model) { - _node.isConnecting = false; - _node.isActive = false; - - if (_node == m_connection) { - _node.isActive = m_connection.isActive; - _node.isConnecting = m_connection.isConnecting; + auto resetState = [this](QList *model){ + for (auto&& node: *model) { + node.isConnecting = false; + node.isActive = false; + + if (node == m_connection) { + node.isActive = m_connection.isActive; + node.isConnecting = m_connection.isConnecting; } } - } + }; + + resetState(&m_customNodes); + resetState(&m_websocketNodes); } void Nodes::exhausted() { - NodeSource nodeSource = this->source(); - auto wsMode = nodeSource == NodeSource::websocket; - if(wsMode) + bool wsMode = (this->source() == NodeSource::websocket); + + if (wsMode) this->WSNodeExhaustedWarning(); else this->nodeExhaustedWarning(); } void Nodes::nodeExhaustedWarning(){ - if(m_customExhaustedWarningEmitted) return; - emit nodeExhausted(); - - auto msg = QString("Could not find an eligible custom node to connect to."); - qWarning() << msg; - activityLog.append(msg); + if (m_customExhaustedWarningEmitted) + return; + emit nodeExhausted(); + qWarning() << "Could not find an eligible custom node to connect to."; m_customExhaustedWarningEmitted = true; - this->m_connectionTimer->stop(); } void Nodes::WSNodeExhaustedWarning() { - if(m_wsExhaustedWarningEmitted) return; - emit WSNodeExhausted(); - - auto msg = QString("Could not find an eligible websocket node to connect to."); - qWarning() << msg; - activityLog.append(msg); + if (m_wsExhaustedWarningEmitted) + return; + emit WSNodeExhausted(); + qWarning() << "Could not find an eligible websocket node to connect to."; m_wsExhaustedWarningEmitted = true; - this->m_connectionTimer->stop(); -} - -void Nodes::onWalletClosing() { - m_connectionTimer->stop(); } QList Nodes::customNodes() { @@ -412,10 +346,38 @@ FeatherNode Nodes::connection() { return m_connection; } -void Nodes::stopTimer(){ - m_connectionTimer->stop(); -} - NodeSource Nodes::source() { return m_source; } + +int Nodes::modeHeight(const QList &nodes) { + QVector heights; + for (const auto &node: nodes) { + heights.push_back(node.height); + } + + std::sort(heights.begin(), heights.end()); + + int max_count = 1, mode_height = heights[0], count = 1; + for (int i = 1; i < heights.count(); i++) { + if (heights[i] == 0) { // Don't consider 0 height nodes + continue; + } + + if (heights[i] == heights[i - 1]) + count++; + else { + if (count > max_count) { + max_count = count; + mode_height = heights[i - 1]; + } + count = 1; + } + } + if (count > max_count) + { + mode_height = heights[heights.count() - 1]; + } + + return mode_height; +} \ No newline at end of file diff --git a/src/utils/nodes.h b/src/utils/nodes.h index 37ba854..81c9627 100644 --- a/src/utils/nodes.h +++ b/src/utils/nodes.h @@ -86,7 +86,6 @@ public: explicit Nodes(AppContext *ctx, QNetworkAccessManager *networkAccessManager, QObject *parent = nullptr); void loadConfig(); void writeConfig(); - void stopTimer(); NodeSource source(); FeatherNode connection(); @@ -95,18 +94,13 @@ public: NodeModel *modelWebsocket; NodeModel *modelCustom; - QStringList activityLog; - public slots: - void onWalletClosing(); void connectToNode(); - void connectToNode(FeatherNode node); + void connectToNode(const FeatherNode &node); void onWSNodesReceived(const QList>& nodes); void onNodeSourceChanged(NodeSource nodeSource); - void setCustomNodes(QList nodes); - -private slots: - void onConnectionTimer(); + void setCustomNodes(const QList& nodes); + void autoConnect(bool forceReconnect = false); signals: void WSNodeExhausted(); @@ -119,18 +113,17 @@ private: QNetworkAccessManager *m_networkAccessManager = nullptr; QJsonObject m_configJson; + QStringList m_recentFailures; + QList m_customNodes; QList m_websocketNodes; FeatherNode m_connection; // current active connection, if any - QTimer *m_connectionTimer = new QTimer(this); - time_t m_connectionAttemptTime = 0; - QStringList m_connectionAttempts; bool m_wsNodesReceived = false; - bool m_wsExhaustedWarningEmitted = true; bool m_customExhaustedWarningEmitted = true; + bool m_enableAutoconnect = true; FeatherNode pickEligibleNode(); @@ -139,6 +132,7 @@ private: void exhausted(); void WSNodeExhaustedWarning(); void nodeExhaustedWarning(); + int modeHeight(const QList &nodes); }; #endif //FEATHER_NODES_H diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index e7d52a4..f99737f 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -541,9 +541,8 @@ QFont Utils::relativeFont(int delta) { double Utils::roundSignificant(double N, double n) { int h; - double l, a, b, c, d, e, i, j, m, f, g; + double b, d, e, i, j, m, f; b = N; - c = floor(N); for (i = 0; b >= 1; ++i) b = b / 10; @@ -564,3 +563,22 @@ double Utils::roundSignificant(double N, double n) j = j / m; return j; } + +QString Utils::formatBytes(quint64 bytes) +{ + QVector sizes = { "B", "KB", "MB", "GB", "TB" }; + + int i; + double _data; + for (i = 0; i < sizes.count() && bytes >= 1000; i++, bytes /= 1000) + _data = bytes / 1000.0; + + if (_data < 0) + _data = 0; + + // unrealistic + if (_data > 1000) + _data = 0; + + return QString("%1 %2").arg(QString::number(_data, 'f', 1), sizes[i]); +} \ No newline at end of file diff --git a/src/utils/utils.h b/src/utils/utils.h index 1595de6..fa0a92a 100644 --- a/src/utils/utils.h +++ b/src/utils/utils.h @@ -95,6 +95,8 @@ public: static bool pixmapWrite(const QString &path, const QPixmap &pixmap); static QFont relativeFont(int delta); static double roundSignificant(double N, double n); + static QString formatBytes(quint64 bytes); + static QStringList randomHTTPAgents; template