From: gg Date: Wed, 7 Jan 2026 11:37:00 +0000 (-0500) Subject: Implement Skip Sync and Data Saving features X-Git-Url: https://git.nutra.tk/v1?a=commitdiff_plain;h=b698555ecc125a4e582d05f7062d503349bff1b0;p=gamesguru%2Ffeather.git 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 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 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() --- diff --git a/CMakeLists.txt b/CMakeLists.txt index dd2087ad..09fd8a86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 index 00000000..9112cf76 --- /dev/null +++ b/setup_flags.sh @@ -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 \ + .. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1ad529ca..f94d2e38 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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) diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 6a2d6b80..7b58f564 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -8,6 +8,10 @@ #include #include #include +#include +#include +#include +#include #include "constants.h" #include "dialog/AddressCheckerIndexDialog.h" @@ -30,10 +34,12 @@ #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 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::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 &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 QVectorget(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 { diff --git a/src/MainWindow.h b/src/MainWindow.h index c44270d3..d465ceba 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -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(); diff --git a/src/WindowManager.cpp b/src/WindowManager.cpp index 5d6696e9..13ad493f 100644 --- a/src/WindowManager.cpp +++ b/src/WindowManager.cpp @@ -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()"; diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index c26e40d9..85ec7399 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -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(nettype)); + + auto lookup = RestoreHeightLookup::fromFile(filename, static_cast(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 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 txids = {txid.toStdString()}; - return m_walletImpl->scanTransactions(txids); -} - // #################### Wallet cache #################### void Wallet::store() { diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index 20d21af4..8dee4365 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -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 m_selectedInputs; + + uint64_t m_stopHeight = 0; + bool m_rangeSyncActive = false; }; #endif // FEATHER_WALLET_H + diff --git a/src/model/SubaddressProxyModel.h b/src/model/SubaddressProxyModel.h index 38069ed1..833c6f25 100644 --- a/src/model/SubaddressProxyModel.h +++ b/src/model/SubaddressProxyModel.h @@ -20,7 +20,7 @@ public slots: void setSearchFilter(const QString& searchString){ m_searchRegExp.setPattern(searchString); m_searchCaseSensitiveRegExp.setPattern(searchString); - invalidateFilter(); + invalidate(); } private: diff --git a/src/utils/Utils.cpp b/src/utils/Utils.cpp index b468fed3..0ef06d99 100644 --- a/src/utils/Utils.cpp +++ b/src/utils/Utils.cpp @@ -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"; +} } diff --git a/src/utils/Utils.h b/src/utils/Utils.h index 22bba27f..2f29bfc3 100644 --- a/src/utils/Utils.h +++ b/src/utils/Utils.h @@ -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 diff --git a/src/utils/config.cpp b/src/utils/config.cpp index b5baa886..f9453d5c 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -76,7 +76,9 @@ static const QHash 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 } diff --git a/src/utils/config.h b/src/utils/config.h index 04d9d759..ffe9a6e6 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -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();