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

spesmilo / electrum / 4878529344569344

04 Mar 2025 10:05AM UTC coverage: 60.716% (-0.02%) from 60.731%
4878529344569344

Pull #9587

CirrusCI

f321x
disable mpp flags in invoice creation if jit channel is required, check against available liquidity if we need a jit channel
Pull Request #9587: Disable mpp flags in invoice creation if jit channel is required and consider available liquidity

5 of 15 new or added lines in 2 files covered. (33.33%)

847 existing lines in 6 files now uncovered.

20678 of 34057 relevant lines covered (60.72%)

3.03 hits per line

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

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

11
from copy import deepcopy
5✔
12

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

23

24
FEE_ETA_TARGETS = [25, 10, 5, 2]
5✔
25
FEE_DEPTH_TARGETS = [10_000_000, 5_000_000, 2_000_000, 1_000_000,
5✔
26
                     800_000, 600_000, 400_000, 250_000, 100_000]
27
FEE_LN_ETA_TARGET = 2       # note: make sure the network is asking for estimates for this target
5✔
28
FEE_LN_LOW_ETA_TARGET = 25  # note: make sure the network is asking for estimates for this target
5✔
29

30
# satoshi per kbyte
31
FEERATE_MAX_DYNAMIC = 1500000
5✔
32
FEERATE_WARNING_HIGH_FEE = 600000
5✔
33
FEERATE_FALLBACK_STATIC_FEE = 150000
5✔
34
FEERATE_DEFAULT_RELAY = 1000
5✔
35
FEERATE_MAX_RELAY = 50000
5✔
36
FEERATE_STATIC_VALUES = [1000, 2000, 5000, 10000, 20000, 30000,
5✔
37
                         50000, 70000, 100000, 150000, 200000, 300000]
38

39
# The min feerate_per_kw that can be used in lightning so that
40
# the resulting onchain tx pays the min relay fee.
41
# This would be FEERATE_DEFAULT_RELAY / 4 if not for rounding errors,
42
# see https://github.com/ElementsProject/lightning/commit/2e687b9b352c9092b5e8bd4a688916ac50b44af0
43
FEERATE_PER_KW_MIN_RELAY_LIGHTNING = 253
5✔
44

45
FEE_RATIO_HIGH_WARNING = 0.05  # warn user if fee/amount for on-chain tx is higher than this
5✔
46

47

48

49
_logger = get_logger(__name__)
5✔
50

51

52
FINAL_CONFIG_VERSION = 3
5✔
53

54

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

57

58
class ConfigVar(property):
5✔
59

60
    def __init__(
5✔
61
        self,
62
        key: str,
63
        *,
64
        default: Union[Any, Callable[['SimpleConfig'], Any]],  # typically a literal, but can also be a callable
65
        type_=None,
66
        convert_getter: Callable[[Any], Any] = None,
67
        short_desc: Callable[[], str] = None,
68
        long_desc: Callable[[], str] = None,
69
    ):
70
        self._key = key
5✔
71
        self._default = default
5✔
72
        self._type = type_
5✔
73
        self._convert_getter = convert_getter
5✔
74
        # note: the descriptions are callables instead of str literals, to delay evaluating the _() translations
75
        #       until after the language is set.
76
        assert short_desc is None or callable(short_desc)
5✔
77
        assert long_desc is None or callable(long_desc)
5✔
78
        self._short_desc = short_desc
5✔
79
        self._long_desc = long_desc
5✔
80
        property.__init__(self, self._get_config_value, self._set_config_value)
5✔
81
        assert key not in _config_var_from_key, f"duplicate config key str: {key!r}"
5✔
82
        _config_var_from_key[key] = self
5✔
83

84
    def _get_config_value(self, config: 'SimpleConfig'):
5✔
85
        with config.lock:
5✔
86
            if config.is_set(self._key):
5✔
87
                value = config.get(self._key)
5✔
88
                # run converter
89
                if self._convert_getter is not None:
5✔
90
                    value = self._convert_getter(value)
5✔
91
                # type-check
92
                if self._type is not None:
5✔
93
                    assert value is not None, f"got None for key={self._key!r}"
5✔
94
                    try:
5✔
95
                        value = self._type(value)
5✔
96
                    except Exception as e:
×
97
                        raise ValueError(
×
98
                            f"ConfigVar.get type-check and auto-conversion failed. "
99
                            f"key={self._key!r}. type={self._type}. value={value!r}") from e
100
            else:
101
                d = self._default
5✔
102
                value = d(config) if callable(d) else d
5✔
103
            return value
5✔
104

105
    def _set_config_value(self, config: 'SimpleConfig', value, *, save=True):
5✔
106
        if self._type is not None and value is not None:
5✔
107
            if not isinstance(value, self._type):
5✔
108
                raise ValueError(
×
109
                    f"ConfigVar.set type-check failed. "
110
                    f"key={self._key!r}. type={self._type}. value={value!r}")
111
        config.set_key(self._key, value, save=save)
5✔
112

113
    def key(self) -> str:
5✔
114
        return self._key
5✔
115

116
    def get_default_value(self) -> Any:
5✔
117
        return self._default
5✔
118

119
    def get_short_desc(self) -> Optional[str]:
5✔
120
        desc = self._short_desc
×
121
        return desc() if desc else None
×
122

123
    def get_long_desc(self) -> Optional[str]:
5✔
124
        desc = self._long_desc
×
125
        return desc() if desc else None
×
126

127
    def __repr__(self):
5✔
128
        return f"<ConfigVar key={self._key!r}>"
×
129

130
    def __deepcopy__(self, memo):
5✔
131
        # We can be considered ~stateless. State is stored in the config, which is external.
132
        return self
5✔
133

134

135
class ConfigVarWithConfig:
5✔
136

137
    def __init__(self, *, config: 'SimpleConfig', config_var: 'ConfigVar'):
5✔
138
        self._config = config
5✔
139
        self._config_var = config_var
5✔
140

141
    def get(self) -> Any:
5✔
142
        return self._config_var._get_config_value(self._config)
5✔
143

144
    def set(self, value: Any, *, save=True) -> None:
5✔
145
        self._config_var._set_config_value(self._config, value, save=save)
5✔
146

147
    def key(self) -> str:
5✔
148
        return self._config_var.key()
5✔
149

150
    def get_default_value(self) -> Any:
5✔
151
        return self._config_var.get_default_value()
5✔
152

153
    def get_short_desc(self) -> Optional[str]:
5✔
154
        return self._config_var.get_short_desc()
×
155

156
    def get_long_desc(self) -> Optional[str]:
5✔
157
        return self._config_var.get_long_desc()
×
158

159
    def is_modifiable(self) -> bool:
5✔
160
        return self._config.is_modifiable(self._config_var)
5✔
161

162
    def is_set(self) -> bool:
5✔
163
        return self._config.is_set(self._config_var)
5✔
164

165
    def __repr__(self):
5✔
166
        return f"<ConfigVarWithConfig key={self.key()!r}>"
×
167

168
    def __eq__(self, other) -> bool:
5✔
169
        if not isinstance(other, ConfigVarWithConfig):
5✔
170
            return False
×
171
        return self._config is other._config and self._config_var is other._config_var
5✔
172

173

174
class SimpleConfig(Logger):
5✔
175
    """
176
    The SimpleConfig class is responsible for handling operations involving
177
    configuration files.
178

179
    There are two different sources of possible configuration values:
180
        1. Command line options.
181
        2. User configuration (in the user's config directory)
182
    They are taken in order (1. overrides config options set in 2.)
183
    """
184

185
    def __init__(self, options=None, read_user_config_function=None,
5✔
186
                 read_user_dir_function=None):
187
        if options is None:
5✔
188
            options = {}
5✔
189
        for config_key in options:
5✔
190
            assert isinstance(config_key, str), f"{config_key=!r} has type={type(config_key)}, expected str"
5✔
191

192
        Logger.__init__(self)
5✔
193

194
        # This lock needs to be acquired for updating and reading the config in
195
        # a thread-safe way.
196
        self.lock = threading.RLock()
5✔
197

198
        self.mempool_fees = None  # type: Optional[Sequence[Tuple[Union[float, int], int]]]
5✔
199
        self.fee_estimates = {}  # type: Dict[int, int]
5✔
200
        self.last_time_fee_estimates_requested = 0  # zero ensures immediate fees
5✔
201

202
        # The following two functions are there for dependency injection when
203
        # testing.
204
        if read_user_config_function is None:
5✔
205
            read_user_config_function = read_user_config
5✔
206
        if read_user_dir_function is None:
5✔
207
            self.user_dir = user_dir
5✔
208
        else:
209
            self.user_dir = read_user_dir_function
5✔
210

211
        # The command line options
212
        self.cmdline_options = deepcopy(options)
5✔
213
        # don't allow to be set on CLI:
214
        self.cmdline_options.pop('config_version', None)
5✔
215

216
        # Set self.path and read the user config
217
        self.user_config = {}  # for self.get in electrum_path()
5✔
218
        self.path = self.electrum_path()
5✔
219
        self.user_config = read_user_config_function(self.path)
5✔
220
        if not self.user_config:
5✔
221
            # avoid new config getting upgraded
222
            self.user_config = {'config_version': FINAL_CONFIG_VERSION}
5✔
223

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

226
        # config "upgrade" - CLI options
227
        self.rename_config_keys(
5✔
228
            self.cmdline_options, {'auto_cycle': 'auto_connect'}, True)
229

230
        # config upgrade - user config
231
        if self.requires_upgrade():
5✔
232
            self.upgrade()
5✔
233

234
        self._check_dependent_keys()
5✔
235

236
        # units and formatting
237
        # FIXME is this duplication (dp, nz, post_sat, thou_sep) due to performance reasons??
238
        self.decimal_point = self.BTC_AMOUNTS_DECIMAL_POINT
5✔
239
        try:
5✔
240
            decimal_point_to_base_unit_name(self.decimal_point)
5✔
241
        except UnknownBaseUnit:
×
242
            self.decimal_point = DECIMAL_POINT_DEFAULT
×
243
        self.num_zeros = self.BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT
5✔
244
        self.amt_precision_post_satoshi = self.BTC_AMOUNTS_PREC_POST_SAT
5✔
245
        self.amt_add_thousands_sep = self.BTC_AMOUNTS_ADD_THOUSANDS_SEP
5✔
246

247
    def list_config_vars(self) -> Sequence[str]:
5✔
UNCOV
248
        return list(sorted(_config_var_from_key.keys()))
×
249

250
    def electrum_path_root(self):
5✔
251
        # Read electrum_path from command line
252
        # Otherwise use the user's default data directory.
253
        path = self.get('electrum_path') or self.user_dir()
5✔
254
        make_dir(path, allow_symlink=False)
5✔
255
        return path
5✔
256

257
    def electrum_path(self):
5✔
258
        path = self.electrum_path_root()
5✔
259
        if self.get('testnet'):
5✔
UNCOV
260
            path = os.path.join(path, 'testnet')
×
UNCOV
261
            make_dir(path, allow_symlink=False)
×
262
        elif self.get('testnet4'):
5✔
263
            path = os.path.join(path, 'testnet4')
×
UNCOV
264
            make_dir(path, allow_symlink=False)
×
265
        elif self.get('regtest'):
5✔
266
            path = os.path.join(path, 'regtest')
×
UNCOV
267
            make_dir(path, allow_symlink=False)
×
268
        elif self.get('simnet'):
5✔
269
            path = os.path.join(path, 'simnet')
×
UNCOV
270
            make_dir(path, allow_symlink=False)
×
271
        elif self.get('signet'):
5✔
272
            path = os.path.join(path, 'signet')
×
UNCOV
273
            make_dir(path, allow_symlink=False)
×
274

275
        self.logger.info(f"electrum directory {path}")
5✔
276
        return path
5✔
277

278
    def rename_config_keys(self, config, keypairs, deprecation_warning=False):
5✔
279
        """Migrate old key names to new ones"""
280
        updated = False
5✔
281
        for old_key, new_key in keypairs.items():
5✔
282
            if old_key in config:
5✔
283
                if new_key not in config:
5✔
284
                    config[new_key] = config[old_key]
5✔
285
                    if deprecation_warning:
5✔
UNCOV
286
                        self.logger.warning('Note that the {} variable has been deprecated. '
×
287
                                            'You should use {} instead.'.format(old_key, new_key))
288
                del config[old_key]
5✔
289
                updated = True
5✔
290
        return updated
5✔
291

292
    def set_key(self, key: Union[str, ConfigVar, ConfigVarWithConfig], value, *, save=True) -> None:
5✔
293
        """Set the value for an arbitrary string config key.
294
        note: try to use explicit predefined ConfigVars instead of this method, whenever possible.
295
              This method side-steps ConfigVars completely, and is mainly kept for situations
296
              where the config key is dynamically constructed.
297
        """
298
        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):
5✔
299
            key = key.key()
5✔
300
        assert isinstance(key, str), key
5✔
301
        if not self.is_modifiable(key):
5✔
302
            self.logger.warning(f"not changing config key '{key}' set on the command line")
5✔
303
            return
5✔
304
        try:
5✔
305
            json.dumps(key)
5✔
306
            json.dumps(value)
5✔
UNCOV
307
        except Exception:
×
UNCOV
308
            self.logger.info(f"json error: cannot save {repr(key)} ({repr(value)})")
×
309
            return
×
310
        self._set_key_in_user_config(key, value, save=save)
5✔
311

312
    def _set_key_in_user_config(self, key: str, value, *, save=True) -> None:
5✔
313
        assert isinstance(key, str), key
5✔
314
        with self.lock:
5✔
315
            if value is not None:
5✔
316
                self.user_config[key] = value
5✔
317
            else:
318
                self.user_config.pop(key, None)
5✔
319
            if save:
5✔
320
                self.save_user_config()
5✔
321

322
    def get(self, key: str, default=None) -> Any:
5✔
323
        """Get the value for an arbitrary string config key.
324
        note: try to use explicit predefined ConfigVars instead of this method, whenever possible.
325
              This method side-steps ConfigVars completely, and is mainly kept for situations
326
              where the config key is dynamically constructed.
327
        """
328
        assert isinstance(key, str), key
5✔
329
        with self.lock:
5✔
330
            out = self.cmdline_options.get(key)
5✔
331
            if out is None:
5✔
332
                out = self.user_config.get(key, 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 _check_dependent_keys(self) -> None:
5✔
343
        if self.NETWORK_SERVERFINGERPRINT:
5✔
UNCOV
344
            if not self.NETWORK_SERVER:
×
UNCOV
345
                raise Exception(
×
346
                    f"config key {self.__class__.NETWORK_SERVERFINGERPRINT.key()!r} requires "
347
                    f"{self.__class__.NETWORK_SERVER.key()!r} to also be set")
UNCOV
348
            self.make_key_not_modifiable(self.__class__.NETWORK_SERVER)
×
349

350
    def requires_upgrade(self):
5✔
351
        return self.get_config_version() < FINAL_CONFIG_VERSION
5✔
352

353
    def upgrade(self):
5✔
354
        with self.lock:
5✔
355
            self.logger.info('upgrading config')
5✔
356

357
            self.convert_version_2()
5✔
358
            self.convert_version_3()
5✔
359

360
            self.set_key('config_version', FINAL_CONFIG_VERSION, save=True)
5✔
361

362
    def convert_version_2(self):
5✔
363
        if not self._is_upgrade_method_needed(1, 1):
5✔
UNCOV
364
            return
×
365

366
        self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'})
5✔
367

368
        try:
5✔
369
            # change server string FROM host:port:proto TO host:port:s
370
            server_str = self.user_config.get('server')
5✔
371
            host, port, protocol = str(server_str).rsplit(':', 2)
5✔
UNCOV
372
            assert protocol in ('s', 't')
×
UNCOV
373
            int(port)  # Throw if cannot be converted to int
×
374
            server_str = '{}:{}:s'.format(host, port)
×
375
            self._set_key_in_user_config('server', server_str)
×
376
        except BaseException:
5✔
377
            self._set_key_in_user_config('server', None)
5✔
378

379
        self.set_key('config_version', 2)
5✔
380

381
    def convert_version_3(self):
5✔
382
        if not self._is_upgrade_method_needed(2, 2):
5✔
UNCOV
383
            return
×
384

385
        base_unit = self.user_config.get('base_unit')
5✔
386
        if isinstance(base_unit, str):
5✔
UNCOV
387
            self._set_key_in_user_config('base_unit', None)
×
UNCOV
388
            map_ = {'btc':8, 'mbtc':5, 'ubtc':2, 'bits':2, 'sat':0}
×
389
            decimal_point = map_.get(base_unit.lower())
×
390
            self._set_key_in_user_config('decimal_point', decimal_point)
×
391

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

394
    def _is_upgrade_method_needed(self, min_version, max_version):
5✔
395
        cur_version = self.get_config_version()
5✔
396
        if cur_version > max_version:
5✔
UNCOV
397
            return False
×
398
        elif cur_version < min_version:
5✔
399
            raise Exception(
×
400
                ('config upgrade: unexpected version %d (should be %d-%d)'
401
                 % (cur_version, min_version, max_version)))
402
        else:
403
            return True
5✔
404

405
    def get_config_version(self):
5✔
406
        config_version = self.get('config_version', 1)
5✔
407
        if config_version > FINAL_CONFIG_VERSION:
5✔
UNCOV
408
            self.logger.warning('config version ({}) is higher than latest ({})'
×
409
                                .format(config_version, FINAL_CONFIG_VERSION))
410
        return config_version
5✔
411

412
    def is_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> bool:
5✔
413
        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):
5✔
414
            key = key.key()
5✔
415
        return (key not in self.cmdline_options
5✔
416
                and key not in self._not_modifiable_keys)
417

418
    def make_key_not_modifiable(self, key: Union[str, ConfigVar, ConfigVarWithConfig]) -> None:
5✔
419
        if isinstance(key, (ConfigVar, ConfigVarWithConfig)):
5✔
420
            key = key.key()
5✔
421
        assert isinstance(key, str), key
5✔
422
        self._not_modifiable_keys.add(key)
5✔
423

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

442
    def get_backup_dir(self) -> Optional[str]:
5✔
443
        # this is used to save wallet file backups (without active lightning channels)
444
        # on Android, the export backup button uses android_backup_dir()
UNCOV
445
        if 'ANDROID_DATA' in os.environ:
×
UNCOV
446
            return None
×
447
        else:
448
            return self.WALLET_BACKUP_DIRECTORY
×
449

450
    def get_wallet_path(self, *, use_gui_last_wallet=False):
5✔
451
        """Set the path of the wallet."""
452

453
        # command line -w option
454
        if self.get('wallet_path'):
5✔
UNCOV
455
            return os.path.join(self.get('cwd', ''), self.get('wallet_path'))
×
456

457
        if use_gui_last_wallet:
5✔
UNCOV
458
            path = self.GUI_LAST_WALLET
×
UNCOV
459
            if path and os.path.exists(path):
×
460
                return path
×
461

462
        new_path = self.get_fallback_wallet_path()
5✔
463

464
        # TODO: this can be removed by now
465
        # default path in pre 1.9 versions
466
        old_path = os.path.join(self.path, "electrum.dat")
5✔
467
        if os.path.exists(old_path) and not os.path.exists(new_path):
5✔
UNCOV
468
            os.rename(old_path, new_path)
×
469

470
        return new_path
5✔
471

472
    def get_datadir_wallet_path(self):
5✔
473
        util.assert_datadir_available(self.path)
5✔
474
        dirpath = os.path.join(self.path, "wallets")
5✔
475
        make_dir(dirpath, allow_symlink=False)
5✔
476
        return dirpath
5✔
477

478
    def get_fallback_wallet_path(self):
5✔
479
        return os.path.join(self.get_datadir_wallet_path(), "default_wallet")
5✔
480

481
    def set_session_timeout(self, seconds):
5✔
UNCOV
482
        self.logger.info(f"session timeout -> {seconds} seconds")
×
UNCOV
483
        self.HWD_SESSION_TIMEOUT = seconds
×
484

485
    def get_session_timeout(self):
5✔
486
        return self.HWD_SESSION_TIMEOUT
5✔
487

488
    def save_last_wallet(self, wallet):
5✔
UNCOV
489
        if self.get('wallet_path') is None:
×
UNCOV
490
            path = wallet.storage.path
×
491
            self.GUI_LAST_WALLET = path
×
492

493
    def impose_hard_limits_on_fee(func):
5✔
494
        def get_fee_within_limits(self, *args, **kwargs):
5✔
495
            fee = func(self, *args, **kwargs)
5✔
496
            if fee is None:
5✔
497
                return fee
5✔
498
            fee = min(FEERATE_MAX_DYNAMIC, fee)
5✔
499
            fee = max(FEERATE_DEFAULT_RELAY, fee)
5✔
500
            return fee
5✔
501
        return get_fee_within_limits
5✔
502

503
    def eta_to_fee(self, slider_pos) -> Optional[int]:
5✔
504
        """Returns fee in sat/kbyte."""
UNCOV
505
        slider_pos = max(slider_pos, 0)
×
UNCOV
506
        slider_pos = min(slider_pos, len(FEE_ETA_TARGETS))
×
507
        if slider_pos < len(FEE_ETA_TARGETS):
×
508
            num_blocks = FEE_ETA_TARGETS[int(slider_pos)]
×
509
            fee = self.eta_target_to_fee(num_blocks)
×
510
        else:
511
            fee = self.eta_target_to_fee(1)
×
UNCOV
512
        return fee
×
513

514
    @impose_hard_limits_on_fee
5✔
515
    def eta_target_to_fee(self, num_blocks: int) -> Optional[int]:
5✔
516
        """Returns fee in sat/kbyte."""
517
        if num_blocks == 1:
5✔
UNCOV
518
            fee = self.fee_estimates.get(2)
×
UNCOV
519
            if fee is not None:
×
520
                fee += fee / 2
×
521
                fee = int(fee)
×
522
        else:
523
            fee = self.fee_estimates.get(num_blocks)
5✔
524
            if fee is not None:
5✔
UNCOV
525
                fee = int(fee)
×
526
        return fee
5✔
527

528
    def fee_to_depth(self, target_fee: Real) -> Optional[int]:
5✔
529
        """For a given sat/vbyte fee, returns an estimate of how deep
530
        it would be in the current mempool in vbytes.
531
        Pessimistic == overestimates the depth.
532
        """
533
        if self.mempool_fees is None:
5✔
UNCOV
534
            return None
×
535
        depth = 0
5✔
536
        for fee, s in self.mempool_fees:
5✔
537
            depth += s
5✔
538
            if fee <= target_fee:
5✔
539
                break
5✔
540
        return depth
5✔
541

542
    def depth_to_fee(self, slider_pos) -> Optional[int]:
5✔
543
        """Returns fee in sat/kbyte."""
UNCOV
544
        target = self.depth_target(slider_pos)
×
UNCOV
545
        return self.depth_target_to_fee(target)
×
546

547
    @impose_hard_limits_on_fee
5✔
548
    def depth_target_to_fee(self, target: int) -> Optional[int]:
5✔
549
        """Returns fee in sat/kbyte.
550
        target: desired mempool depth in vbytes
551
        """
552
        if self.mempool_fees is None:
5✔
553
            return None
5✔
554
        depth = 0
5✔
555
        for fee, s in self.mempool_fees:
5✔
556
            depth += s
5✔
557
            if depth > target:
5✔
558
                break
5✔
559
        else:
560
            return 0
5✔
561
        # add one sat/byte as currently that is the max precision of the histogram
562
        # note: precision depends on server.
563
        #       old ElectrumX <1.16 has 1 s/b prec, >=1.16 has 0.1 s/b prec.
564
        #       electrs seems to use untruncated double-precision floating points.
565
        #       # TODO decrease this to 0.1 s/b next time we bump the required protocol version
566
        fee += 1
5✔
567
        # convert to sat/kbyte
568
        return int(fee * 1000)
5✔
569

570
    def depth_target(self, slider_pos: int) -> int:
5✔
571
        """Returns mempool depth target in bytes for a fee slider position."""
UNCOV
572
        slider_pos = max(slider_pos, 0)
×
UNCOV
573
        slider_pos = min(slider_pos, len(FEE_DEPTH_TARGETS)-1)
×
574
        return FEE_DEPTH_TARGETS[slider_pos]
×
575

576
    def eta_target(self, slider_pos: int) -> int:
5✔
577
        """Returns 'num blocks' ETA target for a fee slider position."""
UNCOV
578
        if slider_pos == len(FEE_ETA_TARGETS):
×
UNCOV
579
            return 1
×
580
        return FEE_ETA_TARGETS[slider_pos]
×
581

582
    def fee_to_eta(self, fee_per_kb: Optional[int]) -> int:
5✔
583
        """Returns 'num blocks' ETA estimate for given fee rate,
584
        or -1 for low fee.
585
        """
UNCOV
586
        import operator
×
UNCOV
587
        lst = list(self.fee_estimates.items())
×
588
        next_block_fee = self.eta_target_to_fee(1)
×
589
        if next_block_fee is not None:
×
590
            lst += [(1, next_block_fee)]
×
591
        if not lst or fee_per_kb is None:
×
592
            return -1
×
593
        dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), lst)
×
594
        min_target, min_value = min(dist, key=operator.itemgetter(1))
×
595
        if fee_per_kb < self.fee_estimates.get(FEE_ETA_TARGETS[0])/2:
×
596
            min_target = -1
×
597
        return min_target
×
598

599
    def get_depth_mb_str(self, depth: int) -> str:
5✔
600
        # e.g. 500_000 -> "0.50 MB"
UNCOV
601
        depth_mb = "{:.2f}".format(depth / 1_000_000)  # maybe .rstrip("0") ?
×
UNCOV
602
        return f"{depth_mb} {util.UI_UNIT_NAME_MEMPOOL_MB}"
×
603

604
    def depth_tooltip(self, depth: Optional[int]) -> str:
5✔
605
        """Returns text tooltip for given mempool depth (in vbytes)."""
UNCOV
606
        if depth is None:
×
UNCOV
607
            return "unknown from tip"
×
608
        depth_mb = self.get_depth_mb_str(depth)
×
609
        return _("{} from tip").format(depth_mb)
×
610

611
    def eta_tooltip(self, x):
5✔
UNCOV
612
        if x < 0:
×
UNCOV
613
            return _('Low fee')
×
614
        elif x == 1:
×
615
            return _('In the next block')
×
616
        else:
617
            return _('Within {} blocks').format(x)
×
618

619
    def get_fee_target(self):
5✔
UNCOV
620
        dyn = self.is_dynfee()
×
UNCOV
621
        mempool = self.use_mempool_fees()
×
622
        pos = self.get_depth_level() if mempool else self.get_fee_level()
×
623
        fee_rate = self.fee_per_kb()
×
624
        target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate)
×
625
        return target, tooltip, dyn
×
626

627
    def get_fee_status(self):
5✔
UNCOV
628
        target, tooltip, dyn = self.get_fee_target()
×
UNCOV
629
        return tooltip + '  [%s]'%target if dyn else target + '  [Static]'
×
630

631
    def get_fee_text(
5✔
632
            self,
633
            slider_pos: int,
634
            dyn: bool,
635
            mempool: bool,
636
            fee_per_kb: Optional[int],
637
    ):
638
        """Returns (text, tooltip) where
639
        text is what we target: static fee / num blocks to confirm in / mempool depth
640
        tooltip is the corresponding estimate (e.g. num blocks for a static fee)
641

642
        fee_rate is in sat/kbyte
643
        """
UNCOV
644
        if fee_per_kb is None:
×
UNCOV
645
            rate_str = 'unknown'
×
646
            fee_per_byte = None
×
647
        else:
648
            fee_per_byte = fee_per_kb/1000
×
UNCOV
649
            rate_str = format_fee_satoshis(fee_per_byte) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE}"
×
650

651
        if dyn:
×
UNCOV
652
            if mempool:
×
653
                depth = self.depth_target(slider_pos)
×
654
                text = self.depth_tooltip(depth)
×
655
            else:
656
                eta = self.eta_target(slider_pos)
×
UNCOV
657
                text = self.eta_tooltip(eta)
×
658
            tooltip = rate_str
×
659
        else:  # using static fees
660
            assert fee_per_kb is not None
×
UNCOV
661
            assert fee_per_byte is not None
×
662
            text = rate_str
×
663
            if mempool and self.has_fee_mempool():
×
664
                depth = self.fee_to_depth(fee_per_byte)
×
665
                tooltip = self.depth_tooltip(depth)
×
666
            elif not mempool and self.has_fee_etas():
×
667
                eta = self.fee_to_eta(fee_per_kb)
×
668
                tooltip = self.eta_tooltip(eta)
×
669
            else:
670
                tooltip = ''
×
UNCOV
671
        return text, tooltip
×
672

673
    def get_depth_level(self) -> int:
5✔
UNCOV
674
        maxp = len(FEE_DEPTH_TARGETS) - 1
×
UNCOV
675
        return min(maxp, self.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS)
×
676

677
    def get_fee_level(self) -> int:
5✔
UNCOV
678
        maxp = len(FEE_ETA_TARGETS)  # not (-1) to have "next block"
×
UNCOV
679
        return min(maxp, self.FEE_EST_DYNAMIC_ETA_SLIDERPOS)
×
680

681
    def get_fee_slider(self, dyn, mempool) -> Tuple[int, int, Optional[int]]:
5✔
UNCOV
682
        if dyn:
×
UNCOV
683
            if mempool:
×
684
                pos = self.get_depth_level()
×
685
                maxp = len(FEE_DEPTH_TARGETS) - 1
×
686
                fee_rate = self.depth_to_fee(pos)
×
687
            else:
688
                pos = self.get_fee_level()
×
UNCOV
689
                maxp = len(FEE_ETA_TARGETS)  # not (-1) to have "next block"
×
690
                fee_rate = self.eta_to_fee(pos)
×
691
        else:
692
            fee_rate = self.fee_per_kb(dyn=False)
×
UNCOV
693
            pos = self.static_fee_index(fee_rate)
×
694
            maxp = len(FEERATE_STATIC_VALUES) - 1
×
695
        return maxp, pos, fee_rate
×
696

697
    def static_fee(self, i):
5✔
UNCOV
698
        return FEERATE_STATIC_VALUES[i]
×
699

700
    def static_fee_index(self, fee_per_kb: Optional[int]) -> int:
5✔
UNCOV
701
        if fee_per_kb is None:
×
UNCOV
702
            raise TypeError('static fee cannot be None')
×
703
        dist = list(map(lambda x: abs(x - fee_per_kb), FEERATE_STATIC_VALUES))
×
704
        return min(range(len(dist)), key=dist.__getitem__)
×
705

706
    def has_fee_etas(self):
5✔
UNCOV
707
        return len(self.fee_estimates) == 4
×
708

709
    def has_fee_mempool(self) -> bool:
5✔
UNCOV
710
        return self.mempool_fees is not None
×
711

712
    def has_dynamic_fees_ready(self):
5✔
UNCOV
713
        if self.use_mempool_fees():
×
UNCOV
714
            return self.has_fee_mempool()
×
715
        else:
716
            return self.has_fee_etas()
×
717

718
    def is_dynfee(self) -> bool:
5✔
719
        return self.FEE_EST_DYNAMIC
5✔
720

721
    def use_mempool_fees(self) -> bool:
5✔
722
        return self.FEE_EST_USE_MEMPOOL
5✔
723

724
    def _feerate_from_fractional_slider_position(self, fee_level: float, dyn: bool,
5✔
725
                                                 mempool: bool) -> Union[int, None]:
UNCOV
726
        fee_level = max(fee_level, 0)
×
UNCOV
727
        fee_level = min(fee_level, 1)
×
728
        if dyn:
×
729
            max_pos = (len(FEE_DEPTH_TARGETS) - 1) if mempool else len(FEE_ETA_TARGETS)
×
730
            slider_pos = round(fee_level * max_pos)
×
731
            fee_rate = self.depth_to_fee(slider_pos) if mempool else self.eta_to_fee(slider_pos)
×
732
        else:
733
            max_pos = len(FEERATE_STATIC_VALUES) - 1
×
UNCOV
734
            slider_pos = round(fee_level * max_pos)
×
735
            fee_rate = FEERATE_STATIC_VALUES[slider_pos]
×
736
        return fee_rate
×
737

738
    def fee_per_kb(self, dyn: bool=None, mempool: bool=None, fee_level: float=None) -> Optional[int]:
5✔
739
        """Returns sat/kvB fee to pay for a txn.
740
        Note: might return None.
741

742
        fee_level: float between 0.0 and 1.0, representing fee slider position
743
        """
744
        if constants.net is constants.BitcoinRegtest:
5✔
UNCOV
745
            return self.FEE_EST_STATIC_FEERATE
×
746
        if dyn is None:
5✔
747
            dyn = self.is_dynfee()
5✔
748
        if mempool is None:
5✔
749
            mempool = self.use_mempool_fees()
5✔
750
        if fee_level is not None:
5✔
UNCOV
751
            return self._feerate_from_fractional_slider_position(fee_level, dyn, mempool)
×
752
        # there is no fee_level specified; will use config.
753
        # note: 'depth_level' and 'fee_level' in config are integer slider positions,
754
        # unlike fee_level here, which (when given) is a float in [0.0, 1.0]
755
        if dyn:
5✔
UNCOV
756
            if mempool:
×
UNCOV
757
                fee_rate = self.depth_to_fee(self.get_depth_level())
×
758
            else:
759
                fee_rate = self.eta_to_fee(self.get_fee_level())
×
760
        else:
761
            fee_rate = self.FEE_EST_STATIC_FEERATE
5✔
762
        if fee_rate is not None:
5✔
763
            fee_rate = int(fee_rate)
5✔
764
        return fee_rate
5✔
765

766
    def getfeerate(self) -> Tuple[str, int, Optional[int], str]:
5✔
UNCOV
767
        dyn = self.is_dynfee()
×
UNCOV
768
        mempool = self.use_mempool_fees()
×
769
        if dyn:
×
770
            if mempool:
×
771
                method = 'mempool'
×
772
                fee_level = self.get_depth_level()
×
773
                value = self.depth_target(fee_level)
×
774
                fee_rate = self.depth_to_fee(fee_level)
×
775
                tooltip = self.depth_tooltip(value)
×
776
            else:
777
                method = 'ETA'
×
UNCOV
778
                fee_level = self.get_fee_level()
×
779
                value = self.eta_target(fee_level)
×
780
                fee_rate = self.eta_to_fee(fee_level)
×
781
                tooltip = self.eta_tooltip(value)
×
782
        else:
783
            method = 'static'
×
UNCOV
784
            value = self.FEE_EST_STATIC_FEERATE
×
785
            fee_rate = value
×
786
            tooltip = 'static feerate'
×
787

788
        return method, value, fee_rate, tooltip
×
789

790
    def setfeerate(self, fee_method: str, value: int):
5✔
UNCOV
791
        if fee_method == 'mempool':
×
UNCOV
792
            if value not in FEE_DEPTH_TARGETS:
×
793
                raise Exception(f"Error: fee_level must be in {FEE_DEPTH_TARGETS}")
×
794
            self.FEE_EST_USE_MEMPOOL = True
×
795
            self.FEE_EST_DYNAMIC = True
×
796
            self.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS = FEE_DEPTH_TARGETS.index(value)
×
797
        elif fee_method == 'ETA':
×
798
            if value not in FEE_ETA_TARGETS:
×
799
                raise Exception(f"Error: fee_level must be in {FEE_ETA_TARGETS}")
×
800
            self.FEE_EST_USE_MEMPOOL = False
×
801
            self.FEE_EST_DYNAMIC = True
×
802
            self.FEE_EST_DYNAMIC_ETA_SLIDERPOS = FEE_ETA_TARGETS.index(value)
×
803
        elif fee_method == 'static':
×
804
            self.FEE_EST_DYNAMIC = False
×
805
            self.FEE_EST_STATIC_FEERATE = value
×
806
        else:
807
            raise Exception(f"Invalid parameter: {fee_method}. Valid methods are: ETA, mempool, static.")
×
808

809
    def fee_per_byte(self):
5✔
810
        """Returns sat/vB fee to pay for a txn.
811
        Note: might return None.
812
        """
UNCOV
813
        fee_per_kb = self.fee_per_kb()
×
UNCOV
814
        return fee_per_kb / 1000 if fee_per_kb is not None else None
×
815

816
    def estimate_fee(self, size: Union[int, float, Decimal], *,
5✔
817
                     allow_fallback_to_static_rates: bool = False) -> int:
818
        fee_per_kb = self.fee_per_kb()
5✔
819
        if fee_per_kb is None:
5✔
UNCOV
820
            if allow_fallback_to_static_rates:
×
UNCOV
821
                fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
×
822
            else:
823
                raise NoDynamicFeeEstimates()
×
824
        return self.estimate_fee_for_feerate(fee_per_kb, size)
5✔
825

826
    @classmethod
5✔
827
    def estimate_fee_for_feerate(cls, fee_per_kb: Union[int, float, Decimal],
5✔
828
                                 size: Union[int, float, Decimal]) -> int:
829
        # note: 'size' is in vbytes
830
        size = Decimal(size)
5✔
831
        fee_per_kb = Decimal(fee_per_kb)
5✔
832
        fee_per_byte = fee_per_kb / 1000
5✔
833
        # to be consistent with what is displayed in the GUI,
834
        # the calculation needs to use the same precision:
835
        fee_per_byte = quantize_feerate(fee_per_byte)
5✔
836
        return round(fee_per_byte * size)
5✔
837

838
    def update_fee_estimates(self, nblock_target: int, fee_per_kb: int):
5✔
UNCOV
839
        assert isinstance(nblock_target, int), f"expected int, got {nblock_target!r}"
×
UNCOV
840
        assert isinstance(fee_per_kb, int), f"expected int, got {fee_per_kb!r}"
×
841
        self.fee_estimates[nblock_target] = fee_per_kb
×
842

843
    def is_fee_estimates_update_required(self):
5✔
844
        """Checks time since last requested and updated fee estimates.
845
        Returns True if an update should be requested.
846
        """
UNCOV
847
        now = time.time()
×
UNCOV
848
        return now - self.last_time_fee_estimates_requested > 60
×
849

850
    def requested_fee_estimates(self):
5✔
UNCOV
851
        self.last_time_fee_estimates_requested = time.time()
×
852

853
    def get_video_device(self):
5✔
UNCOV
854
        device = self.VIDEO_DEVICE_PATH
×
UNCOV
855
        if device == 'default':
×
856
            device = ''
×
857
        return device
×
858

859
    def format_amount(
5✔
860
        self,
861
        amount_sat,
862
        *,
863
        is_diff=False,
864
        whitespaces=False,
865
        precision=None,
866
        add_thousands_sep: bool = None,
867
    ) -> str:
UNCOV
868
        if precision is None:
×
UNCOV
869
            precision = self.amt_precision_post_satoshi
×
870
        if add_thousands_sep is None:
×
871
            add_thousands_sep = self.amt_add_thousands_sep
×
872
        return format_satoshis(
×
873
            amount_sat,
874
            num_zeros=self.num_zeros,
875
            decimal_point=self.decimal_point,
876
            is_diff=is_diff,
877
            whitespaces=whitespaces,
878
            precision=precision,
879
            add_thousands_sep=add_thousands_sep,
880
        )
881

882
    def format_amount_and_units(self, *args, **kwargs) -> str:
5✔
UNCOV
883
        return self.format_amount(*args, **kwargs) + ' ' + self.get_base_unit()
×
884

885
    def format_fee_rate(self, fee_rate) -> str:
5✔
886
        """fee_rate is in sat/kvByte."""
UNCOV
887
        return format_fee_satoshis(fee_rate/1000, num_zeros=self.num_zeros) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE}"
×
888

889
    def get_base_unit(self):
5✔
UNCOV
890
        return decimal_point_to_base_unit_name(self.decimal_point)
×
891

892
    def set_base_unit(self, unit):
5✔
UNCOV
893
        assert unit in base_units.keys()
×
UNCOV
894
        self.decimal_point = base_unit_name_to_decimal_point(unit)
×
895
        self.BTC_AMOUNTS_DECIMAL_POINT = self.decimal_point
×
896

897
    def get_decimal_point(self):
5✔
898
        return self.decimal_point
5✔
899

900
    @cached_property
5✔
901
    def cv(config):
5✔
902
        """Allows getting a reference to a config variable without dereferencing it.
903

904
        Compare:
905
        >>> config.NETWORK_SERVER
906
        'testnet.hsmiths.com:53012:s'
907
        >>> config.cv.NETWORK_SERVER
908
        <ConfigVarWithConfig key='server'>
909
        """
910
        class CVLookupHelper:
5✔
911
            def __getattribute__(self, name: str) -> ConfigVarWithConfig:
5✔
912
                if name in ("from_key", ):  # don't apply magic, just use standard lookup
5✔
913
                    return super().__getattribute__(name)
5✔
914
                config_var = config.__class__.__getattribute__(type(config), name)
5✔
915
                if not isinstance(config_var, ConfigVar):
5✔
UNCOV
916
                    raise AttributeError()
×
917
                return ConfigVarWithConfig(config=config, config_var=config_var)
5✔
918
            def from_key(self, key: str) -> ConfigVarWithConfig:
5✔
919
                try:
5✔
920
                    config_var = _config_var_from_key[key]
5✔
921
                except KeyError:
5✔
922
                    raise KeyError(f"No ConfigVar with key={key!r}") from None
5✔
923
                return ConfigVarWithConfig(config=config, config_var=config_var)
5✔
924
            def __setattr__(self, name, value):
5✔
UNCOV
925
                raise Exception(
×
926
                    f"Cannot assign value to config.cv.{name} directly. "
927
                    f"Either use config.cv.{name}.set() or assign to config.{name} instead.")
928
        return CVLookupHelper()
5✔
929

930
    # config variables ----->
931
    NETWORK_AUTO_CONNECT = ConfigVar('auto_connect', default=True, type_=bool)
5✔
932
    NETWORK_ONESERVER = ConfigVar('oneserver', default=False, type_=bool)
5✔
933
    NETWORK_PROXY = ConfigVar('proxy', default=None, type_=str, convert_getter=lambda v: "none" if v is None else v)
5✔
934
    NETWORK_PROXY_USER = ConfigVar('proxy_user', default=None, type_=str)
5✔
935
    NETWORK_PROXY_PASSWORD = ConfigVar('proxy_password', default=None, type_=str)
5✔
936
    NETWORK_SERVER = ConfigVar('server', default=None, type_=str)
5✔
937
    NETWORK_NOONION = ConfigVar('noonion', default=False, type_=bool)
5✔
938
    NETWORK_OFFLINE = ConfigVar('offline', default=False, type_=bool)
5✔
939
    NETWORK_SKIPMERKLECHECK = ConfigVar('skipmerklecheck', default=False, type_=bool)
5✔
940
    NETWORK_SERVERFINGERPRINT = ConfigVar('serverfingerprint', default=None, type_=str)
5✔
941
    NETWORK_MAX_INCOMING_MSG_SIZE = ConfigVar('network_max_incoming_msg_size', default=1_000_000, type_=int)  # in bytes
5✔
942
    NETWORK_TIMEOUT = ConfigVar('network_timeout', default=None, type_=int)
5✔
943
    NETWORK_BOOKMARKED_SERVERS = ConfigVar('network_bookmarked_servers', default=None)
5✔
944

945
    WALLET_BATCH_RBF = ConfigVar(
5✔
946
        'batch_rbf', default=False, type_=bool,
947
        short_desc=lambda: _('Batch unconfirmed transactions'),
948
        long_desc=lambda: (
949
            _('If you check this box, your unconfirmed transactions will be consolidated into a single transaction.') + '\n' +
950
            _('This will save fees, but might have unwanted effects in terms of privacy')),
951
    )
952
    WALLET_MERGE_DUPLICATE_OUTPUTS = ConfigVar(
5✔
953
        'wallet_merge_duplicate_outputs', default=False, type_=bool,
954
        short_desc=lambda: _('Merge duplicate outputs'),
955
        long_desc=lambda: _('Merge transaction outputs that pay to the same address into '
956
                            'a single output that pays the sum of the original amounts.'),
957
    )
958
    WALLET_SPEND_CONFIRMED_ONLY = ConfigVar(
5✔
959
        'confirmed_only', default=False, type_=bool,
960
        short_desc=lambda: _('Spend only confirmed coins'),
961
        long_desc=lambda: _('Spend only confirmed inputs.'),
962
    )
963
    WALLET_COIN_CHOOSER_POLICY = ConfigVar('coin_chooser', default='Privacy', type_=str)
5✔
964
    WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = ConfigVar(
5✔
965
        'coin_chooser_output_rounding', default=True, type_=bool,
966
        short_desc=lambda: _('Enable output value rounding'),
967
        long_desc=lambda: (
968
            _('Set the value of the change output so that it has similar precision to the other outputs.') + '\n' +
969
            _('This might improve your privacy somewhat.') + '\n' +
970
            _('If enabled, at most 100 satoshis might be lost due to this, per transaction.')),
971
    )
972
    WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT = ConfigVar('unconf_utxo_freeze_threshold', default=5_000, type_=int)
5✔
973
    WALLET_PAYREQ_EXPIRY_SECONDS = ConfigVar('request_expiry', default=invoices.PR_DEFAULT_EXPIRATION_WHEN_CREATING, type_=int)
5✔
974
    WALLET_USE_SINGLE_PASSWORD = ConfigVar('single_password', default=False, type_=bool)
5✔
975
    # note: 'use_change' and 'multiple_change' are per-wallet settings
976
    WALLET_SEND_CHANGE_TO_LIGHTNING = ConfigVar(
5✔
977
        'send_change_to_lightning', default=False, type_=bool,
978
        short_desc=lambda: _('Send change to Lightning'),
979
        long_desc=lambda: _('If possible, send the change of this transaction to your channels, with a submarine swap'),
980
    )
981

982
    FX_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool)
5✔
983
    FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str)
5✔
984
    FX_EXCHANGE = ConfigVar('use_exchange', default='CoinGecko', type_=str)  # default exchange should ideally provide historical rates
5✔
985
    FX_HISTORY_RATES = ConfigVar(
5✔
986
        'history_rates', default=False, type_=bool,
987
        short_desc=lambda: _('Download historical rates'),
988
    )
989
    FX_HISTORY_RATES_CAPITAL_GAINS = ConfigVar(
5✔
990
        'history_rates_capital_gains', default=False, type_=bool,
991
        short_desc=lambda: _('Show Capital Gains'),
992
    )
993
    FX_SHOW_FIAT_BALANCE_FOR_ADDRESSES = ConfigVar(
5✔
994
        'fiat_address', default=False, type_=bool,
995
        short_desc=lambda: _('Show Fiat balances'),
996
    )
997

998
    LIGHTNING_LISTEN = ConfigVar('lightning_listen', default=None, type_=str)
5✔
999
    LIGHTNING_PEERS = ConfigVar('lightning_peers', default=None)
5✔
1000
    LIGHTNING_USE_GOSSIP = ConfigVar(
5✔
1001
        'use_gossip', default=False, type_=bool,
1002
        short_desc=lambda: _("Use trampoline routing"),
1003
        long_desc=lambda: _("""Lightning payments require finding a path through the Lightning Network. You may use trampoline routing, or local routing (gossip).
1004

1005
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."""),
1006
    )
1007
    LIGHTNING_USE_RECOVERABLE_CHANNELS = ConfigVar(
5✔
1008
        'use_recoverable_channels', default=True, type_=bool,
1009
        short_desc=lambda: _("Create recoverable channels"),
1010
        long_desc=lambda: _("""Add extra data to your channel funding transactions, so that a static backup can be recovered from your seed.
1011

1012
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.
1013

1014
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."""),
1015
    )
1016
    LIGHTNING_TO_SELF_DELAY_CSV = ConfigVar('lightning_to_self_delay', default=7 * 144, type_=int)
5✔
1017
    LIGHTNING_MAX_FUNDING_SAT = ConfigVar('lightning_max_funding_sat', default=LN_MAX_FUNDING_SAT_LEGACY, type_=int)
5✔
1018
    LIGHTNING_LEGACY_ADD_TRAMPOLINE = ConfigVar(
5✔
1019
        'lightning_legacy_add_trampoline', default=False, type_=bool,
1020
        short_desc=lambda: _("Add extra trampoline to legacy payments"),
1021
        long_desc=lambda: _("""When paying a non-trampoline invoice, add an extra trampoline to the route, in order to improve your privacy.
1022

1023
This will result in longer routes; it might increase your fees and decrease the success rate of your payments."""),
1024
    )
1025
    INITIAL_TRAMPOLINE_FEE_LEVEL = ConfigVar('initial_trampoline_fee_level', default=1, type_=int)
5✔
1026
    LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS = ConfigVar(
5✔
1027
        'lightning_payment_fee_max_millionths', default=10_000,  # 1%
1028
        type_=int,
1029
        short_desc=lambda: _("Max lightning fees to pay"),
1030
        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.
1031

1032
Warning: setting this to too low will result in lots of payment failures."""),
1033
    )
1034
    LIGHTNING_PAYMENT_FEE_CUTOFF_MSAT = ConfigVar(
5✔
1035
        'lightning_payment_fee_cutoff_msat', default=10_000,  # 10 sat
1036
        type_=int,
1037
        short_desc=lambda: _("Max lightning fees to pay for small payments"),
1038
    )
1039

1040
    LIGHTNING_NODE_ALIAS = ConfigVar('lightning_node_alias', default='', type_=str)
5✔
1041
    EXPERIMENTAL_LN_FORWARD_PAYMENTS = ConfigVar('lightning_forward_payments', default=False, type_=bool)
5✔
1042
    EXPERIMENTAL_LN_FORWARD_TRAMPOLINE_PAYMENTS = ConfigVar('lightning_forward_trampoline_payments', default=False, type_=bool)
5✔
1043
    TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE = ConfigVar('test_fail_htlcs_with_temp_node_failure', default=False, type_=bool)
5✔
1044
    TEST_FAIL_HTLCS_AS_MALFORMED = ConfigVar('test_fail_malformed_htlc', default=False, type_=bool)
5✔
1045
    TEST_FORCE_MPP = ConfigVar('test_force_mpp', default=False, type_=bool)
5✔
1046
    TEST_FORCE_DISABLE_MPP = ConfigVar('test_force_disable_mpp', default=False, type_=bool)
5✔
1047
    TEST_SHUTDOWN_FEE = ConfigVar('test_shutdown_fee', default=None, type_=int)
5✔
1048
    TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None)
5✔
1049
    TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool)
5✔
1050

1051
    FEE_EST_DYNAMIC = ConfigVar('dynamic_fees', default=True, type_=bool)
5✔
1052
    FEE_EST_USE_MEMPOOL = ConfigVar('mempool_fees', default=False, type_=bool)
5✔
1053
    FEE_EST_STATIC_FEERATE = ConfigVar('fee_per_kb', default=FEERATE_FALLBACK_STATIC_FEE, type_=int)
5✔
1054
    FEE_EST_DYNAMIC_ETA_SLIDERPOS = ConfigVar('fee_level', default=2, type_=int)
5✔
1055
    FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS = ConfigVar('depth_level', default=2, type_=int)
5✔
1056

1057
    RPC_USERNAME = ConfigVar('rpcuser', default=None, type_=str)
5✔
1058
    RPC_PASSWORD = ConfigVar('rpcpassword', default=None, type_=str)
5✔
1059
    RPC_HOST = ConfigVar('rpchost', default='127.0.0.1', type_=str)
5✔
1060
    RPC_PORT = ConfigVar('rpcport', default=0, type_=int)
5✔
1061
    RPC_SOCKET_TYPE = ConfigVar('rpcsock', default='auto', type_=str)
5✔
1062
    RPC_SOCKET_FILEPATH = ConfigVar('rpcsockpath', default=None, type_=str)
5✔
1063

1064
    GUI_NAME = ConfigVar('gui', default='qt', type_=str)
5✔
1065
    GUI_LAST_WALLET = ConfigVar('gui_last_wallet', default=None, type_=str)
5✔
1066

1067
    GUI_QT_COLOR_THEME = ConfigVar(
5✔
1068
        'qt_gui_color_theme', default='default', type_=str,
1069
        short_desc=lambda: _('Color theme'),
1070
    )
1071
    GUI_QT_DARK_TRAY_ICON = ConfigVar('dark_icon', default=False, type_=bool)
5✔
1072
    GUI_QT_WINDOW_IS_MAXIMIZED = ConfigVar('is_maximized', default=False, type_=bool)
5✔
1073
    GUI_QT_HIDE_ON_STARTUP = ConfigVar('hide_gui', default=False, type_=bool)
5✔
1074
    GUI_QT_HISTORY_TAB_SHOW_TOOLBAR = ConfigVar('show_toolbar_history', default=False, type_=bool)
5✔
1075
    GUI_QT_ADDRESSES_TAB_SHOW_TOOLBAR = ConfigVar('show_toolbar_addresses', default=False, type_=bool)
5✔
1076
    GUI_QT_TX_DIALOG_FETCH_TXIN_DATA = ConfigVar(
5✔
1077
        'tx_dialog_fetch_txin_data', default=False, type_=bool,
1078
        short_desc=lambda: _('Download missing data'),
1079
        long_desc=lambda: _(
1080
            'Download parent transactions from the network.\n'
1081
            'Allows filling in missing fee and input details.'),
1082
    )
1083
    GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA = ConfigVar(
5✔
1084
        'gui_qt_tx_dialog_export_strip_sensitive_metadata', default=False, type_=bool,
1085
        short_desc=lambda: _('For CoinJoin; strip privates'),
1086
    )
1087
    GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS = ConfigVar(
5✔
1088
        'gui_qt_tx_dialog_export_include_global_xpubs', default=False, type_=bool,
1089
        short_desc=lambda: _('For hardware device; include xpubs'),
1090
    )
1091
    GUI_QT_RECEIVE_TAB_QR_VISIBLE = ConfigVar('receive_qr_visible', default=False, type_=bool)
5✔
1092
    GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar(
5✔
1093
        'show_tx_io', default=False, type_=bool,
1094
        short_desc=lambda: _('Show inputs and outputs'),
1095
    )
1096
    GUI_QT_TX_EDITOR_SHOW_FEE_DETAILS = ConfigVar(
5✔
1097
        'show_tx_fee_details', default=False, type_=bool,
1098
        short_desc=lambda: _('Edit fees manually'),
1099
    )
1100
    GUI_QT_TX_EDITOR_SHOW_LOCKTIME = ConfigVar(
5✔
1101
        'show_tx_locktime', default=False, type_=bool,
1102
        short_desc=lambda: _('Edit Locktime'),
1103
    )
1104
    GUI_QT_SHOW_TAB_ADDRESSES = ConfigVar('show_addresses_tab', default=False, type_=bool)
5✔
1105
    GUI_QT_SHOW_TAB_CHANNELS = ConfigVar('show_channels_tab', default=False, type_=bool)
5✔
1106
    GUI_QT_SHOW_TAB_UTXO = ConfigVar('show_utxo_tab', default=False, type_=bool)
5✔
1107
    GUI_QT_SHOW_TAB_CONTACTS = ConfigVar('show_contacts_tab', default=False, type_=bool)
5✔
1108
    GUI_QT_SHOW_TAB_CONSOLE = ConfigVar('show_console_tab', default=False, type_=bool)
5✔
1109
    GUI_QT_SHOW_TAB_NOTES = ConfigVar('show_notes_tab', default=False, type_=bool)
5✔
1110

1111
    GUI_QML_PREFERRED_REQUEST_TYPE = ConfigVar('preferred_request_type', default='bolt11', type_=str)
5✔
1112
    GUI_QML_USER_KNOWS_PRESS_AND_HOLD = ConfigVar('user_knows_press_and_hold', default=False, type_=bool)
5✔
1113
    GUI_QML_ADDRESS_LIST_SHOW_TYPE = ConfigVar('address_list_show_type', default=1, type_=int)
5✔
1114
    GUI_QML_ADDRESS_LIST_SHOW_USED = ConfigVar('address_list_show_used', default=False, type_=bool)
5✔
1115
    GUI_QML_ALWAYS_ALLOW_SCREENSHOTS = ConfigVar('android_always_allow_screenshots', default=False, type_=bool)
5✔
1116
    GUI_QML_SET_MAX_BRIGHTNESS_ON_QR_DISPLAY = ConfigVar('android_set_max_brightness_on_qr_display', default=True, type_=bool)
5✔
1117

1118
    BTC_AMOUNTS_DECIMAL_POINT = ConfigVar('decimal_point', default=DECIMAL_POINT_DEFAULT, type_=int)
5✔
1119
    BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT = ConfigVar(
5✔
1120
        'num_zeros', default=0, type_=int,
1121
        short_desc=lambda: _('Zeros after decimal point'),
1122
        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"'),
1123
    )
1124
    BTC_AMOUNTS_PREC_POST_SAT = ConfigVar(
5✔
1125
        'amt_precision_post_satoshi', default=0, type_=int,
1126
        short_desc=lambda: _("Show Lightning amounts with msat precision"),
1127
    )
1128
    BTC_AMOUNTS_ADD_THOUSANDS_SEP = ConfigVar(
5✔
1129
        'amt_add_thousands_sep', default=False, type_=bool,
1130
        short_desc=lambda: _("Add thousand separators to bitcoin amounts"),
1131
    )
1132

1133
    BLOCK_EXPLORER = ConfigVar(
5✔
1134
        'block_explorer', default='Blockstream.info', type_=str,
1135
        short_desc=lambda: _('Online Block Explorer'),
1136
        long_desc=lambda: _('Choose which online block explorer to use for functions that open a web browser'),
1137
    )
1138
    BLOCK_EXPLORER_CUSTOM = ConfigVar('block_explorer_custom', default=None)
5✔
1139
    VIDEO_DEVICE_PATH = ConfigVar(
5✔
1140
        'video_device', default='default', type_=str,
1141
        short_desc=lambda: _('Video Device'),
1142
        long_desc=lambda: (_("For scanning QR codes.") + "\n" +
1143
                           _("Install the zbar package to enable this.")),
1144
    )
1145
    OPENALIAS_ID = ConfigVar(
5✔
1146
        'alias', default="", type_=str,
1147
        short_desc=lambda: 'OpenAlias',
1148
        long_desc=lambda: (
1149
            _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n' +
1150
            _('The following alias providers are available:') + '\n' +
1151
            '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n' +
1152
            'For more information, see https://openalias.org'),
1153
    )
1154
    HWD_SESSION_TIMEOUT = ConfigVar('session_timeout', default=300, type_=int)
5✔
1155
    CLI_TIMEOUT = ConfigVar('timeout', default=60, type_=float)
5✔
1156
    AUTOMATIC_CENTRALIZED_UPDATE_CHECKS = ConfigVar(
5✔
1157
        'check_updates', default=False, type_=bool,
1158
        short_desc=lambda: _("Automatically check for software updates"),
1159
    )
1160
    WRITE_LOGS_TO_DISK = ConfigVar(
5✔
1161
        'log_to_file', default=False, type_=bool,
1162
        short_desc=lambda: _("Write logs to file"),
1163
        long_desc=lambda: _('Debug logs can be persisted to disk. These are useful for troubleshooting.'),
1164
    )
1165
    LOGS_NUM_FILES_KEEP = ConfigVar('logs_num_files_keep', default=10, type_=int)
5✔
1166
    GUI_ENABLE_DEBUG_LOGS = ConfigVar('gui_enable_debug_logs', default=False, type_=bool)
5✔
1167
    LOCALIZATION_LANGUAGE = ConfigVar(
5✔
1168
        'language', default="", type_=str,
1169
        short_desc=lambda: _("Language"),
1170
        long_desc=lambda: _("Select which language is used in the GUI (after restart)."),
1171
    )
1172
    BLOCKCHAIN_PREFERRED_BLOCK = ConfigVar('blockchain_preferred_block', default=None)
5✔
1173
    SHOW_CRASH_REPORTER = ConfigVar('show_crash_reporter', default=True, type_=bool)
5✔
1174
    DONT_SHOW_TESTNET_WARNING = ConfigVar('dont_show_testnet_warning', default=False, type_=bool)
5✔
1175
    RECENTLY_OPEN_WALLET_FILES = ConfigVar('recently_open', default=None)
5✔
1176
    IO_DIRECTORY = ConfigVar('io_dir', default=os.path.expanduser('~'), type_=str)
5✔
1177
    WALLET_BACKUP_DIRECTORY = ConfigVar('backup_dir', default=None, type_=str)
5✔
1178
    CONFIG_PIN_CODE = ConfigVar('pin_code', default=None, type_=str)
5✔
1179
    QR_READER_FLIP_X = ConfigVar('qrreader_flip_x', default=True, type_=bool)
5✔
1180
    WIZARD_DONT_CREATE_SEGWIT = ConfigVar('nosegwit', default=False, type_=bool)
5✔
1181
    CONFIG_FORGET_CHANGES = ConfigVar('forget_config', default=False, type_=bool)
5✔
1182

1183
    # connect to remote submarine swap server
1184
    SWAPSERVER_URL = ConfigVar('swapserver_url', default='', type_=str)
5✔
1185
    # run submarine swap server locally
1186
    SWAPSERVER_PORT = ConfigVar('swapserver_port', default=None, type_=int)
5✔
1187
    SWAPSERVER_FEE_MILLIONTHS = ConfigVar('swapserver_fee_millionths', default=5000, type_=int)
5✔
1188
    TEST_SWAPSERVER_REFUND = ConfigVar('test_swapserver_refund', default=False, type_=bool)
5✔
1189
    SWAPSERVER_NPUB = ConfigVar('swapserver_npub', default=None, type_=str)
5✔
1190
    SWAPSERVER_ANN_POW_NONCE = ConfigVar('swapserver_ann_pow_nonce', default=0, type_=int)
5✔
1191
    SWAPSERVER_POW_TARGET = ConfigVar('swapserver_pow_target', default=30, type_=int)
5✔
1192

1193
    # nostr
1194
    NOSTR_RELAYS = ConfigVar(
5✔
1195
        'nostr_relays',
1196
        default='wss://nos.lol,wss://relay.damus.io,wss://brb.io,wss://nostr.mom,'
1197
                'wss://relay.primal.net,wss://ftp.halifax.rwth-aachen.de/nostr,'
1198
                'wss://eu.purplerelay.com,wss://nostr.einundzwanzig.space',
1199
        type_=str,
1200
        short_desc=lambda: _("Nostr relays"),
1201
        long_desc=lambda: ' '.join([
1202
            _('Nostr relays are used to send and receive submarine swap offers'),
1203
            _('If this list is empty, Electrum will use http instead'),
1204
        ]),
1205
    )
1206

1207
    # anchor outputs channels
1208
    ENABLE_ANCHOR_CHANNELS = ConfigVar('enable_anchor_channels', default=False, type_=bool)
5✔
1209
    # zeroconf channels
1210
    ACCEPT_ZEROCONF_CHANNELS = ConfigVar('accept_zeroconf_channels', default=False, type_=bool)
5✔
1211
    ZEROCONF_TRUSTED_NODE = ConfigVar('zeroconf_trusted_node', default='', type_=str)
5✔
1212
    ZEROCONF_MIN_OPENING_FEE = ConfigVar('zeroconf_min_opening_fee', default=5000, type_=int)
5✔
1213

1214
    # connect to remote WT
1215
    WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str)
5✔
1216

1217
    # run WT locally
1218
    WATCHTOWER_SERVER_ENABLED = ConfigVar('run_watchtower', default=False, type_=bool)
5✔
1219
    WATCHTOWER_SERVER_PORT = ConfigVar('watchtower_port', default=None, type_=int)
5✔
1220
    WATCHTOWER_SERVER_USER = ConfigVar('watchtower_user', default=None, type_=str)
5✔
1221
    WATCHTOWER_SERVER_PASSWORD = ConfigVar('watchtower_password', default=None, type_=str)
5✔
1222

1223
    PAYSERVER_PORT = ConfigVar('payserver_port', default=8080, type_=int)
5✔
1224
    PAYSERVER_ROOT = ConfigVar('payserver_root', default='/r', type_=str)
5✔
1225
    PAYSERVER_ALLOW_CREATE_INVOICE = ConfigVar('payserver_allow_create_invoice', default=False, type_=bool)
5✔
1226

1227
    PLUGIN_TRUSTEDCOIN_NUM_PREPAY = ConfigVar('trustedcoin_prepay', default=20, type_=int)
5✔
1228

1229

1230
def read_user_config(path: Optional[str]) -> Dict[str, Any]:
5✔
1231
    """Parse and store the user config settings in electrum.conf into user_config[]."""
1232
    if not path:
5✔
1233
        return {}
5✔
1234
    config_path = os.path.join(path, "config")
5✔
1235
    if not os.path.exists(config_path):
5✔
1236
        return {}
5✔
1237
    try:
5✔
1238
        with open(config_path, "r", encoding='utf-8') as f:
5✔
1239
            data = f.read()
5✔
1240
        result = json.loads(data)
5✔
1241
    except Exception as exc:
5✔
1242
        _logger.warning(f"Cannot read config file at {config_path}: {exc}")
5✔
1243
        return {}
5✔
1244
    if not type(result) is dict:
5✔
UNCOV
1245
        return {}
×
1246
    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