manual input selection, subtract fee from amount

This commit is contained in:
tobtoht 2024-04-28 21:26:54 +02:00
parent 0da98d2003
commit deb9d7ff63
No known key found for this signature in database
GPG key ID: E45B10DD027D2472
31 changed files with 904 additions and 42 deletions

View file

@ -350,6 +350,7 @@ EOF
--container \
--pure \
--no-cwd \
--cores="$JOBS" \
${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"} \
-- echo "$HOST"

View file

@ -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} \

2
monero

@ -1 +1 @@
Subproject commit 85ea9458c8a27814729b24c3b932f60ff331903e
Subproject commit 376fb747ea262cf6cd773cc169bbd3e84670d733

View file

@ -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) {

View file

@ -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<QString, QString> &txHexMap) {
QMapIterator<QString, QString> 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<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())

View file

@ -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<quint64> &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<QString, QString> &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;

View file

@ -469,7 +469,7 @@
<x>0</x>
<y>0</y>
<width>977</width>
<height>24</height>
<height>27</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@ -552,6 +552,7 @@
<addaction name="actionPay_to_many"/>
<addaction name="actionAddress_checker"/>
<addaction name="actionCreateDesktopEntry"/>
<addaction name="actionTxPoolViewer"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
@ -933,6 +934,11 @@
<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>

View file

@ -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);

View file

@ -40,6 +40,9 @@ public slots:
void onPreferredFiatCurrencyChanged();
void setWebsocketEnabled(bool enabled);
void setManualFeeSelectionEnabled(bool enabled);
void setSubtractFeeFromAmountEnabled(bool enabled);
void disableSendButton();
void enableSendButton();

View file

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>647</width>
<height>231</height>
<height>254</height>
</rect>
</property>
<property name="sizePolicy">
@ -172,6 +172,13 @@
</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">
@ -187,7 +194,14 @@
</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>
@ -228,6 +242,35 @@
</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>

View file

@ -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() {

View file

@ -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();

View file

@ -32,7 +32,7 @@
<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">
@ -955,6 +955,20 @@
</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">

View file

@ -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);

View file

@ -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();

View file

@ -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<int>::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;

View file

@ -24,6 +24,7 @@ public:
QString address();
bool churn() const;
int outputs() const;
int feeLevel() const;
private:
QScopedPointer<Ui::OutputSweepDialog> 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;
};

View file

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>720</width>
<height>193</height>
<height>225</height>
</rect>
</property>
<property name="windowTitle">
@ -19,6 +19,9 @@
</property>
<item>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<property name="verticalSpacing">
<number>0</number>
</property>
@ -49,6 +52,42 @@
</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>

View file

@ -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 <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;

View file

@ -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 <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

View file

@ -0,0 +1,280 @@
<?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>

View file

@ -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];
}

View file

@ -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;

View file

@ -4,9 +4,7 @@
#ifndef TRANSFER_H
#define TRANSFER_H
#include <wallet/api/wallet2_api.h>
#include <QObject>
#include <utility>
class Transfer : public QObject
{

View file

@ -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<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());
@ -867,23 +892,20 @@ void Wallet::createTransactionMultiDest(const QVector<QString> &addresses, const
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());
@ -891,13 +913,11 @@ void Wallet::sweepOutputs(const QVector<QString> &keyImages, QString address, bo
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
@ -1335,6 +1355,80 @@ void Wallet::setNewWallet() {
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");

View file

@ -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<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);
@ -412,6 +416,11 @@ public:
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)
@ -435,6 +444,9 @@ signals:
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;

View file

@ -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

View file

@ -407,7 +407,7 @@ QString formatBytes(quint64 bytes)
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;

View file

@ -82,9 +82,13 @@ static const QHash<Config::ConfigKey, ConfigDirective> 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}},

View file

@ -121,6 +121,8 @@ public:
multiBroadcast,
offlineTxSigningMethod,
offlineTxSigningForceKISync,
manualFeeTierSelection,
subtractFeeFromAmount,
// Misc
blockExplorers,

View file

@ -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;