--container \
--pure \
--no-cwd \
+ --cores="$JOBS" \
${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"} \
-- echo "$HOST"
# 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} \
-Subproject commit 85ea9458c8a27814729b24c3b932f60ff331903e
+Subproject commit 376fb747ea262cf6cd773cc169bbd3e84670d733
#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) {
#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"
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);
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);
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);
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){
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();
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<QString, QString> &txHexMap) {
QMapIterator<QString, QString> i(txHexMap);
while (i.hasNext()) {
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();
}
}
+void MainWindow::onTxPoolBacklog(const QVector<quint64> &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())
#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"
void onInitiateTransaction();
void onKeysCorrupted();
void onSelectedInputsChanged(const QStringList &selectedInputs);
+ void onTxPoolBacklog(const QVector<quint64> &backlog, quint64 originalFeeLevel, quint64 automaticFeeLevel);
// libwalletqt
void onBalanceUpdated(quint64 balance, quint64 spendable);
void showViewOnlyDialog();
void showKeyImageSyncWizard();
void showWalletCacheDebugDialog();
+ void showTxPoolViewerDialog();
void showAccountSwitcherDialog();
void showAddressChecker();
void showURDialog();
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<QString, QString> &txHexMap);
private:
SplashDialog *m_splashDialog = nullptr;
AccountSwitcherDialog *m_accountSwitcherDialog = nullptr;
+ TxPoolViewerDialog *m_txPoolViewerDialog = nullptr;
WalletUnlockWidget *m_walletUnlockWidget = nullptr;
ContactsWidget *m_contactsWidget = nullptr;
<x>0</x>
<y>0</y>
<width>977</width>
- <height>24</height>
+ <height>27</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<addaction name="actionPay_to_many"/>
<addaction name="actionAddress_checker"/>
<addaction name="actionCreateDesktopEntry"/>
+ <addaction name="actionTxPoolViewer"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>PlaceholderBegin</string>
</property>
</action>
+ <action name="actionTxPoolViewer">
+ <property name="text">
+ <string>Tx pool viewer</string>
+ </property>
+ </action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
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) {
return;
}
+ bool subtractFeeFromAmount = conf()->get(Config::subtractFeeFromAmount).toBool() && ui->check_subtractFeeFromAmount->isChecked();
+
QString description = ui->lineDescription->text();
if (!outputs.empty()) { // multi destination transaction
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;
}
#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() {
}
}
+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);
void onPreferredFiatCurrencyChanged();
void setWebsocketEnabled(bool enabled);
+ void setManualFeeSelectionEnabled(bool enabled);
+ void setSubtractFeeFromAmountEnabled(bool enabled);
+
void disableSendButton();
void enableSendButton();
<x>0</x>
<y>0</y>
<width>647</width>
- <height>231</height>
+ <height>254</height>
</rect>
</property>
<property name="sizePolicy">
</property>
</widget>
</item>
+ <item>
+ <widget class="QCheckBox" name="check_subtractFeeFromAmount">
+ <property name="text">
+ <string>Subtract fee from amount</string>
+ </property>
+ </widget>
+ </item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
</item>
</layout>
</item>
- <item row="4" column="1">
+ <item row="4" column="0">
+ <widget class="QLabel" name="label_feeTarget">
+ <property name="text">
+ <string>Fee</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>6</number>
</item>
</layout>
</item>
+ <item row="4" column="1">
+ <widget class="QComboBox" name="combo_feePriority">
+ <item>
+ <property name="text">
+ <string>Automatic</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Low</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Normal</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>High</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Highest</string>
+ </property>
+ </item>
+ </widget>
+ </item>
</layout>
</widget>
<customwidgets>
// 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() {
void updateBalance();
void offlineMode(bool offline);
void pluginConfigured(const QString &id);
+ void manualFeeSelectionEnabled(bool enabled);
+ void subtractFeeFromAmountEnabled(bool enabled);
public slots:
// void checkboxExternalLinkWarn();
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
- <number>7</number>
+ <number>5</number>
</property>
<widget class="QWidget" name="page_appearance">
<layout class="QVBoxLayout" name="verticalLayout_6">
</property>
</widget>
</item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_manualFeeTierSelection">
+ <property name="text">
+ <string>Manual fee-tier selection</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_subtractFeeFromAmount">
+ <property name="text">
+ <string>Subtract fee from outputs</string>
+ </property>
+ </widget>
+ </item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
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);
void preferredFiatCurrencyChanged();
void offlineMode(bool offline);
void pluginConfigured(const QString &id);
+ void manualFeeSelectionEnabled(bool enabled);
+ void subtractFeeFromAmountEnabled(bool enabled);
public slots:
void onProxySettingsChanged();
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<int>::of(&QSpinBox::valueChanged), [this](int value){
return m_outputs;
}
+int OutputSweepDialog::feeLevel() const {
+ return m_feeLevel;
+}
+
OutputSweepDialog::~OutputSweepDialog() = default;
\ No newline at end of file
QString address();
bool churn() const;
int outputs() const;
+ int feeLevel() const;
private:
QScopedPointer<Ui::OutputSweepDialog> ui;
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;
};
<x>0</x>
<y>0</y>
<width>720</width>
- <height>193</height>
+ <height>225</height>
</rect>
</property>
<property name="windowTitle">
</property>
<item>
<layout class="QFormLayout" name="formLayout">
+ <property name="fieldGrowthPolicy">
+ <enum>QFormLayout::ExpandingFieldsGrow</enum>
+ </property>
<property name="verticalSpacing">
<number>0</number>
</property>
</property>
</widget>
</item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Fee:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QComboBox" name="combo_feePriority">
+ <item>
+ <property name="text">
+ <string>Automatic</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Low</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Normal</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>High</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>Highest</string>
+ </property>
+ </item>
+ </widget>
+ </item>
</layout>
</item>
<item>
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2020-2024 The Monero Project
+
+#include "TxPoolViewerDialog.h"
+#include "ui_TxPoolViewerDialog.h"
+
+#include <QTreeWidgetItem>
+
+#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<TxBacklogEntry> &txPool, const QVector<quint64> &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;
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2020-2024 The Monero Project
+
+#ifndef FEATHER_TXPOOLVIEWERDIALOG_H
+#define FEATHER_TXPOOLVIEWERDIALOG_H
+
+#include <QDialog>
+
+#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<TxBacklogEntry> &txPool, const QVector<quint64> &baseFees, quint64 blockWeightLimit);
+
+ QVector<FeeTierStats> m_feeTierStats;
+ QScopedPointer<Ui::TxPoolViewerDialog> ui;
+ Wallet *m_wallet;
+};
+
+
+#endif //FEATHER_TXPOOLVIEWERDIALOG_H
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TxPoolViewerDialog</class>
+ <widget class="QDialog" name="TxPoolViewerDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>564</width>
+ <height>779</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Tx Pool Viewer</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="title">
+ <string>Stats</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="labelAlignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string>Transactions:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="label_transactions">
+ <property name="text">
+ <string>Loading..</string>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Total weight:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="label_totalWeight">
+ <property name="text">
+ <string>Loading..</string>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Total fees:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="label_totalFees">
+ <property name="text">
+ <string>Loading..</string>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Full reward zone:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="label_blockWeightLimit">
+ <property name="text">
+ <string>Loading..</string>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string>Transactions</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QTreeWidget" name="tree_pool">
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <column>
+ <property name="text">
+ <string>Weight</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Fee</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Fee / B</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>Fee tiers</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QTreeWidget" name="tree_feeTiers">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <column>
+ <property name="text">
+ <string>Tier</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Fee / B</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Backlog</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Weight from tip</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="btn_refresh">
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Close</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>TxPoolViewerDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>TxPoolViewerDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
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];
}
std::string unsignedTxToBin() const;
QString unsignedTxToBase64() const;
QString signedTxToHex(int index) const;
+ quint64 weight(int index) const;
void refresh();
PendingTransactionInfo * transaction(int index) const;
#ifndef TRANSFER_H
#define TRANSFER_H
-#include <wallet/api/wallet2_api.h>
#include <QObject>
-#include <utility>
class Transfer : public QObject
{
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<quint64> results;
+
+ std::vector<std::pair<uint64_t, uint64_t>> 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<uint32_t> subaddr_indices;
Monero::PendingTransaction *ptImpl = m_walletImpl->createTransaction(address.toStdString(), "", all ? Monero::optional<uint64_t>() : Monero::optional<uint64_t>(amount), constants::mixin,
- Monero::PendingTransaction::Priority_Default,
- currentSubaddressAccount(), subaddr_indices, m_selectedInputs);
+ static_cast<Monero::PendingTransaction::Priority>(feeLevel),
+ currentSubaddressAccount(), subaddr_indices, m_selectedInputs, subtractFeeFromAmount);
QVector<QString> addresses{address};
this->onTransactionCreated(ptImpl, addresses);
});
-
- emit initiateTransaction();
}
-void Wallet::createTransactionMultiDest(const QVector<QString> &addresses, const QVector<quint64> &amounts, const QString &description) {
+void Wallet::createTransactionMultiDest(const QVector<QString> &addresses, const QVector<quint64> &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<std::string> dests;
for (auto &addr : addresses) {
dests.push_back(addr.toStdString());
std::set<uint32_t> subaddr_indices;
Monero::PendingTransaction *ptImpl = m_walletImpl->createTransactionMultDest(dests, "", amount, constants::mixin,
- Monero::PendingTransaction::Priority_Default,
- currentSubaddressAccount(), subaddr_indices, m_selectedInputs);
+ static_cast<Monero::PendingTransaction::Priority>(feeLevel),
+ currentSubaddressAccount(), subaddr_indices, m_selectedInputs, subtractFeeFromAmount);
this->onTransactionCreated(ptImpl, addresses);
});
-
- emit initiateTransaction();
}
-void Wallet::sweepOutputs(const QVector<QString> &keyImages, QString address, bool churn, int outputs) {
- pauseRefresh();
+void Wallet::sweepOutputs(const QVector<QString> &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<std::string> kis;
for (const auto &key_image : keyImages) {
kis.push_back(key_image.toStdString());
Monero::PendingTransaction *ptImpl = m_walletImpl->createTransactionSelected(kis,
address.toStdString(),
outputs,
- Monero::PendingTransaction::Priority_Default);
+ static_cast<Monero::PendingTransaction::Priority>(feeLevel));
QVector<QString> addresses {address};
this->onTransactionCreated(ptImpl, addresses);
});
-
- emit initiateTransaction();
}
// Phase 2: Transaction construction completed
m_newWallet = true;
}
+bool Wallet::getBaseFees(QVector<quint64> &baseFees) {
+ std::vector<uint64_t> 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<quint64> &baseFees, QVector<quint64> &backlog) {
+ std::vector<std::pair<double, double>> fee_levels;
+
+ for (const auto fee : baseFees) {
+ fee_levels.push_back(std::make_pair<double, double>(fee, fee));
+ }
+
+ std::vector<std::pair<uint64_t, uint64_t>> 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<TxBacklogEntry> txPoolBacklog;
+
+ quint64 blockWeightLimit = m_wallet2->get_block_weight_limit();
+ std::vector<uint64_t> base_fees = m_wallet2->get_base_fees();
+
+ QVector<quint64> 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");
#include "utils/networktype.h"
#include "PassphraseHelper.h"
#include "WalletListenerImpl.h"
+#include "rows/TxBacklogEntry.h"
namespace Monero {
struct Wallet; // forward declaration
// ##### 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<QString> &addresses, const QVector<quint64> &amounts, const QString &description);
- void sweepOutputs(const QVector<QString> &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<QString> &addresses, const QVector<quint64> &amounts, const QString &description, int feeLevel = 0, bool subtractFeeFromAmount = false);
+ void sweepOutputs(const QVector<QString> &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<QString, QString> &txHexMap);
void onHeightsRefreshed(bool success, quint64 daemonHeight, quint64 targetHeight);
+ void getTxPoolStatsAsync();
+ bool getBaseFees(QVector<quint64> &baseFees);
+ bool estimateBacklog(const QVector<quint64> &baseFees, QVector<quint64> &backlog);
+ bool getBlockWeightLimit(quint64 &blockWeightLimit);
+
signals:
// emitted on every event happened with wallet
// (money sent/received, new block)
void deviceShowAddressShowed();
void transactionProofVerified(TxProofResult result);
void spendProofVerified(QPair<bool, bool> result);
+ void poolStats(const QVector<TxBacklogEntry> &txPool, const QVector<quint64> &baseFees, quint64 blockWeightLimit);
+ void txPoolBacklog(const QVector<quint64> &backlog, quint64 originalFeeLevel, quint64 adjustedFeeLevel);
+ void preTransactionChecksComplete(int feeLevel);
void connectionStatusChanged(int status) const;
void currentSubaddressAccountChanged() const;
--- /dev/null
+// 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
QVector<QString> 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;
{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}},
multiBroadcast,
offlineTxSigningMethod,
offlineTxSigningForceKISync,
+ manualFeeTierSelection,
+ subtractFeeFromAmount,
// Misc
blockExplorers,
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;
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;