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..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)
 
@@ -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):
@@ -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)
@@ -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)
@@ -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):
@@ -2557,7 +2565,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..41ad1b47f 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,163 @@ 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(config): + """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__(self, name: str) -> ConfigVarWithConfig: + config_var = config.__class__.__getattribute__(type(config), name) + if not isinstance(config_var, ConfigVar): + raise AttributeError() + return ConfigVarWithConfig(config=config, 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_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) + + 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: