]> Nutra Git (v1) - gamesguru/feather.git/commitdiff
Implement Skip Sync and Data Saving features
authorgg <chown_tee@proton.me>
Wed, 7 Jan 2026 11:37:00 +0000 (06:37 -0500)
committergg <chown_tee@proton.me>
Wed, 7 Jan 2026 11:37:00 +0000 (06:37 -0500)
Logic:
- Add 'Skip to Tip', 'Date Range', and 'Full Sync' engine to libwalletqt
- Implement 'Scan Transaction' functionality for specific TXIDs

UI:
- Add context menu actions to bottom bar for selective sync
- Display block-depth count in status bar, courtesy of @masflam

bounty claimed here (as "mr_overquald")
https://bounties.monero.social/posts/79/1-230m-add-a-skip-sync-feature-to-a-monero-wallet

Co-authored-by: MasFlam <masflam@masflam.com>
refactor, cleanup, and format code.

allow for storing a debug version in the build #

wip gem

idk gem, little pruney there

restore master. Let's go from there again

Implement Skip Sync and Data Saving features

Logic:
- Add 'Skip to Tip', 'Date Range', and 'Full Sync' engine to libwalletqt
- Implement 'Scan Transaction' functionality for specific TXIDs

UI:
- Add context menu actions to bottom bar for selective sync
- Display block-depth count in status bar, courtesy of @masflam

bounty claimed here (as "mr_overquald")
https://bounties.monero.social/posts/79/1-230m-add-a-skip-sync-feature-to-a-monero-wallet

Co-authored-by: MasFlam <masflam@masflam.com>
allow for storing a debug version in the build #

idk gem, little pruney there

allow for storing a debug version in the build num (merge: keep-both)

fix cmakelists

wip

ds updates to Wallet.cpp/Wallet.h

fix build error

wip

wip2

super

getting there

fix warning/info messages

bigger scan Tx window

fix transaction diaglogue sizing

fix warning/info in build logs

updae message box stuff

better

fix compile error; hopefully persist settings?

fixing it up

laughable bug

better conditional & debug logging

better?

tidy estimatedBytes

use simple QDialog for transaction Scan window

rename to syncPause

properly dispose of QThreadStorage disposal

$ ./build/bin/feather --version
FeatherWallet 2.8.1-79-g16eec531
QThreadStorage: entry 2 destroyed before end of thread 0x562e3e2b3b90
QThreadStorage: entry 1 destroyed before end of thread 0x562e3e2b3b90

shellcheck stuff?

wip1

wip2

more wip

better

improvements in status bar

debug build

better!

keep trucking

better synch status

trying to get status always updated

put into helper method

restore CMakeLists.txt back to master status

polishing for review

remove formatting diffs; remove BCUR ref

refactor bool importTransaction()

13 files changed:
CMakeLists.txt
setup_flags.sh [new file with mode: 0644]
src/CMakeLists.txt
src/MainWindow.cpp
src/MainWindow.h
src/WindowManager.cpp
src/libwalletqt/Wallet.cpp
src/libwalletqt/Wallet.h
src/model/SubaddressProxyModel.h
src/utils/Utils.cpp
src/utils/Utils.h
src/utils/config.cpp
src/utils/config.h

index dd2087adec942951e743faea3d20f821f9de0c93..09fd8a860ef629ce3693e5acfa1b4319a7685534 100644 (file)
@@ -1,7 +1,34 @@
-cmake_minimum_required(VERSION 3.18)
+cmake_minimum_required(VERSION 3.16)
+
+# Configurable options
+option(FEATHER_VERSION_DEBUG_BUILD "Enable git describe version string" OFF)
+
+if(FEATHER_VERSION_DEBUG_BUILD AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
+    find_package(Git)
+    if(GIT_FOUND)
+        execute_process(
+            COMMAND ${GIT_EXECUTABLE} describe --tags --dirty --always
+            WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+            OUTPUT_VARIABLE GIT_DESCRIBE_VERSION
+            OUTPUT_STRIP_TRAILING_WHITESPACE
+        )
+        message(STATUS "Feather DEBUG BUILD version (git): ${GIT_DESCRIBE_VERSION}")
+        set(DETECTED_FEATHER_VERSION ${GIT_DESCRIBE_VERSION})
+    endif()
+endif()
+
+if(NOT DETECTED_FEATHER_VERSION)
+    set(DETECTED_FEATHER_VERSION "2.8.1")
+endif()
+
+# Extract standard version format for project()
+string(REGEX MATCH "^[0-9]+\\.[0-9]+\\.[0-9]+" PROJECT_VERSION_CLEAN "${DETECTED_FEATHER_VERSION}")
+if(NOT PROJECT_VERSION_CLEAN)
+    set(PROJECT_VERSION_CLEAN "2.8.1")
+endif()
 
 project(feather
-  VERSION "2.8.1"
+  VERSION ${PROJECT_VERSION_CLEAN}
   DESCRIPTION "A free Monero desktop wallet"
   LANGUAGES CXX C ASM
 )
diff --git a/setup_flags.sh b/setup_flags.sh
new file mode 100644 (file)
index 0000000..9112cf7
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+# Configure CMake with custom flags
+# - FEATHER_VERSION_DEBUG_BUILD=ON : Use git describe for version string
+# - CHECK_UPDATES=OFF              : Disable built-in update checker
+# - USE_DEVICE_TREZOR=OFF          : Disable Trezor hardware wallet support
+# - WITH_SCANNER=OFF               : Disable webcam QR scanner support
+
+cmake \
+    -DCMAKE_BUILD_TYPE=Debug \
+    -DFEATHER_VERSION_DEBUG_BUILD=ON \
+    -DCHECK_UPDATES=OFF \
+    -DUSE_DEVICE_TREZOR=OFF \
+    -DWITH_SCANNER=OFF \
+    ..
index 1ad529ca62a5df060b5e986a0620fc6a9c3f220e..f94d2e38d875e28a5469cefcb69f908050337c14 100644 (file)
@@ -185,7 +185,7 @@ target_include_directories(feather PUBLIC
         ${BCUR_INCLUDE_DIR}
 )
 
-target_compile_definitions(feather PRIVATE FEATHER_VERSION="${PROJECT_VERSION}")
+target_compile_definitions(feather PRIVATE FEATHER_VERSION="${DETECTED_FEATHER_VERSION}")
 target_compile_definitions(feather PRIVATE FEATHER_TARGET_TRIPLET="${FEATHER_TARGET_TRIPLET}")
 target_compile_definitions(feather PRIVATE TOR_VERSION="${TOR_VERSION}")
 
@@ -289,7 +289,6 @@ target_link_libraries(feather PRIVATE
         ${ICU_LIBRARIES}
         ${LIBZIP_LIBRARIES}
         ${ZLIB_LIBRARIES}
-        ${BCUR_LIBRARY}
 )
 
 if(CHECK_UPDATES)
index 6a2d6b80368eeea58d95b0e776fce0173e06af23..7b58f564450c6f4feea8838e0f7f06249bf1f85c 100644 (file)
@@ -8,6 +8,10 @@
 #include <QInputDialog>
 #include <QMessageBox>
 #include <QCheckBox>
+#include <QFormLayout>
+#include <QSpinBox>
+#include <QDateEdit>
+#include <QDialogButtonBox>
 
 #include "constants.h"
 #include "dialog/AddressCheckerIndexDialog.h"
 #include "libwalletqt/TransactionHistory.h"
 #include "model/AddressBookModel.h"
 #include "plugins/PluginRegistry.h"
+#include "plugins/Plugin.h"
 #include "utils/AppData.h"
 #include "utils/AsyncTask.h"
 #include "utils/ColorScheme.h"
 #include "utils/Icons.h"
+#include "utils/RestoreHeightLookup.h"
 #include "utils/TorManager.h"
 #include "utils/WebsocketNotifier.h"
 
@@ -80,7 +86,7 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa
 
     this->onOfflineMode(conf()->get(Config::offlineMode).toBool());
     conf()->set(Config::restartRequired, false);
-    
+
     // Websocket notifier
 #ifdef CHECK_UPDATES
     connect(websocketNotifier(), &WebsocketNotifier::UpdatesReceived, m_updater.data(), &Updater::wsUpdatesReceived);
@@ -135,7 +141,7 @@ void MainWindow::initStatusBar() {
     this->statusBar()->setFixedHeight(30);
 
     m_statusLabelStatus = new QLabel("Idle", this);
-    m_statusLabelStatus->setTextInteractionFlags(Qt::TextSelectableByMouse);
+    m_statusLabelStatus->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse);
     this->statusBar()->addWidget(m_statusLabelStatus);
 
     m_statusLabelNetStats = new QLabel("", this);
@@ -188,6 +194,221 @@ void MainWindow::initStatusBar() {
     connect(m_statusBtnHwDevice, &StatusBarButton::clicked, this, &MainWindow::menuHwDeviceClicked);
     this->statusBar()->addPermanentWidget(m_statusBtnHwDevice);
     m_statusBtnHwDevice->hide();
+
+    m_statusLabelStatus->setContextMenuPolicy(Qt::ActionsContextMenu);
+
+    QAction *pauseSyncAction = new QAction(tr("Pause Sync"), this);
+    pauseSyncAction->setCheckable(true);
+    pauseSyncAction->setChecked(conf()->get(Config::syncPaused).toBool());
+    m_statusLabelStatus->addAction(pauseSyncAction);
+
+    QAction *skipSyncAction = new QAction(tr("Skip Sync"), this);
+    m_statusLabelStatus->addAction(skipSyncAction);
+
+    QAction *syncRangeAction = new QAction(tr("Sync Date Range..."), this);
+    m_statusLabelStatus->addAction(syncRangeAction);
+
+    QAction *fullSyncAction = new QAction(tr("Full Sync"), this);
+    m_statusLabelStatus->addAction(fullSyncAction);
+
+    QAction *scanTxAction = new QAction(tr("Scan Transaction"), this);
+    m_statusLabelStatus->addAction(scanTxAction);
+    ui->menuTools->addAction(scanTxAction);
+
+    connect(pauseSyncAction, &QAction::toggled, this, [this](bool checked) {
+        conf()->set(Config::syncPaused, checked);
+        if (m_wallet) {
+            if (checked) {
+                m_wallet->pauseRefresh();
+                
+                this->setPausedSyncStatus();
+            } else {
+                m_wallet->startRefresh();
+                this->setStatusText(tr("Resuming sync..."));
+            }
+        }
+    });
+
+    connect(skipSyncAction, &QAction::triggered, this, [this](){
+        if (!m_wallet) return;
+
+        QString msg = tr("Skip sync will set your wallet's restore height to the current network height.\n\n"
+                          "Use this if you know you haven't received any transactions since your last sync.\n"
+                          "You can always use 'Full Sync' to rescan from the beginning.\n\n"
+                          "Continue?");
+
+        if (QMessageBox::question(this, tr("Skip Sync"), msg) == QMessageBox::Yes) {
+            m_wallet->skipToTip();
+            this->setStatusText(tr("Skipped sync to tip."));
+        }
+    });
+
+    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->setCalendarPopup(true);
+        fromDateEdit->setDisplayFormat("yyyy-MM-dd");
+        fromDateEdit->setToolTip(tr("Calculated from 'End date' and day span."));
+
+
+        toDateEdit->setCalendarPopup(true);
+        toDateEdit->setDisplayFormat("yyyy-MM-dd");
+
+        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());
+
+        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);
+
+            this->setStatusText(QString("Syncing range %1 - %2 (~%3)...")
+                                .arg(fromDateEdit->date().toString("yyyy-MM-dd"))
+                                .arg(toDateEdit->date().toString("yyyy-MM-dd"))
+                                .arg(Utils::formatBytes(size)));
+        }
+    });
+
+    connect(fullSyncAction, &QAction::triggered, this, [this](){
+        if (m_wallet) {
+            QString estBlocks = "Unknown (waiting for node)";
+            QString estSize = "Unknown";
+
+            quint64 walletCreationHeight = m_wallet->getWalletCreationHeight();
+            quint64 daemonHeight = m_wallet->daemonBlockChainHeight();
+            quint64 blocksBehind = 0;
+
+            if (daemonHeight > 0) {
+                blocksBehind = (daemonHeight > walletCreationHeight) ? (daemonHeight - walletCreationHeight) : 0;
+                quint64 estimatedBytes = Utils::estimateSyncDataSize(blocksBehind);
+                estBlocks = QString::number(blocksBehind);
+                estSize = QString("~%1").arg(Utils::formatBytes(estimatedBytes));
+            }
+
+            QString msg = QString("Full sync will rescan from your restore height.\n\n"
+                                  "Blocks behind: %1\n"
+                                  "Estimated data: %2\n\n"
+                                  "Note: We will not rescan blocks which have cached/hashed/checksummed, "
+                                  "and any discrepancy between block height and daemon height can be understood in these terms.\n\n"
+                                  "Continue?")
+                          .arg(estBlocks)
+                          .arg(estSize);
+
+            if (QMessageBox::question(this, "Full Sync", msg) == QMessageBox::Yes) {
+                m_wallet->fullSync();
+                this->setStatusText(QString("Full sync started (%1)...").arg(estSize));
+            }
+        }
+    });
+
+    connect(scanTxAction, &QAction::triggered, this, [this](){
+        if (m_wallet) {
+            QDialog dialog(this);
+            dialog.setWindowTitle("Scan Transaction");
+            dialog.setWindowIcon(QIcon(":/assets/images/appicons/64x64.png"));
+
+            auto *layout = new QVBoxLayout(&dialog);
+            layout->addWidget(new QLabel("Enter transaction ID:"));
+
+            auto *lineEdit = new QLineEdit;
+            lineEdit->setMinimumWidth(600);
+            layout->addWidget(lineEdit);
+
+            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);
+
+            // Compact vertical size, allow horizontal resize
+            dialog.layout()->setSizeConstraint(QLayout::SetFixedSize);
+
+            if (dialog.exec() == QDialog::Accepted) {
+                QString txid = lineEdit->text();
+                if (!txid.isEmpty()) {
+                    m_wallet->importTransaction(txid.trimmed());
+                    this->setStatusText("Transaction scanned: " + txid.trimmed().left(8) + "...");
+                }
+            }
+        }
+    });
 }
 
 void MainWindow::initPlugins() {
@@ -489,6 +710,17 @@ void MainWindow::initWalletContext() {
         this->onConnectionStatusChanged(status);
         m_nodes->autoConnect();
     });
+
+    connect(m_wallet, &Wallet::heightsRefreshed, this, [this](bool success, quint64 daemonHeight, quint64 targetHeight) {
+        // When paused, we might get success=false because wallet->refresh() is skipped,
+        // preventing strict cache updates. We should attempt to fallback to m_nodes info.
+        if (!success && !conf()->get(Config::syncPaused).toBool()) return;
+
+        // Update sync estimate if paused
+        if (conf()->get(Config::syncPaused).toBool()) {
+             this->setPausedSyncStatus();
+        }
+    });
     connect(m_wallet, &Wallet::currentSubaddressAccountChanged, this, &MainWindow::updateTitle);
     connect(m_wallet, &Wallet::walletPassphraseNeeded, this, &MainWindow::onWalletPassphraseNeeded);
 
@@ -509,7 +741,7 @@ void MainWindow::initWalletContext() {
     connect(m_wallet, &Wallet::deviceButtonRequest, this, &MainWindow::onDeviceButtonRequest);
     connect(m_wallet, &Wallet::deviceButtonPressed, this, &MainWindow::onDeviceButtonPressed);
     connect(m_wallet, &Wallet::deviceError,         this, &MainWindow::onDeviceError);
-    
+
     connect(m_wallet, &Wallet::multiBroadcast,      this, &MainWindow::onMultiBroadcast);
 }
 
@@ -590,7 +822,13 @@ void MainWindow::onWalletOpened() {
     this->updatePasswordIcon();
     this->updateTitle();
     m_nodes->allowConnection();
-    m_nodes->connectToNode();
+    if (!conf()->get(Config::disableAutoRefresh).toBool()) {
+        m_nodes->connectToNode();
+        if (conf()->get(Config::syncPaused).toBool()) {
+            m_wallet->pauseRefresh();
+            this->setPausedSyncStatus();
+        }
+    }
     m_updateBytes.start(250);
 
     if (conf()->get(Config::writeRecentlyOpenedWallets).toBool()) {
@@ -632,6 +870,14 @@ void MainWindow::onBalanceUpdated(quint64 balance, quint64 spendable) {
     m_statusLabelBalance->setText(balance_str);
 }
 
+void MainWindow::setPausedSyncStatus() {
+    QString tooltip;
+    QString status = Utils::getPausedSyncStatus(m_wallet, m_nodes, &tooltip);
+    this->setStatusText(status);
+    if (!tooltip.isEmpty())
+        m_statusLabelStatus->setToolTip(tooltip);
+}
+
 void MainWindow::setStatusText(const QString &text, bool override, int timeout) {
     if (override) {
         m_statusOverrideActive = true;
@@ -742,11 +988,30 @@ void MainWindow::onMultiBroadcast(const QMap<QString, QString> &txHexMap) {
 }
 
 void MainWindow::onSyncStatus(quint64 height, quint64 target, bool daemonSync) {
-    if (height >= (target - 1)) {
+    if (height >= (target - 1) && target > 0) {
         this->updateNetStats();
+        this->setStatusText(QString("Synchronized (%1)").arg(height));
+    } else {
+        // Calculate depth
+        quint64 blocksLeft = (target > height) ? (target - height) : 0;
+        // Estimate download size in MB: assuming 500 MB for full history,
+        // and we are blocksLeft behind out of (target-1) total blocks (since block 0 is genesis)
+        double approximateSizeMB = 0.0;
+        if (target > 1) {
+            approximateSizeMB = (blocksLeft * 500.0) / (target - 1);
+        }
+        QString sizeText;
+        if (approximateSizeMB < 1) {
+            sizeText = QString("%1 KB").arg(QString::number(approximateSizeMB * 1024, 'f', 0));
+        } else {
+            sizeText = QString("%1 MB").arg(QString::number(approximateSizeMB, 'f', 1));
+        }
+        QString statusMsg = Utils::formatSyncStatus(height, target, daemonSync);
+        // Shows "# blocks remaining (approx. X MB)" to sync
+        // Shows "Wallet sync: # blocks remaining (approx. X MB)"
+        this->setStatusText(QString("%1 (approx. %2)").arg(statusMsg).arg(sizeText));
     }
-    this->setStatusText(Utils::formatSyncStatus(height, target, daemonSync));
-    m_statusLabelStatus->setToolTip(QString("Wallet height: %1").arg(QString::number(height)));
+    m_statusLabelStatus->setToolTip(QString("Wallet Height: %1 | Network Tip: %2").arg(height).arg(target));
 }
 
 void MainWindow::onConnectionStatusChanged(int status)
@@ -788,6 +1053,12 @@ void MainWindow::onConnectionStatusChanged(int status)
         }
     }
 
+    if (conf()->get(Config::syncPaused).toBool() && !conf()->get(Config::offlineMode).toBool()) {
+        if (status == Wallet::ConnectionStatus_Synchronizing || status == Wallet::ConnectionStatus_Synchronized) {
+            this->setPausedSyncStatus();
+        }
+    }
+
     m_statusBtnConnectionStatusIndicator->setIcon(icon);
 }
 
@@ -967,7 +1238,7 @@ void MainWindow::onTransactionCreated(PendingTransaction *tx, const QVector<QStr
 #ifdef WITH_SCANNER
         OfflineTxSigningWizard wizard(this, m_wallet, tx);
         wizard.exec();
-        
+
         if (!wizard.readyToCommit()) {
             return;
         } else {
@@ -1142,7 +1413,7 @@ void MainWindow::showKeyImageSyncWizard() {
 #ifdef WITH_SCANNER
     OfflineTxSigningWizard wizard{this, m_wallet};
     wizard.exec();
-    
+
     if (wizard.readyToSign()) {
         TxConfAdvDialog dialog{m_wallet, "", this, true};
         dialog.setUnsignedTransaction(wizard.unsignedTransaction());
@@ -1533,6 +1804,11 @@ void MainWindow::updateNetStats() {
         return;
     }
 
+    if (conf()->get(Config::syncPaused).toBool()) {
+        m_statusLabelNetStats->hide();
+        return;
+    }
+
     m_statusLabelNetStats->show();
     m_statusLabelNetStats->setText(QString("(D: %1)").arg(Utils::formatBytes(m_wallet->getBytesReceived())));
 }
@@ -1544,12 +1820,12 @@ void MainWindow::rescanSpent() {
                     "Make sure you are connected to a trusted node.\n\n"
                     "Do you want to proceed?");
     warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
-    
+
     auto r = warning.exec();
     if (r == QMessageBox::No) {
         return;
     }
-    
+
     if (!m_wallet->rescanSpent()) {
         Utils::showError(this, "Failed to rescan spent outputs", m_wallet->errorString());
     } else {
index c44270d32dd8a91bdf243ddbc57a68875913dd7f..d465ceba8ca817272ef347f020929d48a7972211 100644 (file)
@@ -209,6 +209,7 @@ private:
     void unlockWallet(const QString &password);
     void closeQDialogChildren(QObject *object);
     int findTab(const QString &title);
+    void setPausedSyncStatus();
 
     QIcon hardwareDevicePairedIcon();
     QIcon hardwareDeviceUnpairedIcon();
index 5d6696e9abd1cbdc275c2a2d26726726f5398f1e..13ad493f0980bd8aa27120361ed5bd944bfb5c80 100644 (file)
@@ -71,9 +71,13 @@ void WindowManager::setEventFilter(EventFilter *ef) {
 
 WindowManager::~WindowManager() {
     qDebug() << "~WindowManager";
-    m_cleanupThread->quit();
-    m_cleanupThread->wait();
-    qDebug() << "WindowManager: cleanup thread done" << QThread::currentThreadId();
+    if (m_cleanupThread && m_cleanupThread->isRunning()) {
+        m_cleanupThread->quit();
+        m_cleanupThread->wait();
+        qDebug() << "WindowManager: cleanup thread done" << QThread::currentThreadId();
+    } else {
+        qDebug() << "WindowManager: cleanup thread already stopped";
+    }
 }
 
 // ######################## APPLICATION LIFECYCLE ########################
@@ -89,6 +93,20 @@ void WindowManager::quitAfterLastWindow() {
 
 void WindowManager::close() {
     qDebug() << Q_FUNC_INFO << QThread::currentThreadId();
+
+    // Stop all threads before application shutdown to avoid QThreadStorage warnings
+    if (m_cleanupThread && m_cleanupThread->isRunning()) {
+        m_cleanupThread->quit();
+        m_cleanupThread->wait();
+        qDebug() << "WindowManager: cleanup thread stopped in close()";
+    }
+
+    // Stop Tor manager threads
+    torManager()->stop();
+
+    // Wait for all threads in the global thread pool
+    QThreadPool::globalInstance()->waitForDone();
+
     for (const auto &window: m_windows) {
         window->close();
     }
@@ -106,8 +124,6 @@ void WindowManager::close() {
         m_docsDialog->deleteLater();
     }
 
-    torManager()->stop();
-
     deleteLater();
 
     qDebug() << "Calling QApplication::quit()";
index c26e40d99a8e6aca7dc12d305a046c5914ebabe1..85ec7399c1b5739aa38acdc4a9b5b7ad6d8022c6 100644 (file)
@@ -14,6 +14,7 @@
 #include "WalletManager.h"
 #include "WalletListenerImpl.h"
 
+#include "utils/config.h"
 #include "config.h"
 #include "constants.h"
 
@@ -25,6 +26,8 @@
 #include "model/CoinsModel.h"
 
 #include "utils/ScopeGuard.h"
+#include "utils/RestoreHeightLookup.h"
+#include "utils/Utils.h"
 
 #include "wallet/wallet2.h"
 
@@ -83,6 +86,15 @@ Wallet::Wallet(Monero::Wallet *wallet, QObject *parent)
     connect(m_subaddress, &Subaddress::corrupted, [this]{
        emit keysCorrupted();
     });
+
+    // Store original creation height if not already present
+    // This protects the restore height from being overwritten by "Skip Sync" or range syncs
+    if (!cacheAttributeExists("feather.creation_height")) {
+        quint64 height = m_wallet2->get_refresh_from_block_height();
+        if (height > 0) {
+            setCacheAttribute("feather.creation_height", QString::number(height));
+        }
+    }
 }
 
 // #################### Status ####################
@@ -502,13 +514,26 @@ void Wallet::onHeightsRefreshed(bool success, quint64 daemonHeight, quint64 targ
     m_daemonBlockChainHeight = daemonHeight;
     m_daemonBlockChainTargetHeight = targetHeight;
 
+    if (conf()->get(Config::syncPaused).toBool()) {
+        if (success) {
+            quint64 walletHeight = blockChainHeight();
+            if (walletHeight < (targetHeight - 1)) {
+                setConnectionStatus(ConnectionStatus_Synchronizing);
+            } else {
+                setConnectionStatus(ConnectionStatus_Synchronized);
+            }
+        } else {
+             setConnectionStatus(ConnectionStatus_Disconnected);
+        }
+        return;
+    }
+
     if (success) {
         quint64 walletHeight = blockChainHeight();
 
         if (daemonHeight < targetHeight) {
             emit syncStatus(daemonHeight, targetHeight, true);
-        }
-        else {
+        } else {
             this->syncStatusUpdated(walletHeight, daemonHeight);
         }
 
@@ -544,7 +569,120 @@ void Wallet::syncStatusUpdated(quint64 height, quint64 target) {
     emit syncStatus(height, target, false);
 }
 
+void Wallet::skipToTip() {
+    if (!m_wallet2)
+        return;
+    uint64_t tip = m_wallet2->get_blockchain_current_height();
+    if (tip > 0) {
+        // Log previous height for debugging
+        uint64_t prevHeight = m_wallet2->get_refresh_from_block_height();
+        qInfo() << "Skip Sync triggered. Head moving from" << prevHeight << "to:" << tip;
+
+        m_wallet2->set_refresh_from_block_height(tip);
+        pauseRefresh();
+        startRefresh();
+    }
+}
+
+void Wallet::syncDateRange(const QDate &start, const QDate &end) {
+    if (!m_wallet2)
+        return;
+
+    // Convert dates to heights with internal table lookup
+    cryptonote::network_type nettype = m_wallet2->nettype();
+    QString filename = Utils::getRestoreHeightFilename(static_cast<NetworkType::Type>(nettype));
+
+    auto lookup = RestoreHeightLookup::fromFile(filename, static_cast<NetworkType::Type>(nettype));
+    uint64_t startHeight = lookup->dateToHeight(start.startOfDay().toSecsSinceEpoch());
+    uint64_t endHeight = lookup->dateToHeight(end.startOfDay().toSecsSinceEpoch());
+    delete lookup;
+
+    if (startHeight >= endHeight)
+        return;
+
+    m_stopHeight = endHeight;
+    m_rangeSyncActive = true;
+
+    m_wallet2->set_refresh_from_block_height(startHeight);
+    pauseRefresh();
+    startRefresh();
+}
+
+void Wallet::fullSync() {
+    if (!m_wallet2)
+        return;
+
+    // Reset range sync just in case
+    m_rangeSyncActive = false;
+
+    // Retrieve original creation height from persistent storage
+    uint64_t originalHeight = 0;
+    QString storedHeight = this->getCacheAttribute("feather.creation_height");
+    if (!storedHeight.isEmpty()) {
+        originalHeight = storedHeight.toULongLong();
+    } else {
+        // Fallback (dangerous if skipped, but better than 0)
+        originalHeight = m_wallet2->get_refresh_from_block_height();
+>>>>>>> 43ee0e4e (Implement Skip Sync and Data Saving features)
+    }
+
+    m_wallet2->set_refresh_from_block_height(originalHeight);
+    // Trigger rescan
+    pauseRefresh();
+
+    // Optional: Clear transaction cache to ensure fresh view
+    // m_wallet2->clearCache();
+
+    startRefresh();
+
+    qInfo() << "Full Sync triggered. Rescanning from original restore height:" << originalHeight;
+}
+
+void Wallet::syncStatusUpdated(quint64 height, quint64 targetHeight) {
+    if (m_rangeSyncActive && height >= m_stopHeight) {
+        // At end of requested date range, jump to tip
+        m_rangeSyncActive = false;
+        this->skipToTip();
+        return;
+    }
+
+    if (height >= (targetHeight - 1)) {
+        this->updateBalance();
+    }
+    emit syncStatus(height, targetHeight, false);
+}
+
+bool Wallet::importTransaction(const QString &txid) {
+    if (!m_wallet2 || txid.isEmpty())
+        return false;
+
+    // If scanning a specific TX, we shouldn't be constrained by range sync
+    if (m_rangeSyncActive) {
+        m_rangeSyncActive = false;
+    }
+
+    try {
+        std::unordered_set<crypto::hash> txids;
+        crypto::hash txid_hash;
+        if (!epee::string_tools::hex_to_pod(txid.toStdString(), txid_hash)) {
+            qWarning() << "Invalid transaction id: " << txid;
+            return false;
+        }
+        txids.insert(txid_hash);
+        m_wallet2->scan_tx(txids);
+        qInfo() << "Successfully imported transaction:" << txid;
+        this->updateBalance();
+        return true;
+    } catch (const std::exception &e) {
+        qWarning() << "Failed to import transaction: " << txid << ", error: " << e.what();
+    }
+    return false;
+}
+
 void Wallet::onNewBlock(uint64_t walletHeight) {
+    if (conf()->get(Config::syncPaused).toBool()) {
+        return;
+    }
     // Called whenever a new block gets scanned by the wallet
     quint64 daemonHeight = m_daemonBlockChainTargetHeight;
 
@@ -693,11 +831,6 @@ bool Wallet::importOutputsFromStr(const std::string &outputs) {
     return m_walletImpl->importOutputsFromStr(outputs);
 }
 
-bool Wallet::importTransaction(const QString& txid) {
-    std::vector<std::string> txids = {txid.toStdString()};
-    return m_walletImpl->scanTransactions(txids);
-}
-
 // #################### Wallet cache ####################
 
 void Wallet::store() {
index 20d21af496352baf59fd8d88530ad23c43d0c5a9..8dee43654e127a7b11d01bb519bdd5c63b0b45d1 100644 (file)
@@ -142,7 +142,7 @@ public:
     bool isDeterministic() const;
 
     QString walletName() const;
-    
+
     // ##### Balance #####
     //! returns balance
     quint64 balance() const;
@@ -153,7 +153,7 @@ public:
     quint64 unlockedBalance() const;
     quint64 unlockedBalance(quint32 accountIndex) const;
     quint64 unlockedBalanceAll() const;
-    
+
     quint64 viewOnlyBalance(quint32 accountIndex) const;
 
     void updateBalance();
@@ -229,6 +229,11 @@ public:
     quint64 daemonBlockChainTargetHeight() const;
 
     void syncStatusUpdated(quint64 height, quint64 target);
+    Q_INVOKABLE void skipToTip();
+    Q_INVOKABLE void syncDateRange(const QDate &start, const QDate &end);
+    void fullSync(); // from restoreHeight, not genesis - recreate your wallet for that ;P
+
+    bool importTransaction(const QString &txid);
 
     void refreshModels();
 
@@ -250,25 +255,22 @@ public:
     void setForceKeyImageSync(bool enabled);
     bool hasUnknownKeyImages() const;
     bool keyImageSyncNeeded(quint64 amount, bool sendAll) const;
-    
+
     //! export/import key images
     bool exportKeyImages(const QString& path, bool all = false);
     bool exportKeyImagesToStr(std::string &keyImages, bool all = false);
     bool exportKeyImagesForOutputsFromStr(const std::string &outputs, std::string &keyImages);
-    
+
     bool importKeyImages(const QString& path);
     bool importKeyImagesFromStr(const std::string &keyImages);
 
     //! export/import outputs
     bool exportOutputs(const QString& path, bool all = false);
     bool exportOutputsToStr(std::string& outputs, bool all);
-    
+
     bool importOutputs(const QString& path);
     bool importOutputsFromStr(const std::string &outputs);
 
-    //! import a transaction
-    bool importTransaction(const QString& txid);
-
     // ##### Wallet cache #####
     //! saves wallet to the file by given path
     //! empty path stores in current location
@@ -342,7 +344,7 @@ public:
     //! Sign a transfer from file
     UnsignedTransaction * loadTxFile(const QString &fileName);
     UnsignedTransaction * loadUnsignedTransactionFromStr(const std::string &data);
-    
+
     //! Load an unsigned transaction from a base64 encoded string
     UnsignedTransaction * loadTxFromBase64Str(const QString &unsigned_tx);
 
@@ -529,6 +531,10 @@ private:
 
     QTimer *m_storeTimer = nullptr;
     std::set<std::string> m_selectedInputs;
+
+    uint64_t m_stopHeight = 0;
+    bool m_rangeSyncActive = false;
 };
 
 #endif // FEATHER_WALLET_H
+
index 38069ed1e41b96651d8346d70ea2530835576fae..833c6f25dd528bb56887c4e3870d1f0a7d895ba9 100644 (file)
@@ -20,7 +20,7 @@ public slots:
     void setSearchFilter(const QString& searchString){
         m_searchRegExp.setPattern(searchString);
         m_searchCaseSensitiveRegExp.setPattern(searchString);
-        invalidateFilter();
+        invalidate();
     }
 
 private:
index b468fed39f8b01aa119921fffdaf0e42019f8031..0ef06d99324976b6c67e30763d2dce41b49aa6ca 100644 (file)
@@ -26,6 +26,7 @@
 #include "utils/os/tails.h"
 #include "utils/os/whonix.h"
 #include "libwalletqt/Wallet.h"
+#include "utils/nodes.h"
 #include "WindowManager.h"
 
 namespace Utils {
@@ -712,6 +713,52 @@ QString formatSyncStatus(quint64 height, quint64 target, bool daemonSync) {
     return "Synchronized";
 }
 
+QString formatSyncTimeEstimate(quint64 blocks) {
+    quint64 minutes = blocks * 2;
+    quint64 days = minutes / (60 * 24);
+
+    QString timeStr = (days > 0) ? QString("~%1 days").arg(days) : QString("~%1 hours").arg(minutes / 60);
+    if (days == 0 && minutes < 60) timeStr = "< 1 hour";
+    return timeStr;
+}
+
+quint64 estimateSyncDataSize(quint64 blocks) {
+    // Estimate 1024 bytes per block (1KB) for wallet scanning.
+    return blocks * 1024;
+}
+
+QString formatPausedSyncStatus(quint64 blocks) {
+    quint64 size = estimateSyncDataSize(blocks);
+    return QObject::tr("[PAUSED] Sync %1 blocks / %2 upon resume").arg(blocks).arg(formatBytes(size));
+}
+
+QString getPausedSyncStatus(Wallet *wallet, Nodes *nodes, QString *tooltip) {
+    if (!wallet) return QObject::tr("[PAUSED] Sync paused");
+
+    quint64 walletHeight = wallet->blockChainHeight();
+    quint64 creationHeight = wallet->getWalletCreationHeight();
+    quint64 startHeight = (walletHeight > creationHeight) ? walletHeight : creationHeight;
+    quint64 daemonHeight = wallet->daemonBlockChainTargetHeight();
+
+    // If sync is paused or wallet just started, Wallet's internal height might be 0.
+    // If the daemon is connected, use its target_height or height to determine the latest tip.
+    if (daemonHeight == 0 && nodes) {
+        auto connection = nodes->connection();
+        if (connection.target_height > 0) daemonHeight = connection.target_height;
+        else if (connection.height > 0) daemonHeight = connection.height;
+    }
+
+    if (daemonHeight > 0) {
+        if (tooltip) {
+            *tooltip = QString("Wallet Height: %1 | Network Tip: %2").arg(walletHeight).arg(daemonHeight);
+        }
+        quint64 blocksBehind = (daemonHeight > startHeight) ? (daemonHeight - startHeight) : 0;
+        return formatPausedSyncStatus(blocksBehind);
+    } 
+    
+    return "[PAUSED] Sync paused";
+}
+
 QString formatRestoreHeight(quint64 height) {
     const QDateTime restoreDate = appData()->restoreHeights[constants::networkType]->heightToDate(height);
     return QString("%1  (%2)").arg(QString::number(height), restoreDate.toString("yyyy-MM-dd"));
@@ -724,4 +771,13 @@ QString getVersion() {
 #endif
     return version;
 }
+
+QString getRestoreHeightFilename(NetworkType::Type nettype) {
+    if (nettype == NetworkType::MAINNET)
+        return ":/assets/restore_heights_monero_mainnet.txt";
+    else if (nettype == NetworkType::TESTNET)
+        return ":/assets/restore_heights_monero_testnet.txt";
+    else
+        return ":/assets/restore_heights_monero_stagenet.txt";
+}
 }
index 22bba27fd829a61c7d8497a0f8a4bce9233fddd8..2f29bfc3e3ceec33cea3c9801653528a2c253c41 100644 (file)
@@ -14,6 +14,8 @@
 #include "networktype.h"
 
 class SubaddressIndex;
+class Wallet;
+class Nodes;
 
 namespace Utils
 {
@@ -119,9 +121,14 @@ namespace Utils
     void clearLayout(QLayout *layout, bool deleteWidgets = true);
 
     QString formatSyncStatus(quint64 height, quint64 target, bool daemonSync = false);
+    QString formatSyncTimeEstimate(quint64 blocks);
+    quint64 estimateSyncDataSize(quint64 blocks);
+    QString formatPausedSyncStatus(quint64 blocks);
+    QString getPausedSyncStatus(Wallet *wallet, Nodes *nodes, QString *tooltip = nullptr);
     QString formatRestoreHeight(quint64 height);
 
     QString getVersion();
+    QString getRestoreHeightFilename(NetworkType::Type nettype);
 }
 
 #endif //FEATHER_UTILS_H
index b5baa886d797ece9c12bcef280d41dd9bd6dab06..f9453d5cee41f7aa7aeee71af0e8213cce984f4d 100644 (file)
@@ -76,7 +76,9 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
         {Config::showTrayIcon, {QS("showTrayIcon"), true}},
         {Config::minimizeToTray, {QS("minimizeToTray"), false}},
         {Config::disableWebsocket, {QS("disableWebsocket"), false}},
+        {Config::disableAutoRefresh, {QS("disableAutoRefresh"), false}},
         {Config::offlineMode, {QS("offlineMode"), false}},
+        {Config::syncPaused, {QS("syncPaused"), false}},
 
         // Transactions
         {Config::multiBroadcast, {QS("multiBroadcast"), true}},
@@ -242,7 +244,9 @@ QDir Config::defaultConfigDir() {
 #elif defined(Q_OS_MACOS)
     return QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
 #else
-    return QDir(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/feather");
+    QDir path(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/feather");
+    qDebug() << "Config path resolved to: " << path.absolutePath();
+    return path;
 #endif
 }
 
index 04d9d7590c7ccf7b4155d5b886fa541771c97eb0..ffe9a6e674970064d4f8b53270fdcf8d83a84ebd 100644 (file)
@@ -88,6 +88,7 @@ public:
         disableWebsocket,
 
         // Network -> Offline
+        disableAutoRefresh,
         offlineMode,
 
         // Storage -> Logging
@@ -120,7 +121,7 @@ public:
         blockExplorers,
         blockExplorer,
         lastPath,
-        
+
         // UR
         URmsPerFragment,
         URfragmentLength,
@@ -139,6 +140,9 @@ public:
         // Tickers
         tickers,
         tickersShowFiatBalance,
+
+        // Sync & data saver
+        syncPaused,
     };
 
     enum PrivacyLevel {
@@ -169,7 +173,7 @@ public:
         UnifiedResources = 0,
         FileTransfer
     };
-    
+
     ~Config() override;
     QVariant get(ConfigKey key);
     QString getFileName();