name: ci/gh-actions/build
-on: [pull_request]
+on:
+ push: [master, main, dev]
+ pull_request:
jobs:
build-ubuntu-without-scanner:
cmake -DWITH_SCANNER=OFF ..
cmake --build . -j $(nproc)
+ build-ubuntu-22:
+ name: "Ubuntu 22.04"
+ runs-on: ubuntu-latest
+ container:
+ image: ubuntu:22.04
+ steps:
+ - name: update apt
+ run: apt update
+ - name: install dependencies
+ run:
+ apt -y install git cmake build-essential ccache libssl-dev libunbound-dev libboost-all-dev
+ libqrencode-dev qt6-base-dev qt6-svg-dev qt6-websockets-dev qt6-multimedia-dev
+ libzip-dev libsodium-dev libgcrypt20-dev libx11-xcb-dev
+ protobuf-compiler libprotobuf-dev libhidapi-dev libusb-dev
+ libusb-1.0-0-dev
+ - name: configure git
+ run: git config --global --add safe.directory '*'
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ - name: build
+ run: |
+ mkdir build
+ cd build
+ cmake -DWITH_SCANNER=OFF ..
+ cmake --build . -j $(nproc)
+
+ build-ubuntu-20:
+ name: "Ubuntu 20.04"
+ runs-on: ubuntu-latest
+ container:
+ image: ubuntu:20.04
+ env:
+ DEBIAN_FRONTEND: noninteractive
+ steps:
+ - name: update apt
+ run: apt update
+ - name: install dependencies
+ run: |
+ apt -y install software-properties-common
+ add-apt-repository -y ppa:beineri/opt-qt-5.15.2-focal
+ apt update
+ apt -y install git cmake build-essential ccache libssl-dev libunbound-dev libboost-all-dev \
+ libqrencode-dev qt515base qt515svg qt515websockets qt515multimedia \
+ libzip-dev libsodium-dev libgcrypt20-dev libx11-xcb-dev \
+ protobuf-compiler libprotobuf-dev libhidapi-dev libusb-dev libusb-1.0-0-dev
+ - name: configure git
+ run: git config --global --add safe.directory '*'
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ - name: build
+ run: |
+ source /opt/qt515/bin/qt515-env.sh
+ mkdir build
+ cd build
+ cmake -DWITH_SCANNER=OFF ..
+ cmake --build . -j $(nproc)
+
build-arch:
name: "Arch Linux"
runs-on: ubuntu-latest
#include <QFormLayout>
#include <QSpinBox>
#include <QDateEdit>
+#include <QComboBox>
#include <QDialogButtonBox>
#include "constants.h"
#include "dialog/ViewOnlyDialog.h"
#include "dialog/WalletInfoDialog.h"
#include "dialog/WalletCacheDebugDialog.h"
+#include "dialog/SyncRangeDialog.h"
#include "libwalletqt/AddressBook.h"
#include "libwalletqt/rows/CoinsInfo.h"
#include "libwalletqt/rows/Output.h"
// Timers
connect(&m_updateBytes, &QTimer::timeout, this, &MainWindow::updateNetStats);
+ connect(&m_updateBytes, &QTimer::timeout, this, &MainWindow::updateStatusToolTip);
connect(&m_txTimer, &QTimer::timeout, [this]{
QString text = "Constructing transaction" + this->statusDots();
m_statusLabelStatus->setText(text);
connect(syncRangeAction, &QAction::triggered, this, [this](){
if (!m_wallet) return;
- QDialog dialog(this);
- dialog.setWindowTitle(tr("Sync Date Range"));
- dialog.setWindowIcon(QIcon(":/assets/images/appicons/64x64.png"));
- dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
- dialog.setWindowFlags(dialog.windowFlags() | Qt::MSWindowsFixedSizeDialogHint);
-
- auto *layout = new QVBoxLayout(&dialog);
-
- auto *formLayout = new QFormLayout;
- formLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
-
- auto *toDateEdit = new QDateEdit(QDate::currentDate());
- toDateEdit->setCalendarPopup(true);
- toDateEdit->setDisplayFormat("yyyy-MM-dd");
-
- // Load lookup for accurate block calculations
- NetworkType::Type nettype = m_wallet->nettype();
- QString filename = Utils::getRestoreHeightFilename(nettype);
-
- std::unique_ptr<RestoreHeightLookup> lookup(RestoreHeightLookup::fromFile(filename, nettype));
-
- int defaultDays = 7;
-
- auto *daysSpinBox = new QSpinBox;
- daysSpinBox->setRange(1, 3650); // 10 years
- daysSpinBox->setValue(defaultDays);
- daysSpinBox->setSuffix(tr(" days"));
-
- auto *fromDateEdit = new QDateEdit(QDate::currentDate().addDays(-defaultDays));
- fromDateEdit->setCalendarPopup(true);
- fromDateEdit->setDisplayFormat("yyyy-MM-dd");
- fromDateEdit->setToolTip(tr("Calculated from 'End date' and day span."));
-
- auto *infoLabel = new QLabel;
- infoLabel->setWordWrap(true);
- infoLabel->setStyleSheet("QLabel { color: #888; font-size: 11px; }");
-
- formLayout->addRow(tr("Day span:"), daysSpinBox);
- formLayout->addRow(tr("Start date:"), fromDateEdit);
- formLayout->addRow(tr("End date:"), toDateEdit);
-
- layout->addLayout(formLayout);
- layout->addWidget(infoLabel);
-
- auto updateInfo = [=, &lookup]() {
- QDate start = fromDateEdit->date();
- QDate end = toDateEdit->date();
-
- uint64_t startHeight = lookup->dateToHeight(start.startOfDay().toSecsSinceEpoch());
- uint64_t endHeight = lookup->dateToHeight(end.endOfDay().toSecsSinceEpoch());
-
- if (endHeight < startHeight) endHeight = startHeight;
- quint64 blocks = endHeight - startHeight;
- quint64 size = Utils::estimateSyncDataSize(blocks);
-
- infoLabel->setText(tr("Scanning ~%1 blocks\nEst. download size: %2")
- .arg(blocks)
- .arg(Utils::formatBytes(size)));
- };
-
- auto updateFromDate = [=]() {
- fromDateEdit->setDate(toDateEdit->date().addDays(-daysSpinBox->value()));
- updateInfo();
- };
-
- connect(fromDateEdit, &QDateEdit::dateChanged, updateInfo);
- connect(toDateEdit, &QDateEdit::dateChanged, updateFromDate);
- connect(daysSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), updateFromDate);
-
- // Init label
- updateInfo();
-
- auto *btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
- connect(btnBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
- connect(btnBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
- layout->addWidget(btnBox);
-
- dialog.resize(320, dialog.height());
-
+ SyncRangeDialog dialog(this, m_wallet);
if (dialog.exec() == QDialog::Accepted) {
- m_wallet->syncDateRange(fromDateEdit->date(), toDateEdit->date());
-
- // Re-calculate for status text
- uint64_t startHeight = lookup->dateToHeight(fromDateEdit->date().startOfDay().toSecsSinceEpoch());
- uint64_t endHeight = lookup->dateToHeight(toDateEdit->date().endOfDay().toSecsSinceEpoch());
- quint64 blocks = (endHeight > startHeight) ? endHeight - startHeight : 0;
- quint64 size = Utils::estimateSyncDataSize(blocks);
+ m_wallet->syncDateRange(dialog.fromDate(), dialog.toDate());
this->setStatusText(tr("Syncing range %1 - %2 (~%3 blocks)\nEst. download size: %4")
- .arg(fromDateEdit->date().toString("yyyy-MM-dd"))
- .arg(toDateEdit->date().toString("yyyy-MM-dd"))
- .arg(QLocale().toString(blocks))
- .arg(Utils::formatBytes(size)));
+ .arg(dialog.fromDate().toString("yyyy-MM-dd"))
+ .arg(dialog.toDate().toString("yyyy-MM-dd"))
+ .arg(QLocale().toString(dialog.estimatedBlocks()))
+ .arg(Utils::formatBytes(dialog.estimatedSize())));
}
});
if (QMessageBox::question(this, tr("Full Sync"), msg) == QMessageBox::Yes) {
m_wallet->fullSync();
- this->setStatusText(tr("Full sync started (%1 blocks)...").arg(estBlocks));
+ if (estBlocks.startsWith("Unknown")) {
+ this->setStatusText(tr("Full sync started..."));
+ } else {
+ this->setStatusText(tr("Full sync started (%1 blocks)...").arg(estBlocks));
+ }
}
}
});
}
}
- QString toolTip = "Right-click for details";
- if (appData()->prices.lastUpdateTime.isValid()) {
- toolTip += QString("\nLast updated: %1").arg(Utils::timeAgo(appData()->prices.lastUpdateTime));
- }
- m_statusLabelBalance->setToolTip(toolTip);
+ this->updateStatusToolTip();
+
QString finalText = "Balance: " + valueStr + suffixStr;
qDebug() << "Setting balance label text:" << finalText;
m_statusLabelBalance->setProperty("copyableValue", valueStr);
}
+void MainWindow::updateStatusToolTip() {
+ QString toolTip = "Right-click for details";
+ if (appData()->prices.lastUpdateTime.isValid()) {
+ toolTip += QString("\nPrice updated: %1").arg(Utils::timeAgo(appData()->prices.lastUpdateTime));
+ }
+ if (m_wallet->lastSyncTime().isValid()) {
+ toolTip += QString("\nWallet synced: %1").arg(Utils::timeAgo(m_wallet->lastSyncTime()));
+ }
+ m_statusLabelBalance->setToolTip(toolTip);
+}
void MainWindow::setStatusText(const QString &text, bool override, int timeout) {
void MainWindow::onSyncStatus(quint64 height, quint64 target, bool daemonSync) {
qDebug() << "onSyncStatus: Height" << height << "Target" << target << "DaemonSync" << daemonSync;
+
+ quint64 blocksBehind = Utils::blocksBehind(height, target);
+ m_lastSyncStatusUpdate = QDateTime::currentDateTime();
+
if (height >= (target - 1) && target > 0) {
this->updateNetStats();
this->setStatusText(QString("Synchronized (%1)").arg(QLocale().toString(height)));
} else {
- quint64 blocksBehind = Utils::blocksBehind(height, target);
QString type = daemonSync ? tr("Blockchain") : tr("Wallet");
QString blocksStr = QLocale().toString(blocksBehind);
this->setStatusText(tr("%1 sync: %2 blocks behind").arg(type, blocksStr));
}
- m_lastSyncStatusUpdate = QDateTime::currentDateTime();
- QString tooltip = tr("Wallet Height: %1 | Network Tip: %2\nLast updated: %3")
+
+ QString syncStatus = blocksBehind > 0 ? tr("%1 blocks behind").arg(QLocale().toString(blocksBehind)) : tr("Synchronized");
+ QString tooltip = tr("Wallet Height: %1 | Network Tip: %2\n%3\nLast updated: %4")
.arg(QLocale().toString(height))
.arg(QLocale().toString(target))
+ .arg(syncStatus)
.arg(m_lastSyncStatusUpdate.toString("HH:mm:ss"));
qDebug() << "Setting Status Tooltip:" << tooltip;
void fillSendTab(const QString &address, const QString &description);
void userActivity();
void checkUserActivity();
+ void updateStatusToolTip();
void lockWallet();
void unlockWallet(const QString &password);
void closeQDialogChildren(QObject *object);
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#include "SyncRangeDialog.h"
+
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QFormLayout>
+#include <QComboBox>
+#include <QSpinBox>
+#include <QDateEdit>
+#include <QLabel>
+#include <QDialogButtonBox>
+
+#include "utils/Utils.h"
+#include "utils/RestoreHeightLookup.h"
+
+SyncRangeDialog::SyncRangeDialog(QWidget *parent, Wallet *wallet)
+ : QDialog(parent)
+ , m_wallet(wallet)
+{
+ setWindowTitle(tr("Sync Date Range"));
+ setWindowIcon(QIcon(":/assets/images/appicons/64x64.png"));
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+ setWindowFlags(windowFlags() | Qt::MSWindowsFixedSizeDialogHint);
+
+ auto *layout = new QVBoxLayout(this);
+ auto *formLayout = new QFormLayout;
+ formLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
+
+ m_toDateEdit = new QDateEdit(QDate::currentDate());
+ m_toDateEdit->setCalendarPopup(true);
+ m_toDateEdit->setDisplayFormat("yyyy-MM-dd");
+
+ int defaultDays = 7;
+
+ // Preset durations dropdown
+ m_presetCombo = new QComboBox;
+ m_presetCombo->addItem(tr("1 day"), 1);
+ m_presetCombo->addItem(tr("7 days"), 7);
+ m_presetCombo->addItem(tr("30 days"), 30);
+ m_presetCombo->addItem(tr("90 days"), 90);
+ m_presetCombo->addItem(tr("1 year"), 365);
+ m_presetCombo->addItem(tr("Custom..."), -1);
+ m_presetCombo->setCurrentIndex(1); // Default to 7 days
+
+ m_daysSpinBox = new QSpinBox;
+ m_daysSpinBox->setRange(1, 3650); // 10 years
+ m_daysSpinBox->setValue(defaultDays);
+ m_daysSpinBox->setSuffix(tr(" days"));
+ m_daysSpinBox->setVisible(false); // Hidden until "Custom..." is selected
+
+ // Layout for preset + custom spinbox
+ auto *daysLayout = new QHBoxLayout;
+ daysLayout->setContentsMargins(0, 0, 0, 0);
+ daysLayout->addWidget(m_presetCombo, 1);
+ daysLayout->addWidget(m_daysSpinBox, 0);
+
+ m_fromDateEdit = new QDateEdit(QDate::currentDate().addDays(-defaultDays));
+ m_fromDateEdit->setCalendarPopup(true);
+ m_fromDateEdit->setDisplayFormat("yyyy-MM-dd");
+ m_fromDateEdit->setToolTip(tr("Calculated from 'End date' and day span."));
+
+ m_infoLabel = new QLabel;
+ m_infoLabel->setWordWrap(true);
+ m_infoLabel->setStyleSheet("QLabel { color: #888; font-size: 11px; }");
+
+ formLayout->addRow(tr("Day span:"), daysLayout);
+ formLayout->addRow(tr("Start date:"), m_fromDateEdit);
+ formLayout->addRow(tr("End date:"), m_toDateEdit);
+
+ layout->addLayout(formLayout);
+ layout->addWidget(m_infoLabel);
+
+ connect(m_fromDateEdit, &QDateEdit::dateChanged, this, &SyncRangeDialog::updateInfo);
+ connect(m_toDateEdit, &QDateEdit::dateChanged, this, &SyncRangeDialog::updateFromDate);
+ connect(m_daysSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this, &SyncRangeDialog::updateFromDate);
+
+ // Connect preset dropdown
+ connect(m_presetCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
+ int days = m_presetCombo->itemData(index).toInt();
+ if (days == -1) {
+ // Custom mode: show spinbox, keep current value
+ m_daysSpinBox->setVisible(true);
+ } else {
+ // Preset mode: hide spinbox, set value
+ m_daysSpinBox->setVisible(false);
+ m_daysSpinBox->setValue(days);
+ }
+ });
+
+ // Init info
+ updateInfo();
+
+ auto *btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+ connect(btnBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ layout->addWidget(btnBox);
+
+ resize(320, height());
+}
+
+QDate SyncRangeDialog::fromDate() const {
+ return m_fromDateEdit->date();
+}
+
+QDate SyncRangeDialog::toDate() const {
+ return m_toDateEdit->date();
+}
+
+quint64 SyncRangeDialog::estimatedBlocks() const {
+ return m_estimatedBlocks;
+}
+
+quint64 SyncRangeDialog::estimatedSize() const {
+ return m_estimatedSize;
+}
+
+void SyncRangeDialog::updateInfo() {
+ NetworkType::Type nettype = m_wallet->nettype();
+ QString filename = Utils::getRestoreHeightFilename(nettype);
+ std::unique_ptr<RestoreHeightLookup> lookup(RestoreHeightLookup::fromFile(filename, nettype));
+
+ QDate start = m_fromDateEdit->date();
+ QDate end = m_toDateEdit->date();
+
+ uint64_t startHeight = lookup->dateToHeight(start.startOfDay().toSecsSinceEpoch());
+ uint64_t endHeight = lookup->dateToHeight(end.endOfDay().toSecsSinceEpoch());
+
+ if (endHeight < startHeight) endHeight = startHeight;
+ m_estimatedBlocks = endHeight - startHeight;
+ m_estimatedSize = Utils::estimateSyncDataSize(m_estimatedBlocks);
+
+ m_infoLabel->setText(tr("Scanning ~%1 blocks\nEst. download size: %2")
+ .arg(m_estimatedBlocks)
+ .arg(Utils::formatBytes(m_estimatedSize)));
+}
+
+void SyncRangeDialog::updateFromDate() {
+ m_fromDateEdit->setDate(m_toDateEdit->date().addDays(-m_daysSpinBox->value()));
+ updateInfo();
+}
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#ifndef FEATHER_SYNCRANGEDIALOG_H
+#define FEATHER_SYNCRANGEDIALOG_H
+
+#include <QDialog>
+#include <QDate>
+
+#include "libwalletqt/Wallet.h"
+
+class QComboBox;
+class QSpinBox;
+class QDateEdit;
+class QLabel;
+
+class SyncRangeDialog : public QDialog
+{
+Q_OBJECT
+
+public:
+ explicit SyncRangeDialog(QWidget *parent, Wallet *wallet);
+ ~SyncRangeDialog() override = default;
+
+ QDate fromDate() const;
+ QDate toDate() const;
+ quint64 estimatedBlocks() const;
+ quint64 estimatedSize() const;
+
+private:
+ void updateInfo();
+ void updateFromDate();
+
+ Wallet *m_wallet;
+ QComboBox *m_presetCombo;
+ QSpinBox *m_daysSpinBox;
+ QDateEdit *m_fromDateEdit;
+ QDateEdit *m_toDateEdit;
+ QLabel *m_infoLabel;
+
+ quint64 m_estimatedBlocks = 0;
+ quint64 m_estimatedSize = 0;
+};
+
+#endif //FEATHER_SYNCRANGEDIALOG_H
void Wallet::initAsync(const QString &daemonAddress, bool trustedDaemon, quint64 upperTransactionLimit, const QString &proxyAddress)
{
qDebug() << "initAsync: " + daemonAddress;
+
+ if (daemonAddress.isEmpty()) {
+ m_scheduler.run([this] {
+ m_wallet2->set_offline(true);
+ });
+ setConnectionStatus(Wallet::ConnectionStatus_Disconnected);
+ return;
+ }
+
const auto future = m_scheduler.run([this, daemonAddress, trustedDaemon, upperTransactionLimit, proxyAddress] {
// Beware! This code does not run in the GUI thread.
} else {
setConnectionStatus(ConnectionStatus_Disconnected);
}
+
+ if (success) {
+ m_lastSyncTime = QDateTime::currentDateTime();
+ }
}
quint64 Wallet::blockChainHeight() const {
}
}
+QDateTime Wallet::lastSyncTime() const {
+ return m_lastSyncTime;
+}
+
void Wallet::skipToTip() {
if (!m_wallet2)
return;
//! returns if view only wallet
bool viewOnly() const;
+ QDateTime lastSyncTime() const;
+
//! return true if deterministic keys
bool isDeterministic() const;
quint64 m_daemonBlockChainHeight;
quint64 m_daemonBlockChainTargetHeight;
+ QDateTime m_lastSyncTime;
ConnectionStatus m_connectionStatus;
void WebsocketClient::stop() {
qDebug() << Q_FUNC_INFO;
m_stopped = true;
- webSocket->disconnect();
webSocket->abort();
m_connectionTimeout.stop();
m_pingTimer.stop();
}
void Nodes::exhausted() {
- // Do nothing
+ // All nodes have been tried and failed - clear the failure list to allow a new retry cycle
+ qInfo() << "All nodes exhausted, clearing recent failures to retry";
+ m_recentFailures.clear();
}
QList<FeatherNode> Nodes::nodes() {