case copyField::JSON: {
QJsonObject obj;
obj.insert("txid", tx.hash);
- obj.insert("amount", static_cast<double>(tx.amount));
- obj.insert("fee", static_cast<double>(tx.fee));
- obj.insert("height", static_cast<double>(tx.blockHeight));
+ obj.insert("amount", QString::number(tx.amount));
+ obj.insert("fee", QString::number(tx.fee));
+ obj.insert("height", static_cast<qint64>(tx.blockHeight));
obj.insert("timestamp", tx.timestamp.toSecsSinceEpoch());
obj.insert("direction", tx.direction == TransactionRow::Direction_In ? "in" : "out");
obj.insert("payment_id", tx.paymentId);
obj.insert("description", tx.description);
- obj.insert("confirmations", static_cast<double>(tx.confirmations));
+ obj.insert("confirmations", static_cast<qint64>(tx.confirmations));
obj.insert("failed", tx.failed);
obj.insert("pending", tx.pending);
obj.insert("coinbase", tx.coinbase);
connect(m_walletUnlockWidget, &WalletUnlockWidget::closeWallet, this, &MainWindow::close);
connect(m_walletUnlockWidget, &WalletUnlockWidget::unlockWallet, this, &MainWindow::unlockWallet);
- ui->tabWidget->setCurrentIndex(0);
ui->tabWidget->setCurrentIndex(0);
ui->stackedWidget->setCurrentIndex(0);
}
void SendWidget::sendClicked() {
- if (conf()->get(Config::syncPaused).toBool()) {
+ bool syncPaused = conf()->get(Config::syncPaused).toBool();
+
+ if (syncPaused) {
QMessageBox msgBox(this);
msgBox.setIcon(QMessageBox::Warning);
msgBox.setWindowTitle("Are you sure? Create transaction?");
}
}
- if (!m_wallet->isConnected() && !conf()->get(Config::syncPaused).toBool()) {
+ if (!m_wallet->isConnected() && !syncPaused) {
Utils::showError(this, "Unable to create transaction", "Wallet is not connected to a node.",
{"Wait for the wallet to automatically connect to a node.", "Go to File -> Settings -> Network -> Node to manually connect to a node."},
"nodes");
return;
}
- if (!m_wallet->isSynchronized() && !conf()->get(Config::syncPaused).toBool()) {
+ if (!m_wallet->isSynchronized() && !syncPaused) {
Utils::showError(this, "Unable to create transaction", "Wallet is not synchronized", {"Wait for wallet synchronization to complete"}, "synchronization");
return;
}
}
m_closing = true;
-
- // 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()";
+ // Force save all wallets before attempting to close
+ // This ensures that even if the cleanup thread hangs and we _Exit(1), data is saved.
+ for (const auto &window : m_windows) {
+ if (window->m_wallet) {
+ window->m_wallet->store();
+ }
}
// Close all windows first to ensure they cancel their tasks/connections
window->close();
}
+ // 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();
#include <QDateEdit>
#include <QLabel>
#include <QDialogButtonBox>
+#include <QSignalBlocker>
#include "utils/Utils.h"
#include "utils/RestoreHeightLookup.h"
}
void SyncRangeDialog::updateFromDate() {
+ const QSignalBlocker blocker(m_fromDateEdit);
m_fromDateEdit->setDate(m_toDateEdit->date().addDays(-m_daysSpinBox->value()));
updateInfo();
}
void SyncRangeDialog::updateToDate() {
+ const QSignalBlocker blocker(m_toDateEdit);
m_toDateEdit->setDate(m_fromDateEdit->date().addDays(m_daysSpinBox->value()));
updateInfo();
}
QString txid = ui->line_txid->text().trimmed();
if (txid.isEmpty()) return;
+ static const QRegularExpression hexMatcher("^[0-9a-fA-F]{64}$");
+ if (!hexMatcher.match(txid).hasMatch()) {
+ Utils::showError(this, "Invalid TXID", "Transaction ID must be a 64-character hexadecimal string.");
+ return;
+ }
+
if (m_wallet->haveTransaction(txid)) {
Utils::showWarning(this, "Transaction already exists in wallet", "If you can't find it in your history, "
"check if it belongs to a different account (Wallet -> Account)");
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";
-
+ QNetworkAccessManager* nam = getNetwork(url); // Use global network manager
+
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;
this->accept();
return;
}
-
+
quint64 height = tx.value("block_height").toVariant().toULongLong();
if (height > 0) {
+ if (height > std::numeric_limits<quint64>::max() - 10) {
+ Utils::showError(this, "Invalid Block Height", "Block height is too large.");
+ return;
+ }
+
+ // Validate against daemon height
+ quint64 daemonHeight = m_wallet->daemonBlockChainHeight();
+ if (daemonHeight > 0 && height > daemonHeight + 100) {
+ Utils::showError(this, "Invalid Block Height",
+ QString("The node returned a block height significantly in the future (%1). Daemon height: %2.").arg(height).arg(daemonHeight));
+ return;
+ }
+
// Check if wallet is far behind (fresh restore?)
quint64 currentHeight = m_wallet->blockChainHeight();
, m_coins(new Coins(this, wallet->getWallet(), this))
, m_storeTimer(new QTimer(this))
, m_lastRefreshTime(std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count())
+ , m_lastSyncTime(0)
{
m_walletListener = new WalletListenerImpl(this);
m_walletImpl->setListener(m_walletListener);
if (!lastSyncStr.isEmpty()) {
qint64 lastSync = lastSyncStr.toLongLong();
if (lastSync > 0) {
- m_lastSyncTime = QDateTime::fromSecsSinceEpoch(lastSync);
+ m_lastSyncTime = lastSync * 1000;
}
}
}
safeAddress.prepend("http://");
}
}
- qCritical() << "Refresher: Initializing wallet with daemon address:" << safeAddress;
+ qInfo() << "Refresher: Initializing wallet with daemon address:" << safeAddress;
qDebug() << "InitAsync: connecting to" << safeAddress;
m_wallet2->set_offline(false);
success = m_walletImpl->init(safeAddress.toStdString(), upperTransactionLimit, m_daemonUsername.toStdString(), m_daemonPassword.toStdString(), m_useSSL, false, proxyAddress.toStdString());
}
if (success) {
- m_lastSyncTime = QDateTime::currentDateTime();
+ m_lastSyncTime = QDateTime::currentDateTime().toMSecsSinceEpoch();
}
}
}
QDateTime Wallet::lastSyncTime() const {
- return m_lastSyncTime;
+ if (m_lastSyncTime == 0)
+ return QDateTime();
+ return QDateTime::fromMSecsSinceEpoch(m_lastSyncTime);
}
void Wallet::setRefreshInterval(int seconds) {
m_stopHeight = target;
m_rangeSyncActive = true;
m_wallet2->set_refresh_from_block_height(target);
- m_lastSyncTime = QDateTime::currentDateTime();
+ m_lastSyncTime = QDateTime::currentDateTime().toMSecsSinceEpoch();
setConnectionStatus(ConnectionStatus_Synchronized);
startRefresh(true);
m_stopHeight = target;
m_rangeSyncActive = true;
m_pauseAfterSync = true;
- m_lastSyncTime = QDateTime::currentDateTime();
+ m_lastSyncTime = QDateTime::currentDateTime().toMSecsSinceEpoch();
setConnectionStatus(ConnectionStatus_Synchronizing);
startRefresh(true);
}
void Wallet::scanMempool() {
- QMutexLocker locker(&m_asyncMutex);
- try {
- std::vector<std::tuple<cryptonote::transaction, crypto::hash, bool>> process_txs;
- m_wallet2->update_pool_state(process_txs, false, false);
- // Refresh models so the UI picks up the new transaction(s)
- // We invoke this on the main thread to ensure signals (beginResetModel) are processed synchronously
- // with the data update, preventing race conditions or ignored updates in the view.
- QMetaObject::invokeMethod(this, [this]{
- if (m_history) m_history->refresh();
- if (m_coins) m_coins->refresh();
- if (m_subaddress) m_subaddress->refresh();
- }, Qt::QueuedConnection);
-
- emit updated();
- } catch (const std::exception &e) {
- qWarning() << "Failed to scan mempool:" << e.what();
+ {
+ QMutexLocker locker(&m_asyncMutex);
+ try {
+ std::vector<std::tuple<cryptonote::transaction, crypto::hash, bool>> process_txs;
+ m_wallet2->update_pool_state(process_txs, false, false);
+ // Refresh models so the UI picks up the new transaction(s)
+ // We invoke this on the main thread to ensure signals (beginResetModel) are processed synchronously
+ // with the data update, preventing race conditions or ignored updates in the view.
+ QMetaObject::invokeMethod(this, [this]{
+ if (m_history) m_history->refresh();
+ if (m_coins) m_coins->refresh();
+ if (m_subaddress) m_subaddress->refresh();
+ }, Qt::QueuedConnection);
+
+ } catch (const std::exception &e) {
+ qWarning() << "Failed to scan mempool:" << e.what();
+ }
}
+ emit updated();
}
Wallet::~Wallet()
AddressBook *m_addressBook;
AddressBookModel *m_addressBookModel;
- quint64 m_daemonBlockChainHeight;
- quint64 m_daemonBlockChainTargetHeight;
- QDateTime m_lastSyncTime;
+ std::atomic<quint64> m_daemonBlockChainHeight;
+ std::atomic<quint64> m_daemonBlockChainTargetHeight;
+ std::atomic<qint64> m_lastSyncTime;
ConnectionStatus m_connectionStatus;
std::atomic<quint64> m_stopHeight{0};
std::atomic<bool> m_rangeSyncActive{false};
std::atomic<bool> m_syncPaused{false};
- std::atomic<bool> m_lastRefreshTime{0};
+ std::atomic<quint64> m_lastRefreshTime{0};
std::atomic<bool> m_pauseAfterSync{false};
std::atomic<bool> m_refreshThreadStarted{false};
std::atomic<bool> m_scanMempoolWhenPaused{false};
conf()->set(Config::restartRequired, false);
- if (!quiet && !conf()->get(Config::disableLogging).toBool()) {
+ if (!quiet && !conf()->get(Config::disableLoggingStdout).toBool()) {
QList<QPair<QString, QString>> info;
info.emplace_back("Feather", FEATHER_VERSION);
info.emplace_back("Monero", MONERO_VERSION);
quint64 minutes = blocks * 2;
quint64 days = minutes / (60 * 24);
+ quint64 hours = minutes / 60;
+
QString timeStr;
if (days > 0) {
- timeStr = QObject::tr("~%1 days").arg(days);
+ timeStr = QObject::tr("~%1 day%2").arg(days).arg(days == 1 ? "" : "s");
} else if (minutes >= 60) {
- timeStr = QObject::tr("~%1 hours").arg(minutes / 60);
+ timeStr = QObject::tr("~%1 hour%2").arg(hours).arg(hours == 1 ? "" : "s");
} else {
timeStr = QObject::tr("< 1 hour");
}