From ad041141ad8425f3db08ff782ac7566eb41fcf0b Mon Sep 17 00:00:00 2001 From: gg Date: Wed, 14 Jan 2026 11:46:42 -0500 Subject: [PATCH] add dedicated SyncRangeDialog. Add old Ubuntu CI --- .github/workflows/build.yml | 63 ++++++++++++++- src/MainWindow.cpp | 133 ++++++++---------------------- src/MainWindow.h | 1 + src/dialog/SyncRangeDialog.cpp | 142 +++++++++++++++++++++++++++++++++ src/dialog/SyncRangeDialog.h | 45 +++++++++++ src/libwalletqt/Wallet.cpp | 17 ++++ src/libwalletqt/Wallet.h | 3 + src/utils/WebsocketClient.cpp | 1 - src/utils/nodes.cpp | 4 +- 9 files changed, 307 insertions(+), 102 deletions(-) create mode 100644 src/dialog/SyncRangeDialog.cpp create mode 100644 src/dialog/SyncRangeDialog.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4034c64d..07dc6c07 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,8 @@ name: ci/gh-actions/build -on: [pull_request] +on: + push: [master, main, dev] + pull_request: jobs: build-ubuntu-without-scanner: @@ -30,6 +32,65 @@ jobs: 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 diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 242d1de9..d96ea975 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "constants.h" @@ -31,6 +32,7 @@ #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" @@ -119,6 +121,7 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa // 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); @@ -291,99 +294,15 @@ void MainWindow::initStatusBar() { 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 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::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()))); } }); @@ -413,7 +332,11 @@ void MainWindow::initStatusBar() { 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)); + } } } }); @@ -897,11 +820,8 @@ void MainWindow::onBalanceUpdated(quint64 balance, quint64 spendable) { } } - 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; @@ -910,6 +830,16 @@ void MainWindow::onBalanceUpdated(quint64 balance, quint64 spendable) { 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) { @@ -1028,19 +958,24 @@ void MainWindow::onMultiBroadcast(const QMap &txHexMap) { 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; diff --git a/src/MainWindow.h b/src/MainWindow.h index d5d1a862..37c86090 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -207,6 +207,7 @@ private: 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); diff --git a/src/dialog/SyncRangeDialog.cpp b/src/dialog/SyncRangeDialog.cpp new file mode 100644 index 00000000..15e2971d --- /dev/null +++ b/src/dialog/SyncRangeDialog.cpp @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: The Monero Project + +#include "SyncRangeDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#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::of(&QSpinBox::valueChanged), this, &SyncRangeDialog::updateFromDate); + + // Connect preset dropdown + connect(m_presetCombo, QOverload::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 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(); +} diff --git a/src/dialog/SyncRangeDialog.h b/src/dialog/SyncRangeDialog.h new file mode 100644 index 00000000..baa9294d --- /dev/null +++ b/src/dialog/SyncRangeDialog.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: The Monero Project + +#ifndef FEATHER_SYNCRANGEDIALOG_H +#define FEATHER_SYNCRANGEDIALOG_H + +#include +#include + +#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 diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 8aadcb81..f28424ac 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -418,6 +418,15 @@ void Wallet::setDaemonLogin(const QString &daemonUsername, const QString &daemon 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. @@ -567,6 +576,10 @@ void Wallet::onHeightsRefreshed(bool success, quint64 daemonHeight, quint64 targ } else { setConnectionStatus(ConnectionStatus_Disconnected); } + + if (success) { + m_lastSyncTime = QDateTime::currentDateTime(); + } } quint64 Wallet::blockChainHeight() const { @@ -591,6 +604,10 @@ void Wallet::setSyncPaused(bool paused) { } } +QDateTime Wallet::lastSyncTime() const { + return m_lastSyncTime; +} + void Wallet::skipToTip() { if (!m_wallet2) return; diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index 0e62d0bb..099ded2b 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -138,6 +138,8 @@ public: //! returns if view only wallet bool viewOnly() const; + QDateTime lastSyncTime() const; + //! return true if deterministic keys bool isDeterministic() const; @@ -504,6 +506,7 @@ private: quint64 m_daemonBlockChainHeight; quint64 m_daemonBlockChainTargetHeight; + QDateTime m_lastSyncTime; ConnectionStatus m_connectionStatus; diff --git a/src/utils/WebsocketClient.cpp b/src/utils/WebsocketClient.cpp index 45b44799..a894e177 100644 --- a/src/utils/WebsocketClient.cpp +++ b/src/utils/WebsocketClient.cpp @@ -83,7 +83,6 @@ void WebsocketClient::restart() { void WebsocketClient::stop() { qDebug() << Q_FUNC_INFO; m_stopped = true; - webSocket->disconnect(); webSocket->abort(); m_connectionTimeout.stop(); m_pingTimer.stop(); diff --git a/src/utils/nodes.cpp b/src/utils/nodes.cpp index 38dbdb74..4b55fb92 100644 --- a/src/utils/nodes.cpp +++ b/src/utils/nodes.cpp @@ -551,7 +551,9 @@ void Nodes::resetLocalState() { } 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 Nodes::nodes() { -- 2.52.0