]> Nutra Git (v1) - gamesguru/feather.git/commitdiff
Feat: Implement 'Sync Unconfirmed' (Smart Sync) to strictly scan blocks needed for...
authorgg <chown_tee@proton.me>
Mon, 19 Jan 2026 22:07:39 +0000 (17:07 -0500)
committergg <chown_tee@proton.me>
Tue, 20 Jan 2026 01:34:42 +0000 (20:34 -0500)
src/MainWindow.cpp
src/dialog/TxImportDialog.cpp
src/libwalletqt/Wallet.cpp
src/libwalletqt/Wallet.h

index e645422b9cc05d2ad301fffbf75c9fc8ba529ac7..d7bf5dc17c86ed92cbc6b74937707c83a9de1674 100644 (file)
@@ -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)";
index c01ea267c4bc4f53b49b23cc868f94bb29d639aa..842337bfedea87e7d113c4d21e02a4a82fa229b2 100644 (file)
@@ -9,6 +9,13 @@
 #include "utils/NetworkManager.h"
 #include "utils/nodes.h"
 
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QNetworkReply>
+
+
+
 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) {
index d3b00d1b5059a05923f4256b74fc7a22dbcf513e..d30a6d4d2aeeaecd3a8f40ba828f8793703e66f9 100644 (file)
@@ -3,6 +3,8 @@
 
 #include "Wallet.h"
 
+
+
 #include <chrono>
 #include <thread>
 #include <tuple>
@@ -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<std::pair<crypto::hash, tools::wallet2::payment_details>> 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<std::pair<crypto::hash, tools::wallet2::confirmed_transfer_details>> 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();
index 19d4e9d3bbef6d53e19cfae4198e54619b9a61fe..1034393c9b0d0b715201d689e01f82de945f7e63 100644 (file)
@@ -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<quint64> m_stopHeight{0};
     std::atomic<bool> m_rangeSyncActive{false};
     std::atomic<bool> m_syncPaused{false};
-    std::atomic<int64_t> m_lastRefreshTime{0};
+    std::atomic<bool> m_lastRefreshTime{0};
+    std::atomic<bool> m_pauseAfterSync{false};
     std::atomic<bool> m_refreshThreadStarted{false};
     std::atomic<bool> m_scanMempoolWhenPaused{false};
 };