Skip to content

Commit 24980fe

Browse files
committed
config: introduce ConfigVars
A new config API is introduced, and ~all of the codebase is adapted to it. The old API is kept but mainly only for dynamic usage where its extra flexibility is needed. Using examples, the old config API looked this: ``` >>> config.get("request_expiry", 86400) 604800 >>> config.set_key("request_expiry", 86400) >>> ``` The new config API instead: ``` >>> config.WALLET_PAYREQ_EXPIRY_SECONDS 604800 >>> config.WALLET_PAYREQ_EXPIRY_SECONDS = 86400 >>> ``` The old API operated on arbitrary string keys, the new one uses a static ~enum-like list of variables. With the new API: - there is a single centralised list of config variables, as opposed to these being scattered all over - no more duplication of default values (in the getters) - there is now some (minimal for now) type-validation/conversion for the config values closes #5640 closes #5649 Note: there is yet a third API added here, for certain niche/abstract use-cases, where we need a reference to the config variable itself. It should only be used when needed: ``` >>> var = config.cv.WALLET_PAYREQ_EXPIRY_SECONDS >>> var <ConfigVarWithConfig key='request_expiry'> >>> var.get() 604800 >>> var.set(3600) >>> var.get_default_value() 86400 >>> var.is_set() True >>> var.is_modifiable() True ```
1 parent 03ab33f commit 24980fe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+781
-471
lines changed

electrum/address_synchronizer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -959,7 +959,7 @@ def address_is_old(self, address: str, *, req_conf: int = 3) -> bool:
959959
"""
960960
max_conf = -1
961961
h = self.db.get_addr_history(address)
962-
needs_spv_check = not self.config.get("skipmerklecheck", False)
962+
needs_spv_check = not self.config.NETWORK_SKIPMERKLECHECK
963963
for tx_hash, tx_height in h:
964964
if needs_spv_check:
965965
tx_age = self.get_tx_height(tx_hash).conf

electrum/base_crash_reporter.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ class CrashReportResponse(NamedTuple):
4242

4343
class BaseCrashReporter(Logger):
4444
report_server = "https://crashhub.electrum.org"
45-
config_key = "show_crash_reporter"
4645
issue_template = """<h2>Traceback</h2>
4746
<pre>
4847
{traceback}

electrum/base_wizard.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def __init__(self, config: SimpleConfig, plugins: Plugins):
9292
self._stack = [] # type: List[WizardStackItem]
9393
self.plugin = None # type: Optional[BasePlugin]
9494
self.keystores = [] # type: List[KeyStore]
95-
self.is_kivy = config.get('gui') == 'kivy'
95+
self.is_kivy = config.GUI_NAME == 'kivy'
9696
self.seed_type = None
9797

9898
def set_icon(self, icon):
@@ -697,7 +697,7 @@ def show_xpub_and_add_cosigners(self, xpub):
697697
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
698698

699699
def choose_seed_type(self):
700-
seed_type = 'standard' if self.config.get('nosegwit') else 'segwit'
700+
seed_type = 'standard' if self.config.WIZARD_DONT_CREATE_SEGWIT else 'segwit'
701701
self.create_seed(seed_type)
702702

703703
def create_seed(self, seed_type):

electrum/coinchooser.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@
2424
# SOFTWARE.
2525
from collections import defaultdict
2626
from math import floor, log10
27-
from typing import NamedTuple, List, Callable, Sequence, Union, Dict, Tuple, Mapping, Type
27+
from typing import NamedTuple, List, Callable, Sequence, Union, Dict, Tuple, Mapping, Type, TYPE_CHECKING
2828
from decimal import Decimal
2929

3030
from .bitcoin import sha256, COIN, is_address
3131
from .transaction import Transaction, TxOutput, PartialTransaction, PartialTxInput, PartialTxOutput
3232
from .util import NotEnoughFunds
3333
from .logging import Logger
3434

35+
if TYPE_CHECKING:
36+
from .simple_config import SimpleConfig
37+
3538

3639
# A simple deterministic PRNG. Used to deterministically shuffle a
3740
# set of coins - the same set of coins should produce the same output.
@@ -484,20 +487,20 @@ def penalty(buckets: List[Bucket]) -> ScoredCandidate:
484487
'Privacy': CoinChooserPrivacy,
485488
} # type: Mapping[str, Type[CoinChooserBase]]
486489

487-
def get_name(config):
488-
kind = config.get('coin_chooser')
490+
def get_name(config: 'SimpleConfig') -> str:
491+
kind = config.WALLET_COIN_CHOOSER_POLICY
489492
if kind not in COIN_CHOOSERS:
490-
kind = 'Privacy'
493+
kind = config.cv.WALLET_COIN_CHOOSER_POLICY.get_default_value()
491494
return kind
492495

493-
def get_coin_chooser(config) -> CoinChooserBase:
496+
def get_coin_chooser(config: 'SimpleConfig') -> CoinChooserBase:
494497
klass = COIN_CHOOSERS[get_name(config)]
495498
# note: we enable enable_output_value_rounding by default as
496499
# - for sacrificing a few satoshis
497500
# + it gives better privacy for the user re change output
498501
# + it also helps the network as a whole as fees will become noisier
499502
# (trying to counter the heuristic that "whole integer sat/byte feerates" are common)
500503
coinchooser = klass(
501-
enable_output_value_rounding=config.get('coin_chooser_output_rounding', True),
504+
enable_output_value_rounding=config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING,
502505
)
503506
return coinchooser

electrum/commands.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ async def getconfig(self, key):
308308

309309
@classmethod
310310
def _setconfig_normalize_value(cls, key, value):
311-
if key not in ('rpcuser', 'rpcpassword'):
311+
if key not in (SimpleConfig.RPC_USERNAME.key(), SimpleConfig.RPC_PASSWORD.key()):
312312
value = json_decode(value)
313313
# call literal_eval for backward compatibility (see #4225)
314314
try:
@@ -321,9 +321,9 @@ def _setconfig_normalize_value(cls, key, value):
321321
async def setconfig(self, key, value):
322322
"""Set a configuration variable. 'value' may be a string or a Python expression."""
323323
value = self._setconfig_normalize_value(key, value)
324-
if self.daemon and key == 'rpcuser':
324+
if self.daemon and key == SimpleConfig.RPC_USERNAME.key():
325325
self.daemon.commands_server.rpc_user = value
326-
if self.daemon and key == 'rpcpassword':
326+
if self.daemon and key == SimpleConfig.RPC_PASSWORD.key():
327327
self.daemon.commands_server.rpc_password = value
328328
self.config.set_key(key, value)
329329
return True
@@ -1149,7 +1149,7 @@ async def lnpay(self, invoice, timeout=120, wallet: Abstract_Wallet = None):
11491149

11501150
@command('wl')
11511151
async def nodeid(self, wallet: Abstract_Wallet = None):
1152-
listen_addr = self.config.get('lightning_listen')
1152+
listen_addr = self.config.LIGHTNING_LISTEN
11531153
return wallet.lnworker.node_keypair.pubkey.hex() + (('@' + listen_addr) if listen_addr else '')
11541154

11551155
@command('wl')
@@ -1545,13 +1545,14 @@ def subparser_call(self, parser, namespace, values, option_string=None):
15451545

15461546

15471547
def add_network_options(parser):
1548-
parser.add_argument("-f", "--serverfingerprint", dest="serverfingerprint", default=None, help="only allow connecting to servers with a matching SSL certificate SHA256 fingerprint." + " " +
1549-
"To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.")
1550-
parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=None, help="connect to one server only")
1551-
parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
1552-
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")
1553-
parser.add_argument("--noonion", action="store_true", dest="noonion", default=None, help="do not try to connect to onion servers")
1554-
parser.add_argument("--skipmerklecheck", action="store_true", dest="skipmerklecheck", default=None, help="Tolerate invalid merkle proofs from server")
1548+
parser.add_argument("-f", "--serverfingerprint", dest=SimpleConfig.NETWORK_SERVERFINGERPRINT.key(), default=None,
1549+
help="only allow connecting to servers with a matching SSL certificate SHA256 fingerprint. " +
1550+
"To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.")
1551+
parser.add_argument("-1", "--oneserver", action="store_true", dest=SimpleConfig.NETWORK_ONESERVER.key(), default=None, help="connect to one server only")
1552+
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)")
1553+
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")
1554+
parser.add_argument("--noonion", action="store_true", dest=SimpleConfig.NETWORK_NOONION.key(), default=None, help="do not try to connect to onion servers")
1555+
parser.add_argument("--skipmerklecheck", action="store_true", dest=SimpleConfig.NETWORK_SKIPMERKLECHECK.key(), default=None, help="Tolerate invalid merkle proofs from server")
15551556

15561557
def add_global_options(parser):
15571558
group = parser.add_argument_group('global options')
@@ -1563,13 +1564,13 @@ def add_global_options(parser):
15631564
group.add_argument("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest")
15641565
group.add_argument("--simnet", action="store_true", dest="simnet", default=False, help="Use Simnet")
15651566
group.add_argument("--signet", action="store_true", dest="signet", default=False, help="Use Signet")
1566-
group.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
1567-
group.add_argument("--rpcuser", dest="rpcuser", default=argparse.SUPPRESS, help="RPC user")
1568-
group.add_argument("--rpcpassword", dest="rpcpassword", default=argparse.SUPPRESS, help="RPC password")
1567+
group.add_argument("-o", "--offline", action="store_true", dest=SimpleConfig.NETWORK_OFFLINE.key(), default=None, help="Run offline")
1568+
group.add_argument("--rpcuser", dest=SimpleConfig.RPC_USERNAME.key(), default=argparse.SUPPRESS, help="RPC user")
1569+
group.add_argument("--rpcpassword", dest=SimpleConfig.RPC_PASSWORD.key(), default=argparse.SUPPRESS, help="RPC password")
15691570

15701571
def add_wallet_option(parser):
15711572
parser.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
1572-
parser.add_argument("--forgetconfig", action="store_true", dest="forget_config", default=False, help="Forget config on exit")
1573+
parser.add_argument("--forgetconfig", action="store_true", dest=SimpleConfig.CONFIG_FORGET_CHANGES.key(), default=False, help="Forget config on exit")
15731574

15741575
def get_parser():
15751576
# create main parser
@@ -1582,11 +1583,11 @@ def get_parser():
15821583
# gui
15831584
parser_gui = subparsers.add_parser('gui', description="Run Electrum's Graphical User Interface.", help="Run GUI (default)")
15841585
parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)")
1585-
parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'kivy', 'text', 'stdio', 'qml'])
1586-
parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup")
1587-
parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI")
1586+
parser_gui.add_argument("-g", "--gui", dest=SimpleConfig.GUI_NAME.key(), help="select graphical user interface", choices=['qt', 'kivy', 'text', 'stdio', 'qml'])
1587+
parser_gui.add_argument("-m", action="store_true", dest=SimpleConfig.GUI_QT_HIDE_ON_STARTUP.key(), default=False, help="hide GUI on startup")
1588+
parser_gui.add_argument("-L", "--lang", dest=SimpleConfig.LOCALIZATION_LANGUAGE.key(), default=None, help="default language used in GUI")
15881589
parser_gui.add_argument("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed")
1589-
parser_gui.add_argument("--nosegwit", action="store_true", dest="nosegwit", default=False, help="Do not create segwit wallets")
1590+
parser_gui.add_argument("--nosegwit", action="store_true", dest=SimpleConfig.WIZARD_DONT_CREATE_SEGWIT.key(), default=False, help="Do not create segwit wallets")
15901591
add_wallet_option(parser_gui)
15911592
add_network_options(parser_gui)
15921593
add_global_options(parser_gui)
@@ -1595,10 +1596,10 @@ def get_parser():
15951596
parser_daemon.add_argument("-d", "--detached", action="store_true", dest="detach", default=False, help="run daemon in detached mode")
15961597
# FIXME: all these options are rpc-server-side. The CLI client-side cannot use e.g. --rpcport,
15971598
# instead it reads it from the daemon lockfile.
1598-
parser_daemon.add_argument("--rpchost", dest="rpchost", default=argparse.SUPPRESS, help="RPC host")
1599-
parser_daemon.add_argument("--rpcport", dest="rpcport", type=int, default=argparse.SUPPRESS, help="RPC port")
1600-
parser_daemon.add_argument("--rpcsock", dest="rpcsock", default=None, help="what socket type to which to bind RPC daemon", choices=['unix', 'tcp', 'auto'])
1601-
parser_daemon.add_argument("--rpcsockpath", dest="rpcsockpath", help="where to place RPC file socket")
1599+
parser_daemon.add_argument("--rpchost", dest=SimpleConfig.RPC_HOST.key(), default=argparse.SUPPRESS, help="RPC host")
1600+
parser_daemon.add_argument("--rpcport", dest=SimpleConfig.RPC_PORT.key(), type=int, default=argparse.SUPPRESS, help="RPC port")
1601+
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'])
1602+
parser_daemon.add_argument("--rpcsockpath", dest=SimpleConfig.RPC_SOCKET_FILEPATH.key(), help="where to place RPC file socket")
16021603
add_network_options(parser_daemon)
16031604
add_global_options(parser_daemon)
16041605
# commands

electrum/contacts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def resolve(self, k):
9898

9999
def fetch_openalias(self, config):
100100
self.alias_info = None
101-
alias = config.get('alias')
101+
alias = config.OPENALIAS_ID
102102
if alias:
103103
alias = str(alias)
104104
def f():

electrum/daemon.py

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def get_rpcsock_defaultpath(config: SimpleConfig):
6969
return os.path.join(config.path, 'daemon_rpc_socket')
7070

7171
def get_rpcsock_default_type(config: SimpleConfig):
72-
if config.get('rpcport'):
72+
if config.RPC_PORT:
7373
return 'tcp'
7474
# Use unix domain sockets when available,
7575
# with the extra paranoia that in case windows "implements" them,
@@ -106,7 +106,7 @@ def get_file_descriptor(config: SimpleConfig):
106106

107107

108108

109-
def request(config: SimpleConfig, endpoint, args=(), timeout=60):
109+
def request(config: SimpleConfig, endpoint, args=(), timeout: Union[float, int] = 60):
110110
lockfile = get_lockfile(config)
111111
while True:
112112
create_time = None
@@ -152,12 +152,8 @@ async def request_coroutine(
152152

153153

154154
def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
155-
rpc_user = config.get('rpcuser', None)
156-
rpc_password = config.get('rpcpassword', None)
157-
if rpc_user == '':
158-
rpc_user = None
159-
if rpc_password == '':
160-
rpc_password = None
155+
rpc_user = config.RPC_USERNAME or None
156+
rpc_password = config.RPC_PASSWORD or None
161157
if rpc_user is None or rpc_password is None:
162158
rpc_user = 'user'
163159
bits = 128
@@ -166,8 +162,8 @@ def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
166162
pw_b64 = b64encode(
167163
pw_int.to_bytes(nbytes, 'big'), b'-_')
168164
rpc_password = to_string(pw_b64, 'ascii')
169-
config.set_key('rpcuser', rpc_user)
170-
config.set_key('rpcpassword', rpc_password, save=True)
165+
config.RPC_USERNAME = rpc_user
166+
config.RPC_PASSWORD = rpc_password
171167
return rpc_user, rpc_password
172168

173169

@@ -252,17 +248,17 @@ async def handle(self, request):
252248

253249
class CommandsServer(AuthenticatedServer):
254250

255-
def __init__(self, daemon, fd):
251+
def __init__(self, daemon: 'Daemon', fd):
256252
rpc_user, rpc_password = get_rpc_credentials(daemon.config)
257253
AuthenticatedServer.__init__(self, rpc_user, rpc_password)
258254
self.daemon = daemon
259255
self.fd = fd
260256
self.config = daemon.config
261-
sockettype = self.config.get('rpcsock', 'auto')
257+
sockettype = self.config.RPC_SOCKET_TYPE
262258
self.socktype = sockettype if sockettype != 'auto' else get_rpcsock_default_type(self.config)
263-
self.sockpath = self.config.get('rpcsockpath', get_rpcsock_defaultpath(self.config))
264-
self.host = self.config.get('rpchost', '127.0.0.1')
265-
self.port = self.config.get('rpcport', 0)
259+
self.sockpath = self.config.RPC_SOCKET_FILEPATH or get_rpcsock_defaultpath(self.config)
260+
self.host = self.config.RPC_HOST
261+
self.port = self.config.RPC_PORT
266262
self.app = web.Application()
267263
self.app.router.add_post("/", self.handle)
268264
self.register_method(self.ping)
@@ -348,12 +344,12 @@ async def run_cmdline(self, config_options):
348344

349345
class WatchTowerServer(AuthenticatedServer):
350346

351-
def __init__(self, network, netaddress):
347+
def __init__(self, network: 'Network', netaddress):
352348
self.addr = netaddress
353349
self.config = network.config
354350
self.network = network
355-
watchtower_user = self.config.get('watchtower_user', '')
356-
watchtower_password = self.config.get('watchtower_password', '')
351+
watchtower_user = self.config.WATCHTOWER_SERVER_USER or ""
352+
watchtower_password = self.config.WATCHTOWER_SERVER_PASSWORD or ""
357353
AuthenticatedServer.__init__(self, watchtower_user, watchtower_password)
358354
self.lnwatcher = network.local_watchtower
359355
self.app = web.Application()
@@ -403,7 +399,7 @@ def __init__(
403399
self.logger.warning("Ignoring parameter 'wallet_path' for daemon. "
404400
"Use the load_wallet command instead.")
405401
self.asyncio_loop = util.get_asyncio_loop()
406-
if not config.get('offline'):
402+
if not self.config.NETWORK_OFFLINE:
407403
self.network = Network(config, daemon=self)
408404
self.fx = FxThread(config=config)
409405
# path -> wallet; make sure path is standardized.
@@ -444,16 +440,16 @@ async def _run(self, jobs: Iterable = None):
444440

445441
def start_network(self):
446442
self.logger.info(f"starting network.")
447-
assert not self.config.get('offline')
443+
assert not self.config.NETWORK_OFFLINE
448444
assert self.network
449445
# server-side watchtower
450-
if watchtower_address := self.config.get_netaddress('watchtower_address'):
446+
if watchtower_address := self.config.get_netaddress(self.config.cv.WATCHTOWER_SERVER_ADDRESS):
451447
self.watchtower = WatchTowerServer(self.network, watchtower_address)
452448
asyncio.run_coroutine_threadsafe(self.taskgroup.spawn(self.watchtower.run), self.asyncio_loop)
453449

454450
self.network.start(jobs=[self.fx.run])
455451
# prepare lightning functionality, also load channel db early
456-
if self.config.get('use_gossip', False):
452+
if self.config.LIGHTNING_USE_GOSSIP:
457453
self.network.start_gossip()
458454

459455
def with_wallet_lock(func):
@@ -582,7 +578,7 @@ async def stop(self):
582578

583579
def run_gui(self, config: 'SimpleConfig', plugins: 'Plugins'):
584580
threading.current_thread().name = 'GUI'
585-
gui_name = config.get('gui', 'qt')
581+
gui_name = config.GUI_NAME
586582
if gui_name in ['lite', 'classic']:
587583
gui_name = 'qt'
588584
self.logger.info(f'launching GUI: {gui_name}')

0 commit comments

Comments
 (0)