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);
}
});
+ 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)";
#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)
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, "
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) {
#include "Wallet.h"
+
+
#include <chrono>
#include <thread>
#include <tuple>
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;
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;
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;
}
return false;
}
+
+
void Wallet::onNewBlock(uint64_t walletHeight) {
if (m_syncPaused) {
return;
}
}
+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();
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();
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};
};