From: gg Date: Mon, 19 Jan 2026 22:07:39 +0000 (-0500) Subject: Feat: Implement 'Sync Unconfirmed' (Smart Sync) to strictly scan blocks needed for... X-Git-Url: https://git.nutra.tk/v1?a=commitdiff_plain;h=b544cdc713a1c185ec4e0f713d9a03fde8d47bd9;p=gamesguru%2Ffeather.git Feat: Implement 'Sync Unconfirmed' (Smart Sync) to strictly scan blocks needed for unlock --- diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index e645422b..d7bf5dc1 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -253,6 +253,9 @@ void MainWindow::initStatusBar() { QAction *syncRangeAction = new QAction(tr("Sync Date Range..."), this); m_statusLabelStatus->addAction(syncRangeAction); + QAction *scanToTipAction = new QAction(tr("Sync Unconfirmed"), this); + m_statusLabelStatus->addAction(scanToTipAction); + QAction *fullSyncAction = new QAction(tr("Full Sync"), this); m_statusLabelStatus->addAction(fullSyncAction); @@ -296,6 +299,19 @@ void MainWindow::initStatusBar() { } }); + connect(scanToTipAction, &QAction::triggered, this, [this](){ + if (!m_wallet) return; + + QString msg = tr("Sync Unconfirmed (Smart Sync) will scan only the specific blocks required to unlock your pending funds (e.g. 10 confirmations).\n\n" + "This minimizes data usage by pausing immediately after verification.\n\n" + "Continue?"); + + if (QMessageBox::question(this, tr("Sync Unconfirmed"), msg) == QMessageBox::Yes) { + m_wallet->startSmartSync(); + this->setStatusText(tr("Scanning to tip...")); + } + }); + connect(fullSyncAction, &QAction::triggered, this, [this](){ if (m_wallet) { QString estBlocks = "Unknown (waiting for node)"; diff --git a/src/dialog/TxImportDialog.cpp b/src/dialog/TxImportDialog.cpp index c01ea267..842337bf 100644 --- a/src/dialog/TxImportDialog.cpp +++ b/src/dialog/TxImportDialog.cpp @@ -9,6 +9,13 @@ #include "utils/NetworkManager.h" #include "utils/nodes.h" +#include +#include +#include +#include + + + TxImportDialog::TxImportDialog(QWidget *parent, Wallet *wallet, Nodes *nodes) : WindowModalDialog(parent) , ui(new Ui::TxImportDialog) @@ -31,11 +38,13 @@ TxImportDialog::TxImportDialog(QWidget *parent, Wallet *wallet, Nodes *nodes) void TxImportDialog::onImport() { if (m_wallet->connectionStatus() == Wallet::ConnectionStatus_Disconnected) { m_nodes->connectToNode(); + m_wallet->setScanMempoolWhenPaused(true); this->updateStatus(Wallet::ConnectionStatus_Connecting); return; } - QString txid = ui->line_txid->text(); + QString txid = ui->line_txid->text().trimmed(); + if (txid.isEmpty()) return; if (m_wallet->haveTransaction(txid)) { Utils::showWarning(this, "Transaction already exists in wallet", "If you can't find it in your history, " @@ -43,16 +52,79 @@ void TxImportDialog::onImport() { return; } - if (m_wallet->importTransaction(txid)) { - if (!m_wallet->haveTransaction(txid)) { - Utils::showError(this, "Unable to import transaction", "This transaction does not belong to the wallet"); - return; + // Async Import: Fetch height from daemon, then Smart Sync to it. + ui->btn_import->setEnabled(false); + ui->btn_import->setText("Checking..."); + + QNetworkAccessManager* nam = getNetwork(); // Use global network manager + QString url = m_nodes->connection().toURL() + "/get_transactions"; + + QJsonObject req; + req["txs_hashes"] = QJsonArray({txid}); + + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QNetworkReply* reply = nam->post(request, QJsonDocument(req).toJson()); + + connect(reply, &QNetworkReply::finished, this, [this, reply, txid]() { + reply->deleteLater(); + ui->btn_import->setEnabled(true); + ui->btn_import->setText("Import"); + + if (reply->error() != QNetworkReply::NoError) { + Utils::showError(this, "Connection error", reply->errorString()); + return; } - Utils::showInfo(this, "Transaction imported successfully", ""); - } else { - Utils::showError(this, "Failed to import transaction", ""); - } - m_wallet->refreshModels(); + + QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object(); + QJsonObject error = json.value("error").toObject(); + if (!error.isEmpty()) { + Utils::showError(this, "Node error", error.value("message").toString()); + return; + } + + QJsonArray txs = json.value("txs").toArray(); + bool found = false; + + for (const auto &val : txs) { + QJsonObject tx = val.toObject(); + if (tx.value("tx_hash").toString() == txid) { + found = true; + if (tx.value("in_pool").toBool()) { + Utils::showInfo(this, "Transaction is in mempool", "Feather will detect it automatically in a moment."); + this->accept(); + return; + } + + quint64 height = tx.value("block_height").toVariant().toULongLong(); + if (height > 0) { + // Check if wallet is far behind (fresh restore?) + quint64 currentHeight = m_wallet->blockChainHeight(); + + if (height > currentHeight + 100000) { + // Jump ahead to avoid full scan + quint64 restoreHeight = (height > 20000) ? height - 20000 : 0; + m_wallet->setWalletCreationHeight(restoreHeight); + m_wallet->rescanBlockchainAsync(); + Utils::showInfo(this, "Optimizing Sync", "Jumped to block " + QString::number(restoreHeight) + " to find transaction."); + } + + m_wallet->startSmartSync(height + 10); + Utils::showInfo(this, "Import started", "Scanning block " + QString::number(height) + " for transaction..."); + this->accept(); + return; + } + } + } + + if (!found) { + Utils::showError(this, "Transaction not found on node", "The connected node does not know this transaction."); + } else { + // Found but failed to get height? Fallback. + Utils::showError(this, "Failed to determine block height", "Could not read block height from node response."); + } + }); } void TxImportDialog::updateStatus(int status) { diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index d3b00d1b..d30a6d4d 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -3,6 +3,8 @@ #include "Wallet.h" + + #include #include #include @@ -607,7 +609,10 @@ void Wallet::startRefreshThread() uint64_t blocks_fetched = 0; bool received_money = false; - m_wallet2->refresh(m_wallet2->is_trusted_daemon(), 0, blocks_fetched, received_money, true, true, max_blocks); + // Ensure we respect the wallet creation height (restore height) if it's set higher than current + uint64_t startHeight = std::max((uint64_t)walletHeight, m_wallet2->get_refresh_from_block_height()); + + m_wallet2->refresh(m_wallet2->is_trusted_daemon(), startHeight, blocks_fetched, received_money, true, true, max_blocks); if (m_walletImpl->blockChainHeight() >= m_stopHeight) { m_rangeSyncActive = false; @@ -748,6 +753,91 @@ void Wallet::skipToTip() { emit syncStatus(target, target, true); } +quint64 Wallet::getUnlockTargetHeight() const { + if (!m_wallet2) return 0; + + uint64_t current = blockChainHeight(); + uint64_t target = 0; + + // Check incoming transfers (last 1000 blocks) + uint64_t min_height = (current > 1000) ? current - 1000 : 0; + uint64_t max_height = (uint64_t)-1; + + std::list> in_payments; + m_wallet2->get_payments(in_payments, min_height, max_height); + + for (const auto &p : in_payments) { + // Standard unlock time is block_height + 10 + uint64_t unlock_height = p.second.m_block_height + 10; + // Explicit unlock_time override + if (p.second.m_unlock_time > 0) { + unlock_height = p.second.m_unlock_time; + } + + if (unlock_height > current) { + target = std::max(target, unlock_height); + } + } + + // Check outgoing transfers (change) + std::list> out_payments; + m_wallet2->get_payments_out(out_payments, min_height, max_height); + + for (const auto &p : out_payments) { + // Change is locked for 10 blocks + uint64_t unlock_height = p.second.m_block_height + 10; + if (unlock_height > current) { + target = std::max(target, unlock_height); + } + } + + return target; +} + +void Wallet::startSmartSync(quint64 requestedTarget) { + if (!m_wallet2) return; + + uint64_t tip = m_daemonBlockChainTargetHeight; + if (tip == 0) { + qWarning() << "Cannot start smart sync: Network target unknown. Connect first."; + return; + } + + uint64_t current = blockChainHeight(); + uint64_t target = tip; + uint64_t unlockTarget = getUnlockTargetHeight(); + + // "Smart Sync": Only scan what is needed to unlock funds + if (requestedTarget > 0) { + target = std::min((uint64_t)requestedTarget, tip); + qInfo() << "Smart Sync: Scanning to requested target:" << target; + } else if (unlockTarget > current) { + // If we have locked funds, scan to their unlock height (clamped to tip) + target = std::min(unlockTarget, tip); + qInfo() << "Smart Sync: Scanning to unlock target:" << target; + } else { + // No locked funds. + if (tip > current) { + // Minimal connectivity check + target = std::min(current + 10, tip); + qInfo() << "Smart Sync: No locked funds. Scanning small buffer to:" << target; + } else { + qInfo() << "Smart Sync: Already at tip."; + return; + } + } + + QMutexLocker locker(&m_asyncMutex); + m_stopHeight = target; + m_rangeSyncActive = true; + m_pauseAfterSync = true; + m_lastSyncTime = QDateTime::currentDateTime(); + + setConnectionStatus(ConnectionStatus_Synchronizing); + startRefresh(true); + emit syncStatus(target, target, true); +} + void Wallet::syncDateRange(const QDate &start, const QDate &end) { if (!m_wallet2) return; @@ -810,7 +900,15 @@ 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(); + + if (m_pauseAfterSync) { + m_pauseAfterSync = false; + // We reached the tip via scan. Just go back to paused/idle. + setSyncPaused(true); + } else { + // Normal date range sync behavior: skip the rest + this->skipToTip(); + } return; } @@ -848,6 +946,8 @@ bool Wallet::importTransaction(const QString &txid) { return false; } + + void Wallet::onNewBlock(uint64_t walletHeight) { if (m_syncPaused) { return; @@ -895,6 +995,13 @@ void Wallet::onRefreshed(bool success, const QString &message) { } } +void Wallet::rescanBlockchainAsync() { + m_wallet2->rescan_blockchain(); + // After rescan, the wallet's local height is reset to the refresh-from height. + // We trigger a refresh to update the UI state. + this->refresh(); +} + void Wallet::refreshModels() { m_history->refresh(); m_coins->refresh(); diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index 19d4e9d3..1034393c 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -240,11 +240,15 @@ public: quint64 daemonBlockChainTargetHeight() const; void syncStatusUpdated(quint64 height, quint64 target); + quint64 getUnlockTargetHeight() const; Q_INVOKABLE void skipToTip(); + Q_INVOKABLE void startSmartSync(quint64 target = 0); Q_INVOKABLE void syncDateRange(const QDate &start, const QDate &end); void fullSync(); // Rescans from wallet creation height, not genesis block + Q_INVOKABLE void rescanBlockchainAsync(); + bool importTransaction(const QString &txid); void refreshModels(); @@ -550,7 +554,8 @@ private: std::atomic m_stopHeight{0}; std::atomic m_rangeSyncActive{false}; std::atomic m_syncPaused{false}; - std::atomic m_lastRefreshTime{0}; + std::atomic m_lastRefreshTime{0}; + std::atomic m_pauseAfterSync{false}; std::atomic m_refreshThreadStarted{false}; std::atomic m_scanMempoolWhenPaused{false}; };