m_splashDialog = new SplashDialog(this);
m_accountSwitcherDialog = new AccountSwitcherDialog(m_ctx, this);
+ m_updater = QSharedPointer<Updater>(new Updater(this));
+
this->restoreGeo();
this->initStatusBar();
connect(websocketNotifier(), &WebsocketNotifier::BountyReceived, ui->bountiesWidget->model(), &BountiesModel::updateBounties);
connect(websocketNotifier(), &WebsocketNotifier::RedditReceived, ui->redditWidget->model(), &RedditModel::updatePosts);
connect(websocketNotifier(), &WebsocketNotifier::RevuoReceived, ui->revuoWidget, &RevuoWidget::updateItems);
- connect(websocketNotifier(), &WebsocketNotifier::UpdatesReceived, this, &MainWindow::onUpdatesAvailable);
+ connect(websocketNotifier(), &WebsocketNotifier::UpdatesReceived, m_updater.data(), &Updater::wsUpdatesReceived);
#ifdef HAS_XMRIG
connect(websocketNotifier(), &WebsocketNotifier::XMRigDownloadsReceived, m_xmrig, &XMRigWidget::onDownloads);
#endif
connect(torManager(), &TorManager::connectionStateChanged, this, &MainWindow::onTorConnectionStateChanged);
this->onTorConnectionStateChanged(torManager()->torConnected);
+ connect(m_updater.data(), &Updater::updateAvailable, this, &MainWindow::showUpdateNotification);
+
ColorScheme::updateFromWidget(this);
QTimer::singleShot(1, [this]{this->updateWidgetIcons();});
// [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);
m_statusBtnTor->setIcon(icons()->icon("tor_logo_disabled.png"));
}
-void MainWindow::onCheckUpdatesComplete(const QString &version, const QString &binaryFilename,
- const QString &hash, const QString &signer) {
- QString versionDisplay{version};
+void MainWindow::showUpdateNotification() {
+ 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->show();
m_statusUpdateAvailable->disconnect();
- connect(m_statusUpdateAvailable, &StatusBarButton::clicked, [this, version, binaryFilename, hash, signer] {
- this->onShowUpdateCheck(version, binaryFilename, hash, signer);
- });
+ connect(m_statusUpdateAvailable, &StatusBarButton::clicked, this, &MainWindow::showUpdateDialog);
}
-void MainWindow::onShowUpdateCheck(const QString &version, const QString &binaryFilename,
- const QString &hash, const QString &signer) {
- QString platformTag = this->getPlatformTag();
- QString downloadUrl = QString("https://featherwallet.org/files/releases/%1/%2").arg(platformTag, binaryFilename);
-
- UpdateDialog updateDialog{this, version, downloadUrl, hash, signer, platformTag};
+void MainWindow::showUpdateDialog() {
+ UpdateDialog updateDialog{this, m_updater};
connect(&updateDialog, &UpdateDialog::restartWallet, m_windowManager, &WindowManager::restartApplication);
updateDialog.exec();
}
-void MainWindow::onUpdatesAvailable(const QJsonObject &updates) {
- QString featherVersionStr{FEATHER_VERSION};
-
- auto featherVersion = SemanticVersion::fromString(featherVersionStr);
-
- QString platformTag = getPlatformTag();
- if (platformTag.isEmpty()) {
- qWarning() << "Unsupported platform, unable to fetch update";
- return;
- }
-
- QJsonObject platformData = updates["platform"].toObject()[platformTag].toObject();
- if (platformData.isEmpty()) {
- qWarning() << "Unable to find current platform in updates data";
- return;
- }
-
- QString newVersion = platformData["version"].toString();
- if (SemanticVersion::fromString(newVersion) <= featherVersion) {
- return;
- }
-
- // Hooray! New update available
-
- QString hashesUrl = QString("%1/files/releases/hashes-%2-plain.txt").arg(constants::websiteUrl, newVersion);
-
- UtilsNetworking network{getNetworkTor()};
- QNetworkReply *reply = network.get(hashesUrl);
-
- connect(reply, &QNetworkReply::finished, this, std::bind(&MainWindow::onSignedHashesReceived, this, reply, platformTag, newVersion));
-}
-
-void MainWindow::onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version) {
- if (reply->error() != QNetworkReply::NoError) {
- qWarning() << "Unable to fetch signed hashes: " << reply->errorString();
- return;
- }
-
- QByteArray armoredSignedHashes = reply->readAll();
- reply->deleteLater();
-
- const QString binaryFilename = QString("feather-%1-%2.zip").arg(version, platformTag);
- QString signer;
- QByteArray signedHash = AsyncTask::runAndWaitForFuture([armoredSignedHashes, binaryFilename, &signer]{
- try {
- return Updater().verifyParseSignedHashes(armoredSignedHashes, binaryFilename, signer);
- }
- catch (const std::exception &e) {
- qWarning() << "Failed to fetch and verify signed hash: " << e.what();
- return QByteArray{};
- }
- });
- if (signedHash.isEmpty()) {
- return;
- }
-
- QString hash = signedHash.toHex();
- qInfo() << "Update found: " << binaryFilename << hash << "signed by:" << signer;
- this->onCheckUpdatesComplete(version, binaryFilename, hash, signer);
-}
-
void MainWindow::onInitiateTransaction() {
m_statusDots = 0;
m_constructingTransaction = true;
#include "utils/networking.h"
#include "utils/config.h"
#include "utils/EventFilter.h"
+#include "utils/Updater.h"
#include "widgets/CCSWidget.h"
#include "widgets/RedditWidget.h"
#include "widgets/TickerWidget.h"
void loadSignedTxFromText();
void onTorConnectionStateChanged(bool connected);
- void onCheckUpdatesComplete(const QString &version, const QString &binaryFilename, const QString &hash, const QString &signer);
- void onShowUpdateCheck(const QString &version, const QString &binaryFilename, const QString &hash, const QString &signer);
- void onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version);
+ void showUpdateDialog();
void onInitiateTransaction();
void onEndTransaction();
void onKeysCorrupted();
void onDeviceButtonPressed();
void onWalletPassphraseNeeded(bool on_device);
void menuHwDeviceClicked();
- void onUpdatesAvailable(const QJsonObject &updates);
void toggleSearchbar(bool enabled);
void tryStoreWallet();
void onWebsocketStatusChanged(bool enabled);
+ void showUpdateNotification();
private:
friend WindowManager;
EventFilter *m_eventFilter = nullptr;
qint64 m_userLastActive = QDateTime::currentSecsSinceEpoch();
+
+ QSharedPointer<Updater> m_updater = nullptr;
};
#endif // FEATHER_MAINWINDOW_H
<string>Help</string>
</property>
<addaction name="actionAbout"/>
+ <addaction name="actionCheckForUpdates"/>
<addaction name="actionOfficialWebsite"/>
<addaction name="separator"/>
<addaction name="actionDocumentation"/>
<string>Lock wallet</string>
</property>
</action>
+ <action name="actionCheckForUpdates">
+ <property name="text">
+ <string>Check for updates</string>
+ </property>
+ </action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
#include <QFileDialog>
+#include "constants.h"
#include "utils/AsyncTask.h"
#include "utils/networking.h"
#include "utils/NetworkManager.h"
#include "zip.h"
-UpdateDialog::UpdateDialog(QWidget *parent, QString version, QString downloadUrl, QString hash, QString signer, QString platformTag)
- : QDialog(parent)
- , ui(new Ui::UpdateDialog)
- , m_version(std::move(version))
- , m_downloadUrl(std::move(downloadUrl))
- , m_hash(std::move(hash))
- , m_signer(std::move(signer))
- , m_platformTag(std::move(platformTag))
+UpdateDialog::UpdateDialog(QWidget *parent, QSharedPointer<Updater> updater)
+ : QDialog(parent)
+ , ui(new Ui::UpdateDialog)
+ , m_updater(std::move(updater))
{
ui->setupUi(this);
- ui->btn_installUpdate->hide();
- ui->btn_restart->hide();
- ui->progressBar->hide();
-
auto bigFont = Utils::relativeFont(4);
ui->label_header->setFont(bigFont);
- ui->label_header->setText(QString("New Feather version %1 is available").arg(m_version));
+ ui->frame->hide();
+
+ bool updateAvailable = (m_updater->state == Updater::State::UPDATE_AVAILABLE);
+ if (updateAvailable) {
+ this->updateAvailable();
+ } else {
+ this->checkForUpdates();
+ }
connect(ui->btn_cancel, &QPushButton::clicked, [this]{
if (m_reply) {
connect(ui->btn_installUpdate, &QPushButton::clicked, this, &UpdateDialog::onInstallUpdate);
connect(ui->btn_restart, &QPushButton::clicked, this, &UpdateDialog::onRestartClicked);
+ connect(m_updater.data(), &Updater::updateAvailable, this, &UpdateDialog::updateAvailable);
+ connect(m_updater.data(), &Updater::noUpdateAvailable, this, &UpdateDialog::noUpdateAvailable);
+ connect(m_updater.data(), &Updater::updateCheckFailed, this, &UpdateDialog::onUpdateCheckFailed);
+
this->adjustSize();
}
+void UpdateDialog::checkForUpdates() {
+ ui->label_header->setText("Checking for updates...");
+ ui->label_body->setText("...");
+ m_updater->checkForUpdates();
+}
+
+void UpdateDialog::noUpdateAvailable() {
+ this->setStatus("Feather is up-to-date.", true);
+}
+
+void UpdateDialog::updateAvailable() {
+ ui->frame->show();
+ ui->btn_installUpdate->hide();
+ ui->btn_restart->hide();
+ ui->progressBar->hide();
+ ui->label_header->setText(QString("New Feather version %1 is available").arg(m_updater->version));
+ ui->label_body->setText("Do you want to download and verify the new version?");
+}
+
+void UpdateDialog::onUpdateCheckFailed(const QString &errorMsg) {
+ this->setStatus(QString("Failed to check for updates: %1").arg(errorMsg), false);
+}
+
void UpdateDialog::onDownloadClicked() {
ui->label_body->setText("Downloading update..");
ui->btn_download->hide();
UtilsNetworking network{getNetworkTor()};
- m_reply = network.get(m_downloadUrl);
+ m_reply = network.get(m_updater->downloadUrl);
connect(m_reply, &QNetworkReply::downloadProgress, this, &UpdateDialog::onDownloadProgress);
connect(m_reply, &QNetworkReply::finished, this, &UpdateDialog::onDownloadFinished);
}
return Updater().getHash(&responseStr[0], responseStr.size());
});
- const QByteArray signedHash = QByteArray::fromHex(m_hash.toUtf8());
+ const QByteArray signedHash = QByteArray::fromHex(m_updater->hash.toUtf8());
if (signedHash != calculatedHash) {
this->onDownloadError("Error: Hash sum mismatch.");
QDir applicationDir(Utils::applicationPath());
QString filePath = applicationDir.filePath(name);
- if (m_platformTag == "win-installer") {
+ if (m_updater->platformTag == "win-installer") {
filePath = QString("%1/%2").arg(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation), name);
}
return;
}
- if (m_platformTag == "win-installer") {
+ if (m_updater->platformTag == "win-installer") {
this->setStatus("Installer written. Click 'restart' to close Feather and start the installer.");
} else {
this->setStatus("Installation successful. Do you want to restart Feather now?");
if (appPath.endsWith("Contents/MacOS")) {
appDir.cd("../../..");
}
- QString appName = QString("feather-%1").arg(m_version);
+ QString appName = QString("feather-%1").arg(m_updater->version);
QString zipName = QString("%1.zip").arg(appName);
QString fPath = appDir.filePath(zipName);
#include <QDialog>
#include <QNetworkReply>
+#include "utils/Updater.h"
+
namespace Ui {
class UpdateDialog;
}
Q_OBJECT
public:
- explicit UpdateDialog(QWidget *parent, QString version, QString downloadUrl, QString hash, QString signer, QString platformTag);
+ explicit UpdateDialog(QWidget *parent, QSharedPointer<Updater> updater);
~UpdateDialog() override;
private slots:
void onInstallUpdate();
void onInstallError(const QString &errMsg);
void onRestartClicked();
+ void onUpdateCheckFailed(const QString &onUpdateCheckFailed);
signals:
void restartWallet(const QString &binaryFilename);
private:
+ void checkForUpdates();
+ void noUpdateAvailable();
+ void updateAvailable();
void setStatus(const QString &msg, bool success = false);
void installUpdateMac();
QScopedPointer<Ui::UpdateDialog> ui;
+ QSharedPointer<Updater> m_updater;
- QString m_version;
QString m_downloadUrl;
- QString m_hash;
- QString m_signer;
- QString m_platformTag;
QString m_updatePath;
<rect>
<x>0</x>
<y>0</y>
- <width>569</width>
- <height>148</height>
+ <width>540</width>
+ <height>144</height>
</rect>
</property>
<property name="windowTitle">
- <string>Update Available</string>
+ <string>Updater</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<property name="text">
<string>Do you want to download and verify the new version?</string>
</property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+ </property>
</widget>
</item>
<item>
- <widget class="QProgressBar" name="progressBar">
- <property name="value">
- <number>0</number>
+ <widget class="QFrame" name="frame">
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
</property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btn_cancel">
+ <property name="text">
+ <string>Cancel</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btn_download">
+ <property name="text">
+ <string>Download</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btn_installUpdate">
+ <property name="text">
+ <string>Install Update</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btn_restart">
+ <property name="text">
+ <string>Restart Feather</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
</widget>
</item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout">
- <item>
- <spacer name="horizontalSpacer">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="btn_cancel">
- <property name="text">
- <string>Cancel</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="btn_download">
- <property name="text">
- <string>Download</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="btn_installUpdate">
- <property name="text">
- <string>Install Update</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="btn_restart">
- <property name="text">
- <string>Restart Feather</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
</layout>
</widget>
<resources/>
#include <common/util.h>
#include <openpgp/hash.h>
+#include "config-feather.h"
+#include "constants.h"
#include "Utils.h"
+#include "utils/AsyncTask.h"
+#include "utils/networking.h"
+#include "utils/NetworkManager.h"
+#include "utils/SemanticVersion.h"
-Updater::Updater() {
+Updater::Updater(QObject *parent) :
+ QObject(parent)
+{
std::string featherWallet = Utils::fileOpen(":/assets/gpg_keys/featherwallet.asc").toStdString();
m_maintainers.emplace_back(featherWallet);
}
+void Updater::checkForUpdates() {
+ UtilsNetworking network{getNetworkTor()};
+ QNetworkReply *reply = network.getJson("https://featherwallet.org/updates.json");
+
+ connect(reply, &QNetworkReply::finished, this, std::bind(&Updater::onUpdateCheckResponse, this, reply));
+}
+
+void Updater::onUpdateCheckResponse(QNetworkReply *reply) {
+ const QString err = reply->errorString();
+
+ QByteArray data = reply->readAll();
+ reply->deleteLater();
+
+ QJsonObject updates;
+ if (!data.isEmpty() && Utils::validateJSON(data)) {
+ auto doc = QJsonDocument::fromJson(data);
+ updates = doc.object();
+ }
+ else {
+ qWarning() << err;
+ emit updateCheckFailed(err);
+ return;
+ }
+
+ this->wsUpdatesReceived(updates);
+}
+
+void Updater::wsUpdatesReceived(const QJsonObject &updates) {
+ QString featherVersionStr{FEATHER_VERSION};
+
+ auto featherVersion = SemanticVersion::fromString(featherVersionStr);
+
+ QString platformTag = getPlatformTag();
+ if (platformTag.isEmpty()) {
+ QString err{"Unsupported platform, unable to fetch update"};
+ emit updateCheckFailed(err);
+ qWarning() << err;
+ return;
+ }
+
+ QJsonObject platformData = updates["platform"].toObject()[platformTag].toObject();
+ if (platformData.isEmpty()) {
+ QString err{"Unable to find current platform in updates data"};
+ emit updateCheckFailed(err);
+ qWarning() << err;
+ return;
+ }
+
+ QString newVersion = platformData["version"].toString();
+ if (SemanticVersion::fromString(newVersion) <= featherVersion) {
+ emit noUpdateAvailable();
+ return;
+ }
+
+ // Hooray! New update available
+
+ QString hashesUrl = QString("%1/files/releases/hashes-%2-plain.txt").arg(constants::websiteUrl, newVersion);
+
+ UtilsNetworking network{getNetworkTor()};
+ QNetworkReply *reply = network.get(hashesUrl);
+
+ connect(reply, &QNetworkReply::finished, this, std::bind(&Updater::onSignedHashesReceived, this, reply, platformTag, newVersion));
+}
+
+void Updater::onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version) {
+ if (reply->error() != QNetworkReply::NoError) {
+ QString err{QString("Unable to fetch signed hashed: %1").arg(reply->errorString())};
+ emit updateCheckFailed(err);
+ qWarning() << err;
+ return;
+ }
+
+ QByteArray armoredSignedHashes = reply->readAll();
+ reply->deleteLater();
+
+ const QString binaryFilename = QString("feather-%1-%2.zip").arg(version, platformTag);
+ QByteArray signedHash{};
+ QString signer;
+ try {
+ signedHash = this->verifyParseSignedHashes(armoredSignedHashes, binaryFilename, signer);
+ }
+ catch (const std::exception &e) {
+ QString err{QString("Failed to fetch and verify signed hash: %1").arg(e.what())};
+ emit updateCheckFailed(err);
+ qWarning() << err;
+ return;
+ }
+
+ QString hash = signedHash.toHex();
+ qInfo() << "Update found: " << binaryFilename << hash << "signed by:" << signer;
+
+ this->state = Updater::State::UPDATE_AVAILABLE;
+ this->version = version;
+ this->binaryFilename = binaryFilename;
+ this->downloadUrl = QString("https://featherwallet.org/files/releases/%1/%2").arg(platformTag, binaryFilename);
+ this->hash = hash;
+ this->signer = signer;
+ this->platformTag = platformTag;
+
+ emit updateAvailable();
+}
+
+QString Updater::getPlatformTag() {
+#ifdef Q_OS_MACOS
+ return "mac";
+#endif
+#ifdef Q_OS_WIN
+ #ifdef PLATFORM_INSTALLER
+ return "win-installer";
+#endif
+ return "win";
+#endif
+#ifdef Q_OS_LINUX
+ QString tag = "";
+
+ QString arch = QSysInfo::buildCpuArchitecture();
+ if (arch == "arm64") {
+ tag += "linux-arm64";
+ } else if (arch == "arm") {
+ tag += "linux-arm";
+ } else {
+ tag += "linux";
+ }
+
+ if (!qEnvironmentVariableIsEmpty("APPIMAGE")) {
+ tag += "-appimage";
+ }
+
+ return tag;
+#endif
+ return "";
+}
+
QByteArray Updater::verifyParseSignedHashes(
const QByteArray &armoredSignedHashes,
const QString &binaryFilename,
#include <openpgp/openpgp.h>
-class Updater
+class Updater : public QObject
{
+Q_OBJECT
+
+public:
+ enum State {
+ NO_UPDATE = 0,
+ UPDATE_AVAILABLE = 1
+ };
+
public:
- explicit Updater();
+ explicit Updater(QObject *parent = nullptr);
+
+ void checkForUpdates();
QByteArray verifyParseSignedHashes(const QByteArray &armoredSignedHashes, const QString &binaryFilename, QString &signers) const;
QString verifySignature(const QByteArray &armoredSignedMessage, QString &signer) const;
QByteArray parseShasumOutput(const QString &message, const QString &filename) const;
+ State state = State::NO_UPDATE;
+ QString version;
+ QString binaryFilename;
+ QString downloadUrl;
+ QString hash;
+ QString signer;
+ QString platformTag;
+
+signals:
+ void updateCheckFailed(const QString &error);
+ void noUpdateAvailable();
+ void updateAvailable();
+
+public slots:
+ void onUpdateCheckResponse(QNetworkReply *reply);
+ void wsUpdatesReceived(const QJsonObject &updates);
+
private:
QString verifySignature(const epee::span<const uint8_t> data, const openpgp::signature_rsa &signature) const;
+ void onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version);
+ QString getPlatformTag();
private:
std::vector<openpgp::public_key_block> m_maintainers;