optionsdialog.cpp raw
1 // Copyright (c) 2011-2022 The Limenka developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #include <limenka-build-config.h> // IWYU pragma: keep
6
7 #include <qt/optionsdialog.h>
8 #include <qt/forms/ui_optionsdialog.h>
9
10 #include <qt/limenkaamountfield.h>
11 #include <qt/limenkaunits.h>
12 #include <qt/clientmodel.h>
13 #include <qt/guiconstants.h>
14 #include <qt/guiutil.h>
15 #include <qt/optionsmodel.h>
16
17 #include <common/args.h>
18 #include <common/system.h>
19 #include <consensus/consensus.h> // for MAX_BLOCK_SERIALIZED_SIZE
20 #include <index/blockfilterindex.h>
21 #include <interfaces/node.h>
22 #include <netbase.h>
23 #include <node/caches.h>
24 #include <node/dbcache.h>
25 #include <node/chainstatemanager_args.h>
26 #include <node/mempool_args.h> // for ParseDustDynamicOpt
27 #include <outputtype.h>
28 #include <primitives/transaction.h> // for WITNESS_SCALE_FACTOR
29 #include <txmempool.h> // for maxmempoolMinimum
30 #include <util/check.h>
31 #include <util/strencodings.h>
32 #include <chrono>
33 #include <cmath>
34 #include <utility>
35
36 #include <QApplication>
37 #include <QBoxLayout>
38 #include <QDataWidgetMapper>
39 #include <QDir>
40 #include <QDoubleSpinBox>
41 #include <QFontDialog>
42 #include <QGroupBox>
43 #include <QHBoxLayout>
44 #include <QInputDialog>
45 #include <QIntValidator>
46 #include <QLabel>
47 #include <QLocale>
48 #include <QMessageBox>
49 #include <QRadioButton>
50 #include <QScrollArea>
51 #include <QScrollBar>
52 #include <QSpacerItem>
53 #include <QString>
54 #include <QStringList>
55 #include <QSystemTrayIcon>
56 #include <QTimer>
57 #include <QVBoxLayout>
58 #include <QWidget>
59
60 ModScrollArea::ModScrollArea()
61 {
62 setWidgetResizable(true);
63 setFrameShape(QFrame::NoFrame);
64 setObjectName(QStringLiteral("scroll"));
65 setStyleSheet("QScrollArea#scroll, QScrollArea#scroll > QWidget > QWidget { background: transparent; } QScrollArea#scroll > QWidget > QScrollBar { background: palette(base); }");
66 }
67
68 ModScrollArea *ModScrollArea::fromWidget(QWidget * const parent, QWidget * const o)
69 {
70 auto * const scroll = new ModScrollArea;
71 scroll->setWidget(o);
72 return scroll;
73 }
74
75 QSize ModScrollArea::minimumSizeHint() const
76 {
77 auto w = widget()->minimumSizeHint().width();
78 w += verticalScrollBar()->sizeHint().width();
79 const auto h = fontMetrics().height() * 2;
80 return QSize(w, h);
81 }
82
83 QSize ModScrollArea::sizeHint() const
84 {
85 QSize sz = widget()->sizeHint();
86 sz.rwidth() += verticalScrollBar()->sizeHint().width();
87 return sz;
88 }
89
90 void OptionsDialog::FixTabOrder(QWidget * const o)
91 {
92 LimenkaAmountField * const af = qobject_cast<LimenkaAmountField *>(o);
93 if (af) {
94 prevwidget = af->setupTabChain(prevwidget);
95 } else {
96 setTabOrder(prevwidget, o);
97 prevwidget = o;
98 }
99 }
100
101 struct CreateOptionUIOpts {
102 QBoxLayout *horizontal_layout{nullptr};
103 int stretch{1};
104 int insert_at{-1};
105 int indent{0};
106 };
107
108 void OptionsDialog::CreateOptionUI(QBoxLayout * const layout, const QString& text, const std::vector<QWidget *>& objs, const CreateOptionUIOpts& opts)
109 {
110 Assert(!objs.empty());
111
112 auto& first_o = objs[0];
113 QWidget * const parent = first_o->parentWidget();
114
115 QBoxLayout * const horizontalLayout = opts.horizontal_layout ? opts.horizontal_layout : (new QHBoxLayout);
116
117 if (opts.indent) horizontalLayout->addSpacing(opts.indent);
118
119 int processed{0}, index_start{0};
120 QWidget *last_widget{nullptr};
121 while (true) {
122 int pos = text.indexOf('%', index_start);
123 int idx;
124 if (pos == -1) {
125 pos = text.size();
126 idx = -1;
127 } else {
128 const int pos_next{pos + 1};
129 const auto char_next = text[pos_next];
130 idx = (char_next == 's') ? 0 : (char_next.digitValue() - 1);
131 if (pos_next == text.size() || idx < 0 || idx > 8 || (unsigned)idx >= objs.size()) {
132 index_start = pos_next;
133 continue;
134 }
135 }
136 if (processed != pos) {
137 auto label_text = text.mid(processed, pos - processed);
138 if (auto last_widget_as_qcheckbox = qobject_cast<QCheckBox*>(last_widget)) {
139 if (label_text[0].isSpace()) label_text = label_text.mid(1);
140 last_widget_as_qcheckbox->setText(label_text);
141 } else {
142 const auto label = new QLabel(parent);
143 label->setText(label_text);
144 label->setTextFormat(Qt::PlainText);
145 label->setBuddy(first_o);
146 label->setToolTip(first_o->toolTip());
147 horizontalLayout->addWidget(label);
148 }
149 }
150 if (idx == -1) break;
151 last_widget = objs[idx];
152 horizontalLayout->addWidget(last_widget);
153 index_start = processed = pos + 2;
154 }
155
156 if (opts.stretch) horizontalLayout->addStretch(opts.stretch);
157
158 layout->insertLayout(opts.insert_at, horizontalLayout);
159
160 for (auto& o : objs) {
161 o->setProperty("L", QVariant::fromValue((QLayout*)horizontalLayout));
162 FixTabOrder(o);
163 }
164 }
165
166 void OptionsDialog::CreateOptionUI(QBoxLayout * const layout, const QString& text, const std::vector<QWidget *>& objs)
167 {
168 CreateOptionUI(layout, text, objs, {});
169 }
170
171 void OptionsDialog::CreateOptionUI(QBoxLayout * const layout, QWidget * const o, const QString& text, QBoxLayout *horizontalLayout)
172 {
173 CreateOptionUI(layout, text, {o}, { .horizontal_layout = horizontalLayout, });
174 }
175
176 static void setSiblingsEnabled(QWidget * const o, const bool state)
177 {
178 auto layout = o->property("L").value<QLayout*>();
179 Assert(layout);
180 // NOTE: QLayout::children does not do what we need here
181 for (int i = layout->count(); i-- > 0; ) {
182 QLayoutItem * const layoutitem = layout->itemAt(i);
183 QWidget * const childwidget = layoutitem->widget();
184 if (!childwidget) continue;
185 childwidget->setEnabled(state);
186 }
187 }
188
189 int setFontChoice(QComboBox* cb, const OptionsModel::FontChoice& fc)
190 {
191 int i;
192 for (i = cb->count(); --i >= 0; ) {
193 QVariant item_data = cb->itemData(i);
194 if (!item_data.canConvert<OptionsModel::FontChoice>()) continue;
195 if (item_data.value<OptionsModel::FontChoice>() == fc) {
196 break;
197 }
198 }
199 if (i == -1) {
200 // New item needed
201 QFont chosen_font = OptionsModel::getFontForChoice(fc);
202 QSignalBlocker block_currentindexchanged_signal(cb); // avoid triggering QFontDialog
203 cb->insertItem(0, QFontInfo(chosen_font).family(), QVariant::fromValue(fc));
204 i = 0;
205 }
206
207 cb->setCurrentIndex(i);
208 return i;
209 }
210
211 void setupFontOptions(QComboBox* cb, QLabel* preview)
212 {
213 QFont embedded_font{GUIUtil::fixedPitchFont(true)};
214 QFont system_font{GUIUtil::fixedPitchFont(false)};
215 cb->addItem(QObject::tr("%1").arg(QFontInfo(embedded_font).family()), QVariant::fromValue(OptionsModel::FontChoice{OptionsModel::FontChoiceAbstract::EmbeddedFont}));
216 cb->addItem(QObject::tr("Default system font \"%1\"").arg(QFontInfo(system_font).family()), QVariant::fromValue(OptionsModel::FontChoice{OptionsModel::FontChoiceAbstract::BestSystemFont}));
217 cb->addItem(QObject::tr("Custom…"));
218
219 const auto& on_font_choice_changed = [cb, preview](int index) {
220 static int previous_index = -1;
221 QVariant item_data = cb->itemData(index);
222 QFont f;
223 if (item_data.canConvert<OptionsModel::FontChoice>()) {
224 f = OptionsModel::getFontForChoice(item_data.value<OptionsModel::FontChoice>());
225 } else {
226 bool ok;
227 f = QFontDialog::getFont(&ok, GUIUtil::fixedPitchFont(false), cb->parentWidget());
228 if (!ok) {
229 cb->setCurrentIndex(previous_index);
230 return;
231 }
232 index = setFontChoice(cb, OptionsModel::FontChoice{f});
233 }
234 if (preview) {
235 preview->setFont(f);
236 }
237 previous_index = index;
238 };
239 QObject::connect(cb, QOverload<int>::of(&QComboBox::currentIndexChanged), on_font_choice_changed);
240 on_font_choice_changed(cb->currentIndex());
241 }
242
243 OptionsDialog::OptionsDialog(QWidget* parent, bool enableWallet)
244 : QDialog(parent, GUIUtil::dialog_flags | Qt::WindowMaximizeButtonHint),
245 ui(new Ui::OptionsDialog)
246 {
247 ui->setupUi(this);
248
249 ui->verticalLayout->setStretchFactor(ui->tabWidget, 1);
250
251 /* Main elements init */
252 ui->databaseCache->setRange(MIN_DBCACHE_BYTES / 1_MiB, std::numeric_limits<int>::max());
253 ui->threadsScriptVerif->setMinimum(-GetNumCores());
254 ui->threadsScriptVerif->setMaximum(MAX_SCRIPTCHECK_THREADS);
255 ui->threadsWarning->setVisible(false);
256 ui->threadsWarning->setStyleSheet("QLabel { color: red; }");
257 ui->pruneWarning->setVisible(false);
258 ui->pruneWarning->setStyleSheet("QLabel { color: red; }");
259
260 ui->pruneSizeMiB->setEnabled(false);
261 #if (QT_VERSION >= QT_VERSION_CHECK(6, 7, 0))
262 connect(ui->prune, &QCheckBox::checkStateChanged, [this](const Qt::CheckState state){
263 #else
264 connect(ui->prune, &QCheckBox::stateChanged, [this](const int state){
265 #endif
266 ui->pruneSizeMiB->setEnabled(state == Qt::Checked);
267 });
268
269 ui->networkPort->setValidator(new QIntValidator(1024, 65535, this));
270 connect(ui->networkPort, SIGNAL(textChanged(const QString&)), this, SLOT(checkLineEdit()));
271
272 /* Network elements init */
273 ui->proxyIp->setEnabled(false);
274 ui->proxyPort->setEnabled(false);
275 ui->proxyPort->setValidator(new QIntValidator(1, 65535, this));
276
277 ui->proxyIpTor->setEnabled(false);
278 ui->proxyPortTor->setEnabled(false);
279 ui->proxyPortTor->setValidator(new QIntValidator(1, 65535, this));
280
281 connect(ui->connectSocks, &QPushButton::toggled, ui->proxyIp, &QWidget::setEnabled);
282 connect(ui->connectSocks, &QPushButton::toggled, ui->proxyPort, &QWidget::setEnabled);
283 connect(ui->connectSocks, &QPushButton::toggled, this, &OptionsDialog::updateProxyValidationState);
284
285 connect(ui->connectSocksTor, &QPushButton::toggled, ui->proxyIpTor, &QWidget::setEnabled);
286 connect(ui->connectSocksTor, &QPushButton::toggled, ui->proxyPortTor, &QWidget::setEnabled);
287 connect(ui->connectSocksTor, &QPushButton::toggled, this, &OptionsDialog::updateProxyValidationState);
288
289 ui->maxuploadtarget->setMinimum(144 /* MiB/day */);
290 ui->maxuploadtarget->setMaximum(std::numeric_limits<int>::max());
291 connect(ui->maxuploadtargetCheckbox, SIGNAL(toggled(bool)), ui->maxuploadtarget, SLOT(setEnabled(bool)));
292
293 prevwidget = ui->tabWidget;
294
295 walletrbf = new QCheckBox(ui->tabWallet);
296 walletrbf->setText(tr("Request Replace-By-Fee"));
297 walletrbf->setToolTip(tr("Indicates that the sender may wish to replace this transaction with a new one paying higher fees (prior to being confirmed). Can be overridden per send."));
298 ui->verticalLayout_Wallet->insertWidget(0, walletrbf);
299 FixTabOrder(walletrbf);
300
301 QStyleOptionButton styleoptbtn;
302 const auto checkbox_indent = ui->allowIncoming->style()->subElementRect(QStyle::SE_CheckBoxIndicator, &styleoptbtn, ui->allowIncoming).width();
303
304 /* Network tab */
305 QLayoutItem *spacer = ui->verticalLayout_Network->takeAt(ui->verticalLayout_Network->count() - 1);
306
307 prevwidget = ui->allowIncoming;
308 ui->verticalLayout_Network->removeWidget(ui->mapPortNatpmp);
309 int insert_at = ui->verticalLayout_Network->indexOf(ui->connectSocks);
310 // NOTE: Re-inserted in bottom-to-top order
311 CreateOptionUI(ui->verticalLayout_Network, QStringLiteral("%1"), {ui->mapPortNatpmp}, { .insert_at=insert_at, .indent=checkbox_indent, });
312 upnp = new QCheckBox(ui->tabNetwork);
313 upnp->setText(tr("Automatically configure router(s) that support &UPnP"));
314 upnp->setToolTip(tr("Automatically open the Limenka client port on the router. This only works when your router supports UPnP and it is enabled."));
315 #ifndef USE_UPNP
316 upnp->setEnabled(false);
317 #endif
318 CreateOptionUI(ui->verticalLayout_Network, QStringLiteral("%1"), {upnp}, { .insert_at=insert_at, .indent=checkbox_indent, });
319 connect(ui->allowIncoming, &QPushButton::toggled, upnp, &QWidget::setEnabled);
320 connect(ui->allowIncoming, &QPushButton::toggled, ui->mapPortNatpmp, &QWidget::setEnabled);
321 upnp->setEnabled(ui->allowIncoming->isChecked());
322 ui->mapPortNatpmp->setEnabled(ui->allowIncoming->isChecked());
323
324 prevwidget = dynamic_cast<QWidgetItem*>(ui->verticalLayout_Network->itemAt(ui->verticalLayout_Network->count() - 1))->widget();
325
326 blockreconstructionextratxn = new QSpinBox(ui->tabNetwork);
327 blockreconstructionextratxn->setMinimum(0);
328 blockreconstructionextratxn->setMaximum(std::numeric_limits<int>::max());
329 CreateOptionUI(ui->verticalLayout_Network, blockreconstructionextratxn, tr("Keep at most %s extra transactions in memory for compact block reconstruction"));
330
331 blockreconstructionextratxnsize = new QDoubleSpinBox(ui->tabNetwork);
332 blockreconstructionextratxnsize->setDecimals(0);
333 blockreconstructionextratxnsize->setMinimum(0);
334 blockreconstructionextratxnsize->setMaximum(std::numeric_limits<size_t>::max() / 1'000'000);
335 CreateOptionUI(ui->verticalLayout_Network, blockreconstructionextratxnsize, tr("Limit extra transactions for compact block reconstruction to %s MB"));
336
337 ui->verticalLayout_Network->addItem(spacer);
338
339 prevwidget = ui->peerbloomfilters;
340
341 /* Mempool tab */
342
343 QWidget * const tabMempool = new QWidget();
344 QVBoxLayout * const verticalLayout_Mempool = new QVBoxLayout(tabMempool);
345 ui->tabWidget->insertTab(ui->tabWidget->indexOf(ui->tabWindow), tabMempool, tr("Mem&pool"));
346
347 mempoolreplacement = new QValueComboBox(tabMempool);
348 mempoolreplacement->addItem(QString("never"), QVariant("never"));
349 mempoolreplacement->addItem(QString("with a higher mining fee, and opt-in"), QVariant("fee,optin"));
350 mempoolreplacement->addItem(QString("with a higher mining fee (no opt-out)"), QVariant("fee,-optin"));
351 CreateOptionUI(verticalLayout_Mempool, mempoolreplacement, tr("Transaction &replacement: %s"));
352
353 incrementalrelayfee = new LimenkaAmountField(tabMempool);
354 connect(incrementalrelayfee, SIGNAL(valueChanged()), this, SLOT(incrementalrelayfee_changed()));
355 CreateOptionUI(verticalLayout_Mempool, incrementalrelayfee, tr("Require transaction fees to be at least %s per kvB higher than transactions they are replacing."));
356
357 rejectspkreuse = new QCheckBox(tabMempool);
358 rejectspkreuse->setText(tr("Disallow most address reuse"));
359 rejectspkreuse->setToolTip(tr("With this option enabled, your memory pool will only allow each unique payment destination to be used once, effectively deprioritising address reuse. Address reuse is not technically supported, and harms the privacy of all Limenka users. It also has limited real-world utility, and has been known to be common with spam."));
360 verticalLayout_Mempool->addWidget(rejectspkreuse);
361 FixTabOrder(rejectspkreuse);
362
363 mempooltruc = new QValueComboBox(tabMempool);
364 mempooltruc->addItem(QString("do not relay or mine at all"), QVariant("reject"));
365 mempooltruc->addItem(QString("handle the same as other transactions"), QVariant("accept"));
366 mempooltruc->addItem(QString("impose stricter limits requested"), QVariant("enforce"));
367 mempooltruc->setToolTip(tr("Some transactions signal a request to limit both themselves and other related transactions to more restrictive expectations. Specifically, this would disallow more than 1 unconfirmed predecessor or spending transaction, as well as smaller size limits (see BIP 431 for details), regardless of what policy you have configured."));
368 CreateOptionUI(verticalLayout_Mempool, mempooltruc, tr("Transactions requesting more restrictive policy limits (TRUC): %s"));
369
370 maxorphantx = new QSpinBox(tabMempool);
371 maxorphantx->setMinimum(0);
372 maxorphantx->setMaximum(std::numeric_limits<int>::max());
373 CreateOptionUI(verticalLayout_Mempool, maxorphantx, tr("Keep at most %s unconnected transactions in memory"));
374
375 maxmempool = new QSpinBox(tabMempool);
376 maxmempool->setMinimum(1);
377 maxmempool->setMaximum(std::numeric_limits<int>::max());
378 CreateOptionUI(verticalLayout_Mempool, maxmempool, tr("Keep the transaction memory pool below %s MB"));
379
380 mempoolexpiry = new QSpinBox(tabMempool);
381 mempoolexpiry->setMinimum(1);
382 mempoolexpiry->setMaximum(std::numeric_limits<int>::max());
383 CreateOptionUI(verticalLayout_Mempool, mempoolexpiry, tr("Do not keep transactions in memory more than %s hours"));
384
385 verticalLayout_Mempool->addItem(new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding));
386
387 /* Filters tab */
388
389 QWidget * const tabFilters = new QWidget();
390 auto& groupBox_Spamfiltering = tabFilters;
391 ui->tabWidget->insertTab(ui->tabWidget->indexOf(ui->tabWindow), ModScrollArea::fromWidget(this, groupBox_Spamfiltering), tr("Spam &filtering"));
392 QVBoxLayout * const verticalLayout_Spamfiltering = new QVBoxLayout(groupBox_Spamfiltering);
393
394 rejectunknownscripts = new QCheckBox(groupBox_Spamfiltering);
395 rejectunknownscripts->setText(tr("Ignore unrecognised receiver scripts"));
396 rejectunknownscripts->setToolTip(tr("With this option enabled, unrecognised receiver (\"pubkey\") scripts will be ignored. Unrecognisable scripts could be used to bypass further spam filters. If your software is outdated, they may also be used to trick you into thinking you were sent limenkas that will never confirm."));
397 verticalLayout_Spamfiltering->addWidget(rejectunknownscripts);
398 FixTabOrder(rejectunknownscripts);
399
400 rejectunknownwitness = new QCheckBox(groupBox_Spamfiltering);
401 rejectunknownwitness->setText(tr("Reject unknown witness script versions"));
402 rejectunknownwitness->setToolTip(tr("Some attempts to spam Limenka intentionally use undefined witness script formats reserved for future use. By enabling this option, your node will reject transactions using these undefined/future versions. Note that if you send to many addresses in a single transaction, the entire transaction may be rejected if any single one of them attempts to use an undefined format."));
403 verticalLayout_Spamfiltering->addWidget(rejectunknownwitness);
404 FixTabOrder(rejectunknownwitness);
405
406 rejectparasites = new QCheckBox(groupBox_Spamfiltering);
407 rejectparasites->setText(tr("Reject parasite transactions"));
408 rejectparasites->setToolTip(tr("With this option enabled, transactions related to parasitic overlay protocols will be ignored. Parasites are transactions using Limenka as a technical infrastructure to animate other protocols, unrelated to ordinary money transfers."));
409 verticalLayout_Spamfiltering->addWidget(rejectparasites);
410 FixTabOrder(rejectparasites);
411
412 rejecttokens = new QCheckBox(groupBox_Spamfiltering);
413 rejecttokens->setText(tr("Ignore transactions involving non-limenka token/asset overlay protocols"));
414 rejecttokens->setToolTip(tr("With this option enabled, transactions involving non-limenka tokens/assets will not be relayed or mined by your node. Due to not having value, and some technical design flaws, token mints and transfers are often spammy and can bog down the network."));
415 verticalLayout_Spamfiltering->addWidget(rejecttokens);
416 FixTabOrder(rejecttokens);
417
418 subdustfeepenalty = new QCheckBox(groupBox_Spamfiltering);
419 subdustfeepenalty->setText(tr("Penalize effective fee for sub-dust outputs"));
420 subdustfeepenalty->setToolTip(tr("For each output below the dust threshold, reduce the transaction's effective fee by the difference between the dust threshold and the output value. This makes transactions creating dust outputs require higher fees to be relayed and mined."));
421 verticalLayout_Spamfiltering->addWidget(subdustfeepenalty);
422 FixTabOrder(subdustfeepenalty);
423
424 minrelaytxfee = new LimenkaAmountField(groupBox_Spamfiltering);
425 CreateOptionUI(verticalLayout_Spamfiltering, minrelaytxfee, tr("Ignore transactions offering miners less than %s per kvB in transaction fees."));
426
427 minrelaycoinblocks = new LimenkaAmountField(groupBox_Spamfiltering);
428 minrelaycoinblocks->SetMaxValue(std::numeric_limits<CAmount>::max());
429 minrelaycoinblocks->setToolTip(tr("This effectively acts as a rate limit. When limenkas are spent, they reset to zero \"coinblocks\" (aka coin age) and slowly build up more coinblocks based on their value each block afterward. Small coins take longer than large amounts."));
430 CreateOptionUI(verticalLayout_Spamfiltering, minrelaycoinblocks, tr("Delay accepting transactions spending coins that have been at rest less than %s per block."));
431
432 minrelaymaturity = new QSpinBox(groupBox_Spamfiltering);
433 minrelaymaturity->setMinimum(0);
434 minrelaymaturity->setMaximum(std::numeric_limits<int>::max());
435 minrelaymaturity->setToolTip(tr("This effectively acts as a rate limit. When limenkas are spent, they reset to zero blocks and slowly mature each block afterward, regardless of their value."));
436 CreateOptionUI(verticalLayout_Spamfiltering, minrelaymaturity, tr("Delay accepting transactions spending coins that have been at rest fewer than %s blocks."));
437
438 bytespersigop = new QSpinBox(groupBox_Spamfiltering);
439 bytespersigop->setMinimum(1);
440 bytespersigop->setMaximum(std::numeric_limits<int>::max());
441 CreateOptionUI(verticalLayout_Spamfiltering, bytespersigop, tr("Treat each consensus-counted sigop as at least %s bytes."));
442
443 bytespersigopstrict = new QSpinBox(groupBox_Spamfiltering);
444 bytespersigopstrict->setMinimum(1);
445 bytespersigopstrict->setMaximum(std::numeric_limits<int>::max());
446 CreateOptionUI(verticalLayout_Spamfiltering, bytespersigopstrict, tr("Ignore transactions with fewer than %s bytes per potentially-executed sigop."));
447
448 limitancestorcount = new QSpinBox(groupBox_Spamfiltering);
449 limitancestorcount->setMinimum(1);
450 limitancestorcount->setMaximum(std::numeric_limits<int>::max());
451 CreateOptionUI(verticalLayout_Spamfiltering, limitancestorcount, tr("Ignore transactions with %s or more unconfirmed ancestors."));
452
453 limitancestorsize = new QSpinBox(groupBox_Spamfiltering);
454 limitancestorsize->setMinimum(1);
455 limitancestorsize->setMaximum(std::numeric_limits<int>::max());
456 CreateOptionUI(verticalLayout_Spamfiltering, limitancestorsize, tr("Ignore transactions whose size with all unconfirmed ancestors exceeds %s kilobytes."));
457
458 limitdescendantcount = new QSpinBox(groupBox_Spamfiltering);
459 limitdescendantcount->setMinimum(1);
460 limitdescendantcount->setMaximum(std::numeric_limits<int>::max());
461 CreateOptionUI(verticalLayout_Spamfiltering, limitdescendantcount, tr("Ignore transactions if any ancestor would have %s or more unconfirmed descendants."));
462
463 limitdescendantsize = new QSpinBox(groupBox_Spamfiltering);
464 limitdescendantsize->setMinimum(1);
465 limitdescendantsize->setMaximum(std::numeric_limits<int>::max());
466 CreateOptionUI(verticalLayout_Spamfiltering, limitdescendantsize, tr("Ignore transactions if any ancestor would have more than %s kilobytes of unconfirmed descendants."));
467
468 connect(maxmempool, &QSpinBox::editingFinished, [&]() {
469 const int64_t limitdescendantsize_max_kvB = limitdescendantsizeMaximumVBytes(int64_t{maxmempool->value()} * 1'000'000) / 1'000;
470 if (limitdescendantsize_max_kvB < limitdescendantsize->value()) {
471 if (QMessageBox::question(this, tr("Confirm change"), tr("Decreasing your mempool size to %1 MB requires also decreasing your unconfirmed descendants size limit to %2 kB (currently %3 kB, on the Spam filtering tab).<br><br>Do you wish to make these changes?").arg(maxmempool->value()).arg(limitdescendantsize_max_kvB).arg(limitdescendantsize->value()), QMessageBox::Apply | QMessageBox::Cancel) == QMessageBox::Apply) {
472 limitdescendantsize->setValue(limitdescendantsize_max_kvB);
473 limitdescendantsize->setProperty("pv", (int)limitdescendantsize_max_kvB);
474 } else { // Cancel
475 maxmempool->setValue(maxmempool->property("pv").toInt());
476 return;
477 }
478 }
479 maxmempool->setProperty("pv", maxmempool->value());
480 });
481 connect(limitdescendantsize, &QSpinBox::editingFinished, [&]() {
482 const int maxmempool_min_MB = std::ceil(maxmempoolMinimumBytes(int64_t{limitdescendantsize->value()} * 1'000) / 1'000'000.0);
483 if (maxmempool_min_MB > maxmempool->value()) {
484 if (QMessageBox::question(this, tr("Confirm change"), tr("Increasing your descendant size limit to %1 kB requires also increasing your mempool size to %2 MB (currently %3 MB, on the Mempool tab).<br><br>Do you wish to make these changes?").arg(limitdescendantsize->value()).arg(maxmempool_min_MB).arg(maxmempool->value()), QMessageBox::Apply | QMessageBox::Cancel) == QMessageBox::Apply) {
485 maxmempool->setValue(maxmempool_min_MB);
486 maxmempool->setProperty("pv", maxmempool_min_MB);
487 } else { // Cancel
488 limitdescendantsize->setValue(limitdescendantsize->property("pv").toInt());
489 return;
490 }
491 }
492 limitdescendantsize->setProperty("pv", limitdescendantsize->value());
493 });
494
495 rejectbarepubkey = new QCheckBox(groupBox_Spamfiltering);
496 rejectbarepubkey->setText(tr("Ignore bare/exposed public keys (pay-to-IP)"));
497 rejectbarepubkey->setToolTip(tr("Spam is sometimes disguised to appear as if it is a deprecated pay-to-IP (bare pubkey) transaction, where the \"key\" is actually arbitrary data (not a real key) instead. Support for pay-to-IP was only ever supported by Satoshi's early Limenka wallet, which has been abandoned since 2011."));
498 verticalLayout_Spamfiltering->addWidget(rejectbarepubkey);
499 FixTabOrder(rejectbarepubkey);
500
501 rejectbaremultisig = new QCheckBox(groupBox_Spamfiltering);
502 rejectbaremultisig->setText(tr("Ignore bare/exposed \"multisig\" scripts"));
503 rejectbaremultisig->setToolTip(tr("Spam is sometimes disguised to appear as if it is an old-style N-of-M multi-party transaction, where most of the keys are really bogus. At the same time, legitimate multi-party transactions typically have always used P2SH format (which is not filtered by this option), which is more secure."));
504 verticalLayout_Spamfiltering->addWidget(rejectbaremultisig);
505 FixTabOrder(rejectbaremultisig);
506
507 permitephemeral = new QValueComboBox(tabMempool);
508 permitephemeral->addItem(QString("(no exception allowed)"), QVariant("reject"));
509 permitephemeral->addItem(QString("anchor (recommended)"), QVariant("anchor,-send,-dust"));
510 permitephemeral->addItem(QString("zero-value anchor/send"), QVariant("anchor,send,-dust"));
511 permitephemeral->addItem(QString("zero-value send-only"), QVariant("-anchor,send,-dust"));
512 permitephemeral->addItem(QString("dust send"), QVariant("-anchor,send,dust"));
513 permitephemeral->addItem(QString("dust"), QVariant("anchor,send,dust"));
514 permitephemeral->addItem(QString("dust anchor"), QVariant("anchor,-send,dust"));
515 permitephemeral->setToolTip(tr("For some smart contracts, it is impractical to increase the fee after the transaction is created. For this reason, they may use zero-value \"anchors\" to chain two transactions together, the subsequent transaction simply covering the fee for both. Ordinarily, these anchors might be rejected as dust, so it may make sense to make an exception when they are sent together. Variants of this can however be abused for anti-fungibility attacks and possibly spam."));
516 CreateOptionUI(verticalLayout_Spamfiltering, permitephemeral, tr("Allow transactions to have at most one ephemeral %s output"));
517
518 rejectbareanchor = new QCheckBox(groupBox_Spamfiltering);
519 rejectbareanchor->setText(tr("Reject transactions that only have an anchor"));
520 rejectbareanchor->setToolTip(tr("Anchors are a way to allow fee-bumping smart contract transactions long after they have been created. With this option set, your node will refuse to relay or mine transactions that have only an anchor but no real sends."));
521 verticalLayout_Spamfiltering->addWidget(rejectbareanchor);
522 FixTabOrder(rejectbareanchor);
523
524 maxscriptsize = new QSpinBox(groupBox_Spamfiltering);
525 maxscriptsize->setMinimum(0);
526 maxscriptsize->setMaximum(std::numeric_limits<int>::max());
527 maxscriptsize->setToolTip(tr("There may be rare smart contracts that require a large amount of code, but more often a larger code segment is actually just spam finding new ways to try to evade filtering. 1650 bytes is sometimes considered the high end of what might be normal, usually for N-of-20 multisig."));
528 CreateOptionUI(verticalLayout_Spamfiltering, maxscriptsize, tr("Ignore transactions with smart contract code larger than %s bytes."));
529
530 maxtxlegacysigops = new QSpinBox(groupBox_Spamfiltering);
531 maxtxlegacysigops->setMinimum(1);
532 maxtxlegacysigops->setMaximum(1000000);
533 maxtxlegacysigops->setToolTip(tr("Each signature operation in scripts to spend pre-segwit coins require calculations to be performed on the entire transaction. These \"legacy sigops\" can add up quickly, and there is typically only one per coin spent."));
534 CreateOptionUI(verticalLayout_Spamfiltering, maxtxlegacysigops, tr("Ignore transactions with more than %s \"legacy\" signature operations."));
535
536 datacarriersize = new QSpinBox(groupBox_Spamfiltering);
537 datacarriersize->setMinimum(0);
538 datacarriersize->setMaximum(std::numeric_limits<int>::max());
539 datacarriersize->setToolTip(tr("While Limenka itself does not support attaching arbitrary data to transactions, despite that various methods for disguising it have been devised over the years. Since it is sometimes impractical to detect small spam disguised as ordinary transactions, it is sometimes considered beneficial to tolerate certain kinds of less harmful data attachments."));
540 CreateOptionUI(verticalLayout_Spamfiltering, datacarriersize, tr("Ignore transactions with additional data larger than %s bytes."));
541
542 datacarriercost = new QDoubleSpinBox(groupBox_Spamfiltering);
543 datacarriercost->setDecimals(2);
544 datacarriercost->setStepType(QAbstractSpinBox::DefaultStepType);
545 datacarriercost->setSingleStep(0.25);
546 datacarriercost->setMinimum(0.25);
547 datacarriercost->setMaximum(MAX_BLOCK_SERIALIZED_SIZE);
548 datacarriercost->setToolTip(tr("As an alternative to, or in addition to, limiting the size of disguised data, you can also configure how it is accounted for in comparison to legitimate transaction data. For example, 1 vbyte per actual byte would count it as equivalent to ordinary transaction data; 0.25 vB/B would allow it to benefit from the so-called \"segwit discount\"; or 2 vB/B would establish a bias toward legitimate transactions."));
549 CreateOptionUI(verticalLayout_Spamfiltering, datacarriercost, tr("Weigh embedded data as %s virtual bytes per actual byte."));
550 connect(datacarriercost, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [&](double d){
551 const double w = d * 4;
552 const double wf = floor(w);
553 if (w != wf) datacarriercost->setValue(wf / 4);
554 });
555
556 rejectnonstddatacarrier = new QCheckBox(groupBox_Spamfiltering);
557 rejectnonstddatacarrier->setText(tr("Ignore data embedded with non-standard formats"));
558 rejectnonstddatacarrier->setToolTip(tr("Some attempts to spam Limenka intentionally use non-standard formats in an attempt to bypass the datacarrier limits. Without this option, %1 will attempt to detect these and enforce the intended limits. By enabling this option, your node will ignore these transactions entirely (when detected) even if they fall within the configured limits otherwise."));
559 verticalLayout_Spamfiltering->addWidget(rejectnonstddatacarrier);
560 FixTabOrder(rejectnonstddatacarrier);
561
562 dustrelayfee = new LimenkaAmountField(groupBox_Spamfiltering);
563 CreateOptionUI(verticalLayout_Spamfiltering, dustrelayfee, tr("Ignore transactions with values that would cost more to spend at a fee rate of %s per kvB (\"dust\")."));
564
565 rejectbaredatacarrier = new QCheckBox(groupBox_Spamfiltering);
566 rejectbaredatacarrier->setText(tr("Reject \"transactions\" that are only arbitrary data"));
567 rejectbaredatacarrier->setToolTip(tr("With this option set, arbitrary data will only be permitted as defined above in addition to an otherwise-valid transaction. If there are no real recipients, the transaction will be rejected no matter how little data it includes."));
568 verticalLayout_Spamfiltering->addWidget(rejectbaredatacarrier);
569 FixTabOrder(rejectbaredatacarrier);
570
571
572 dustdynamic_enable = new QCheckBox(groupBox_Spamfiltering);
573 dustdynamic_multiplier = new QDoubleSpinBox(groupBox_Spamfiltering);
574 dustdynamic_multiplier->setDecimals(3);
575 dustdynamic_multiplier->setStepType(QAbstractSpinBox::DefaultStepType);
576 dustdynamic_multiplier->setSingleStep(1);
577 dustdynamic_multiplier->setMinimum(0.001);
578 dustdynamic_multiplier->setMaximum(65);
579 dustdynamic_multiplier->setValue(DEFAULT_DUST_RELAY_MULTIPLIER / 1000.0);
580 CreateOptionUI(verticalLayout_Spamfiltering, tr("%1 Automatically adjust the dust limit upward to %2 times:"), {dustdynamic_enable, dustdynamic_multiplier});
581
582 dustdynamic_target = new QRadioButton(groupBox_Spamfiltering);
583 dustdynamic_target_blocks = new QSpinBox(groupBox_Spamfiltering);
584 dustdynamic_target_blocks->setMinimum(2);
585 dustdynamic_target_blocks->setMaximum(1008); // FIXME: Get this from the fee estimator
586 dustdynamic_target_blocks->setValue(1008);
587 CreateOptionUI(verticalLayout_Spamfiltering, tr("%1 fee estimate for %2 blocks."), {dustdynamic_target, dustdynamic_target_blocks}, { .indent = checkbox_indent, });
588 // FIXME: Make it possible to click labels to select + focus spinbox
589
590 dustdynamic_mempool = new QRadioButton(groupBox_Spamfiltering);
591 dustdynamic_mempool_kvB = new QSpinBox(groupBox_Spamfiltering);
592 dustdynamic_mempool_kvB->setMinimum(1);
593 dustdynamic_mempool_kvB->setMaximum(std::numeric_limits<int32_t>::max());
594 dustdynamic_mempool_kvB->setValue(3024000);
595 CreateOptionUI(verticalLayout_Spamfiltering, tr("%1 the lowest fee of the best known %2 kvB of unconfirmed transactions."), {dustdynamic_mempool, dustdynamic_mempool_kvB}, { .indent = checkbox_indent, });
596
597 const auto dustdynamic_enable_toggled = [this](const bool state){
598 dustdynamic_multiplier->setEnabled(state);
599 setSiblingsEnabled(dustdynamic_target_blocks, state);
600 setSiblingsEnabled(dustdynamic_mempool_kvB, state);
601 if (state) {
602 if (!dustdynamic_mempool->isChecked()) dustdynamic_target->setChecked(true);
603 dustdynamic_target_blocks->setEnabled(dustdynamic_target->isChecked());
604 dustdynamic_mempool_kvB->setEnabled(dustdynamic_mempool->isChecked());
605 }
606 };
607 connect(dustdynamic_enable, &QAbstractButton::toggled, dustdynamic_enable_toggled);
608 dustdynamic_enable_toggled(dustdynamic_enable->isChecked());
609 connect(dustdynamic_target, &QAbstractButton::toggled, [this](const bool state){
610 dustdynamic_target_blocks->setEnabled(state);
611 });
612 connect(dustdynamic_mempool, &QAbstractButton::toggled, [this](const bool state){
613 dustdynamic_mempool_kvB->setEnabled(state);
614 });
615
616
617 connect(rejectunknownscripts, &QAbstractButton::toggled, [this, dustdynamic_enable_toggled](const bool state){
618 rejectunknownwitness->setEnabled(state);
619 rejectbarepubkey->setEnabled(state);
620 rejectbaremultisig->setEnabled(state);
621 permitephemeral->setEnabled(state);
622 rejectbareanchor->setEnabled(state);
623 rejectbaredatacarrier->setEnabled(state);
624 rejectparasites->setEnabled(state);
625 rejecttokens->setEnabled(state);
626 setSiblingsEnabled(dustrelayfee, state);
627 setSiblingsEnabled(maxscriptsize, state);
628 setSiblingsEnabled(maxtxlegacysigops, state);
629 setSiblingsEnabled(dustdynamic_multiplier, state);
630 dustdynamic_enable_toggled(state && dustdynamic_enable->isChecked());
631 });
632
633
634 verticalLayout_Spamfiltering->addStretch(1);
635
636 /* Mining tab */
637
638 QWidget * const tabMining = new QWidget();
639 QVBoxLayout * const verticalLayout_Mining = new QVBoxLayout(tabMining);
640 ui->tabWidget->insertTab(ui->tabWidget->indexOf(ui->tabWindow), tabMining, tr("M&ining"));
641
642 verticalLayout_Mining->addWidget(new QLabel(tr("<strong>Note that mining is heavily influenced by the settings on the Mempool and Spam filtering tabs.</strong>")));
643
644 blockmintxfee = new LimenkaAmountField(tabMining);
645 CreateOptionUI(verticalLayout_Mining, blockmintxfee, tr("Only mine transactions paying a fee of at least %s per kvB."));
646
647 blockmaxsize = new QSpinBox(tabMining);
648 blockmaxsize->setMinimum(1);
649 blockmaxsize->setMaximum((MAX_BLOCK_SERIALIZED_SIZE - 1000) / 1000);
650 connect(blockmaxsize, SIGNAL(valueChanged(int)), this, SLOT(blockmaxsize_changed(int)));
651 CreateOptionUI(verticalLayout_Mining, blockmaxsize, tr("Never mine a block larger than %s kB."));
652
653 blockprioritysize = new QSpinBox(tabMining);
654 blockprioritysize->setMinimum(0);
655 blockprioritysize->setMaximum(blockmaxsize->maximum());
656 connect(blockprioritysize, SIGNAL(valueChanged(int)), this, SLOT(blockmaxsize_increase(int)));
657 CreateOptionUI(verticalLayout_Mining, blockprioritysize, tr("Mine first %s kB of transactions sorted by coin-age priority."));
658
659 blockmaxweight = new QSpinBox(tabMining);
660 blockmaxweight->setMinimum(1);
661 blockmaxweight->setMaximum((MAX_BLOCK_WEIGHT-4000) / 1000);
662 connect(blockmaxweight, SIGNAL(valueChanged(int)), this, SLOT(blockmaxweight_changed(int)));
663 CreateOptionUI(verticalLayout_Mining, blockmaxweight, tr("Never mine a block weighing more than %s kWU."));
664
665 verticalLayout_Mining->addItem(new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding));
666
667 /* Window elements init */
668 #ifdef Q_OS_MACOS
669 /* remove Window tab on Mac */
670 ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->tabWindow));
671 /* hide launch at startup option on macOS */
672 ui->limenkaAtStartup->setVisible(false);
673 ui->verticalLayout_Main->removeWidget(ui->limenkaAtStartup);
674 ui->verticalLayout_Main->removeItem(ui->horizontalSpacer_0_Main);
675 #endif
676
677 /* remove Wallet tab and 3rd party-URL textbox in case of -disablewallet */
678 if (!enableWallet) {
679 ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->tabWallet));
680 ui->thirdPartyTxUrlsLabel->setVisible(false);
681 ui->thirdPartyTxUrls->setVisible(false);
682 } else {
683 for (OutputType type : OUTPUT_TYPES) {
684 const QString& val = QString::fromStdString(FormatOutputType(type));
685 const auto [text, tooltip] = GetOutputTypeDescription(type);
686
687 const auto index = ui->addressType->count();
688 ui->addressType->addItem(text, val);
689 ui->addressType->setItemData(index, tooltip, Qt::ToolTipRole);
690 }
691 }
692
693 #ifdef ENABLE_EXTERNAL_SIGNER
694 ui->externalSignerPath->setToolTip(ui->externalSignerPath->toolTip().arg(CLIENT_NAME));
695 #else
696 //: "External signing" means using devices such as hardware wallets.
697 ui->externalSignerPath->setToolTip(tr("Compiled without external signing support (required for external signing)"));
698 ui->externalSignerPath->setEnabled(false);
699 #endif
700 /* Display elements init */
701 QDir translations(":translations");
702
703 ui->limenkaAtStartup->setToolTip(ui->limenkaAtStartup->toolTip().arg(CLIENT_NAME));
704 ui->limenkaAtStartup->setText(ui->limenkaAtStartup->text().arg(CLIENT_NAME));
705
706 ui->openLimenkaConfButton->setToolTip(ui->openLimenkaConfButton->toolTip().arg(CLIENT_NAME));
707
708 ui->lang->setToolTip(ui->lang->toolTip().arg(CLIENT_NAME));
709 ui->lang->addItem(QString("(") + tr("default") + QString(")"), QVariant(""));
710 for (const QString &langStr : translations.entryList())
711 {
712 QLocale locale(langStr);
713
714 /** check if the locale name consists of 2 parts (language_country) */
715 if(langStr.contains("_"))
716 {
717 /** display language strings as "native language - native country/territory (locale name)", e.g. "Deutsch - Deutschland (de)" */
718 ui->lang->addItem(locale.nativeLanguageName() + QString(" - ") +
719 #if (QT_VERSION >= QT_VERSION_CHECK(6, 2, 0))
720 locale.nativeTerritoryName() +
721 #else
722 locale.nativeCountryName() +
723 #endif
724 QString(" (") + langStr + QString(")"), QVariant(langStr));
725
726 }
727 else
728 {
729 /** display language strings as "native language (locale name)", e.g. "Deutsch (de)" */
730 ui->lang->addItem(locale.nativeLanguageName() + QString(" (") + langStr + QString(")"), QVariant(langStr));
731 }
732 }
733 ui->unit->setModel(new LimenkaUnits(this));
734
735 /* Widget-to-option mapper */
736 mapper = new QDataWidgetMapper(this);
737 mapper->setSubmitPolicy(QDataWidgetMapper::ManualSubmit);
738 mapper->setOrientation(Qt::Vertical);
739
740 GUIUtil::ItemDelegate* delegate = new GUIUtil::ItemDelegate(mapper);
741 connect(delegate, &GUIUtil::ItemDelegate::keyEscapePressed, this, &OptionsDialog::reject);
742 mapper->setItemDelegate(delegate);
743
744 /* setup/change UI elements when proxy IPs are invalid/valid */
745 ui->proxyIp->setCheckValidator(new ProxyAddressValidator(parent));
746 ui->proxyIpTor->setCheckValidator(new ProxyAddressValidator(parent));
747 connect(ui->proxyIp, &QValidatedLineEdit::validationDidChange, this, &OptionsDialog::updateProxyValidationState);
748 connect(ui->proxyIpTor, &QValidatedLineEdit::validationDidChange, this, &OptionsDialog::updateProxyValidationState);
749 connect(ui->proxyPort, &QLineEdit::textChanged, this, &OptionsDialog::updateProxyValidationState);
750 connect(ui->proxyPortTor, &QLineEdit::textChanged, this, &OptionsDialog::updateProxyValidationState);
751
752 if (!QSystemTrayIcon::isSystemTrayAvailable()) {
753 ui->showTrayIcon->setChecked(false);
754 ui->showTrayIcon->setEnabled(false);
755 ui->minimizeToTray->setChecked(false);
756 ui->minimizeToTray->setEnabled(false);
757 }
758
759 setupFontOptions(ui->moneyFont, ui->moneyFont_preview);
760 setupFontOptions(ui->qrFont, ui->qrFont_preview);
761 #ifndef USE_QRCODE
762 ui->qrFontLabel->setVisible(false);
763 ui->qrFont->setVisible(false);
764 ui->qrFont_preview->setVisible(false);
765 #endif
766
767 adjustSize();
768
769 GUIUtil::handleCloseWindowShortcut(this);
770 updateThemeColors();
771 }
772
773 OptionsDialog::~OptionsDialog()
774 {
775 delete ui;
776 }
777
778 void OptionsDialog::setClientModel(ClientModel* client_model)
779 {
780 m_client_model = client_model;
781 }
782
783 void OptionsDialog::setModel(OptionsModel *_model)
784 {
785 this->model = _model;
786
787 if(_model)
788 {
789 /* check if client restart is needed and show persistent message */
790 if (_model->isRestartRequired())
791 showRestartWarning(true);
792
793 static constexpr uint64_t nMinDiskSpace = (MIN_DISK_SPACE_FOR_BLOCK_FILES + MiB_BYTES - 1) / MiB_BYTES;
794 ui->pruneSizeMiB->setRange(nMinDiskSpace, std::numeric_limits<int>::max());
795
796 QString strLabel = _model->getOverriddenByCommandLine();
797 if (strLabel.isEmpty())
798 strLabel = tr("none");
799 ui->overriddenByCommandLineLabel->setText(strLabel);
800
801 mapper->setModel(_model);
802 setMapper();
803 mapper->toFirst();
804
805 const auto& font_for_money = _model->data(_model->index(OptionsModel::FontForMoney, 0), Qt::EditRole).value<OptionsModel::FontChoice>();
806 setFontChoice(ui->moneyFont, font_for_money);
807
808 const auto& font_for_qrcodes = _model->data(_model->index(OptionsModel::FontForQRCodes, 0), Qt::EditRole).value<OptionsModel::FontChoice>();
809 setFontChoice(ui->qrFont, font_for_qrcodes);
810
811 updateDefaultProxyNets();
812
813 maxmempool->setProperty("pv", maxmempool->value());
814 limitdescendantsize->setProperty("pv", limitdescendantsize->value());
815 }
816
817 /* warn when one of the following settings changes by user action (placed here so init via mapper doesn't trigger them) */
818
819 /* Main */
820 connect(ui->prune, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
821 connect(ui->prune, &QCheckBox::clicked, this, &OptionsDialog::togglePruneWarning);
822 connect(ui->pruneSizeMiB, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning);
823 connect(ui->databaseCache, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning);
824 connect(ui->externalSignerPath, &QLineEdit::textChanged, [this]{ showRestartWarning(); });
825 connect(ui->threadsScriptVerif, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning);
826 connect(ui->threadsScriptVerif, qOverload<int>(&QSpinBox::valueChanged), this, [this](int value) {
827 ui->threadsWarning->setVisible(value > GetNumCores());
828 });
829 ui->threadsWarning->setVisible(ui->threadsScriptVerif->value() > GetNumCores());
830 /* Wallet */
831 connect(ui->spendZeroConfChange, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
832 /* Network */
833 connect(ui->networkPort, SIGNAL(textChanged(const QString &)), this, SLOT(showRestartWarning()));
834 connect(ui->allowIncoming, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
835 connect(ui->enableServer, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
836 connect(ui->connectSocks, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
837 connect(ui->connectSocksTor, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
838 connect(ui->peerbloomfilters, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
839 connect(ui->peerblockfilters, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
840 /* Mempool */
841 connect(rejectspkreuse, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
842 /* Display */
843 connect(ui->lang, qOverload<>(&QValueComboBox::valueChanged), [this]{ showRestartWarning(); });
844 connect(ui->thirdPartyTxUrls, &QLineEdit::textChanged, [this]{ showRestartWarning(); });
845 }
846
847 void OptionsDialog::setCurrentTab(OptionsDialog::Tab tab)
848 {
849 QWidget *tab_widget = nullptr;
850 if (tab == OptionsDialog::Tab::TAB_NETWORK) tab_widget = ui->tabNetwork;
851 if (tab == OptionsDialog::Tab::TAB_MAIN) tab_widget = ui->tabMain;
852 if (tab_widget && ui->tabWidget->currentWidget() != tab_widget) {
853 ui->tabWidget->setCurrentWidget(tab_widget);
854 }
855 }
856
857 void OptionsDialog::setMapper()
858 {
859 /* Main */
860 mapper->addMapping(ui->limenkaAtStartup, OptionsModel::StartAtStartup);
861 mapper->addMapping(ui->threadsScriptVerif, OptionsModel::ThreadsScriptVerif);
862 mapper->addMapping(ui->databaseCache, OptionsModel::DatabaseCache);
863
864 const auto prune_checkstate = model->data(model->index(OptionsModel::PruneTristate, 0), Qt::EditRole).value<Qt::CheckState>();
865 if (prune_checkstate == Qt::PartiallyChecked) {
866 ui->prune->setTristate();
867 }
868 ui->prune->setCheckState(prune_checkstate);
869 mapper->addMapping(ui->pruneSizeMiB, OptionsModel::PruneSizeMiB);
870
871 /* Wallet */
872 mapper->addMapping(walletrbf, OptionsModel::walletrbf);
873 mapper->addMapping(ui->addressType, OptionsModel::addresstype);
874 mapper->addMapping(ui->spendZeroConfChange, OptionsModel::SpendZeroConfChange);
875 mapper->addMapping(ui->coinControlFeatures, OptionsModel::CoinControlFeatures);
876 mapper->addMapping(ui->subFeeFromAmount, OptionsModel::SubFeeFromAmount);
877 mapper->addMapping(ui->externalSignerPath, OptionsModel::ExternalSignerPath);
878 mapper->addMapping(ui->m_enable_psbt_controls, OptionsModel::EnablePSBTControls);
879
880 /* Network */
881 mapper->addMapping(ui->networkPort, OptionsModel::NetworkPort);
882 mapper->addMapping(upnp, OptionsModel::MapPortUPnP);
883 mapper->addMapping(ui->mapPortNatpmp, OptionsModel::MapPortNatpmp);
884 mapper->addMapping(ui->allowIncoming, OptionsModel::Listen);
885 mapper->addMapping(ui->enableServer, OptionsModel::Server);
886
887 mapper->addMapping(ui->connectSocks, OptionsModel::ProxyUse);
888 mapper->addMapping(ui->proxyIp, OptionsModel::ProxyIP);
889 mapper->addMapping(ui->proxyPort, OptionsModel::ProxyPort);
890
891 mapper->addMapping(ui->connectSocksTor, OptionsModel::ProxyUseTor);
892 mapper->addMapping(ui->proxyIpTor, OptionsModel::ProxyIPTor);
893 mapper->addMapping(ui->proxyPortTor, OptionsModel::ProxyPortTor);
894
895 int current_maxuploadtarget = model->data(model->index(OptionsModel::maxuploadtarget, 0), Qt::EditRole).toInt();
896 if (current_maxuploadtarget == 0) {
897 ui->maxuploadtargetCheckbox->setChecked(false);
898 ui->maxuploadtarget->setEnabled(false);
899 ui->maxuploadtarget->setValue(ui->maxuploadtarget->minimum());
900 } else {
901 if (current_maxuploadtarget < ui->maxuploadtarget->minimum()) {
902 ui->maxuploadtarget->setMinimum(current_maxuploadtarget);
903 }
904 ui->maxuploadtargetCheckbox->setChecked(true);
905 ui->maxuploadtarget->setEnabled(true);
906 ui->maxuploadtarget->setValue(current_maxuploadtarget);
907 }
908
909 mapper->addMapping(ui->peerbloomfilters, OptionsModel::peerbloomfilters);
910 mapper->addMapping(ui->peerblockfilters, OptionsModel::peerblockfilters);
911 if (prune_checkstate != Qt::Unchecked && !GetBlockFilterIndex(BlockFilterType::BASIC)) {
912 // Once pruning begins, it's too late to enable block filters, and doing so will prevent starting the client
913 // Rather than try to monitor sync state, just disable the option once pruning is enabled
914 // Advanced users can override this manually anyway
915 ui->peerblockfilters->setEnabled(false);
916 ui->peerblockfilters->setToolTip(ui->peerblockfilters->toolTip() + " " + tr("(only available if enabled at least once before turning on pruning)"));
917 }
918
919 mapper->addMapping(blockreconstructionextratxn, OptionsModel::blockreconstructionextratxn);
920 mapper->addMapping(blockreconstructionextratxnsize, OptionsModel::blockreconstructionextratxnsize);
921
922 /* Mempool tab */
923
924 QVariant current_mempoolreplacement = model->data(model->index(OptionsModel::mempoolreplacement, 0), Qt::EditRole);
925 int current_mempoolreplacement_index = mempoolreplacement->findData(current_mempoolreplacement);
926 if (current_mempoolreplacement_index == -1) {
927 mempoolreplacement->addItem(current_mempoolreplacement.toString(), current_mempoolreplacement);
928 current_mempoolreplacement_index = mempoolreplacement->count() - 1;
929 }
930 mempoolreplacement->setCurrentIndex(current_mempoolreplacement_index);
931
932 QVariant current_mempooltruc = model->data(model->index(OptionsModel::mempooltruc, 0), Qt::EditRole);
933 int current_mempooltruc_index = mempooltruc->findData(current_mempooltruc);
934 if (current_mempooltruc_index == -1) {
935 mempooltruc->addItem(current_mempooltruc.toString(), current_mempooltruc);
936 current_mempooltruc_index = mempooltruc->count() - 1;
937 }
938 mempooltruc->setCurrentIndex(current_mempooltruc_index);
939
940 mapper->addMapping(maxorphantx, OptionsModel::maxorphantx);
941 mapper->addMapping(maxmempool, OptionsModel::maxmempool);
942 mapper->addMapping(incrementalrelayfee, OptionsModel::incrementalrelayfee);
943 mapper->addMapping(mempoolexpiry, OptionsModel::mempoolexpiry);
944
945 mapper->addMapping(rejectunknownscripts, OptionsModel::rejectunknownscripts);
946 mapper->addMapping(rejectunknownwitness, OptionsModel::rejectunknownwitness);
947 mapper->addMapping(rejectparasites, OptionsModel::rejectparasites);
948 mapper->addMapping(rejecttokens, OptionsModel::rejecttokens);
949 mapper->addMapping(subdustfeepenalty, OptionsModel::subdustfeepenalty);
950 mapper->addMapping(rejectspkreuse, OptionsModel::rejectspkreuse);
951 mapper->addMapping(minrelaytxfee, OptionsModel::minrelaytxfee);
952 mapper->addMapping(minrelaycoinblocks, OptionsModel::minrelaycoinblocks);
953 mapper->addMapping(minrelaymaturity, OptionsModel::minrelaymaturity);
954 mapper->addMapping(bytespersigop, OptionsModel::bytespersigop);
955 mapper->addMapping(bytespersigopstrict, OptionsModel::bytespersigopstrict);
956 mapper->addMapping(limitancestorcount, OptionsModel::limitancestorcount);
957 mapper->addMapping(limitancestorsize, OptionsModel::limitancestorsize);
958 mapper->addMapping(limitdescendantcount, OptionsModel::limitdescendantcount);
959 mapper->addMapping(limitdescendantsize, OptionsModel::limitdescendantsize);
960 mapper->addMapping(rejectbarepubkey, OptionsModel::rejectbarepubkey);
961 mapper->addMapping(rejectbaremultisig, OptionsModel::rejectbaremultisig);
962 mapper->addMapping(rejectbareanchor, OptionsModel::rejectbareanchor);
963 mapper->addMapping(rejectbaredatacarrier, OptionsModel::rejectbaredatacarrier);
964 mapper->addMapping(maxscriptsize, OptionsModel::maxscriptsize);
965 mapper->addMapping(maxtxlegacysigops, OptionsModel::maxtxlegacysigops);
966 mapper->addMapping(datacarriercost, OptionsModel::datacarriercost);
967 mapper->addMapping(datacarriersize, OptionsModel::datacarriersize);
968 mapper->addMapping(rejectnonstddatacarrier, OptionsModel::rejectnonstddatacarrier);
969 mapper->addMapping(dustrelayfee, OptionsModel::dustrelayfee);
970
971 QVariant current_permitephemeral = model->data(model->index(OptionsModel::permitephemeral, 0), Qt::EditRole);
972 int current_permitephemeral_index = permitephemeral->findData(current_permitephemeral);
973 if (current_permitephemeral_index == -1) {
974 permitephemeral->addItem(current_permitephemeral.toString(), current_permitephemeral);
975 current_permitephemeral_index = permitephemeral->count() - 1;
976 }
977 permitephemeral->setCurrentIndex(current_permitephemeral_index);
978
979 QVariant current_dustdynamic = model->data(model->index(OptionsModel::dustdynamic, 0), Qt::EditRole);
980 const util::Result<std::pair<int32_t, int>> parsed_dustdynamic = ParseDustDynamicOpt(current_dustdynamic.toString().toStdString(), std::numeric_limits<unsigned int>::max());
981 if (parsed_dustdynamic) {
982 if (parsed_dustdynamic->first == 0) {
983 dustdynamic_enable->setChecked(false);
984 } else {
985 dustdynamic_multiplier->setValue(parsed_dustdynamic->second / 1000.0);
986 if (parsed_dustdynamic->first < 0) {
987 dustdynamic_target->setChecked(true);
988 dustdynamic_target_blocks->setValue(-parsed_dustdynamic->first);
989 } else {
990 dustdynamic_mempool->setChecked(true);
991 dustdynamic_mempool_kvB->setValue(parsed_dustdynamic->first);
992 }
993 dustdynamic_enable->setChecked(true);
994 }
995 }
996
997 /* Mining tab */
998
999 mapper->addMapping(blockmintxfee, OptionsModel::blockmintxfee);
1000 mapper->addMapping(blockmaxsize, OptionsModel::blockmaxsize);
1001 mapper->addMapping(blockprioritysize, OptionsModel::blockprioritysize);
1002 mapper->addMapping(blockmaxweight, OptionsModel::blockmaxweight);
1003
1004 /* Window */
1005 #ifndef Q_OS_MACOS
1006 if (QSystemTrayIcon::isSystemTrayAvailable()) {
1007 mapper->addMapping(ui->showTrayIcon, OptionsModel::ShowTrayIcon);
1008 mapper->addMapping(ui->minimizeToTray, OptionsModel::MinimizeToTray);
1009 }
1010 mapper->addMapping(ui->minimizeOnClose, OptionsModel::MinimizeOnClose);
1011 #endif
1012
1013 /* Display */
1014 mapper->addMapping(ui->peersTabAlternatingRowColors, OptionsModel::PeersTabAlternatingRowColors);
1015 mapper->addMapping(ui->lang, OptionsModel::Language);
1016 mapper->addMapping(ui->unit, OptionsModel::DisplayUnit);
1017 mapper->addMapping(ui->displayAddresses, OptionsModel::DisplayAddresses);
1018 mapper->addMapping(ui->thirdPartyTxUrls, OptionsModel::ThirdPartyTxUrls);
1019 }
1020
1021 void OptionsDialog::checkLineEdit()
1022 {
1023 QLineEdit * const lineedit = qobject_cast<QLineEdit*>(QObject::sender());
1024 if (lineedit->hasAcceptableInput()) {
1025 lineedit->setStyleSheet("");
1026 } else {
1027 // Check the line edit's actual background to choose appropriate warning color
1028 const bool lineedit_dark = GUIUtil::isDarkMode(lineedit->palette().color(lineedit->backgroundRole()));
1029 const QColor lineedit_warning = lineedit_dark ? QColor("#FF8080") : QColor("#FF0000");
1030 lineedit->setStyleSheet(QStringLiteral("color: %1;").arg(lineedit_warning.name()));
1031 }
1032 }
1033
1034 void OptionsDialog::setOkButtonState(bool fState)
1035 {
1036 ui->okButton->setEnabled(fState);
1037 }
1038
1039 void OptionsDialog::incrementalrelayfee_changed()
1040 {
1041 if (incrementalrelayfee->value() > minrelaytxfee->value()) {
1042 minrelaytxfee->setValue(incrementalrelayfee->value());
1043 }
1044 }
1045
1046 void OptionsDialog::blockmaxsize_changed(int i)
1047 {
1048 if (blockprioritysize->value() > i) {
1049 blockprioritysize->setValue(i);
1050 }
1051
1052 if (blockmaxweight->value() < i) {
1053 blockmaxweight->setValue(i);
1054 } else if (blockmaxweight->value() > i * WITNESS_SCALE_FACTOR) {
1055 blockmaxweight->setValue(i * WITNESS_SCALE_FACTOR);
1056 }
1057 }
1058
1059 void OptionsDialog::blockmaxsize_increase(int i)
1060 {
1061 if (blockmaxsize->value() < i) {
1062 blockmaxsize->setValue(i);
1063 }
1064 }
1065
1066 void OptionsDialog::blockmaxweight_changed(int i)
1067 {
1068 if (blockmaxsize->value() < i / WITNESS_SCALE_FACTOR) {
1069 blockmaxsize->setValue(i / WITNESS_SCALE_FACTOR);
1070 } else if (blockmaxsize->value() > i) {
1071 blockmaxsize->setValue(i);
1072 }
1073 }
1074
1075 void OptionsDialog::on_resetButton_clicked()
1076 {
1077 if (model) {
1078 // confirmation dialog
1079 /*: Text explaining that the settings changed will not come into effect
1080 until the client is restarted. */
1081 QString reset_dialog_text = tr("Client restart required to activate changes.") + "<br><br>";
1082 /*: Text explaining to the user that the client's current settings
1083 will be backed up at a specific location. %1 is a stand-in
1084 argument for the backup location's path. */
1085 reset_dialog_text.append(tr("Current settings will be backed up at \"%1\".").arg(m_client_model->dataDir()) + "<br><br>");
1086 /*: Text asking the user to confirm if they would like to proceed
1087 with a client shutdown. */
1088 reset_dialog_text.append(tr("Client will be shut down. Do you want to proceed?"));
1089 //: Window title text of pop-up window shown when the user has chosen to reset options.
1090 QStringList items;
1091 QString strPrefix = tr("Use policy defaults for %1");
1092 items << strPrefix.arg(tr(CLIENT_NAME));
1093 items << strPrefix.arg(tr("Limenka")+" ");
1094
1095 QInputDialog dialog(this);
1096 dialog.setWindowTitle(tr("Confirm options reset"));
1097 dialog.setLabelText(reset_dialog_text);
1098 dialog.setComboBoxItems(items);
1099 dialog.setTextValue(items[0]);
1100 dialog.setComboBoxEditable(false);
1101
1102 if (!dialog.exec()) {
1103 return;
1104 }
1105
1106 /* reset all options and close GUI */
1107 model->Reset();
1108 model->setData(model->index(OptionsModel::corepolicy, 0), items.indexOf(dialog.textValue()));
1109 close();
1110 Q_EMIT quitOnReset();
1111 }
1112 }
1113
1114 void OptionsDialog::on_openLimenkaConfButton_clicked()
1115 {
1116 QMessageBox config_msgbox(this);
1117 config_msgbox.setIcon(QMessageBox::Information);
1118 //: Window title text of pop-up box that allows opening up of configuration file.
1119 config_msgbox.setWindowTitle(tr("Configuration options"));
1120 /*: Explanatory text about the priority order of instructions considered by client.
1121 The order from high to low being: command-line, configuration file, GUI settings. */
1122 config_msgbox.setText(tr("The configuration file is used to specify advanced user options which override GUI settings. "
1123 "Additionally, any command-line options will override this configuration file."));
1124
1125 QPushButton* open_button = config_msgbox.addButton(tr("Continue"), QMessageBox::ActionRole);
1126 config_msgbox.addButton(tr("Cancel"), QMessageBox::RejectRole);
1127 open_button->setDefault(true);
1128
1129 config_msgbox.exec();
1130
1131 if (config_msgbox.clickedButton() != open_button) return;
1132
1133 /* show an error if there was some problem opening the file */
1134 if (!GUIUtil::openLimenkaConf())
1135 QMessageBox::critical(this, tr("Error"), tr("The configuration file could not be opened."));
1136 }
1137
1138 void OptionsDialog::on_okButton_clicked()
1139 {
1140 for (int i = 0; i < ui->tabWidget->count(); ++i) {
1141 QWidget * const tab = ui->tabWidget->widget(i);
1142 Q_FOREACH(QObject* o, tab->children()) {
1143 QLineEdit * const lineedit = qobject_cast<QLineEdit*>(o);
1144 if (lineedit && !lineedit->hasAcceptableInput()) {
1145 int row = mapper->mappedSection(lineedit);
1146 if (model->data(model->index(row, 0), Qt::EditRole) == lineedit->text()) {
1147 // Allow unchanged fields through
1148 continue;
1149 }
1150 ui->tabWidget->setCurrentWidget(tab);
1151 lineedit->setFocus(Qt::OtherFocusReason);
1152 lineedit->selectAll();
1153 QMessageBox::critical(this, tr("Invalid setting"), tr("The value entered is invalid."));
1154 return;
1155 }
1156 }
1157 }
1158
1159 model->setData(model->index(OptionsModel::PruneTristate, 0), ui->prune->checkState());
1160
1161 model->setData(model->index(OptionsModel::FontForMoney, 0), ui->moneyFont->itemData(ui->moneyFont->currentIndex()));
1162 model->setData(model->index(OptionsModel::FontForQRCodes, 0), ui->qrFont->itemData(ui->qrFont->currentIndex()));
1163
1164 if (ui->maxuploadtargetCheckbox->isChecked()) {
1165 model->setData(model->index(OptionsModel::maxuploadtarget, 0), ui->maxuploadtarget->value());
1166 } else {
1167 model->setData(model->index(OptionsModel::maxuploadtarget, 0), 0);
1168 }
1169
1170 model->setData(model->index(OptionsModel::mempoolreplacement, 0), mempoolreplacement->itemData(mempoolreplacement->currentIndex()));
1171 model->setData(model->index(OptionsModel::mempooltruc, 0), mempooltruc->itemData(mempooltruc->currentIndex()));
1172 model->setData(model->index(OptionsModel::permitephemeral, 0), permitephemeral->itemData(permitephemeral->currentIndex()));
1173
1174 if (dustdynamic_enable->isChecked()) {
1175 if (dustdynamic_target->isChecked()) {
1176 model->setData(model->index(OptionsModel::dustdynamic, 0), QStringLiteral("%2*target:%1").arg(dustdynamic_target_blocks->value()).arg(dustdynamic_multiplier->value()));
1177 } else if (dustdynamic_mempool->isChecked()) {
1178 model->setData(model->index(OptionsModel::dustdynamic, 0), QStringLiteral("%2*mempool:%1").arg(dustdynamic_mempool_kvB->value()).arg(dustdynamic_multiplier->value()));
1179 }
1180 } else {
1181 model->setData(model->index(OptionsModel::dustdynamic, 0), "off");
1182 }
1183
1184 mapper->submit();
1185 accept();
1186 updateDefaultProxyNets();
1187 }
1188
1189 void OptionsDialog::on_cancelButton_clicked()
1190 {
1191 reject();
1192 }
1193
1194 void OptionsDialog::on_showTrayIcon_stateChanged(int state)
1195 {
1196 if (state == Qt::Checked) {
1197 ui->minimizeToTray->setEnabled(true);
1198 } else {
1199 ui->minimizeToTray->setChecked(false);
1200 ui->minimizeToTray->setEnabled(false);
1201 }
1202 }
1203
1204 void OptionsDialog::changeEvent(QEvent* e)
1205 {
1206 if (e->type() == QEvent::PaletteChange) {
1207 updateThemeColors();
1208 }
1209
1210 QWidget::changeEvent(e);
1211 }
1212
1213 void OptionsDialog::togglePruneWarning(bool enabled)
1214 {
1215 ui->pruneWarning->setVisible(!ui->pruneWarning->isVisible());
1216 }
1217
1218 void OptionsDialog::showRestartWarning(bool fPersistent)
1219 {
1220 if(fPersistent)
1221 {
1222 ui->statusLabel->setText(tr("Client restart required to activate changes."));
1223 }
1224 else
1225 {
1226 ui->statusLabel->setText(tr("This change would require a client restart."));
1227 // clear non-persistent status label after 10 seconds
1228 // Todo: should perhaps be a class attribute, if we extend the use of statusLabel
1229 QTimer::singleShot(10s, this, &OptionsDialog::clearStatusLabel);
1230 }
1231 }
1232
1233 void OptionsDialog::clearStatusLabel()
1234 {
1235 ui->statusLabel->clear();
1236 if (model && model->isRestartRequired()) {
1237 showRestartWarning(true);
1238 }
1239 }
1240
1241 void OptionsDialog::updateProxyValidationState()
1242 {
1243 QValidatedLineEdit *pUiProxyIp = ui->proxyIp;
1244 QValidatedLineEdit *otherProxyWidget = (pUiProxyIp == ui->proxyIpTor) ? ui->proxyIp : ui->proxyIpTor;
1245 if (pUiProxyIp->isValid() && (!ui->proxyPort->isEnabled() || ui->proxyPort->text().toInt() > 0) && (!ui->proxyPortTor->isEnabled() || ui->proxyPortTor->text().toInt() > 0))
1246 {
1247 setOkButtonState(otherProxyWidget->isValid()); //only enable ok button if both proxies are valid
1248 clearStatusLabel();
1249 }
1250 else
1251 {
1252 setOkButtonState(false);
1253 ui->statusLabel->setText(tr("The supplied proxy address is invalid."));
1254 }
1255 }
1256
1257 void OptionsDialog::updateDefaultProxyNets()
1258 {
1259 std::string proxyIpText{ui->proxyIp->text().toStdString()};
1260 if (!IsUnixSocketPath(proxyIpText)) {
1261 const std::optional<CNetAddr> ui_proxy_netaddr{LookupHost(proxyIpText, /*fAllowLookup=*/false)};
1262 const CService ui_proxy{ui_proxy_netaddr.value_or(CNetAddr{}), ui->proxyPort->text().toUShort()};
1263 proxyIpText = ui_proxy.ToStringAddrPort();
1264 }
1265
1266 Proxy proxy;
1267 bool has_proxy;
1268
1269 has_proxy = model->node().getProxy(NET_IPV4, proxy);
1270 ui->proxyReachIPv4->setChecked(has_proxy && proxy.ToString() == proxyIpText);
1271
1272 has_proxy = model->node().getProxy(NET_IPV6, proxy);
1273 ui->proxyReachIPv6->setChecked(has_proxy && proxy.ToString() == proxyIpText);
1274
1275 has_proxy = model->node().getProxy(NET_ONION, proxy);
1276 ui->proxyReachTor->setChecked(has_proxy && proxy.ToString() == proxyIpText);
1277 }
1278
1279 void OptionsDialog::updateThemeColors()
1280 {
1281 // Detect dark mode for color palette selection
1282 const bool dark_mode = GUIUtil::isDarkMode(palette().color(backgroundRole()));
1283
1284 // set message warning color based on dark mode
1285 const QColor warning_color = dark_mode ? QColor("#FF8080") : QColor("#FF0000");
1286 ui->pruneWarning->setStyleSheet(QStringLiteral("QLabel { color: %1; }").arg(warning_color.name()));
1287 ui->statusLabel->setStyleSheet(QStringLiteral("QLabel { color: %1; }").arg(warning_color.name()));
1288
1289 // Update networkPort line edit color if it has validation errors
1290 if (!ui->networkPort->hasAcceptableInput()) {
1291 // Check networkPort's actual background for appropriate warning color
1292 const bool networkport_dark = GUIUtil::isDarkMode(ui->networkPort->palette().color(ui->networkPort->backgroundRole()));
1293 const QColor networkport_warning = networkport_dark ? QColor("#FF8080") : QColor("#FF0000");
1294 ui->networkPort->setStyleSheet(QStringLiteral("color: %1;").arg(networkport_warning.name()));
1295 }
1296 }
1297
1298 ProxyAddressValidator::ProxyAddressValidator(QObject *parent) :
1299 QValidator(parent)
1300 {
1301 }
1302
1303 QValidator::State ProxyAddressValidator::validate(QString &input, int &pos) const
1304 {
1305 Q_UNUSED(pos);
1306 uint16_t port{0};
1307 std::string hostname;
1308 if (!SplitHostPort(input.toStdString(), port, hostname) || port != 0) return QValidator::Invalid;
1309
1310 CService serv(LookupNumeric(input.toStdString(), DEFAULT_GUI_PROXY_PORT));
1311 Proxy addrProxy = Proxy(serv, true);
1312 if (addrProxy.IsValid())
1313 return QValidator::Acceptable;
1314
1315 return QValidator::Invalid;
1316 }
1317