From: tobtoht Date: Sun, 28 Apr 2024 19:26:54 +0000 (+0200) Subject: manual input selection, subtract fee from amount X-Git-Url: https://git.nutra.tk/v1?a=commitdiff_plain;h=deb9d7ff634363879f9885333b3e515582270768;p=gamesguru%2Ffeather.git manual input selection, subtract fee from amount --- diff --git a/contrib/guix/guix-build b/contrib/guix/guix-build index 26926974..d6ec1535 100755 --- a/contrib/guix/guix-build +++ b/contrib/guix/guix-build @@ -350,6 +350,7 @@ EOF --container \ --pure \ --no-cwd \ + --cores="$JOBS" \ ${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"} \ -- echo "$HOST" diff --git a/contrib/guix/libexec/build.sh b/contrib/guix/libexec/build.sh index b3a47474..b0473ec7 100755 --- a/contrib/guix/libexec/build.sh +++ b/contrib/guix/libexec/build.sh @@ -222,6 +222,8 @@ mkdir -p "$OUTDIR" # Log the depends build ids make -C contrib/depends --no-print-directory HOST="$HOST" print-final_build_id_long | tr ':' '\n' > ${LOGDIR}/depends-hashes.txt +export CMAKE_BUILD_PARALLEL_LEVEL=$JOBS + # Build the depends tree, overriding variables that assume multilib gcc make -C contrib/depends --jobs="$JOBS" HOST="$HOST" \ ${V:+V=1} \ diff --git a/monero b/monero index 85ea9458..376fb747 160000 --- a/monero +++ b/monero @@ -1 +1 @@ -Subproject commit 85ea9458c8a27814729b24c3b932f60ff331903e +Subproject commit 376fb747ea262cf6cd773cc169bbd3e84670d733 diff --git a/src/CoinsWidget.cpp b/src/CoinsWidget.cpp index 84715d27..f592ef12 100644 --- a/src/CoinsWidget.cpp +++ b/src/CoinsWidget.cpp @@ -245,7 +245,16 @@ void CoinsWidget::onSweepOutputs() { #endif } - m_wallet->sweepOutputs(keyImages, dialog.address(), dialog.churn(), dialog.outputs()); + QString address = dialog.address(); + bool churn = dialog.churn(); + int outputs = dialog.outputs(); + + QtFuture::connect(m_wallet, &Wallet::preTransactionChecksComplete) + .then([this, keyImages, address, churn, outputs](int feeLevel){ + m_wallet->sweepOutputs(keyImages, address, churn, outputs, feeLevel); + }); + + m_wallet->preTransactionChecks(dialog.feeLevel()); } void CoinsWidget::copy(copyField field) { diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 14b7db8a..6c1877c4 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -19,6 +19,7 @@ #include "dialog/TxConfDialog.h" #include "dialog/TxImportDialog.h" #include "dialog/TxInfoDialog.h" +#include "dialog/TxPoolViewerDialog.h" #include "dialog/ViewOnlyDialog.h" #include "dialog/WalletInfoDialog.h" #include "dialog/WalletCacheDebugDialog.h" @@ -88,9 +89,13 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa connect(m_windowManager, &WindowManager::websocketStatusChanged, this, &MainWindow::onWebsocketStatusChanged); this->onWebsocketStatusChanged(!conf()->get(Config::disableWebsocket).toBool()); - connect(m_windowManager, &WindowManager::proxySettingsChanged, this, &MainWindow::onProxySettingsChanged); + connect(m_windowManager, &WindowManager::proxySettingsChanged, [this]{ + this->onProxySettingsChanged(); + }); connect(m_windowManager, &WindowManager::updateBalance, m_wallet, &Wallet::updateBalance); connect(m_windowManager, &WindowManager::offlineMode, this, &MainWindow::onOfflineMode); + connect(m_windowManager, &WindowManager::manualFeeSelectionEnabled, this, &MainWindow::onManualFeeSelectionEnabled); + connect(m_windowManager, &WindowManager::subtractFeeFromAmountEnabled, this, &MainWindow::onSubtractFeeFromAmountEnabled); connect(torManager(), &TorManager::connectionStateChanged, this, &MainWindow::onTorConnectionStateChanged); this->onTorConnectionStateChanged(torManager()->torConnected); @@ -178,7 +183,7 @@ void MainWindow::initStatusBar() { m_statusBtnProxySettings = new StatusBarButton(icons()->icon("tor_logo_disabled.png"), "Proxy settings", this); connect(m_statusBtnProxySettings, &StatusBarButton::clicked, this, &MainWindow::menuProxySettingsClicked); this->statusBar()->addPermanentWidget(m_statusBtnProxySettings); - this->onProxySettingsChanged(); + this->onProxySettingsChanged(false); m_statusBtnHwDevice = new StatusBarButton(this->hardwareDevicePairedIcon(), this->getHardwareDevice(), this); connect(m_statusBtnHwDevice, &StatusBarButton::clicked, this, &MainWindow::menuHwDeviceClicked); @@ -302,6 +307,7 @@ void MainWindow::initMenu() { connect(ui->actionRefresh_tabs, &QAction::triggered, [this]{m_wallet->refreshModels();}); connect(ui->actionRescan_spent, &QAction::triggered, this, &MainWindow::rescanSpent); connect(ui->actionWallet_cache_debug, &QAction::triggered, this, &MainWindow::showWalletCacheDebugDialog); + connect(ui->actionTxPoolViewer, &QAction::triggered, this, &MainWindow::showTxPoolViewerDialog); // [Wallet] -> [History] connect(ui->actionExport_CSV, &QAction::triggered, this, &MainWindow::onExportHistoryCSV); @@ -458,6 +464,7 @@ void MainWindow::initWalletContext() { connect(m_wallet, &Wallet::initiateTransaction, this, &MainWindow::onInitiateTransaction); connect(m_wallet, &Wallet::keysCorrupted, this, &MainWindow::onKeysCorrupted); connect(m_wallet, &Wallet::selectedInputsChanged, this, &MainWindow::onSelectedInputsChanged); + connect(m_wallet, &Wallet::txPoolBacklog, this, &MainWindow::onTxPoolBacklog); // Wallet connect(m_wallet, &Wallet::connectionStatusChanged, [this](int status){ @@ -644,8 +651,10 @@ void MainWindow::onWebsocketStatusChanged(bool enabled) { m_sendWidget->setWebsocketEnabled(enabled); } -void MainWindow::onProxySettingsChanged() { - m_nodes->connectToNode(); +void MainWindow::onProxySettingsChanged(bool connect) { + if (connect) { + m_nodes->connectToNode(); + } int proxy = conf()->get(Config::proxy).toInt(); @@ -682,6 +691,14 @@ void MainWindow::onOfflineMode(bool offline) { m_statusBtnProxySettings->setVisible(!offline); } +void MainWindow::onManualFeeSelectionEnabled(bool enabled) { + m_sendWidget->setManualFeeSelectionEnabled(enabled); +} + +void MainWindow::onSubtractFeeFromAmountEnabled(bool enabled) { + m_sendWidget->setSubtractFeeFromAmountEnabled(enabled); +} + void MainWindow::onMultiBroadcast(const QMap &txHexMap) { QMapIterator i(txHexMap); while (i.hasNext()) { @@ -1309,6 +1326,14 @@ void MainWindow::showWalletCacheDebugDialog() { dialog.exec(); } +void MainWindow::showTxPoolViewerDialog() { + if (!m_txPoolViewerDialog) { + m_txPoolViewerDialog = new TxPoolViewerDialog{this, m_wallet}; + } + + m_txPoolViewerDialog->show(); +} + void MainWindow::showAccountSwitcherDialog() { m_accountSwitcherDialog->show(); m_accountSwitcherDialog->update(); @@ -1624,6 +1649,36 @@ void MainWindow::onSelectedInputsChanged(const QStringList &selectedInputs) { } } +void MainWindow::onTxPoolBacklog(const QVector &backlog, quint64 originalFeeLevel, quint64 automaticFeeLevel) { + bool automatic = (originalFeeLevel == 0); + + if (automaticFeeLevel == 0) { + qWarning() << "Automatic fee level wasn't adjusted"; + automaticFeeLevel = 2; + } + + quint64 feeLevel = automatic ? automaticFeeLevel : originalFeeLevel; + + for (int i = 0; i < backlog.size(); i++) { + qDebug() << QString("Fee level: %1, backlog: %2").arg(QString::number(i), QString::number(backlog[i])); + } + + if (automatic) { + if (backlog.size() >= 1 && backlog[1] >= 2) { + auto button = QMessageBox::question(this, "Transaction Pool Backlog", + QString("There is a backlog of %1 blocks (≈ %2 minutes) in the transaction pool " + "at the maximum automatic fee level.\n\n" + "Do you want to increase the fee for this transaction?") + .arg(QString::number(backlog[1]), QString::number(backlog[1] * 2))); + if (button == QMessageBox::Yes) { + feeLevel = 3; + } + } + } + + m_wallet->confirmPreTransactionChecks(feeLevel); +} + void MainWindow::onExportHistoryCSV() { QString fn = QFileDialog::getSaveFileName(this, "Save CSV file", QDir::homePath(), "CSV (*.csv)"); if (fn.isEmpty()) diff --git a/src/MainWindow.h b/src/MainWindow.h index 4b2ceb17..f5786b0b 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -19,6 +19,7 @@ #include "dialog/KeysDialog.h" #include "dialog/AboutDialog.h" #include "dialog/SplashDialog.h" +#include "dialog/TxPoolViewerDialog.h" #include "libwalletqt/Wallet.h" #include "model/SubaddressModel.h" #include "model/SubaddressProxyModel.h" @@ -124,6 +125,7 @@ private slots: void onInitiateTransaction(); void onKeysCorrupted(); void onSelectedInputsChanged(const QStringList &selectedInputs); + void onTxPoolBacklog(const QVector &backlog, quint64 originalFeeLevel, quint64 automaticFeeLevel); // libwalletqt void onBalanceUpdated(quint64 balance, quint64 spendable); @@ -141,6 +143,7 @@ private slots: void showViewOnlyDialog(); void showKeyImageSyncWizard(); void showWalletCacheDebugDialog(); + void showTxPoolViewerDialog(); void showAccountSwitcherDialog(); void showAddressChecker(); void showURDialog(); @@ -162,8 +165,10 @@ private slots: void tryStoreWallet(); void onWebsocketStatusChanged(bool enabled); void showUpdateNotification(); - void onProxySettingsChanged(); + void onProxySettingsChanged(bool connect = true); void onOfflineMode(bool offline); + void onManualFeeSelectionEnabled(bool enabled); + void onSubtractFeeFromAmountEnabled(bool enabled); void onMultiBroadcast(const QMap &txHexMap); private: @@ -213,6 +218,7 @@ private: SplashDialog *m_splashDialog = nullptr; AccountSwitcherDialog *m_accountSwitcherDialog = nullptr; + TxPoolViewerDialog *m_txPoolViewerDialog = nullptr; WalletUnlockWidget *m_walletUnlockWidget = nullptr; ContactsWidget *m_contactsWidget = nullptr; diff --git a/src/MainWindow.ui b/src/MainWindow.ui index e9b390e5..7cde5195 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -469,7 +469,7 @@ 0 0 977 - 24 + 27 @@ -552,6 +552,7 @@ + @@ -933,6 +934,11 @@ PlaceholderBegin + + + Tx pool viewer + + diff --git a/src/SendWidget.cpp b/src/SendWidget.cpp index 89866007..6e4c6875 100644 --- a/src/SendWidget.cpp +++ b/src/SendWidget.cpp @@ -66,6 +66,9 @@ SendWidget::SendWidget(Wallet *wallet, QWidget *parent) ui->lineAddress->setNetType(constants::networkType); this->setupComboBox(); + + this->setManualFeeSelectionEnabled(conf()->get(Config::manualFeeTierSelection).toBool()); + this->setSubtractFeeFromAmountEnabled(conf()->get(Config::subtractFeeFromAmount).toBool()); } void SendWidget::currencyComboChanged(int index) { @@ -175,6 +178,8 @@ void SendWidget::sendClicked() { return; } + bool subtractFeeFromAmount = conf()->get(Config::subtractFeeFromAmount).toBool() && ui->check_subtractFeeFromAmount->isChecked(); + QString description = ui->lineDescription->text(); if (!outputs.empty()) { // multi destination transaction @@ -190,7 +195,13 @@ void SendWidget::sendClicked() { amounts.push_back(output.amount); } - m_wallet->createTransactionMultiDest(addresses, amounts, description); + QtFuture::connect(m_wallet, &Wallet::preTransactionChecksComplete) + .then([this, addresses, amounts, description, subtractFeeFromAmount](int feeLevel){ + m_wallet->createTransactionMultiDest(addresses, amounts, description, feeLevel, subtractFeeFromAmount); + }); + + m_wallet->preTransactionChecks(ui->combo_feePriority->currentIndex()); + return; } @@ -243,7 +254,12 @@ void SendWidget::sendClicked() { #endif } - m_wallet->createTransaction(recipient, amount, description, sendAll); + QtFuture::connect(m_wallet, &Wallet::preTransactionChecksComplete) + .then([this, recipient, amount, description, sendAll, subtractFeeFromAmount](int feeLevel){ + m_wallet->createTransaction(recipient, amount, description, sendAll, feeLevel, subtractFeeFromAmount); + }); + + m_wallet->preTransactionChecks(ui->combo_feePriority->currentIndex()); } void SendWidget::aliasClicked() { @@ -377,6 +393,15 @@ void SendWidget::setWebsocketEnabled(bool enabled) { } } +void SendWidget::setManualFeeSelectionEnabled(bool enabled) { + ui->label_feeTarget->setVisible(enabled); + ui->combo_feePriority->setVisible(enabled); +} + +void SendWidget::setSubtractFeeFromAmountEnabled(bool enabled) { + ui->check_subtractFeeFromAmount->setVisible(enabled); +} + void SendWidget::onDataPasted(const QString &data) { if (!data.isEmpty()) { QVariantMap uriData = m_wallet->parse_uri_to_object(data); diff --git a/src/SendWidget.h b/src/SendWidget.h index d4aace7b..fa7c78d1 100644 --- a/src/SendWidget.h +++ b/src/SendWidget.h @@ -40,6 +40,9 @@ public slots: void onPreferredFiatCurrencyChanged(); void setWebsocketEnabled(bool enabled); + void setManualFeeSelectionEnabled(bool enabled); + void setSubtractFeeFromAmountEnabled(bool enabled); + void disableSendButton(); void enableSendButton(); diff --git a/src/SendWidget.ui b/src/SendWidget.ui index 8ad083d5..59f1f695 100644 --- a/src/SendWidget.ui +++ b/src/SendWidget.ui @@ -7,7 +7,7 @@ 0 0 647 - 231 + 254 @@ -172,6 +172,13 @@ + + + + Subtract fee from amount + + + @@ -187,7 +194,14 @@ - + + + + Fee + + + + 6 @@ -228,6 +242,35 @@ + + + + + Automatic + + + + + Low + + + + + Normal + + + + + High + + + + + Highest + + + + diff --git a/src/SettingsDialog.cpp b/src/SettingsDialog.cpp index 28cee3ed..37e69e41 100644 --- a/src/SettingsDialog.cpp +++ b/src/SettingsDialog.cpp @@ -319,6 +319,28 @@ void Settings::setupTransactionsTab() { // Hide unimplemented settings ui->checkBox_alwaysOpenAdvancedTxDialog->hide(); ui->checkBox_requirePasswordToSpend->hide(); + + // [Manual fee-tier selection] + ui->checkBox_manualFeeTierSelection->setChecked(conf()->get(Config::manualFeeTierSelection).toBool()); + connect(ui->checkBox_manualFeeTierSelection, &QCheckBox::toggled, [this](bool toggled){ + if (toggled) { + auto result = QMessageBox::question(this, "Privacy warning", "Using a non-automatic fee makes your transactions stick out and harms your privacy.\n\nAre you sure you want to enable manual fee-tier selection?"); + if (result == QMessageBox::No) { + ui->checkBox_manualFeeTierSelection->setChecked(false); + return; + } + + } + + conf()->set(Config::manualFeeTierSelection, toggled); + emit manualFeeSelectionEnabled(toggled); + }); + + ui->checkBox_subtractFeeFromAmount->setChecked(conf()->get(Config::subtractFeeFromAmount).toBool()); + connect(ui->checkBox_subtractFeeFromAmount, &QCheckBox::toggled, [this](bool toggled){ + conf()->set(Config::subtractFeeFromAmount, toggled); + emit subtractFeeFromAmountEnabled(toggled); + }); } void Settings::setupPluginsTab() { diff --git a/src/SettingsDialog.h b/src/SettingsDialog.h index 10703e59..604dd9c5 100644 --- a/src/SettingsDialog.h +++ b/src/SettingsDialog.h @@ -45,6 +45,8 @@ signals: void updateBalance(); void offlineMode(bool offline); void pluginConfigured(const QString &id); + void manualFeeSelectionEnabled(bool enabled); + void subtractFeeFromAmountEnabled(bool enabled); public slots: // void checkboxExternalLinkWarn(); diff --git a/src/SettingsDialog.ui b/src/SettingsDialog.ui index 897e7c68..f27703c5 100644 --- a/src/SettingsDialog.ui +++ b/src/SettingsDialog.ui @@ -32,7 +32,7 @@ - 7 + 5 @@ -955,6 +955,20 @@ + + + + Manual fee-tier selection + + + + + + + Subtract fee from outputs + + + diff --git a/src/WindowManager.cpp b/src/WindowManager.cpp index 90b3e1f5..616cd6dd 100644 --- a/src/WindowManager.cpp +++ b/src/WindowManager.cpp @@ -176,6 +176,8 @@ void WindowManager::showSettings(Nodes *nodes, QWidget *parent, bool showProxyTa connect(&settings, &Settings::proxySettingsChanged, this, &WindowManager::onProxySettingsChanged); connect(&settings, &Settings::websocketStatusChanged, this, &WindowManager::onWebsocketStatusChanged); connect(&settings, &Settings::offlineMode, this, &WindowManager::offlineMode); + connect(&settings, &Settings::manualFeeSelectionEnabled, this, &WindowManager::manualFeeSelectionEnabled); + connect(&settings, &Settings::subtractFeeFromAmountEnabled, this, &WindowManager::subtractFeeFromAmountEnabled); connect(&settings, &Settings::hideUpdateNotifications, [this](bool hidden){ for (const auto &window : m_windows) { window->onHideUpdateNotifications(hidden); diff --git a/src/WindowManager.h b/src/WindowManager.h index eb4a8b42..92c9225d 100644 --- a/src/WindowManager.h +++ b/src/WindowManager.h @@ -50,6 +50,8 @@ signals: void preferredFiatCurrencyChanged(); void offlineMode(bool offline); void pluginConfigured(const QString &id); + void manualFeeSelectionEnabled(bool enabled); + void subtractFeeFromAmountEnabled(bool enabled); public slots: void onProxySettingsChanged(); diff --git a/src/dialog/OutputSweepDialog.cpp b/src/dialog/OutputSweepDialog.cpp index c0490e71..b9e4c00f 100644 --- a/src/dialog/OutputSweepDialog.cpp +++ b/src/dialog/OutputSweepDialog.cpp @@ -22,6 +22,7 @@ OutputSweepDialog::OutputSweepDialog(QWidget *parent, quint64 amount) m_address = ui->lineEdit_address->text(); m_churn = ui->checkBox_churn->isChecked(); m_outputs = ui->spinBox_numOutputs->value(); + m_feeLevel = ui->combo_feePriority->currentIndex(); }); connect(ui->spinBox_numOutputs, QOverload::of(&QSpinBox::valueChanged), [this](int value){ @@ -52,4 +53,8 @@ int OutputSweepDialog::outputs() const { return m_outputs; } +int OutputSweepDialog::feeLevel() const { + return m_feeLevel; +} + OutputSweepDialog::~OutputSweepDialog() = default; \ No newline at end of file diff --git a/src/dialog/OutputSweepDialog.h b/src/dialog/OutputSweepDialog.h index e11a37b7..a2744824 100644 --- a/src/dialog/OutputSweepDialog.h +++ b/src/dialog/OutputSweepDialog.h @@ -24,6 +24,7 @@ public: QString address(); bool churn() const; int outputs() const; + int feeLevel() const; private: QScopedPointer ui; @@ -31,8 +32,9 @@ private: uint64_t m_amount; QString m_address; - bool m_churn; - int m_outputs; + bool m_churn = false; + int m_outputs = 1; + int m_feeLevel = 0; }; diff --git a/src/dialog/OutputSweepDialog.ui b/src/dialog/OutputSweepDialog.ui index fc417c4d..5225aa14 100644 --- a/src/dialog/OutputSweepDialog.ui +++ b/src/dialog/OutputSweepDialog.ui @@ -7,7 +7,7 @@ 0 0 720 - 193 + 225 @@ -19,6 +19,9 @@ + + QFormLayout::ExpandingFieldsGrow + 0 @@ -49,6 +52,42 @@ + + + + Fee: + + + + + + + + Automatic + + + + + Low + + + + + Normal + + + + + High + + + + + Highest + + + + diff --git a/src/dialog/TxPoolViewerDialog.cpp b/src/dialog/TxPoolViewerDialog.cpp new file mode 100644 index 00000000..30f4e0dd --- /dev/null +++ b/src/dialog/TxPoolViewerDialog.cpp @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "TxPoolViewerDialog.h" +#include "ui_TxPoolViewerDialog.h" + +#include + +#include "utils/Utils.h" +#include "utils/ColorScheme.h" +#include "libwalletqt/WalletManager.h" + +TxPoolViewerDialog::TxPoolViewerDialog(QWidget *parent, Wallet *wallet) + : QDialog(parent) + , ui(new Ui::TxPoolViewerDialog) + , m_wallet(wallet) +{ + ui->setupUi(this); + + connect(ui->btn_refresh, &QPushButton::clicked, this, &TxPoolViewerDialog::refresh); + connect(m_wallet, &Wallet::poolStats, this, &TxPoolViewerDialog::onTxPoolBacklog); + + ui->tree_pool->sortByColumn(2, Qt::DescendingOrder); + + this->refresh(); +} + +void TxPoolViewerDialog::refresh() { + ui->btn_refresh->setEnabled(false); + m_wallet->getTxPoolStatsAsync(); +} + +class TxPoolSortItem : public QTreeWidgetItem { +public: + using QTreeWidgetItem::QTreeWidgetItem; + + bool operator<(const QTreeWidgetItem &other) const override { + int column = treeWidget()->sortColumn(); + + if (column == 2) { + return this->text(column).toInt() < other.text(column).toInt(); + } + + return this->text(column) < other.text(column); + } +}; + +void TxPoolViewerDialog::onTxPoolBacklog(const QVector &txPool, const QVector &baseFees, quint64 blockWeightLimit) { + ui->btn_refresh->setEnabled(true); + + if (baseFees.size() != 4) { + return; + } + + ui->tree_pool->clear(); + ui->tree_feeTiers->clear(); + + m_feeTierStats.clear(); + for (int i = 0; i < 4; i++) { + m_feeTierStats.push_back(FeeTierStats{}); + } + + ui->label_transactions->setText(QString::number(txPool.size())); + + uint64_t totalWeight = 0; + uint64_t totalFees = 0; + for (const auto &entry : txPool) { + totalWeight += entry.weight; + totalFees += entry.fee; + + auto* item = new TxPoolSortItem(); + item->setText(0, QString("%1 B").arg(QString::number(entry.weight))); + item->setTextAlignment(0, Qt::AlignRight); + + item->setText(1, QString("%1 XMR").arg(WalletManager::displayAmount(entry.fee))); + item->setTextAlignment(1, Qt::AlignRight); + + quint64 fee_per_byte = entry.fee / entry.weight; + item->setText(2, QString::number(entry.fee / entry.weight)); + item->setTextAlignment(2, Qt::AlignRight); + + if (fee_per_byte == baseFees[0]) { + item->setBackground(2, QBrush(ColorScheme::BLUE.asColor(true))); + } + if (fee_per_byte == baseFees[1]) { + item->setBackground(2, QBrush(ColorScheme::GREEN.asColor(true))); + } + if (fee_per_byte == baseFees[2]) { + item->setBackground(2, QBrush(ColorScheme::YELLOW.asColor(true))); + } + if (fee_per_byte == baseFees[3]) { + item->setBackground(2, QBrush(ColorScheme::RED.asColor(true))); + } + + if (fee_per_byte >= baseFees[3]) { + m_feeTierStats[3].weightFromTip += entry.weight; + } + if (fee_per_byte >= baseFees[2]) { + m_feeTierStats[2].weightFromTip += entry.weight; + } + if (fee_per_byte >= baseFees[1]) { + m_feeTierStats[1].weightFromTip += entry.weight; + } + if (fee_per_byte >= baseFees[0]) { + m_feeTierStats[0].weightFromTip += entry.weight; + } + + ui->tree_pool->addTopLevelItem(item); + } + + ui->tree_pool->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + ui->tree_pool->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + + ui->label_totalWeight->setText(Utils::formatBytes(totalWeight)); + ui->label_totalFees->setText(QString("%1 XMR").arg(WalletManager::displayAmount(totalFees))); + + quint64 fullRewardZone = blockWeightLimit >> 1; + ui->label_blockWeightLimit->setText(Utils::formatBytes(fullRewardZone)); + + for (int i = 0; i < 4; i++) { + QString tierName; + switch (i) { + case 0: + tierName = "Low"; + break; + case 1: + tierName = "Normal"; + break; + case 2: + tierName = "High"; + break; + case 3: + default: + tierName = "Highest "; + break; + } + + auto* item = new QTreeWidgetItem(); + item->setText(0, tierName); + + item->setText(1, QString::number(baseFees[i])); + item->setTextAlignment(1, Qt::AlignRight); + + item->setText(2, QString(" %1 blocks").arg(QString::number(m_feeTierStats[i].weightFromTip / fullRewardZone))); // approximation + item->setTextAlignment(2, Qt::AlignRight); + + item->setText(3, QString("%1 kB").arg(QString::number(m_feeTierStats[i].weightFromTip / 1000))); + item->setTextAlignment(3, Qt::AlignRight); + + ui->tree_feeTiers->addTopLevelItem(item); + } + + ui->tree_feeTiers->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + ui->tree_feeTiers->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + ui->tree_feeTiers->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + ui->tree_feeTiers->headerItem()->setTextAlignment(2, Qt::AlignRight); + ui->tree_feeTiers->headerItem()->setTextAlignment(3, Qt::AlignRight); +} + +TxPoolViewerDialog::~TxPoolViewerDialog() = default; diff --git a/src/dialog/TxPoolViewerDialog.h b/src/dialog/TxPoolViewerDialog.h new file mode 100644 index 00000000..bec5db35 --- /dev/null +++ b/src/dialog/TxPoolViewerDialog.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_TXPOOLVIEWERDIALOG_H +#define FEATHER_TXPOOLVIEWERDIALOG_H + +#include + +#include "components.h" +#include "libwalletqt/Wallet.h" + +namespace Ui { + class TxPoolViewerDialog; +} + +struct FeeTierStats { + quint64 transactions = 0; + quint64 totalWeight = 0; + quint64 weightFromTip = 0; +}; + +class TxPoolViewerDialog : public QDialog +{ +Q_OBJECT + +public: + explicit TxPoolViewerDialog(QWidget *parent, Wallet *wallet); + ~TxPoolViewerDialog() override; + +private: + void refresh(); + void onTxPoolBacklog(const QVector &txPool, const QVector &baseFees, quint64 blockWeightLimit); + + QVector m_feeTierStats; + QScopedPointer ui; + Wallet *m_wallet; +}; + + +#endif //FEATHER_TXPOOLVIEWERDIALOG_H diff --git a/src/dialog/TxPoolViewerDialog.ui b/src/dialog/TxPoolViewerDialog.ui new file mode 100644 index 00000000..9d3c35f8 --- /dev/null +++ b/src/dialog/TxPoolViewerDialog.ui @@ -0,0 +1,280 @@ + + + TxPoolViewerDialog + + + + 0 + 0 + 564 + 779 + + + + Tx Pool Viewer + + + + + + Stats + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Transactions: + + + + + + + Loading.. + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Total weight: + + + + + + + Loading.. + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Total fees: + + + + + + + Loading.. + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Full reward zone: + + + + + + + Loading.. + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Transactions + + + + + + false + + + true + + + + Weight + + + + + Fee + + + + + Fee / B + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Fee tiers + + + + + + + 0 + 0 + + + + false + + + + Tier + + + + + Fee / B + + + + + Backlog + + + + + Weight from tip + + + + + + + + + + + + + Refresh + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + + + buttonBox + accepted() + TxPoolViewerDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + TxPoolViewerDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/libwalletqt/PendingTransaction.cpp b/src/libwalletqt/PendingTransaction.cpp index 68767f51..85e751da 100644 --- a/src/libwalletqt/PendingTransaction.cpp +++ b/src/libwalletqt/PendingTransaction.cpp @@ -83,6 +83,11 @@ QString PendingTransaction::signedTxToHex(int index) const return QString::fromStdString(m_pimpl->signedTxToHex(index)); } +quint64 PendingTransaction::weight(int index) const +{ + return m_pimpl->weight(index); +} + PendingTransactionInfo * PendingTransaction::transaction(int index) const { return m_pending_tx_info[index]; } diff --git a/src/libwalletqt/PendingTransaction.h b/src/libwalletqt/PendingTransaction.h index 9e71f71a..754cfcd1 100644 --- a/src/libwalletqt/PendingTransaction.h +++ b/src/libwalletqt/PendingTransaction.h @@ -36,6 +36,7 @@ public: std::string unsignedTxToBin() const; QString unsignedTxToBase64() const; QString signedTxToHex(int index) const; + quint64 weight(int index) const; void refresh(); PendingTransactionInfo * transaction(int index) const; diff --git a/src/libwalletqt/Transfer.h b/src/libwalletqt/Transfer.h index d502db21..d2ae3001 100644 --- a/src/libwalletqt/Transfer.h +++ b/src/libwalletqt/Transfer.h @@ -4,9 +4,7 @@ #ifndef TRANSFER_H #define TRANSFER_H -#include #include -#include class Transfer : public QObject { diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index cb133295..c5eb5a8b 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -824,37 +824,62 @@ void Wallet::setSelectedInputs(const QStringList &selectedInputs) { emit selectedInputsChanged(selectedInputs); } +void Wallet::preTransactionChecks(int feeLevel) { + pauseRefresh(); + emit initiateTransaction(); + this->automaticFeeAdjustment(feeLevel); +} + +void Wallet::automaticFeeAdjustment(int feeLevel) { + m_scheduler.run([this, feeLevel]{ + QVector results; + + std::vector> blocks; + uint64_t priority = 0; + try { + priority = m_wallet2->adjust_priority(0, blocks); + } + catch (const std::exception &e) { } + + for (const auto &block : blocks) { + results.append(block.first); + } + + emit txPoolBacklog(results, feeLevel, priority); + }); +} + +void Wallet::confirmPreTransactionChecks(int feeLevel) { + emit preTransactionChecksComplete(feeLevel); +} + // Phase 1: Transaction creation // Pick one: // - createTransaction // - createTransactionMultiDest // - sweepOutputs -void Wallet::createTransaction(const QString &address, quint64 amount, const QString &description, bool all) { +void Wallet::createTransaction(const QString &address, quint64 amount, const QString &description, bool all, int feeLevel, bool subtractFeeFromAmount) { this->tmpTxDescription = description; - pauseRefresh(); qInfo() << "Creating transaction"; - m_scheduler.run([this, all, address, amount] { + m_scheduler.run([this, all, address, amount, feeLevel, subtractFeeFromAmount] { std::set subaddr_indices; Monero::PendingTransaction *ptImpl = m_walletImpl->createTransaction(address.toStdString(), "", all ? Monero::optional() : Monero::optional(amount), constants::mixin, - Monero::PendingTransaction::Priority_Default, - currentSubaddressAccount(), subaddr_indices, m_selectedInputs); + static_cast(feeLevel), + currentSubaddressAccount(), subaddr_indices, m_selectedInputs, subtractFeeFromAmount); QVector addresses{address}; this->onTransactionCreated(ptImpl, addresses); }); - - emit initiateTransaction(); } -void Wallet::createTransactionMultiDest(const QVector &addresses, const QVector &amounts, const QString &description) { +void Wallet::createTransactionMultiDest(const QVector &addresses, const QVector &amounts, const QString &description, int feeLevel, bool subtractFeeFromAmount) { this->tmpTxDescription = description; - pauseRefresh(); qInfo() << "Creating transaction"; - m_scheduler.run([this, addresses, amounts] { + m_scheduler.run([this, addresses, amounts, feeLevel, subtractFeeFromAmount] { std::vector dests; for (auto &addr : addresses) { dests.push_back(addr.toStdString()); @@ -867,23 +892,20 @@ void Wallet::createTransactionMultiDest(const QVector &addresses, const std::set subaddr_indices; Monero::PendingTransaction *ptImpl = m_walletImpl->createTransactionMultDest(dests, "", amount, constants::mixin, - Monero::PendingTransaction::Priority_Default, - currentSubaddressAccount(), subaddr_indices, m_selectedInputs); + static_cast(feeLevel), + currentSubaddressAccount(), subaddr_indices, m_selectedInputs, subtractFeeFromAmount); this->onTransactionCreated(ptImpl, addresses); }); - - emit initiateTransaction(); } -void Wallet::sweepOutputs(const QVector &keyImages, QString address, bool churn, int outputs) { - pauseRefresh(); +void Wallet::sweepOutputs(const QVector &keyImages, QString address, bool churn, int outputs, int feeLevel) { if (churn) { address = this->address(0, 0); } qInfo() << "Creating transaction"; - m_scheduler.run([this, keyImages, address, outputs] { + m_scheduler.run([this, keyImages, address, outputs, feeLevel] { std::vector kis; for (const auto &key_image : keyImages) { kis.push_back(key_image.toStdString()); @@ -891,13 +913,11 @@ void Wallet::sweepOutputs(const QVector &keyImages, QString address, bo Monero::PendingTransaction *ptImpl = m_walletImpl->createTransactionSelected(kis, address.toStdString(), outputs, - Monero::PendingTransaction::Priority_Default); + static_cast(feeLevel)); QVector addresses {address}; this->onTransactionCreated(ptImpl, addresses); }); - - emit initiateTransaction(); } // Phase 2: Transaction construction completed @@ -1335,6 +1355,80 @@ void Wallet::setNewWallet() { m_newWallet = true; } +bool Wallet::getBaseFees(QVector &baseFees) { + std::vector base_fees; + + try { + base_fees = m_wallet2->get_base_fees(); + } + catch (const std::exception &e) { + qWarning() << "Failed to get base fees: " << QString::fromStdString(e.what()); + return false; + } + + for (const auto fee : base_fees) { + baseFees.append(fee); + } + + return true; +} + +bool Wallet::estimateBacklog(const QVector &baseFees, QVector &backlog) { + std::vector> fee_levels; + + for (const auto fee : baseFees) { + fee_levels.push_back(std::make_pair(fee, fee)); + } + + std::vector> backlog_; + try { + backlog_ = m_wallet2->estimate_backlog(fee_levels); + } + catch (const std::exception &e) { + qWarning() << "Failed to estimate backlog: " << QString::fromStdString(e.what()); + return false; + } + + for (const auto b : backlog_) { + backlog.append(b.first); + } + + return true; +} + +bool Wallet::getBlockWeightLimit(quint64 &blockWeightLimit) { + try { + blockWeightLimit = m_wallet2->get_block_weight_limit(); + } + catch (const std::exception &e) { + return false; + } + + return true; +} + +void Wallet::getTxPoolStatsAsync() { + m_scheduler.run([this] { + QVector txPoolBacklog; + + quint64 blockWeightLimit = m_wallet2->get_block_weight_limit(); + std::vector base_fees = m_wallet2->get_base_fees(); + + QVector baseFees; + for (const auto &fee : base_fees) { + baseFees.push_back(fee); + } + + auto entries = m_wallet2->get_txpool_backlog(); + for (const auto &entry : entries) { + TxBacklogEntry result{entry.weight, entry.fee, entry.time_in_pool}; + txPoolBacklog.push_back(result); + } + + emit poolStats(txPoolBacklog, baseFees, blockWeightLimit); + }); +} + Wallet::~Wallet() { qDebug("~Wallet: Closing wallet"); diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index 7dbc0380..bf5636b0 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -16,6 +16,7 @@ #include "utils/networktype.h" #include "PassphraseHelper.h" #include "WalletListenerImpl.h" +#include "rows/TxBacklogEntry.h" namespace Monero { struct Wallet; // forward declaration @@ -313,10 +314,13 @@ public: // ##### Transactions ##### void setSelectedInputs(const QStringList &selected); + void preTransactionChecks(int feeLevel); + void automaticFeeAdjustment(int feeLevel); + void confirmPreTransactionChecks(int feeLevel); - void createTransaction(const QString &address, quint64 amount, const QString &description, bool all); - void createTransactionMultiDest(const QVector &addresses, const QVector &amounts, const QString &description); - void sweepOutputs(const QVector &keyImages, QString address, bool churn, int outputs); + void createTransaction(const QString &address, quint64 amount, const QString &description, bool all, int feeLevel = 0, bool subtractFeeFromAmount = false); + void createTransactionMultiDest(const QVector &addresses, const QVector &amounts, const QString &description, int feeLevel = 0, bool subtractFeeFromAmount = false); + void sweepOutputs(const QVector &keyImages, QString address, bool churn, int outputs, int feeLevel = 0); void commitTransaction(PendingTransaction *tx, const QString &description=""); void onTransactionCommitted(bool success, PendingTransaction *tx, const QStringList& txid, const QMap &txHexMap); @@ -412,6 +416,11 @@ public: void onHeightsRefreshed(bool success, quint64 daemonHeight, quint64 targetHeight); + void getTxPoolStatsAsync(); + bool getBaseFees(QVector &baseFees); + bool estimateBacklog(const QVector &baseFees, QVector &backlog); + bool getBlockWeightLimit(quint64 &blockWeightLimit); + signals: // emitted on every event happened with wallet // (money sent/received, new block) @@ -435,6 +444,9 @@ signals: void deviceShowAddressShowed(); void transactionProofVerified(TxProofResult result); void spendProofVerified(QPair result); + void poolStats(const QVector &txPool, const QVector &baseFees, quint64 blockWeightLimit); + void txPoolBacklog(const QVector &backlog, quint64 originalFeeLevel, quint64 adjustedFeeLevel); + void preTransactionChecksComplete(int feeLevel); void connectionStatusChanged(int status) const; void currentSubaddressAccountChanged() const; diff --git a/src/libwalletqt/rows/TxBacklogEntry.h b/src/libwalletqt/rows/TxBacklogEntry.h new file mode 100644 index 00000000..1ff18d74 --- /dev/null +++ b/src/libwalletqt/rows/TxBacklogEntry.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_TXBACKLOGENTRY_H +#define FEATHER_TXBACKLOGENTRY_H + +struct TxBacklogEntry { + quint64 weight; + quint64 fee; + quint64 timeInPool; +}; + +#endif //FEATHER_TXBACKLOGENTRY_H diff --git a/src/utils/Utils.cpp b/src/utils/Utils.cpp index e625aea0..e85f615d 100644 --- a/src/utils/Utils.cpp +++ b/src/utils/Utils.cpp @@ -407,7 +407,7 @@ QString formatBytes(quint64 bytes) QVector sizes = { "B", "KB", "MB", "GB", "TB" }; int i; - double _data; + double _data = bytes; for (i = 0; i < sizes.count() && bytes >= 10000; i++, bytes /= 1000) _data = bytes / 1000.0; diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 8617109e..c04799ac 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -82,9 +82,13 @@ static const QHash configStrings = { {Config::disableWebsocket, {QS("disableWebsocket"), false}}, {Config::offlineMode, {QS("offlineMode"), false}}, + // Transactions {Config::multiBroadcast, {QS("multiBroadcast"), true}}, {Config::offlineTxSigningMethod, {QS("offlineTxSigningMethod"), Config::OTSMethod::UnifiedResources}}, {Config::offlineTxSigningForceKISync, {QS("offlineTxSigningForceKISync"), false}}, + {Config::manualFeeTierSelection, {QS("manualFeeTierSelection"), false}}, + {Config::subtractFeeFromAmount, {QS("subtractFeeFromAmount"), false}}, + {Config::warnOnExternalLink,{QS("warnOnExternalLink"), true}}, {Config::hideBalance, {QS("hideBalance"), false}}, {Config::hideNotifications, {QS("hideNotifications"), false}}, diff --git a/src/utils/config.h b/src/utils/config.h index 59c18102..9f163e0e 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -121,6 +121,8 @@ public: multiBroadcast, offlineTxSigningMethod, offlineTxSigningForceKISync, + manualFeeTierSelection, + subtractFeeFromAmount, // Misc blockExplorers, diff --git a/src/utils/nodes.cpp b/src/utils/nodes.cpp index f475301c..a31acc2f 100644 --- a/src/utils/nodes.cpp +++ b/src/utils/nodes.cpp @@ -253,6 +253,14 @@ void Nodes::autoConnect(bool forceReconnect) { return; } + if (!m_allowConnection) { + return; + } + + if (conf()->get(Config::offlineMode).toBool()) { + return; + } + // this function is responsible for automatically connecting to a daemon. if (m_wallet == nullptr || !m_enableAutoconnect) { return; @@ -334,6 +342,13 @@ FeatherNode Nodes::pickEligibleNode() { continue; } + if (conf()->get(Config::proxy).toInt() == Config::Proxy::Tor && conf()->get(Config::torOnlyAllowOnion).toBool()) { + if (!node.isOnion() && !node.isLocal()) { + // We only want to connect to .onion nodes, but local nodes get an exception. + continue; + } + } + // Don't connect to nodes that failed to connect recently if (m_recentFailures.contains(node.toAddress())) { continue;