optionsmodel.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/optionsmodel.h>
8
9 #include <qt/limenkaunits.h>
10 #include <qt/guiconstants.h>
11 #include <qt/guiutil.h>
12 #include <qt/tonalutils.h>
13
14 #include <chainparams.h>
15 #include <consensus/params.h>
16 #include <common/args.h>
17 #include <consensus/consensus.h>
18 #include <index/blockfilterindex.h>
19 #include <interfaces/node.h>
20 #include <kernel/mempool_options.h> // for DEFAULT_MAX_MEMPOOL_SIZE_MB, DEFAULT_MEMPOOL_EXPIRY_HOURS
21 #include <mapport.h>
22 #include <policy/settings.h>
23 #include <net.h>
24 #include <net_processing.h>
25 #include <netbase.h>
26 #include <node/caches.h>
27 #include <node/dbcache.h>
28 #include <node/chainstatemanager_args.h>
29 #include <node/context.h>
30 #include <node/mempool_args.h> // for ParseDustDynamicOpt
31 #include <outputtype.h>
32 #include <policy/settings.h>
33 #include <util/moneystr.h> // for FormatMoney
34 #include <util/string.h>
35 #include <validation.h> // For DEFAULT_SCRIPTCHECK_THREADS
36 #include <wallet/wallet.h> // For DEFAULT_SPEND_ZEROCONF_CHANGE
37
38 #ifdef ENABLE_WALLET
39 #include <interfaces/wallet.h>
40 #endif
41
42 #include <chrono>
43 #include <string>
44 #include <unordered_set>
45 #include <utility>
46
47 #include <QDebug>
48 #include <QLatin1Char>
49 #include <QSettings>
50 #include <QStringList>
51 #include <QVariant>
52
53 #include <univalue.h>
54
55 const char *DEFAULT_GUI_PROXY_HOST = "127.0.0.1";
56
57 static QString GetDefaultProxyAddress();
58
59 /** Map GUI option ID to node setting name. */
60 static const char* SettingName(OptionsModel::OptionID option)
61 {
62 switch (option) {
63 case OptionsModel::DatabaseCache: return "dbcache";
64 case OptionsModel::ThreadsScriptVerif: return "par";
65 case OptionsModel::SpendZeroConfChange: return "spendzeroconfchange";
66 case OptionsModel::ExternalSignerPath: return "signer";
67 case OptionsModel::MapPortUPnP: return "upnp";
68 case OptionsModel::MapPortNatpmp: return "natpmp";
69 case OptionsModel::Listen: return "listen";
70 case OptionsModel::Server: return "server";
71 case OptionsModel::walletrbf: return "walletrbf";
72 case OptionsModel::addresstype: return "addresstype";
73 case OptionsModel::PruneSizeMiB: return "prune";
74 case OptionsModel::PruneTristate: return "prune";
75 case OptionsModel::ProxyIP: return "proxy";
76 case OptionsModel::ProxyPort: return "proxy";
77 case OptionsModel::ProxyUse: return "proxy";
78 case OptionsModel::ProxyIPTor: return "onion";
79 case OptionsModel::ProxyPortTor: return "onion";
80 case OptionsModel::ProxyUseTor: return "onion";
81 case OptionsModel::Language: return "lang";
82 case OptionsModel::maxuploadtarget: return "maxuploadtarget";
83 case OptionsModel::peerbloomfilters: return "peerbloomfilters";
84 case OptionsModel::peerblockfilters: return "peerblockfilters";
85 case OptionsModel::mempoolreplacement: return "mempoolreplacement";
86 case OptionsModel::mempooltruc: return "mempooltruc";
87 case OptionsModel::maxorphantx: return "maxorphantx";
88 case OptionsModel::maxmempool: return "maxmempool";
89 case OptionsModel::incrementalrelayfee: return "incrementalrelayfee";
90 case OptionsModel::mempoolexpiry: return "mempoolexpiry";
91 case OptionsModel::rejectunknownscripts: return "rejectunknownscripts";
92 case OptionsModel::rejectunknownwitness: return "rejectunknownwitness";
93 case OptionsModel::rejectparasites: return "rejectparasites";
94 case OptionsModel::rejecttokens: return "rejecttokens";
95 case OptionsModel::subdustfeepenalty: return "subdustfeepenalty";
96 case OptionsModel::rejectspkreuse: return "rejectspkreuse";
97 case OptionsModel::minrelaytxfee: return "minrelaytxfee";
98 case OptionsModel::minrelaycoinblocks: return "minrelaycoinblocks";
99 case OptionsModel::minrelaymaturity: return "minrelaymaturity";
100 case OptionsModel::bytespersigop: return "bytespersigop";
101 case OptionsModel::bytespersigopstrict: return "bytespersigopstrict";
102 case OptionsModel::limitancestorcount: return "limitancestorcount";
103 case OptionsModel::limitancestorsize: return "limitancestorsize";
104 case OptionsModel::limitdescendantcount: return "limitdescendantcount";
105 case OptionsModel::limitdescendantsize: return "limitdescendantsize";
106 case OptionsModel::rejectbarepubkey: return "rejectbarepubkey";
107 case OptionsModel::rejectbaremultisig: return "rejectbaremultisig";
108 case OptionsModel::permitephemeral: return "permitephemeral";
109 case OptionsModel::rejectbareanchor: return "rejectbareanchor";
110 case OptionsModel::rejectbaredatacarrier: return "rejectbaredatacarrier";
111 case OptionsModel::maxscriptsize: return "maxscriptsize";
112 case OptionsModel::maxtxlegacysigops: return "maxtxlegacysigops";
113 case OptionsModel::datacarriercost: return "datacarriercost";
114 case OptionsModel::datacarriersize: return "datacarriersize";
115 case OptionsModel::rejectnonstddatacarrier: return "rejectnonstddatacarrier";
116 case OptionsModel::dustrelayfee: return "dustrelayfee";
117 case OptionsModel::dustdynamic: return "dustdynamic";
118 case OptionsModel::blockmintxfee: return "blockmintxfee";
119 case OptionsModel::blockmaxsize: return "blockmaxsize";
120 case OptionsModel::blockprioritysize: return "blockprioritysize";
121 case OptionsModel::blockmaxweight: return "blockmaxweight";
122 case OptionsModel::blockreconstructionextratxn: return "blockreconstructionextratxn";
123 case OptionsModel::blockreconstructionextratxnsize: return "blockreconstructionextratxnsize";
124 default: throw std::logic_error(strprintf("GUI option %i has no corresponding node setting.", option));
125 }
126 }
127
128 /** Call node.updateRwSetting() with Limenka 22.x workaround. */
129 static void UpdateRwSetting(interfaces::Node& node, OptionsModel::OptionID option, const std::string& suffix, const common::SettingsValue& value)
130 {
131 if (value.isNum() &&
132 (option == OptionsModel::DatabaseCache ||
133 option == OptionsModel::ThreadsScriptVerif ||
134 option == OptionsModel::PruneTristate ||
135 option == OptionsModel::PruneSizeMiB)) {
136 // Write certain old settings as strings, even though they are numbers,
137 // because Limenka 22.x releases try to read these specific settings as
138 // strings in addOverriddenOption() calls at startup, triggering
139 // uncaught exceptions in UniValue::get_str(). These errors were fixed
140 // in later releases by https://github.com/limenka/limenka/pull/24498.
141 // If new numeric settings are added, they can be written as numbers
142 // instead of strings, because limenka 22.x will not try to read these.
143 node.updateRwSetting(SettingName(option) + suffix, value.getValStr());
144 } else {
145 node.updateRwSetting(SettingName(option) + suffix, value);
146 }
147 }
148
149 //! Convert enabled/size values to limenka -prune setting.
150 static common::SettingsValue PruneSettingFromMiB(Qt::CheckState prune_enabled, int prune_size_mib)
151 {
152 assert(prune_enabled != Qt::Checked || prune_size_mib >= 1); // PruneSizeMiB and ParsePruneSizeMiB never return less
153 switch (prune_enabled) {
154 case Qt::Unchecked:
155 return 0;
156 case Qt::PartiallyChecked:
157 return 1;
158 default:
159 return prune_size_mib;
160 }
161 }
162
163 //! Get pruning enabled value to show in GUI from limenka -prune setting.
164 static bool PruneEnabled(const common::SettingsValue& prune_setting)
165 {
166 // -prune=1 setting is manual pruning mode, so disabled for purposes of the gui
167 return SettingToInt(prune_setting, 0) > 1;
168 }
169
170 //! Get pruning enabled value to show in GUI from limenka -prune setting.
171 static Qt::CheckState PruneSettingAsTristate(const common::SettingsValue& prune_setting)
172 {
173 switch (SettingToInt(prune_setting, 0)) {
174 case 0:
175 return Qt::Unchecked;
176 case 1:
177 return Qt::PartiallyChecked;
178 default:
179 return Qt::Checked;
180 }
181 }
182
183 //! Get pruning size value to show in GUI from limenka -prune setting. If
184 //! pruning is not enabled, just show default recommended pruning size (2GB).
185 static int PruneSizeAsMiB(const common::SettingsValue& prune_setting)
186 {
187 int value = SettingToInt(prune_setting, 0);
188 return value > 1 ? value : DEFAULT_PRUNE_TARGET_MiB;
189 }
190
191 struct ProxySetting {
192 bool is_set;
193 QString ip;
194 QString port;
195 };
196 static ProxySetting ParseProxyString(const std::string& proxy);
197 static std::string ProxyString(bool is_set, QString ip, QString port);
198
199 static const QLatin1String fontchoice_str_embedded{"embedded"};
200 static const QLatin1String fontchoice_str_best_system{"best_system"};
201 static const QString fontchoice_str_custom_prefix{QStringLiteral("custom, ")};
202
203 static const std::map<OutputType, std::pair<const char*, const char*>> UntranslatedOutputTypeDescriptions{
204 {OutputType::LEGACY, {
205 QT_TRANSLATE_NOOP("Output type name", "Base58 (Legacy)"),
206 QT_TRANSLATE_NOOP("Output type description", "Widest compatibility and best for health of the Limenka network, but may result in higher fees later. Recommended."),
207 }},
208 {OutputType::P2SH_SEGWIT, {
209 QT_TRANSLATE_NOOP("Output type name", "Base58 (P2SH Segwit)"),
210 QT_TRANSLATE_NOOP("Output type description", "Compatible with most older wallets, and may result in lower fees than Legacy."),
211 }},
212 {OutputType::BECH32, {
213 QT_TRANSLATE_NOOP("Output type name", "Native Segwit (Bech32)"),
214 QT_TRANSLATE_NOOP("Output type description", "Lower fees than Base58, but some old wallets don't support it."),
215 }},
216 {OutputType::BECH32M, {
217 QT_TRANSLATE_NOOP("Output type name", "Taproot (Bech32m)"),
218 QT_TRANSLATE_NOOP("Output type description", "Lowest fees, but wallet support is still limited."),
219 }},
220 };
221
222 std::pair<QString, QString> GetOutputTypeDescription(const OutputType type)
223 {
224 auto& untr = UntranslatedOutputTypeDescriptions.at(type);
225 QString text = QCoreApplication::translate("Output type name", untr.first);
226 QString tooltip = QCoreApplication::translate("Output type description", untr.second);
227 return std::make_pair(text, tooltip);
228 }
229
230 QString OptionsModel::FontChoiceToString(const OptionsModel::FontChoice& f)
231 {
232 if (std::holds_alternative<FontChoiceAbstract>(f)) {
233 if (f == UseBestSystemFont) {
234 return fontchoice_str_best_system;
235 } else {
236 return fontchoice_str_embedded;
237 }
238 }
239 return fontchoice_str_custom_prefix + std::get<QFont>(f).toString();
240 }
241
242 OptionsModel::FontChoice OptionsModel::FontChoiceFromString(const QString& s)
243 {
244 if (s == fontchoice_str_best_system) {
245 return FontChoiceAbstract::BestSystemFont;
246 } else if (s == fontchoice_str_embedded) {
247 return FontChoiceAbstract::EmbeddedFont;
248 } else if (s.startsWith(fontchoice_str_custom_prefix)) {
249 QFont f;
250 f.fromString(s.mid(fontchoice_str_custom_prefix.size()));
251 return f;
252 } else {
253 return FontChoiceAbstract::EmbeddedFont; // default
254 }
255 }
256
257 static QString CanonicalMempoolReplacement(const OptionsModel& model)
258 {
259 switch (model.node().mempool().m_opts.rbf_policy) {
260 case RBFPolicy::Never: return "never";
261 case RBFPolicy::OptIn: return "fee,optin";
262 case RBFPolicy::Always: return "fee,-optin";
263 }
264 assert(0);
265 }
266
267 static QString CanonicalMempoolTRUC(const OptionsModel& model)
268 {
269 switch (model.node().mempool().m_opts.truc_policy) {
270 case TRUCPolicy::Reject: return "reject";
271 case TRUCPolicy::Accept: return "accept";
272 case TRUCPolicy::Enforce: return "enforce";
273 }
274 assert(0);
275 }
276
277 static QString CanonicalPermitEphemeral(const OptionsModel& model)
278 {
279 const auto& opts = model.node().mempool().m_opts;
280 if (!(opts.permitephemeral_anchor || opts.permitephemeral_send)) {
281 return "reject";
282 }
283 return
284 QString(opts.permitephemeral_anchor ? "" : "-") + "anchor," +
285 (opts.permitephemeral_send ? "" : "-") + "send," +
286 (opts.permitephemeral_dust ? "" : "-") + "dust";
287 }
288
289 OptionsModel::OptionsModel(interfaces::Node& node, QObject *parent) :
290 QAbstractListModel(parent), m_node{node}
291 {
292 }
293
294 void OptionsModel::addOverriddenOption(const std::string &option)
295 {
296 strOverriddenByCommandLine += QString::fromStdString(option) + "=" + QString::fromStdString(gArgs.GetArg(option, "")) + " ";
297 }
298
299 // Writes all missing QSettings with their default values
300 bool OptionsModel::Init(bilingual_str& error)
301 {
302 // Initialize display settings from stored settings.
303 language = QString::fromStdString(SettingToString(node().getPersistentSetting("lang"), ""));
304
305 checkAndMigrate();
306
307 QSettings settings;
308
309 // Ensure restart flag is unset on client startup
310 setRestartRequired(false);
311
312 // These are Qt-only settings:
313
314 // Window
315 if (!settings.contains("fHideTrayIcon")) {
316 settings.setValue("fHideTrayIcon", false);
317 }
318 m_show_tray_icon = !settings.value("fHideTrayIcon").toBool();
319 Q_EMIT showTrayIconChanged(m_show_tray_icon);
320
321 if (!settings.contains("fMinimizeToTray"))
322 settings.setValue("fMinimizeToTray", false);
323 fMinimizeToTray = settings.value("fMinimizeToTray").toBool() && m_show_tray_icon;
324
325 if (!settings.contains("fMinimizeOnClose"))
326 settings.setValue("fMinimizeOnClose", false);
327 fMinimizeOnClose = settings.value("fMinimizeOnClose").toBool();
328
329 // Display
330 if (!settings.contains("DisplayLimenkaUnit")) {
331 auto init_unit = LimenkaUnit::BTC;
332 if (settings.contains("nDisplayUnit")) {
333 // Migrate to new setting
334 init_unit = LimenkaUnits::FromSetting(settings.value("nDisplayUnit").toString(), init_unit);
335 }
336 settings.setValue("DisplayLimenkaUnit", QVariant::fromValue(init_unit));
337 }
338
339 constexpr auto unit_set_to_variant = [](LimenkaUnit& out, const QVariant& unit_variant){
340 if (unit_variant.isNull()) return false;
341 if (!unit_variant.canConvert<LimenkaUnit>()) return false;
342 const auto unit = unit_variant.value<LimenkaUnit>();
343 if (!LimenkaUnits::availableUnits().contains(unit)) return false;
344 out = unit;
345 return true;
346 };
347 if (!unit_set_to_variant(m_display_limenka_unit, settings.value("DisplayLimenkaUnitKnots"))) {
348 if (!unit_set_to_variant(m_display_limenka_unit, settings.value("DisplayLimenkaUnit"))) {
349 m_display_limenka_unit = LimenkaUnit::BTC;
350 }
351 }
352
353 if (!settings.contains("bDisplayAddresses"))
354 settings.setValue("bDisplayAddresses", false);
355 bDisplayAddresses = settings.value("bDisplayAddresses", false).toBool();
356
357 if (!settings.contains("strThirdPartyTxUrls"))
358 settings.setValue("strThirdPartyTxUrls", "");
359 strThirdPartyTxUrls = settings.value("strThirdPartyTxUrls", "").toString();
360
361 if (!settings.contains("fCoinControlFeatures"))
362 settings.setValue("fCoinControlFeatures", false);
363 fCoinControlFeatures = settings.value("fCoinControlFeatures", false).toBool();
364
365 if (!settings.contains("enable_psbt_controls")) {
366 settings.setValue("enable_psbt_controls", false);
367 }
368 m_enable_psbt_controls = settings.value("enable_psbt_controls", false).toBool();
369
370 // These are shared with the core or have a command-line parameter
371 // and we want command-line parameters to overwrite the GUI settings.
372 std::unordered_set<std::string> checked_settings;
373 for (OptionID option : {DatabaseCache, ThreadsScriptVerif, SpendZeroConfChange, ExternalSignerPath, MapPortUPnP,
374 MapPortNatpmp, Listen, Server, PruneTristate, ProxyUse, ProxyUseTor, Language}) {
375 // isSettingIgnored will have a false positive here during first-run prune changes
376 if (option == PruneTristate && m_prune_forced_by_gui) continue;
377
378 std::string setting = SettingName(option);
379 checked_settings.insert(setting);
380 if (node().isSettingIgnored(setting)) addOverriddenOption("-" + setting);
381 try {
382 getOption(option);
383 } catch (const std::exception& e) {
384 // This handles exceptions thrown by univalue that can happen if
385 // settings in settings.json don't have the expected types.
386 error.original = strprintf("Could not read setting \"%s\", %s.", setting, e.what());
387 error.translated = tr("Could not read setting \"%1\", %2.").arg(QString::fromStdString(setting), e.what()).toStdString();
388 return false;
389 }
390 }
391
392 if (m_prune_forced_by_gui) checked_settings.insert("prune");
393 for (OptionID option = OptionID(0); option < OptionIDRowCount; option = OptionID(option + 1)) {
394 std::string setting;
395 try {
396 setting = SettingName(option);
397 } catch (const std::logic_error&) {
398 continue; // Ignore GUI-only settings
399 }
400 if (!checked_settings.insert(setting).second) continue;
401 if (node().isSettingIgnored(setting)) addOverriddenOption("-" + setting);
402 }
403
404 // If setting doesn't exist create it with defaults.
405
406 // Main
407 if (!settings.contains("strDataDir"))
408 settings.setValue("strDataDir", GUIUtil::getDefaultDataDirectory());
409
410 // Wallet
411 #ifdef ENABLE_WALLET
412 if (!settings.contains("SubFeeFromAmount")) {
413 settings.setValue("SubFeeFromAmount", false);
414 }
415 m_sub_fee_from_amount = settings.value("SubFeeFromAmount", false).toBool();
416 #endif
417
418 // Network
419 if (!settings.contains("nNetworkPort"))
420 settings.setValue("nNetworkPort", (quint16)Params().GetDefaultPort());
421 if (!gArgs.SoftSetArg("-port", settings.value("nNetworkPort").toString().toStdString()))
422 addOverriddenOption("-port");
423
424 // rwconf settings that require a restart
425 // Caution: This is before general initialisation occurs!
426 f_peerbloomfilters = gArgs.GetBoolArg("-peerbloomfilters", DEFAULT_PEERBLOOMFILTERS);
427 f_rejectspkreuse = !(gArgs.GetArg("-spkreuse", DEFAULT_SPKREUSE) == "allow" || gArgs.GetBoolArg("-spkreuse", false));
428
429 // Display
430 if (settings.contains("FontForMoney")) {
431 m_font_money = FontChoiceFromString(settings.value("FontForMoney").toString());
432 } else if (settings.contains("UseEmbeddedMonospacedFont")) {
433 if (settings.value("UseEmbeddedMonospacedFont").toBool()) {
434 m_font_money = FontChoiceAbstract::EmbeddedFont;
435 } else {
436 m_font_money = FontChoiceAbstract::BestSystemFont;
437 }
438 }
439 m_font_money_supports_tonal = TonalUtils::font_supports_tonal(getFontForMoney(LimenkaUnit::BTC));
440 Q_EMIT fontForMoneyChanged(getFontForMoney(LimenkaUnit::BTC));
441
442 if (settings.contains("FontForQRCodes")) {
443 m_font_qrcodes = FontChoiceFromString(settings.value("FontForQRCodes").toString());
444 }
445 Q_EMIT fontForQRCodesChanged(getFontChoiceForQRCodes());
446
447 if (!settings.contains("PeersTabAlternatingRowColors")) {
448 settings.setValue("PeersTabAlternatingRowColors", "false");
449 }
450 m_peers_tab_alternating_row_colors = settings.value("PeersTabAlternatingRowColors").toBool();
451 Q_EMIT peersTabAlternatingRowColorsChanged(m_peers_tab_alternating_row_colors);
452
453 m_mask_values = settings.value("mask_values", false).toBool();
454
455 return true;
456 }
457
458 /** Helper function to copy contents from one QSettings to another.
459 * By using allKeys this also covers nested settings in a hierarchy.
460 */
461 static void CopySettings(QSettings& dst, const QSettings& src)
462 {
463 for (const QString& key : src.allKeys()) {
464 dst.setValue(key, src.value(key));
465 }
466 }
467
468 /** Back up a QSettings to an ini-formatted file. */
469 static void BackupSettings(const fs::path& filename, const QSettings& src)
470 {
471 qInfo() << "Backing up GUI settings to" << GUIUtil::PathToQString(filename);
472 QSettings dst(GUIUtil::PathToQString(filename), QSettings::IniFormat);
473 dst.clear();
474 CopySettings(dst, src);
475 }
476
477 void OptionsModel::Reset()
478 {
479 // Backup and reset settings.json
480 node().resetSettings();
481
482 QSettings settings;
483
484 // Backup old settings to chain-specific datadir for troubleshooting
485 BackupSettings(gArgs.GetDataDirNet() / "guisettings.ini.bak", settings);
486
487 // Save the strDataDir setting
488 QString dataDir = GUIUtil::getDefaultDataDirectory();
489 dataDir = settings.value("strDataDir", dataDir).toString();
490
491 // Remove rw config file
492 gArgs.EraseRWConfigFile();
493
494 // Remove all entries from our QSettings object
495 settings.clear();
496
497 // Set strDataDir
498 settings.setValue("strDataDir", dataDir);
499
500 // Set prune option iff it was configured in rwconf
501 if (gArgs.RWConfigHasPruneOption()) {
502 SetPruneTargetMiB(gArgs.GetIntArg("-prune", 0));
503 }
504
505 // Set that this was reset
506 settings.setValue("fReset", true);
507
508 // default setting for OptionsModel::StartAtStartup - disabled
509 if (GUIUtil::GetStartOnSystemStartup())
510 GUIUtil::SetStartOnSystemStartup(false);
511 }
512
513 int OptionsModel::rowCount(const QModelIndex & parent) const
514 {
515 return OptionIDRowCount;
516 }
517
518 static ProxySetting ParseProxyString(const QString& proxy)
519 {
520 static const ProxySetting default_val = {false, DEFAULT_GUI_PROXY_HOST, QString("%1").arg(DEFAULT_GUI_PROXY_PORT)};
521 // Handle the case that the setting is not set at all
522 if (proxy.isEmpty()) {
523 return default_val;
524 }
525 uint16_t port{0};
526 std::string hostname;
527 if (SplitHostPort(proxy.toStdString(), port, hostname) && port != 0) {
528 // Valid and port within the valid range
529 // Check if the hostname contains a colon, indicating an IPv6 address
530 if (hostname.find(':') != std::string::npos) {
531 hostname = "[" + hostname + "]"; // Wrap IPv6 address in brackets
532 }
533 return {true, QString::fromStdString(hostname), QString::number(port)};
534 } else { // Invalid: return default
535 return default_val;
536 }
537 }
538
539 static ProxySetting ParseProxyString(const std::string& proxy)
540 {
541 return ParseProxyString(QString::fromStdString(proxy));
542 }
543
544 static std::string ProxyString(bool is_set, QString ip, QString port)
545 {
546 return is_set ? QString(ip + ":" + port).toStdString() : "";
547 }
548
549 static QString GetDefaultProxyAddress()
550 {
551 return QString("%1:%2").arg(DEFAULT_GUI_PROXY_HOST).arg(DEFAULT_GUI_PROXY_PORT);
552 }
553
554 void OptionsModel::SetPruneTargetMiB(int prune_target_mib)
555 {
556 const common::SettingsValue cur_value = node().getPersistentSetting("prune");
557 const common::SettingsValue new_value = prune_target_mib;
558
559 // Force setting to take effect. It is still safe to change the value at
560 // this point because this function is only called after the intro screen is
561 // shown, before the node starts.
562 node().forceSetting("prune", new_value);
563 m_prune_forced_by_gui = true;
564
565 // Update settings.json if value configured in intro screen is different
566 // from saved value. Avoid writing settings.json if limenka.conf value
567 // doesn't need to be overridden.
568 if (cur_value.write() != new_value.write()) {
569 // Call UpdateRwSetting() instead of setOption() to avoid setting
570 // RestartRequired flag
571 UpdateRwSetting(node(), PruneTristate, "", new_value);
572 gArgs.ModifyRWConfigFile("prune", new_value.getValStr());
573 }
574
575 // Keep previous pruning size, if pruning was disabled.
576 if (PruneEnabled(cur_value)) {
577 UpdateRwSetting(node(), PruneTristate, "-prev", PruneEnabled(new_value) ? common::SettingsValue{} : cur_value);
578 }
579 }
580
581 // read QSettings values and return them
582 QVariant OptionsModel::data(const QModelIndex & index, int role) const
583 {
584 if(role == Qt::EditRole)
585 {
586 return getOption(OptionID(index.row()));
587 }
588 return QVariant();
589 }
590
591 // write QSettings values
592 bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, int role)
593 {
594 bool successful = true; /* set to false on parse error */
595 if(role == Qt::EditRole)
596 {
597 successful = setOption(OptionID(index.row()), value);
598 }
599
600 Q_EMIT dataChanged(index, index);
601
602 return successful;
603 }
604
605 // NOLINTNEXTLINE(misc-no-recursion)
606 QVariant OptionsModel::getOption(OptionID option, const std::string& suffix) const
607 {
608 auto setting = [&]{ return node().getPersistentSetting(SettingName(option) + suffix); };
609
610 QSettings settings;
611 switch (option) {
612 case StartAtStartup:
613 return GUIUtil::GetStartOnSystemStartup();
614 case ShowTrayIcon:
615 return m_show_tray_icon;
616 case MinimizeToTray:
617 return fMinimizeToTray;
618 case NetworkPort:
619 return settings.value("nNetworkPort");
620 case MapPortUPnP:
621 #ifdef USE_UPNP
622 return SettingToBool(setting(), DEFAULT_UPNP);
623 #else
624 return false;
625 #endif // USE_UPNP
626 case MapPortNatpmp:
627 return SettingToBool(setting(), DEFAULT_NATPMP);
628 case MinimizeOnClose:
629 return fMinimizeOnClose;
630
631 // default proxy
632 case ProxyUse:
633 case ProxyUseTor:
634 return ParseProxyString(SettingToString(setting(), "")).is_set;
635 case ProxyIP:
636 case ProxyIPTor: {
637 ProxySetting proxy = ParseProxyString(SettingToString(setting(), ""));
638 if (proxy.is_set) {
639 return proxy.ip;
640 } else if (suffix.empty()) {
641 return getOption(option, "-prev");
642 } else {
643 return ParseProxyString(GetDefaultProxyAddress().toStdString()).ip;
644 }
645 }
646 case ProxyPort:
647 case ProxyPortTor: {
648 ProxySetting proxy = ParseProxyString(SettingToString(setting(), ""));
649 if (proxy.is_set) {
650 return proxy.port;
651 } else if (suffix.empty()) {
652 return getOption(option, "-prev");
653 } else {
654 return ParseProxyString(GetDefaultProxyAddress().toStdString()).port;
655 }
656 }
657
658 #ifdef ENABLE_WALLET
659 case SpendZeroConfChange:
660 return SettingToBool(setting(), wallet::DEFAULT_SPEND_ZEROCONF_CHANGE);
661 case ExternalSignerPath:
662 return QString::fromStdString(SettingToString(setting(), ""));
663 case SubFeeFromAmount:
664 return m_sub_fee_from_amount;
665 case addresstype:
666 {
667 const OutputType default_address_type = ParseOutputType(gArgs.GetArg("-addresstype", "")).value_or(wallet::DEFAULT_ADDRESS_TYPE);
668 return QString::fromStdString(FormatOutputType(default_address_type));
669 }
670 #endif
671 case DisplayUnit:
672 return QVariant::fromValue(m_display_limenka_unit);
673 case DisplayAddresses:
674 return bDisplayAddresses;
675 case ThirdPartyTxUrls:
676 return strThirdPartyTxUrls;
677 case Language:
678 return QString::fromStdString(SettingToString(setting(), ""));
679 case FontForMoney:
680 return QVariant::fromValue(m_font_money);
681 case FontForQRCodes:
682 return QVariant::fromValue(m_font_qrcodes);
683 case PeersTabAlternatingRowColors:
684 return m_peers_tab_alternating_row_colors;
685 #ifdef ENABLE_WALLET
686 case walletrbf:
687 return gArgs.GetBoolArg("-walletrbf", wallet::DEFAULT_WALLET_RBF);
688 #endif
689 case CoinControlFeatures:
690 return fCoinControlFeatures;
691 case EnablePSBTControls:
692 return settings.value("enable_psbt_controls");
693 case PruneTristate:
694 return PruneSettingAsTristate(setting());
695 case PruneSizeMiB:
696 return PruneEnabled(setting()) ? PruneSizeAsMiB(setting()) :
697 suffix.empty() ? getOption(option, "-prev") :
698 DEFAULT_PRUNE_TARGET_MiB;
699 case DatabaseCache:
700 return qlonglong(SettingToInt(setting(), node::GetDefaultDBCache() / 1_MiB));
701 case ThreadsScriptVerif:
702 return qlonglong(SettingToInt(setting(), DEFAULT_SCRIPTCHECK_THREADS));
703 case Listen:
704 return SettingToBool(setting(), DEFAULT_LISTEN);
705 case Server:
706 return SettingToBool(setting(), false);
707 case MaskValues:
708 return m_mask_values;
709 case maxuploadtarget:
710 return qlonglong(node().context()->connman->GetMaxOutboundTarget() / 1024 / 1024);
711 case peerbloomfilters:
712 return f_peerbloomfilters;
713 case peerblockfilters:
714 return gArgs.GetBoolArg("-peerblockfilters", DEFAULT_PEERBLOCKFILTERS);
715 case mempoolreplacement:
716 return CanonicalMempoolReplacement(*this);
717 case mempooltruc:
718 return CanonicalMempoolTRUC(*this);
719 case maxorphantx:
720 return qlonglong(gArgs.GetIntArg("-maxorphantx", DEFAULT_MAX_ORPHAN_TRANSACTIONS));
721 case maxmempool:
722 return qlonglong(node().mempool().m_opts.max_size_bytes / 1'000'000);
723 case incrementalrelayfee:
724 return qlonglong(node().mempool().m_opts.incremental_relay_feerate.GetFeePerK());
725 case mempoolexpiry:
726 return qlonglong(std::chrono::duration_cast<std::chrono::hours>(node().mempool().m_opts.expiry).count());
727 case rejectunknownscripts:
728 return node().mempool().m_opts.require_standard;
729 case rejectunknownwitness:
730 return !node().mempool().m_opts.acceptunknownwitness;
731 case rejectparasites:
732 return node().mempool().m_opts.reject_parasites;
733 case rejecttokens:
734 return node().mempool().m_opts.reject_tokens;
735 case subdustfeepenalty:
736 return node().mempool().m_opts.subdustfeepenalty;
737 case rejectspkreuse:
738 return f_rejectspkreuse;
739 case minrelaytxfee:
740 return qlonglong(node().mempool().m_opts.min_relay_feerate.GetFeePerK());
741 case minrelaycoinblocks:
742 return qlonglong(node().mempool().m_opts.minrelaycoinblocks);
743 case minrelaymaturity:
744 return node().mempool().m_opts.minrelaymaturity;
745 case bytespersigop:
746 return nBytesPerSigOp;
747 case bytespersigopstrict:
748 return nBytesPerSigOpStrict;
749 case limitancestorcount:
750 return qlonglong(node().mempool().m_opts.limits.ancestor_count);
751 case limitancestorsize:
752 return qlonglong(node().mempool().m_opts.limits.ancestor_size_vbytes / 1'000);
753 case limitdescendantcount:
754 return qlonglong(node().mempool().m_opts.limits.descendant_count);
755 case limitdescendantsize:
756 return qlonglong(node().mempool().m_opts.limits.descendant_size_vbytes / 1'000);
757 case rejectbarepubkey:
758 return !node().mempool().m_opts.permit_bare_pubkey;
759 case rejectbaremultisig:
760 return !node().mempool().m_opts.permit_bare_multisig;
761 case permitephemeral:
762 return CanonicalPermitEphemeral(*this);
763 case rejectbareanchor:
764 return !node().mempool().m_opts.permitbareanchor;
765 case rejectbaredatacarrier:
766 return !node().mempool().m_opts.permitbaredatacarrier;
767 case maxscriptsize:
768 return ::g_script_size_policy_limit;
769 case maxtxlegacysigops:
770 return node().mempool().m_opts.maxtxlegacysigops;
771 case datacarriercost:
772 return double(::g_weight_per_data_byte) / WITNESS_SCALE_FACTOR;
773 case datacarriersize:
774 return qlonglong(node().mempool().m_opts.max_datacarrier_bytes.value_or(0));
775 case rejectnonstddatacarrier:
776 return !node().mempool().m_opts.accept_non_std_datacarrier;
777 case dustrelayfee:
778 return qlonglong(node().mempool().m_opts.dust_relay_feerate_floor.GetFeePerK());
779 case dustdynamic:
780 return QString::fromStdString(SettingToString(setting(), DEFAULT_DUST_DYNAMIC));
781 case blockmintxfee:
782 if (gArgs.IsArgSet("-blockmintxfee")) {
783 return qlonglong(ParseMoney(gArgs.GetArg("-blockmintxfee", "")).value_or(0));
784 } else {
785 return qlonglong(DEFAULT_BLOCK_MIN_TX_FEE);
786 }
787 case blockmaxsize:
788 return qlonglong(gArgs.GetIntArg("-blockmaxsize", DEFAULT_BLOCK_MAX_SIZE) / 1000);
789 case blockprioritysize:
790 return qlonglong(gArgs.GetIntArg("-blockprioritysize", DEFAULT_BLOCK_PRIORITY_SIZE) / 1000);
791 case blockmaxweight:
792 return qlonglong(gArgs.GetIntArg("-blockmaxweight", DEFAULT_BLOCK_MAX_WEIGHT) / 1000);
793 case blockreconstructionextratxn:
794 return qlonglong(gArgs.GetIntArg("-blockreconstructionextratxn", DEFAULT_BLOCK_RECONSTRUCTION_EXTRA_TXN));
795 case blockreconstructionextratxnsize:
796 return qlonglong(gArgs.GetIntArg("-blockreconstructionextratxnsize", DEFAULT_BLOCK_RECONSTRUCTION_EXTRA_TXN_SIZE / 1'000'000));
797 default:
798 return QVariant();
799 }
800 }
801
802 QFont OptionsModel::getFontForChoice(const FontChoice& fc)
803 {
804 QFont f;
805 if (std::holds_alternative<FontChoiceAbstract>(fc)) {
806 f = GUIUtil::fixedPitchFont(fc != UseBestSystemFont);
807 if (fc == UseBestSystemFont) f.setWeight(QFont::Bold);
808 } else {
809 f = std::get<QFont>(fc);
810 }
811 return f;
812 }
813
814 QFont OptionsModel::getFontForMoney(const LimenkaUnit unit) const
815 {
816 if (LimenkaUnits::numsys(unit) == LimenkaUnits::Unit::TBC && !m_font_money_supports_tonal) {
817 return getFontForChoice(FontChoiceAbstract::EmbeddedFont);
818 }
819 return getFontForChoice(m_font_money);
820 }
821
822 // NOLINTNEXTLINE(misc-no-recursion)
823 bool OptionsModel::setOption(OptionID option, const QVariant& value, const std::string& suffix)
824 {
825 auto changed = [&] { return value.isValid() && value != getOption(option, suffix); };
826 auto update = [&](const common::SettingsValue& value) { return UpdateRwSetting(node(), option, suffix, value); };
827
828 bool successful = true; /* set to false on parse error */
829 QSettings settings;
830
831 switch (option) {
832 case StartAtStartup:
833 successful = GUIUtil::SetStartOnSystemStartup(value.toBool());
834 break;
835 case ShowTrayIcon:
836 m_show_tray_icon = value.toBool();
837 settings.setValue("fHideTrayIcon", !m_show_tray_icon);
838 Q_EMIT showTrayIconChanged(m_show_tray_icon);
839 break;
840 case MinimizeToTray:
841 fMinimizeToTray = value.toBool();
842 settings.setValue("fMinimizeToTray", fMinimizeToTray);
843 break;
844 case NetworkPort:
845 if (settings.value("nNetworkPort") != value) {
846 // If the port input box is empty, set to default port
847 if (value.toString().isEmpty()) {
848 settings.setValue("nNetworkPort", (quint16)Params().GetDefaultPort());
849 }
850 else {
851 settings.setValue("nNetworkPort", (quint16)value.toInt());
852 }
853 setRestartRequired(true);
854 }
855 break;
856 case MapPortUPnP: // core option - can be changed on-the-fly
857 if (changed()) {
858 update(value.toBool());
859 if (gArgs.GetBoolArg("-listen", DEFAULT_LISTEN)) {
860 node().mapPort(value.toBool(), getOption(MapPortNatpmp).toBool());
861 }
862 }
863 break;
864 case MapPortNatpmp: // core option - can be changed on-the-fly
865 if (changed()) {
866 update(value.toBool());
867 if (gArgs.GetBoolArg("-listen", DEFAULT_LISTEN)) {
868 node().mapPort(getOption(MapPortUPnP).toBool(), value.toBool());
869 }
870 }
871 break;
872 case MinimizeOnClose:
873 fMinimizeOnClose = value.toBool();
874 settings.setValue("fMinimizeOnClose", fMinimizeOnClose);
875 break;
876
877 // default proxy
878 case ProxyUse:
879 if (changed()) {
880 if (suffix.empty() && !value.toBool()) setOption(option, true, "-prev");
881 update(ProxyString(value.toBool(), getOption(ProxyIP).toString(), getOption(ProxyPort).toString()));
882 if (suffix.empty() && value.toBool()) UpdateRwSetting(node(), option, "-prev", {});
883 if (suffix.empty()) setRestartRequired(true);
884 }
885 break;
886 case ProxyIP:
887 if (changed()) {
888 if (suffix.empty() && !getOption(ProxyUse).toBool()) {
889 setOption(option, value, "-prev");
890 } else {
891 update(ProxyString(true, value.toString(), getOption(ProxyPort).toString()));
892 }
893 if (suffix.empty() && getOption(ProxyUse).toBool()) setRestartRequired(true);
894 }
895 break;
896 case ProxyPort:
897 if (changed()) {
898 if (suffix.empty() && !getOption(ProxyUse).toBool()) {
899 setOption(option, value, "-prev");
900 } else {
901 update(ProxyString(true, getOption(ProxyIP).toString(), value.toString()));
902 }
903 if (suffix.empty() && getOption(ProxyUse).toBool()) setRestartRequired(true);
904 }
905 break;
906
907 // separate Tor proxy
908 case ProxyUseTor:
909 if (changed()) {
910 if (suffix.empty() && !value.toBool()) setOption(option, true, "-prev");
911 update(ProxyString(value.toBool(), getOption(ProxyIPTor).toString(), getOption(ProxyPortTor).toString()));
912 if (suffix.empty() && value.toBool()) UpdateRwSetting(node(), option, "-prev", {});
913 if (suffix.empty()) setRestartRequired(true);
914 }
915 break;
916 case ProxyIPTor:
917 if (changed()) {
918 if (suffix.empty() && !getOption(ProxyUseTor).toBool()) {
919 setOption(option, value, "-prev");
920 } else {
921 update(ProxyString(true, value.toString(), getOption(ProxyPortTor).toString()));
922 }
923 if (suffix.empty() && getOption(ProxyUseTor).toBool()) setRestartRequired(true);
924 }
925 break;
926 case ProxyPortTor:
927 if (changed()) {
928 if (suffix.empty() && !getOption(ProxyUseTor).toBool()) {
929 setOption(option, value, "-prev");
930 } else {
931 update(ProxyString(true, getOption(ProxyIPTor).toString(), value.toString()));
932 }
933 if (suffix.empty() && getOption(ProxyUseTor).toBool()) setRestartRequired(true);
934 }
935 break;
936
937 #ifdef ENABLE_WALLET
938 case SpendZeroConfChange:
939 if (changed()) {
940 update(value.toBool());
941 setRestartRequired(true);
942 }
943 break;
944 case ExternalSignerPath:
945 if (changed()) {
946 update(value.toString().toStdString());
947 setRestartRequired(true);
948 }
949 break;
950 case SubFeeFromAmount:
951 m_sub_fee_from_amount = value.toBool();
952 settings.setValue("SubFeeFromAmount", m_sub_fee_from_amount);
953 break;
954 case addresstype:
955 {
956 const std::string newvalue_str = value.toString().toStdString();
957 const OutputType oldvalue = ParseOutputType(gArgs.GetArg("-addresstype", "")).value_or(wallet::DEFAULT_ADDRESS_TYPE);
958 const OutputType newvalue = ParseOutputType(newvalue_str).value_or(oldvalue);
959 if (newvalue != oldvalue) {
960 gArgs.ModifyRWConfigFile("addresstype", newvalue_str);
961 gArgs.ForceSetArg("-addresstype", newvalue_str);
962 for (auto& wallet_interface : m_node.walletLoader().getWallets()) {
963 wallet::CWallet *wallet;
964 if (wallet_interface && (wallet = wallet_interface->wallet())) {
965 wallet->m_default_address_type = newvalue;
966 } else {
967 setRestartRequired(true);
968 continue;
969 }
970 }
971 Q_EMIT addresstypeChanged(newvalue);
972 }
973 break;
974 }
975 #endif
976 case DisplayUnit:
977 setDisplayUnit(value);
978 break;
979 case DisplayAddresses:
980 bDisplayAddresses = value.toBool();
981 settings.setValue("bDisplayAddresses", bDisplayAddresses);
982 break;
983 case ThirdPartyTxUrls:
984 if (strThirdPartyTxUrls != value.toString()) {
985 strThirdPartyTxUrls = value.toString();
986 settings.setValue("strThirdPartyTxUrls", strThirdPartyTxUrls);
987 setRestartRequired(true);
988 }
989 break;
990 case Language:
991 if (changed()) {
992 update(value.toString().toStdString());
993 setRestartRequired(true);
994 }
995 break;
996 case FontForMoney:
997 {
998 const auto& new_font = value.value<FontChoice>();
999 if (m_font_money == new_font) break;
1000 settings.setValue("FontForMoney", FontChoiceToString(new_font));
1001 m_font_money = new_font;
1002 m_font_money_supports_tonal = TonalUtils::font_supports_tonal(getFontForMoney(LimenkaUnit::BTC));
1003 Q_EMIT fontForMoneyChanged(getFontForMoney(LimenkaUnit::BTC));
1004 break;
1005 }
1006 case FontForQRCodes:
1007 {
1008 const auto& new_font = value.value<FontChoice>();
1009 if (m_font_qrcodes == new_font) break;
1010 settings.setValue("FontForQRCodes", FontChoiceToString(new_font));
1011 m_font_qrcodes = new_font;
1012 Q_EMIT fontForQRCodesChanged(new_font);
1013 break;
1014 }
1015 case PeersTabAlternatingRowColors:
1016 m_peers_tab_alternating_row_colors = value.toBool();
1017 settings.setValue("PeersTabAlternatingRowColors", m_peers_tab_alternating_row_colors);
1018 Q_EMIT peersTabAlternatingRowColorsChanged(m_peers_tab_alternating_row_colors);
1019 break;
1020 #ifdef ENABLE_WALLET
1021 case walletrbf:
1022 if (changed()) {
1023 const bool fNewValue = value.toBool();
1024 const std::string newvalue_str = strprintf("%d", fNewValue);
1025 gArgs.ModifyRWConfigFile("walletrbf", newvalue_str);
1026 gArgs.ForceSetArg("-walletrbf", newvalue_str);
1027 for (auto& wallet_interface : m_node.walletLoader().getWallets()) {
1028 wallet::CWallet *wallet;
1029 if (wallet_interface && (wallet = wallet_interface->wallet())) {
1030 wallet->m_signal_rbf = fNewValue;
1031 } else {
1032 setRestartRequired(true);
1033 }
1034 }
1035 }
1036 break;
1037 #endif
1038 case CoinControlFeatures:
1039 fCoinControlFeatures = value.toBool();
1040 settings.setValue("fCoinControlFeatures", fCoinControlFeatures);
1041 Q_EMIT coinControlFeaturesChanged(fCoinControlFeatures);
1042 break;
1043 case EnablePSBTControls:
1044 m_enable_psbt_controls = value.toBool();
1045 settings.setValue("enable_psbt_controls", m_enable_psbt_controls);
1046 break;
1047 case PruneTristate:
1048 if (changed()) {
1049 const bool is_autoprune = (value.value<Qt::CheckState>() == Qt::Checked);
1050 const auto prune_setting = PruneSettingFromMiB(value.value<Qt::CheckState>(), getOption(PruneSizeMiB).toInt());
1051 if (suffix.empty() && !is_autoprune) setOption(option, Qt::Checked, "-prev");
1052 update(prune_setting);
1053 if (suffix.empty()) gArgs.ModifyRWConfigFile("prune", prune_setting.getValStr());
1054 if (suffix.empty() && is_autoprune) UpdateRwSetting(node(), option, "-prev", {});
1055 if (suffix.empty()) setRestartRequired(true);
1056 }
1057 break;
1058 case PruneSizeMiB:
1059 if (changed()) {
1060 const bool is_autoprune = (Qt::Checked == getOption(PruneTristate).value<Qt::CheckState>());
1061 if (suffix.empty() && !is_autoprune) {
1062 setOption(option, value, "-prev");
1063 } else {
1064 const auto prune_setting = PruneSettingFromMiB(Qt::Checked, value.toInt());
1065 update(prune_setting);
1066 if (suffix.empty()) gArgs.ModifyRWConfigFile("prune", prune_setting.getValStr());
1067 }
1068 if (suffix.empty() && is_autoprune) setRestartRequired(true);
1069 }
1070 break;
1071 case DatabaseCache:
1072 if (changed()) {
1073 update(static_cast<int64_t>(value.toLongLong()));
1074 setRestartRequired(true);
1075 }
1076 break;
1077 case ThreadsScriptVerif:
1078 if (changed()) {
1079 update(static_cast<int64_t>(value.toLongLong()));
1080 setRestartRequired(true);
1081 }
1082 break;
1083 case Listen:
1084 case Server:
1085 if (changed()) {
1086 update(value.toBool());
1087 setRestartRequired(true);
1088 }
1089 break;
1090 case MaskValues:
1091 m_mask_values = value.toBool();
1092 settings.setValue("mask_values", m_mask_values);
1093 break;
1094 case maxuploadtarget:
1095 {
1096 if (changed()) {
1097 gArgs.ModifyRWConfigFile("maxuploadtarget", value.toString().toStdString());
1098 node().context()->connman->SetMaxOutboundTarget(value.toLongLong() * 1024 * 1024);
1099 }
1100 break;
1101 }
1102 case peerbloomfilters:
1103 if (changed()) {
1104 gArgs.ModifyRWConfigFile("peerbloomfilters", strprintf("%d", value.toBool()));
1105 f_peerbloomfilters = value.toBool();
1106 setRestartRequired(true);
1107 }
1108 break;
1109 case peerblockfilters:
1110 {
1111 bool nv = value.toBool();
1112 if (gArgs.GetBoolArg("-peerblockfilters", DEFAULT_PEERBLOCKFILTERS) != nv) {
1113 gArgs.ModifyRWConfigFile("peerblockfilters", strprintf("%d", nv));
1114 gArgs.ModifyRWConfigFile("peercfilters", strprintf("%d", nv), /*also_settings_json=*/ false); // for downgrade compatibility with Knots 0.19
1115 gArgs.ForceSetArg("peerblockfilters", nv);
1116 if (nv && !GetBlockFilterIndex(BlockFilterType::BASIC)) {
1117 // TODO: When other options are possible, we need to append a list!
1118 // TODO: Some way to unset/delete this...
1119 gArgs.ModifyRWConfigFile("blockfilterindex", "basic");
1120 gArgs.ForceSetArg("blockfilterindex", "basic");
1121 }
1122 setRestartRequired(true);
1123 }
1124 break;
1125 }
1126 case mempoolreplacement:
1127 {
1128 if (changed()) {
1129 QString nv = value.toString();
1130 if (nv == "never") {
1131 node().mempool().m_opts.rbf_policy = RBFPolicy::Never;
1132 node().updateRwSetting("mempoolfullrbf", "0");
1133 } else if (nv == "fee,optin") {
1134 node().mempool().m_opts.rbf_policy = RBFPolicy::OptIn;
1135 node().updateRwSetting("mempoolfullrbf", "0");
1136 } else { // "fee,-optin"
1137 node().mempool().m_opts.rbf_policy = RBFPolicy::Always;
1138 node().updateRwSetting("mempoolfullrbf", "1");
1139 }
1140 gArgs.ModifyRWConfigFile("mempoolreplacement", nv.toStdString());
1141 }
1142 break;
1143 }
1144 case mempooltruc:
1145 {
1146 if (changed()) {
1147 QString nv = value.toString();
1148 if (nv == "reject") {
1149 node().mempool().m_opts.truc_policy = TRUCPolicy::Reject;
1150 } else if (nv == "accept") {
1151 node().mempool().m_opts.truc_policy = TRUCPolicy::Accept;
1152 } else if (nv == "enforce") {
1153 node().mempool().m_opts.truc_policy = TRUCPolicy::Enforce;
1154 }
1155 node().updateRwSetting("mempooltruc", nv.toStdString());
1156 }
1157 break;
1158 }
1159 case maxorphantx:
1160 {
1161 if (changed()) {
1162 unsigned int nMaxOrphanTx = gArgs.GetIntArg("-maxorphantx", DEFAULT_MAX_ORPHAN_TRANSACTIONS);
1163 unsigned int nNv = value.toLongLong();
1164 std::string strNv = value.toString().toStdString();
1165 gArgs.ForceSetArg("-maxorphantx", strNv);
1166 gArgs.ModifyRWConfigFile("maxorphantx", strNv);
1167 if (nNv < nMaxOrphanTx) {
1168 assert(node().context() && node().context()->peerman);
1169 node().context()->peerman->LimitOrphanTxSize(nNv);
1170 }
1171 }
1172 break;
1173 }
1174 case maxmempool:
1175 {
1176 if (changed()) {
1177 long long nOldValue = node().mempool().m_opts.max_size_bytes;
1178 long long nNv = value.toLongLong();
1179 std::string strNv = value.toString().toStdString();
1180 node().mempool().m_opts.max_size_bytes = nNv * 1'000'000;
1181 gArgs.ForceSetArg("-maxmempool", strNv);
1182 gArgs.ModifyRWConfigFile("maxmempool", strNv);
1183 if (nNv < nOldValue) {
1184 LOCK(cs_main);
1185 auto node_ctx = node().context();
1186 assert(node_ctx && node_ctx->mempool && node_ctx->chainman);
1187 auto& active_chainstate = node_ctx->chainman->ActiveChainstate();
1188 LOCK(node_ctx->mempool->cs);
1189 LimitMempoolSize(*node_ctx->mempool, active_chainstate.CoinsTip());
1190 }
1191 }
1192 break;
1193 }
1194 case incrementalrelayfee:
1195 if (changed()) {
1196 CAmount nNv = value.toLongLong();
1197 gArgs.ModifyRWConfigFile("incrementalrelayfee", FormatMoney(nNv));
1198 node().mempool().m_opts.incremental_relay_feerate = CFeeRate(nNv);
1199 }
1200 break;
1201 case mempoolexpiry:
1202 {
1203 if (changed()) {
1204 const auto old_value = node().mempool().m_opts.expiry;
1205 const std::chrono::hours new_value{value.toLongLong()};
1206 std::string strNv = value.toString().toStdString();
1207 node().mempool().m_opts.expiry = new_value;
1208 gArgs.ForceSetArg("-mempoolexpiry", strNv);
1209 gArgs.ModifyRWConfigFile("mempoolexpiry", strNv);
1210 if (new_value < old_value) {
1211 LOCK(cs_main);
1212 auto node_ctx = node().context();
1213 assert(node_ctx && node_ctx->mempool && node_ctx->chainman);
1214 auto& active_chainstate = node_ctx->chainman->ActiveChainstate();
1215 LOCK(node_ctx->mempool->cs);
1216 LimitMempoolSize(*node_ctx->mempool, active_chainstate.CoinsTip());
1217 }
1218 }
1219 break;
1220 }
1221 case rejectunknownscripts:
1222 {
1223 if (changed()) {
1224 const bool fNewValue = value.toBool();
1225 node().mempool().m_opts.require_standard = fNewValue;
1226 // This option is inverted in the config:
1227 gArgs.ModifyRWConfigFile("acceptnonstdtxn", strprintf("%d", ! fNewValue));
1228 }
1229 break;
1230 }
1231 case rejectunknownwitness:
1232 if (changed()) {
1233 // This option is inverted
1234 const bool new_value = ! value.toBool();
1235 node().updateRwSetting("acceptunknownwitness" + suffix, new_value);
1236 node().mempool().m_opts.acceptunknownwitness = new_value;
1237 }
1238 break;
1239 case rejectparasites:
1240 {
1241 if (changed()) {
1242 const bool nv = value.toBool();
1243 node().mempool().m_opts.reject_parasites = nv;
1244 node().updateRwSetting("rejectparasites", nv);
1245 }
1246 break;
1247 }
1248 case rejecttokens:
1249 {
1250 if (changed()) {
1251 const bool nv = value.toBool();
1252 node().mempool().m_opts.reject_tokens = nv;
1253 node().updateRwSetting("rejecttokens", nv);
1254 }
1255 break;
1256 }
1257 case subdustfeepenalty:
1258 {
1259 if (changed()) {
1260 const bool nv = value.toBool();
1261 node().mempool().m_opts.subdustfeepenalty = nv;
1262 node().updateRwSetting("subdustfeepenalty", nv);
1263 }
1264 break;
1265 }
1266 case rejectspkreuse:
1267 if (changed()) {
1268 const bool fNewValue = value.toBool();
1269 gArgs.ModifyRWConfigFile("spkreuse", fNewValue ? "conflict" : "allow");
1270 f_rejectspkreuse = fNewValue;
1271 setRestartRequired(true);
1272 }
1273 break;
1274 case minrelaytxfee:
1275 if (changed()) {
1276 CAmount nNv = value.toLongLong();
1277 gArgs.ModifyRWConfigFile("minrelaytxfee", FormatMoney(nNv));
1278 node().mempool().m_opts.min_relay_feerate = CFeeRate(nNv);
1279 }
1280 break;
1281 case minrelaycoinblocks:
1282 if (changed()) {
1283 uint64_t nNv = value.toLongLong();
1284 update(nNv);
1285 node().mempool().m_opts.minrelaycoinblocks = nNv;
1286 }
1287 break;
1288 case minrelaymaturity:
1289 if (changed()) {
1290 int nNv = value.toInt();
1291 update(nNv);
1292 node().mempool().m_opts.minrelaymaturity = nNv;
1293 }
1294 break;
1295 case bytespersigop:
1296 if (changed()) {
1297 gArgs.ModifyRWConfigFile("bytespersigop", value.toString().toStdString());
1298 nBytesPerSigOp = value.toLongLong();
1299 }
1300 break;
1301 case bytespersigopstrict:
1302 if (changed()) {
1303 gArgs.ModifyRWConfigFile("bytespersigopstrict", value.toString().toStdString());
1304 nBytesPerSigOpStrict = value.toLongLong();
1305 }
1306 break;
1307 case limitancestorcount:
1308 if (changed()) {
1309 long long nNv = value.toLongLong();
1310 std::string strNv = value.toString().toStdString();
1311 node().mempool().m_opts.limits.ancestor_count = nNv;
1312 gArgs.ForceSetArg("-limitancestorcount", strNv);
1313 gArgs.ModifyRWConfigFile("limitancestorcount", strNv);
1314 }
1315 break;
1316 case limitancestorsize:
1317 if (changed()) {
1318 long long nNv = value.toLongLong();
1319 std::string strNv = value.toString().toStdString();
1320 node().mempool().m_opts.limits.ancestor_size_vbytes = nNv * 1'000;
1321 gArgs.ForceSetArg("-limitancestorsize", strNv);
1322 gArgs.ModifyRWConfigFile("limitancestorsize", strNv);
1323 }
1324 break;
1325 case limitdescendantcount:
1326 if (changed()) {
1327 long long nNv = value.toLongLong();
1328 std::string strNv = value.toString().toStdString();
1329 node().mempool().m_opts.limits.descendant_count = nNv;
1330 gArgs.ForceSetArg("-limitdescendantcount", strNv);
1331 gArgs.ModifyRWConfigFile("limitdescendantcount", strNv);
1332 }
1333 break;
1334 case limitdescendantsize:
1335 if (changed()) {
1336 long long nNv = value.toLongLong();
1337 std::string strNv = value.toString().toStdString();
1338 node().mempool().m_opts.limits.descendant_size_vbytes = nNv * 1'000;
1339 gArgs.ForceSetArg("-limitdescendantsize", strNv);
1340 gArgs.ModifyRWConfigFile("limitdescendantsize", strNv);
1341 }
1342 break;
1343 case rejectbarepubkey:
1344 if (changed()) {
1345 // The config and internal option is inverted
1346 const bool nv = ! value.toBool();
1347 node().mempool().m_opts.permit_bare_pubkey = nv;
1348 node().updateRwSetting("permitbarepubkey", nv);
1349 }
1350 break;
1351 case rejectbaremultisig:
1352 if (changed()) {
1353 // The config and internal option is inverted
1354 const bool fNewValue = ! value.toBool();
1355 node().mempool().m_opts.permit_bare_multisig = fNewValue;
1356 gArgs.ModifyRWConfigFile("permitbaremultisig", strprintf("%d", fNewValue));
1357 }
1358 break;
1359 case permitephemeral:
1360 {
1361 if (changed()) {
1362 std::string nv = value.toString().toStdString();
1363 ApplyPermitEphemeralOption(nv, node().mempool().m_opts);
1364 update(nv);
1365 }
1366 break;
1367 }
1368 case rejectbareanchor:
1369 if (changed()) {
1370 // The config and internal option is inverted
1371 const bool nv = ! value.toBool();
1372 node().mempool().m_opts.permitbareanchor = nv;
1373 node().updateRwSetting("permitbareanchor", nv);
1374 }
1375 break;
1376 case rejectbaredatacarrier:
1377 if (changed()) {
1378 // The config and internal option is inverted
1379 const bool nv = ! value.toBool();
1380 node().mempool().m_opts.permitbaredatacarrier = nv;
1381 node().updateRwSetting("permitbaredatacarrier", nv);
1382 }
1383 break;
1384 case maxscriptsize:
1385 if (changed()) {
1386 const auto nv = value.toLongLong();
1387 update(nv);
1388 ::g_script_size_policy_limit = nv;
1389 }
1390 break;
1391 case maxtxlegacysigops:
1392 if (changed()) {
1393 const auto nv = value.toLongLong();
1394 update(nv);
1395 node().mempool().m_opts.maxtxlegacysigops = nv;
1396 }
1397 break;
1398 case datacarriercost:
1399 if (changed()) {
1400 const double nNewSize = value.toDouble();
1401 update(nNewSize);
1402 ::g_weight_per_data_byte = nNewSize * WITNESS_SCALE_FACTOR;
1403 }
1404 break;
1405 case datacarriersize:
1406 if (changed()) {
1407 const int nNewSize = value.toInt();
1408 const bool fNewEn = (nNewSize > 0);
1409 if (fNewEn) {
1410 if (!node().mempool().m_opts.max_datacarrier_bytes.has_value()) {
1411 gArgs.ModifyRWConfigFile("datacarrier", strprintf("%d", fNewEn));
1412 }
1413 gArgs.ModifyRWConfigFile("datacarriersize", value.toString().toStdString());
1414 node().mempool().m_opts.max_datacarrier_bytes = nNewSize;
1415 } else {
1416 gArgs.ModifyRWConfigFile("datacarrier", "0");
1417 node().mempool().m_opts.max_datacarrier_bytes = std::nullopt;
1418 }
1419 }
1420 break;
1421 case rejectnonstddatacarrier:
1422 if (changed()) {
1423 // This option is inverted
1424 const bool new_value = ! value.toBool();
1425 node().updateRwSetting("acceptnonstddatacarrier" + suffix, new_value);
1426 node().mempool().m_opts.accept_non_std_datacarrier = new_value;
1427 }
1428 break;
1429 case dustrelayfee:
1430 if (changed()) {
1431 CAmount nNv = value.toLongLong();
1432 gArgs.ModifyRWConfigFile("dustrelayfee", FormatMoney(nNv));
1433 CFeeRate feerate{nNv};
1434 node().mempool().m_opts.dust_relay_feerate_floor = feerate;
1435 if (node().mempool().m_opts.dust_relay_feerate < feerate || !node().mempool().m_opts.dust_relay_target) {
1436 node().mempool().m_opts.dust_relay_feerate = feerate;
1437 } else {
1438 node().mempool().UpdateDynamicDustFeerate();
1439 }
1440 }
1441 break;
1442 case dustdynamic:
1443 if (changed()) {
1444 const std::string newvalue_str = value.toString().toStdString();
1445 const util::Result<std::pair<int32_t, int>> parsed = ParseDustDynamicOpt(newvalue_str, 1008 /* FIXME: get from estimator */);
1446 assert(parsed); // FIXME: what to do if it fails to parse?
1447 // FIXME: save -prev-<type> for each type
1448 update(newvalue_str);
1449 node().mempool().m_opts.dust_relay_target = parsed->first;
1450 node().mempool().m_opts.dust_relay_multiplier = parsed->second;
1451 }
1452 break;
1453 case blockmintxfee:
1454 if (changed()) {
1455 std::string strNv = FormatMoney(value.toLongLong());
1456 gArgs.ForceSetArg("-blockmintxfee", strNv);
1457 gArgs.ModifyRWConfigFile("blockmintxfee", strNv);
1458 }
1459 break;
1460 case blockmaxsize:
1461 case blockprioritysize:
1462 case blockmaxweight:
1463 if (changed()) {
1464 const int nNewValue_kB = value.toInt();
1465 std::string strNv = strprintf("%d000", nNewValue_kB);
1466 std::string strKey;
1467 switch (option) {
1468 case blockmaxsize:
1469 strKey = "blockmaxsize";
1470 break;
1471 case blockprioritysize:
1472 strKey = "blockprioritysize";
1473 break;
1474 case blockmaxweight:
1475 strKey = "blockmaxweight";
1476 break;
1477 default: assert(0);
1478 }
1479 gArgs.ForceSetArg("-" + strKey, strNv);
1480 gArgs.ModifyRWConfigFile(strKey, strNv);
1481 }
1482 break;
1483 case blockreconstructionextratxn:
1484 if (changed()) {
1485 std::string strNv = value.toString().toStdString();
1486 gArgs.ForceSetArg("-blockreconstructionextratxn", strNv);
1487 gArgs.ModifyRWConfigFile("blockreconstructionextratxn", strNv);
1488 setRestartRequired(true);
1489 }
1490 break;
1491 case blockreconstructionextratxnsize:
1492 if (changed()) {
1493 update(value.toLongLong());
1494 gArgs.ForceSetArg("-blockreconstructionextratxnsize", value.toString().toStdString());
1495 setRestartRequired(true);
1496 }
1497 break;
1498 case corepolicy:
1499 gArgs.ModifyRWConfigFile("corepolicy", value.toString().toStdString());
1500 break;
1501 default:
1502 break;
1503 }
1504
1505 return successful;
1506 }
1507
1508 void OptionsModel::setDisplayUnit(const QVariant& new_unit)
1509 {
1510 if (new_unit.isNull() || new_unit.value<LimenkaUnit>() == m_display_limenka_unit) return;
1511
1512 LimenkaUnit unit = new_unit.value<LimenkaUnit>();
1513
1514 // ATTO unit only available after fork activation
1515 if (unit == LimenkaUnit::ATTO) {
1516 // Reject ATTO selection before fork activation (MTP gate).
1517 const Consensus::Params& consensus = Params().GetConsensus();
1518 if (GetTime() < consensus.nForkActivationMTP) {
1519 return;
1520 }
1521 }
1522
1523 m_display_limenka_unit = unit;
1524 QSettings settings;
1525 if (LimenkaUnits::numsys(m_display_limenka_unit) == LimenkaUnit::BTC) {
1526 settings.setValue("DisplayLimenkaUnit", QVariant::fromValue(m_display_limenka_unit));
1527 settings.remove("DisplayLimenkaUnitKnots");
1528 } else {
1529 settings.setValue("DisplayLimenkaUnitKnots", QVariant::fromValue(m_display_limenka_unit));
1530 }
1531 {
1532 // For older versions:
1533 auto setting_val = LimenkaUnits::ToSetting(m_display_limenka_unit);
1534 if (const QString* setting_str = std::get_if<QString>(&setting_val)) {
1535 settings.setValue("nDisplayUnit", *setting_str);
1536 } else {
1537 settings.setValue("nDisplayUnit", std::get<qint8>(setting_val));
1538 }
1539 }
1540 Q_EMIT displayUnitChanged(m_display_limenka_unit);
1541 }
1542
1543 void OptionsModel::setRestartRequired(bool fRequired)
1544 {
1545 QSettings settings;
1546 return settings.setValue("fRestartRequired", fRequired);
1547 }
1548
1549 bool OptionsModel::isRestartRequired() const
1550 {
1551 QSettings settings;
1552 return settings.value("fRestartRequired", false).toBool();
1553 }
1554
1555 bool OptionsModel::hasSigner()
1556 {
1557 return gArgs.GetArg("-signer", "") != "";
1558 }
1559
1560 void OptionsModel::checkAndMigrate()
1561 {
1562 // Migration of default values
1563 // Check if the QSettings container was already loaded with this client version
1564 QSettings settings;
1565 static const char strSettingsVersionKey[] = "nSettingsVersion";
1566 int settingsVersion = settings.contains(strSettingsVersionKey) ? settings.value(strSettingsVersionKey).toInt() : 0;
1567 if (settingsVersion < CLIENT_VERSION)
1568 {
1569 // -dbcache was bumped from 100 to 300 in 0.13
1570 // see https://github.com/limenka/limenka/pull/8273
1571 // force people to upgrade to the new value if they are using 100MB
1572 if (settingsVersion < 130000 && settings.contains("nDatabaseCache") && settings.value("nDatabaseCache").toLongLong() == 100)
1573 settings.setValue("nDatabaseCache", qint64(node::GetDefaultDBCache() / 1_MiB));
1574
1575 settings.setValue(strSettingsVersionKey, CLIENT_VERSION);
1576 }
1577
1578 // Overwrite the 'addrProxy' setting in case it has been set to an illegal
1579 // default value (see issue #12623; PR #12650).
1580 if (settings.contains("addrProxy") && settings.value("addrProxy").toString().endsWith("%2")) {
1581 settings.setValue("addrProxy", GetDefaultProxyAddress());
1582 }
1583
1584 // Overwrite the 'addrSeparateProxyTor' setting in case it has been set to an illegal
1585 // default value (see issue #12623; PR #12650).
1586 if (settings.contains("addrSeparateProxyTor") && settings.value("addrSeparateProxyTor").toString().endsWith("%2")) {
1587 settings.setValue("addrSeparateProxyTor", GetDefaultProxyAddress());
1588 }
1589
1590 // Migrate and delete legacy GUI settings that have now moved to <datadir>/settings.json.
1591 auto migrate_setting = [&](OptionID option, const QString& qt_name) {
1592 if (!settings.contains(qt_name)) return;
1593 QVariant value = settings.value(qt_name);
1594 if (node().getPersistentSetting(SettingName(option)).isNull()) {
1595 if (option == ProxyIP) {
1596 ProxySetting parsed = ParseProxyString(value.toString());
1597 setOption(ProxyIP, parsed.ip);
1598 setOption(ProxyPort, parsed.port);
1599 } else if (option == ProxyIPTor) {
1600 ProxySetting parsed = ParseProxyString(value.toString());
1601 setOption(ProxyIPTor, parsed.ip);
1602 setOption(ProxyPortTor, parsed.port);
1603 } else if (option == PruneSizeMiB) {
1604 // Stored as GB
1605 const int64_t prune_size_gb = value.toInt();
1606 const int prune_size_mib = std::max(prune_size_gb * GB_BYTES / MiB_BYTES, MIN_DISK_SPACE_FOR_BLOCK_FILES / MiB_BYTES);
1607 setOption(option, prune_size_mib);
1608 } else if (option == PruneTristate) {
1609 // Stored as bool
1610 setOption(option, value.toBool() ? Qt::Checked : Qt::Unchecked);
1611 } else {
1612 setOption(option, value);
1613 }
1614 }
1615 settings.remove(qt_name);
1616 };
1617
1618 migrate_setting(DatabaseCache, "nDatabaseCache");
1619 migrate_setting(ThreadsScriptVerif, "nThreadsScriptVerif");
1620 #ifdef ENABLE_WALLET
1621 migrate_setting(SpendZeroConfChange, "bSpendZeroConfChange");
1622 migrate_setting(ExternalSignerPath, "external_signer_path");
1623 #endif
1624 migrate_setting(MapPortUPnP, "fUseUPnP");
1625 migrate_setting(MapPortNatpmp, "fUseNatpmp");
1626 migrate_setting(Listen, "fListen");
1627 migrate_setting(Server, "server");
1628 migrate_setting(PruneSizeMiB, "nPruneSize");
1629 migrate_setting(PruneTristate, "bPrune");
1630 migrate_setting(ProxyIP, "addrProxy");
1631 migrate_setting(ProxyUse, "fUseProxy");
1632 migrate_setting(ProxyIPTor, "addrSeparateProxyTor");
1633 migrate_setting(ProxyUseTor, "fUseSeparateProxyTor");
1634 migrate_setting(Language, "language");
1635
1636 // In case migrating QSettings caused any settings value to change, rerun
1637 // parameter interaction code to update other settings. This is particularly
1638 // important for the -listen setting, which should cause -listenonion, -upnp,
1639 // and other settings to default to false if it was set to false.
1640 // (https://github.com/limenka/gui/issues/567).
1641 node().initParameterInteraction();
1642 }
1643