• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

spesmilo / electrum / 4911558783926272

11 Apr 2025 05:06PM UTC coverage: 60.306% (+0.03%) from 60.278%
4911558783926272

Pull #9729

CirrusCI

ecdsa
recursive config file

move plugin variables into sub dictionaries of user config
Pull Request #9729: recursive config file

26 of 35 new or added lines in 2 files covered. (74.29%)

92 existing lines in 6 files now uncovered.

21586 of 35794 relevant lines covered (60.31%)

3.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.11
/electrum/simple_config.py
1
import json
5✔
2
import threading
5✔
3
import time
5✔
4
import os
5✔
5
import stat
5✔
6
from decimal import Decimal
5✔
7
from typing import Union, Optional, Dict, Sequence, Tuple, Any, Set, Callable
5✔
8
from numbers import Real
5✔
9
from functools import cached_property
5✔
10

11
from copy import deepcopy
5✔
12

13
from . import util
5✔
14
from . import constants
5✔
15
from . import invoices
5✔
16
from .util import base_units, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, UnknownBaseUnit, DECIMAL_POINT_DEFAULT
5✔
17
from .util import format_satoshis, format_fee_satoshis, os_chmod
5✔
18
from .util import user_dir, make_dir
5✔
19
from .lnutil import LN_MAX_FUNDING_SAT_LEGACY
5✔
20
from .i18n import _
5✔
21
from .logging import get_logger, Logger
5✔
22

23

24

25

26

27

28
_logger = get_logger(__name__)
5✔
29

30

31
FINAL_CONFIG_VERSION = 3
5✔
32

33

34
_config_var_from_key = {}  # type: Dict[str, 'ConfigVar']
5✔
35

36

37
class ConfigVar(property):
5✔
38

39
    def __init__(
5✔
40
        self,
41
        key: str,
42
        *,
43
        default: Union[Any, Callable[['SimpleConfig'], Any]],  # typically a literal, but can also be a callable
44
        type_=None,
45
        convert_getter: Callable[[Any], Any] = None,
46
        short_desc: Callable[[], str] = None,
47
        long_desc: Callable[[], str] = None,
48
        plugin: Optional[str] = None,
49
    ):
50
        self._key = key
5✔
51
        self._default = default
5✔
52
        self._type = type_
5✔
53
        self._convert_getter = convert_getter
5✔
54
        # note: the descriptions are callables instead of str literals, to delay evaluating the _() translations
55
        #       until after the language is set.
56
        assert short_desc is None or callable(short_desc)
5✔
57
        assert long_desc is None or callable(long_desc)
5✔
58
        self._short_desc = short_desc
5✔
59
        self._long_desc = long_desc
5✔
60
        if plugin:  # enforce "key" starts with 'plugins.<name of plugin>.'
5✔
61
            pkg_prefix = "electrum.plugins."  # for internal plugins
×
62
            if plugin.startswith(pkg_prefix):
×
63
                plugin = plugin[len(pkg_prefix):]
×
64
            assert "." not in plugin, plugin
×
NEW
65
            key_prefix = f'plugins.{plugin}.'
×
NEW
66
            assert key.startswith(key_prefix), f"ConfigVar {key=} must be prefixed with ({key_prefix})"
×
67
        property.__init__(self, self._get_config_value, self._set_config_value)
5✔
68
        assert key not in _config_var_from_key, f"duplicate config key str: {key!r}"
5✔
69
        _config_var_from_key[key] = self
5✔
70

71
    def _get_config_value(self, config: 'SimpleConfig'):
5✔
72
        with config.lock:
5✔
73
            if config.is_set(self._key):
5✔
74
                value = config.get(self._key)
5✔
75
                # run converter
76
                if self._convert_getter is not None:
5✔
77
                    value = self._convert_getter(value)
5✔
78
                # type-check
79
                if self._type is not None:
5✔
80
                    assert value is not None, f"got None for key={self._key!r}"
5✔
81
                    try:
5✔
82
                        value = self._type(value)
5✔
83
                    except Exception as e:
×
84
                        raise ValueError(
×
85
                            f"ConfigVar.get type-check and auto-conversion failed. "
86
                            f"key={self._key!r}. type={self._type}. value={value!r}") from e
87
            else:
88
                d = self._default
5✔
89
                value = d(config) if callable(d) else d
5✔
90
            return value
5✔
91

92
    def _set_config_value(self, config: 'SimpleConfig', value, *, save=True):
5✔
93
        if self._type is not None and value is not None:
5✔
94
            if not isinstance(value, self._type):
5✔
95
                raise ValueError(
×
96
                    f"ConfigVar.set type-check failed. "
97
                    f"key={self._key!r}. type={self._type}. value={value!r}")
98
        config.set_key(self._key, value, save=save)
5✔
99

100
    def key(self) -> str:
5✔
101
        return self._key
5✔
102

103
    def get_default_value(self) -> Any:
5✔
104
        return self._default
5✔
105

106
    def get_short_desc(self) -> Optional[str]:
5✔
107
        desc = self._short_desc
×
108
        return desc() if desc else None
×
109

110
    def get_long_desc(self) -> Optional[str]:
5✔
111
        desc = self._long_desc
×
112
        return desc() if desc else None
×
113

114
    def __repr__(self):
5✔
115
        return f"<ConfigVar key={self._key!r}>"
×
116

117
    def __deepcopy__(self, memo):
5✔
118
        # We can be considered ~stateless. State is stored in the config, which is external.
119
        return self
5✔
120

121

122
class ConfigVarWithConfig:
5✔
123

124
    def __init__(self, *, config: 'SimpleConfig', config_var: 'ConfigVar'):
5✔
125
        self._config = config
5✔
126
        self._config_var = config_var
5✔
127

128
    def get(self) -> Any:
5✔
129
        return self._config_var._get_config_value(self._config)
5✔
130

131
    def set(self, value: Any, *, save=True) -> None:
5✔
132
        self._config_var._set_config_value(self._config, value, save=save)
5✔
133

134
    def key(self) -> str:
5✔
135
        return self._config_var.key()
5✔
136

137
    def get_default_value(self) -> Any:
5✔
138
        return self._config_var.get_default_value()
5✔
139

140
    def get_short_desc(self) -> Optional[str]:
5✔
141
        return self._config_var.get_short_desc()
×
142

143
    def get_long_desc(self) -> Optional[str]:
5✔
144
        return self._config_var.get_long_desc()
×
145

146
    def is_modifiable(self) -> bool:
5✔
147
        return self._config.is_modifiable(self._config_var)
5✔
148

149
    def is_set(self) -> bool:
5✔
150
        return self._config.is_set(self._config_var)
5✔
151

152
    def __repr__(self):
5✔
153
        return f"<ConfigVarWithConfig key={self.key()!r}>"
×
154

155
    def __eq__(self, other) -> bool:
5✔
156
        if not isinstance(other, ConfigVarWithConfig):
5✔
157
            return False
×
158
        return self._config is other._config and self._config_var is other._config_var
5✔
159

160

161
class SimpleConfig(Logger):
5✔
162
    """
163
    The SimpleConfig class is responsible for handling operations involving
164
    configuration files.
165

166
    There are two different sources of possible configuration values:
167
        1. Command line options.
168
        2. User configuration (in the user's config directory)
169
    They are taken in order (1. overrides config options set in 2.)
170
    """
171

172
    def __init__(self, options=None, read_user_config_function=None,
5✔
173
                 read_user_dir_function=None):
174
        if options is None:
5✔
175
            options = {}
5✔
176
        for config_key in options:
5✔
177
            assert isinstance(config_key, str), f"{config_key=!r} has type={type(config_key)}, expected str"
5✔
178

179
        Logger.__init__(self)
5✔
180

181
        # This lock needs to be acquired for updating and reading the config in
182
        # a thread-safe way.
183
        self.lock = threading.RLock()
5✔
184

185

186
        # The following two functions are there for dependency injection when
187
        # testing.
188
        if read_user_config_function is None:
5✔
189
            read_user_config_function = read_user_config
5✔
190
        if read_user_dir_function is None:
5✔
191
            self.user_dir = user_dir
5✔
192
        else:
193
            self.user_dir = read_user_dir_function
5✔
194

195
        # The command line options
196
        self.cmdline_options = deepcopy(options)
5✔
197
        # don't allow to be set on CLI:
198
        self.cmdline_options.pop('config_version', None)
5✔
199

200
        # Set self.path and read the user config
201
        self.user_config = {}  # for self.get in electrum_path()
5✔
202
        self.path = self.electrum_path()
5✔
203
        self.user_config = read_user_config_function(self.path)
5✔
204
        if not self.user_config:
5✔
205
            # avoid new config getting upgraded
206
            self.user_config = {'config_version': FINAL_CONFIG_VERSION}
5✔
207

208
        self._not_modifiable_keys = set()  # type: Set[str]
5✔
209

210
        # config "upgrade" - CLI options
211
        self.rename_config_keys(
5✔
212
            self.cmdline_options, {'auto_cycle': 'auto_connect'}, True)
213

214
        # config upgrade - user config
215
        if self.requires_upgrade():
5✔
216
            self.upgrade()
5✔
217

218
        self._check_dependent_keys()
5✔
219

220
        # units and formatting
221
        # FIXME is this duplication (dp, nz, post_sat, thou_sep) due to performance reasons??
222
        self.decimal_point = self.BTC_AMOUNTS_DECIMAL_POINT
5✔
223
        try:
5✔
224
            decimal_point_to_base_unit_name(self.decimal_point)
5✔
225
        except UnknownBaseUnit:
×
226
            self.decimal_point = DECIMAL_POINT_DEFAULT
×
227
        self.num_zeros = self.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT
5✔
228
        self.amt_precision_post_satoshi = self.BTC_AMOUNTS_PREC_POST_SAT
5✔
229
        self.amt_add_thousands_sep = self.BTC_AMOUNTS_ADD_THOUSANDS_SEP
5✔
230

231
        self._init_done = True
5✔
232

233
    def list_config_vars(self) -> Sequence[str]:
5✔
234
        return list(sorted(_config_var_from_key.keys()))
×
235

236
    def electrum_path_root(self):
5✔
237
        # Read electrum_path from command line
238
        # Otherwise use the user's default data directory.
239
        path = self.get('electrum_path') or self.user_dir()
5✔
240
        make_dir(path, allow_symlink=False)
5✔
241
        return path
5✔
242

243
    def electrum_path(self):
5✔
244
        path = self.electrum_path_root()
5✔
245
        if self.get('testnet'):
5✔
246
            path = os.path.join(path, 'testnet')
×
247
            make_dir(path, allow_symlink=False)
×
248
        elif self.get('testnet4'):
5✔
249
            path = os.path.join(path, 'testnet4')
×
250
            make_dir(path, allow_symlink=False)
×
251
        elif self.get('regtest'):
5✔
252
            path = os.path.join(path, 'regtest')
×
253
            make_dir(path, allow_symlink=False)
×
254
        elif self.get('simnet'):
5✔
255
            path = os.path.join(path, 'simnet')
×
256
            make_dir(path, allow_symlink=False)
×
257
        elif self.get('signet'):
5✔
258
            path = os.path.join(path, 'signet')
×
259
            make_dir(path, allow_symlink=False)
×
260

261
        self.logger.info(f"electrum directory {path}")
5✔
262
        return path
5✔
263

264
    def rename_config_keys(self, config, keypairs, deprecation_warning=False):
5✔
265
        """Migrate old key names to new ones"""
266
        updated = False
5✔
267
        for old_key, new_key in keypairs.items():
5✔
268
            if old_key in config:
5✔
269
                if new_key not in config:
5✔
270
                    config[new_key] = config[old_key]
5✔
271
                    if deprecation_warning:
5✔
272
                        self.logger.warning('Note that the {} variable has been deprecated. '
×
273
                                            'You should use {} instead.'.format(old_key, new_key))
274
                del config[old_key]
5✔
275
                updated = True
5✔
276
        return updated
5✔
277

278
    def set_key(self, key: Union[str, ConfigVar, ConfigVarWithConfig], value, *, save=True) -> None:
5✔
279
        """Set the value for an arbitrary string config key.
280
        note: try to use explicit predefined ConfigVars instead of this method, whenever possible.
281
              This method side-steps ConfigVars completely, and is mainly kept for situations
282
              where the config key is dynamically constructed.
283
        """
284
        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):
5✔
285
            key = key.key()
5✔
286
        assert isinstance(key, str), key
5✔
287
        if not self.is_modifiable(key):
5✔
288
            self.logger.warning(f"not changing config key '{key}' set on the command line")
5✔
289
            return
5✔
290
        try:
5✔
291
            json.dumps(key)
5✔
292
            json.dumps(value)
5✔
293
        except Exception:
×
294
            self.logger.info(f"json error: cannot save {repr(key)} ({repr(value)})")
×
295
            return
×
296
        self._set_key_in_user_config(key, value, save=save)
5✔
297

298
    def _set_key_in_user_config(self, key: str, value, *, save=True) -> None:
5✔
299
        assert isinstance(key, str), key
5✔
300
        with self.lock:
5✔
301
            if value is not None:
5✔
302
                keypath = key.split('.')
5✔
303
                d = self.user_config
5✔
304
                for x in keypath[0:-1]:
5✔
305
                    d2 = d.get(x)
5✔
306
                    if d2 is None:
5✔
307
                        d2 = d[x] = {}
5✔
308
                    d = d2
5✔
309
                d[keypath[-1]] = value
5✔
310
            else:
311
                def delete_key(d, key):
5✔
312
                    if '.' not in key:
5✔
313
                        d.pop(key, None)
5✔
314
                    else:
315
                        prefix, suffix = key.split('.', 1)
5✔
316
                        d2 = d.get(prefix)
5✔
317
                        empty = delete_key(d2, suffix)
5✔
318
                        if empty:
5✔
319
                            d.pop(prefix)
5✔
320
                    return len(d) == 0
5✔
321
                delete_key(self.user_config, key)
5✔
322
            if save:
5✔
323
                self.save_user_config()
5✔
324

325
    def get(self, key: str, default=None) -> Any:
5✔
326
        """Get the value for an arbitrary string config key.
327
        note: try to use explicit predefined ConfigVars instead of this method, whenever possible.
328
              This method side-steps ConfigVars completely, and is mainly kept for situations
329
              where the config key is dynamically constructed.
330
        """
331
        assert isinstance(key, str), key
5✔
332
        with self.lock:
5✔
333
            out = self.cmdline_options.get(key)
5✔
334
            if out is None:
5✔
335
                d = self.user_config
5✔
336
                path = key.split('.')
5✔
337
                for key in path[0:-1]:
5✔
338
                    d = d.get(key, {})
5✔
339
                out = d.get(path[-1], default)
5✔
340
        return out
5✔
341

342
    def is_set(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> bool:
5✔
343
        """Returns whether the config key has any explicit value set/defined."""
344
        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):
5✔
345
            key = key.key()
5✔
346
        assert isinstance(key, str), key
5✔
347
        return self.get(key, default=...) is not ...
5✔
348

349
    def _check_dependent_keys(self) -> None:
5✔
350
        if self.NETWORK_SERVERFINGERPRINT:
5✔
351
            if not self.NETWORK_SERVER:
×
352
                raise Exception(
×
353
                    f"config key {self.__class__.NETWORK_SERVERFINGERPRINT.key()!r} requires "
354
                    f"{self.__class__.NETWORK_SERVER.key()!r} to also be set")
355
            self.make_key_not_modifiable(self.__class__.NETWORK_SERVER)
×
356

357
    def requires_upgrade(self):
5✔
358
        return self.get_config_version() < FINAL_CONFIG_VERSION
5✔
359

360
    def upgrade(self):
5✔
361
        with self.lock:
5✔
362
            self.logger.info('upgrading config')
5✔
363

364
            self.convert_version_2()
5✔
365
            self.convert_version_3()
5✔
366

367
            self.set_key('config_version', FINAL_CONFIG_VERSION, save=True)
5✔
368

369
    def convert_version_2(self):
5✔
370
        if not self._is_upgrade_method_needed(1, 1):
5✔
371
            return
×
372

373
        self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'})
5✔
374

375
        try:
5✔
376
            # change server string FROM host:port:proto TO host:port:s
377
            server_str = self.user_config.get('server')
5✔
378
            host, port, protocol = str(server_str).rsplit(':', 2)
5✔
379
            assert protocol in ('s', 't')
×
380
            int(port)  # Throw if cannot be converted to int
×
381
            server_str = '{}:{}:s'.format(host, port)
×
382
            self._set_key_in_user_config('server', server_str)
×
383
        except BaseException:
5✔
384
            self._set_key_in_user_config('server', None)
5✔
385

386
        self.set_key('config_version', 2)
5✔
387

388
    def convert_version_3(self):
5✔
389
        if not self._is_upgrade_method_needed(2, 2):
5✔
390
            return
×
391

392
        base_unit = self.user_config.get('base_unit')
5✔
393
        if isinstance(base_unit, str):
5✔
394
            self._set_key_in_user_config('base_unit', None)
×
395
            map_ = {'btc':8, 'mbtc':5, 'ubtc':2, 'bits':2, 'sat':0}
×
396
            decimal_point = map_.get(base_unit.lower())
×
397
            self._set_key_in_user_config('decimal_point', decimal_point)
×
398

399
        self.set_key('config_version', 3)
5✔
400

401
    def _is_upgrade_method_needed(self, min_version, max_version):
5✔
402
        cur_version = self.get_config_version()
5✔
403
        if cur_version > max_version:
5✔
404
            return False
×
405
        elif cur_version < min_version:
5✔
406
            raise Exception(
×
407
                ('config upgrade: unexpected version %d (should be %d-%d)'
408
                 % (cur_version, min_version, max_version)))
409
        else:
410
            return True
5✔
411

412
    def get_config_version(self):
5✔
413
        config_version = self.get('config_version', 1)
5✔
414
        if config_version > FINAL_CONFIG_VERSION:
5✔
415
            self.logger.warning('config version ({}) is higher than latest ({})'
×
416
                                .format(config_version, FINAL_CONFIG_VERSION))
417
        return config_version
5✔
418

419
    def is_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> bool:
5✔
420
        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):
5✔
421
            key = key.key()
5✔
422
        return (key not in self.cmdline_options
5✔
423
                and key not in self._not_modifiable_keys)
424

425
    def make_key_not_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> None:
5✔
426
        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):
5✔
427
            key = key.key()
5✔
428
        assert isinstance(key, str), key
5✔
429
        self._not_modifiable_keys.add(key)
5✔
430

431
    def save_user_config(self):
5✔
432
        if self.CONFIG_FORGET_CHANGES:
5✔
433
            return
×
434
        if not self.path:
5✔
435
            return
×
436
        path = os.path.join(self.path, "config")
5✔
437
        s = json.dumps(self.user_config, indent=4, sort_keys=True)
5✔
438
        try:
5✔
439
            with open(path, "w", encoding='utf-8') as f:
5✔
440
                os_chmod(path, stat.S_IREAD | stat.S_IWRITE)  # set restrictive perms *before* we write data
5✔
441
                f.write(s)
5✔
442
        except OSError:
×
443
            # datadir probably deleted while running... e.g. portable exe running on ejected USB drive
444
            # (in which case it is typically either FileNotFoundError or PermissionError,
445
            #  but let's just catch the more generic OSError and test explicitly)
446
            if os.path.exists(self.path):  # or maybe not?
×
447
                raise
×
448

449
    def get_backup_dir(self) -> Optional[str]:
5✔
450
        # this is used to save wallet file backups (without active lightning channels)
451
        # on Android, the export backup button uses android_backup_dir()
452
        if 'ANDROID_DATA' in os.environ:
×
453
            return None
×
454
        else:
455
            return self.WALLET_BACKUP_DIRECTORY
×
456

457
    def get_wallet_path(self, *, use_gui_last_wallet=False):
5✔
458
        """Set the path of the wallet."""
459

460
        # command line -w option
461
        if self.get('wallet_path'):
5✔
462
            return os.path.join(self.get('cwd', ''), self.get('wallet_path'))
×
463

464
        if use_gui_last_wallet:
5✔
465
            path = self.GUI_LAST_WALLET
×
466
            if path and os.path.exists(path):
×
467
                return path
×
468

469
        new_path = self.get_fallback_wallet_path()
5✔
470

471
        # TODO: this can be removed by now
472
        # default path in pre 1.9 versions
473
        old_path = os.path.join(self.path, "electrum.dat")
5✔
474
        if os.path.exists(old_path) and not os.path.exists(new_path):
5✔
475
            os.rename(old_path, new_path)
×
476

477
        return new_path
5✔
478

479
    def get_datadir_wallet_path(self):
5✔
480
        util.assert_datadir_available(self.path)
5✔
481
        dirpath = os.path.join(self.path, "wallets")
5✔
482
        make_dir(dirpath, allow_symlink=False)
5✔
483
        return dirpath
5✔
484

485
    def get_fallback_wallet_path(self):
5✔
486
        return os.path.join(self.get_datadir_wallet_path(), "default_wallet")
5✔
487

488
    def set_session_timeout(self, seconds):
5✔
489
        self.logger.info(f"session timeout -> {seconds} seconds")
×
490
        self.HWD_SESSION_TIMEOUT = seconds
×
491

492
    def get_session_timeout(self):
5✔
493
        return self.HWD_SESSION_TIMEOUT
5✔
494

495
    def save_last_wallet(self, wallet):
5✔
496
        if self.get('wallet_path') is None:
×
497
            path = wallet.storage.path
×
498
            self.GUI_LAST_WALLET = path
×
499

500
    def get_video_device(self):
5✔
501
        device = self.VIDEO_DEVICE_PATH
×
502
        if device == 'default':
×
503
            device = ''
×
504
        return device
×
505

506
    def format_amount(
5✔
507
        self,
508
        amount_sat,
509
        *,
510
        is_diff=False,
511
        whitespaces=False,
512
        precision=None,
513
        add_thousands_sep: bool = None,
514
    ) -> str:
515
        if precision is None:
×
516
            precision = self.amt_precision_post_satoshi
×
517
        if add_thousands_sep is None:
×
518
            add_thousands_sep = self.amt_add_thousands_sep
×
519
        return format_satoshis(
×
520
            amount_sat,
521
            num_zeros=self.num_zeros,
522
            decimal_point=self.decimal_point,
523
            is_diff=is_diff,
524
            whitespaces=whitespaces,
525
            precision=precision,
526
            add_thousands_sep=add_thousands_sep,
527
        )
528

529
    def format_amount_and_units(self, *args, **kwargs) -> str:
5✔
530
        return self.format_amount(*args, **kwargs) + ' ' + self.get_base_unit()
×
531

532
    def format_fee_rate(self, fee_rate) -> str:
5✔
533
        """fee_rate is in sat/kvByte."""
534
        return format_fee_satoshis(fee_rate/1000, num_zeros=self.num_zeros) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE}"
×
535

536
    def get_base_unit(self):
5✔
537
        return decimal_point_to_base_unit_name(self.decimal_point)
×
538

539
    def set_base_unit(self, unit):
5✔
540
        assert unit in base_units.keys()
×
541
        self.decimal_point = base_unit_name_to_decimal_point(unit)
×
542
        self.BTC_AMOUNTS_DECIMAL_POINT = self.decimal_point
×
543

544
    def get_decimal_point(self):
5✔
545
        return self.decimal_point
5✔
546

547
    def __setattr__(self, name, value):
5✔
548
        """Disallows setting instance attributes outside __init__.
549

550
        The point is to make the following code raise:
551
        >>> config.NETORK_AUTO_CONNECTT = False
552
        (i.e. catch mistyped or non-existent ConfigVars)
553
        """
554
        # If __init__ not finished yet, or this field already exists, set it:
555
        if not getattr(self, "_init_done", False) or hasattr(self, name):
5✔
556
            return super().__setattr__(name, value)
5✔
557
        raise AttributeError(
5✔
558
            f"Tried to define new instance attribute for config: {name=!r}. "
559
            "Did you perhaps mistype a ConfigVar?"
560
        )
561

562
    @cached_property
5✔
563
    def cv(config):
5✔
564
        """Allows getting a reference to a config variable without dereferencing it.
565

566
        Compare:
567
        >>> config.NETWORK_SERVER
568
        'testnet.hsmiths.com:53012:s'
569
        >>> config.cv.NETWORK_SERVER
570
        <ConfigVarWithConfig key='server'>
571
        """
572
        class CVLookupHelper:
5✔
573
            def __getattribute__(self, name: str) -> ConfigVarWithConfig:
5✔
574
                if name in ("from_key", ):  # don't apply magic, just use standard lookup
5✔
575
                    return super().__getattribute__(name)
5✔
576
                config_var = config.__class__.__getattribute__(type(config), name)
5✔
577
                if not isinstance(config_var, ConfigVar):
5✔
578
                    raise AttributeError()
×
579
                return ConfigVarWithConfig(config=config, config_var=config_var)
5✔
580
            def from_key(self, key: str) -> ConfigVarWithConfig:
5✔
581
                try:
5✔
582
                    config_var = _config_var_from_key[key]
5✔
583
                except KeyError:
5✔
584
                    raise KeyError(f"No ConfigVar with key={key!r}") from None
5✔
585
                return ConfigVarWithConfig(config=config, config_var=config_var)
5✔
586
            def __setattr__(self, name, value):
5✔
587
                raise Exception(
×
588
                    f"Cannot assign value to config.cv.{name} directly. "
589
                    f"Either use config.cv.{name}.set() or assign to config.{name} instead.")
590
        return CVLookupHelper()
5✔
591

592
    # config variables ----->
593
    NETWORK_AUTO_CONNECT = ConfigVar('auto_connect', default=True, type_=bool)
5✔
594
    NETWORK_ONESERVER = ConfigVar('oneserver', default=False, type_=bool)
5✔
595
    NETWORK_PROXY = ConfigVar('proxy', default=None, type_=str, convert_getter=lambda v: "none" if v is None else v)
5✔
596
    NETWORK_PROXY_USER = ConfigVar('proxy_user', default=None, type_=str)
5✔
597
    NETWORK_PROXY_PASSWORD = ConfigVar('proxy_password', default=None, type_=str)
5✔
598
    NETWORK_PROXY_ENABLED = ConfigVar('enable_proxy', default=lambda config: config.NETWORK_PROXY not in [None, "none"], type_=bool)
5✔
599
    NETWORK_SERVER = ConfigVar('server', default=None, type_=str)
5✔
600
    NETWORK_NOONION = ConfigVar('noonion', default=False, type_=bool)
5✔
601
    NETWORK_OFFLINE = ConfigVar('offline', default=False, type_=bool)
5✔
602
    NETWORK_SKIPMERKLECHECK = ConfigVar('skipmerklecheck', default=False, type_=bool)
5✔
603
    NETWORK_SERVERFINGERPRINT = ConfigVar('serverfingerprint', default=None, type_=str)
5✔
604
    NETWORK_MAX_INCOMING_MSG_SIZE = ConfigVar('network_max_incoming_msg_size', default=1_000_000, type_=int)  # in bytes
5✔
605
    NETWORK_TIMEOUT = ConfigVar('network_timeout', default=None, type_=int)
5✔
606
    NETWORK_BOOKMARKED_SERVERS = ConfigVar('network_bookmarked_servers', default=None)
5✔
607

608
    WALLET_MERGE_DUPLICATE_OUTPUTS = ConfigVar(
5✔
609
        'wallet_merge_duplicate_outputs', default=False, type_=bool,
610
        short_desc=lambda: _('Merge duplicate outputs'),
611
        long_desc=lambda: _('Merge transaction outputs that pay to the same address into '
612
                            'a single output that pays the sum of the original amounts.'),
613
    )
614
    WALLET_SPEND_CONFIRMED_ONLY = ConfigVar(
5✔
615
        'confirmed_only', default=False, type_=bool,
616
        short_desc=lambda: _('Spend only confirmed coins'),
617
        long_desc=lambda: _('Spend only confirmed inputs.'),
618
    )
619
    WALLET_COIN_CHOOSER_POLICY = ConfigVar('coin_chooser', default='Privacy', type_=str)
5✔
620
    WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = ConfigVar(
5✔
621
        'coin_chooser_output_rounding', default=True, type_=bool,
622
        short_desc=lambda: _('Enable output value rounding'),
623
        long_desc=lambda: (
624
            _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' +
625
            _('This might improve your privacy somewhat.') + '\n' +
626
            _('If enabled, at most 100 satoshis might be lost due to this, per transaction.')),
627
    )
628
    WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT = ConfigVar('unconf_utxo_freeze_threshold', default=5_000, type_=int)
5✔
629
    WALLET_PAYREQ_EXPIRY_SECONDS = ConfigVar('request_expiry', default=invoices.PR_DEFAULT_EXPIRATION_WHEN_CREATING, type_=int)
5✔
630
    WALLET_USE_SINGLE_PASSWORD = ConfigVar('single_password', default=False, type_=bool)
5✔
631
    # note: 'use_change' and 'multiple_change' are per-wallet settings
632
    WALLET_SEND_CHANGE_TO_LIGHTNING = ConfigVar(
5✔
633
        'send_change_to_lightning', default=False, type_=bool,
634
        short_desc=lambda: _('Send change to Lightning'),
635
        long_desc=lambda: _('If possible, send the change of this transaction to your channels, with a submarine swap'),
636
    )
637
    WALLET_FREEZE_REUSED_ADDRESS_UTXOS = ConfigVar(
5✔
638
        'wallet_freeze_reused_address_utxos', default=False, type_=bool,
639
        short_desc=lambda: _('Avoid spending from used addresses'),
640
        long_desc=lambda: _("""Automatically freeze coins received to already used addresses.
641
This can eliminate a serious privacy issue where a malicious user can track your spends by sending small payments
642
to a previously-paid address of yours that would then be included with unrelated inputs in your future payments."""),
643
    )
644

645
    FX_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool)
5✔
646
    FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str)
5✔
647
    FX_EXCHANGE = ConfigVar('use_exchange', default='CoinGecko', type_=str)  # default exchange should ideally provide historical rates
5✔
648
    FX_HISTORY_RATES = ConfigVar(
5✔
649
        'history_rates', default=False, type_=bool,
650
        short_desc=lambda: _('Download historical rates'),
651
    )
652
    FX_HISTORY_RATES_CAPITAL_GAINS = ConfigVar(
5✔
653
        'history_rates_capital_gains', default=False, type_=bool,
654
        short_desc=lambda: _('Show Capital Gains'),
655
    )
656
    FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES = ConfigVar(
5✔
657
        'fiat_address', default=False, type_=bool,
658
        short_desc=lambda: _('Show Fiat balances'),
659
    )
660

661
    LIGHTNING_LISTEN = ConfigVar('lightning_listen', default=None, type_=str)
5✔
662
    LIGHTNING_PEERS = ConfigVar('lightning_peers', default=None)
5✔
663
    LIGHTNING_USE_GOSSIP = ConfigVar(
5✔
664
        'use_gossip', default=False, type_=bool,
665
        short_desc=lambda: _("Use trampoline routing"),
666
        long_desc=lambda: _("""Lightning payments require finding a path through the Lightning Network. You may use trampoline routing, or local routing (gossip).
667

668
Downloading the network gossip uses quite some bandwidth and storage, and is not recommended on mobile devices. If you use trampoline, you can only open channels with trampoline nodes."""),
669
    )
670
    LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar(
5✔
671
        'use_recoverable_channels', default=True, type_=bool,
672
        short_desc=lambda: _("Create recoverable channels"),
673
        long_desc=lambda: _("""Add extra data to your channel funding transactions, so that a static backup can be recovered from your seed.
674

675
Note that static backups only allow you to request a force-close with the remote node. This assumes that the remote node is still online, did not lose its data, and accepts to force close the channel.
676

677
If this is enabled, other nodes cannot open a channel to you. Channel recovery data is encrypted, so that only your wallet can decrypt it. However, blockchain analysis will be able to tell that the transaction was probably created by Electrum."""),
678
    )
679
    LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int)
5✔
680
    LIGHTNING_MAX_FUNDING_SAT = ConfigVar('lightning_max_funding_sat', default=LN_MAX_FUNDING_SAT_LEGACY, type_=int)
5✔
681
    LIGHTNING_LEGACY_ADD_TRAMPOLINE = ConfigVar(
5✔
682
        'lightning_legacy_add_trampoline', default=False, type_=bool,
683
        short_desc=lambda: _("Add extra trampoline to legacy payments"),
684
        long_desc=lambda: _("""When paying a non-trampoline invoice, add an extra trampoline to the route, in order to improve your privacy.
685

686
This will result in longer routes; it might increase your fees and decrease the success rate of your payments."""),
687
    )
688
    INITIAL_TRAMPOLINE_FEE_LEVEL = ConfigVar('initial_trampoline_fee_level', default=1, type_=int)
5✔
689
    LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS = ConfigVar(
5✔
690
        'lightning_payment_fee_max_millionths', default=10_000,  # 1%
691
        type_=int,
692
        short_desc=lambda: _("Max lightning fees to pay"),
693
        long_desc=lambda: _("""When sending lightning payments, this value is an upper bound for the fees we allow paying, proportional to the payment amount. The fees are paid in addition to the payment amount, by the sender.
694

695
Warning: setting this to too low will result in lots of payment failures."""),
696
    )
697
    LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT = ConfigVar(
5✔
698
        'lightning_payment_fee_cutoff_msat', default=10_000,  # 10 sat
699
        type_=int,
700
        short_desc=lambda: _("Max lightning fees to pay for small payments"),
701
    )
702

703
    LIGHTNING_NODE_ALIAS = ConfigVar('lightning_node_alias', default='', type_=str)
5✔
704
    LIGHTNING_NODE_COLOR_RGB = ConfigVar('lightning_node_color_rgb', default='000000', type_=str)
5✔
705
    EXPERIMENTAL_LN_FORWARD_PAYMENTS = ConfigVar('lightning_forward_payments', default=False, type_=bool)
5✔
706
    EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS = ConfigVar('lightning_forward_trampoline_payments', default=False, type_=bool)
5✔
707
    TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = ConfigVar('test_fail_htlcs_with_temp_node_failure', default=False, type_=bool)
5✔
708
    TEST_FAIL_HTLCS_AS_MALFORMED = ConfigVar('test_fail_malformed_htlc', default=False, type_=bool)
5✔
709
    TEST_FORCE_MPP = ConfigVar('test_force_mpp', default=False, type_=bool)
5✔
710
    TEST_FORCE_DISABLE_MPP = ConfigVar('test_force_disable_mpp', default=False, type_=bool)
5✔
711
    TEST_SHUTDOWN_FEE = ConfigVar('test_shutdown_fee', default=None, type_=int)
5✔
712
    TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None)
5✔
713
    TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool)
5✔
714

715
    FEE_POLICY = ConfigVar('fee_policy', default='eta:2', type_=str) # exposed to GUI
5✔
716
    FEE_POLICY_LIGHTNING = ConfigVar('fee_policy_lightning', default='eta:2', type_=str) # for txbatcher (sweeping)
5✔
717
    FEE_POLICY_SWAPS = ConfigVar('fee_policy_swaps', default='eta:2', type_=str) # for txbatcher (sweeping and sending if we are a swapserver)
5✔
718

719
    RPC_USERNAME = ConfigVar('rpcuser', default=None, type_=str)
5✔
720
    RPC_PASSWORD = ConfigVar('rpcpassword', default=None, type_=str)
5✔
721
    RPC_HOST = ConfigVar('rpchost', default='127.0.0.1', type_=str)
5✔
722
    RPC_PORT = ConfigVar('rpcport', default=0, type_=int)
5✔
723
    RPC_SOCKET_TYPE = ConfigVar('rpcsock', default='auto', type_=str)
5✔
724
    RPC_SOCKET_FILEPATH = ConfigVar('rpcsockpath', default=None, type_=str)
5✔
725

726
    GUI_NAME = ConfigVar('gui', default='qt', type_=str)
5✔
727
    GUI_LAST_WALLET = ConfigVar('gui_last_wallet', default=None, type_=str)
5✔
728

729
    GUI_QT_COLOR_THEME = ConfigVar(
5✔
730
        'qt_gui_color_theme', default='default', type_=str,
731
        short_desc=lambda: _('Color theme'),
732
    )
733
    GUI_QT_DARK_TRAY_ICON = ConfigVar('dark_icon', default=False, type_=bool)
5✔
734
    GUI_QT_WINDOW_IS_MAXIMIZED = ConfigVar('is_maximized', default=False, type_=bool)
5✔
735
    GUI_QT_HIDE_ON_STARTUP = ConfigVar('hide_gui', default=False, type_=bool)
5✔
736
    GUI_QT_HISTORY_TAB_SHOW_TOOLBAR = ConfigVar('show_toolbar_history', default=False, type_=bool)
5✔
737
    GUI_QT_ADDRESSES_TAB_SHOW_TOOLBAR = ConfigVar('show_toolbar_addresses', default=False, type_=bool)
5✔
738
    GUI_QT_TX_DIALOG_FETCH_TXIN_DATA = ConfigVar(
5✔
739
        'tx_dialog_fetch_txin_data', default=False, type_=bool,
740
        short_desc=lambda: _('Download missing data'),
741
        long_desc=lambda: _(
742
            'Download parent transactions from the network.\n'
743
            'Allows filling in missing fee and input details.'),
744
    )
745
    GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA = ConfigVar(
5✔
746
        'gui_qt_tx_dialog_export_strip_sensitive_metadata', default=False, type_=bool,
747
        short_desc=lambda: _('For CoinJoin; strip privates'),
748
    )
749
    GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS = ConfigVar(
5✔
750
        'gui_qt_tx_dialog_export_include_global_xpubs', default=False, type_=bool,
751
        short_desc=lambda: _('For hardware device; include xpubs'),
752
    )
753
    GUI_QT_RECEIVE_TAB_QR_VISIBLE = ConfigVar('receive_qr_visible', default=False, type_=bool)
5✔
754
    GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar(
5✔
755
        'show_tx_io', default=False, type_=bool,
756
        short_desc=lambda: _('Show inputs and outputs'),
757
    )
758
    GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS = ConfigVar(
5✔
759
        'show_tx_fee_details', default=False, type_=bool,
760
        short_desc=lambda: _('Edit fees manually'),
761
    )
762
    GUI_QT_TX_EDITOR_SHOW_LOCKTIME = ConfigVar(
5✔
763
        'show_tx_locktime', default=False, type_=bool,
764
        short_desc=lambda: _('Edit Locktime'),
765
    )
766
    GUI_QT_SHOW_TAB_ADDRESSES = ConfigVar('show_addresses_tab', default=False, type_=bool)
5✔
767
    GUI_QT_SHOW_TAB_CHANNELS = ConfigVar('show_channels_tab', default=False, type_=bool)
5✔
768
    GUI_QT_SHOW_TAB_UTXO = ConfigVar('show_utxo_tab', default=False, type_=bool)
5✔
769
    GUI_QT_SHOW_TAB_CONTACTS = ConfigVar('show_contacts_tab', default=False, type_=bool)
5✔
770
    GUI_QT_SHOW_TAB_CONSOLE = ConfigVar('show_console_tab', default=False, type_=bool)
5✔
771
    GUI_QT_SHOW_TAB_NOTES = ConfigVar('show_notes_tab', default=False, type_=bool)
5✔
772

773
    GUI_QML_PREFERRED_REQUEST_TYPE = ConfigVar('preferred_request_type', default='bolt11', type_=str)
5✔
774
    GUI_QML_USER_KNOWS_PRESS_AND_HOLD = ConfigVar('user_knows_press_and_hold', default=False, type_=bool)
5✔
775
    GUI_QML_ADDRESS_LIST_SHOW_TYPE = ConfigVar('address_list_show_type', default=1, type_=int)
5✔
776
    GUI_QML_ADDRESS_LIST_SHOW_USED = ConfigVar('address_list_show_used', default=False, type_=bool)
5✔
777
    GUI_QML_ALWAYS_ALLOW_SCREENSHOTS = ConfigVar('android_always_allow_screenshots', default=False, type_=bool)
5✔
778
    GUI_QML_SET_MAX_BRIGHTNESS_ON_QR_DISPLAY = ConfigVar('android_set_max_brightness_on_qr_display', default=True, type_=bool)
5✔
779

780
    BTC_AMOUNTS_DECIMAL_POINT = ConfigVar('decimal_point', default=DECIMAL_POINT_DEFAULT, type_=int)
5✔
781
    BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT = ConfigVar(
5✔
782
        'num_zeros', default=0, type_=int,
783
        short_desc=lambda: _('Zeros after decimal point'),
784
        long_desc=lambda: _('Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"'),
785
    )
786
    BTC_AMOUNTS_PREC_POST_SAT = ConfigVar(
5✔
787
        'amt_precision_post_satoshi', default=0, type_=int,
788
        short_desc=lambda: _("Show Lightning amounts with msat precision"),
789
    )
790
    BTC_AMOUNTS_ADD_THOUSANDS_SEP = ConfigVar(
5✔
791
        'amt_add_thousands_sep', default=False, type_=bool,
792
        short_desc=lambda: _("Add thousand separators to bitcoin amounts"),
793
    )
794

795
    BLOCK_EXPLORER = ConfigVar(
5✔
796
        'block_explorer', default='Blockstream.info', type_=str,
797
        short_desc=lambda: _('Online Block Explorer'),
798
        long_desc=lambda: _('Choose which online block explorer to use for functions that open a web browser'),
799
    )
800
    BLOCK_EXPLORER_CUSTOM = ConfigVar('block_explorer_custom', default=None)
5✔
801
    VIDEO_DEVICE_PATH = ConfigVar(
5✔
802
        'video_device', default='default', type_=str,
803
        short_desc=lambda: _('Video Device'),
804
        long_desc=lambda: (_("For scanning QR codes.") + "\n" +
805
                           _("Install the zbar package to enable this.")),
806
    )
807
    OPENALIAS_ID = ConfigVar(
5✔
808
        'alias', default="", type_=str,
809
        short_desc=lambda: 'OpenAlias',
810
        long_desc=lambda: (
811
            _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n' +
812
            _('The following alias providers are available:') + '\n' +
813
            '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n' +
814
            'For more information, see https://openalias.org'),
815
    )
816
    HWD_SESSION_TIMEOUT = ConfigVar('session_timeout', default=300, type_=int)
5✔
817
    CLI_TIMEOUT = ConfigVar('timeout', default=60, type_=float)
5✔
818
    AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = ConfigVar(
5✔
819
        'check_updates', default=False, type_=bool,
820
        short_desc=lambda: _("Automatically check for software updates"),
821
    )
822
    WRITE_LOGS_TO_DISK = ConfigVar(
5✔
823
        'log_to_file', default=False, type_=bool,
824
        short_desc=lambda: _("Write logs to file"),
825
        long_desc=lambda: _('Debug logs can be persisted to disk. These are useful for troubleshooting.'),
826
    )
827
    LOGS_NUM_FILES_KEEP = ConfigVar('logs_num_files_keep', default=10, type_=int)
5✔
828
    GUI_ENABLE_DEBUG_LOGS = ConfigVar('gui_enable_debug_logs', default=False, type_=bool)
5✔
829
    LOCALIZATION_LANGUAGE = ConfigVar(
5✔
830
        'language', default="", type_=str,
831
        short_desc=lambda: _("Language"),
832
        long_desc=lambda: _("Select which language is used in the GUI (after restart)."),
833
    )
834
    BLOCKCHAIN_PREFERRED_BLOCK = ConfigVar('blockchain_preferred_block', default=None)
5✔
835
    SHOW_CRASH_REPORTER = ConfigVar('show_crash_reporter', default=True, type_=bool)
5✔
836
    DONT_SHOW_TESTNET_WARNING = ConfigVar('dont_show_testnet_warning', default=False, type_=bool)
5✔
837
    RECENTLY_OPEN_WALLET_FILES = ConfigVar('recently_open', default=None)
5✔
838
    IO_DIRECTORY = ConfigVar('io_dir', default=os.path.expanduser('~'), type_=str)
5✔
839
    WALLET_BACKUP_DIRECTORY = ConfigVar('backup_dir', default=None, type_=str)
5✔
840
    CONFIG_PIN_CODE = ConfigVar('pin_code', default=None, type_=str)
5✔
841
    QR_READER_FLIP_X = ConfigVar('qrreader_flip_x', default=True, type_=bool)
5✔
842
    WIZARD_DONT_CREATE_SEGWIT = ConfigVar('nosegwit', default=False, type_=bool)
5✔
843
    CONFIG_FORGET_CHANGES = ConfigVar('forget_config', default=False, type_=bool)
5✔
844

845
    # connect to remote submarine swap server
846
    SWAPSERVER_URL = ConfigVar('swapserver_url', default='', type_=str)
5✔
847
    TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool)
5✔
848
    SWAPSERVER_NPUB = ConfigVar('swapserver_npub', default=None, type_=str)
5✔
849
    SWAPSERVER_POW_TARGET = ConfigVar('swapserver_pow_target', default=30, type_=int)
5✔
850

851
    # nostr
852
    NOSTR_RELAYS = ConfigVar(
5✔
853
        'nostr_relays',
854
        default='wss://relay.getalby.com/v1,wss://nos.lol,wss://relay.damus.io,wss://brb.io,'
855
                'wss://relay.primal.net,wss://ftp.halifax.rwth-aachen.de/nostr,'
856
                'wss://eu.purplerelay.com,wss://nostr.einundzwanzig.space,wss://nostr.mom',
857
        type_=str,
858
        short_desc=lambda: _("Nostr relays"),
859
        long_desc=lambda: ' '.join([
860
            _('Nostr relays are used to send and receive submarine swap offers'),
861
            _('If this list is empty, Electrum will use http instead'),
862
        ]),
863
    )
864

865
    # anchor outputs channels
866
    ENABLE_ANCHOR_CHANNELS = ConfigVar('enable_anchor_channels', default=False, type_=bool)
5✔
867
    # zeroconf channels
868
    ACCEPT_ZEROCONF_CHANNELS = ConfigVar('accept_zeroconf_channels', default=False, type_=bool)
5✔
869
    ZEROCONF_TRUSTED_NODE = ConfigVar('zeroconf_trusted_node', default='', type_=str)
5✔
870
    ZEROCONF_MIN_OPENING_FEE = ConfigVar('zeroconf_min_opening_fee', default=5000, type_=int)
5✔
871
    LN_UTXO_RESERVE = ConfigVar(
5✔
872
        'ln_utxo_reserve',
873
        default=10000,
874
        type_=int,
875
        short_desc=lambda: _("Amount that must be kept on-chain in order to sweep anchor output channels"),
876
        long_desc=lambda: _("Do not set this below dust limit"),
877
    )
878

879
    # connect to remote WT
880
    WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str)
5✔
881

882
    PLUGIN_TRUSTEDCOIN_NUM_PREPAY = ConfigVar('trustedcoin_prepay', default=20, type_=int)
5✔
883

884

885
def read_user_config(path: Optional[str]) -> Dict[str, Any]:
5✔
886
    """Parse and store the user config settings in electrum.conf into user_config[]."""
887
    if not path:
5✔
888
        return {}
5✔
889
    config_path = os.path.join(path, "config")
5✔
890
    if not os.path.exists(config_path):
5✔
891
        return {}
5✔
892
    try:
5✔
893
        with open(config_path, "r", encoding='utf-8') as f:
5✔
894
            data = f.read()
5✔
895
        result = json.loads(data)
5✔
896
    except Exception as exc:
5✔
897
        _logger.warning(f"Cannot read config file at {config_path}: {exc}")
5✔
898
        return {}
5✔
899
    if not type(result) is dict:
5✔
900
        return {}
×
901
    return result
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc