From 24980feab71957df05b1e55f72fbb7dc8cf34036 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 24 May 2023 17:41:44 +0000 Subject: [PATCH 1/3] config: introduce ConfigVars A new config API is introduced, and ~all of the codebase is adapted to it. The old API is kept but mainly only for dynamic usage where its extra flexibility is needed. Using examples, the old config API looked this: ``` >>> config.get("request_expiry", 86400) 604800 >>> config.set_key("request_expiry", 86400) >>> ``` The new config API instead: ``` >>> config.WALLET_PAYREQ_EXPIRY_SECONDS 604800 >>> config.WALLET_PAYREQ_EXPIRY_SECONDS = 86400 >>> ``` The old API operated on arbitrary string keys, the new one uses a static ~enum-like list of variables. With the new API: - there is a single centralised list of config variables, as opposed to these being scattered all over - no more duplication of default values (in the getters) - there is now some (minimal for now) type-validation/conversion for the config values closes https://github.com/spesmilo/electrum/pull/5640 closes https://github.com/spesmilo/electrum/pull/5649 Note: there is yet a third API added here, for certain niche/abstract use-cases, where we need a reference to the config variable itself. It should only be used when needed: ``` >>> var = config.cv.WALLET_PAYREQ_EXPIRY_SECONDS >>> var >>> var.get() 604800 >>> var.set(3600) >>> var.get_default_value() 86400 >>> var.is_set() True >>> var.is_modifiable() True ``` --- electrum/address_synchronizer.py | 2 +- electrum/base_crash_reporter.py | 1 - electrum/base_wizard.py | 4 +- electrum/coinchooser.py | 15 +- electrum/commands.py | 47 +-- electrum/contacts.py | 2 +- electrum/daemon.py | 42 +-- electrum/exchange_rate.py | 27 +- electrum/gui/kivy/main_window.py | 34 +- .../gui/kivy/uix/dialogs/crash_reporter.py | 12 +- electrum/gui/kivy/uix/dialogs/fee_dialog.py | 10 +- electrum/gui/kivy/uix/dialogs/settings.py | 8 +- electrum/gui/kivy/uix/screens.py | 4 +- electrum/gui/qml/qeapp.py | 4 +- electrum/gui/qml/qechannelopener.py | 5 +- electrum/gui/qml/qeconfig.py | 82 ++--- electrum/gui/qml/qedaemon.py | 4 +- electrum/gui/qml/qefx.py | 6 +- electrum/gui/qml/qeinvoice.py | 2 +- electrum/gui/qml/qetxfinalizer.py | 10 +- electrum/gui/qt/__init__.py | 10 +- electrum/gui/qt/address_list.py | 8 +- electrum/gui/qt/confirm_tx_dialog.py | 52 +-- electrum/gui/qt/exception_window.py | 4 +- electrum/gui/qt/fee_slider.py | 4 +- electrum/gui/qt/history_list.py | 12 +- electrum/gui/qt/installwizard.py | 4 +- electrum/gui/qt/main_window.py | 26 +- electrum/gui/qt/my_treeview.py | 28 +- electrum/gui/qt/network_dialog.py | 10 +- electrum/gui/qt/new_channel_dialog.py | 2 +- .../qt/qrreader/qtmultimedia/camera_dialog.py | 4 +- electrum/gui/qt/receive_tab.py | 26 +- electrum/gui/qt/settings_dialog.py | 75 ++-- electrum/gui/qt/swap_dialog.py | 8 +- electrum/gui/qt/transaction_dialog.py | 4 +- electrum/gui/qt/util.py | 10 +- electrum/gui/text.py | 6 +- electrum/interface.py | 8 +- electrum/lnpeer.py | 26 +- electrum/lnworker.py | 12 +- electrum/logging.py | 4 +- electrum/network.py | 38 +- electrum/paymentrequest.py | 10 +- electrum/plugins/ledger/ledger.py | 1 - electrum/plugins/payserver/payserver.py | 12 +- electrum/plugins/payserver/qt.py | 14 +- electrum/plugins/trustedcoin/qt.py | 4 +- electrum/plugins/trustedcoin/trustedcoin.py | 12 +- electrum/simple_config.py | 326 +++++++++++++++--- electrum/submarine_swaps.py | 2 +- electrum/tests/test_daemon.py | 4 +- electrum/tests/test_lnpeer.py | 42 +-- electrum/tests/test_simple_config.py | 73 ++++ electrum/tests/test_sswaps.py | 4 +- electrum/tests/test_wallet_vertical.py | 10 +- electrum/util.py | 11 +- electrum/verifier.py | 2 +- electrum/wallet.py | 8 +- run_electrum | 15 +- 60 files changed, 781 insertions(+), 471 deletions(-) diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py index 0b8578213..b42be0474 100644 --- a/electrum/address_synchronizer.py +++ b/electrum/address_synchronizer.py @@ -959,7 +959,7 @@ class AddressSynchronizer(Logger, EventListener): """ max_conf = -1 h = self.db.get_addr_history(address) - needs_spv_check = not self.config.get("skipmerklecheck", False) + needs_spv_check = not self.config.NETWORK_SKIPMERKLECHECK for tx_hash, tx_height in h: if needs_spv_check: tx_age = self.get_tx_height(tx_hash).conf diff --git a/electrum/base_crash_reporter.py b/electrum/base_crash_reporter.py index 8471ee679..6d113dd3b 100644 --- a/electrum/base_crash_reporter.py +++ b/electrum/base_crash_reporter.py @@ -42,7 +42,6 @@ class CrashReportResponse(NamedTuple): class BaseCrashReporter(Logger): report_server = "https://crashhub.electrum.org" - config_key = "show_crash_reporter" issue_template = """

Traceback

 {traceback}
diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
index acb77ec04..e7b88d1a2 100644
--- a/electrum/base_wizard.py
+++ b/electrum/base_wizard.py
@@ -92,7 +92,7 @@ class BaseWizard(Logger):
         self._stack = []  # type: List[WizardStackItem]
         self.plugin = None  # type: Optional[BasePlugin]
         self.keystores = []  # type: List[KeyStore]
-        self.is_kivy = config.get('gui') == 'kivy'
+        self.is_kivy = config.GUI_NAME == 'kivy'
         self.seed_type = None
 
     def set_icon(self, icon):
@@ -697,7 +697,7 @@ class BaseWizard(Logger):
         self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
 
     def choose_seed_type(self):
-        seed_type = 'standard' if self.config.get('nosegwit') else 'segwit'
+        seed_type = 'standard' if self.config.WIZARD_DONT_CREATE_SEGWIT else 'segwit'
         self.create_seed(seed_type)
 
     def create_seed(self, seed_type):
diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py
index e59e3e6d7..aefd39603 100644
--- a/electrum/coinchooser.py
+++ b/electrum/coinchooser.py
@@ -24,7 +24,7 @@
 # SOFTWARE.
 from collections import defaultdict
 from math import floor, log10
-from typing import NamedTuple, List, Callable, Sequence, Union, Dict, Tuple, Mapping, Type
+from typing import NamedTuple, List, Callable, Sequence, Union, Dict, Tuple, Mapping, Type, TYPE_CHECKING
 from decimal import Decimal
 
 from .bitcoin import sha256, COIN, is_address
@@ -32,6 +32,9 @@ from .transaction import Transaction, TxOutput, PartialTransaction, PartialTxInp
 from .util import NotEnoughFunds
 from .logging import Logger
 
+if TYPE_CHECKING:
+    from .simple_config import SimpleConfig
+
 
 # A simple deterministic PRNG.  Used to deterministically shuffle a
 # set of coins - the same set of coins should produce the same output.
@@ -484,13 +487,13 @@ COIN_CHOOSERS = {
     'Privacy': CoinChooserPrivacy,
 }  # type: Mapping[str, Type[CoinChooserBase]]
 
-def get_name(config):
-    kind = config.get('coin_chooser')
+def get_name(config: 'SimpleConfig') -> str:
+    kind = config.WALLET_COIN_CHOOSER_POLICY
     if kind not in COIN_CHOOSERS:
-        kind = 'Privacy'
+        kind = config.cv.WALLET_COIN_CHOOSER_POLICY.get_default_value()
     return kind
 
-def get_coin_chooser(config) -> CoinChooserBase:
+def get_coin_chooser(config: 'SimpleConfig') -> CoinChooserBase:
     klass = COIN_CHOOSERS[get_name(config)]
     # note: we enable enable_output_value_rounding by default as
     #       - for sacrificing a few satoshis
@@ -498,6 +501,6 @@ def get_coin_chooser(config) -> CoinChooserBase:
     #       + it also helps the network as a whole as fees will become noisier
     #         (trying to counter the heuristic that "whole integer sat/byte feerates" are common)
     coinchooser = klass(
-        enable_output_value_rounding=config.get('coin_chooser_output_rounding', True),
+        enable_output_value_rounding=config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING,
     )
     return coinchooser
diff --git a/electrum/commands.py b/electrum/commands.py
index 2038a6f85..336e69a47 100644
--- a/electrum/commands.py
+++ b/electrum/commands.py
@@ -308,7 +308,7 @@ class Commands:
 
     @classmethod
     def _setconfig_normalize_value(cls, key, value):
-        if key not in ('rpcuser', 'rpcpassword'):
+        if key not in (SimpleConfig.RPC_USERNAME.key(), SimpleConfig.RPC_PASSWORD.key()):
             value = json_decode(value)
             # call literal_eval for backward compatibility (see #4225)
             try:
@@ -321,9 +321,9 @@ class Commands:
     async def setconfig(self, key, value):
         """Set a configuration variable. 'value' may be a string or a Python expression."""
         value = self._setconfig_normalize_value(key, value)
-        if self.daemon and key == 'rpcuser':
+        if self.daemon and key == SimpleConfig.RPC_USERNAME.key():
             self.daemon.commands_server.rpc_user = value
-        if self.daemon and key == 'rpcpassword':
+        if self.daemon and key == SimpleConfig.RPC_PASSWORD.key():
             self.daemon.commands_server.rpc_password = value
         self.config.set_key(key, value)
         return True
@@ -1149,7 +1149,7 @@ class Commands:
 
     @command('wl')
     async def nodeid(self, wallet: Abstract_Wallet = None):
-        listen_addr = self.config.get('lightning_listen')
+        listen_addr = self.config.LIGHTNING_LISTEN
         return wallet.lnworker.node_keypair.pubkey.hex() + (('@' + listen_addr) if listen_addr else '')
 
     @command('wl')
@@ -1545,13 +1545,14 @@ argparse._SubParsersAction.__call__ = subparser_call
 
 
 def add_network_options(parser):
-    parser.add_argument("-f", "--serverfingerprint", dest="serverfingerprint", default=None, help="only allow connecting to servers with a matching SSL certificate SHA256 fingerprint." + " " +
-                                                                                                  "To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.")
-    parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=None, help="connect to one server only")
-    parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
-    parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port] (or 'none' to disable proxy), where type is socks4,socks5 or http")
-    parser.add_argument("--noonion", action="store_true", dest="noonion", default=None, help="do not try to connect to onion servers")
-    parser.add_argument("--skipmerklecheck", action="store_true", dest="skipmerklecheck", default=None, help="Tolerate invalid merkle proofs from server")
+    parser.add_argument("-f", "--serverfingerprint", dest=SimpleConfig.NETWORK_SERVERFINGERPRINT.key(), default=None,
+                        help="only allow connecting to servers with a matching SSL certificate SHA256 fingerprint. " +
+                             "To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.")
+    parser.add_argument("-1", "--oneserver", action="store_true", dest=SimpleConfig.NETWORK_ONESERVER.key(), default=None, help="connect to one server only")
+    parser.add_argument("-s", "--server", dest=SimpleConfig.NETWORK_SERVER.key(), default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
+    parser.add_argument("-p", "--proxy", dest=SimpleConfig.NETWORK_PROXY.key(), default=None, help="set proxy [type:]host[:port] (or 'none' to disable proxy), where type is socks4,socks5 or http")
+    parser.add_argument("--noonion", action="store_true", dest=SimpleConfig.NETWORK_NOONION.key(), default=None, help="do not try to connect to onion servers")
+    parser.add_argument("--skipmerklecheck", action="store_true", dest=SimpleConfig.NETWORK_SKIPMERKLECHECK.key(), default=None, help="Tolerate invalid merkle proofs from server")
 
 def add_global_options(parser):
     group = parser.add_argument_group('global options')
@@ -1563,13 +1564,13 @@ def add_global_options(parser):
     group.add_argument("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest")
     group.add_argument("--simnet", action="store_true", dest="simnet", default=False, help="Use Simnet")
     group.add_argument("--signet", action="store_true", dest="signet", default=False, help="Use Signet")
-    group.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
-    group.add_argument("--rpcuser", dest="rpcuser", default=argparse.SUPPRESS, help="RPC user")
-    group.add_argument("--rpcpassword", dest="rpcpassword", default=argparse.SUPPRESS, help="RPC password")
+    group.add_argument("-o", "--offline", action="store_true", dest=SimpleConfig.NETWORK_OFFLINE.key(), default=None, help="Run offline")
+    group.add_argument("--rpcuser", dest=SimpleConfig.RPC_USERNAME.key(), default=argparse.SUPPRESS, help="RPC user")
+    group.add_argument("--rpcpassword", dest=SimpleConfig.RPC_PASSWORD.key(), default=argparse.SUPPRESS, help="RPC password")
 
 def add_wallet_option(parser):
     parser.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
-    parser.add_argument("--forgetconfig", action="store_true", dest="forget_config", default=False, help="Forget config on exit")
+    parser.add_argument("--forgetconfig", action="store_true", dest=SimpleConfig.CONFIG_FORGET_CHANGES.key(), default=False, help="Forget config on exit")
 
 def get_parser():
     # create main parser
@@ -1582,11 +1583,11 @@ def get_parser():
     # gui
     parser_gui = subparsers.add_parser('gui', description="Run Electrum's Graphical User Interface.", help="Run GUI (default)")
     parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)")
-    parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'kivy', 'text', 'stdio', 'qml'])
-    parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup")
-    parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI")
+    parser_gui.add_argument("-g", "--gui", dest=SimpleConfig.GUI_NAME.key(), help="select graphical user interface", choices=['qt', 'kivy', 'text', 'stdio', 'qml'])
+    parser_gui.add_argument("-m", action="store_true", dest=SimpleConfig.GUI_QT_HIDE_ON_STARTUP.key(), default=False, help="hide GUI on startup")
+    parser_gui.add_argument("-L", "--lang", dest=SimpleConfig.LOCALIZATION_LANGUAGE.key(), default=None, help="default language used in GUI")
     parser_gui.add_argument("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed")
-    parser_gui.add_argument("--nosegwit", action="store_true", dest="nosegwit", default=False, help="Do not create segwit wallets")
+    parser_gui.add_argument("--nosegwit", action="store_true", dest=SimpleConfig.WIZARD_DONT_CREATE_SEGWIT.key(), default=False, help="Do not create segwit wallets")
     add_wallet_option(parser_gui)
     add_network_options(parser_gui)
     add_global_options(parser_gui)
@@ -1595,10 +1596,10 @@ def get_parser():
     parser_daemon.add_argument("-d", "--detached", action="store_true", dest="detach", default=False, help="run daemon in detached mode")
     # FIXME: all these options are rpc-server-side. The CLI client-side cannot use e.g. --rpcport,
     #        instead it reads it from the daemon lockfile.
-    parser_daemon.add_argument("--rpchost", dest="rpchost", default=argparse.SUPPRESS, help="RPC host")
-    parser_daemon.add_argument("--rpcport", dest="rpcport", type=int, default=argparse.SUPPRESS, help="RPC port")
-    parser_daemon.add_argument("--rpcsock", dest="rpcsock", default=None, help="what socket type to which to bind RPC daemon", choices=['unix', 'tcp', 'auto'])
-    parser_daemon.add_argument("--rpcsockpath", dest="rpcsockpath", help="where to place RPC file socket")
+    parser_daemon.add_argument("--rpchost", dest=SimpleConfig.RPC_HOST.key(), default=argparse.SUPPRESS, help="RPC host")
+    parser_daemon.add_argument("--rpcport", dest=SimpleConfig.RPC_PORT.key(), type=int, default=argparse.SUPPRESS, help="RPC port")
+    parser_daemon.add_argument("--rpcsock", dest=SimpleConfig.RPC_SOCKET_TYPE.key(), default=None, help="what socket type to which to bind RPC daemon", choices=['unix', 'tcp', 'auto'])
+    parser_daemon.add_argument("--rpcsockpath", dest=SimpleConfig.RPC_SOCKET_FILEPATH.key(), help="where to place RPC file socket")
     add_network_options(parser_daemon)
     add_global_options(parser_daemon)
     # commands
diff --git a/electrum/contacts.py b/electrum/contacts.py
index 69e8dc060..cc7906554 100644
--- a/electrum/contacts.py
+++ b/electrum/contacts.py
@@ -98,7 +98,7 @@ class Contacts(dict, Logger):
 
     def fetch_openalias(self, config):
         self.alias_info = None
-        alias = config.get('alias')
+        alias = config.OPENALIAS_ID
         if alias:
             alias = str(alias)
             def f():
diff --git a/electrum/daemon.py b/electrum/daemon.py
index 674e143bc..8b6485801 100644
--- a/electrum/daemon.py
+++ b/electrum/daemon.py
@@ -69,7 +69,7 @@ def get_rpcsock_defaultpath(config: SimpleConfig):
     return os.path.join(config.path, 'daemon_rpc_socket')
 
 def get_rpcsock_default_type(config: SimpleConfig):
-    if config.get('rpcport'):
+    if config.RPC_PORT:
         return 'tcp'
     # Use unix domain sockets when available,
     # with the extra paranoia that in case windows "implements" them,
@@ -106,7 +106,7 @@ def get_file_descriptor(config: SimpleConfig):
 
 
 
-def request(config: SimpleConfig, endpoint, args=(), timeout=60):
+def request(config: SimpleConfig, endpoint, args=(), timeout: Union[float, int] = 60):
     lockfile = get_lockfile(config)
     while True:
         create_time = None
@@ -152,12 +152,8 @@ def request(config: SimpleConfig, endpoint, args=(), timeout=60):
 
 
 def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
-    rpc_user = config.get('rpcuser', None)
-    rpc_password = config.get('rpcpassword', None)
-    if rpc_user == '':
-        rpc_user = None
-    if rpc_password == '':
-        rpc_password = None
+    rpc_user = config.RPC_USERNAME or None
+    rpc_password = config.RPC_PASSWORD or None
     if rpc_user is None or rpc_password is None:
         rpc_user = 'user'
         bits = 128
@@ -166,8 +162,8 @@ def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
         pw_b64 = b64encode(
             pw_int.to_bytes(nbytes, 'big'), b'-_')
         rpc_password = to_string(pw_b64, 'ascii')
-        config.set_key('rpcuser', rpc_user)
-        config.set_key('rpcpassword', rpc_password, save=True)
+        config.RPC_USERNAME = rpc_user
+        config.RPC_PASSWORD = rpc_password
     return rpc_user, rpc_password
 
 
@@ -252,17 +248,17 @@ class AuthenticatedServer(Logger):
 
 class CommandsServer(AuthenticatedServer):
 
-    def __init__(self, daemon, fd):
+    def __init__(self, daemon: 'Daemon', fd):
         rpc_user, rpc_password = get_rpc_credentials(daemon.config)
         AuthenticatedServer.__init__(self, rpc_user, rpc_password)
         self.daemon = daemon
         self.fd = fd
         self.config = daemon.config
-        sockettype = self.config.get('rpcsock', 'auto')
+        sockettype = self.config.RPC_SOCKET_TYPE
         self.socktype = sockettype if sockettype != 'auto' else get_rpcsock_default_type(self.config)
-        self.sockpath = self.config.get('rpcsockpath', get_rpcsock_defaultpath(self.config))
-        self.host = self.config.get('rpchost', '127.0.0.1')
-        self.port = self.config.get('rpcport', 0)
+        self.sockpath = self.config.RPC_SOCKET_FILEPATH or get_rpcsock_defaultpath(self.config)
+        self.host = self.config.RPC_HOST
+        self.port = self.config.RPC_PORT
         self.app = web.Application()
         self.app.router.add_post("/", self.handle)
         self.register_method(self.ping)
@@ -348,12 +344,12 @@ class CommandsServer(AuthenticatedServer):
 
 class WatchTowerServer(AuthenticatedServer):
 
-    def __init__(self, network, netaddress):
+    def __init__(self, network: 'Network', netaddress):
         self.addr = netaddress
         self.config = network.config
         self.network = network
-        watchtower_user = self.config.get('watchtower_user', '')
-        watchtower_password = self.config.get('watchtower_password', '')
+        watchtower_user = self.config.WATCHTOWER_SERVER_USER or ""
+        watchtower_password = self.config.WATCHTOWER_SERVER_PASSWORD or ""
         AuthenticatedServer.__init__(self, watchtower_user, watchtower_password)
         self.lnwatcher = network.local_watchtower
         self.app = web.Application()
@@ -403,7 +399,7 @@ class Daemon(Logger):
             self.logger.warning("Ignoring parameter 'wallet_path' for daemon. "
                                 "Use the load_wallet command instead.")
         self.asyncio_loop = util.get_asyncio_loop()
-        if not config.get('offline'):
+        if not self.config.NETWORK_OFFLINE:
             self.network = Network(config, daemon=self)
         self.fx = FxThread(config=config)
         # path -> wallet;   make sure path is standardized.
@@ -444,16 +440,16 @@ class Daemon(Logger):
 
     def start_network(self):
         self.logger.info(f"starting network.")
-        assert not self.config.get('offline')
+        assert not self.config.NETWORK_OFFLINE
         assert self.network
         # server-side watchtower
-        if watchtower_address := self.config.get_netaddress('watchtower_address'):
+        if watchtower_address := self.config.get_netaddress(self.config.cv.WATCHTOWER_SERVER_ADDRESS):
             self.watchtower = WatchTowerServer(self.network, watchtower_address)
             asyncio.run_coroutine_threadsafe(self.taskgroup.spawn(self.watchtower.run), self.asyncio_loop)
 
         self.network.start(jobs=[self.fx.run])
         # prepare lightning functionality, also load channel db early
-        if self.config.get('use_gossip', False):
+        if self.config.LIGHTNING_USE_GOSSIP:
             self.network.start_gossip()
 
     def with_wallet_lock(func):
@@ -582,7 +578,7 @@ class Daemon(Logger):
 
     def run_gui(self, config: 'SimpleConfig', plugins: 'Plugins'):
         threading.current_thread().name = 'GUI'
-        gui_name = config.get('gui', 'qt')
+        gui_name = config.GUI_NAME
         if gui_name in ['lite', 'classic']:
             gui_name = 'qt'
         self.logger.info(f'launching GUI: {gui_name}')
diff --git a/electrum/exchange_rate.py b/electrum/exchange_rate.py
index 8a0ac21e6..e3bdcc79e 100644
--- a/electrum/exchange_rate.py
+++ b/electrum/exchange_rate.py
@@ -23,11 +23,6 @@ from .simple_config import SimpleConfig
 from .logging import Logger
 
 
-DEFAULT_ENABLED = False
-DEFAULT_CURRENCY = "EUR"
-DEFAULT_EXCHANGE = "CoinGecko"  # default exchange should ideally provide historical rates
-
-
 # See https://en.wikipedia.org/wiki/ISO_4217
 CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,
                   'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0,
@@ -577,29 +572,29 @@ class FxThread(ThreadJob, EventListener):
             if self.is_enabled():
                 await self.exchange.update_safe(self.ccy)
 
-    def is_enabled(self):
-        return bool(self.config.get('use_exchange_rate', DEFAULT_ENABLED))
+    def is_enabled(self) -> bool:
+        return self.config.FX_USE_EXCHANGE_RATE
 
-    def set_enabled(self, b):
-        self.config.set_key('use_exchange_rate', bool(b))
+    def set_enabled(self, b: bool) -> None:
+        self.config.FX_USE_EXCHANGE_RATE = b
         self.trigger_update()
 
     def can_have_history(self):
         return self.is_enabled() and self.ccy in self.exchange.history_ccys()
 
     def has_history(self) -> bool:
-        return self.can_have_history() and bool(self.config.get('history_rates', False))
+        return self.can_have_history() and self.config.FX_HISTORY_RATES
 
     def get_currency(self) -> str:
         '''Use when dynamic fetching is needed'''
-        return self.config.get("currency", DEFAULT_CURRENCY)
+        return self.config.FX_CURRENCY
 
     def config_exchange(self):
-        return self.config.get('use_exchange', DEFAULT_EXCHANGE)
+        return self.config.FX_EXCHANGE
 
     def set_currency(self, ccy: str):
         self.ccy = ccy
-        self.config.set_key('currency', ccy, save=True)
+        self.config.FX_CURRENCY = ccy
         self.trigger_update()
         self.on_quotes()
 
@@ -608,10 +603,10 @@ class FxThread(ThreadJob, EventListener):
         loop.call_soon_threadsafe(self._trigger.set)
 
     def set_exchange(self, name):
-        class_ = globals().get(name) or globals().get(DEFAULT_EXCHANGE)
+        class_ = globals().get(name) or globals().get(self.config.cv.FX_EXCHANGE.get_default_value())
         self.logger.info(f"using exchange {name}")
         if self.config_exchange() != name:
-            self.config.set_key('use_exchange', name, save=True)
+            self.config.FX_EXCHANGE = name
         assert issubclass(class_, ExchangeBase), f"unexpected type {class_} for {name}"
         self.exchange = class_(self.on_quotes, self.on_history)  # type: ExchangeBase
         # A new exchange means new fx quotes, initially empty.  Force
@@ -689,4 +684,4 @@ class FxThread(ThreadJob, EventListener):
         return self.history_rate(date)
 
 
-assert globals().get(DEFAULT_EXCHANGE), f"default exchange {DEFAULT_EXCHANGE} does not exist"
+assert globals().get(SimpleConfig.FX_EXCHANGE.get_default_value()), f"default exchange {SimpleConfig.FX_EXCHANGE.get_default_value()} does not exist"
diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
index eb1a5fba5..6270436a2 100644
--- a/electrum/gui/kivy/main_window.py
+++ b/electrum/gui/kivy/main_window.py
@@ -25,6 +25,7 @@ from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
 from electrum.interface import PREFERRED_NETWORK_PROTOCOL, ServerAddr
 from electrum.logging import Logger
 from electrum.bitcoin import COIN
+from electrum.simple_config import SimpleConfig
 
 from electrum.gui import messages
 from .i18n import _
@@ -94,7 +95,6 @@ from .uix.dialogs.lightning_channels import LightningChannelsDialog, SwapDialog
 
 if TYPE_CHECKING:
     from . import ElectrumGui
-    from electrum.simple_config import SimpleConfig
     from electrum.plugin import Plugins
     from electrum.paymentrequest import PaymentRequest
 
@@ -133,7 +133,7 @@ class ElectrumWindow(App, Logger, EventListener):
     def set_auto_connect(self, b: bool):
         # This method makes sure we persist x into the config even if self.auto_connect == b.
         # Note: on_auto_connect() only gets called if the value of the self.auto_connect property *changes*.
-        self.electrum_config.set_key('auto_connect', b)
+        self.electrum_config.NETWORK_AUTO_CONNECT = b
         self.auto_connect = b
 
     def toggle_auto_connect(self, x):
@@ -196,7 +196,7 @@ class ElectrumWindow(App, Logger, EventListener):
 
     use_gossip = BooleanProperty(False)
     def on_use_gossip(self, instance, x):
-        self.electrum_config.set_key('use_gossip', self.use_gossip, save=True)
+        self.electrum_config.LIGHTNING_USE_GOSSIP = self.use_gossip
         if self.network:
             if self.use_gossip:
                 self.network.start_gossip()
@@ -206,7 +206,7 @@ class ElectrumWindow(App, Logger, EventListener):
 
     enable_debug_logs = BooleanProperty(False)
     def on_enable_debug_logs(self, instance, x):
-        self.electrum_config.set_key('gui_enable_debug_logs', self.enable_debug_logs, save=True)
+        self.electrum_config.GUI_ENABLE_DEBUG_LOGS = self.enable_debug_logs
 
     use_change = BooleanProperty(False)
     def on_use_change(self, instance, x):
@@ -217,11 +217,11 @@ class ElectrumWindow(App, Logger, EventListener):
 
     use_unconfirmed = BooleanProperty(False)
     def on_use_unconfirmed(self, instance, x):
-        self.electrum_config.set_key('confirmed_only', not self.use_unconfirmed, save=True)
+        self.electrum_config.WALLET_SPEND_CONFIRMED_ONLY = not self.use_unconfirmed
 
     use_recoverable_channels = BooleanProperty(True)
     def on_use_recoverable_channels(self, instance, x):
-        self.electrum_config.set_key('use_recoverable_channels', self.use_recoverable_channels, save=True)
+        self.electrum_config.LIGHTNING_USE_RECOVERABLE_CHANNELS = self.use_recoverable_channels
 
     def switch_to_send_screen(func):
         # try until send_screen is available
@@ -414,7 +414,7 @@ class ElectrumWindow(App, Logger, EventListener):
         Logger.__init__(self)
 
         self.electrum_config = config = kwargs.get('config', None)  # type: SimpleConfig
-        self.language = config.get('language', get_default_language())
+        self.language = config.LOCALIZATION_LANGUAGE or get_default_language()
         self.network = network = kwargs.get('network', None)  # type: Network
         if self.network:
             self.num_blocks = self.network.get_local_height()
@@ -431,9 +431,9 @@ class ElectrumWindow(App, Logger, EventListener):
         self.gui_object = kwargs.get('gui_object', None)  # type: ElectrumGui
         self.daemon = self.gui_object.daemon
         self.fx = self.daemon.fx
-        self.use_gossip = config.get('use_gossip', False)
-        self.use_unconfirmed = not config.get('confirmed_only', False)
-        self.enable_debug_logs = config.get('gui_enable_debug_logs', False)
+        self.use_gossip = config.LIGHTNING_USE_GOSSIP
+        self.use_unconfirmed = not config.WALLET_SPEND_CONFIRMED_ONLY
+        self.enable_debug_logs = config.GUI_ENABLE_DEBUG_LOGS
 
         # create triggers so as to minimize updating a max of 2 times a sec
         self._trigger_update_wallet = Clock.create_trigger(self.update_wallet, .5)
@@ -644,7 +644,7 @@ class ElectrumWindow(App, Logger, EventListener):
             self.on_new_intent(mactivity.getIntent())
             activity.bind(on_new_intent=self.on_new_intent)
         self.register_callbacks()
-        if self.network and self.electrum_config.get('auto_connect') is None:
+        if self.network and not self.electrum_config.cv.NETWORK_AUTO_CONNECT.is_set():
             self.popup_dialog("first_screen")
             # load_wallet_on_start will be called later, after initial network setup is completed
         else:
@@ -676,7 +676,7 @@ class ElectrumWindow(App, Logger, EventListener):
 
     def on_wizard_success(self, storage, db, password):
         self.password = password
-        if self.electrum_config.get('single_password'):
+        if self.electrum_config.WALLET_USE_SINGLE_PASSWORD:
             self._use_single_password = self.daemon.update_password_for_directory(
                 old_password=password, new_password=password)
         self.logger.info(f'use single password: {self._use_single_password}')
@@ -811,7 +811,7 @@ class ElectrumWindow(App, Logger, EventListener):
             Clock.schedule_once(lambda dt: self._channels_dialog.update())
 
     def is_wallet_creation_disabled(self):
-        return bool(self.electrum_config.get('single_password')) and self.password is None
+        return self.electrum_config.WALLET_USE_SINGLE_PASSWORD and self.password is None
 
     def wallets_dialog(self):
         from .uix.dialogs.wallets import WalletDialog
@@ -1278,7 +1278,7 @@ class ElectrumWindow(App, Logger, EventListener):
         self.set_fee_status()
 
     def protected(self, msg, f, args):
-        if self.electrum_config.get('pin_code'):
+        if self.electrum_config.CONFIG_PIN_CODE:
             msg += "\n" + _("Enter your PIN code to proceed")
             on_success = lambda pw: f(*args, self.password)
             d = PincodeDialog(
@@ -1337,10 +1337,10 @@ class ElectrumWindow(App, Logger, EventListener):
             label.data += '\n\n' + _('Passphrase') + ': ' + passphrase
 
     def has_pin_code(self):
-        return bool(self.electrum_config.get('pin_code'))
+        return bool(self.electrum_config.CONFIG_PIN_CODE)
 
     def check_pin_code(self, pin):
-        if pin != self.electrum_config.get('pin_code'):
+        if pin != self.electrum_config.CONFIG_PIN_CODE:
             raise InvalidPassword
 
     def change_password(self, cb):
@@ -1386,7 +1386,7 @@ class ElectrumWindow(App, Logger, EventListener):
         d.open()
 
     def _set_new_pin_code(self, new_pin, cb):
-        self.electrum_config.set_key('pin_code', new_pin)
+        self.electrum_config.CONFIG_PIN_CODE = new_pin
         cb()
         self.show_info(_("PIN updated") if new_pin else _('PIN disabled'))
 
diff --git a/electrum/gui/kivy/uix/dialogs/crash_reporter.py b/electrum/gui/kivy/uix/dialogs/crash_reporter.py
index f5aa0a9da..d28437f44 100644
--- a/electrum/gui/kivy/uix/dialogs/crash_reporter.py
+++ b/electrum/gui/kivy/uix/dialogs/crash_reporter.py
@@ -1,5 +1,6 @@
 import sys
 import json
+from typing import TYPE_CHECKING
 
 from aiohttp.client_exceptions import ClientError
 from kivy import base, utils
@@ -15,6 +16,9 @@ from electrum.gui.kivy.i18n import _
 from electrum.base_crash_reporter import BaseCrashReporter, EarlyExceptionsQueue
 from electrum.logging import Logger
 
+if TYPE_CHECKING:
+    from electrum.gui.kivy.main_window import ElectrumWindow
+
 
 Builder.load_string('''
 
@@ -95,7 +99,7 @@ class CrashReporter(BaseCrashReporter, Factory.Popup):
  * Locale: {locale}
         """
 
-    def __init__(self, main_window, exctype, value, tb):
+    def __init__(self, main_window: 'ElectrumWindow', exctype, value, tb):
         BaseCrashReporter.__init__(self, exctype, value, tb)
         Factory.Popup.__init__(self)
         self.main_window = main_window
@@ -156,7 +160,7 @@ class CrashReporter(BaseCrashReporter, Factory.Popup):
         currentActivity.startActivity(browserIntent)
 
     def show_never(self):
-        self.main_window.electrum_config.set_key(BaseCrashReporter.config_key, False)
+        self.main_window.electrum_config.SHOW_CRASH_REPORTER = False
         self.dismiss()
 
     def get_user_description(self):
@@ -175,11 +179,11 @@ class CrashReportDetails(Factory.Popup):
 
 
 class ExceptionHook(base.ExceptionHandler, Logger):
-    def __init__(self, main_window):
+    def __init__(self, main_window: 'ElectrumWindow'):
         base.ExceptionHandler.__init__(self)
         Logger.__init__(self)
         self.main_window = main_window
-        if not main_window.electrum_config.get(BaseCrashReporter.config_key, default=True):
+        if not main_window.electrum_config.SHOW_CRASH_REPORTER:
             EarlyExceptionsQueue.set_hook_as_ready()  # flush already queued exceptions
             return
         # For exceptions in Kivy:
diff --git a/electrum/gui/kivy/uix/dialogs/fee_dialog.py b/electrum/gui/kivy/uix/dialogs/fee_dialog.py
index af0ab98b8..2fa2436e9 100644
--- a/electrum/gui/kivy/uix/dialogs/fee_dialog.py
+++ b/electrum/gui/kivy/uix/dialogs/fee_dialog.py
@@ -99,15 +99,15 @@ class FeeSliderDialog:
     def save_config(self):
         value = int(self.slider.value)
         dynfees, mempool = self.get_method()
-        self.config.set_key('dynamic_fees', dynfees, save=False)
-        self.config.set_key('mempool_fees', mempool, save=False)
+        self.config.FEE_EST_DYNAMIC = dynfees
+        self.config.FEE_EST_USE_MEMPOOL = mempool
         if dynfees:
             if mempool:
-                self.config.set_key('depth_level', value, save=True)
+                self.config.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS = value
             else:
-                self.config.set_key('fee_level', value, save=True)
+                self.config.FEE_EST_DYNAMIC_ETA_SLIDERPOS = value
         else:
-            self.config.set_key('fee_per_kb', self.config.static_fee(value), save=True)
+            self.config.FEE_EST_STATIC_FEERATE_FALLBACK = self.config.static_fee(value)
 
     def update_text(self):
         pass
diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py
index 7e5007464..3193fc130 100644
--- a/electrum/gui/kivy/uix/dialogs/settings.py
+++ b/electrum/gui/kivy/uix/dialogs/settings.py
@@ -146,7 +146,7 @@ class SettingsDialog(Factory.Popup):
         self.enable_toggle_use_recoverable_channels = bool(self.wallet.lnworker and self.wallet.lnworker.can_have_recoverable_channels())
 
     def get_language_name(self) -> str:
-        lang = self.config.get('language') or ''
+        lang = self.config.LOCALIZATION_LANGUAGE
         return languages.get(lang) or languages.get('') or ''
 
     def change_password(self, dt):
@@ -157,9 +157,9 @@ class SettingsDialog(Factory.Popup):
 
     def language_dialog(self, item, dt):
         if self._language_dialog is None:
-            l = self.config.get('language') or ''
+            l = self.config.LOCALIZATION_LANGUAGE
             def cb(key):
-                self.config.set_key("language", key, save=True)
+                self.config.LOCALIZATION_LANGUAGE = key
                 item.lang = self.get_language_name()
                 self.app.language = key
             self._language_dialog = ChoiceDialog(_('Language'), languages, l, cb)
@@ -194,7 +194,7 @@ class SettingsDialog(Factory.Popup):
             choosers = sorted(coinchooser.COIN_CHOOSERS.keys())
             chooser_name = coinchooser.get_name(self.config)
             def cb(text):
-                self.config.set_key('coin_chooser', text)
+                self.config.WALLET_COIN_CHOOSER_POLICY = text
                 item.status = text
             self._coinselect_dialog = ChoiceDialog(_('Coin selection'), choosers, chooser_name, cb)
         self._coinselect_dialog.open()
diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
index 9359fc207..23556ac92 100644
--- a/electrum/gui/kivy/uix/screens.py
+++ b/electrum/gui/kivy/uix/screens.py
@@ -480,7 +480,7 @@ class ReceiveScreen(CScreen):
         self.expiration_text = pr_expiration_values[c]
 
     def expiry(self):
-        return self.app.electrum_config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
+        return self.app.electrum_config.WALLET_PAYREQ_EXPIRY_SECONDS
 
     def clear(self):
         self.address = ''
@@ -587,7 +587,7 @@ class ReceiveScreen(CScreen):
     def expiration_dialog(self, obj):
         from .dialogs.choice_dialog import ChoiceDialog
         def callback(c):
-            self.app.electrum_config.set_key('request_expiry', c)
+            self.app.electrum_config.WALLET_PAYREQ_EXPIRY_SECONDS = c
             self.expiration_text = pr_expiration_values[c]
         d = ChoiceDialog(_('Expiration date'), pr_expiration_values, self.expiry(), callback)
         d.open()
diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py
index c4f3c29c4..bf41bd932 100644
--- a/electrum/gui/qml/qeapp.py
+++ b/electrum/gui/qml/qeapp.py
@@ -272,7 +272,7 @@ class QEAppController(BaseCrashReporter, QObject):
 
     @pyqtSlot()
     def showNever(self):
-        self.config.set_key(BaseCrashReporter.config_key, False)
+        self.config.SHOW_CRASH_REPORTER = False
 
     @pyqtSlot(str)
     def setCrashUserText(self, text):
@@ -425,7 +425,7 @@ class Exception_Hook(QObject, Logger):
 
     @classmethod
     def maybe_setup(cls, *, config: 'SimpleConfig', wallet: 'Abstract_Wallet' = None, slot = None) -> None:
-        if not config.get(BaseCrashReporter.config_key, default=True):
+        if not config.SHOW_CRASH_REPORTER:
             EarlyExceptionsQueue.set_hook_as_ready()  # flush already queued exceptions
             return
         if not cls._INSTANCE:
diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py
index fff270c00..d05e7d94f 100644
--- a/electrum/gui/qml/qechannelopener.py
+++ b/electrum/gui/qml/qechannelopener.py
@@ -1,6 +1,7 @@
 import threading
 from concurrent.futures import CancelledError
 from asyncio.exceptions import TimeoutError
+from typing import TYPE_CHECKING, Optional
 
 from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
 
@@ -32,7 +33,7 @@ class QEChannelOpener(QObject, AuthMixin):
     def __init__(self, parent=None):
         super().__init__(parent)
 
-        self._wallet = None
+        self._wallet = None  # type: Optional[QEWallet]
         self._connect_str = None
         self._amount = QEAmount()
         self._valid = False
@@ -101,7 +102,7 @@ class QEChannelOpener(QObject, AuthMixin):
         connect_str_valid = False
         if self._connect_str:
             self._logger.debug(f'checking if {self._connect_str=!r} is valid')
-            if not self._wallet.wallet.config.get('use_gossip', False):
+            if not self._wallet.wallet.config.LIGHTNING_USE_GOSSIP:
                 # using trampoline: connect_str is the name of a trampoline node
                 peer_addr = hardcoded_trampoline_nodes()[self._connect_str]
                 self._node_pubkey = peer_addr.pubkey
diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py
index 7e032028b..159e3e0d1 100644
--- a/electrum/gui/qml/qeconfig.py
+++ b/electrum/gui/qml/qeconfig.py
@@ -9,13 +9,11 @@ from electrum.i18n import set_language, languages
 from electrum.logging import get_logger
 from electrum.util import DECIMAL_POINT_DEFAULT, base_unit_name_to_decimal_point
 from electrum.invoices import PR_DEFAULT_EXPIRATION_WHEN_CREATING
+from electrum.simple_config import SimpleConfig
 
 from .qetypes import QEAmount
 from .auth import AuthMixin, auth_protect
 
-if TYPE_CHECKING:
-    from electrum.simple_config import SimpleConfig
-
 
 class QEConfig(AuthMixin, QObject):
     _logger = get_logger(__name__)
@@ -27,14 +25,14 @@ class QEConfig(AuthMixin, QObject):
     languageChanged = pyqtSignal()
     @pyqtProperty(str, notify=languageChanged)
     def language(self):
-        return self.config.get('language')
+        return self.config.LOCALIZATION_LANGUAGE
 
     @language.setter
     def language(self, language):
         if language not in languages:
             return
-        if self.config.get('language') != language:
-            self.config.set_key('language', language)
+        if self.config.LOCALIZATION_LANGUAGE != language:
+            self.config.LOCALIZATION_LANGUAGE = language
             set_language(language)
             self.languageChanged.emit()
 
@@ -51,27 +49,17 @@ class QEConfig(AuthMixin, QObject):
     autoConnectChanged = pyqtSignal()
     @pyqtProperty(bool, notify=autoConnectChanged)
     def autoConnect(self):
-        return self.config.get('auto_connect')
+        return self.config.NETWORK_AUTO_CONNECT
 
     @autoConnect.setter
     def autoConnect(self, auto_connect):
-        self.config.set_key('auto_connect', auto_connect, save=True)
+        self.config.NETWORK_AUTO_CONNECT = auto_connect
         self.autoConnectChanged.emit()
 
     # auto_connect is actually a tri-state, expose the undefined case
     @pyqtProperty(bool, notify=autoConnectChanged)
     def autoConnectDefined(self):
-        return self.config.get('auto_connect') is not None
-
-    manualServerChanged = pyqtSignal()
-    @pyqtProperty(bool, notify=manualServerChanged)
-    def manualServer(self):
-        return self.config.get('oneserver')
-
-    @manualServer.setter
-    def manualServer(self, oneserver):
-        self.config.set_key('oneserver', oneserver, save=True)
-        self.manualServerChanged.emit()
+        return self.config.cv.NETWORK_AUTO_CONNECT.is_set()
 
     baseUnitChanged = pyqtSignal()
     @pyqtProperty(str, notify=baseUnitChanged)
@@ -98,129 +86,129 @@ class QEConfig(AuthMixin, QObject):
     thousandsSeparatorChanged = pyqtSignal()
     @pyqtProperty(bool, notify=thousandsSeparatorChanged)
     def thousandsSeparator(self):
-        return self.config.get('amt_add_thousands_sep', False)
+        return self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP
 
     @thousandsSeparator.setter
     def thousandsSeparator(self, checked):
-        self.config.set_key('amt_add_thousands_sep', checked)
+        self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP = checked
         self.config.amt_add_thousands_sep = checked
         self.thousandsSeparatorChanged.emit()
 
     spendUnconfirmedChanged = pyqtSignal()
     @pyqtProperty(bool, notify=spendUnconfirmedChanged)
     def spendUnconfirmed(self):
-        return not self.config.get('confirmed_only', False)
+        return not self.config.WALLET_SPEND_CONFIRMED_ONLY
 
     @spendUnconfirmed.setter
     def spendUnconfirmed(self, checked):
-        self.config.set_key('confirmed_only', not checked, save=True)
+        self.config.WALLET_SPEND_CONFIRMED_ONLY = not checked
         self.spendUnconfirmedChanged.emit()
 
     requestExpiryChanged = pyqtSignal()
     @pyqtProperty(int, notify=requestExpiryChanged)
     def requestExpiry(self):
-        return self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
+        return self.config.WALLET_PAYREQ_EXPIRY_SECONDS
 
     @requestExpiry.setter
     def requestExpiry(self, expiry):
-        self.config.set_key('request_expiry', expiry)
+        self.config.WALLET_PAYREQ_EXPIRY_SECONDS = expiry
         self.requestExpiryChanged.emit()
 
     pinCodeChanged = pyqtSignal()
     @pyqtProperty(str, notify=pinCodeChanged)
     def pinCode(self):
-        return self.config.get('pin_code', '')
+        return self.config.CONFIG_PIN_CODE or ""
 
     @pinCode.setter
     def pinCode(self, pin_code):
         if pin_code == '':
             self.pinCodeRemoveAuth()
         else:
-            self.config.set_key('pin_code', pin_code, save=True)
+            self.config.CONFIG_PIN_CODE = pin_code
             self.pinCodeChanged.emit()
 
     @auth_protect(method='wallet')
     def pinCodeRemoveAuth(self):
-        self.config.set_key('pin_code', '', save=True)
+        self.config.CONFIG_PIN_CODE = ""
         self.pinCodeChanged.emit()
 
     useGossipChanged = pyqtSignal()
     @pyqtProperty(bool, notify=useGossipChanged)
     def useGossip(self):
-        return self.config.get('use_gossip', False)
+        return self.config.LIGHTNING_USE_GOSSIP
 
     @useGossip.setter
     def useGossip(self, gossip):
-        self.config.set_key('use_gossip', gossip)
+        self.config.LIGHTNING_USE_GOSSIP = gossip
         self.useGossipChanged.emit()
 
     useFallbackAddressChanged = pyqtSignal()
     @pyqtProperty(bool, notify=useFallbackAddressChanged)
     def useFallbackAddress(self):
-        return self.config.get('bolt11_fallback', True)
+        return self.config.WALLET_BOLT11_FALLBACK
 
     @useFallbackAddress.setter
     def useFallbackAddress(self, use_fallback):
-        self.config.set_key('bolt11_fallback', use_fallback)
+        self.config.WALLET_BOLT11_FALLBACK = use_fallback
         self.useFallbackAddressChanged.emit()
 
     enableDebugLogsChanged = pyqtSignal()
     @pyqtProperty(bool, notify=enableDebugLogsChanged)
     def enableDebugLogs(self):
-        gui_setting = self.config.get('gui_enable_debug_logs', False)
+        gui_setting = self.config.GUI_ENABLE_DEBUG_LOGS
         return gui_setting or bool(self.config.get('verbosity'))
 
     @pyqtProperty(bool, notify=enableDebugLogsChanged)
     def canToggleDebugLogs(self):
-        gui_setting = self.config.get('gui_enable_debug_logs', False)
+        gui_setting = self.config.GUI_ENABLE_DEBUG_LOGS
         return not self.config.get('verbosity') or gui_setting
 
     @enableDebugLogs.setter
     def enableDebugLogs(self, enable):
-        self.config.set_key('gui_enable_debug_logs', enable)
+        self.config.GUI_ENABLE_DEBUG_LOGS = enable
         self.enableDebugLogsChanged.emit()
 
     useRecoverableChannelsChanged = pyqtSignal()
     @pyqtProperty(bool, notify=useRecoverableChannelsChanged)
     def useRecoverableChannels(self):
-        return self.config.get('use_recoverable_channels', True)
+        return self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS
 
     @useRecoverableChannels.setter
     def useRecoverableChannels(self, useRecoverableChannels):
-        self.config.set_key('use_recoverable_channels', useRecoverableChannels)
+        self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS = useRecoverableChannels
         self.useRecoverableChannelsChanged.emit()
 
     trustedcoinPrepayChanged = pyqtSignal()
     @pyqtProperty(int, notify=trustedcoinPrepayChanged)
     def trustedcoinPrepay(self):
-        return self.config.get('trustedcoin_prepay', 20)
+        return self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY
 
     @trustedcoinPrepay.setter
     def trustedcoinPrepay(self, num_prepay):
-        if num_prepay != self.config.get('trustedcoin_prepay', 20):
-            self.config.set_key('trustedcoin_prepay', num_prepay)
+        if num_prepay != self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY:
+            self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY = num_prepay
             self.trustedcoinPrepayChanged.emit()
 
     preferredRequestTypeChanged = pyqtSignal()
     @pyqtProperty(str, notify=preferredRequestTypeChanged)
     def preferredRequestType(self):
-        return self.config.get('preferred_request_type', 'bolt11')
+        return self.config.GUI_QML_PREFERRED_REQUEST_TYPE
 
     @preferredRequestType.setter
     def preferredRequestType(self, preferred_request_type):
-        if preferred_request_type != self.config.get('preferred_request_type', 'bolt11'):
-            self.config.set_key('preferred_request_type', preferred_request_type)
+        if preferred_request_type != self.config.GUI_QML_PREFERRED_REQUEST_TYPE:
+            self.config.GUI_QML_PREFERRED_REQUEST_TYPE = preferred_request_type
             self.preferredRequestTypeChanged.emit()
 
     userKnowsPressAndHoldChanged = pyqtSignal()
     @pyqtProperty(bool, notify=userKnowsPressAndHoldChanged)
     def userKnowsPressAndHold(self):
-        return self.config.get('user_knows_press_and_hold', False)
+        return self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD
 
     @userKnowsPressAndHold.setter
     def userKnowsPressAndHold(self, userKnowsPressAndHold):
-        if userKnowsPressAndHold != self.config.get('user_knows_press_and_hold', False):
-            self.config.set_key('user_knows_press_and_hold', userKnowsPressAndHold)
+        if userKnowsPressAndHold != self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD:
+            self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD = userKnowsPressAndHold
             self.userKnowsPressAndHoldChanged.emit()
 
 
@@ -251,7 +239,7 @@ class QEConfig(AuthMixin, QObject):
 
     # TODO delegate all this to config.py/util.py
     def decimal_point(self):
-        return self.config.get('decimal_point', DECIMAL_POINT_DEFAULT)
+        return self.config.BTC_AMOUNTS_DECIMAL_POINT
 
     def max_precision(self):
         return self.decimal_point() + 0 #self.extra_precision
diff --git a/electrum/gui/qml/qedaemon.py b/electrum/gui/qml/qedaemon.py
index 093d6a124..4d7c52d9d 100644
--- a/electrum/gui/qml/qedaemon.py
+++ b/electrum/gui/qml/qedaemon.py
@@ -162,7 +162,7 @@ class QEDaemon(AuthMixin, QObject):
         if path is None:
             self._path = self.daemon.config.get('wallet_path') # command line -w option
             if self._path is None:
-                self._path = self.daemon.config.get('gui_last_wallet')
+                self._path = self.daemon.config.GUI_LAST_WALLET
         else:
             self._path = path
         if self._path is None:
@@ -208,7 +208,7 @@ class QEDaemon(AuthMixin, QObject):
                     # we need the correct current wallet password below
                     local_password = QEWallet.getInstanceFor(wallet).password
 
-                if self.daemon.config.get('single_password'):
+                if self.daemon.config.WALLET_USE_SINGLE_PASSWORD:
                     self._use_single_password = self.daemon.update_password_for_directory(old_password=local_password, new_password=local_password)
                     self._password = local_password
                     self.singlePasswordChanged.emit()
diff --git a/electrum/gui/qml/qefx.py b/electrum/gui/qml/qefx.py
index c742d990d..57df12c72 100644
--- a/electrum/gui/qml/qefx.py
+++ b/electrum/gui/qml/qefx.py
@@ -72,12 +72,14 @@ class QEFX(QObject, QtEventListener):
     historicRatesChanged = pyqtSignal()
     @pyqtProperty(bool, notify=historicRatesChanged)
     def historicRates(self):
-        return bool(self.fx.config.get('history_rates', True))
+        if not self.fx.config.cv.FX_HISTORY_RATES.is_set():
+            self.fx.config.FX_HISTORY_RATES = True  # override default
+        return self.fx.config.FX_HISTORY_RATES
 
     @historicRates.setter
     def historicRates(self, checked):
         if checked != self.historicRates:
-            self.fx.config.set_key('history_rates', bool(checked))
+            self.fx.config.FX_HISTORY_RATES = bool(checked)
             self.historicRatesChanged.emit()
             self.rateSourcesChanged.emit()
 
diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py
index 6ef09e7c7..a6f2a395c 100644
--- a/electrum/gui/qml/qeinvoice.py
+++ b/electrum/gui/qml/qeinvoice.py
@@ -387,7 +387,7 @@ class QEInvoice(QObject, QtEventListener):
 
     def get_max_spendable_onchain(self):
         spendable = self._wallet.confirmedBalance.satsInt
-        if not self._wallet.wallet.config.get('confirmed_only', False):
+        if not self._wallet.wallet.config.WALLET_SPEND_CONFIRMED_ONLY:
             spendable += self._wallet.unconfirmedBalance.satsInt
         return spendable
 
diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py
index 59e621007..9748c1c60 100644
--- a/electrum/gui/qml/qetxfinalizer.py
+++ b/electrum/gui/qml/qetxfinalizer.py
@@ -110,15 +110,15 @@ class FeeSlider(QObject):
     def save_config(self):
         value = int(self._sliderPos)
         dynfees, mempool = self.get_method()
-        self._config.set_key('dynamic_fees', dynfees, save=False)
-        self._config.set_key('mempool_fees', mempool, save=False)
+        self._config.FEE_EST_DYNAMIC = dynfees
+        self._config.FEE_EST_USE_MEMPOOL = mempool
         if dynfees:
             if mempool:
-                self._config.set_key('depth_level', value, save=True)
+                self._config.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS = value
             else:
-                self._config.set_key('fee_level', value, save=True)
+                self._config.FEE_EST_DYNAMIC_ETA_SLIDERPOS = value
         else:
-            self._config.set_key('fee_per_kb', self._config.static_fee(value), save=True)
+            self._config.FEE_EST_STATIC_FEERATE_FALLBACK = self._config.static_fee(value)
         self.update_target()
         self.update()
 
diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py
index f06785a59..fdb54b98f 100644
--- a/electrum/gui/qt/__init__.py
+++ b/electrum/gui/qt/__init__.py
@@ -63,6 +63,7 @@ from electrum.wallet import Wallet, Abstract_Wallet
 from electrum.wallet_db import WalletDB
 from electrum.logging import Logger
 from electrum.gui import BaseElectrumGui
+from electrum.simple_config import SimpleConfig
 
 from .installwizard import InstallWizard, WalletAlreadyOpenInMemory
 from .util import read_QIcon, ColorScheme, custom_message_box, MessageBoxMixin
@@ -75,7 +76,6 @@ from .exception_window import Exception_Hook
 
 if TYPE_CHECKING:
     from electrum.daemon import Daemon
-    from electrum.simple_config import SimpleConfig
     from electrum.plugin import Plugins
 
 
@@ -139,7 +139,7 @@ class ElectrumGui(BaseElectrumGui, Logger):
         self.watchtower_dialog = None
         self._num_wizards_in_progress = 0
         self._num_wizards_lock = threading.Lock()
-        self.dark_icon = self.config.get("dark_icon", False)
+        self.dark_icon = self.config.GUI_QT_DARK_TRAY_ICON
         self.tray = None
         self._init_tray()
         self.app.new_window_signal.connect(self.start_new_window)
@@ -167,7 +167,7 @@ class ElectrumGui(BaseElectrumGui, Logger):
              - in Coins tab, the color for "frozen" UTXOs, or
              - in TxDialog, the receiving/change address colors
         """
-        use_dark_theme = self.config.get('qt_gui_color_theme', 'default') == 'dark'
+        use_dark_theme = self.config.GUI_QT_COLOR_THEME == 'dark'
         if use_dark_theme:
             try:
                 import qdarkstyle
@@ -219,7 +219,7 @@ class ElectrumGui(BaseElectrumGui, Logger):
         if not self.tray:
             return
         self.dark_icon = not self.dark_icon
-        self.config.set_key("dark_icon", self.dark_icon, save=True)
+        self.config.GUI_QT_DARK_TRAY_ICON = self.dark_icon
         self.tray.setIcon(self.tray_icon())
 
     def tray_activated(self, reason):
@@ -436,7 +436,7 @@ class ElectrumGui(BaseElectrumGui, Logger):
         """Start the network, including showing a first-start network dialog if config does not exist."""
         if self.daemon.network:
             # first-start network-setup
-            if self.config.get('auto_connect') is None:
+            if not self.config.cv.NETWORK_AUTO_CONNECT.is_set():
                 wizard = InstallWizard(self.config, self.app, self.plugins, gui_object=self)
                 wizard.init_network(self.daemon.network)
                 wizard.terminate()
diff --git a/electrum/gui/qt/address_list.py b/electrum/gui/qt/address_list.py
index 7989bbc2c..51effed92 100644
--- a/electrum/gui/qt/address_list.py
+++ b/electrum/gui/qt/address_list.py
@@ -36,6 +36,7 @@ from electrum.util import block_explorer_URL, profiler
 from electrum.plugin import run_hook
 from electrum.bitcoin import is_address
 from electrum.wallet import InternalAddressCorruption
+from electrum.simple_config import SimpleConfig
 
 from .util import MONOSPACE_FONT, ColorScheme, webopen
 from .my_treeview import MyTreeView, MySortModel
@@ -115,23 +116,24 @@ class AddressList(MyTreeView):
         self.setModel(self.proxy)
         self.update()
         self.sortByColumn(self.Columns.TYPE, Qt.AscendingOrder)
+        if self.config:
+            self.configvar_show_toolbar = self.config.cv.GUI_QT_ADDRESSES_TAB_SHOW_TOOLBAR
 
     def on_double_click(self, idx):
         addr = self.get_role_data_for_current_item(col=0, role=self.ROLE_ADDRESS_STR)
         self.main_window.show_address(addr)
 
-    CONFIG_KEY_SHOW_TOOLBAR = "show_toolbar_addresses"
     def create_toolbar(self, config):
         toolbar, menu = self.create_toolbar_with_menu('')
         self.num_addr_label = toolbar.itemAt(0).widget()
         self._toolbar_checkbox = menu.addToggle(_("Show Filter"), lambda: self.toggle_toolbar())
-        menu.addConfig(_('Show Fiat balances'), 'fiat_address', False, callback=self.main_window.app.update_fiat_signal.emit)
+        menu.addConfig(_('Show Fiat balances'), config.cv.FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES, callback=self.main_window.app.update_fiat_signal.emit)
         hbox = self.create_toolbar_buttons()
         toolbar.insertLayout(1, hbox)
         return toolbar
 
     def should_show_fiat(self):
-        return self.main_window.fx and self.main_window.fx.is_enabled() and self.config.get('fiat_address', False)
+        return self.main_window.fx and self.main_window.fx.is_enabled() and self.config.FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES
 
     def get_toolbar_buttons(self):
         return self.change_button, self.used_button
diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py
index ac0c118aa..de5dc296d 100644
--- a/electrum/gui/qt/confirm_tx_dialog.py
+++ b/electrum/gui/qt/confirm_tx_dialog.py
@@ -102,9 +102,9 @@ class TxEditor(WindowModalDialog):
         vbox.addStretch(1)
         vbox.addLayout(buttons)
 
-        self.set_io_visible(self.config.get('show_tx_io', False))
-        self.set_fee_edit_visible(self.config.get('show_tx_fee_details', False))
-        self.set_locktime_visible(self.config.get('show_tx_locktime', False))
+        self.set_io_visible(self.config.GUI_QT_TX_EDITOR_SHOW_IO)
+        self.set_fee_edit_visible(self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS)
+        self.set_locktime_visible(self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME)
         self.update_fee_target()
         self.resize(self.layout().sizeHint())
 
@@ -127,11 +127,11 @@ class TxEditor(WindowModalDialog):
     def set_fee_config(self, dyn, pos, fee_rate):
         if dyn:
             if self.config.use_mempool_fees():
-                self.config.set_key('depth_level', pos, save=False)
+                self.config.cv.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS.set(pos, save=False)
             else:
-                self.config.set_key('fee_level', pos, save=False)
+                self.config.cv.FEE_EST_DYNAMIC_ETA_SLIDERPOS.set(pos, save=False)
         else:
-            self.config.set_key('fee_per_kb', fee_rate, save=False)
+            self.config.cv.FEE_EST_STATIC_FEERATE_FALLBACK.set(fee_rate, save=False)
 
     def update_tx(self, *, fallback_to_zero_fee: bool = False):
         # expected to set self.tx, self.message and self.error
@@ -383,15 +383,15 @@ class TxEditor(WindowModalDialog):
             m.setToolTip(tooltip)
             return m
         add_pref_action(
-            self.config.get('show_tx_io', False),
+            self.config.GUI_QT_TX_EDITOR_SHOW_IO,
             self.toggle_io_visibility,
             _('Show inputs and outputs'), '')
         add_pref_action(
-            self.config.get('show_tx_fee_details', False),
+            self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS,
             self.toggle_fee_details,
             _('Edit fees manually'), '')
         add_pref_action(
-            self.config.get('show_tx_locktime', False),
+            self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME,
             self.toggle_locktime,
             _('Edit Locktime'), '')
         self.pref_menu.addSeparator()
@@ -410,18 +410,18 @@ class TxEditor(WindowModalDialog):
             ]))
         self.use_multi_change_menu.setEnabled(self.wallet.use_change)
         add_pref_action(
-            self.config.get('batch_rbf', False),
+            self.config.WALLET_BATCH_RBF,
             self.toggle_batch_rbf,
             _('Batch unconfirmed transactions'),
             _('If you check this box, your unconfirmed transactions will be consolidated into a single transaction.') + '\n' + \
             _('This will save fees, but might have unwanted effects in terms of privacy'))
         add_pref_action(
-            self.config.get('confirmed_only', False),
+            self.config.WALLET_SPEND_CONFIRMED_ONLY,
             self.toggle_confirmed_only,
             _('Spend only confirmed coins'),
             _('Spend only confirmed inputs.'))
         add_pref_action(
-            self.config.get('coin_chooser_output_rounding', True),
+            self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING,
             self.toggle_output_rounding,
             _('Enable output value rounding'),
             _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' + \
@@ -445,8 +445,8 @@ class TxEditor(WindowModalDialog):
         self.resize(size)
 
     def toggle_output_rounding(self):
-        b = not self.config.get('coin_chooser_output_rounding', True)
-        self.config.set_key('coin_chooser_output_rounding', b)
+        b = not self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING
+        self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = b
         self.trigger_update()
 
     def toggle_use_change(self):
@@ -461,30 +461,30 @@ class TxEditor(WindowModalDialog):
         self.trigger_update()
 
     def toggle_batch_rbf(self):
-        b = not self.config.get('batch_rbf', False)
-        self.config.set_key('batch_rbf', b)
+        b = not self.config.WALLET_BATCH_RBF
+        self.config.WALLET_BATCH_RBF = b
         self.trigger_update()
 
     def toggle_confirmed_only(self):
-        b = not self.config.get('confirmed_only', False)
-        self.config.set_key('confirmed_only', b)
+        b = not self.config.WALLET_SPEND_CONFIRMED_ONLY
+        self.config.WALLET_SPEND_CONFIRMED_ONLY = b
         self.trigger_update()
 
     def toggle_io_visibility(self):
-        b = not self.config.get('show_tx_io', False)
-        self.config.set_key('show_tx_io', b)
+        b = not self.config.GUI_QT_TX_EDITOR_SHOW_IO
+        self.config.GUI_QT_TX_EDITOR_SHOW_IO = b
         self.set_io_visible(b)
         self.resize_to_fit_content()
 
     def toggle_fee_details(self):
-        b = not self.config.get('show_tx_fee_details', False)
-        self.config.set_key('show_tx_fee_details', b)
+        b = not self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS
+        self.config.GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS = b
         self.set_fee_edit_visible(b)
         self.resize_to_fit_content()
 
     def toggle_locktime(self):
-        b = not self.config.get('show_tx_locktime', False)
-        self.config.set_key('show_tx_locktime', b)
+        b = not self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME
+        self.config.GUI_QT_TX_EDITOR_SHOW_LOCKTIME = b
         self.set_locktime_visible(b)
         self.resize_to_fit_content()
 
@@ -524,7 +524,7 @@ class TxEditor(WindowModalDialog):
         self._update_amount_label()
         if self.not_enough_funds:
             self.error = _('Not enough funds.')
-            confirmed_only = self.config.get('confirmed_only', False)
+            confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY
             if confirmed_only and self.can_pay_assuming_zero_fees(confirmed_only=False):
                 self.error += ' ' + _('Change your settings to allow spending unconfirmed coins.')
             elif self.can_pay_assuming_zero_fees(confirmed_only=confirmed_only):
@@ -631,7 +631,7 @@ class ConfirmTxDialog(TxEditor):
 
     def update_tx(self, *, fallback_to_zero_fee: bool = False):
         fee_estimator = self.get_fee_estimator()
-        confirmed_only = self.config.get('confirmed_only', False)
+        confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY
         try:
             self.tx = self.make_tx(fee_estimator, confirmed_only=confirmed_only)
             self.not_enough_funds = False
diff --git a/electrum/gui/qt/exception_window.py b/electrum/gui/qt/exception_window.py
index d549d75d1..820d7db1a 100644
--- a/electrum/gui/qt/exception_window.py
+++ b/electrum/gui/qt/exception_window.py
@@ -132,7 +132,7 @@ class Exception_Window(BaseCrashReporter, QWidget, MessageBoxMixin, Logger):
         self.close()
 
     def show_never(self):
-        self.config.set_key(BaseCrashReporter.config_key, False)
+        self.config.SHOW_CRASH_REPORTER = False
         self.close()
 
     def closeEvent(self, event):
@@ -177,7 +177,7 @@ class Exception_Hook(QObject, Logger):
 
     @classmethod
     def maybe_setup(cls, *, config: 'SimpleConfig', wallet: 'Abstract_Wallet' = None) -> None:
-        if not config.get(BaseCrashReporter.config_key, default=True):
+        if not config.SHOW_CRASH_REPORTER:
             EarlyExceptionsQueue.set_hook_as_ready()  # flush already queued exceptions
             return
         if not cls._INSTANCE:
diff --git a/electrum/gui/qt/fee_slider.py b/electrum/gui/qt/fee_slider.py
index 05f681426..34bb0cca7 100644
--- a/electrum/gui/qt/fee_slider.py
+++ b/electrum/gui/qt/fee_slider.py
@@ -23,8 +23,8 @@ class FeeComboBox(QComboBox):
         )
 
     def on_fee_type(self, x):
-        self.config.set_key('mempool_fees', x==2)
-        self.config.set_key('dynamic_fees', x>0)
+        self.config.FEE_EST_USE_MEMPOOL = (x == 2)
+        self.config.FEE_EST_DYNAMIC = (x > 0)
         self.fee_slider.update()
 
 
diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py
index 83c64be51..ce4951fef 100644
--- a/electrum/gui/qt/history_list.py
+++ b/electrum/gui/qt/history_list.py
@@ -47,6 +47,7 @@ from electrum.util import (block_explorer_URL, profiler, TxMinedInfo,
                            OrderedDictWithIndex, timestamp_to_datetime,
                            Satoshis, Fiat, format_time)
 from electrum.logging import get_logger, Logger
+from electrum.simple_config import SimpleConfig
 
 from .custom_model import CustomNode, CustomModel
 from .util import (read_QIcon, MONOSPACE_FONT, Buttons, CancelButton, OkButton,
@@ -252,7 +253,7 @@ class HistoryModel(CustomModel, Logger):
         return True
 
     def should_show_fiat(self):
-        if not bool(self.window.config.get('history_rates', False)):
+        if not self.window.config.FX_HISTORY_RATES:
             return False
         fx = self.window.fx
         if not fx or not fx.is_enabled():
@@ -260,7 +261,7 @@ class HistoryModel(CustomModel, Logger):
         return fx.has_history()
 
     def should_show_capital_gains(self):
-        return self.should_show_fiat() and self.window.config.get('history_rates_capital_gains', False)
+        return self.should_show_fiat() and self.window.config.FX_HISTORY_RATES_CAPITAL_GAINS
 
     @profiler
     def refresh(self, reason: str):
@@ -518,6 +519,8 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
         for col in HistoryColumns:
             sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents
             self.header().setSectionResizeMode(col, sm)
+        if self.config:
+            self.configvar_show_toolbar = self.config.cv.GUI_QT_HISTORY_TAB_SHOW_TOOLBAR
 
     def update(self):
         self.hm.refresh('HistoryList.update()')
@@ -546,13 +549,12 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
             self.end_button.setText(_('To') + ' ' + self.format_date(self.end_date))
         self.hide_rows()
 
-    CONFIG_KEY_SHOW_TOOLBAR = "show_toolbar_history"
     def create_toolbar(self, config):
         toolbar, menu = self.create_toolbar_with_menu('')
         self.num_tx_label = toolbar.itemAt(0).widget()
         self._toolbar_checkbox = menu.addToggle(_("Filter by Date"), lambda: self.toggle_toolbar())
-        self.menu_fiat = menu.addConfig(_('Show Fiat Values'), 'history_rates', False, callback=self.main_window.app.update_fiat_signal.emit)
-        self.menu_capgains = menu.addConfig(_('Show Capital Gains'), 'history_rates_capital_gains', False, callback=self.main_window.app.update_fiat_signal.emit)
+        self.menu_fiat = menu.addConfig(_('Show Fiat Values'), config.cv.FX_HISTORY_RATES, callback=self.main_window.app.update_fiat_signal.emit)
+        self.menu_capgains = menu.addConfig(_('Show Capital Gains'), config.cv.FX_HISTORY_RATES_CAPITAL_GAINS, callback=self.main_window.app.update_fiat_signal.emit)
         self.menu_summary = menu.addAction(_("&Summary"), self.show_summary)
         menu.addAction(_("&Plot"), self.plot_history_dialog)
         menu.addAction(_("&Export"), self.export_history_dialog)
diff --git a/electrum/gui/qt/installwizard.py b/electrum/gui/qt/installwizard.py
index 2ad1e73a1..0fb20d705 100644
--- a/electrum/gui/qt/installwizard.py
+++ b/electrum/gui/qt/installwizard.py
@@ -746,10 +746,10 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
             nlayout = NetworkChoiceLayout(network, self.config, wizard=True)
             if self.exec_layout(nlayout.layout()):
                 nlayout.accept()
-                self.config.set_key('auto_connect', network.auto_connect, save=True)
+                self.config.NETWORK_AUTO_CONNECT = network.auto_connect
         else:
             network.auto_connect = True
-            self.config.set_key('auto_connect', True, save=True)
+            self.config.NETWORK_AUTO_CONNECT = True
 
     @wizard_dialog
     def multisig_dialog(self, run_next):
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
index 6c568fe09..08eb6490f 100644
--- a/electrum/gui/qt/main_window.py
+++ b/electrum/gui/qt/main_window.py
@@ -242,7 +242,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
 
         self.setMinimumWidth(640)
         self.setMinimumHeight(400)
-        if self.config.get("is_maximized"):
+        if self.config.GUI_QT_WINDOW_IS_MAXIMIZED:
             self.showMaximized()
 
         self.setWindowIcon(read_QIcon("electrum.png"))
@@ -280,14 +280,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
         self.contacts.fetch_openalias(self.config)
 
         # If the option hasn't been set yet
-        if config.get('check_updates') is None:
+        if not config.cv.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS.is_set():
             choice = self.question(title="Electrum - " + _("Enable update check"),
                                    msg=_("For security reasons we advise that you always use the latest version of Electrum.") + " " +
                                        _("Would you like to be notified when there is a newer version of Electrum available?"))
-            config.set_key('check_updates', bool(choice), save=True)
+            config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = bool(choice)
 
         self._update_check_thread = None
-        if config.get('check_updates', False):
+        if config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS:
             # The references to both the thread and the window need to be stored somewhere
             # to prevent GC from getting in our way.
             def on_version_received(v):
@@ -497,7 +497,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
         self.channels_list.update()
         self.tabs.show()
         self.init_geometry()
-        if self.config.get('hide_gui') and self.gui_object.tray.isVisible():
+        if self.config.GUI_QT_HIDE_ON_STARTUP and self.gui_object.tray.isVisible():
             self.hide()
         else:
             self.show()
@@ -552,7 +552,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
         if not constants.net.TESTNET:
             return
         # user might have opted out already
-        if self.config.get('dont_show_testnet_warning', False):
+        if self.config.DONT_SHOW_TESTNET_WARNING:
             return
         # only show once per process lifecycle
         if getattr(self.gui_object, '_warned_testnet', False):
@@ -571,7 +571,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
         cb.stateChanged.connect(on_cb)
         self.show_warning(msg, title=_('Testnet'), checkbox=cb)
         if cb_checked:
-            self.config.set_key('dont_show_testnet_warning', True)
+            self.config.DONT_SHOW_TESTNET_WARNING = True
 
     def open_wallet(self):
         try:
@@ -585,10 +585,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
         self.gui_object.new_window(filename)
 
     def select_backup_dir(self, b):
-        name = self.config.get('backup_dir', '')
+        name = self.config.WALLET_BACKUP_DIRECTORY or ""
         dirname = QFileDialog.getExistingDirectory(self, "Select your wallet backup directory", name)
         if dirname:
-            self.config.set_key('backup_dir', dirname)
+            self.config.WALLET_BACKUP_DIRECTORY = dirname
             self.backup_dir_e.setText(dirname)
 
     def backup_wallet(self):
@@ -596,7 +596,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
         vbox = QVBoxLayout(d)
         grid = QGridLayout()
         backup_help = ""
-        backup_dir = self.config.get('backup_dir')
+        backup_dir = self.config.WALLET_BACKUP_DIRECTORY
         backup_dir_label = HelpLabel(_('Backup directory') + ':', backup_help)
         msg = _('Please select a backup directory')
         if self.wallet.has_lightning() and self.wallet.lnworker.channels:
@@ -628,7 +628,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
         return True
 
     def update_recently_visited(self, filename):
-        recent = self.config.get('recently_open', [])
+        recent = self.config.RECENTLY_OPEN_WALLET_FILES or []
         try:
             sorted(recent)
         except Exception:
@@ -638,7 +638,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
         recent.insert(0, filename)
         recent = [path for path in recent if os.path.exists(path)]
         recent = recent[:5]
-        self.config.set_key('recently_open', recent)
+        self.config.RECENTLY_OPEN_WALLET_FILES = recent
         self.recently_visited_menu.clear()
         for i, k in enumerate(sorted(recent)):
             b = os.path.basename(k)
@@ -2557,7 +2557,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
         for fut in coro_keys:
             fut.cancel()
         self.unregister_callbacks()
-        self.config.set_key("is_maximized", self.isMaximized())
+        self.config.GUI_QT_WINDOW_IS_MAXIMIZED = self.isMaximized()
         if not self.isMaximized():
             g = self.geometry()
             self.wallet.db.put("winpos-qt", [g.left(),g.top(),
diff --git a/electrum/gui/qt/my_treeview.py b/electrum/gui/qt/my_treeview.py
index c31c2ee15..da79f9554 100644
--- a/electrum/gui/qt/my_treeview.py
+++ b/electrum/gui/qt/my_treeview.py
@@ -59,6 +59,7 @@ from electrum.util import EventListener, event_listener
 from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING, PR_UNCONFIRMED
 from electrum.logging import Logger
 from electrum.qrreader import MissingQrDetectionLib
+from electrum.simple_config import ConfigVarWithConfig
 
 from .util import read_QIcon
 
@@ -80,17 +81,18 @@ class MyMenu(QMenu):
         m.setToolTip(tooltip)
         return m
 
-    def addConfig(self, text: str, name: str, default: bool, *, tooltip='', callback=None) -> QAction:
-        b = self.config.get(name, default)
-        m = self.addAction(text, lambda: self._do_toggle_config(name, default, callback))
+    def addConfig(self, text: str, configvar: 'ConfigVarWithConfig', *, tooltip='', callback=None) -> QAction:
+        assert isinstance(configvar, ConfigVarWithConfig), configvar
+        b = configvar.get()
+        m = self.addAction(text, lambda: self._do_toggle_config(configvar, callback=callback))
         m.setCheckable(True)
         m.setChecked(bool(b))
         m.setToolTip(tooltip)
         return m
 
-    def _do_toggle_config(self, name, default, callback):
-        b = self.config.get(name, default)
-        self.config.set_key(name, not b)
+    def _do_toggle_config(self, configvar: 'ConfigVarWithConfig', *, callback):
+        b = configvar.get()
+        configvar.set(not b)
         if callback:
             callback()
 
@@ -387,7 +389,7 @@ class MyTreeView(QTreeView):
         for row in range(self.model().rowCount()):
             self.hide_row(row)
 
-    def create_toolbar(self, config):
+    def create_toolbar(self, config: 'SimpleConfig'):
         return
 
     def create_toolbar_buttons(self):
@@ -402,13 +404,13 @@ class MyTreeView(QTreeView):
     def create_toolbar_with_menu(self, title):
         return create_toolbar_with_menu(self.config, title)
 
-    CONFIG_KEY_SHOW_TOOLBAR = None  # type: Optional[str]
+    configvar_show_toolbar = None  # type: Optional[ConfigVarWithConfig]
     _toolbar_checkbox = None  # type: Optional[QAction]
     def show_toolbar(self, state: bool = None):
         if state is None:  # get value from config
-            if self.config and self.CONFIG_KEY_SHOW_TOOLBAR:
-                state = self.config.get(self.CONFIG_KEY_SHOW_TOOLBAR, None)
-            if state is None:
+            if self.configvar_show_toolbar:
+                state = self.configvar_show_toolbar.get()
+            else:
                 return
         assert isinstance(state, bool), state
         if state == self.toolbar_shown:
@@ -428,8 +430,8 @@ class MyTreeView(QTreeView):
     def toggle_toolbar(self):
         new_state = not self.toolbar_shown
         self.show_toolbar(new_state)
-        if self.config and self.CONFIG_KEY_SHOW_TOOLBAR:
-            self.config.set_key(self.CONFIG_KEY_SHOW_TOOLBAR, new_state)
+        if self.configvar_show_toolbar:
+            self.configvar_show_toolbar.set(new_state)
 
     def add_copy_menu(self, menu: QMenu, idx) -> QMenu:
         cc = menu.addMenu(_("Copy"))
diff --git a/electrum/gui/qt/network_dialog.py b/electrum/gui/qt/network_dialog.py
index 7a08bd3fa..fb66903c0 100644
--- a/electrum/gui/qt/network_dialog.py
+++ b/electrum/gui/qt/network_dialog.py
@@ -41,14 +41,12 @@ from electrum.interface import ServerAddr, PREFERRED_NETWORK_PROTOCOL
 from electrum.network import Network
 from electrum.logging import get_logger
 from electrum.util import detect_tor_socks_proxy
+from electrum.simple_config import SimpleConfig
 
 from .util import (Buttons, CloseButton, HelpButton, read_QIcon, char_width_in_lineedit,
                    PasswordLineEdit)
 from .util import QtEventListener, qt_event_listener
 
-if TYPE_CHECKING:
-    from electrum.simple_config import SimpleConfig
-
 
 _logger = get_logger(__name__)
 
@@ -279,7 +277,7 @@ class NetworkChoiceLayout(object):
         grid.addWidget(HelpButton(msg), 0, 4)
 
         self.autoconnect_cb = QCheckBox(_('Select server automatically'))
-        self.autoconnect_cb.setEnabled(self.config.is_modifiable('auto_connect'))
+        self.autoconnect_cb.setEnabled(self.config.cv.NETWORK_AUTO_CONNECT.is_modifiable())
         self.autoconnect_cb.clicked.connect(self.set_server)
         self.autoconnect_cb.clicked.connect(self.update)
         msg = ' '.join([
@@ -327,13 +325,13 @@ class NetworkChoiceLayout(object):
             self.td = None
 
     def check_disable_proxy(self, b):
-        if not self.config.is_modifiable('proxy'):
+        if not self.config.cv.NETWORK_PROXY.is_modifiable():
             b = False
         for w in [self.proxy_mode, self.proxy_host, self.proxy_port, self.proxy_user, self.proxy_password]:
             w.setEnabled(b)
 
     def enable_set_server(self):
-        if self.config.is_modifiable('server'):
+        if self.config.cv.NETWORK_SERVER.is_modifiable():
             enabled = not self.autoconnect_cb.isChecked()
             self.server_e.setEnabled(enabled)
         else:
diff --git a/electrum/gui/qt/new_channel_dialog.py b/electrum/gui/qt/new_channel_dialog.py
index 36b589026..e71f21fa9 100644
--- a/electrum/gui/qt/new_channel_dialog.py
+++ b/electrum/gui/qt/new_channel_dialog.py
@@ -37,7 +37,7 @@ class NewChannelDialog(WindowModalDialog):
         toolbar, menu = create_toolbar_with_menu(self.config, '')
         recov_tooltip = messages.to_rtf(messages.MSG_RECOVERABLE_CHANNELS)
         menu.addConfig(
-            _("Create recoverable channels"), 'use_recoverable_channels', True,
+            _("Create recoverable channels"), self.config.cv.LIGHTNING_USE_RECOVERABLE_CHANNELS,
             tooltip=recov_tooltip,
         ).setEnabled(self.lnworker.can_have_recoverable_channels())
         vbox.addLayout(toolbar)
diff --git a/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py b/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py
index 9b25e1e76..856cb8d44 100644
--- a/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py
+++ b/electrum/gui/qt/qrreader/qtmultimedia/camera_dialog.py
@@ -130,7 +130,7 @@ class QrReaderCameraDialog(Logger, MessageBoxMixin, QDialog):
         # Flip horizontally checkbox with default coming from global config
         self.flip_x = QCheckBox()
         self.flip_x.setText(_("&Flip horizontally"))
-        self.flip_x.setChecked(bool(self.config.get('qrreader_flip_x', True)))
+        self.flip_x.setChecked(self.config.QR_READER_FLIP_X)
         self.flip_x.stateChanged.connect(self._on_flip_x_changed)
         controls_layout.addWidget(self.flip_x)
 
@@ -155,7 +155,7 @@ class QrReaderCameraDialog(Logger, MessageBoxMixin, QDialog):
         self.finished.connect(self._on_finished, Qt.QueuedConnection)
 
     def _on_flip_x_changed(self, _state: int):
-        self.config.set_key('qrreader_flip_x', self.flip_x.isChecked())
+        self.config.QR_READER_FLIP_X = self.flip_x.isChecked()
 
     def _get_resolution(self, resolutions: List[QSize], min_size: int) -> QSize:
         """
diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py
index e5a4ed4c0..3b279ee31 100644
--- a/electrum/gui/qt/receive_tab.py
+++ b/electrum/gui/qt/receive_tab.py
@@ -147,10 +147,10 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
         self.toolbar.insertWidget(2, self.toggle_view_button)
         # menu
         menu.addConfig(
-            _('Add on-chain fallback to lightning requests'), 'bolt11_fallback', True,
+            _('Add on-chain fallback to lightning requests'), self.config.cv.WALLET_BOLT11_FALLBACK,
             callback=self.on_toggle_bolt11_fallback)
         menu.addConfig(
-            _('Add lightning requests to bitcoin URIs'), 'bip21_lightning', False,
+            _('Add lightning requests to bitcoin URIs'), self.config.cv.WALLET_BIP21_LIGHTNING,
             tooltip=_('This may result in large QR codes'),
             callback=self.update_current_request)
         self.qr_menu_action = menu.addToggle(_("Show detached QR code window"), self.window.toggle_qr_window)
@@ -181,7 +181,7 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
         self.update_expiry_text()
 
     def update_expiry_text(self):
-        expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
+        expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS
         text = pr_expiration_values[expiry]
         self.expiry_button.setText(text)
 
@@ -196,11 +196,11 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
             '\n\n',
             _('For Lightning requests, payments will not be accepted after the expiration.'),
         ])
-        expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
+        expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS
         v = self.window.query_choice(msg, pr_expiration_values, title=_('Expiry'), default_choice=expiry)
         if v is None:
             return
-        self.config.set_key('request_expiry', v)
+        self.config.WALLET_PAYREQ_EXPIRY_SECONDS = v
         self.update_expiry_text()
 
     def on_toggle_bolt11_fallback(self):
@@ -210,7 +210,7 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
         self.update_current_request()
 
     def update_view_button(self):
-        i = self.config.get('receive_tabs_index', 0)
+        i = self.config.GUI_QT_RECEIVE_TABS_INDEX
         if i == 0:
             icon, text = read_QIcon("link.png"), _('Bitcoin URI')
         elif i == 1:
@@ -221,9 +221,9 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
         self.toggle_view_button.setIcon(icon)
 
     def toggle_view(self):
-        i = self.config.get('receive_tabs_index', 0)
+        i = self.config.GUI_QT_RECEIVE_TABS_INDEX
         i = (i + 1) % (3 if self.wallet.has_lightning() else 2)
-        self.config.set_key('receive_tabs_index', i)
+        self.config.GUI_QT_RECEIVE_TABS_INDEX = i
         self.update_current_request()
         self.update_view_button()
 
@@ -239,12 +239,12 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
         self.window.do_copy(data, title=title)
 
     def toggle_receive_qr(self):
-        b = not self.config.get('receive_qr_visible', False)
-        self.config.set_key('receive_qr_visible', b)
+        b = not self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE
+        self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE = b
         self.update_receive_widgets()
 
     def update_receive_widgets(self):
-        b = self.config.get('receive_qr_visible', False)
+        b = self.config.GUI_QT_RECEIVE_TAB_QR_VISIBLE
         self.receive_widget.update_visibility(b)
 
     def update_current_request(self):
@@ -286,7 +286,7 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
         self.update_receive_qr_window()
 
     def get_tab_data(self):
-        i = self.config.get('receive_tabs_index', 0)
+        i = self.config.GUI_QT_RECEIVE_TABS_INDEX
         if i == 0:
             out = self.URI, self.URI, self.URI_help, _('Bitcoin URI')
         elif i == 1:
@@ -305,7 +305,7 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
     def create_invoice(self):
         amount_sat = self.receive_amount_e.get_amount()
         message = self.receive_message_e.text()
-        expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
+        expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS
 
         if amount_sat and amount_sat < self.wallet.dust_threshold():
             address = None
diff --git a/electrum/gui/qt/settings_dialog.py b/electrum/gui/qt/settings_dialog.py
index 2a25e876d..99b491ba4 100644
--- a/electrum/gui/qt/settings_dialog.py
+++ b/electrum/gui/qt/settings_dialog.py
@@ -72,18 +72,18 @@ class SettingsDialog(QDialog, QtEventListener):
         lang_combo = QComboBox()
         lang_combo.addItems(list(languages.values()))
         lang_keys = list(languages.keys())
-        lang_cur_setting = self.config.get("language", '')
+        lang_cur_setting = self.config.LOCALIZATION_LANGUAGE
         try:
             index = lang_keys.index(lang_cur_setting)
         except ValueError:  # not in list
             index = 0
         lang_combo.setCurrentIndex(index)
-        if not self.config.is_modifiable('language'):
+        if not self.config.cv.LOCALIZATION_LANGUAGE.is_modifiable():
             for w in [lang_combo, lang_label]: w.setEnabled(False)
         def on_lang(x):
             lang_request = list(languages.keys())[lang_combo.currentIndex()]
-            if lang_request != self.config.get('language'):
-                self.config.set_key("language", lang_request, save=True)
+            if lang_request != self.config.LOCALIZATION_LANGUAGE:
+                self.config.LOCALIZATION_LANGUAGE = lang_request
                 self.need_restart = True
         lang_combo.currentIndexChanged.connect(on_lang)
 
@@ -93,13 +93,13 @@ class SettingsDialog(QDialog, QtEventListener):
         nz.setMinimum(0)
         nz.setMaximum(self.config.decimal_point)
         nz.setValue(self.config.num_zeros)
-        if not self.config.is_modifiable('num_zeros'):
+        if not self.config.cv.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT.is_modifiable():
             for w in [nz, nz_label]: w.setEnabled(False)
         def on_nz():
             value = nz.value()
             if self.config.num_zeros != value:
                 self.config.num_zeros = value
-                self.config.set_key('num_zeros', value, save=True)
+                self.config.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT = value
                 self.app.refresh_tabs_signal.emit()
                 self.app.update_status_signal.emit()
         nz.valueChanged.connect(on_nz)
@@ -108,7 +108,7 @@ class SettingsDialog(QDialog, QtEventListener):
         help_trampoline = messages.MSG_HELP_TRAMPOLINE
         trampoline_cb = QCheckBox(_("Use trampoline routing"))
         trampoline_cb.setToolTip(messages.to_rtf(help_trampoline))
-        trampoline_cb.setChecked(not bool(self.config.get('use_gossip', False)))
+        trampoline_cb.setChecked(not self.config.LIGHTNING_USE_GOSSIP)
         def on_trampoline_checked(use_trampoline):
             use_trampoline = bool(use_trampoline)
             if not use_trampoline:
@@ -119,7 +119,7 @@ class SettingsDialog(QDialog, QtEventListener):
                 ])):
                     trampoline_cb.setCheckState(Qt.Checked)
                     return
-            self.config.set_key('use_gossip', not use_trampoline)
+            self.config.LIGHTNING_USE_GOSSIP = not use_trampoline
             if not use_trampoline:
                 self.network.start_gossip()
             else:
@@ -137,17 +137,17 @@ class SettingsDialog(QDialog, QtEventListener):
         ])
         remote_wt_cb = QCheckBox(_("Use a remote watchtower"))
         remote_wt_cb.setToolTip('

'+help_remote_wt+'

') - remote_wt_cb.setChecked(bool(self.config.get('use_watchtower', False))) + remote_wt_cb.setChecked(self.config.WATCHTOWER_CLIENT_ENABLED) def on_remote_wt_checked(x): - self.config.set_key('use_watchtower', bool(x)) + self.config.WATCHTOWER_CLIENT_ENABLED = bool(x) self.watchtower_url_e.setEnabled(bool(x)) remote_wt_cb.stateChanged.connect(on_remote_wt_checked) - watchtower_url = self.config.get('watchtower_url') + watchtower_url = self.config.WATCHTOWER_CLIENT_URL self.watchtower_url_e = QLineEdit(watchtower_url) - self.watchtower_url_e.setEnabled(self.config.get('use_watchtower', False)) + self.watchtower_url_e.setEnabled(self.config.WATCHTOWER_CLIENT_ENABLED) def on_wt_url(): url = self.watchtower_url_e.text() or None - watchtower_url = self.config.set_key('watchtower_url', url) + self.config.WATCHTOWER_CLIENT_URL = url self.watchtower_url_e.editingFinished.connect(on_wt_url) msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\ @@ -155,18 +155,18 @@ class SettingsDialog(QDialog, QtEventListener): + '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\ + 'For more information, see https://openalias.org' alias_label = HelpLabel(_('OpenAlias') + ':', msg) - alias = self.config.get('alias','') + alias = self.config.OPENALIAS_ID self.alias_e = QLineEdit(alias) self.set_alias_color() self.alias_e.editingFinished.connect(self.on_alias_edit) msat_cb = QCheckBox(_("Show Lightning amounts with msat precision")) - msat_cb.setChecked(bool(self.config.get('amt_precision_post_satoshi', False))) + msat_cb.setChecked(self.config.BTC_AMOUNTS_PREC_POST_SAT > 0) def on_msat_checked(v): prec = 3 if v == Qt.Checked else 0 if self.config.amt_precision_post_satoshi != prec: self.config.amt_precision_post_satoshi = prec - self.config.set_key('amt_precision_post_satoshi', prec) + self.config.BTC_AMOUNTS_PREC_POST_SAT = prec self.app.refresh_tabs_signal.emit() msat_cb.stateChanged.connect(on_msat_checked) @@ -191,12 +191,12 @@ class SettingsDialog(QDialog, QtEventListener): unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz)) thousandsep_cb = QCheckBox(_("Add thousand separators to bitcoin amounts")) - thousandsep_cb.setChecked(bool(self.config.get('amt_add_thousands_sep', False))) + thousandsep_cb.setChecked(self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP) def on_set_thousandsep(v): checked = v == Qt.Checked if self.config.amt_add_thousands_sep != checked: self.config.amt_add_thousands_sep = checked - self.config.set_key('amt_add_thousands_sep', checked) + self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP = checked self.app.refresh_tabs_signal.emit() thousandsep_cb.stateChanged.connect(on_set_thousandsep) @@ -209,32 +209,33 @@ class SettingsDialog(QDialog, QtEventListener): system_cameras = find_system_cameras() for cam_desc, cam_path in system_cameras.items(): qr_combo.addItem(cam_desc, cam_path) - index = qr_combo.findData(self.config.get("video_device")) + index = qr_combo.findData(self.config.VIDEO_DEVICE_PATH) qr_combo.setCurrentIndex(index) - on_video_device = lambda x: self.config.set_key("video_device", qr_combo.itemData(x), save=True) + def on_video_device(x): + self.config.VIDEO_DEVICE_PATH = qr_combo.itemData(x) qr_combo.currentIndexChanged.connect(on_video_device) colortheme_combo = QComboBox() colortheme_combo.addItem(_('Light'), 'default') colortheme_combo.addItem(_('Dark'), 'dark') - index = colortheme_combo.findData(self.config.get('qt_gui_color_theme', 'default')) + index = colortheme_combo.findData(self.config.GUI_QT_COLOR_THEME) colortheme_combo.setCurrentIndex(index) colortheme_label = QLabel(_('Color theme') + ':') def on_colortheme(x): - self.config.set_key('qt_gui_color_theme', colortheme_combo.itemData(x), save=True) + self.config.GUI_QT_COLOR_THEME = colortheme_combo.itemData(x) self.need_restart = True colortheme_combo.currentIndexChanged.connect(on_colortheme) updatecheck_cb = QCheckBox(_("Automatically check for software updates")) - updatecheck_cb.setChecked(bool(self.config.get('check_updates', False))) + updatecheck_cb.setChecked(self.config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS) def on_set_updatecheck(v): - self.config.set_key('check_updates', v == Qt.Checked, save=True) + self.config.AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = (v == Qt.Checked) updatecheck_cb.stateChanged.connect(on_set_updatecheck) filelogging_cb = QCheckBox(_("Write logs to file")) - filelogging_cb.setChecked(bool(self.config.get('log_to_file', False))) + filelogging_cb.setChecked(self.config.WRITE_LOGS_TO_DISK) def on_set_filelogging(v): - self.config.set_key('log_to_file', v == Qt.Checked, save=True) + self.config.WRITE_LOGS_TO_DISK = (v == Qt.Checked) self.need_restart = True filelogging_cb.stateChanged.connect(on_set_filelogging) filelogging_cb.setToolTip(_('Debug logs can be persisted to disk. These are useful for troubleshooting.')) @@ -256,7 +257,7 @@ class SettingsDialog(QDialog, QtEventListener): chooser_combo.setCurrentIndex(i) def on_chooser(x): chooser_name = choosers[chooser_combo.currentIndex()] - self.config.set_key('coin_chooser', chooser_name) + self.config.WALLET_COIN_CHOOSER_POLICY = chooser_name chooser_combo.currentIndexChanged.connect(on_chooser) block_explorers = sorted(util.block_explorer_info().keys()) @@ -267,7 +268,7 @@ class SettingsDialog(QDialog, QtEventListener): msg = _('Choose which online block explorer to use for functions that open a web browser') block_ex_label = HelpLabel(_('Online Block Explorer') + ':', msg) block_ex_combo = QComboBox() - block_ex_custom_e = QLineEdit(str(self.config.get('block_explorer_custom') or '')) + block_ex_custom_e = QLineEdit(str(self.config.BLOCK_EXPLORER_CUSTOM or '')) block_ex_combo.addItems(block_explorers) block_ex_combo.setCurrentIndex( block_ex_combo.findText(util.block_explorer(self.config) or BLOCK_EX_CUSTOM_ITEM)) @@ -279,8 +280,8 @@ class SettingsDialog(QDialog, QtEventListener): on_be_edit() else: be_result = block_explorers[block_ex_combo.currentIndex()] - self.config.set_key('block_explorer_custom', None, save=False) - self.config.set_key('block_explorer', be_result, save=True) + self.config.BLOCK_EXPLORER_CUSTOM = None + self.config.BLOCK_EXPLORER = be_result showhide_block_ex_custom_e() block_ex_combo.currentIndexChanged.connect(on_be_combo) def on_be_edit(): @@ -289,7 +290,7 @@ class SettingsDialog(QDialog, QtEventListener): val = ast.literal_eval(val) # to also accept tuples except Exception: pass - self.config.set_key('block_explorer_custom', val) + self.config.BLOCK_EXPLORER_CUSTOM = val block_ex_custom_e.editingFinished.connect(on_be_edit) block_ex_hbox = QHBoxLayout() block_ex_hbox.setContentsMargins(0, 0, 0, 0) @@ -307,7 +308,7 @@ class SettingsDialog(QDialog, QtEventListener): def update_currencies(): if not self.fx: return - h = bool(self.config.get('history_rates', False)) + h = self.config.FX_HISTORY_RATES currencies = sorted(self.fx.get_currencies(h)) ccy_combo.clear() ccy_combo.addItems([_('None')] + currencies) @@ -319,7 +320,7 @@ class SettingsDialog(QDialog, QtEventListener): b = self.fx.is_enabled() ex_combo.setEnabled(b) if b: - h = bool(self.config.get('history_rates', False)) + h = self.config.FX_HISTORY_RATES c = self.fx.get_currency() exchanges = self.fx.get_exchanges_by_ccy(c, h) else: @@ -347,7 +348,7 @@ class SettingsDialog(QDialog, QtEventListener): self.app.update_fiat_signal.emit() def on_history_rates(checked): - self.config.set_key('history_rates', bool(checked)) + self.config.FX_HISTORY_RATES = bool(checked) if not self.fx: return update_exchanges() @@ -356,7 +357,7 @@ class SettingsDialog(QDialog, QtEventListener): update_currencies() update_exchanges() ccy_combo.currentIndexChanged.connect(on_currency) - self.history_rates_cb.setChecked(bool(self.config.get('history_rates', False))) + self.history_rates_cb.setChecked(self.config.FX_HISTORY_RATES) self.history_rates_cb.stateChanged.connect(on_history_rates) ex_combo.currentIndexChanged.connect(on_exchange) @@ -417,7 +418,7 @@ class SettingsDialog(QDialog, QtEventListener): self.app.alias_received_signal.emit() def set_alias_color(self): - if not self.config.get('alias'): + if not self.config.OPENALIAS_ID: self.alias_e.setStyleSheet("") return if self.wallet.contacts.alias_info: @@ -429,7 +430,7 @@ class SettingsDialog(QDialog, QtEventListener): def on_alias_edit(self): self.alias_e.setStyleSheet("") alias = str(self.alias_e.text()) - self.config.set_key('alias', alias, save=True) + self.config.OPENALIAS_ID = alias if alias: self.wallet.contacts.fetch_openalias(self.config) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 1f47dc972..0c52442ff 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -45,7 +45,7 @@ class SwapDialog(WindowModalDialog, QtEventListener): vbox = QVBoxLayout(self) toolbar, menu = create_toolbar_with_menu(self.config, '') menu.addConfig( - _("Allow instant swaps"), 'allow_instant_swaps', False, + _("Allow instant swaps"), self.config.cv.LIGHTNING_ALLOW_INSTANT_SWAPS, tooltip=messages.to_rtf(messages.MSG_CONFIG_INSTANT_SWAPS), ).setEnabled(self.lnworker.can_have_recoverable_channels()) vbox.addLayout(toolbar) @@ -138,11 +138,11 @@ class SwapDialog(WindowModalDialog, QtEventListener): def fee_slider_callback(self, dyn, pos, fee_rate): if dyn: if self.config.use_mempool_fees(): - self.config.set_key('depth_level', pos, save=False) + self.config.cv.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS.set(pos, save=False) else: - self.config.set_key('fee_level', pos, save=False) + self.config.cv.FEE_EST_DYNAMIC_ETA_SLIDERPOS.set(pos, save=False) else: - self.config.set_key('fee_per_kb', fee_rate, save=False) + self.config.cv.FEE_EST_STATIC_FEERATE_FALLBACK.set(fee_rate, save=False) if self.send_follows: self.on_recv_edited() else: diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index c29de2cbb..b9e5bda8e 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -416,7 +416,7 @@ class TxDialog(QDialog, MessageBoxMixin): self.setLayout(vbox) toolbar, menu = create_toolbar_with_menu(self.config, '') menu.addConfig( - _('Download missing data'), 'tx_dialog_fetch_txin_data', False, + _('Download missing data'), self.config.cv.GUI_QT_TX_DIALOG_FETCH_TXIN_DATA, tooltip=_( 'Download parent transactions from the network.\n' 'Allows filling in missing fee and input details.'), @@ -945,7 +945,7 @@ class TxDialog(QDialog, MessageBoxMixin): We could also SPV-verify the tx, to fill in missing tx_mined_status (block height, blockhash, timestamp), but this is not done currently. """ - if not self.config.get('tx_dialog_fetch_txin_data', False): + if not self.config.GUI_QT_TX_DIALOG_FETCH_TXIN_DATA: return tx = self.tx if not tx: diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index bfae00a88..a393c2bdb 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -483,7 +483,7 @@ def filename_field(parent, config, defaultname, select_msg): hbox = QHBoxLayout() - directory = config.get('io_dir', os.path.expanduser('~')) + directory = config.IO_DIRECTORY path = os.path.join(directory, defaultname) filename_e = QLineEdit() filename_e.setText(path) @@ -1048,10 +1048,10 @@ def export_meta_gui(electrum_window: 'ElectrumWindow', title, exporter): def getOpenFileName(*, parent, title, filter="", config: 'SimpleConfig') -> Optional[str]: """Custom wrapper for getOpenFileName that remembers the path selected by the user.""" - directory = config.get('io_dir', os.path.expanduser('~')) + directory = config.IO_DIRECTORY fileName, __ = QFileDialog.getOpenFileName(parent, title, directory, filter) if fileName and directory != os.path.dirname(fileName): - config.set_key('io_dir', os.path.dirname(fileName), save=True) + config.IO_DIRECTORY = os.path.dirname(fileName) return fileName @@ -1066,7 +1066,7 @@ def getSaveFileName( config: 'SimpleConfig', ) -> Optional[str]: """Custom wrapper for getSaveFileName that remembers the path selected by the user.""" - directory = config.get('io_dir', os.path.expanduser('~')) + directory = config.IO_DIRECTORY path = os.path.join(directory, filename) file_dialog = QFileDialog(parent, title, path, filter) @@ -1082,7 +1082,7 @@ def getSaveFileName( selected_path = file_dialog.selectedFiles()[0] if selected_path and directory != os.path.dirname(selected_path): - config.set_key('io_dir', os.path.dirname(selected_path), save=True) + config.IO_DIRECTORY = os.path.dirname(selected_path) return selected_path diff --git a/electrum/gui/text.py b/electrum/gui/text.py index b1d3eae75..5fc954c2e 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -558,7 +558,7 @@ class ElectrumGui(BaseElectrumGui, EventListener): if not address: return message = self.str_recv_description - expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) + expiry = self.config.WALLET_PAYREQ_EXPIRY_SECONDS key = self.wallet.create_request(amount_sat, message, expiry, address) self.do_clear_request() self.pos = self.max_pos @@ -719,7 +719,7 @@ class ElectrumGui(BaseElectrumGui, EventListener): srv = 'auto-connect' if auto_connect else str(self.network.default_server) out = self.run_dialog('Network', [ {'label':'server', 'type':'str', 'value':srv}, - {'label':'proxy', 'type':'str', 'value':self.config.get('proxy', '')}, + {'label':'proxy', 'type':'str', 'value':self.config.NETWORK_PROXY}, ], buttons = 1) if out: if out.get('server'): @@ -747,7 +747,7 @@ class ElectrumGui(BaseElectrumGui, EventListener): if out: if out.get('Default fee'): fee = int(Decimal(out['Default fee']) * COIN) - self.config.set_key('fee_per_kb', fee, save=True) + self.config.FEE_EST_STATIC_FEERATE_FALLBACK = fee def password_dialog(self): out = self.run_dialog('Password', [ diff --git a/electrum/interface.py b/electrum/interface.py index 782c5af41..f1d6f4d9f 100644 --- a/electrum/interface.py +++ b/electrum/interface.py @@ -68,8 +68,6 @@ ca_path = certifi.where() BUCKET_NAME_OF_ONION_SERVERS = 'onion' -MAX_INCOMING_MSG_SIZE = 1_000_000 # in bytes - _KNOWN_NETWORK_PROTOCOLS = {'t', 's'} PREFERRED_NETWORK_PROTOCOL = 's' assert PREFERRED_NETWORK_PROTOCOL in _KNOWN_NETWORK_PROTOCOLS @@ -216,8 +214,8 @@ class NotificationSession(RPCSession): def default_framer(self): # overridden so that max_size can be customized - max_size = int(self.interface.network.config.get('network_max_incoming_msg_size', - MAX_INCOMING_MSG_SIZE)) + max_size = self.interface.network.config.NETWORK_MAX_INCOMING_MSG_SIZE + assert max_size > 500_000, f"{max_size=} (< 500_000) is too small" return NewlineFramer(max_size=max_size) async def close(self, *, force_after: int = None): @@ -604,7 +602,7 @@ class Interface(Logger): def _get_expected_fingerprint(self) -> Optional[str]: if self.is_main_server(): - return self.network.config.get("serverfingerprint") + return self.network.config.NETWORK_SERVERFINGERPRINT def _verify_certificate_fingerprint(self, certificate): expected_fingerprint = self._get_expected_fingerprint() diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 7d034a06d..be53811f7 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -647,7 +647,7 @@ class Peer(Logger): channel_seed=channel_seed, static_remotekey=static_remotekey, upfront_shutdown_script=upfront_shutdown_script, - to_self_delay=self.network.config.get('lightning_to_self_delay', 7 * 144), + to_self_delay=self.network.config.LIGHTNING_TO_SELF_DELAY_CSV, dust_limit_sat=dust_limit_sat, max_htlc_value_in_flight_msat=funding_sat * 1000, max_accepted_htlcs=30, @@ -1389,7 +1389,7 @@ class Peer(Logger): if pending_channel_update: chan.set_remote_update(pending_channel_update) self.logger.info(f"CHANNEL OPENING COMPLETED ({chan.get_id_for_log()})") - forwarding_enabled = self.network.config.get('lightning_forward_payments', False) + forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS if forwarding_enabled: # send channel_update of outgoing edge to peer, # so that channel can be used to to receive payments @@ -1578,7 +1578,7 @@ class Peer(Logger): # (same for trampoline forwarding) # - we could check for the exposure to dust HTLCs, see: # https://github.com/ACINQ/eclair/pull/1985 - forwarding_enabled = self.network.config.get('lightning_forward_payments', False) + forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS if not forwarding_enabled: self.logger.info(f"forwarding is disabled. failing htlc.") raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') @@ -1660,8 +1660,8 @@ class Peer(Logger): htlc: UpdateAddHtlc, trampoline_onion: ProcessedOnionPacket): - forwarding_enabled = self.network.config.get('lightning_forward_payments', False) - forwarding_trampoline_enabled = self.network.config.get('lightning_forward_trampoline_payments', False) + forwarding_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_PAYMENTS + forwarding_trampoline_enabled = self.network.config.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS if not (forwarding_enabled and forwarding_trampoline_enabled): self.logger.info(f"trampoline forwarding is disabled. failing htlc.") raise OnionRoutingFailure(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') @@ -1996,8 +1996,8 @@ class Peer(Logger): """ return the closing fee and fee range we initially try to enforce """ config = self.network.config our_fee = None - if config.get('test_shutdown_fee'): - our_fee = config.get('test_shutdown_fee') + if config.TEST_SHUTDOWN_FEE: + our_fee = config.TEST_SHUTDOWN_FEE else: fee_rate_per_kb = config.eta_target_to_fee(FEE_LN_ETA_TARGET) if fee_rate_per_kb is None: # fallback @@ -2012,10 +2012,10 @@ class Peer(Logger): our_fee = max_fee our_fee = min(our_fee, max_fee) # config modern_fee_negotiation can be set in tests - if config.get('test_shutdown_legacy'): + if config.TEST_SHUTDOWN_LEGACY: our_fee_range = None - elif config.get('test_shutdown_fee_range'): - our_fee_range = config.get('test_shutdown_fee_range') + elif config.TEST_SHUTDOWN_FEE_RANGE: + our_fee_range = config.TEST_SHUTDOWN_FEE_RANGE else: # we aim at a fee between next block inclusion and some lower value our_fee_range = {'min_fee_satoshis': our_fee // 2, 'max_fee_satoshis': our_fee * 2} @@ -2101,7 +2101,7 @@ class Peer(Logger): fee_range_sent = our_fee_range and (is_initiator or (their_previous_fee is not None)) # The sending node, if it is not the funder: - if our_fee_range and their_fee_range and not is_initiator and not self.network.config.get('test_shutdown_fee_range'): + if our_fee_range and their_fee_range and not is_initiator and not self.network.config.TEST_SHUTDOWN_FEE_RANGE: # SHOULD set max_fee_satoshis to at least the max_fee_satoshis received our_fee_range['max_fee_satoshis'] = max(their_fee_range['max_fee_satoshis'], our_fee_range['max_fee_satoshis']) # SHOULD set min_fee_satoshis to a fairly low value @@ -2400,8 +2400,8 @@ class Peer(Logger): except Exception as e: self.logger.info(f"error processing onion packet: {e!r}") raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=failure_data) - if self.network.config.get('test_fail_malformed_htlc'): + if self.network.config.TEST_FAIL_HTLCS_AS_MALFORMED: raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=failure_data) - if self.network.config.get('test_fail_htlcs_with_temp_node_failure'): + if self.network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE: raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'') return processed_onion diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 2abe2662c..563b66a0b 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -253,13 +253,13 @@ class LNWorker(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): async def maybe_listen(self): # FIXME: only one LNWorker can listen at a time (single port) - listen_addr = self.config.get('lightning_listen') + listen_addr = self.config.LIGHTNING_LISTEN if listen_addr: self.logger.info(f'lightning_listen enabled. will try to bind: {listen_addr!r}') try: netaddr = NetAddress.from_string(listen_addr) except Exception as e: - self.logger.error(f"failed to parse config key 'lightning_listen'. got: {e!r}") + self.logger.error(f"failed to parse config key '{self.config.cv.LIGHTNING_LISTEN.key()}'. got: {e!r}") return addr = str(netaddr.host) async def cb(reader, writer): @@ -351,7 +351,7 @@ class LNWorker(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): await self.taskgroup.cancel_remaining() def _add_peers_from_config(self): - peer_list = self.config.get('lightning_peers', []) + peer_list = self.config.LIGHTNING_PEERS or [] for host, port, pubkey in peer_list: asyncio.run_coroutine_threadsafe( self._add_peer(host, int(port), bfh(pubkey)), @@ -675,14 +675,14 @@ class LNWallet(LNWorker): def can_have_recoverable_channels(self) -> bool: return (self.has_deterministic_node_id() - and not (self.config.get('lightning_listen'))) + and not self.config.LIGHTNING_LISTEN) def has_recoverable_channels(self) -> bool: """Whether *future* channels opened by this wallet would be recoverable from seed (via putting OP_RETURN outputs into funding txs). """ return (self.can_have_recoverable_channels() - and self.config.get('use_recoverable_channels', True)) + and self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS) @property def channels(self) -> Mapping[bytes, Channel]: @@ -728,7 +728,7 @@ class LNWallet(LNWorker): while True: # periodically poll if the user updated 'watchtower_url' await asyncio.sleep(5) - watchtower_url = self.config.get('watchtower_url') + watchtower_url = self.config.WATCHTOWER_CLIENT_URL if not watchtower_url: continue parsed_url = urllib.parse.urlparse(watchtower_url) diff --git a/electrum/logging.py b/electrum/logging.py index 54cf35948..4891d306f 100644 --- a/electrum/logging.py +++ b/electrum/logging.py @@ -318,12 +318,12 @@ def configure_logging(config: 'SimpleConfig', *, log_to_file: Optional[bool] = N verbosity = config.get('verbosity') verbosity_shortcuts = config.get('verbosity_shortcuts') - if not verbosity and config.get('gui_enable_debug_logs'): + if not verbosity and config.GUI_ENABLE_DEBUG_LOGS: verbosity = '*' _configure_stderr_logging(verbosity=verbosity, verbosity_shortcuts=verbosity_shortcuts) if log_to_file is None: - log_to_file = config.get('log_to_file', False) + log_to_file = config.WRITE_LOGS_TO_DISK log_to_file |= is_android_debug_apk() if log_to_file: log_directory = pathlib.Path(config.path) / "logs" diff --git a/electrum/network.py b/electrum/network.py index e1269899c..ab08adb49 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -172,7 +172,7 @@ def serialize_proxy(p): p.get('user', ''), p.get('password', '')]) -def deserialize_proxy(s: str) -> Optional[dict]: +def deserialize_proxy(s: Optional[str]) -> Optional[dict]: if not isinstance(s, str): return None if s.lower() == 'none': @@ -295,7 +295,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): blockchain.read_blockchains(self.config) blockchain.init_headers_file_for_best_chain() self.logger.info(f"blockchains {list(map(lambda b: b.forkpoint, blockchain.blockchains.values()))}") - self._blockchain_preferred_block = self.config.get('blockchain_preferred_block', None) # type: Dict[str, Any] + self._blockchain_preferred_block = self.config.BLOCKCHAIN_PREFERRED_BLOCK # type: Dict[str, Any] if self._blockchain_preferred_block is None: self._set_preferred_chain(None) self._blockchain = blockchain.get_best_chain() @@ -342,7 +342,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): self._was_started = False # lightning network - if self.config.get('run_watchtower', False): + if self.config.WATCHTOWER_SERVER_ENABLED: from . import lnwatcher self.local_watchtower = lnwatcher.WatchTower(self) asyncio.ensure_future(self.local_watchtower.start_watching()) @@ -358,7 +358,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): from . import lnrouter from . import channel_db from . import lnworker - if not self.config.get('use_gossip'): + if not self.config.LIGHTNING_USE_GOSSIP: return if self.lngossip is None: self.channel_db = channel_db.ChannelDB(self) @@ -489,9 +489,9 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): oneserver=self.oneserver) def _init_parameters_from_config(self) -> None: - self.auto_connect = self.config.get('auto_connect', True) + self.auto_connect = self.config.NETWORK_AUTO_CONNECT self._set_default_server() - self._set_proxy(deserialize_proxy(self.config.get('proxy'))) + self._set_proxy(deserialize_proxy(self.config.NETWORK_PROXY)) self._maybe_set_oneserver() def get_donation_address(self): @@ -554,7 +554,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): else: out[server.host] = {server.protocol: port} # potentially filter out some - if self.config.get('noonion'): + if self.config.NETWORK_NOONION: out = filter_noonion(out) return out @@ -590,7 +590,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): def _set_default_server(self) -> None: # Server for addresses and transactions - server = self.config.get('server', None) + server = self.config.NETWORK_SERVER # Sanitize default server if server: try: @@ -628,14 +628,14 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): int(proxy['port']) except Exception: return - self.config.set_key('auto_connect', net_params.auto_connect, save=False) - self.config.set_key('oneserver', net_params.oneserver, save=False) - self.config.set_key('proxy', proxy_str, save=False) - self.config.set_key('server', str(server), save=True) + self.config.NETWORK_AUTO_CONNECT = net_params.auto_connect + self.config.NETWORK_ONESERVER = net_params.oneserver + self.config.NETWORK_PROXY = proxy_str + self.config.NETWORK_SERVER = str(server) # abort if changes were not allowed by config - if self.config.get('server') != str(server) \ - or self.config.get('proxy') != proxy_str \ - or self.config.get('oneserver') != net_params.oneserver: + if self.config.NETWORK_SERVER != str(server) \ + or self.config.NETWORK_PROXY != proxy_str \ + or self.config.NETWORK_ONESERVER != net_params.oneserver: return proxy_changed = self.proxy != proxy @@ -657,7 +657,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): util.trigger_callback('network_updated') def _maybe_set_oneserver(self) -> None: - oneserver = bool(self.config.get('oneserver', False)) + oneserver = self.config.NETWORK_ONESERVER self.oneserver = oneserver self.num_server = NUM_TARGET_CONNECTED_SERVERS if not oneserver else 0 @@ -788,8 +788,8 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): util.trigger_callback('network_updated') def get_network_timeout_seconds(self, request_type=NetworkTimeout.Generic) -> int: - if self.config.get('network_timeout', None): - return int(self.config.get('network_timeout')) + if self.config.NETWORK_TIMEOUT: + return self.config.NETWORK_TIMEOUT if self.oneserver and not self.auto_connect: return request_type.MOST_RELAXED if self.proxy: @@ -1191,7 +1191,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]): 'height': height, 'hash': header_hash, } - self.config.set_key('blockchain_preferred_block', self._blockchain_preferred_block) + self.config.BLOCKCHAIN_PREFERRED_BLOCK = self._blockchain_preferred_block async def follow_chain_given_id(self, chain_id: str) -> None: bc = blockchain.blockchains.get(chain_id) diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py index 00e6d4f62..73273dd7e 100644 --- a/electrum/paymentrequest.py +++ b/electrum/paymentrequest.py @@ -400,10 +400,10 @@ def verify_cert_chain(chain): return x509_chain[0], ca -def check_ssl_config(config): +def check_ssl_config(config: 'SimpleConfig'): from . import pem - key_path = config.get('ssl_keyfile') - cert_path = config.get('ssl_certfile') + key_path = config.SSL_KEYFILE_PATH + cert_path = config.SSL_CERTFILE_PATH with open(key_path, 'r', encoding='utf-8') as f: params = pem.parse_private_key(f.read()) with open(cert_path, 'r', encoding='utf-8') as f: @@ -453,8 +453,8 @@ def serialize_request(req): # FIXME this is broken def make_request(config: 'SimpleConfig', req: 'Invoice'): pr = make_unsigned_request(req) - key_path = config.get('ssl_keyfile') - cert_path = config.get('ssl_certfile') + key_path = config.SSL_KEYFILE_PATH + cert_path = config.SSL_CERTFILE_PATH if key_path and cert_path: sign_request_with_x509(pr, key_path, cert_path) return pr diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py index 950297229..c6f9942f4 100644 --- a/electrum/plugins/ledger/ledger.py +++ b/electrum/plugins/ledger/ledger.py @@ -1344,7 +1344,6 @@ class LedgerPlugin(HW_PluginBase): SUPPORTED_XTYPES = ('standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh') def __init__(self, parent, config, name): - self.segwit = config.get("segwit") HW_PluginBase.__init__(self, parent, config, name) self.libraries_available = self.check_libraries_available() if not self.libraries_available: diff --git a/electrum/plugins/payserver/payserver.py b/electrum/plugins/payserver/payserver.py index 8f46c99bf..9300741c1 100644 --- a/electrum/plugins/payserver/payserver.py +++ b/electrum/plugins/payserver/payserver.py @@ -60,7 +60,7 @@ class PayServerPlugin(BasePlugin): # we use the first wallet loaded if self.server is not None: return - if self.config.get('offline'): + if self.config.NETWORK_OFFLINE: return self.server = PayServer(self.config, wallet) asyncio.run_coroutine_threadsafe(daemon.taskgroup.spawn(self.server.run()), daemon.asyncio_loop) @@ -79,7 +79,7 @@ class PayServer(Logger, EventListener): assert self.has_www_dir(), self.WWW_DIR self.config = config self.wallet = wallet - url = self.config.get('payserver_address', 'localhost:8080') + url = self.config.PAYSERVER_ADDRESS self.addr = NetAddress.from_string(url) self.pending = defaultdict(asyncio.Event) self.register_callbacks() @@ -91,15 +91,15 @@ class PayServer(Logger, EventListener): @property def base_url(self): - payserver = self.config.get('payserver_address', 'localhost:8080') + payserver = self.config.PAYSERVER_ADDRESS payserver = NetAddress.from_string(payserver) - use_ssl = bool(self.config.get('ssl_keyfile')) + use_ssl = bool(self.config.SSL_KEYFILE_PATH) protocol = 'https' if use_ssl else 'http' return '%s://%s:%d'%(protocol, payserver.host, payserver.port) @property def root(self): - return self.config.get('payserver_root', '/r') + return self.config.PAYSERVER_ROOT @event_listener async def on_event_request_status(self, wallet, key, status): @@ -118,7 +118,7 @@ class PayServer(Logger, EventListener): # to minimise attack surface. note: "add_routes" call order matters (inner path goes first) app.add_routes([web.static(f"{self.root}/vendor", os.path.join(self.WWW_DIR, 'vendor'), follow_symlinks=True)]) app.add_routes([web.static(self.root, self.WWW_DIR)]) - if self.config.get('payserver_allow_create_invoice'): + if self.config.PAYSERVER_ALLOW_CREATE_INVOICE: app.add_routes([web.post('/api/create_invoice', self.create_request)]) runner = web.AppRunner(app) await runner.setup() diff --git a/electrum/plugins/payserver/qt.py b/electrum/plugins/payserver/qt.py index 48c65e03c..e2e36b470 100644 --- a/electrum/plugins/payserver/qt.py +++ b/electrum/plugins/payserver/qt.py @@ -59,19 +59,19 @@ class Plugin(PayServerPlugin): partial(self.settings_dialog, window)) def settings_dialog(self, window: WindowModalDialog): - if self.config.get('offline'): + if self.config.NETWORK_OFFLINE: window.show_error(_("You are offline.")) return d = WindowModalDialog(window, _("PayServer Settings")) form = QtWidgets.QFormLayout(None) - addr = self.config.get('payserver_address', 'localhost:8080') + addr = self.config.PAYSERVER_ADDRESS assert self.server url = self.server.base_url + self.server.root + '/create_invoice.html' self.help_button = QtWidgets.QPushButton('View sample invoice creation form') self.help_button.clicked.connect(lambda: webopen(url)) address_e = QtWidgets.QLineEdit(addr) - keyfile_e = QtWidgets.QLineEdit(self.config.get('ssl_keyfile', '')) - certfile_e = QtWidgets.QLineEdit(self.config.get('ssl_certfile', '')) + keyfile_e = QtWidgets.QLineEdit(self.config.SSL_KEYFILE_PATH) + certfile_e = QtWidgets.QLineEdit(self.config.SSL_CERTFILE_PATH) form.addRow(QtWidgets.QLabel("Network address:"), address_e) form.addRow(QtWidgets.QLabel("SSL key file:"), keyfile_e) form.addRow(QtWidgets.QLabel("SSL cert file:"), certfile_e) @@ -82,9 +82,9 @@ class Plugin(PayServerPlugin): vbox.addSpacing(20) vbox.addLayout(Buttons(OkButton(d))) if d.exec_(): - self.config.set_key('payserver_address', str(address_e.text())) - self.config.set_key('ssl_keyfile', str(keyfile_e.text())) - self.config.set_key('ssl_certfile', str(certfile_e.text())) + self.config.PAYSERVER_ADDRESS = str(address_e.text()) + self.config.SSL_KEYFILE_PATH = str(keyfile_e.text()) + self.config.SSL_CERTFILE_PATH = str(certfile_e.text()) # fixme: restart the server window.show_message('Please restart Electrum to enable those changes') diff --git a/electrum/plugins/trustedcoin/qt.py b/electrum/plugins/trustedcoin/qt.py index 0aa918e4b..037e30f1e 100644 --- a/electrum/plugins/trustedcoin/qt.py +++ b/electrum/plugins/trustedcoin/qt.py @@ -208,7 +208,9 @@ class Plugin(TrustedCoinPlugin): grid.addWidget(QLabel(window.format_amount(v/k) + ' ' + window.base_unit() + "/tx"), i, 1) b = QRadioButton() b.setChecked(k == n_prepay) - b.clicked.connect(lambda b, k=k: self.config.set_key('trustedcoin_prepay', k, save=True)) + def on_click(b, k): + self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY = k + b.clicked.connect(partial(on_click, k=k)) grid.addWidget(b, i, 2) i += 1 diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py index 181425dc2..b79a77a29 100644 --- a/electrum/plugins/trustedcoin/trustedcoin.py +++ b/electrum/plugins/trustedcoin/trustedcoin.py @@ -296,11 +296,11 @@ class Wallet_2fa(Multisig_Wallet): return min(self.price_per_tx.keys()) def num_prepay(self): - default = self.min_prepay() - n = self.config.get('trustedcoin_prepay', default) - if n not in self.price_per_tx: - n = default - return n + default_fallback = self.min_prepay() + num = self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY + if num not in self.price_per_tx: + num = default_fallback + return num def extra_fee(self): if self.can_sign_without_server(): @@ -559,7 +559,7 @@ class TrustedCoinPlugin(BasePlugin): wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run) def choose_seed_type(self, wizard): - seed_type = '2fa' if self.config.get('nosegwit') else '2fa_segwit' + seed_type = '2fa' if self.config.WIZARD_DONT_CREATE_SEGWIT else '2fa_segwit' self.create_seed(wizard, seed_type) def create_seed(self, wizard, seed_type): diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 1a73bdbfa..d98b00b7d 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -5,14 +5,16 @@ import os import stat import ssl from decimal import Decimal -from typing import Union, Optional, Dict, Sequence, Tuple +from typing import Union, Optional, Dict, Sequence, Tuple, Any, Set from numbers import Real +from functools import cached_property from copy import deepcopy from aiorpcx import NetAddress from . import util from . import constants +from . import invoices from .util import base_units, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, UnknownBaseUnit, DECIMAL_POINT_DEFAULT from .util import format_satoshis, format_fee_satoshis, os_chmod from .util import user_dir, make_dir, NoDynamicFeeEstimates, quantize_feerate @@ -50,6 +52,72 @@ _logger = get_logger(__name__) FINAL_CONFIG_VERSION = 3 +class ConfigVar(property): + + def __init__(self, key: str, *, default, type_=None): + self._key = key + self._default = default + self._type = type_ + property.__init__(self, self._get_config_value, self._set_config_value) + + def _get_config_value(self, config: 'SimpleConfig'): + value = config.get(self._key, default=self._default) + if self._type is not None and value != self._default: + assert value is not None, f"got None for key={self._key!r}" + try: + value = self._type(value) + except Exception as e: + raise ValueError( + f"ConfigVar.get type-check and auto-conversion failed. " + f"key={self._key!r}. type={self._type}. value={value!r}") from e + return value + + def _set_config_value(self, config: 'SimpleConfig', value, *, save=True): + if self._type is not None and value is not None: + if not isinstance(value, self._type): + raise ValueError( + f"ConfigVar.set type-check failed. " + f"key={self._key!r}. type={self._type}. value={value!r}") + config.set_key(self._key, value, save=save) + + def key(self) -> str: + return self._key + + def get_default_value(self) -> Any: + return self._default + + def __repr__(self): + return f"" + + +class ConfigVarWithConfig: + + def __init__(self, *, config: 'SimpleConfig', config_var: 'ConfigVar'): + self._config = config + self._config_var = config_var + + def get(self) -> Any: + return self._config_var._get_config_value(self._config) + + def set(self, value: Any, *, save=True) -> None: + self._config_var._set_config_value(self._config, value, save=save) + + def key(self) -> str: + return self._config_var.key() + + def get_default_value(self) -> Any: + return self._config_var.get_default_value() + + def is_modifiable(self) -> bool: + return self._config.is_modifiable(self._config_var) + + def is_set(self) -> bool: + return self._config.is_set(self._config_var) + + def __repr__(self): + return f"" + + class SimpleConfig(Logger): """ The SimpleConfig class is responsible for handling operations involving @@ -98,7 +166,7 @@ class SimpleConfig(Logger): # avoid new config getting upgraded self.user_config = {'config_version': FINAL_CONFIG_VERSION} - self._not_modifiable_keys = set() + self._not_modifiable_keys = set() # type: Set[str] # config "upgrade" - CLI options self.rename_config_keys( @@ -111,14 +179,15 @@ class SimpleConfig(Logger): self._check_dependent_keys() # units and formatting - self.decimal_point = self.get('decimal_point', DECIMAL_POINT_DEFAULT) + # FIXME is this duplication (dp, nz, post_sat, thou_sep) due to performance reasons?? + self.decimal_point = self.BTC_AMOUNTS_DECIMAL_POINT try: decimal_point_to_base_unit_name(self.decimal_point) except UnknownBaseUnit: self.decimal_point = DECIMAL_POINT_DEFAULT - self.num_zeros = int(self.get('num_zeros', 0)) - self.amt_precision_post_satoshi = int(self.get('amt_precision_post_satoshi', 0)) - self.amt_add_thousands_sep = bool(self.get('amt_add_thousands_sep', False)) + self.num_zeros = self.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT + self.amt_precision_post_satoshi = self.BTC_AMOUNTS_PREC_POST_SAT + self.amt_add_thousands_sep = self.BTC_AMOUNTS_ADD_THOUSANDS_SEP def electrum_path(self): # Read electrum_path from command line @@ -158,7 +227,15 @@ class SimpleConfig(Logger): updated = True return updated - def set_key(self, key, value, *, save=True): + def set_key(self, key: Union[str, ConfigVar, ConfigVarWithConfig], value, *, save=True) -> None: + """Set the value for an arbitrary string config key. + note: try to use explicit predefined ConfigVars instead of this method, whenever possible. + This method side-steps ConfigVars completely, and is mainly kept for situations + where the config key is dynamically constructed. + """ + if isinstance(key, (ConfigVar, ConfigVarWithConfig)): + key = key.key() + assert isinstance(key, str), key if not self.is_modifiable(key): self.logger.warning(f"not changing config key '{key}' set on the command line") return @@ -170,7 +247,8 @@ class SimpleConfig(Logger): return self._set_key_in_user_config(key, value, save=save) - def _set_key_in_user_config(self, key, value, *, save=True): + def _set_key_in_user_config(self, key: str, value, *, save=True) -> None: + assert isinstance(key, str), key with self.lock: if value is not None: self.user_config[key] = value @@ -179,18 +257,33 @@ class SimpleConfig(Logger): if save: self.save_user_config() - def get(self, key, default=None): + def get(self, key: str, default=None) -> Any: + """Get the value for an arbitrary string config key. + note: try to use explicit predefined ConfigVars instead of this method, whenever possible. + This method side-steps ConfigVars completely, and is mainly kept for situations + where the config key is dynamically constructed. + """ + assert isinstance(key, str), key with self.lock: out = self.cmdline_options.get(key) if out is None: out = self.user_config.get(key, default) return out + def is_set(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> bool: + """Returns whether the config key has any explicit value set/defined.""" + if isinstance(key, (ConfigVar, ConfigVarWithConfig)): + key = key.key() + assert isinstance(key, str), key + return self.get(key, default=...) is not ... + def _check_dependent_keys(self) -> None: - if self.get('serverfingerprint'): - if not self.get('server'): - raise Exception("config key 'serverfingerprint' requires 'server' to also be set") - self.make_key_not_modifiable('server') + if self.NETWORK_SERVERFINGERPRINT: + if not self.NETWORK_SERVER: + raise Exception( + f"config key {self.__class__.NETWORK_SERVERFINGERPRINT.key()!r} requires " + f"{self.__class__.NETWORK_SERVER.key()!r} to also be set") + self.make_key_not_modifiable(self.__class__.NETWORK_SERVER) def requires_upgrade(self): return self.get_config_version() < FINAL_CONFIG_VERSION @@ -254,15 +347,20 @@ class SimpleConfig(Logger): .format(config_version, FINAL_CONFIG_VERSION)) return config_version - def is_modifiable(self, key) -> bool: + def is_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> bool: + if isinstance(key, (ConfigVar, ConfigVarWithConfig)): + key = key.key() return (key not in self.cmdline_options and key not in self._not_modifiable_keys) - def make_key_not_modifiable(self, key) -> None: + def make_key_not_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> None: + if isinstance(key, (ConfigVar, ConfigVarWithConfig)): + key = key.key() + assert isinstance(key, str), key self._not_modifiable_keys.add(key) def save_user_config(self): - if self.get('forget_config'): + if self.CONFIG_FORGET_CHANGES: return if not self.path: return @@ -277,13 +375,13 @@ class SimpleConfig(Logger): if os.path.exists(self.path): # or maybe not? raise - def get_backup_dir(self): + def get_backup_dir(self) -> Optional[str]: # this is used to save wallet file backups (without active lightning channels) # on Android, the export backup button uses android_backup_dir() if 'ANDROID_DATA' in os.environ: return None else: - return self.get('backup_dir') + return self.WALLET_BACKUP_DIRECTORY def get_wallet_path(self, *, use_gui_last_wallet=False): """Set the path of the wallet.""" @@ -293,7 +391,7 @@ class SimpleConfig(Logger): return os.path.join(self.get('cwd', ''), self.get('wallet_path')) if use_gui_last_wallet: - path = self.get('gui_last_wallet') + path = self.GUI_LAST_WALLET if path and os.path.exists(path): return path @@ -314,22 +412,22 @@ class SimpleConfig(Logger): return path def remove_from_recently_open(self, filename): - recent = self.get('recently_open', []) + recent = self.RECENTLY_OPEN_WALLET_FILES or [] if filename in recent: recent.remove(filename) - self.set_key('recently_open', recent) + self.RECENTLY_OPEN_WALLET_FILES = recent def set_session_timeout(self, seconds): self.logger.info(f"session timeout -> {seconds} seconds") - self.set_key('session_timeout', seconds) + self.HWD_SESSION_TIMEOUT = seconds def get_session_timeout(self): - return self.get('session_timeout', 300) + return self.HWD_SESSION_TIMEOUT def save_last_wallet(self, wallet): if self.get('wallet_path') is None: path = wallet.storage.path - self.set_key('gui_last_wallet', path) + self.GUI_LAST_WALLET = path def impose_hard_limits_on_fee(func): def get_fee_within_limits(self, *args, **kwargs): @@ -511,13 +609,13 @@ class SimpleConfig(Logger): tooltip = '' return text, tooltip - def get_depth_level(self): + def get_depth_level(self) -> int: maxp = len(FEE_DEPTH_TARGETS) - 1 - return min(maxp, self.get('depth_level', 2)) + return min(maxp, self.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS) - def get_fee_level(self): + def get_fee_level(self) -> int: maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block" - return min(maxp, self.get('fee_level', 2)) + return min(maxp, self.FEE_EST_DYNAMIC_ETA_SLIDERPOS) def get_fee_slider(self, dyn, mempool) -> Tuple[int, int, Optional[int]]: if dyn: @@ -556,11 +654,11 @@ class SimpleConfig(Logger): else: return self.has_fee_etas() - def is_dynfee(self): - return bool(self.get('dynamic_fees', True)) + def is_dynfee(self) -> bool: + return self.FEE_EST_DYNAMIC - def use_mempool_fees(self): - return bool(self.get('mempool_fees', False)) + def use_mempool_fees(self) -> bool: + return self.FEE_EST_USE_MEMPOOL def _feerate_from_fractional_slider_position(self, fee_level: float, dyn: bool, mempool: bool) -> Union[int, None]: @@ -599,7 +697,7 @@ class SimpleConfig(Logger): else: fee_rate = self.eta_to_fee(self.get_fee_level()) else: - fee_rate = self.get('fee_per_kb', FEERATE_FALLBACK_STATIC_FEE) + fee_rate = self.FEE_EST_STATIC_FEERATE_FALLBACK if fee_rate is not None: fee_rate = int(fee_rate) return fee_rate @@ -648,14 +746,14 @@ class SimpleConfig(Logger): self.last_time_fee_estimates_requested = time.time() def get_video_device(self): - device = self.get("video_device", "default") + device = self.VIDEO_DEVICE_PATH if device == 'default': device = '' return device def get_ssl_context(self): - ssl_keyfile = self.get('ssl_keyfile') - ssl_certfile = self.get('ssl_certfile') + ssl_keyfile = self.SSL_KEYFILE_PATH + ssl_certfile = self.SSL_CERTFILE_PATH if ssl_keyfile and ssl_certfile: ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.load_cert_chain(ssl_certfile, ssl_keyfile) @@ -663,13 +761,16 @@ class SimpleConfig(Logger): def get_ssl_domain(self): from .paymentrequest import check_ssl_config - if self.get('ssl_keyfile') and self.get('ssl_certfile'): + if self.SSL_KEYFILE_PATH and self.SSL_CERTFILE_PATH: SSL_identity = check_ssl_config(self) else: SSL_identity = None return SSL_identity - def get_netaddress(self, key: str) -> Optional[NetAddress]: + def get_netaddress(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> Optional[NetAddress]: + if isinstance(key, (ConfigVar, ConfigVarWithConfig)): + key = key.key() + assert isinstance(key, str), key text = self.get(key) if text: try: @@ -709,13 +810,158 @@ class SimpleConfig(Logger): def set_base_unit(self, unit): assert unit in base_units.keys() self.decimal_point = base_unit_name_to_decimal_point(unit) - self.set_key('decimal_point', self.decimal_point, save=True) + self.BTC_AMOUNTS_DECIMAL_POINT = self.decimal_point def get_decimal_point(self): return self.decimal_point + @cached_property + def cv(self): + """Allows getting a reference to a config variable without dereferencing it. -def read_user_config(path): + Compare: + >>> config.NETWORK_SERVER + 'testnet.hsmiths.com:53012:s' + >>> config.cv.NETWORK_SERVER + + """ + class CVLookupHelper: + def __getattribute__(self2, name: str) -> ConfigVarWithConfig: + config_var = self.__class__.__getattribute__(type(self), name) + if not isinstance(config_var, ConfigVar): + raise AttributeError() + return ConfigVarWithConfig(config=self, config_var=config_var) + def __setattr__(self, name, value): + raise Exception( + f"Cannot assign value to config.cv.{name} directly. " + f"Either use config.cv.{name}.set() or assign to config.{name} instead.") + return CVLookupHelper() + + # config variables -----> + + NETWORK_AUTO_CONNECT = ConfigVar('auto_connect', default=True, type_=bool) + NETWORK_ONESERVER = ConfigVar('oneserver', default=False, type_=bool) + NETWORK_PROXY = ConfigVar('proxy', default=None) + NETWORK_SERVER = ConfigVar('server', default=None, type_=str) + NETWORK_NOONION = ConfigVar('noonion', default=False, type_=bool) + NETWORK_OFFLINE = ConfigVar('offline', default=False, type_=bool) + NETWORK_SKIPMERKLECHECK = ConfigVar('skipmerklecheck', default=False, type_=bool) + NETWORK_SERVERFINGERPRINT = ConfigVar('serverfingerprint', default=None, type_=str) + NETWORK_MAX_INCOMING_MSG_SIZE = ConfigVar('network_max_incoming_msg_size', default=1_000_000, type_=int) # in bytes + NETWORK_TIMEOUT = ConfigVar('network_timeout', default=None, type_=int) + + WALLET_BATCH_RBF = ConfigVar('batch_rbf', default=False, type_=bool) + WALLET_SPEND_CONFIRMED_ONLY = ConfigVar('confirmed_only', default=False, type_=bool) + WALLET_COIN_CHOOSER_POLICY = ConfigVar('coin_chooser', default='Privacy', type_=str) + WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = ConfigVar('coin_chooser_output_rounding', default=True, type_=bool) + WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT = ConfigVar('unconf_utxo_freeze_threshold', default=5_000, type_=int) + WALLET_BIP21_LIGHTNING = ConfigVar('bip21_lightning', default=False, type_=bool) + WALLET_BOLT11_FALLBACK = ConfigVar('bolt11_fallback', default=True, type_=bool) + WALLET_PAYREQ_EXPIRY_SECONDS = ConfigVar('request_expiry', default=invoices.PR_DEFAULT_EXPIRATION_WHEN_CREATING, type_=int) + WALLET_USE_SINGLE_PASSWORD = ConfigVar('single_password', default=False, type_=bool) + # note: 'use_change' and 'multiple_change' are per-wallet settings + + FX_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool) + FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str) + FX_EXCHANGE = ConfigVar('use_exchange', default='CoinGecko', type_=str) # default exchange should ideally provide historical rates + FX_HISTORY_RATES = ConfigVar('history_rates', default=False, type_=bool) + FX_HISTORY_RATES_CAPITAL_GAINS = ConfigVar('history_rates_capital_gains', default=False, type_=bool) + FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES = ConfigVar('fiat_address', default=False, type_=bool) + + LIGHTNING_LISTEN = ConfigVar('lightning_listen', default=None, type_=str) + LIGHTNING_PEERS = ConfigVar('lightning_peers', default=None) + LIGHTNING_USE_GOSSIP = ConfigVar('use_gossip', default=False, type_=bool) + LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar('use_recoverable_channels', default=True, type_=bool) + LIGHTNING_ALLOW_INSTANT_SWAPS = ConfigVar('allow_instant_swaps', default=False, type_=bool) + LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int) + + EXPERIMENTAL_LN_FORWARD_PAYMENTS = ConfigVar('lightning_forward_payments', default=False, type_=bool) + EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS = ConfigVar('lightning_forward_trampoline_payments', default=False, type_=bool) + TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = ConfigVar('test_fail_htlcs_with_temp_node_failure', default=False, type_=bool) + TEST_FAIL_HTLCS_AS_MALFORMED = ConfigVar('test_fail_malformed_htlc', default=False, type_=bool) + TEST_SHUTDOWN_FEE = ConfigVar('test_shutdown_fee', default=None, type_=int) + TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None) + TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool) + + FEE_EST_DYNAMIC = ConfigVar('dynamic_fees', default=True, type_=bool) + FEE_EST_USE_MEMPOOL = ConfigVar('mempool_fees', default=False, type_=bool) + FEE_EST_STATIC_FEERATE_FALLBACK = ConfigVar('fee_per_kb', default=FEERATE_FALLBACK_STATIC_FEE, type_=int) + FEE_EST_DYNAMIC_ETA_SLIDERPOS = ConfigVar('fee_level', default=2, type_=int) + FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS = ConfigVar('depth_level', default=2, type_=int) + + RPC_USERNAME = ConfigVar('rpcuser', default=None, type_=str) + RPC_PASSWORD = ConfigVar('rpcpassword', default=None, type_=str) + RPC_HOST = ConfigVar('rpchost', default='127.0.0.1', type_=str) + RPC_PORT = ConfigVar('rpcport', default=0, type_=int) + RPC_SOCKET_TYPE = ConfigVar('rpcsock', default='auto', type_=str) + RPC_SOCKET_FILEPATH = ConfigVar('rpcsockpath', default=None, type_=str) + + GUI_NAME = ConfigVar('gui', default='qt', type_=str) + GUI_LAST_WALLET = ConfigVar('gui_last_wallet', default=None, type_=str) + + GUI_QT_COLOR_THEME = ConfigVar('qt_gui_color_theme', default='default', type_=str) + GUI_QT_DARK_TRAY_ICON = ConfigVar('dark_icon', default=False, type_=bool) + GUI_QT_WINDOW_IS_MAXIMIZED = ConfigVar('is_maximized', default=False, type_=bool) + GUI_QT_HIDE_ON_STARTUP = ConfigVar('hide_gui', default=False, type_=bool) + GUI_QT_HISTORY_TAB_SHOW_TOOLBAR = ConfigVar('show_toolbar_history', default=False, type_=bool) + GUI_QT_ADDRESSES_TAB_SHOW_TOOLBAR = ConfigVar('show_toolbar_addresses', default=False, type_=bool) + GUI_QT_TX_DIALOG_FETCH_TXIN_DATA = ConfigVar('tx_dialog_fetch_txin_data', default=False, type_=bool) + GUI_QT_RECEIVE_TABS_INDEX = ConfigVar('receive_tabs_index', default=0, type_=int) + GUI_QT_RECEIVE_TAB_QR_VISIBLE = ConfigVar('receive_qr_visible', default=False, type_=bool) + GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar('show_tx_io', default=False, type_=bool) + GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS = ConfigVar('show_tx_fee_details', default=False, type_=bool) + GUI_QT_TX_EDITOR_SHOW_LOCKTIME = ConfigVar('show_tx_locktime', default=False, type_=bool) + + GUI_QML_PREFERRED_REQUEST_TYPE = ConfigVar('preferred_request_type', default='bolt11', type_=str) + GUI_QML_USER_KNOWS_PRESS_AND_HOLD = ConfigVar('user_knows_press_and_hold', default=False, type_=bool) + + BTC_AMOUNTS_DECIMAL_POINT = ConfigVar('decimal_point', default=DECIMAL_POINT_DEFAULT, type_=int) + BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT = ConfigVar('num_zeros', default=0, type_=int) + BTC_AMOUNTS_PREC_POST_SAT = ConfigVar('amt_precision_post_satoshi', default=0, type_=int) + BTC_AMOUNTS_ADD_THOUSANDS_SEP = ConfigVar('amt_add_thousands_sep', default=False, type_=bool) + + BLOCK_EXPLORER = ConfigVar('block_explorer', default='Blockstream.info', type_=str) + BLOCK_EXPLORER_CUSTOM = ConfigVar('block_explorer_custom', default=None) + VIDEO_DEVICE_PATH = ConfigVar('video_device', default='default', type_=str) + OPENALIAS_ID = ConfigVar('alias', default="", type_=str) + HWD_SESSION_TIMEOUT = ConfigVar('session_timeout', default=300, type_=int) + CLI_TIMEOUT = ConfigVar('timeout', default=60, type_=float) + AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = ConfigVar('check_updates', default=False, type_=bool) + WRITE_LOGS_TO_DISK = ConfigVar('log_to_file', default=False, type_=bool) + GUI_ENABLE_DEBUG_LOGS = ConfigVar('gui_enable_debug_logs', default=False, type_=bool) + LOCALIZATION_LANGUAGE = ConfigVar('language', default="", type_=str) + BLOCKCHAIN_PREFERRED_BLOCK = ConfigVar('blockchain_preferred_block', default=None) + SHOW_CRASH_REPORTER = ConfigVar('show_crash_reporter', default=True, type_=bool) + DONT_SHOW_TESTNET_WARNING = ConfigVar('dont_show_testnet_warning', default=False, type_=bool) + RECENTLY_OPEN_WALLET_FILES = ConfigVar('recently_open', default=None) + IO_DIRECTORY = ConfigVar('io_dir', default=os.path.expanduser('~'), type_=str) + WALLET_BACKUP_DIRECTORY = ConfigVar('backup_dir', default=None, type_=str) + CONFIG_PIN_CODE = ConfigVar('pin_code', default=None, type_=str) + QR_READER_FLIP_X = ConfigVar('qrreader_flip_x', default=True, type_=bool) + WIZARD_DONT_CREATE_SEGWIT = ConfigVar('nosegwit', default=False, type_=bool) + CONFIG_FORGET_CHANGES = ConfigVar('forget_config', default=False, type_=bool) + + SSL_CERTFILE_PATH = ConfigVar('ssl_certfile', default='', type_=str) + SSL_KEYFILE_PATH = ConfigVar('ssl_keyfile', default='', type_=str) + + # connect to remote WT + WATCHTOWER_CLIENT_ENABLED = ConfigVar('use_watchtower', default=False, type_=bool) + WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str) + + # run WT locally + WATCHTOWER_SERVER_ENABLED = ConfigVar('run_watchtower', default=False, type_=bool) + WATCHTOWER_SERVER_ADDRESS = ConfigVar('watchtower_address', default=None, type_=str) + WATCHTOWER_SERVER_USER = ConfigVar('watchtower_user', default=None, type_=str) + WATCHTOWER_SERVER_PASSWORD = ConfigVar('watchtower_password', default=None, type_=str) + + PAYSERVER_ADDRESS = ConfigVar('payserver_address', default='localhost:8080', type_=str) + PAYSERVER_ROOT = ConfigVar('payserver_root', default='/r', type_=str) + PAYSERVER_ALLOW_CREATE_INVOICE = ConfigVar('payserver_allow_create_invoice', default=False, type_=bool) + + PLUGIN_TRUSTEDCOIN_NUM_PREPAY = ConfigVar('trustedcoin_prepay', default=20, type_=int) + + +def read_user_config(path: Optional[str]) -> Dict[str, Any]: """Parse and store the user config settings in electrum.conf into user_config[].""" if not path: return {} diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 5708a0df9..43e11951e 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -203,7 +203,7 @@ class SwapManager(Logger): self.lnwatcher.remove_callback(swap.lockup_address) swap.is_redeemed = True elif spent_height == TX_HEIGHT_LOCAL: - if txin.block_height > 0 or self.wallet.config.get('allow_instant_swaps', False): + if txin.block_height > 0 or self.wallet.config.LIGHTNING_ALLOW_INSTANT_SWAPS: tx = self.lnwatcher.adb.get_transaction(txin.spent_txid) self.logger.info(f'broadcasting tx {txin.spent_txid}') await self.network.broadcast_transaction(tx) diff --git a/electrum/tests/test_daemon.py b/electrum/tests/test_daemon.py index 688890880..d5e707335 100644 --- a/electrum/tests/test_daemon.py +++ b/electrum/tests/test_daemon.py @@ -15,8 +15,8 @@ class TestUnifiedPassword(ElectrumTestCase): def setUp(self): super().setUp() self.config = SimpleConfig({'electrum_path': self.electrum_path}) - self.config.set_key("single_password", True) - self.config.set_key("offline", True) + self.config.WALLET_USE_SINGLE_PASSWORD = True + self.config.NETWORK_OFFLINE = True self.wallet_dir = os.path.dirname(self.config.get_wallet_path()) assert "wallets" == os.path.basename(self.wallet_dir) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 095165469..99051c516 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -366,8 +366,8 @@ GRAPH_DEFINITIONS = { 'dave': high_fee_channel.copy(), }, 'config': { - 'lightning_forward_payments': True, - 'lightning_forward_trampoline_payments': True, + SimpleConfig.EXPERIMENTAL_LN_FORWARD_PAYMENTS: True, + SimpleConfig.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS: True, }, }, 'carol': { @@ -375,8 +375,8 @@ GRAPH_DEFINITIONS = { 'dave': low_fee_channel.copy(), }, 'config': { - 'lightning_forward_payments': True, - 'lightning_forward_trampoline_payments': True, + SimpleConfig.EXPERIMENTAL_LN_FORWARD_PAYMENTS: True, + SimpleConfig.EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS: True, }, }, 'dave': { @@ -932,8 +932,8 @@ class TestPeer(ElectrumTestCase): @needs_test_with_all_chacha20_implementations async def test_payment_multihop_temp_node_failure(self): graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) - graph.workers['bob'].network.config.set_key('test_fail_htlcs_with_temp_node_failure', True) - graph.workers['carol'].network.config.set_key('test_fail_htlcs_with_temp_node_failure', True) + graph.workers['bob'].network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = True + graph.workers['carol'].network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = True peers = graph.peers.values() async def pay(lnaddr, pay_req): self.assertEqual(PR_UNPAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash)) @@ -959,7 +959,7 @@ class TestPeer(ElectrumTestCase): # Alice will pay Dave. Alice first tries A->C->D route, due to lower fees, but Carol # will fail the htlc and get blacklisted. Alice will then try A->B->D and succeed. graph = self.prepare_chans_and_peers_in_graph(GRAPH_DEFINITIONS['square_graph']) - graph.workers['carol'].network.config.set_key('test_fail_htlcs_with_temp_node_failure', True) + graph.workers['carol'].network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = True peers = graph.peers.values() async def pay(lnaddr, pay_req): self.assertEqual(500000000000, graph.channels[('alice', 'bob')].balance(LOCAL)) @@ -1298,16 +1298,16 @@ class TestPeer(ElectrumTestCase): async def _test_shutdown(self, alice_fee, bob_fee, alice_fee_range=None, bob_fee_range=None): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) - w1.network.config.set_key('test_shutdown_fee', alice_fee) - w2.network.config.set_key('test_shutdown_fee', bob_fee) + w1.network.config.TEST_SHUTDOWN_FEE = alice_fee + w2.network.config.TEST_SHUTDOWN_FEE = bob_fee if alice_fee_range is not None: - w1.network.config.set_key('test_shutdown_fee_range', alice_fee_range) + w1.network.config.TEST_SHUTDOWN_FEE_RANGE = alice_fee_range else: - w1.network.config.set_key('test_shutdown_legacy', True) + w1.network.config.TEST_SHUTDOWN_LEGACY = True if bob_fee_range is not None: - w2.network.config.set_key('test_shutdown_fee_range', bob_fee_range) + w2.network.config.TEST_SHUTDOWN_FEE_RANGE = bob_fee_range else: - w2.network.config.set_key('test_shutdown_legacy', True) + w2.network.config.TEST_SHUTDOWN_LEGACY = True w2.enable_htlc_settle = False lnaddr, pay_req = self.prepare_invoice(w2) async def pay(): @@ -1377,10 +1377,10 @@ class TestPeer(ElectrumTestCase): bob_channel.config[HTLCOwner.LOCAL].upfront_shutdown_script = b'' p1, p2, w1, w2, q1, q2 = self.prepare_peers(alice_channel, bob_channel) - w1.network.config.set_key('dynamic_fees', False) - w2.network.config.set_key('dynamic_fees', False) - w1.network.config.set_key('fee_per_kb', 5000) - w2.network.config.set_key('fee_per_kb', 1000) + w1.network.config.FEE_EST_DYNAMIC = False + w2.network.config.FEE_EST_DYNAMIC = False + w1.network.config.FEE_EST_STATIC_FEERATE_FALLBACK = 5000 + w2.network.config.FEE_EST_STATIC_FEERATE_FALLBACK = 1000 async def test(): async def close(): @@ -1407,10 +1407,10 @@ class TestPeer(ElectrumTestCase): bob_channel.config[HTLCOwner.LOCAL].upfront_shutdown_script = bob_uss p1, p2, w1, w2, q1, q2 = self.prepare_peers(alice_channel, bob_channel) - w1.network.config.set_key('dynamic_fees', False) - w2.network.config.set_key('dynamic_fees', False) - w1.network.config.set_key('fee_per_kb', 5000) - w2.network.config.set_key('fee_per_kb', 1000) + w1.network.config.FEE_EST_DYNAMIC = False + w2.network.config.FEE_EST_DYNAMIC = False + w1.network.config.FEE_EST_STATIC_FEERATE_FALLBACK = 5000 + w2.network.config.FEE_EST_STATIC_FEERATE_FALLBACK = 1000 async def test(): async def close(): diff --git a/electrum/tests/test_simple_config.py b/electrum/tests/test_simple_config.py index f15e87a14..3204328a9 100644 --- a/electrum/tests/test_simple_config.py +++ b/electrum/tests/test_simple_config.py @@ -10,6 +10,10 @@ from electrum.simple_config import (SimpleConfig, read_user_config) from . import ElectrumTestCase +MAX_MSG_SIZE_DEFAULT = SimpleConfig.NETWORK_MAX_INCOMING_MSG_SIZE.get_default_value() +assert isinstance(MAX_MSG_SIZE_DEFAULT, int), MAX_MSG_SIZE_DEFAULT + + class Test_SimpleConfig(ElectrumTestCase): def setUp(self): @@ -109,6 +113,75 @@ class Test_SimpleConfig(ElectrumTestCase): result.pop('config_version', None) self.assertEqual({"something": "a"}, result) + def test_configvars_set_and_get(self): + config = SimpleConfig(self.options) + self.assertEqual("server", config.cv.NETWORK_SERVER.key()) + + def _set_via_assignment(): + config.NETWORK_SERVER = "example.com:443:s" + + for f in ( + lambda: config.set_key("server", "example.com:443:s"), + _set_via_assignment, + lambda: config.cv.NETWORK_SERVER.set("example.com:443:s"), + ): + self.assertTrue(config.get("server") is None) + self.assertTrue(config.NETWORK_SERVER is None) + self.assertTrue(config.cv.NETWORK_SERVER.get() is None) + f() + self.assertEqual("example.com:443:s", config.get("server")) + self.assertEqual("example.com:443:s", config.NETWORK_SERVER) + self.assertEqual("example.com:443:s", config.cv.NETWORK_SERVER.get()) + # revert: + config.NETWORK_SERVER = None + + def test_configvars_get_default_value(self): + config = SimpleConfig(self.options) + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.get_default_value()) + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE) + + config.NETWORK_MAX_INCOMING_MSG_SIZE = 5_555_555 + self.assertEqual(5_555_555, config.NETWORK_MAX_INCOMING_MSG_SIZE) + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.get_default_value()) + + config.NETWORK_MAX_INCOMING_MSG_SIZE = None + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE) + + def test_configvars_is_set(self): + config = SimpleConfig(self.options) + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE) + self.assertFalse(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_set()) + + config.NETWORK_MAX_INCOMING_MSG_SIZE = 5_555_555 + self.assertTrue(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_set()) + + config.NETWORK_MAX_INCOMING_MSG_SIZE = None + self.assertFalse(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_set()) + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE) + + config.NETWORK_MAX_INCOMING_MSG_SIZE = MAX_MSG_SIZE_DEFAULT + self.assertTrue(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_set()) + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE) + + def test_configvars_is_modifiable(self): + config = SimpleConfig({**self.options, "server": "example.com:443:s"}) + + self.assertFalse(config.is_modifiable("server")) + self.assertFalse(config.cv.NETWORK_SERVER.is_modifiable()) + + config.NETWORK_SERVER = "other-example.com:80:t" + self.assertEqual("example.com:443:s", config.NETWORK_SERVER) + + self.assertEqual(MAX_MSG_SIZE_DEFAULT, config.NETWORK_MAX_INCOMING_MSG_SIZE) + self.assertTrue(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_modifiable()) + config.NETWORK_MAX_INCOMING_MSG_SIZE = 5_555_555 + self.assertEqual(5_555_555, config.NETWORK_MAX_INCOMING_MSG_SIZE) + + config.make_key_not_modifiable(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE) + self.assertFalse(config.cv.NETWORK_MAX_INCOMING_MSG_SIZE.is_modifiable()) + config.NETWORK_MAX_INCOMING_MSG_SIZE = 2_222_222 + self.assertEqual(5_555_555, config.NETWORK_MAX_INCOMING_MSG_SIZE) + def test_depth_target_to_fee(self): config = SimpleConfig(self.options) config.mempool_fees = [[49, 100110], [10, 121301], [6, 153731], [5, 125872], [1, 36488810]] diff --git a/electrum/tests/test_sswaps.py b/electrum/tests/test_sswaps.py index 3ce2906e0..89aaf9a4b 100644 --- a/electrum/tests/test_sswaps.py +++ b/electrum/tests/test_sswaps.py @@ -12,8 +12,8 @@ class TestSwapTxs(ElectrumTestCase): def setUp(self): super().setUp() self.config = SimpleConfig({'electrum_path': self.electrum_path}) - self.config.set_key('dynamic_fees', False) - self.config.set_key('fee_per_kb', 1000) + self.config.FEE_EST_DYNAMIC = False + self.config.FEE_EST_STATIC_FEERATE_FALLBACK = 1000 def test_claim_tx_for_successful_reverse_swap(self): swap_data = SwapData( diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index aeeeeb92a..ebd05b551 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -1040,7 +1040,7 @@ class TestWalletSending(ElectrumTestCase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.config = SimpleConfig({'electrum_path': self.name}) - self.config.set_key('coin_chooser_output_rounding', False) + self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = False def __enter__(self): return self.config @@ -1744,7 +1744,7 @@ class TestWalletSending(ElectrumTestCase): async def _rbf_batching(self, *, simulate_moving_txs, config): wallet = self.create_standard_wallet_from_seed('frost repair depend effort salon ring foam oak cancel receive save usage', config=config) - wallet.config.set_key('batch_rbf', True) + wallet.config.WALLET_BATCH_RBF = True # bootstrap wallet (incoming funding_tx1) funding_tx1 = Transaction('01000000000102acd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba020000001716001455c7f5e0631d8e6f5f05dddb9f676cec48845532fdffffffd146691ef6a207b682b13da5f2388b1f0d2a2022c8cfb8dc27b65434ec9ec8f701000000171600147b3be8a7ceaf15f57d7df2a3d216bc3c259e3225fdffffff02a9875b000000000017a914ea5a99f83e71d1c1dfc5d0370e9755567fe4a141878096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b702483045022100dde1ba0c9a2862a65791b8d91295a6603207fb79635935a67890506c214dd96d022046c6616642ef5971103c1db07ac014e63fa3b0e15c5729eacdd3e77fcb7d2086012103a72410f185401bb5b10aaa30989c272b554dc6d53bda6da85a76f662723421af024730440220033d0be8f74e782fbcec2b396647c7715d2356076b442423f23552b617062312022063c95cafdc6d52ccf55c8ee0f9ceb0f57afb41ea9076eb74fe633f59c50c6377012103b96a4954d834fbcfb2bbf8cf7de7dc2b28bc3d661c1557d1fd1db1bfc123a94abb391400') @@ -1879,7 +1879,7 @@ class TestWalletSending(ElectrumTestCase): coins = wallet.get_spendable_coins(domain=None) self.assertEqual(2, len(coins)) - wallet.config.set_key('batch_rbf', batch_rbf) + wallet.config.WALLET_BATCH_RBF = batch_rbf tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=1000) tx.set_rbf(True) tx.locktime = 2423302 @@ -2211,7 +2211,7 @@ class TestWalletSending(ElectrumTestCase): async def test_dscancel(self, mock_save_db): self.maxDiff = None config = SimpleConfig({'electrum_path': self.electrum_path}) - config.set_key('coin_chooser_output_rounding', False) + config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = False for simulate_moving_txs in (False, True): with self.subTest(msg="_dscancel_when_all_outputs_are_ismine", simulate_moving_txs=simulate_moving_txs): @@ -3691,8 +3691,8 @@ class TestWalletHistory_EvilGapLimit(ElectrumTestCase): super().setUp() self.config = SimpleConfig({ 'electrum_path': self.electrum_path, - 'skipmerklecheck': True, # needed for Synchronizer to generate new addresses without SPV }) + self.config.NETWORK_SKIPMERKLECHECK = True # needed for Synchronizer to generate new addresses without SPV def create_wallet(self): ks = keystore.from_xpub('vpub5Vhmk4dEJKanDTTw6immKXa3thw45u3gbd1rPYjREB6viP13sVTWcH6kvbR2YeLtGjradr6SFLVt9PxWDBSrvw1Dc1nmd3oko3m24CQbfaJ') diff --git a/electrum/util.py b/electrum/util.py index 11b551848..d09a31a18 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -974,25 +974,24 @@ def block_explorer(config: 'SimpleConfig') -> Optional[str]: """Returns name of selected block explorer, or None if a custom one (not among hardcoded ones) is configured. """ - if config.get('block_explorer_custom') is not None: + if config.BLOCK_EXPLORER_CUSTOM is not None: return None - default_ = 'Blockstream.info' - be_key = config.get('block_explorer', default_) + be_key = config.BLOCK_EXPLORER be_tuple = block_explorer_info().get(be_key) if be_tuple is None: - be_key = default_ + be_key = config.cv.BLOCK_EXPLORER.get_default_value() assert isinstance(be_key, str), f"{be_key!r} should be str" return be_key def block_explorer_tuple(config: 'SimpleConfig') -> Optional[Tuple[str, dict]]: - custom_be = config.get('block_explorer_custom') + custom_be = config.BLOCK_EXPLORER_CUSTOM if custom_be: if isinstance(custom_be, str): return custom_be, _block_explorer_default_api_loc if isinstance(custom_be, (tuple, list)) and len(custom_be) == 2: return tuple(custom_be) - _logger.warning(f"not using 'block_explorer_custom' from config. " + _logger.warning(f"not using {config.cv.BLOCK_EXPLORER_CUSTOM.key()!r} from config. " f"expected a str or a pair but got {custom_be!r}") return None else: diff --git a/electrum/verifier.py b/electrum/verifier.py index 13f556b0d..ab44220ba 100644 --- a/electrum/verifier.py +++ b/electrum/verifier.py @@ -121,7 +121,7 @@ class SPV(NetworkJobOnDefaultServer): try: verify_tx_is_in_block(tx_hash, merkle_branch, pos, header, tx_height) except MerkleVerificationFailure as e: - if self.network.config.get("skipmerklecheck"): + if self.network.config.NETWORK_SKIPMERKLECHECK: self.logger.info(f"skipping merkle proof check {tx_hash}") else: self.logger.info(repr(e)) diff --git a/electrum/wallet.py b/electrum/wallet.py index 1ac27628d..f2efbc441 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -1731,7 +1731,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): # Let the coin chooser select the coins to spend coin_chooser = coinchooser.get_coin_chooser(self.config) # If there is an unconfirmed RBF tx, merge with it - base_tx = self.get_unconfirmed_base_tx_for_batching(outputs, coins) if self.config.get('batch_rbf', False) else None + base_tx = self.get_unconfirmed_base_tx_for_batching(outputs, coins) if self.config.WALLET_BATCH_RBF else None if base_tx: # make sure we don't try to spend change from the tx-to-be-replaced: coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()] @@ -1847,7 +1847,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): # exempt large value UTXOs value_sats = utxo.value_sats() assert value_sats is not None - threshold = self.config.get('unconf_utxo_freeze_threshold', 5_000) + threshold = self.config.WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT if value_sats >= threshold: return False # if funding tx has any is_mine input, then UTXO is fine @@ -2457,7 +2457,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): def get_request_URI(self, req: Request) -> Optional[str]: lightning_invoice = None - if self.config.get('bip21_lightning', False): + if self.config.WALLET_BIP21_LIGHTNING: lightning_invoice = self.get_bolt11_invoice(req) return req.get_bip21_URI(lightning_invoice=lightning_invoice) @@ -2614,7 +2614,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): amount_msat=amount_msat, message=req.message, expiry=req.exp, - fallback_address=req.get_address() if self.config.get('bolt11_fallback', True) else None) + fallback_address=req.get_address() if self.config.WALLET_BOLT11_FALLBACK else None) return invoice def create_request(self, amount_sat: int, message: str, exp_delay: int, address: Optional[str]): diff --git a/run_electrum b/run_electrum index 0f32d954f..36d9817b9 100755 --- a/run_electrum +++ b/run_electrum @@ -341,8 +341,8 @@ def main(): config_options = { 'verbosity': '*' if util.is_android_debug_apk() else '', 'cmd': 'gui', - 'gui': android_gui, - 'single_password': True, + SimpleConfig.GUI_NAME.key(): android_gui, + SimpleConfig.WALLET_USE_SINGLE_PASSWORD.key(): True, } if util.get_android_package_name() == "org.electrum.testnet.electrum": # ~hack for easier testnet builds. pkgname subject to change. @@ -394,8 +394,8 @@ def main(): # to not-yet-evaluated strings. if cmdname == 'gui': from electrum.gui.default_lang import get_default_language - gui_name = config.get('gui', 'qt') - lang = config.get('language') + gui_name = config.GUI_NAME + lang = config.LOCALIZATION_LANGUAGE if not lang: lang = get_default_language(gui_name=gui_name) _logger.info(f"get_default_language: detected default as {lang=!r}") @@ -459,7 +459,7 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict): configure_logging(config) fd = daemon.get_file_descriptor(config) if fd is not None: - plugins = init_plugins(config, config.get('gui', 'qt')) + plugins = init_plugins(config, config.GUI_NAME) d = daemon.Daemon(config, fd, start_network=False) try: d.run_gui(config, plugins) @@ -490,10 +490,9 @@ def handle_cmd(*, cmdname: str, config: 'SimpleConfig', config_options: dict): configure_logging(config, log_to_file=False) # don't spam logfiles for each client-side RPC, but support "-v" cmd = known_commands[cmdname] wallet_path = config.get_wallet_path() - if not config.get('offline'): + if not config.NETWORK_OFFLINE: init_cmdline(config_options, wallet_path, True, config=config) - timeout = config.get('timeout', 60) - if timeout: timeout = int(timeout) + timeout = config.CLI_TIMEOUT try: result = daemon.request(config, 'run_cmdline', (config_options,), timeout) except daemon.DaemonNotRunning: From dfa2b71bc326883da38865aa8609509eaf70eff2 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 May 2023 14:03:03 +0000 Subject: [PATCH 2/3] config: trivial rename for better readability --- electrum/simple_config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index d98b00b7d..4ebf37f72 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -816,7 +816,7 @@ class SimpleConfig(Logger): return self.decimal_point @cached_property - def cv(self): + def cv(config): """Allows getting a reference to a config variable without dereferencing it. Compare: @@ -826,11 +826,11 @@ class SimpleConfig(Logger): """ class CVLookupHelper: - def __getattribute__(self2, name: str) -> ConfigVarWithConfig: - config_var = self.__class__.__getattribute__(type(self), name) + def __getattribute__(self, name: str) -> ConfigVarWithConfig: + config_var = config.__class__.__getattribute__(type(config), name) if not isinstance(config_var, ConfigVar): raise AttributeError() - return ConfigVarWithConfig(config=self, config_var=config_var) + return ConfigVarWithConfig(config=config, config_var=config_var) def __setattr__(self, name, value): raise Exception( f"Cannot assign value to config.cv.{name} directly. " From 328a2bb3f23b7db7b46479dcc6572cfc55b7e3d1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 30 May 2023 13:47:56 +0000 Subject: [PATCH 3/3] config: migrate qt gui optional tabs to config vars --- electrum/gui/qt/main_window.py | 40 ++++++++++++++++++++-------------- electrum/simple_config.py | 5 +++++ 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 08eb6490f..2eefc1ec8 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -103,6 +103,7 @@ from .swap_dialog import SwapDialog, InvalidSwapParameters from .balance_dialog import BalanceToolButton, COLOR_FROZEN, COLOR_UNMATURED, COLOR_UNCONFIRMED, COLOR_CONFIRMED, COLOR_LIGHTNING, COLOR_FROZEN_LIGHTNING if TYPE_CHECKING: + from electrum.simple_config import ConfigVarWithConfig from . import ElectrumGui @@ -173,8 +174,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.gui_thread = gui_object.gui_thread assert wallet, "no wallet" self.wallet = wallet - if wallet.has_lightning(): - self.wallet.config.set_key('show_channels_tab', True) + if wallet.has_lightning() and not self.config.cv.GUI_QT_SHOW_TAB_CHANNELS.is_set(): + self.config.GUI_QT_SHOW_TAB_CHANNELS = True # override default, but still allow disabling tab manually Exception_Hook.maybe_setup(config=self.config, wallet=self.wallet) @@ -216,19 +217,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): tabs.addTab(self.send_tab, read_QIcon("tab_send.png"), _('Send')) tabs.addTab(self.receive_tab, read_QIcon("tab_receive.png"), _('Receive')) - def add_optional_tab(tabs, tab, icon, description, name): + def add_optional_tab(tabs, tab, icon, description): tab.tab_icon = icon tab.tab_description = description tab.tab_pos = len(tabs) - tab.tab_name = name - if self.config.get('show_{}_tab'.format(name), False): + if tab.is_shown_cv.get(): tabs.addTab(tab, icon, description.replace("&", "")) - add_optional_tab(tabs, self.addresses_tab, read_QIcon("tab_addresses.png"), _("&Addresses"), "addresses") - add_optional_tab(tabs, self.channels_tab, read_QIcon("lightning.png"), _("Channels"), "channels") - add_optional_tab(tabs, self.utxo_tab, read_QIcon("tab_coins.png"), _("Co&ins"), "utxo") - add_optional_tab(tabs, self.contacts_tab, read_QIcon("tab_contacts.png"), _("Con&tacts"), "contacts") - add_optional_tab(tabs, self.console_tab, read_QIcon("tab_console.png"), _("Con&sole"), "console") + add_optional_tab(tabs, self.addresses_tab, read_QIcon("tab_addresses.png"), _("&Addresses")) + add_optional_tab(tabs, self.channels_tab, read_QIcon("lightning.png"), _("Channels")) + add_optional_tab(tabs, self.utxo_tab, read_QIcon("tab_coins.png"), _("Co&ins")) + add_optional_tab(tabs, self.contacts_tab, read_QIcon("tab_contacts.png"), _("Con&tacts")) + add_optional_tab(tabs, self.console_tab, read_QIcon("tab_console.png"), _("Con&sole")) tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) @@ -339,8 +339,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.address_list.refresh_all() def toggle_tab(self, tab): - show = not self.config.get('show_{}_tab'.format(tab.tab_name), False) - self.config.set_key('show_{}_tab'.format(tab.tab_name), show) + show = not tab.is_shown_cv.get() + tab.is_shown_cv.set(show) if show: # Find out where to place the tab index = len(self.tabs) @@ -698,7 +698,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F")) def add_toggle_action(view_menu, tab): - is_shown = self.config.get('show_{}_tab'.format(tab.tab_name), False) + is_shown = tab.is_shown_cv.get() tab.menu_action = view_menu.addAction(tab.tab_description, lambda: self.toggle_tab(tab)) tab.menu_action.setCheckable(True) tab.menu_action.setChecked(is_shown) @@ -1026,7 +1026,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def create_channels_tab(self): self.channels_list = ChannelsList(self) - return self.create_list_tab(self.channels_list) + tab = self.create_list_tab(self.channels_list) + tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_CHANNELS + return tab def create_history_tab(self): self.history_model = HistoryModel(self) @@ -1351,17 +1353,22 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): from .address_list import AddressList self.address_list = AddressList(self) tab = self.create_list_tab(self.address_list) + tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_ADDRESSES return tab def create_utxo_tab(self): from .utxo_list import UTXOList self.utxo_list = UTXOList(self) - return self.create_list_tab(self.utxo_list) + tab = self.create_list_tab(self.utxo_list) + tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_UTXO + return tab def create_contacts_tab(self): from .contact_list import ContactList self.contact_list = l = ContactList(self) - return self.create_list_tab(l) + tab = self.create_list_tab(l) + tab.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_CONTACTS + return tab def remove_address(self, addr): if not self.question(_("Do you want to remove {} from your wallet?").format(addr)): @@ -1489,6 +1496,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): def create_console_tab(self): from .console import Console self.console = console = Console() + console.is_shown_cv = self.config.cv.GUI_QT_SHOW_TAB_CONSOLE return console def update_console(self): diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 4ebf37f72..41ad1b47f 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -911,6 +911,11 @@ class SimpleConfig(Logger): GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar('show_tx_io', default=False, type_=bool) GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS = ConfigVar('show_tx_fee_details', default=False, type_=bool) GUI_QT_TX_EDITOR_SHOW_LOCKTIME = ConfigVar('show_tx_locktime', default=False, type_=bool) + GUI_QT_SHOW_TAB_ADDRESSES = ConfigVar('show_addresses_tab', default=False, type_=bool) + GUI_QT_SHOW_TAB_CHANNELS = ConfigVar('show_channels_tab', default=False, type_=bool) + GUI_QT_SHOW_TAB_UTXO = ConfigVar('show_utxo_tab', default=False, type_=bool) + GUI_QT_SHOW_TAB_CONTACTS = ConfigVar('show_contacts_tab', default=False, type_=bool) + GUI_QT_SHOW_TAB_CONSOLE = ConfigVar('show_console_tab', default=False, type_=bool) GUI_QML_PREFERRED_REQUEST_TYPE = ConfigVar('preferred_request_type', default='bolt11', type_=str) GUI_QML_USER_KNOWS_PRESS_AND_HOLD = ConfigVar('user_knows_press_and_hold', default=False, type_=bool)