optionsmodel.cpp raw

   1  // Copyright (c) 2011-present The Bitcoin Core 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 <bitcoin-build-config.h> // IWYU pragma: keep
   6  
   7  #include <qt/optionsmodel.h>
   8  
   9  #include <qt/bitcoinunits.h>
  10  #include <qt/guiconstants.h>
  11  #include <qt/guiutil.h>
  12  
  13  #include <common/args.h>
  14  #include <interfaces/node.h>
  15  #include <mapport.h>
  16  #include <net.h>
  17  #include <netbase.h>
  18  #include <node/caches.h>
  19  #include <node/chainstatemanager_args.h>
  20  #include <univalue.h>
  21  #include <util/string.h>
  22  #include <validation.h>
  23  #include <wallet/wallet.h>
  24  
  25  #include <QDebug>
  26  #include <QLatin1Char>
  27  #include <QSettings>
  28  #include <QStringList>
  29  #include <QVariant>
  30  
  31  const char *DEFAULT_GUI_PROXY_HOST = "127.0.0.1";
  32  
  33  static QString GetDefaultProxyAddress();
  34  
  35  /** Map GUI option ID to node setting name. */
  36  static const char* SettingName(OptionsModel::OptionID option)
  37  {
  38      switch (option) {
  39      case OptionsModel::DatabaseCache: return "dbcache";
  40      case OptionsModel::ThreadsScriptVerif: return "par";
  41      case OptionsModel::SpendZeroConfChange: return "spendzeroconfchange";
  42      case OptionsModel::ExternalSignerPath: return "signer";
  43      case OptionsModel::MapPortNatpmp: return "natpmp";
  44      case OptionsModel::Listen: return "listen";
  45      case OptionsModel::Server: return "server";
  46      case OptionsModel::PruneSize: return "prune";
  47      case OptionsModel::Prune: return "prune";
  48      case OptionsModel::ProxyIP: return "proxy";
  49      case OptionsModel::ProxyPort: return "proxy";
  50      case OptionsModel::ProxyUse: return "proxy";
  51      case OptionsModel::ProxyIPTor: return "onion";
  52      case OptionsModel::ProxyPortTor: return "onion";
  53      case OptionsModel::ProxyUseTor: return "onion";
  54      case OptionsModel::Language: return "lang";
  55      default: throw std::logic_error(strprintf("GUI option %i has no corresponding node setting.", option));
  56      }
  57  }
  58  
  59  /** Call node.updateRwSetting() with Bitcoin 22.x workaround. */
  60  static void UpdateRwSetting(interfaces::Node& node, OptionsModel::OptionID option, const std::string& suffix, const common::SettingsValue& value)
  61  {
  62      if (value.isNum() &&
  63          (option == OptionsModel::DatabaseCache ||
  64           option == OptionsModel::ThreadsScriptVerif ||
  65           option == OptionsModel::Prune ||
  66           option == OptionsModel::PruneSize)) {
  67          // Write certain old settings as strings, even though they are numbers,
  68          // because Bitcoin 22.x releases try to read these specific settings as
  69          // strings in addOverriddenOption() calls at startup, triggering
  70          // uncaught exceptions in UniValue::get_str(). These errors were fixed
  71          // in later releases by https://github.com/bitcoin/bitcoin/pull/24498.
  72          // If new numeric settings are added, they can be written as numbers
  73          // instead of strings, because bitcoin 22.x will not try to read these.
  74          node.updateRwSetting(SettingName(option) + suffix, value.getValStr());
  75      } else {
  76          node.updateRwSetting(SettingName(option) + suffix, value);
  77      }
  78  }
  79  
  80  //! Convert enabled/size values to bitcoin -prune setting.
  81  static common::SettingsValue PruneSetting(bool prune_enabled, int prune_size_gb)
  82  {
  83      assert(!prune_enabled || prune_size_gb >= 1); // PruneSizeGB and ParsePruneSizeGB never return less
  84      return prune_enabled ? PruneGBtoMiB(prune_size_gb) : 0;
  85  }
  86  
  87  //! Get pruning enabled value to show in GUI from bitcoin -prune setting.
  88  static bool PruneEnabled(const common::SettingsValue& prune_setting)
  89  {
  90      // -prune=1 setting is manual pruning mode, so disabled for purposes of the gui
  91      return SettingTo<int64_t>(prune_setting, 0) > 1;
  92  }
  93  
  94  //! Get pruning size value to show in GUI from bitcoin -prune setting. If
  95  //! pruning is not enabled, just show default recommended pruning size (2GB).
  96  static int PruneSizeGB(const common::SettingsValue& prune_setting)
  97  {
  98      int value = SettingTo<int64_t>(prune_setting, 0);
  99      return value > 1 ? PruneMiBtoGB(value) : DEFAULT_PRUNE_TARGET_GB;
 100  }
 101  
 102  //! Parse pruning size value provided by user in GUI or loaded from QSettings
 103  //! (windows registry key or qt .conf file). Smallest value that the GUI can
 104  //! display is 1 GB, so round up if anything less is parsed.
 105  static int ParsePruneSizeGB(const QVariant& prune_size)
 106  {
 107      return std::max(1, prune_size.toInt());
 108  }
 109  
 110  struct ProxySetting {
 111      bool is_set;
 112      QString ip;
 113      QString port;
 114  };
 115  static ProxySetting ParseProxyString(const std::string& proxy);
 116  static std::string ProxyString(bool is_set, QString ip, QString port);
 117  
 118  static const QLatin1String fontchoice_str_embedded{"embedded"};
 119  static const QLatin1String fontchoice_str_best_system{"best_system"};
 120  static const QString fontchoice_str_custom_prefix{QStringLiteral("custom, ")};
 121  
 122  QString OptionsModel::FontChoiceToString(const OptionsModel::FontChoice& f)
 123  {
 124      if (std::holds_alternative<FontChoiceAbstract>(f)) {
 125          if (f == UseBestSystemFont) {
 126              return fontchoice_str_best_system;
 127          } else {
 128              return fontchoice_str_embedded;
 129          }
 130      }
 131      return fontchoice_str_custom_prefix + std::get<QFont>(f).toString();
 132  }
 133  
 134  OptionsModel::FontChoice OptionsModel::FontChoiceFromString(const QString& s)
 135  {
 136      if (s == fontchoice_str_best_system) {
 137          return FontChoiceAbstract::BestSystemFont;
 138      } else if (s == fontchoice_str_embedded) {
 139          return FontChoiceAbstract::EmbeddedFont;
 140      } else if (s.startsWith(fontchoice_str_custom_prefix)) {
 141          QFont f;
 142          f.fromString(s.mid(fontchoice_str_custom_prefix.size()));
 143          return f;
 144      } else {
 145          return FontChoiceAbstract::EmbeddedFont;  // default
 146      }
 147  }
 148  
 149  OptionsModel::OptionsModel(interfaces::Node& node, QObject *parent) :
 150      QAbstractListModel(parent), m_node{node}
 151  {
 152  }
 153  
 154  void OptionsModel::addOverriddenOption(const std::string &option)
 155  {
 156      strOverriddenByCommandLine += QString::fromStdString(option) + "=" + QString::fromStdString(gArgs.GetArg(option, "")) + " ";
 157  }
 158  
 159  // Writes all missing QSettings with their default values
 160  bool OptionsModel::Init(bilingual_str& error)
 161  {
 162      // Initialize display settings from stored settings.
 163      language = QString::fromStdString(SettingToString(node().getPersistentSetting("lang"), ""));
 164  
 165      checkAndMigrate();
 166  
 167      QSettings settings;
 168  
 169      // Ensure restart flag is unset on client startup
 170      setRestartRequired(false);
 171  
 172      // These are Qt-only settings:
 173  
 174      // Window
 175      if (!settings.contains("fHideTrayIcon")) {
 176          settings.setValue("fHideTrayIcon", false);
 177      }
 178      m_show_tray_icon = !settings.value("fHideTrayIcon").toBool();
 179      Q_EMIT showTrayIconChanged(m_show_tray_icon);
 180  
 181      if (!settings.contains("fMinimizeToTray"))
 182          settings.setValue("fMinimizeToTray", false);
 183      fMinimizeToTray = settings.value("fMinimizeToTray").toBool() && m_show_tray_icon;
 184  
 185      if (!settings.contains("fMinimizeOnClose"))
 186          settings.setValue("fMinimizeOnClose", false);
 187      fMinimizeOnClose = settings.value("fMinimizeOnClose").toBool();
 188  
 189      // Display
 190      if (!settings.contains("DisplayBitcoinUnit")) {
 191          settings.setValue("DisplayBitcoinUnit", QVariant::fromValue(BitcoinUnit::BTC));
 192      }
 193      QVariant unit = settings.value("DisplayBitcoinUnit");
 194      if (unit.canConvert<BitcoinUnit>()) {
 195          m_display_bitcoin_unit = unit.value<BitcoinUnit>();
 196      } else {
 197          m_display_bitcoin_unit = BitcoinUnit::BTC;
 198          settings.setValue("DisplayBitcoinUnit", QVariant::fromValue(m_display_bitcoin_unit));
 199      }
 200  
 201      if (!settings.contains("strThirdPartyTxUrls"))
 202          settings.setValue("strThirdPartyTxUrls", "");
 203      strThirdPartyTxUrls = settings.value("strThirdPartyTxUrls", "").toString();
 204  
 205      if (!settings.contains("fCoinControlFeatures"))
 206          settings.setValue("fCoinControlFeatures", false);
 207      fCoinControlFeatures = settings.value("fCoinControlFeatures", false).toBool();
 208  
 209      if (!settings.contains("enable_psbt_controls")) {
 210          settings.setValue("enable_psbt_controls", false);
 211      }
 212      m_enable_psbt_controls = settings.value("enable_psbt_controls", false).toBool();
 213  
 214      // These are shared with the core or have a command-line parameter
 215      // and we want command-line parameters to overwrite the GUI settings.
 216      for (OptionID option : {DatabaseCache, ThreadsScriptVerif, SpendZeroConfChange, ExternalSignerPath,
 217                              MapPortNatpmp, Listen, Server, Prune, ProxyUse, ProxyUseTor, Language}) {
 218          std::string setting = SettingName(option);
 219          if (node().isSettingIgnored(setting)) addOverriddenOption("-" + setting);
 220          try {
 221              getOption(option);
 222          } catch (const std::exception& e) {
 223              // This handles exceptions thrown by univalue that can happen if
 224              // settings in settings.json don't have the expected types.
 225              error.original = strprintf("Could not read setting \"%s\", %s.", setting, e.what());
 226              error.translated = tr("Could not read setting \"%1\", %2.").arg(QString::fromStdString(setting), e.what()).toStdString();
 227              return false;
 228          }
 229      }
 230  
 231      // If setting doesn't exist create it with defaults.
 232  
 233      // Main
 234      if (!settings.contains("strDataDir"))
 235          settings.setValue("strDataDir", GUIUtil::getDefaultDataDirectory());
 236  
 237      // Wallet
 238  #ifdef ENABLE_WALLET
 239      if (!settings.contains("SubFeeFromAmount")) {
 240          settings.setValue("SubFeeFromAmount", false);
 241      }
 242      m_sub_fee_from_amount = settings.value("SubFeeFromAmount", false).toBool();
 243  #endif
 244  
 245      // Display
 246      if (settings.contains("FontForMoney")) {
 247          m_font_money = FontChoiceFromString(settings.value("FontForMoney").toString());
 248      } else if (settings.contains("UseEmbeddedMonospacedFont")) {
 249          if (settings.value("UseEmbeddedMonospacedFont").toBool()) {
 250              m_font_money = FontChoiceAbstract::EmbeddedFont;
 251          } else {
 252              m_font_money = FontChoiceAbstract::BestSystemFont;
 253          }
 254      }
 255      Q_EMIT fontForMoneyChanged(getFontForMoney());
 256  
 257      m_mask_values = settings.value("mask_values", false).toBool();
 258  
 259      return true;
 260  }
 261  
 262  /** Helper function to copy contents from one QSettings to another.
 263   * By using allKeys this also covers nested settings in a hierarchy.
 264   */
 265  static void CopySettings(QSettings& dst, const QSettings& src)
 266  {
 267      for (const QString& key : src.allKeys()) {
 268          dst.setValue(key, src.value(key));
 269      }
 270  }
 271  
 272  /** Back up a QSettings to an ini-formatted file. */
 273  static void BackupSettings(const fs::path& filename, const QSettings& src)
 274  {
 275      qInfo() << "Backing up GUI settings to" << GUIUtil::PathToQString(filename);
 276      QSettings dst(GUIUtil::PathToQString(filename), QSettings::IniFormat);
 277      dst.clear();
 278      CopySettings(dst, src);
 279  }
 280  
 281  void OptionsModel::Reset()
 282  {
 283      // Backup and reset settings.json
 284      node().resetSettings();
 285  
 286      QSettings settings;
 287  
 288      // Backup old settings to chain-specific datadir for troubleshooting
 289      BackupSettings(gArgs.GetDataDirNet() / "guisettings.ini.bak", settings);
 290  
 291      // Save the strDataDir setting
 292      QString dataDir = GUIUtil::getDefaultDataDirectory();
 293      dataDir = settings.value("strDataDir", dataDir).toString();
 294  
 295      // Remove all entries from our QSettings object
 296      settings.clear();
 297  
 298      // Set strDataDir
 299      settings.setValue("strDataDir", dataDir);
 300  
 301      // Set that this was reset
 302      settings.setValue("fReset", true);
 303  
 304      // default setting for OptionsModel::StartAtStartup - disabled
 305      if (GUIUtil::GetStartOnSystemStartup())
 306          GUIUtil::SetStartOnSystemStartup(false);
 307  }
 308  
 309  int OptionsModel::rowCount(const QModelIndex & parent) const
 310  {
 311      return OptionIDRowCount;
 312  }
 313  
 314  static ProxySetting ParseProxyString(const QString& proxy)
 315  {
 316      static const ProxySetting default_val = {false, DEFAULT_GUI_PROXY_HOST, QString("%1").arg(DEFAULT_GUI_PROXY_PORT)};
 317      // Handle the case that the setting is not set at all
 318      if (proxy.isEmpty()) {
 319          return default_val;
 320      }
 321      uint16_t port{0};
 322      std::string hostname;
 323      if (SplitHostPort(proxy.toStdString(), port, hostname) && port != 0) {
 324          // Valid and port within the valid range
 325          // Check if the hostname contains a colon, indicating an IPv6 address
 326          if (hostname.find(':') != std::string::npos) {
 327              hostname = "[" + hostname + "]"; // Wrap IPv6 address in brackets
 328          }
 329          return {true, QString::fromStdString(hostname), QString::number(port)};
 330      } else { // Invalid: return default
 331          return default_val;
 332      }
 333  }
 334  
 335  static ProxySetting ParseProxyString(const std::string& proxy)
 336  {
 337      return ParseProxyString(QString::fromStdString(proxy));
 338  }
 339  
 340  static std::string ProxyString(bool is_set, QString ip, QString port)
 341  {
 342      return is_set ? QString(ip + ":" + port).toStdString() : "";
 343  }
 344  
 345  static QString GetDefaultProxyAddress()
 346  {
 347      return QString("%1:%2").arg(DEFAULT_GUI_PROXY_HOST).arg(DEFAULT_GUI_PROXY_PORT);
 348  }
 349  
 350  void OptionsModel::SetPruneTargetGB(int prune_target_gb)
 351  {
 352      const common::SettingsValue cur_value = node().getPersistentSetting("prune");
 353      const common::SettingsValue new_value = PruneSetting(prune_target_gb > 0, prune_target_gb);
 354  
 355      // Force setting to take effect. It is still safe to change the value at
 356      // this point because this function is only called after the intro screen is
 357      // shown, before the node starts.
 358      node().forceSetting("prune", new_value);
 359  
 360      // Update settings.json if value configured in intro screen is different
 361      // from saved value. Avoid writing settings.json if bitcoin.conf value
 362      // doesn't need to be overridden.
 363      if (PruneEnabled(cur_value) != PruneEnabled(new_value) ||
 364          PruneSizeGB(cur_value) != PruneSizeGB(new_value)) {
 365          // Call UpdateRwSetting() instead of setOption() to avoid setting
 366          // RestartRequired flag
 367          UpdateRwSetting(node(), Prune, "", new_value);
 368      }
 369  
 370      // Keep previous pruning size, if pruning was disabled.
 371      if (PruneEnabled(cur_value)) {
 372          UpdateRwSetting(node(), Prune, "-prev", PruneEnabled(new_value) ? common::SettingsValue{} : cur_value);
 373      }
 374  }
 375  
 376  // read QSettings values and return them
 377  QVariant OptionsModel::data(const QModelIndex & index, int role) const
 378  {
 379      if(role == Qt::EditRole)
 380      {
 381          return getOption(OptionID(index.row()));
 382      }
 383      return QVariant();
 384  }
 385  
 386  // write QSettings values
 387  bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, int role)
 388  {
 389      bool successful = true; /* set to false on parse error */
 390      if(role == Qt::EditRole)
 391      {
 392          successful = setOption(OptionID(index.row()), value);
 393      }
 394  
 395      Q_EMIT dataChanged(index, index);
 396  
 397      return successful;
 398  }
 399  
 400  // NOLINTNEXTLINE(misc-no-recursion)
 401  QVariant OptionsModel::getOption(OptionID option, const std::string& suffix) const
 402  {
 403      auto setting = [&]{ return node().getPersistentSetting(SettingName(option) + suffix); };
 404  
 405      QSettings settings;
 406      switch (option) {
 407      case StartAtStartup:
 408          return GUIUtil::GetStartOnSystemStartup();
 409      case ShowTrayIcon:
 410          return m_show_tray_icon;
 411      case MinimizeToTray:
 412          return fMinimizeToTray;
 413      case MapPortNatpmp:
 414          return SettingToBool(setting(), DEFAULT_NATPMP);
 415      case MinimizeOnClose:
 416          return fMinimizeOnClose;
 417  
 418      // default proxy
 419      case ProxyUse:
 420      case ProxyUseTor:
 421          return ParseProxyString(SettingToString(setting(), "")).is_set;
 422      case ProxyIP:
 423      case ProxyIPTor: {
 424          ProxySetting proxy = ParseProxyString(SettingToString(setting(), ""));
 425          if (proxy.is_set) {
 426              return proxy.ip;
 427          } else if (suffix.empty()) {
 428              return getOption(option, "-prev");
 429          } else {
 430              return ParseProxyString(GetDefaultProxyAddress().toStdString()).ip;
 431          }
 432      }
 433      case ProxyPort:
 434      case ProxyPortTor: {
 435          ProxySetting proxy = ParseProxyString(SettingToString(setting(), ""));
 436          if (proxy.is_set) {
 437              return proxy.port;
 438          } else if (suffix.empty()) {
 439              return getOption(option, "-prev");
 440          } else {
 441              return ParseProxyString(GetDefaultProxyAddress().toStdString()).port;
 442          }
 443      }
 444  
 445  #ifdef ENABLE_WALLET
 446      case SpendZeroConfChange:
 447          return SettingToBool(setting(), wallet::DEFAULT_SPEND_ZEROCONF_CHANGE);
 448      case ExternalSignerPath:
 449          return QString::fromStdString(SettingToString(setting(), ""));
 450      case SubFeeFromAmount:
 451          return m_sub_fee_from_amount;
 452  #endif
 453      case DisplayUnit:
 454          return QVariant::fromValue(m_display_bitcoin_unit);
 455      case ThirdPartyTxUrls:
 456          return strThirdPartyTxUrls;
 457      case Language:
 458          return QString::fromStdString(SettingToString(setting(), ""));
 459      case FontForMoney:
 460          return QVariant::fromValue(m_font_money);
 461      case CoinControlFeatures:
 462          return fCoinControlFeatures;
 463      case EnablePSBTControls:
 464          return settings.value("enable_psbt_controls");
 465      case Prune:
 466          return PruneEnabled(setting());
 467      case PruneSize:
 468          return PruneEnabled(setting()) ? PruneSizeGB(setting()) :
 469                 suffix.empty()          ? getOption(option, "-prev") :
 470                                           DEFAULT_PRUNE_TARGET_GB;
 471      case DatabaseCache:
 472          return qlonglong(SettingTo<int64_t>(setting(), node::GetDefaultDBCache() >> 20));
 473      case ThreadsScriptVerif:
 474          return qlonglong(SettingTo<int64_t>(setting(), DEFAULT_SCRIPTCHECK_THREADS));
 475      case Listen:
 476          return SettingToBool(setting(), DEFAULT_LISTEN);
 477      case Server:
 478          return SettingToBool(setting(), false);
 479      case MaskValues:
 480          return m_mask_values;
 481      default:
 482          return QVariant();
 483      }
 484  }
 485  
 486  QFont OptionsModel::getFontForChoice(const FontChoice& fc)
 487  {
 488      QFont f;
 489      if (std::holds_alternative<FontChoiceAbstract>(fc)) {
 490          f = GUIUtil::fixedPitchFont(fc != UseBestSystemFont);
 491          f.setWeight(QFont::Bold);
 492      } else {
 493          f = std::get<QFont>(fc);
 494      }
 495      return f;
 496  }
 497  
 498  QFont OptionsModel::getFontForMoney() const
 499  {
 500      return getFontForChoice(m_font_money);
 501  }
 502  
 503  // NOLINTNEXTLINE(misc-no-recursion)
 504  bool OptionsModel::setOption(OptionID option, const QVariant& value, const std::string& suffix)
 505  {
 506      auto changed = [&] { return value.isValid() && value != getOption(option, suffix); };
 507      auto update = [&](const common::SettingsValue& value) { return UpdateRwSetting(node(), option, suffix, value); };
 508  
 509      bool successful = true; /* set to false on parse error */
 510      QSettings settings;
 511  
 512      switch (option) {
 513      case StartAtStartup:
 514          successful = GUIUtil::SetStartOnSystemStartup(value.toBool());
 515          break;
 516      case ShowTrayIcon:
 517          m_show_tray_icon = value.toBool();
 518          settings.setValue("fHideTrayIcon", !m_show_tray_icon);
 519          Q_EMIT showTrayIconChanged(m_show_tray_icon);
 520          break;
 521      case MinimizeToTray:
 522          fMinimizeToTray = value.toBool();
 523          settings.setValue("fMinimizeToTray", fMinimizeToTray);
 524          break;
 525      case MapPortNatpmp: // core option - can be changed on-the-fly
 526          if (changed()) {
 527              update(value.toBool());
 528              node().mapPort(value.toBool());
 529          }
 530          break;
 531      case MinimizeOnClose:
 532          fMinimizeOnClose = value.toBool();
 533          settings.setValue("fMinimizeOnClose", fMinimizeOnClose);
 534          break;
 535  
 536      // default proxy
 537      case ProxyUse:
 538          if (changed()) {
 539              if (suffix.empty() && !value.toBool()) setOption(option, true, "-prev");
 540              update(ProxyString(value.toBool(), getOption(ProxyIP).toString(), getOption(ProxyPort).toString()));
 541              if (suffix.empty() && value.toBool()) UpdateRwSetting(node(), option, "-prev", {});
 542              if (suffix.empty()) setRestartRequired(true);
 543          }
 544          break;
 545      case ProxyIP:
 546          if (changed()) {
 547              if (suffix.empty() && !getOption(ProxyUse).toBool()) {
 548                  setOption(option, value, "-prev");
 549              } else {
 550                  update(ProxyString(true, value.toString(), getOption(ProxyPort).toString()));
 551              }
 552              if (suffix.empty() && getOption(ProxyUse).toBool()) setRestartRequired(true);
 553          }
 554          break;
 555      case ProxyPort:
 556          if (changed()) {
 557              if (suffix.empty() && !getOption(ProxyUse).toBool()) {
 558                  setOption(option, value, "-prev");
 559              } else {
 560                  update(ProxyString(true, getOption(ProxyIP).toString(), value.toString()));
 561              }
 562              if (suffix.empty() && getOption(ProxyUse).toBool()) setRestartRequired(true);
 563          }
 564          break;
 565  
 566      // separate Tor proxy
 567      case ProxyUseTor:
 568          if (changed()) {
 569              if (suffix.empty() && !value.toBool()) setOption(option, true, "-prev");
 570              update(ProxyString(value.toBool(), getOption(ProxyIPTor).toString(), getOption(ProxyPortTor).toString()));
 571              if (suffix.empty() && value.toBool()) UpdateRwSetting(node(), option, "-prev", {});
 572              if (suffix.empty()) setRestartRequired(true);
 573          }
 574          break;
 575      case ProxyIPTor:
 576          if (changed()) {
 577              if (suffix.empty() && !getOption(ProxyUseTor).toBool()) {
 578                  setOption(option, value, "-prev");
 579              } else {
 580                  update(ProxyString(true, value.toString(), getOption(ProxyPortTor).toString()));
 581              }
 582              if (suffix.empty() && getOption(ProxyUseTor).toBool()) setRestartRequired(true);
 583          }
 584          break;
 585      case ProxyPortTor:
 586          if (changed()) {
 587              if (suffix.empty() && !getOption(ProxyUseTor).toBool()) {
 588                  setOption(option, value, "-prev");
 589              } else {
 590                  update(ProxyString(true, getOption(ProxyIPTor).toString(), value.toString()));
 591              }
 592              if (suffix.empty() && getOption(ProxyUseTor).toBool()) setRestartRequired(true);
 593          }
 594          break;
 595  
 596  #ifdef ENABLE_WALLET
 597      case SpendZeroConfChange:
 598          if (changed()) {
 599              update(value.toBool());
 600              setRestartRequired(true);
 601          }
 602          break;
 603      case ExternalSignerPath:
 604          if (changed()) {
 605              update(value.toString().toStdString());
 606              setRestartRequired(true);
 607          }
 608          break;
 609      case SubFeeFromAmount:
 610          m_sub_fee_from_amount = value.toBool();
 611          settings.setValue("SubFeeFromAmount", m_sub_fee_from_amount);
 612          break;
 613  #endif
 614      case DisplayUnit:
 615          setDisplayUnit(value);
 616          break;
 617      case ThirdPartyTxUrls:
 618          if (strThirdPartyTxUrls != value.toString()) {
 619              strThirdPartyTxUrls = value.toString();
 620              settings.setValue("strThirdPartyTxUrls", strThirdPartyTxUrls);
 621              setRestartRequired(true);
 622          }
 623          break;
 624      case Language:
 625          if (changed()) {
 626              update(value.toString().toStdString());
 627              setRestartRequired(true);
 628          }
 629          break;
 630      case FontForMoney:
 631      {
 632          const auto& new_font = value.value<FontChoice>();
 633          if (m_font_money == new_font) break;
 634          settings.setValue("FontForMoney", FontChoiceToString(new_font));
 635          m_font_money = new_font;
 636          Q_EMIT fontForMoneyChanged(getFontForMoney());
 637          break;
 638      }
 639      case CoinControlFeatures:
 640          fCoinControlFeatures = value.toBool();
 641          settings.setValue("fCoinControlFeatures", fCoinControlFeatures);
 642          Q_EMIT coinControlFeaturesChanged(fCoinControlFeatures);
 643          break;
 644      case EnablePSBTControls:
 645          m_enable_psbt_controls = value.toBool();
 646          settings.setValue("enable_psbt_controls", m_enable_psbt_controls);
 647          break;
 648      case Prune:
 649          if (changed()) {
 650              if (suffix.empty() && !value.toBool()) setOption(option, true, "-prev");
 651              update(PruneSetting(value.toBool(), getOption(PruneSize).toInt()));
 652              if (suffix.empty() && value.toBool()) UpdateRwSetting(node(), option, "-prev", {});
 653              if (suffix.empty()) setRestartRequired(true);
 654          }
 655          break;
 656      case PruneSize:
 657          if (changed()) {
 658              if (suffix.empty() && !getOption(Prune).toBool()) {
 659                  setOption(option, value, "-prev");
 660              } else {
 661                  update(PruneSetting(true, ParsePruneSizeGB(value)));
 662              }
 663              if (suffix.empty() && getOption(Prune).toBool()) setRestartRequired(true);
 664          }
 665          break;
 666      case DatabaseCache:
 667          if (changed()) {
 668              update(static_cast<int64_t>(value.toLongLong()));
 669              setRestartRequired(true);
 670          }
 671          break;
 672      case ThreadsScriptVerif:
 673          if (changed()) {
 674              update(static_cast<int64_t>(value.toLongLong()));
 675              setRestartRequired(true);
 676          }
 677          break;
 678      case Listen:
 679      case Server:
 680          if (changed()) {
 681              update(value.toBool());
 682              setRestartRequired(true);
 683          }
 684          break;
 685      case MaskValues:
 686          m_mask_values = value.toBool();
 687          settings.setValue("mask_values", m_mask_values);
 688          break;
 689      default:
 690          break;
 691      }
 692  
 693      return successful;
 694  }
 695  
 696  void OptionsModel::setDisplayUnit(const QVariant& new_unit)
 697  {
 698      if (new_unit.isNull() || new_unit.value<BitcoinUnit>() == m_display_bitcoin_unit) return;
 699      m_display_bitcoin_unit = new_unit.value<BitcoinUnit>();
 700      QSettings settings;
 701      settings.setValue("DisplayBitcoinUnit", QVariant::fromValue(m_display_bitcoin_unit));
 702      Q_EMIT displayUnitChanged(m_display_bitcoin_unit);
 703  }
 704  
 705  void OptionsModel::setRestartRequired(bool fRequired)
 706  {
 707      QSettings settings;
 708      return settings.setValue("fRestartRequired", fRequired);
 709  }
 710  
 711  bool OptionsModel::isRestartRequired() const
 712  {
 713      QSettings settings;
 714      return settings.value("fRestartRequired", false).toBool();
 715  }
 716  
 717  bool OptionsModel::hasSigner()
 718  {
 719      return gArgs.GetArg("-signer", "") != "";
 720  }
 721  
 722  void OptionsModel::checkAndMigrate()
 723  {
 724      // Migration of default values
 725      // Check if the QSettings container was already loaded with this client version
 726      QSettings settings;
 727      static const char strSettingsVersionKey[] = "nSettingsVersion";
 728      int settingsVersion = settings.contains(strSettingsVersionKey) ? settings.value(strSettingsVersionKey).toInt() : 0;
 729      if (settingsVersion < CLIENT_VERSION)
 730      {
 731          // -dbcache was bumped from 100 to 300 in 0.13
 732          // see https://github.com/bitcoin/bitcoin/pull/8273
 733          // force people to upgrade to the new value if they are using 100MB
 734          if (settingsVersion < 130000 && settings.contains("nDatabaseCache") && settings.value("nDatabaseCache").toLongLong() == 100)
 735              settings.setValue("nDatabaseCache", (qint64)(DEFAULT_DB_CACHE >> 20));
 736  
 737          settings.setValue(strSettingsVersionKey, CLIENT_VERSION);
 738      }
 739  
 740      // Overwrite the 'addrProxy' setting in case it has been set to an illegal
 741      // default value (see issue #12623; PR #12650).
 742      if (settings.contains("addrProxy") && settings.value("addrProxy").toString().endsWith("%2")) {
 743          settings.setValue("addrProxy", GetDefaultProxyAddress());
 744      }
 745  
 746      // Overwrite the 'addrSeparateProxyTor' setting in case it has been set to an illegal
 747      // default value (see issue #12623; PR #12650).
 748      if (settings.contains("addrSeparateProxyTor") && settings.value("addrSeparateProxyTor").toString().endsWith("%2")) {
 749          settings.setValue("addrSeparateProxyTor", GetDefaultProxyAddress());
 750      }
 751  
 752      // Migrate and delete legacy GUI settings that have now moved to <datadir>/settings.json.
 753      auto migrate_setting = [&](OptionID option, const QString& qt_name) {
 754          if (!settings.contains(qt_name)) return;
 755          QVariant value = settings.value(qt_name);
 756          if (node().getPersistentSetting(SettingName(option)).isNull()) {
 757              if (option == ProxyIP) {
 758                  ProxySetting parsed = ParseProxyString(value.toString());
 759                  setOption(ProxyIP, parsed.ip);
 760                  setOption(ProxyPort, parsed.port);
 761              } else if (option == ProxyIPTor) {
 762                  ProxySetting parsed = ParseProxyString(value.toString());
 763                  setOption(ProxyIPTor, parsed.ip);
 764                  setOption(ProxyPortTor, parsed.port);
 765              } else {
 766                  setOption(option, value);
 767              }
 768          }
 769          settings.remove(qt_name);
 770      };
 771  
 772      migrate_setting(DatabaseCache, "nDatabaseCache");
 773      migrate_setting(ThreadsScriptVerif, "nThreadsScriptVerif");
 774  #ifdef ENABLE_WALLET
 775      migrate_setting(SpendZeroConfChange, "bSpendZeroConfChange");
 776      migrate_setting(ExternalSignerPath, "external_signer_path");
 777  #endif
 778      migrate_setting(MapPortNatpmp, "fUseNatpmp");
 779      migrate_setting(Listen, "fListen");
 780      migrate_setting(Server, "server");
 781      migrate_setting(PruneSize, "nPruneSize");
 782      migrate_setting(Prune, "bPrune");
 783      migrate_setting(ProxyIP, "addrProxy");
 784      migrate_setting(ProxyUse, "fUseProxy");
 785      migrate_setting(ProxyIPTor, "addrSeparateProxyTor");
 786      migrate_setting(ProxyUseTor, "fUseSeparateProxyTor");
 787      migrate_setting(Language, "language");
 788  
 789      // In case migrating QSettings caused any settings value to change, rerun
 790      // parameter interaction code to update other settings. This is particularly
 791      // important for the -listen setting, which should cause -listenonion
 792      // and other settings to default to false if it was set to false.
 793      // (https://github.com/bitcoin-core/gui/issues/567).
 794      node().initParameterInteraction();
 795  }
 796