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

spesmilo / electrum / 6601523842514944

26 Jun 2025 02:22PM UTC coverage: 59.829% (-0.001%) from 59.83%
6601523842514944

Pull #9983

CirrusCI

f321x
android: build BarcodeScannerView from src

Adds a script `make_barcode_scanner.sh` which builds the
`BarcodeScannerView` library and its dependencies, `zxing-cpp` and
`CameraView` from source. Builds `zxing-cpp` architecture dependent
reducing the final apk size.
Pull Request #9983: android: replace qr code scanning library

21943 of 36676 relevant lines covered (59.83%)

2.99 hits per line

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

83.05
/electrum/simple_config.py
1
import json
5✔
2
import threading
5✔
3
import os
5✔
4
import stat
5✔
5
from typing import Union, Optional, Dict, Sequence, Any, Set, Callable, AbstractSet, Type
5✔
6
from functools import cached_property
5✔
7

8
from copy import deepcopy
5✔
9

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

21

22
_logger = get_logger(__name__)
5✔
23

24

25
FINAL_CONFIG_VERSION = 3
5✔
26

27

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

30

31
class ConfigVar(property):
5✔
32

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

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

86
    def _set_config_value(self, config: 'SimpleConfig', value, *, save=True):
5✔
87
        if self._type is not None and value is not None:
5✔
88
            if not isinstance(value, self._type):
5✔
89
                raise ValueError(
×
90
                    f"ConfigVar.set type-check failed. "
91
                    f"key={self._key!r}. type={self._type}. value={value!r}")
92
        config.set_key(self._key, value, save=save)
5✔
93

94
    def key(self) -> str:
5✔
95
        return self._key
5✔
96

97
    def get_default_value(self) -> Any:
5✔
98
        return self._default
5✔
99

100
    def get_short_desc(self) -> Optional[str]:
5✔
101
        desc = self._short_desc
×
102
        return desc() if desc else None
×
103

104
    def get_long_desc(self) -> Optional[str]:
5✔
105
        desc = self._long_desc
×
106
        return desc() if desc else None
×
107

108
    def __repr__(self):
5✔
109
        return f"<ConfigVar key={self._key!r}>"
×
110

111
    def __deepcopy__(self, memo):
5✔
112
        # We can be considered ~stateless. State is stored in the config, which is external.
113
        return self
5✔
114

115

116
class ConfigVarWithConfig:
5✔
117

118
    def __init__(self, *, config: 'SimpleConfig', config_var: 'ConfigVar'):
5✔
119
        self._config = config
5✔
120
        self._config_var = config_var
5✔
121

122
    def get(self) -> Any:
5✔
123
        return self._config_var._get_config_value(self._config)
5✔
124

125
    def set(self, value: Any, *, save=True) -> None:
5✔
126
        self._config_var._set_config_value(self._config, value, save=save)
5✔
127

128
    def key(self) -> str:
5✔
129
        return self._config_var.key()
5✔
130

131
    def get_default_value(self) -> Any:
5✔
132
        return self._config_var.get_default_value()
5✔
133

134
    def get_short_desc(self) -> Optional[str]:
5✔
135
        return self._config_var.get_short_desc()
×
136

137
    def get_long_desc(self) -> Optional[str]:
5✔
138
        return self._config_var.get_long_desc()
×
139

140
    def is_modifiable(self) -> bool:
5✔
141
        return self._config.is_modifiable(self._config_var)
5✔
142

143
    def is_set(self) -> bool:
5✔
144
        return self._config.is_set(self._config_var)
5✔
145

146
    def __repr__(self):
5✔
147
        return f"<ConfigVarWithConfig key={self.key()!r}>"
×
148

149
    def __eq__(self, other) -> bool:
5✔
150
        if not isinstance(other, ConfigVarWithConfig):
5✔
151
            return False
×
152
        return self._config is other._config and self._config_var is other._config_var
5✔
153

154

155
class SimpleConfig(Logger):
5✔
156
    """
157
    The SimpleConfig class is responsible for handling operations involving
158
    configuration files.
159

160
    There are two different sources of possible configuration values:
161
        1. Command line options.
162
        2. User configuration (in the user's config directory)
163
    They are taken in order (1. overrides config options set in 2.)
164
    """
165

166
    def __init__(self, options=None, read_user_config_function=None,
5✔
167
                 read_user_dir_function=None):
168
        if options is None:
5✔
169
            options = {}
5✔
170
        for config_key in options:
5✔
171
            assert isinstance(config_key, str), f"{config_key=!r} has type={type(config_key)}, expected str"
5✔
172

173
        Logger.__init__(self)
5✔
174

175
        # This lock needs to be acquired for updating and reading the config in
176
        # a thread-safe way.
177
        self.lock = threading.RLock()
5✔
178

179
        # The following two functions are there for dependency injection when
180
        # testing.
181
        if read_user_config_function is None:
5✔
182
            read_user_config_function = read_user_config
5✔
183
        if read_user_dir_function is None:
5✔
184
            self.user_dir = user_dir
5✔
185
        else:
186
            self.user_dir = read_user_dir_function
5✔
187

188
        # The command line options
189
        self.cmdline_options = deepcopy(options)
5✔
190
        # don't allow to be set on CLI:
191
        self.cmdline_options.pop('config_version', None)
5✔
192

193
        # Set self.path and read the user config
194
        self.user_config = {}  # for self.get in electrum_path()
5✔
195
        self.path = self.electrum_path()
5✔
196
        self.user_config = read_user_config_function(self.path)
5✔
197
        if not self.user_config:
5✔
198
            # avoid new config getting upgraded
199
            self.user_config = {'config_version': FINAL_CONFIG_VERSION}
5✔
200

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

203
        # config "upgrade" - CLI options
204
        self.rename_config_keys(
5✔
205
            self.cmdline_options, {'auto_cycle': 'auto_connect'}, True)
206

207
        # config upgrade - user config
208
        if self.requires_upgrade():
5✔
209
            self.upgrade()
5✔
210

211
        self._check_dependent_keys()
5✔
212

213
        # units and formatting
214
        # FIXME is this duplication (dp, nz, post_sat, thou_sep) due to performance reasons??
215
        self.decimal_point = self.BTC_AMOUNTS_DECIMAL_POINT
5✔
216
        try:
5✔
217
            decimal_point_to_base_unit_name(self.decimal_point)
5✔
218
        except UnknownBaseUnit:
×
219
            self.decimal_point = DECIMAL_POINT_DEFAULT
×
220
        self.num_zeros = self.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT
5✔
221
        self.amt_precision_post_satoshi = self.BTC_AMOUNTS_PREC_POST_SAT
5✔
222
        self.amt_add_thousands_sep = self.BTC_AMOUNTS_ADD_THOUSANDS_SEP
5✔
223

224
        self._init_done = True
5✔
225

226
    def list_config_vars(self) -> Sequence[str]:
5✔
227
        return list(sorted(_config_var_from_key.keys()))
×
228

229
    def electrum_path_root(self):
5✔
230
        # Read electrum_path from command line
231
        # Otherwise use the user's default data directory.
232
        path = self.get('electrum_path') or self.user_dir()
5✔
233
        make_dir(path, allow_symlink=False)
5✔
234
        return path
5✔
235

236
    def get_selected_chain(self) -> Type[constants.AbstractNet]:
5✔
237
        selected_chains = [
5✔
238
            chain for chain in constants.NETS_LIST
239
            if self.get(chain.config_key())]
240
        if selected_chains:
5✔
241
            # note: if multiple are selected, we just pick one deterministically random
242
            return selected_chains[0]
×
243
        return constants.BitcoinMainnet
5✔
244

245
    def electrum_path(self):
5✔
246
        path = self.electrum_path_root()
5✔
247
        chain = self.get_selected_chain()
5✔
248
        if subdir := chain.datadir_subdir():
5✔
249
            path = os.path.join(path, subdir)
×
250
            make_dir(path, allow_symlink=False)
×
251

252
        self.logger.info(f"electrum directory {path} (chain={chain.NET_NAME})")
5✔
253
        return path
5✔
254

255
    def rename_config_keys(self, config, keypairs, deprecation_warning=False):
5✔
256
        """Migrate old key names to new ones"""
257
        updated = False
5✔
258
        for old_key, new_key in keypairs.items():
5✔
259
            if old_key in config:
5✔
260
                if new_key not in config:
5✔
261
                    config[new_key] = config[old_key]
5✔
262
                    if deprecation_warning:
5✔
263
                        self.logger.warning('Note that the {} variable has been deprecated. '
×
264
                                            'You should use {} instead.'.format(old_key, new_key))
265
                del config[old_key]
5✔
266
                updated = True
5✔
267
        return updated
5✔
268

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

289
    def _set_key_in_user_config(self, key: str, value, *, save=True) -> None:
5✔
290
        assert isinstance(key, str), key
5✔
291
        with self.lock:
5✔
292
            if value is not None:
5✔
293
                keypath = key.split('.')
5✔
294
                d = self.user_config
5✔
295
                for x in keypath[0:-1]:
5✔
296
                    d2 = d.get(x)
5✔
297
                    if not isinstance(d2, dict):
5✔
298
                        d2 = d[x] = {}
5✔
299
                    d = d2
5✔
300
                d[keypath[-1]] = value
5✔
301
            else:
302
                def delete_key(d, key):
5✔
303
                    if '.' not in key:
5✔
304
                        d.pop(key, None)
5✔
305
                    else:
306
                        prefix, suffix = key.split('.', 1)
5✔
307
                        d2 = d.get(prefix)
5✔
308
                        empty = delete_key(d2, suffix)
5✔
309
                        if empty:
5✔
310
                            d.pop(prefix)
5✔
311
                    return len(d) == 0
5✔
312
                delete_key(self.user_config, key)
5✔
313
            if save:
5✔
314
                self.save_user_config()
5✔
315

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

335
    def is_set(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> bool:
5✔
336
        """Returns whether the config key has any explicit value set/defined."""
337
        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):
5✔
338
            key = key.key()
5✔
339
        assert isinstance(key, str), key
5✔
340
        return self.get(key, default=...) is not ...
5✔
341

342
    def is_plugin_enabled(self, name: str) -> bool:
5✔
343
        return bool(self.get(f'plugins.{name}.enabled'))
×
344

345
    def get_installed_plugins(self) -> AbstractSet[str]:
5✔
346
        """Returns all plugin names registered in the config."""
347
        return self.get('plugins', {}).keys()
5✔
348

349
    def enable_plugin(self, name: str):
5✔
350
        self.set_key(f'plugins.{name}.enabled', True, save=True)
×
351

352
    def disable_plugin(self, name: str):
5✔
353
        self.set_key(f'plugins.{name}.enabled', False, save=True)
×
354

355
    def _check_dependent_keys(self) -> None:
5✔
356
        if self.NETWORK_SERVERFINGERPRINT:
5✔
357
            if not self.NETWORK_SERVER:
×
358
                raise Exception(
×
359
                    f"config key {self.__class__.NETWORK_SERVERFINGERPRINT.key()!r} requires "
360
                    f"{self.__class__.NETWORK_SERVER.key()!r} to also be set")
361
            self.make_key_not_modifiable(self.__class__.NETWORK_SERVER)
×
362

363
    def requires_upgrade(self):
5✔
364
        return self.get_config_version() < FINAL_CONFIG_VERSION
5✔
365

366
    def upgrade(self):
5✔
367
        with self.lock:
5✔
368
            self.logger.info('upgrading config')
5✔
369

370
            self.convert_version_2()
5✔
371
            self.convert_version_3()
5✔
372

373
            self.set_key('config_version', FINAL_CONFIG_VERSION, save=True)
5✔
374

375
    def convert_version_2(self):
5✔
376
        if not self._is_upgrade_method_needed(1, 1):
5✔
377
            return
×
378

379
        self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'})
5✔
380

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

392
        self.set_key('config_version', 2)
5✔
393

394
    def convert_version_3(self):
5✔
395
        if not self._is_upgrade_method_needed(2, 2):
5✔
396
            return
×
397
        base_unit = self.user_config.get('base_unit')
5✔
398
        if isinstance(base_unit, str):
5✔
399
            self._set_key_in_user_config('base_unit', None)
×
400
            map_ = {'btc': 8, 'mbtc': 5, 'ubtc': 2, 'bits': 2, 'sat': 0}
×
401
            decimal_point = map_.get(base_unit.lower())
×
402
            self._set_key_in_user_config('decimal_point', decimal_point)
×
403
        self.set_key('config_version', 3)
5✔
404

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

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

423
    def is_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> bool:
5✔
424
        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):
5✔
425
            key = key.key()
5✔
426
        return (key not in self.cmdline_options
5✔
427
                and key not in self._not_modifiable_keys)
428

429
    def make_key_not_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> None:
5✔
430
        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):
5✔
431
            key = key.key()
5✔
432
        assert isinstance(key, str), key
5✔
433
        self._not_modifiable_keys.add(key)
5✔
434

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

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

461
    def maybe_complete_wallet_path(self, path: Optional[str]) -> str:
5✔
462
        return self._complete_wallet_path(path) if path is not None else self.get_wallet_path()
5✔
463

464
    def _complete_wallet_path(self, path: str) -> str:
5✔
465
        """ add user wallets directory if needed """
466
        if os.path.split(path)[0] == '':
5✔
467
            path = os.path.join(self.get_datadir_wallet_path(), path)
5✔
468
        return path
5✔
469

470
    def get_wallet_path(self) -> str:
5✔
471
        """Returns the wallet path."""
472
        # command line -w option
473
        if path:= self.get('wallet_path'):
5✔
474
            return self._complete_wallet_path(path)
×
475
        # current wallet
476
        path = self.CURRENT_WALLET
5✔
477
        if path and os.path.exists(path):
5✔
478
            return path
5✔
479
        return self.get_fallback_wallet_path()
5✔
480

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

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

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

494
    def get_session_timeout(self):
5✔
495
        return self.HWD_SESSION_TIMEOUT
5✔
496

497
    def get_video_device(self):
5✔
498
        device = self.VIDEO_DEVICE_PATH
×
499
        if device == 'default':
×
500
            device = ''
×
501
        return device
×
502

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

526
    def format_amount_and_units(self, *args, **kwargs) -> str:
5✔
527
        return self.format_amount(*args, **kwargs) + ' ' + self.get_base_unit()
×
528

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

533
    def get_base_unit(self):
5✔
534
        return decimal_point_to_base_unit_name(self.decimal_point)
×
535

536
    def set_base_unit(self, unit):
5✔
537
        assert unit in base_units.keys()
×
538
        self.decimal_point = base_unit_name_to_decimal_point(unit)
×
539
        self.BTC_AMOUNTS_DECIMAL_POINT = self.decimal_point
×
540

541
    def get_decimal_point(self):
5✔
542
        return self.decimal_point
5✔
543

544
    def get_nostr_relays(self) -> Sequence[str]:
5✔
545
        relays = []
×
546
        for url in self.NOSTR_RELAYS.split(','):
×
547
            url = url.strip()
×
548
            if url and is_valid_websocket_url(url):
×
549
                relays.append(url)
×
550
        return relays
×
551

552
    def add_nostr_relay(self, relay: str):
5✔
553
        l = self.get_nostr_relays()
×
554
        if is_valid_websocket_url(relay) and relay not in l:
×
555
            l.append(relay)
×
556
            self.NOSTR_RELAYS = ','.join(l)
×
557

558
    def remove_nostr_relay(self, relay: str):
5✔
559
        l = self.get_nostr_relays()
×
560
        if relay in l:
×
561
            l.remove(relay)
×
562
            self.NOSTR_RELAYS = ','.join(l)
×
563

564
    def __setattr__(self, name, value):
5✔
565
        """Disallows setting instance attributes outside __init__.
566

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

579
    @cached_property
5✔
580
    def cv(config):
5✔
581
        """Allows getting a reference to a config variable without dereferencing it.
582

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

609
    # config variables ----->
610
    NETWORK_AUTO_CONNECT = ConfigVar(
5✔
611
        'auto_connect', default=True, type_=bool,
612
        short_desc=lambda: _('Select server automatically'),
613
        long_desc=lambda: _("If auto-connect is enabled, Electrum will always use a server that is on the longest blockchain. "
614
                            "If it is disabled, you have to choose a server you want to use. Electrum will warn you if your server is lagging."),
615
    )
616
    NETWORK_ONESERVER = ConfigVar(
5✔
617
        'oneserver', default=False, type_=bool,
618
        short_desc=lambda: _('Only connect to one server (full trust)'),
619
        long_desc=lambda: _(
620
            "This is only intended for connecting to your own fully trusted server. "
621
            "Using this option on a public server is a security risk and is discouraged."
622
            "\n\n"
623
            "By default, Electrum tries to maintain connections to ~10 servers. "
624
            "One of these nodes gets selected to be the history server and will learn the wallet addresses. "
625
            "All the other nodes are *only* used for block header notifications. "
626
            "\n\n"
627
            "Getting block headers from multiple sources is useful to detect lagging servers, chain splits, and forks. "
628
            "Chain split detection is security-critical for determining number of confirmations."
629
        )
630
    )
631
    NETWORK_PROXY = ConfigVar('proxy', default=None, type_=str, convert_getter=lambda v: "none" if v is None else v)
5✔
632
    NETWORK_PROXY_USER = ConfigVar('proxy_user', default=None, type_=str)
5✔
633
    NETWORK_PROXY_PASSWORD = ConfigVar('proxy_password', default=None, type_=str)
5✔
634
    NETWORK_PROXY_ENABLED = ConfigVar('enable_proxy', default=lambda config: config.NETWORK_PROXY not in [None, "none"], type_=bool)
5✔
635
    NETWORK_SERVER = ConfigVar('server', default=None, type_=str)
5✔
636
    NETWORK_NOONION = ConfigVar('noonion', default=False, type_=bool)
5✔
637
    NETWORK_OFFLINE = ConfigVar('offline', default=False, type_=bool)
5✔
638
    NETWORK_SKIPMERKLECHECK = ConfigVar('skipmerklecheck', default=False, type_=bool)
5✔
639
    NETWORK_SERVERFINGERPRINT = ConfigVar('serverfingerprint', default=None, type_=str)
5✔
640
    NETWORK_MAX_INCOMING_MSG_SIZE = ConfigVar('network_max_incoming_msg_size', default=1_000_000, type_=int)  # in bytes
5✔
641
    NETWORK_TIMEOUT = ConfigVar('network_timeout', default=None, type_=int)
5✔
642
    NETWORK_BOOKMARKED_SERVERS = ConfigVar('network_bookmarked_servers', default=None)
5✔
643

644
    WALLET_MERGE_DUPLICATE_OUTPUTS = ConfigVar(
5✔
645
        'wallet_merge_duplicate_outputs', default=False, type_=bool,
646
        short_desc=lambda: _('Merge duplicate outputs'),
647
        long_desc=lambda: _('Merge transaction outputs that pay to the same address into '
648
                            'a single output that pays the sum of the original amounts.'),
649
    )
650
    WALLET_SPEND_CONFIRMED_ONLY = ConfigVar(
5✔
651
        'confirmed_only', default=False, type_=bool,
652
        short_desc=lambda: _('Spend only confirmed coins'),
653
        long_desc=lambda: _('Spend only confirmed inputs.'),
654
    )
655
    WALLET_COIN_CHOOSER_POLICY = ConfigVar('coin_chooser', default='Privacy', type_=str)
5✔
656
    WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = ConfigVar(
5✔
657
        'coin_chooser_output_rounding', default=True, type_=bool,
658
        short_desc=lambda: _('Enable output value rounding'),
659
        long_desc=lambda: (
660
            _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' +
661
            _('This might improve your privacy somewhat.') + '\n' +
662
            _('If enabled, at most 100 satoshis might be lost due to this, per transaction.')),
663
    )
664
    WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT = ConfigVar('unconf_utxo_freeze_threshold', default=5_000, type_=int)
5✔
665
    WALLET_PAYREQ_EXPIRY_SECONDS = ConfigVar('request_expiry', default=invoices.PR_DEFAULT_EXPIRATION_WHEN_CREATING, type_=int)
5✔
666
    WALLET_USE_SINGLE_PASSWORD = ConfigVar('single_password', default=False, type_=bool)
5✔
667
    # note: 'use_change' and 'multiple_change' are per-wallet settings
668
    WALLET_SEND_CHANGE_TO_LIGHTNING = ConfigVar(
5✔
669
        'send_change_to_lightning', default=False, type_=bool,
670
        short_desc=lambda: _('Send change to Lightning'),
671
        long_desc=lambda: _('If possible, send the change of this transaction to your channels, with a submarine swap'),
672
    )
673
    WALLET_FREEZE_REUSED_ADDRESS_UTXOS = ConfigVar(
5✔
674
        'wallet_freeze_reused_address_utxos', default=False, type_=bool,
675
        short_desc=lambda: _('Avoid spending from used addresses'),
676
        long_desc=lambda: _("""Automatically freeze coins received to already used addresses.
677
This can eliminate a serious privacy issue where a malicious user can track your spends by sending small payments
678
to a previously-paid address of yours that would then be included with unrelated inputs in your future payments."""),
679
    )
680

681
    FX_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool)
5✔
682
    FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str)
5✔
683
    FX_EXCHANGE = ConfigVar('use_exchange', default='CoinGecko', type_=str)  # default exchange should ideally provide historical rates
5✔
684
    FX_HISTORY_RATES = ConfigVar(
5✔
685
        'history_rates', default=False, type_=bool,
686
        short_desc=lambda: _('Download historical rates'),
687
    )
688
    FX_HISTORY_RATES_CAPITAL_GAINS = ConfigVar(
5✔
689
        'history_rates_capital_gains', default=False, type_=bool,
690
        short_desc=lambda: _('Show Capital Gains'),
691
    )
692
    FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES = ConfigVar(
5✔
693
        'fiat_address', default=False, type_=bool,
694
        short_desc=lambda: _('Show Fiat balances'),
695
    )
696

697
    LIGHTNING_LISTEN = ConfigVar('lightning_listen', default=None, type_=str)
5✔
698
    LIGHTNING_PEERS = ConfigVar('lightning_peers', default=None)
5✔
699
    LIGHTNING_USE_GOSSIP = ConfigVar(
5✔
700
        'use_gossip', default=False, type_=bool,
701
        short_desc=lambda: _("Use trampoline routing"),
702
        long_desc=lambda: _("""Lightning payments require finding a path through the Lightning Network. You may use trampoline routing, or local routing (gossip).
703

704
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."""),
705
    )
706
    LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar(
5✔
707
        'use_recoverable_channels', default=True, type_=bool,
708
        short_desc=lambda: _("Create recoverable channels"),
709
        long_desc=lambda: _("""Add extra data to your channel funding transactions, so that a static backup can be recovered from your seed.
710

711
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.
712

713
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."""),
714
    )
715
    LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int)
5✔
716
    LIGHTNING_MAX_FUNDING_SAT = ConfigVar('lightning_max_funding_sat', default=LN_MAX_FUNDING_SAT_LEGACY, type_=int)
5✔
717
    LIGHTNING_MAX_HTLC_VALUE_IN_FLIGHT_MSAT = ConfigVar('lightning_max_htlc_value_in_flight_msat', default=None, type_=int)
5✔
718
    INITIAL_TRAMPOLINE_FEE_LEVEL = ConfigVar('initial_trampoline_fee_level', default=1, type_=int)
5✔
719
    LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS = ConfigVar(
5✔
720
        'lightning_payment_fee_max_millionths', default=10_000,  # 1%
721
        type_=int,
722
        short_desc=lambda: _("Max lightning fees to pay"),
723
        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.
724

725
Warning: setting this to too low will result in lots of payment failures."""),
726
    )
727
    LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT = ConfigVar(
5✔
728
        'lightning_payment_fee_cutoff_msat', default=10_000,  # 10 sat
729
        type_=int,
730
        short_desc=lambda: _("Max lightning fees to pay for small payments"),
731
    )
732

733
    LIGHTNING_NODE_ALIAS = ConfigVar('lightning_node_alias', default='', type_=str)
5✔
734
    LIGHTNING_NODE_COLOR_RGB = ConfigVar('lightning_node_color_rgb', default='000000', type_=str)
5✔
735
    EXPERIMENTAL_LN_FORWARD_PAYMENTS = ConfigVar('lightning_forward_payments', default=False, type_=bool)
5✔
736
    EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS = ConfigVar('lightning_forward_trampoline_payments', default=False, type_=bool)
5✔
737
    TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = ConfigVar('test_fail_htlcs_with_temp_node_failure', default=False, type_=bool)
5✔
738
    TEST_FAIL_HTLCS_AS_MALFORMED = ConfigVar('test_fail_malformed_htlc', default=False, type_=bool)
5✔
739
    TEST_FORCE_MPP = ConfigVar('test_force_mpp', default=False, type_=bool)
5✔
740
    TEST_FORCE_DISABLE_MPP = ConfigVar('test_force_disable_mpp', default=False, type_=bool)
5✔
741
    TEST_SHUTDOWN_FEE = ConfigVar('test_shutdown_fee', default=None, type_=int)
5✔
742
    TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None)
5✔
743
    TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool)
5✔
744

745
    # fee_policy is a dict: fee_policy_name -> fee_policy_descriptor
746
    FEE_POLICY = ConfigVar('fee_policy.default', default='eta:2', type_=str)  # exposed to GUI
5✔
747
    FEE_POLICY_LIGHTNING = ConfigVar('fee_policy.lnwatcher', default='eta:2', type_=str)  # for txbatcher (sweeping)
5✔
748
    FEE_POLICY_SWAPS = ConfigVar('fee_policy.swaps', default='eta:2', type_=str)  # for txbatcher (sweeping and sending if we are a swapserver)
5✔
749

750
    RPC_USERNAME = ConfigVar('rpcuser', default=None, type_=str)
5✔
751
    RPC_PASSWORD = ConfigVar('rpcpassword', default=None, type_=str)
5✔
752
    RPC_HOST = ConfigVar('rpchost', default='127.0.0.1', type_=str)
5✔
753
    RPC_PORT = ConfigVar('rpcport', default=0, type_=int)
5✔
754
    RPC_SOCKET_TYPE = ConfigVar('rpcsock', default='auto', type_=str)
5✔
755
    RPC_SOCKET_FILEPATH = ConfigVar('rpcsockpath', default=None, type_=str)
5✔
756

757
    GUI_NAME = ConfigVar('gui', default='qt', type_=str)
5✔
758
    CURRENT_WALLET = ConfigVar('current_wallet', default=None, type_=str)
5✔
759

760
    GUI_QT_COLOR_THEME = ConfigVar(
5✔
761
        'qt_gui_color_theme', default='default', type_=str,
762
        short_desc=lambda: _('Color theme'),
763
    )
764
    GUI_QT_DARK_TRAY_ICON = ConfigVar('dark_icon', default=False, type_=bool)
5✔
765
    GUI_QT_WINDOW_IS_MAXIMIZED = ConfigVar('is_maximized', default=False, type_=bool)
5✔
766
    GUI_QT_HIDE_ON_STARTUP = ConfigVar('hide_gui', default=False, type_=bool)
5✔
767
    GUI_QT_HISTORY_TAB_SHOW_TOOLBAR = ConfigVar('show_toolbar_history', default=False, type_=bool)
5✔
768
    GUI_QT_ADDRESSES_TAB_SHOW_TOOLBAR = ConfigVar('show_toolbar_addresses', default=False, type_=bool)
5✔
769
    GUI_QT_TX_DIALOG_FETCH_TXIN_DATA = ConfigVar(
5✔
770
        'tx_dialog_fetch_txin_data', default=False, type_=bool,
771
        short_desc=lambda: _('Download missing data'),
772
        long_desc=lambda: _(
773
            'Download parent transactions from the network.\n'
774
            'Allows filling in missing fee and input details.'),
775
    )
776
    GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA = ConfigVar(
5✔
777
        'gui_qt_tx_dialog_export_strip_sensitive_metadata', default=False, type_=bool,
778
        short_desc=lambda: _('For CoinJoin; strip privates'),
779
    )
780
    GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS = ConfigVar(
5✔
781
        'gui_qt_tx_dialog_export_include_global_xpubs', default=False, type_=bool,
782
        short_desc=lambda: _('For hardware device; include xpubs'),
783
    )
784
    GUI_QT_RECEIVE_TAB_QR_VISIBLE = ConfigVar('receive_qr_visible', default=False, type_=bool)
5✔
785
    GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar(
5✔
786
        'show_tx_io', default=False, type_=bool,
787
        short_desc=lambda: _('Show inputs and outputs'),
788
    )
789
    GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS = ConfigVar(
5✔
790
        'show_tx_fee_details', default=False, type_=bool,
791
        short_desc=lambda: _('Edit fees manually'),
792
    )
793
    GUI_QT_TX_EDITOR_SHOW_LOCKTIME = ConfigVar(
5✔
794
        'show_tx_locktime', default=False, type_=bool,
795
        short_desc=lambda: _('Edit Locktime'),
796
    )
797
    GUI_QT_SHOW_TAB_ADDRESSES = ConfigVar('show_addresses_tab', default=False, type_=bool)
5✔
798
    GUI_QT_SHOW_TAB_CHANNELS = ConfigVar('show_channels_tab', default=False, type_=bool)
5✔
799
    GUI_QT_SHOW_TAB_UTXO = ConfigVar('show_utxo_tab', default=False, type_=bool)
5✔
800
    GUI_QT_SHOW_TAB_CONTACTS = ConfigVar('show_contacts_tab', default=False, type_=bool)
5✔
801
    GUI_QT_SHOW_TAB_CONSOLE = ConfigVar('show_console_tab', default=False, type_=bool)
5✔
802
    GUI_QT_SHOW_TAB_NOTES = ConfigVar('show_notes_tab', default=False, type_=bool)
5✔
803
    GUI_QT_SCREENSHOT_PROTECTION = ConfigVar(
5✔
804
        'screenshot_protection', default=True, type_=bool,
805
        short_desc=lambda: _("Prevent screenshots"),
806
        # currently this option is Windows only, so the description can be specific to Windows
807
        long_desc=lambda: _(
808
            'Signals Windows to disallow recordings and screenshots of the application window. '
809
            'There is no guarantee Windows will respect this signal.'),
810
    )
811

812
    GUI_QML_PREFERRED_REQUEST_TYPE = ConfigVar('preferred_request_type', default='bolt11', type_=str)
5✔
813
    GUI_QML_USER_KNOWS_PRESS_AND_HOLD = ConfigVar('user_knows_press_and_hold', default=False, type_=bool)
5✔
814
    GUI_QML_ADDRESS_LIST_SHOW_TYPE = ConfigVar('address_list_show_type', default=1, type_=int)
5✔
815
    GUI_QML_ADDRESS_LIST_SHOW_USED = ConfigVar('address_list_show_used', default=False, type_=bool)
5✔
816
    GUI_QML_ALWAYS_ALLOW_SCREENSHOTS = ConfigVar('android_always_allow_screenshots', default=False, type_=bool)
5✔
817
    GUI_QML_SET_MAX_BRIGHTNESS_ON_QR_DISPLAY = ConfigVar('android_set_max_brightness_on_qr_display', default=True, type_=bool)
5✔
818

819
    BTC_AMOUNTS_DECIMAL_POINT = ConfigVar('decimal_point', default=DECIMAL_POINT_DEFAULT, type_=int)
5✔
820
    BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT = ConfigVar(
5✔
821
        'num_zeros', default=0, type_=int,
822
        short_desc=lambda: _('Zeros after decimal point'),
823
        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"'),
824
    )
825
    BTC_AMOUNTS_PREC_POST_SAT = ConfigVar(
5✔
826
        'amt_precision_post_satoshi', default=0, type_=int,
827
        short_desc=lambda: _("Show Lightning amounts with msat precision"),
828
    )
829
    BTC_AMOUNTS_ADD_THOUSANDS_SEP = ConfigVar(
5✔
830
        'amt_add_thousands_sep', default=False, type_=bool,
831
        short_desc=lambda: _("Add thousand separators to bitcoin amounts"),
832
    )
833

834
    BLOCK_EXPLORER = ConfigVar(
5✔
835
        'block_explorer', default='Blockstream.info', type_=str,
836
        short_desc=lambda: _('Online Block Explorer'),
837
        long_desc=lambda: _('Choose which online block explorer to use for functions that open a web browser'),
838
    )
839
    BLOCK_EXPLORER_CUSTOM = ConfigVar('block_explorer_custom', default=None)
5✔
840
    VIDEO_DEVICE_PATH = ConfigVar(
5✔
841
        'video_device', default='default', type_=str,
842
        short_desc=lambda: _('Video Device'),
843
        long_desc=lambda: (_("For scanning QR codes.") + "\n" +
844
                           _("Install the zbar package to enable this.")),
845
    )
846
    OPENALIAS_ID = ConfigVar(
5✔
847
        'alias', default="", type_=str,
848
        short_desc=lambda: 'OpenAlias',
849
        long_desc=lambda: (
850
            _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n' +
851
            _('The following alias providers are available:') + '\n' +
852
            '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n' +
853
            'For more information, see https://openalias.org'),
854
    )
855
    HWD_SESSION_TIMEOUT = ConfigVar('session_timeout', default=300, type_=int)
5✔
856
    CLI_TIMEOUT = ConfigVar('timeout', default=60, type_=float)
5✔
857
    AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = ConfigVar(
5✔
858
        'check_updates', default=False, type_=bool,
859
        short_desc=lambda: _("Automatically check for software updates"),
860
    )
861
    WRITE_LOGS_TO_DISK = ConfigVar(
5✔
862
        'log_to_file', default=False, type_=bool,
863
        short_desc=lambda: _("Write logs to file"),
864
        long_desc=lambda: _('Debug logs can be persisted to disk. These are useful for troubleshooting.'),
865
    )
866
    LOGS_NUM_FILES_KEEP = ConfigVar('logs_num_files_keep', default=10, type_=int)
5✔
867
    GUI_ENABLE_DEBUG_LOGS = ConfigVar('gui_enable_debug_logs', default=False, type_=bool)
5✔
868
    LOCALIZATION_LANGUAGE = ConfigVar(
5✔
869
        'language', default="", type_=str,
870
        short_desc=lambda: _("Language"),
871
        long_desc=lambda: _("Select which language is used in the GUI (after restart)."),
872
    )
873
    BLOCKCHAIN_PREFERRED_BLOCK = ConfigVar('blockchain_preferred_block', default=None)
5✔
874
    SHOW_CRASH_REPORTER = ConfigVar('show_crash_reporter', default=True, type_=bool)
5✔
875
    DONT_SHOW_TESTNET_WARNING = ConfigVar('dont_show_testnet_warning', default=False, type_=bool)
5✔
876
    RECENTLY_OPEN_WALLET_FILES = ConfigVar('recently_open', default=None)
5✔
877
    IO_DIRECTORY = ConfigVar('io_dir', default=os.path.expanduser('~'), type_=str)
5✔
878
    WALLET_BACKUP_DIRECTORY = ConfigVar('backup_dir', default=None, type_=str)
5✔
879
    CONFIG_PIN_CODE = ConfigVar('pin_code', default=None, type_=str)
5✔
880
    QR_READER_FLIP_X = ConfigVar('qrreader_flip_x', default=True, type_=bool)
5✔
881
    WIZARD_DONT_CREATE_SEGWIT = ConfigVar('nosegwit', default=False, type_=bool)
5✔
882
    CONFIG_FORGET_CHANGES = ConfigVar('forget_config', default=False, type_=bool)
5✔
883
    TERMS_OF_USE_ACCEPTED = ConfigVar('terms_of_use_accepted', default=0, type_=int)
5✔
884

885
    # connect to remote submarine swap server
886
    SWAPSERVER_URL = ConfigVar('swapserver_url', default='', type_=str)
5✔
887
    TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool)
5✔
888
    SWAPSERVER_NPUB = ConfigVar('swapserver_npub', default=None, type_=str)
5✔
889
    SWAPSERVER_POW_TARGET = ConfigVar('swapserver_pow_target', default=30, type_=int)
5✔
890

891
    # nostr
892
    NOSTR_RELAYS = ConfigVar(
5✔
893
        'nostr_relays',
894
        default='wss://relay.getalby.com/v1,wss://nos.lol,wss://relay.damus.io,wss://brb.io,'
895
                'wss://relay.primal.net,wss://ftp.halifax.rwth-aachen.de/nostr,'
896
                'wss://eu.purplerelay.com,wss://nostr.einundzwanzig.space,wss://nostr.mom',
897
        type_=str,
898
        short_desc=lambda: _("Nostr relays"),
899
        long_desc=lambda: ' '.join([
900
            _('Nostr relays are used to send and receive submarine swap offers.'),
901
            _('These relays are also used for some plugins, e.g. Nostr Wallet Connect or Nostr Cosigner'),
902
        ]),
903
    )
904

905
    # anchor outputs channels
906
    ENABLE_ANCHOR_CHANNELS = ConfigVar('enable_anchor_channels', default=True, type_=bool)
5✔
907
    # zeroconf channels
908
    ACCEPT_ZEROCONF_CHANNELS = ConfigVar('accept_zeroconf_channels', default=False, type_=bool)
5✔
909
    ZEROCONF_TRUSTED_NODE = ConfigVar('zeroconf_trusted_node', default='', type_=str)
5✔
910
    ZEROCONF_MIN_OPENING_FEE = ConfigVar('zeroconf_min_opening_fee', default=5000, type_=int)
5✔
911
    LN_UTXO_RESERVE = ConfigVar(
5✔
912
        'ln_utxo_reserve',
913
        default=10000,
914
        type_=int,
915
        short_desc=lambda: _("Amount that must be kept on-chain in order to sweep anchor output channels"),
916
        long_desc=lambda: _("Do not set this below dust limit"),
917
    )
918

919
    # connect to remote WT
920
    WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str)
5✔
921

922
    PLUGIN_TRUSTEDCOIN_NUM_PREPAY = ConfigVar('trustedcoin_prepay', default=20, type_=int)
5✔
923

924

925
def read_user_config(path: Optional[str]) -> Dict[str, Any]:
5✔
926
    """Parse and store the user config settings in electrum.conf into user_config[]."""
927
    if not path:
5✔
928
        return {}
5✔
929
    config_path = os.path.join(path, "config")
5✔
930
    if not os.path.exists(config_path):
5✔
931
        return {}
5✔
932
    try:
5✔
933
        with open(config_path, "r", encoding='utf-8') as f:
5✔
934
            data = f.read()
5✔
935
        result = json.loads(data)
5✔
936
        assert isinstance(result, dict), "config file is not a dict"
5✔
937
    except Exception as e:
5✔
938
        raise ValueError(f"Invalid config file at {config_path}: {str(e)}")
5✔
939
    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