diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index cf8a5fb3c4..4955f6fbda 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -21,12 +21,14 @@ #include #include #include +#include // for ParseDustDynamicOpt #include #include // for WITNESS_SCALE_FACTOR #include #include // for maxmempoolMinimum #include #include +#include #include #include @@ -41,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -67,13 +70,15 @@ void OptionsDialog::CreateOptionUI(QBoxLayout * const layout, QWidget * const o, if (!horizontalLayout) horizontalLayout = new QHBoxLayout(); - QLabel * const labelBefore = new QLabel(parent); - labelBefore->setText(text_parts[0]); - labelBefore->setTextFormat(Qt::PlainText); - labelBefore->setBuddy(o); - labelBefore->setToolTip(o->toolTip()); + if (!text_parts[0].isEmpty()) { + QLabel * const labelBefore = new QLabel(parent); + labelBefore->setText(text_parts[0]); + labelBefore->setTextFormat(Qt::PlainText); + labelBefore->setBuddy(o); + labelBefore->setToolTip(o->toolTip()); + horizontalLayout->addWidget(labelBefore); + } - horizontalLayout->addWidget(labelBefore); horizontalLayout->addWidget(o); QLabel * const labelAfter = new QLabel(parent); @@ -340,7 +345,67 @@ OptionsDialog::OptionsDialog(QWidget* parent, bool enableWallet) }); dustrelayfee = new BitcoinAmountField(groupBox_Spamfiltering); - CreateOptionUI(verticalLayout_Spamfiltering, dustrelayfee, tr("Ignore transactions with values that would cost more to spend at a fee rate of %s per kvB.")); + CreateOptionUI(verticalLayout_Spamfiltering, dustrelayfee, tr("Ignore transactions with values that would cost more to spend at a fee rate of %s per kvB (\"dust\").")); + + + auto hlayout = new QHBoxLayout(); + dustdynamic_enable = new QCheckBox(groupBox_Spamfiltering); + dustdynamic_enable->setText(tr("Automatically adjust the dust limit upward to")); + hlayout->addWidget(dustdynamic_enable); + dustdynamic_multiplier = new QDoubleSpinBox(groupBox_Spamfiltering); + dustdynamic_multiplier->setDecimals(3); + dustdynamic_multiplier->setStepType(QAbstractSpinBox::DefaultStepType); + dustdynamic_multiplier->setSingleStep(1); + dustdynamic_multiplier->setMinimum(0.001); + dustdynamic_multiplier->setMaximum(65); + dustdynamic_multiplier->setValue(DEFAULT_DUST_RELAY_MULTIPLIER / 1000.0); + CreateOptionUI(verticalLayout_Spamfiltering, dustdynamic_multiplier, tr("%s times:"), hlayout); + + QStyleOptionButton styleoptbtn; + const auto checkbox_indent = dustdynamic_enable->style()->subElementRect(QStyle::SE_CheckBoxIndicator, &styleoptbtn, dustdynamic_enable).width(); + + hlayout = new QHBoxLayout(); + hlayout->addSpacing(checkbox_indent); + dustdynamic_target = new QRadioButton(groupBox_Spamfiltering); + hlayout->addWidget(dustdynamic_target); + dustdynamic_target_blocks = new QSpinBox(groupBox_Spamfiltering); + dustdynamic_target_blocks->setMinimum(2); + dustdynamic_target_blocks->setMaximum(1008); // FIXME: Get this from the fee estimator + dustdynamic_target_blocks->setValue(1008); + CreateOptionUI(verticalLayout_Spamfiltering, dustdynamic_target_blocks, tr("fee estimate for %s blocks."), hlayout); + // FIXME: Make it possible to click labels to select + focus spinbox + + hlayout = new QHBoxLayout(); + hlayout->addSpacing(checkbox_indent); + dustdynamic_mempool = new QRadioButton(groupBox_Spamfiltering); + hlayout->addWidget(dustdynamic_mempool); + dustdynamic_mempool_kvB = new QSpinBox(groupBox_Spamfiltering); + dustdynamic_mempool_kvB->setMinimum(1); + dustdynamic_mempool_kvB->setMaximum(std::numeric_limits::max()); + dustdynamic_mempool_kvB->setValue(3024000); + CreateOptionUI(verticalLayout_Spamfiltering, dustdynamic_mempool_kvB, tr("the lowest fee of the best known %s kvB of unconfirmed transactions."), hlayout); + + connect(dustdynamic_enable, &QAbstractButton::toggled, [this](const bool state){ + dustdynamic_multiplier->setEnabled(state); + dustdynamic_target->setEnabled(state); + dustdynamic_mempool->setEnabled(state); + if (state) { + if (!dustdynamic_mempool->isChecked()) dustdynamic_target->setChecked(true); + dustdynamic_target_blocks->setEnabled(dustdynamic_target->isChecked()); + dustdynamic_mempool_kvB->setEnabled(dustdynamic_mempool->isChecked()); + } else { + dustdynamic_target_blocks->setEnabled(false); + dustdynamic_mempool_kvB->setEnabled(false); + } + }); + dustdynamic_enable->toggled(dustdynamic_enable->isChecked()); + connect(dustdynamic_target, &QAbstractButton::toggled, [this](const bool state){ + dustdynamic_target_blocks->setEnabled(state); + }); + connect(dustdynamic_mempool, &QAbstractButton::toggled, [this](const bool state){ + dustdynamic_mempool_kvB->setEnabled(state); + }); + verticalLayout_Mempool->addWidget(groupBox_Spamfiltering); @@ -645,6 +710,24 @@ void OptionsDialog::setMapper() mapper->addMapping(datacarriersize, OptionsModel::datacarriersize); mapper->addMapping(dustrelayfee, OptionsModel::dustrelayfee); + QVariant current_dustdynamic = model->data(model->index(OptionsModel::dustdynamic, 0), Qt::EditRole); + const util::Result> parsed_dustdynamic = ParseDustDynamicOpt(current_dustdynamic.toString().toStdString(), std::numeric_limits::max()); + if (parsed_dustdynamic) { + if (parsed_dustdynamic->first == 0) { + dustdynamic_enable->setChecked(false); + } else { + dustdynamic_multiplier->setValue(parsed_dustdynamic->second / 1000.0); + if (parsed_dustdynamic->first < 0) { + dustdynamic_target->setChecked(true); + dustdynamic_target_blocks->setValue(-parsed_dustdynamic->first); + } else { + dustdynamic_mempool->setChecked(true); + dustdynamic_mempool_kvB->setValue(parsed_dustdynamic->first); + } + dustdynamic_enable->setChecked(true); + } + } + /* Mining tab */ mapper->addMapping(blockmintxfee, OptionsModel::blockmintxfee); @@ -817,6 +900,16 @@ void OptionsDialog::on_okButton_clicked() model->setData(model->index(OptionsModel::mempoolreplacement, 0), mempoolreplacement->itemData(mempoolreplacement->currentIndex())); + if (dustdynamic_enable->isChecked()) { + if (dustdynamic_target->isChecked()) { + model->setData(model->index(OptionsModel::dustdynamic, 0), QStringLiteral("%2*target:%1").arg(dustdynamic_target_blocks->value()).arg(dustdynamic_multiplier->value())); + } else if (dustdynamic_mempool->isChecked()) { + model->setData(model->index(OptionsModel::dustdynamic, 0), QStringLiteral("%2*mempool:%1").arg(dustdynamic_mempool_kvB->value()).arg(dustdynamic_multiplier->value())); + } + } else { + model->setData(model->index(OptionsModel::dustdynamic, 0), "off"); + } + mapper->submit(); accept(); updateDefaultProxyNets(); diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h index 1ba3d1be4c..606e78f486 100644 --- a/src/qt/optionsdialog.h +++ b/src/qt/optionsdialog.h @@ -19,6 +19,7 @@ class QCheckBox; class QDataWidgetMapper; class QDoubleSpinBox; class QLayout; +class QRadioButton; class QSpinBox; class QString; class QValueComboBox; @@ -121,6 +122,12 @@ private: QSpinBox *datacarriersize; QDoubleSpinBox *datacarriercost; BitcoinAmountField *dustrelayfee; + QCheckBox *dustdynamic_enable; + QDoubleSpinBox *dustdynamic_multiplier; + QRadioButton *dustdynamic_target; + QSpinBox *dustdynamic_target_blocks; + QRadioButton *dustdynamic_mempool; + QSpinBox *dustdynamic_mempool_kvB; BitcoinAmountField *blockmintxfee; QSpinBox *blockmaxsize, *blockprioritysize, *blockmaxweight; diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index 27acaf06f4..4963476c87 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -23,6 +23,7 @@ #include #include #include +#include // for ParseDustDynamicOpt #include #include #include // for -dbcache defaults @@ -38,6 +39,7 @@ #include #include #include +#include #include #include @@ -77,6 +79,7 @@ static const char* SettingName(OptionsModel::OptionID option) case OptionsModel::peerbloomfilters: return "peerbloomfilters"; case OptionsModel::peerblockfilters: return "peerblockfilters"; case OptionsModel::datacarriercost: return "datacarriercost"; + case OptionsModel::dustdynamic: return "dustdynamic"; default: throw std::logic_error(strprintf("GUI option %i has no corresponding node setting.", option)); } } @@ -685,6 +688,8 @@ QVariant OptionsModel::getOption(OptionID option, const std::string& suffix) con return qlonglong(node().mempool().m_opts.max_datacarrier_bytes.value_or(0)); case dustrelayfee: return qlonglong(node().mempool().m_opts.dust_relay_feerate_floor.GetFeePerK()); + case dustdynamic: + return QString::fromStdString(SettingToString(setting(), DEFAULT_DUST_DYNAMIC)); case blockmintxfee: if (gArgs.IsArgSet("-blockmintxfee")) { return qlonglong(ParseMoney(gArgs.GetArg("-blockmintxfee", "")).value_or(0)); @@ -1230,6 +1235,17 @@ bool OptionsModel::setOption(OptionID option, const QVariant& value, const std:: } } break; + case dustdynamic: + if (changed()) { + const std::string newvalue_str = value.toString().toStdString(); + const util::Result> parsed = ParseDustDynamicOpt(newvalue_str, 1008 /* FIXME: get from estimator */); + assert(parsed); // FIXME: what to do if it fails to parse? + // FIXME: save -prev- for each type + update(newvalue_str); + node().mempool().m_opts.dust_relay_target = parsed->first; + node().mempool().m_opts.dust_relay_multiplier = parsed->second; + } + break; case blockmintxfee: if (changed()) { std::string strNv = FormatMoney(value.toLongLong()); diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index 77df89f3ce..73397368f4 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -101,6 +101,7 @@ public: datacarriercost, // double datacarriersize, dustrelayfee, + dustdynamic, // QString blockmintxfee, blockmaxsize, blockprioritysize,