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

spesmilo / electrum / 5735552722403328

16 May 2025 10:28AM UTC coverage: 59.722% (+0.002%) from 59.72%
5735552722403328

Pull #9833

CirrusCI

f321x
make lightning dns seed fetching async
Pull Request #9833: dns: use async dnspython interface

22 of 50 new or added lines in 7 files covered. (44.0%)

1107 existing lines in 11 files now uncovered.

21549 of 36082 relevant lines covered (59.72%)

2.39 hits per line

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

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

11
from copy import deepcopy
4✔
12

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

23

24

25

26

27

28
_logger = get_logger(__name__)
4✔
29

30

31
FINAL_CONFIG_VERSION = 3
4✔
32

33

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

36

37
class ConfigVar(property):
4✔
38

39
    def __init__(
4✔
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
4✔
51
        self._default = default
4✔
52
        self._type = type_
4✔
53
        self._convert_getter = convert_getter
4✔
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)
4✔
57
        assert long_desc is None or callable(long_desc)
4✔
58
        self._short_desc = short_desc
4✔
59
        self._long_desc = long_desc
4✔
60
        if plugin:  # enforce "key" starts with 'plugins.<name of plugin>.'
4✔
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
×
65
            key_prefix = f'plugins.{plugin}.'
×
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)
4✔
68
        assert key not in _config_var_from_key, f"duplicate config key str: {key!r}"
4✔
69
        _config_var_from_key[key] = self
4✔
70

71
    def _get_config_value(self, config: 'SimpleConfig'):
4✔
72
        with config.lock:
4✔
73
            if config.is_set(self._key):
4✔
74
                value = config.get(self._key)
4✔
75
                # run converter
76
                if self._convert_getter is not None:
4✔
77
                    value = self._convert_getter(value)
4✔
78
                # type-check
79
                if self._type is not None:
4✔
80
                    assert value is not None, f"got None for key={self._key!r}"
4✔
81
                    try:
4✔
82
                        value = self._type(value)
4✔
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
4✔
89
                value = d(config) if callable(d) else d
4✔
90
            return value
4✔
91

92
    def _set_config_value(self, config: 'SimpleConfig', value, *, save=True):
4✔
93
        if self._type is not None and value is not None:
4✔
94
            if not isinstance(value, self._type):
4✔
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)
4✔
99

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

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

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

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

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

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

121

122
class ConfigVarWithConfig:
4✔
123

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

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

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

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

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

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

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

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

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

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

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

160

161
class SimpleConfig(Logger):
4✔
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,
4✔
173
                 read_user_dir_function=None):
174
        if options is None:
4✔
175
            options = {}
4✔
176
        for config_key in options:
4✔
177
            assert isinstance(config_key, str), f"{config_key=!r} has type={type(config_key)}, expected str"
4✔
178

179
        Logger.__init__(self)
4✔
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()
4✔
184

185

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

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

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

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

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

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

218
        self._check_dependent_keys()
4✔
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
4✔
223
        try:
4✔
224
            decimal_point_to_base_unit_name(self.decimal_point)
4✔
225
        except UnknownBaseUnit:
×
226
            self.decimal_point = DECIMAL_POINT_DEFAULT
×
227
        self.num_zeros = self.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT
4✔
228
        self.amt_precision_post_satoshi = self.BTC_AMOUNTS_PREC_POST_SAT
4✔
229
        self.amt_add_thousands_sep = self.BTC_AMOUNTS_ADD_THOUSANDS_SEP
4✔
230

231
        self._init_done = True
4✔
232

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

236
    def electrum_path_root(self):
4✔
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()
4✔
240
        make_dir(path, allow_symlink=False)
4✔
241
        return path
4✔
242

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

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

264
    def rename_config_keys(self, config, keypairs, deprecation_warning=False):
4✔
265
        """Migrate old key names to new ones"""
266
        updated = False
4✔
267
        for old_key, new_key in keypairs.items():
4✔
268
            if old_key in config:
4✔
269
                if new_key not in config:
4✔
270
                    config[new_key] = config[old_key]
4✔
271
                    if deprecation_warning:
4✔
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]
4✔
275
                updated = True
4✔
276
        return updated
4✔
277

278
    def set_key(self, key: Union[str, ConfigVar, ConfigVarWithConfig], value, *, save=True) -> None:
4✔
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)):
4✔
285
            key = key.key()
4✔
286
        assert isinstance(key, str), key
4✔
287
        if not self.is_modifiable(key):
4✔
288
            self.logger.warning(f"not changing config key '{key}' set on the command line")
4✔
289
            return
4✔
290
        try:
4✔
291
            json.dumps(key)
4✔
292
            json.dumps(value)
4✔
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)
4✔
297

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

325
    def get(self, key: str, default=None) -> Any:
4✔
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
4✔
332
        with self.lock:
4✔
333
            out = self.cmdline_options.get(key)
4✔
334
            if out is None:
4✔
335
                d = self.user_config
4✔
336
                path = key.split('.')
4✔
337
                for key in path[0:-1]:
4✔
338
                    d = d.get(key, {})
4✔
339
                out = d.get(path[-1], default)
4✔
340
        return out
4✔
341

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

349
    def is_plugin_enabled(self, name: str) -> bool:
4✔
350
        return bool(self.get(f'plugins.{name}.enabled'))
×
351

352
    def get_installed_plugins(self) -> AbstractSet[str]:
4✔
353
        """Returns all plugin names registered in the config."""
354
        return self.get('plugins', {}).keys()
4✔
355

356
    def enable_plugin(self, name: str):
4✔
357
        self.set_key(f'plugins.{name}.enabled', True, save=True)
×
358

359
    def disable_plugin(self, name: str):
4✔
360
        self.set_key(f'plugins.{name}.enabled', False, save=True)
×
361

362
    def _check_dependent_keys(self) -> None:
4✔
363
        if self.NETWORK_SERVERFINGERPRINT:
4✔
364
            if not self.NETWORK_SERVER:
×
365
                raise Exception(
×
366
                    f"config key {self.__class__.NETWORK_SERVERFINGERPRINT.key()!r} requires "
367
                    f"{self.__class__.NETWORK_SERVER.key()!r} to also be set")
368
            self.make_key_not_modifiable(self.__class__.NETWORK_SERVER)
×
369

370
    def requires_upgrade(self):
4✔
371
        return self.get_config_version() < FINAL_CONFIG_VERSION
4✔
372

373
    def upgrade(self):
4✔
374
        with self.lock:
4✔
375
            self.logger.info('upgrading config')
4✔
376

377
            self.convert_version_2()
4✔
378
            self.convert_version_3()
4✔
379

380
            self.set_key('config_version', FINAL_CONFIG_VERSION, save=True)
4✔
381

382
    def convert_version_2(self):
4✔
383
        if not self._is_upgrade_method_needed(1, 1):
4✔
384
            return
×
385

386
        self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'})
4✔
387

388
        try:
4✔
389
            # change server string FROM host:port:proto TO host:port:s
390
            server_str = self.user_config.get('server')
4✔
391
            host, port, protocol = str(server_str).rsplit(':', 2)
4✔
392
            assert protocol in ('s', 't')
×
393
            int(port)  # Throw if cannot be converted to int
×
394
            server_str = '{}:{}:s'.format(host, port)
×
395
            self._set_key_in_user_config('server', server_str)
×
396
        except BaseException:
4✔
397
            self._set_key_in_user_config('server', None)
4✔
398

399
        self.set_key('config_version', 2)
4✔
400

401
    def convert_version_3(self):
4✔
402
        if not self._is_upgrade_method_needed(2, 2):
4✔
403
            return
×
404

405
        base_unit = self.user_config.get('base_unit')
4✔
406
        if isinstance(base_unit, str):
4✔
407
            self._set_key_in_user_config('base_unit', None)
×
408
            map_ = {'btc':8, 'mbtc':5, 'ubtc':2, 'bits':2, 'sat':0}
×
409
            decimal_point = map_.get(base_unit.lower())
×
410
            self._set_key_in_user_config('decimal_point', decimal_point)
×
411

412
        self.set_key('config_version', 3)
4✔
413

414
    def _is_upgrade_method_needed(self, min_version, max_version):
4✔
415
        cur_version = self.get_config_version()
4✔
416
        if cur_version > max_version:
4✔
417
            return False
×
418
        elif cur_version < min_version:
4✔
419
            raise Exception(
×
420
                ('config upgrade: unexpected version %d (should be %d-%d)'
421
                 % (cur_version, min_version, max_version)))
422
        else:
423
            return True
4✔
424

425
    def get_config_version(self):
4✔
426
        config_version = self.get('config_version', 1)
4✔
427
        if config_version > FINAL_CONFIG_VERSION:
4✔
428
            self.logger.warning('config version ({}) is higher than latest ({})'
×
429
                                .format(config_version, FINAL_CONFIG_VERSION))
430
        return config_version
4✔
431

432
    def is_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> bool:
4✔
433
        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):
4✔
434
            key = key.key()
4✔
435
        return (key not in self.cmdline_options
4✔
436
                and key not in self._not_modifiable_keys)
437

438
    def make_key_not_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> None:
4✔
439
        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):
4✔
440
            key = key.key()
4✔
441
        assert isinstance(key, str), key
4✔
442
        self._not_modifiable_keys.add(key)
4✔
443

444
    def save_user_config(self):
4✔
445
        if self.CONFIG_FORGET_CHANGES:
4✔
446
            return
×
447
        if not self.path:
4✔
448
            return
×
449
        path = os.path.join(self.path, "config")
4✔
450
        s = json.dumps(self.user_config, indent=4, sort_keys=True)
4✔
451
        try:
4✔
452
            with open(path, "w", encoding='utf-8') as f:
4✔
453
                os_chmod(path, stat.S_IREAD | stat.S_IWRITE)  # set restrictive perms *before* we write data
4✔
454
                f.write(s)
4✔
455
        except OSError:
×
456
            # datadir probably deleted while running... e.g. portable exe running on ejected USB drive
457
            # (in which case it is typically either FileNotFoundError or PermissionError,
458
            #  but let's just catch the more generic OSError and test explicitly)
459
            if os.path.exists(self.path):  # or maybe not?
×
460
                raise
×
461

462
    def get_backup_dir(self) -> Optional[str]:
4✔
463
        # this is used to save wallet file backups (without active lightning channels)
464
        # on Android, the export backup button uses android_backup_dir()
465
        if 'ANDROID_DATA' in os.environ:
×
466
            return None
×
467
        else:
468
            return self.WALLET_BACKUP_DIRECTORY
×
469

470
    def get_wallet_path(self, *, use_gui_last_wallet=False):
4✔
471
        """Set the path of the wallet."""
472

473
        # command line -w option
474
        if self.get('wallet_path'):
4✔
475
            return os.path.join(self.get('cwd', ''), self.get('wallet_path'))
×
476

477
        if use_gui_last_wallet:
4✔
478
            path = self.GUI_LAST_WALLET
×
479
            if path and os.path.exists(path):
×
480
                return path
×
481

482
        new_path = self.get_fallback_wallet_path()
4✔
483

484
        # TODO: this can be removed by now
485
        # default path in pre 1.9 versions
486
        old_path = os.path.join(self.path, "electrum.dat")
4✔
487
        if os.path.exists(old_path) and not os.path.exists(new_path):
4✔
488
            os.rename(old_path, new_path)
×
489

490
        return new_path
4✔
491

492
    def get_datadir_wallet_path(self):
4✔
493
        util.assert_datadir_available(self.path)
4✔
494
        dirpath = os.path.join(self.path, "wallets")
4✔
495
        make_dir(dirpath, allow_symlink=False)
4✔
496
        return dirpath
4✔
497

498
    def get_fallback_wallet_path(self):
4✔
499
        return os.path.join(self.get_datadir_wallet_path(), "default_wallet")
4✔
500

501
    def set_session_timeout(self, seconds):
4✔
502
        self.logger.info(f"session timeout -> {seconds} seconds")
×
503
        self.HWD_SESSION_TIMEOUT = seconds
×
504

505
    def get_session_timeout(self):
4✔
506
        return self.HWD_SESSION_TIMEOUT
4✔
507

508
    def save_last_wallet(self, wallet):
4✔
509
        if self.get('wallet_path') is None:
×
510
            path = wallet.storage.path
×
511
            self.GUI_LAST_WALLET = path
×
512

513
    def get_video_device(self):
4✔
514
        device = self.VIDEO_DEVICE_PATH
×
515
        if device == 'default':
×
516
            device = ''
×
517
        return device
×
518

519
    def format_amount(
4✔
520
        self,
521
        amount_sat,
522
        *,
523
        is_diff=False,
524
        whitespaces=False,
525
        precision=None,
526
        add_thousands_sep: bool = None,
527
    ) -> str:
528
        if precision is None:
×
529
            precision = self.amt_precision_post_satoshi
×
530
        if add_thousands_sep is None:
×
531
            add_thousands_sep = self.amt_add_thousands_sep
×
532
        return format_satoshis(
×
533
            amount_sat,
534
            num_zeros=self.num_zeros,
535
            decimal_point=self.decimal_point,
536
            is_diff=is_diff,
537
            whitespaces=whitespaces,
538
            precision=precision,
539
            add_thousands_sep=add_thousands_sep,
540
        )
541

542
    def format_amount_and_units(self, *args, **kwargs) -> str:
4✔
543
        return self.format_amount(*args, **kwargs) + ' ' + self.get_base_unit()
×
544

545
    def format_fee_rate(self, fee_rate) -> str:
4✔
546
        """fee_rate is in sat/kvByte."""
547
        return format_fee_satoshis(fee_rate/1000, num_zeros=self.num_zeros) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE}"
×
548

549
    def get_base_unit(self):
4✔
550
        return decimal_point_to_base_unit_name(self.decimal_point)
×
551

552
    def set_base_unit(self, unit):
4✔
553
        assert unit in base_units.keys()
×
554
        self.decimal_point = base_unit_name_to_decimal_point(unit)
×
555
        self.BTC_AMOUNTS_DECIMAL_POINT = self.decimal_point
×
556

557
    def get_decimal_point(self):
4✔
558
        return self.decimal_point
4✔
559

560
    def __setattr__(self, name, value):
4✔
561
        """Disallows setting instance attributes outside __init__.
562

563
        The point is to make the following code raise:
564
        >>> config.NETORK_AUTO_CONNECTT = False
565
        (i.e. catch mistyped or non-existent ConfigVars)
566
        """
567
        # If __init__ not finished yet, or this field already exists, set it:
568
        if not getattr(self, "_init_done", False) or hasattr(self, name):
4✔
569
            return super().__setattr__(name, value)
4✔
570
        raise AttributeError(
4✔
571
            f"Tried to define new instance attribute for config: {name=!r}. "
572
            "Did you perhaps mistype a ConfigVar?"
573
        )
574

575
    @cached_property
4✔
576
    def cv(config):
4✔
577
        """Allows getting a reference to a config variable without dereferencing it.
578

579
        Compare:
580
        >>> config.NETWORK_SERVER
581
        'testnet.hsmiths.com:53012:s'
582
        >>> config.cv.NETWORK_SERVER
583
        <ConfigVarWithConfig key='server'>
584
        """
585
        class CVLookupHelper:
4✔
586
            def __getattribute__(self, name: str) -> ConfigVarWithConfig:
4✔
587
                if name in ("from_key", ):  # don't apply magic, just use standard lookup
4✔
588
                    return super().__getattribute__(name)
4✔
589
                config_var = config.__class__.__getattribute__(type(config), name)
4✔
590
                if not isinstance(config_var, ConfigVar):
4✔
591
                    raise AttributeError()
×
592
                return ConfigVarWithConfig(config=config, config_var=config_var)
4✔
593
            def from_key(self, key: str) -> ConfigVarWithConfig:
4✔
594
                try:
4✔
595
                    config_var = _config_var_from_key[key]
4✔
596
                except KeyError:
4✔
597
                    raise KeyError(f"No ConfigVar with key={key!r}") from None
4✔
598
                return ConfigVarWithConfig(config=config, config_var=config_var)
4✔
599
            def __setattr__(self, name, value):
4✔
600
                raise Exception(
×
601
                    f"Cannot assign value to config.cv.{name} directly. "
602
                    f"Either use config.cv.{name}.set() or assign to config.{name} instead.")
603
        return CVLookupHelper()
4✔
604

605
    # config variables ----->
606
    NETWORK_AUTO_CONNECT = ConfigVar('auto_connect', default=True, type_=bool)
4✔
607
    NETWORK_ONESERVER = ConfigVar('oneserver', default=False, type_=bool)
4✔
608
    NETWORK_PROXY = ConfigVar('proxy', default=None, type_=str, convert_getter=lambda v: "none" if v is None else v)
4✔
609
    NETWORK_PROXY_USER = ConfigVar('proxy_user', default=None, type_=str)
4✔
610
    NETWORK_PROXY_PASSWORD = ConfigVar('proxy_password', default=None, type_=str)
4✔
611
    NETWORK_PROXY_ENABLED = ConfigVar('enable_proxy', default=lambda config: config.NETWORK_PROXY not in [None, "none"], type_=bool)
4✔
612
    NETWORK_SERVER = ConfigVar('server', default=None, type_=str)
4✔
613
    NETWORK_NOONION = ConfigVar('noonion', default=False, type_=bool)
4✔
614
    NETWORK_OFFLINE = ConfigVar('offline', default=False, type_=bool)
4✔
615
    NETWORK_SKIPMERKLECHECK = ConfigVar('skipmerklecheck', default=False, type_=bool)
4✔
616
    NETWORK_SERVERFINGERPRINT = ConfigVar('serverfingerprint', default=None, type_=str)
4✔
617
    NETWORK_MAX_INCOMING_MSG_SIZE = ConfigVar('network_max_incoming_msg_size', default=1_000_000, type_=int)  # in bytes
4✔
618
    NETWORK_TIMEOUT = ConfigVar('network_timeout', default=None, type_=int)
4✔
619
    NETWORK_BOOKMARKED_SERVERS = ConfigVar('network_bookmarked_servers', default=None)
4✔
620

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

658
    FX_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool)
4✔
659
    FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str)
4✔
660
    FX_EXCHANGE = ConfigVar('use_exchange', default='CoinGecko', type_=str)  # default exchange should ideally provide historical rates
4✔
661
    FX_HISTORY_RATES = ConfigVar(
4✔
662
        'history_rates', default=False, type_=bool,
663
        short_desc=lambda: _('Download historical rates'),
664
    )
665
    FX_HISTORY_RATES_CAPITAL_GAINS = ConfigVar(
4✔
666
        'history_rates_capital_gains', default=False, type_=bool,
667
        short_desc=lambda: _('Show Capital Gains'),
668
    )
669
    FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES = ConfigVar(
4✔
670
        'fiat_address', default=False, type_=bool,
671
        short_desc=lambda: _('Show Fiat balances'),
672
    )
673

674
    LIGHTNING_LISTEN = ConfigVar('lightning_listen', default=None, type_=str)
4✔
675
    LIGHTNING_PEERS = ConfigVar('lightning_peers', default=None)
4✔
676
    LIGHTNING_USE_GOSSIP = ConfigVar(
4✔
677
        'use_gossip', default=False, type_=bool,
678
        short_desc=lambda: _("Use trampoline routing"),
679
        long_desc=lambda: _("""Lightning payments require finding a path through the Lightning Network. You may use trampoline routing, or local routing (gossip).
680

681
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."""),
682
    )
683
    LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar(
4✔
684
        'use_recoverable_channels', default=True, type_=bool,
685
        short_desc=lambda: _("Create recoverable channels"),
686
        long_desc=lambda: _("""Add extra data to your channel funding transactions, so that a static backup can be recovered from your seed.
687

688
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.
689

690
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."""),
691
    )
692
    LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int)
4✔
693
    LIGHTNING_MAX_FUNDING_SAT = ConfigVar('lightning_max_funding_sat', default=LN_MAX_FUNDING_SAT_LEGACY, type_=int)
4✔
694
    LIGHTNING_MAX_HTLC_VALUE_IN_FLIGHT_MSAT = ConfigVar('lightning_max_htlc_value_in_flight_msat', default=None, type_=int)
4✔
695
    INITIAL_TRAMPOLINE_FEE_LEVEL = ConfigVar('initial_trampoline_fee_level', default=1, type_=int)
4✔
696
    LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS = ConfigVar(
4✔
697
        'lightning_payment_fee_max_millionths', default=10_000,  # 1%
698
        type_=int,
699
        short_desc=lambda: _("Max lightning fees to pay"),
700
        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.
701

702
Warning: setting this to too low will result in lots of payment failures."""),
703
    )
704
    LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT = ConfigVar(
4✔
705
        'lightning_payment_fee_cutoff_msat', default=10_000,  # 10 sat
706
        type_=int,
707
        short_desc=lambda: _("Max lightning fees to pay for small payments"),
708
    )
709

710
    LIGHTNING_NODE_ALIAS = ConfigVar('lightning_node_alias', default='', type_=str)
4✔
711
    LIGHTNING_NODE_COLOR_RGB = ConfigVar('lightning_node_color_rgb', default='000000', type_=str)
4✔
712
    EXPERIMENTAL_LN_FORWARD_PAYMENTS = ConfigVar('lightning_forward_payments', default=False, type_=bool)
4✔
713
    EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS = ConfigVar('lightning_forward_trampoline_payments', default=False, type_=bool)
4✔
714
    TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = ConfigVar('test_fail_htlcs_with_temp_node_failure', default=False, type_=bool)
4✔
715
    TEST_FAIL_HTLCS_AS_MALFORMED = ConfigVar('test_fail_malformed_htlc', default=False, type_=bool)
4✔
716
    TEST_FORCE_MPP = ConfigVar('test_force_mpp', default=False, type_=bool)
4✔
717
    TEST_FORCE_DISABLE_MPP = ConfigVar('test_force_disable_mpp', default=False, type_=bool)
4✔
718
    TEST_SHUTDOWN_FEE = ConfigVar('test_shutdown_fee', default=None, type_=int)
4✔
719
    TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None)
4✔
720
    TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool)
4✔
721

722
    FEE_POLICY = ConfigVar('fee_policy', default='eta:2', type_=str) # exposed to GUI
4✔
723
    FEE_POLICY_LIGHTNING = ConfigVar('fee_policy_lightning', default='eta:2', type_=str) # for txbatcher (sweeping)
4✔
724
    FEE_POLICY_SWAPS = ConfigVar('fee_policy_swaps', default='eta:2', type_=str) # for txbatcher (sweeping and sending if we are a swapserver)
4✔
725

726
    RPC_USERNAME = ConfigVar('rpcuser', default=None, type_=str)
4✔
727
    RPC_PASSWORD = ConfigVar('rpcpassword', default=None, type_=str)
4✔
728
    RPC_HOST = ConfigVar('rpchost', default='127.0.0.1', type_=str)
4✔
729
    RPC_PORT = ConfigVar('rpcport', default=0, type_=int)
4✔
730
    RPC_SOCKET_TYPE = ConfigVar('rpcsock', default='auto', type_=str)
4✔
731
    RPC_SOCKET_FILEPATH = ConfigVar('rpcsockpath', default=None, type_=str)
4✔
732

733
    GUI_NAME = ConfigVar('gui', default='qt', type_=str)
4✔
734
    GUI_LAST_WALLET = ConfigVar('gui_last_wallet', default=None, type_=str)
4✔
735

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

780
    GUI_QML_PREFERRED_REQUEST_TYPE = ConfigVar('preferred_request_type', default='bolt11', type_=str)
4✔
781
    GUI_QML_USER_KNOWS_PRESS_AND_HOLD = ConfigVar('user_knows_press_and_hold', default=False, type_=bool)
4✔
782
    GUI_QML_ADDRESS_LIST_SHOW_TYPE = ConfigVar('address_list_show_type', default=1, type_=int)
4✔
783
    GUI_QML_ADDRESS_LIST_SHOW_USED = ConfigVar('address_list_show_used', default=False, type_=bool)
4✔
784
    GUI_QML_ALWAYS_ALLOW_SCREENSHOTS = ConfigVar('android_always_allow_screenshots', default=False, type_=bool)
4✔
785
    GUI_QML_SET_MAX_BRIGHTNESS_ON_QR_DISPLAY = ConfigVar('android_set_max_brightness_on_qr_display', default=True, type_=bool)
4✔
786

787
    BTC_AMOUNTS_DECIMAL_POINT = ConfigVar('decimal_point', default=DECIMAL_POINT_DEFAULT, type_=int)
4✔
788
    BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT = ConfigVar(
4✔
789
        'num_zeros', default=0, type_=int,
790
        short_desc=lambda: _('Zeros after decimal point'),
791
        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"'),
792
    )
793
    BTC_AMOUNTS_PREC_POST_SAT = ConfigVar(
4✔
794
        'amt_precision_post_satoshi', default=0, type_=int,
795
        short_desc=lambda: _("Show Lightning amounts with msat precision"),
796
    )
797
    BTC_AMOUNTS_ADD_THOUSANDS_SEP = ConfigVar(
4✔
798
        'amt_add_thousands_sep', default=False, type_=bool,
799
        short_desc=lambda: _("Add thousand separators to bitcoin amounts"),
800
    )
801

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

853
    # connect to remote submarine swap server
854
    SWAPSERVER_URL = ConfigVar('swapserver_url', default='', type_=str)
4✔
855
    TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool)
4✔
856
    SWAPSERVER_NPUB = ConfigVar('swapserver_npub', default=None, type_=str)
4✔
857
    SWAPSERVER_POW_TARGET = ConfigVar('swapserver_pow_target', default=30, type_=int)
4✔
858

859
    # nostr
860
    NOSTR_RELAYS = ConfigVar(
4✔
861
        'nostr_relays',
862
        default='wss://relay.getalby.com/v1,wss://nos.lol,wss://relay.damus.io,wss://brb.io,'
863
                'wss://relay.primal.net,wss://ftp.halifax.rwth-aachen.de/nostr,'
864
                'wss://eu.purplerelay.com,wss://nostr.einundzwanzig.space,wss://nostr.mom',
865
        type_=str,
866
        short_desc=lambda: _("Nostr relays"),
867
        long_desc=lambda: ' '.join([
868
            _('Nostr relays are used to send and receive submarine swap offers.'),
869
            _('These relays are also used for some plugins, e.g. Nostr Wallet Connect or Nostr Cosigner'),
870
        ]),
871
    )
872

873
    # anchor outputs channels
874
    ENABLE_ANCHOR_CHANNELS = ConfigVar('enable_anchor_channels', default=True, type_=bool)
4✔
875
    # zeroconf channels
876
    ACCEPT_ZEROCONF_CHANNELS = ConfigVar('accept_zeroconf_channels', default=False, type_=bool)
4✔
877
    ZEROCONF_TRUSTED_NODE = ConfigVar('zeroconf_trusted_node', default='', type_=str)
4✔
878
    ZEROCONF_MIN_OPENING_FEE = ConfigVar('zeroconf_min_opening_fee', default=5000, type_=int)
4✔
879
    LN_UTXO_RESERVE = ConfigVar(
4✔
880
        'ln_utxo_reserve',
881
        default=10000,
882
        type_=int,
883
        short_desc=lambda: _("Amount that must be kept on-chain in order to sweep anchor output channels"),
884
        long_desc=lambda: _("Do not set this below dust limit"),
885
    )
886

887
    # connect to remote WT
888
    WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str)
4✔
889

890
    PLUGIN_TRUSTEDCOIN_NUM_PREPAY = ConfigVar('trustedcoin_prepay', default=20, type_=int)
4✔
891

892

893
def read_user_config(path: Optional[str]) -> Dict[str, Any]:
4✔
894
    """Parse and store the user config settings in electrum.conf into user_config[]."""
895
    if not path:
4✔
896
        return {}
4✔
897
    config_path = os.path.join(path, "config")
4✔
898
    if not os.path.exists(config_path):
4✔
899
        return {}
4✔
900
    try:
4✔
901
        with open(config_path, "r", encoding='utf-8') as f:
4✔
902
            data = f.read()
4✔
903
        result = json.loads(data)
4✔
904
    except Exception as exc:
4✔
905
        _logger.warning(f"Cannot read config file at {config_path}: {exc}")
4✔
906
        return {}
4✔
907
    if not type(result) is dict:
4✔
UNCOV
908
        return {}
×
909
    return result
4✔
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

© 2025 Coveralls, Inc