#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"
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);
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);
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() {
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);
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);
}
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()) {
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;
}
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)
}
}
+ 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);
}
#ifdef WITH_SCANNER
OfflineTxSigningWizard wizard(this, m_wallet, tx);
wizard.exec();
-
+
if (!wizard.readyToCommit()) {
return;
} else {
#ifdef WITH_SCANNER
OfflineTxSigningWizard wizard{this, m_wallet};
wizard.exec();
-
+
if (wizard.readyToSign()) {
TxConfAdvDialog dialog{m_wallet, "", this, true};
dialog.setUnsignedTransaction(wizard.unsignedTransaction());
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())));
}
"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 {
#include "WalletManager.h"
#include "WalletListenerImpl.h"
+#include "utils/config.h"
#include "config.h"
#include "constants.h"
#include "model/CoinsModel.h"
#include "utils/ScopeGuard.h"
+#include "utils/RestoreHeightLookup.h"
+#include "utils/Utils.h"
#include "wallet/wallet2.h"
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 ####################
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);
}
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;
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() {