tsimple_config.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
tsimple_config.py (25073B)
---
1 import json
2 import threading
3 import time
4 import os
5 import stat
6 import ssl
7 from decimal import Decimal
8 from typing import Union, Optional, Dict, Sequence, Tuple
9 from numbers import Real
10
11 from copy import deepcopy
12 from aiorpcx import NetAddress
13
14 from . import util
15 from . import constants
16 from .util import base_units, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, UnknownBaseUnit, DECIMAL_POINT_DEFAULT
17 from .util import format_satoshis, format_fee_satoshis
18 from .util import user_dir, make_dir, NoDynamicFeeEstimates, quantize_feerate
19 from .i18n import _
20 from .logging import get_logger, Logger
21
22
23 FEE_ETA_TARGETS = [25, 10, 5, 2]
24 FEE_DEPTH_TARGETS = [10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000]
25 FEE_LN_ETA_TARGET = 2 # note: make sure the network is asking for estimates for this target
26
27 # satoshi per kbyte
28 FEERATE_MAX_DYNAMIC = 1500000
29 FEERATE_WARNING_HIGH_FEE = 600000
30 FEERATE_FALLBACK_STATIC_FEE = 150000
31 FEERATE_DEFAULT_RELAY = 1000
32 FEERATE_MAX_RELAY = 50000
33 FEERATE_STATIC_VALUES = [1000, 2000, 5000, 10000, 20000, 30000,
34 50000, 70000, 100000, 150000, 200000, 300000]
35 FEERATE_REGTEST_HARDCODED = 180000 # for eclair compat
36
37 FEE_RATIO_HIGH_WARNING = 0.05 # warn user if fee/amount for on-chain tx is higher than this
38
39
40 _logger = get_logger(__name__)
41
42
43 FINAL_CONFIG_VERSION = 3
44
45
46 class SimpleConfig(Logger):
47 """
48 The SimpleConfig class is responsible for handling operations involving
49 configuration files.
50
51 There are two different sources of possible configuration values:
52 1. Command line options.
53 2. User configuration (in the user's config directory)
54 They are taken in order (1. overrides config options set in 2.)
55 """
56
57 def __init__(self, options=None, read_user_config_function=None,
58 read_user_dir_function=None):
59 if options is None:
60 options = {}
61
62 Logger.__init__(self)
63
64 # This lock needs to be acquired for updating and reading the config in
65 # a thread-safe way.
66 self.lock = threading.RLock()
67
68 self.mempool_fees = None # type: Optional[Sequence[Tuple[Union[float, int], int]]]
69 self.fee_estimates = {}
70 self.fee_estimates_last_updated = {}
71 self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees
72
73 # The following two functions are there for dependency injection when
74 # testing.
75 if read_user_config_function is None:
76 read_user_config_function = read_user_config
77 if read_user_dir_function is None:
78 self.user_dir = user_dir
79 else:
80 self.user_dir = read_user_dir_function
81
82 # The command line options
83 self.cmdline_options = deepcopy(options)
84 # don't allow to be set on CLI:
85 self.cmdline_options.pop('config_version', None)
86
87 # Set self.path and read the user config
88 self.user_config = {} # for self.get in electrum_path()
89 self.path = self.electrum_path()
90 self.user_config = read_user_config_function(self.path)
91 if not self.user_config:
92 # avoid new config getting upgraded
93 self.user_config = {'config_version': FINAL_CONFIG_VERSION}
94
95 self._not_modifiable_keys = set()
96
97 # config "upgrade" - CLI options
98 self.rename_config_keys(
99 self.cmdline_options, {'auto_cycle': 'auto_connect'}, True)
100
101 # config upgrade - user config
102 if self.requires_upgrade():
103 self.upgrade()
104
105 self._check_dependent_keys()
106
107 # units and formatting
108 self.decimal_point = self.get('decimal_point', DECIMAL_POINT_DEFAULT)
109 try:
110 decimal_point_to_base_unit_name(self.decimal_point)
111 except UnknownBaseUnit:
112 self.decimal_point = DECIMAL_POINT_DEFAULT
113 self.num_zeros = int(self.get('num_zeros', 0))
114
115 def electrum_path(self):
116 # Read electrum_path from command line
117 # Otherwise use the user's default data directory.
118 path = self.get('electrum_path')
119 if path is None:
120 path = self.user_dir()
121
122 make_dir(path, allow_symlink=False)
123 if self.get('testnet'):
124 path = os.path.join(path, 'testnet')
125 make_dir(path, allow_symlink=False)
126 elif self.get('regtest'):
127 path = os.path.join(path, 'regtest')
128 make_dir(path, allow_symlink=False)
129 elif self.get('simnet'):
130 path = os.path.join(path, 'simnet')
131 make_dir(path, allow_symlink=False)
132
133 self.logger.info(f"electrum directory {path}")
134 return path
135
136 def rename_config_keys(self, config, keypairs, deprecation_warning=False):
137 """Migrate old key names to new ones"""
138 updated = False
139 for old_key, new_key in keypairs.items():
140 if old_key in config:
141 if new_key not in config:
142 config[new_key] = config[old_key]
143 if deprecation_warning:
144 self.logger.warning('Note that the {} variable has been deprecated. '
145 'You should use {} instead.'.format(old_key, new_key))
146 del config[old_key]
147 updated = True
148 return updated
149
150 def set_key(self, key, value, save=True):
151 if not self.is_modifiable(key):
152 self.logger.warning(f"not changing config key '{key}' set on the command line")
153 return
154 try:
155 json.dumps(key)
156 json.dumps(value)
157 except:
158 self.logger.info(f"json error: cannot save {repr(key)} ({repr(value)})")
159 return
160 self._set_key_in_user_config(key, value, save)
161
162 def _set_key_in_user_config(self, key, value, save=True):
163 with self.lock:
164 if value is not None:
165 self.user_config[key] = value
166 else:
167 self.user_config.pop(key, None)
168 if save:
169 self.save_user_config()
170
171 def get(self, key, default=None):
172 with self.lock:
173 out = self.cmdline_options.get(key)
174 if out is None:
175 out = self.user_config.get(key, default)
176 return out
177
178 def _check_dependent_keys(self) -> None:
179 if self.get('serverfingerprint'):
180 if not self.get('server'):
181 raise Exception("config key 'serverfingerprint' requires 'server' to also be set")
182 self.make_key_not_modifiable('server')
183
184 def requires_upgrade(self):
185 return self.get_config_version() < FINAL_CONFIG_VERSION
186
187 def upgrade(self):
188 with self.lock:
189 self.logger.info('upgrading config')
190
191 self.convert_version_2()
192 self.convert_version_3()
193
194 self.set_key('config_version', FINAL_CONFIG_VERSION, save=True)
195
196 def convert_version_2(self):
197 if not self._is_upgrade_method_needed(1, 1):
198 return
199
200 self.rename_config_keys(self.user_config, {'auto_cycle': 'auto_connect'})
201
202 try:
203 # change server string FROM host:port:proto TO host:port:s
204 server_str = self.user_config.get('server')
205 host, port, protocol = str(server_str).rsplit(':', 2)
206 assert protocol in ('s', 't')
207 int(port) # Throw if cannot be converted to int
208 server_str = '{}:{}:s'.format(host, port)
209 self._set_key_in_user_config('server', server_str)
210 except BaseException:
211 self._set_key_in_user_config('server', None)
212
213 self.set_key('config_version', 2)
214
215 def convert_version_3(self):
216 if not self._is_upgrade_method_needed(2, 2):
217 return
218
219 base_unit = self.user_config.get('base_unit')
220 if isinstance(base_unit, str):
221 self._set_key_in_user_config('base_unit', None)
222 map_ = {'btc':8, 'mbtc':5, 'ubtc':2, 'bits':2, 'sat':0}
223 decimal_point = map_.get(base_unit.lower())
224 self._set_key_in_user_config('decimal_point', decimal_point)
225
226 self.set_key('config_version', 3)
227
228 def _is_upgrade_method_needed(self, min_version, max_version):
229 cur_version = self.get_config_version()
230 if cur_version > max_version:
231 return False
232 elif cur_version < min_version:
233 raise Exception(
234 ('config upgrade: unexpected version %d (should be %d-%d)'
235 % (cur_version, min_version, max_version)))
236 else:
237 return True
238
239 def get_config_version(self):
240 config_version = self.get('config_version', 1)
241 if config_version > FINAL_CONFIG_VERSION:
242 self.logger.warning('config version ({}) is higher than latest ({})'
243 .format(config_version, FINAL_CONFIG_VERSION))
244 return config_version
245
246 def is_modifiable(self, key) -> bool:
247 return (key not in self.cmdline_options
248 and key not in self._not_modifiable_keys)
249
250 def make_key_not_modifiable(self, key) -> None:
251 self._not_modifiable_keys.add(key)
252
253 def save_user_config(self):
254 if self.get('forget_config'):
255 return
256 if not self.path:
257 return
258 path = os.path.join(self.path, "config")
259 s = json.dumps(self.user_config, indent=4, sort_keys=True)
260 try:
261 with open(path, "w", encoding='utf-8') as f:
262 f.write(s)
263 os.chmod(path, stat.S_IREAD | stat.S_IWRITE)
264 except FileNotFoundError:
265 # datadir probably deleted while running...
266 if os.path.exists(self.path): # or maybe not?
267 raise
268
269 def get_wallet_path(self, *, use_gui_last_wallet=False):
270 """Set the path of the wallet."""
271
272 # command line -w option
273 if self.get('wallet_path'):
274 return os.path.join(self.get('cwd', ''), self.get('wallet_path'))
275
276 if use_gui_last_wallet:
277 path = self.get('gui_last_wallet')
278 if path and os.path.exists(path):
279 return path
280
281 # default path
282 util.assert_datadir_available(self.path)
283 dirpath = os.path.join(self.path, "wallets")
284 make_dir(dirpath, allow_symlink=False)
285
286 new_path = os.path.join(self.path, "wallets", "default_wallet")
287
288 # default path in pre 1.9 versions
289 old_path = os.path.join(self.path, "electrum.dat")
290 if os.path.exists(old_path) and not os.path.exists(new_path):
291 os.rename(old_path, new_path)
292
293 return new_path
294
295 def remove_from_recently_open(self, filename):
296 recent = self.get('recently_open', [])
297 if filename in recent:
298 recent.remove(filename)
299 self.set_key('recently_open', recent)
300
301 def set_session_timeout(self, seconds):
302 self.logger.info(f"session timeout -> {seconds} seconds")
303 self.set_key('session_timeout', seconds)
304
305 def get_session_timeout(self):
306 return self.get('session_timeout', 300)
307
308 def save_last_wallet(self, wallet):
309 if self.get('wallet_path') is None:
310 path = wallet.storage.path
311 self.set_key('gui_last_wallet', path)
312
313 def impose_hard_limits_on_fee(func):
314 def get_fee_within_limits(self, *args, **kwargs):
315 fee = func(self, *args, **kwargs)
316 if fee is None:
317 return fee
318 fee = min(FEERATE_MAX_DYNAMIC, fee)
319 fee = max(FEERATE_DEFAULT_RELAY, fee)
320 return fee
321 return get_fee_within_limits
322
323 def eta_to_fee(self, slider_pos) -> Optional[int]:
324 """Returns fee in sat/kbyte."""
325 slider_pos = max(slider_pos, 0)
326 slider_pos = min(slider_pos, len(FEE_ETA_TARGETS))
327 if slider_pos < len(FEE_ETA_TARGETS):
328 num_blocks = FEE_ETA_TARGETS[int(slider_pos)]
329 fee = self.eta_target_to_fee(num_blocks)
330 else:
331 fee = self.eta_target_to_fee(1)
332 return fee
333
334 @impose_hard_limits_on_fee
335 def eta_target_to_fee(self, num_blocks: int) -> Optional[int]:
336 """Returns fee in sat/kbyte."""
337 if num_blocks == 1:
338 fee = self.fee_estimates.get(2)
339 if fee is not None:
340 fee += fee / 2
341 fee = int(fee)
342 else:
343 fee = self.fee_estimates.get(num_blocks)
344 if fee is not None:
345 fee = int(fee)
346 return fee
347
348 def fee_to_depth(self, target_fee: Real) -> Optional[int]:
349 """For a given sat/vbyte fee, returns an estimate of how deep
350 it would be in the current mempool in vbytes.
351 Pessimistic == overestimates the depth.
352 """
353 if self.mempool_fees is None:
354 return None
355 depth = 0
356 for fee, s in self.mempool_fees:
357 depth += s
358 if fee <= target_fee:
359 break
360 return depth
361
362 def depth_to_fee(self, slider_pos) -> Optional[int]:
363 """Returns fee in sat/kbyte."""
364 target = self.depth_target(slider_pos)
365 return self.depth_target_to_fee(target)
366
367 @impose_hard_limits_on_fee
368 def depth_target_to_fee(self, target: int) -> Optional[int]:
369 """Returns fee in sat/kbyte.
370 target: desired mempool depth in vbytes
371 """
372 if self.mempool_fees is None:
373 return None
374 depth = 0
375 for fee, s in self.mempool_fees:
376 depth += s
377 if depth > target:
378 break
379 else:
380 return 0
381 # add one sat/byte as currently that is
382 # the max precision of the histogram
383 # (well, in case of ElectrumX at least. not for electrs)
384 fee += 1
385 # convert to sat/kbyte
386 return int(fee * 1000)
387
388 def depth_target(self, slider_pos: int) -> int:
389 """Returns mempool depth target in bytes for a fee slider position."""
390 slider_pos = max(slider_pos, 0)
391 slider_pos = min(slider_pos, len(FEE_DEPTH_TARGETS)-1)
392 return FEE_DEPTH_TARGETS[slider_pos]
393
394 def eta_target(self, slider_pos: int) -> int:
395 """Returns 'num blocks' ETA target for a fee slider position."""
396 if slider_pos == len(FEE_ETA_TARGETS):
397 return 1
398 return FEE_ETA_TARGETS[slider_pos]
399
400 def fee_to_eta(self, fee_per_kb: int) -> int:
401 """Returns 'num blocks' ETA estimate for given fee rate,
402 or -1 for low fee.
403 """
404 import operator
405 lst = list(self.fee_estimates.items()) + [(1, self.eta_to_fee(len(FEE_ETA_TARGETS)))]
406 dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), lst)
407 min_target, min_value = min(dist, key=operator.itemgetter(1))
408 if fee_per_kb < self.fee_estimates.get(FEE_ETA_TARGETS[0])/2:
409 min_target = -1
410 return min_target
411
412 def depth_tooltip(self, depth: Optional[int]) -> str:
413 """Returns text tooltip for given mempool depth (in vbytes)."""
414 if depth is None:
415 return "unknown from tip"
416 return "%.1f MB from tip" % (depth/1_000_000)
417
418 def eta_tooltip(self, x):
419 if x < 0:
420 return _('Low fee')
421 elif x == 1:
422 return _('In the next block')
423 else:
424 return _('Within {} blocks').format(x)
425
426 def get_fee_target(self):
427 dyn = self.is_dynfee()
428 mempool = self.use_mempool_fees()
429 pos = self.get_depth_level() if mempool else self.get_fee_level()
430 fee_rate = self.fee_per_kb()
431 target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate)
432 return target, tooltip, dyn
433
434 def get_fee_status(self):
435 target, tooltip, dyn = self.get_fee_target()
436 return tooltip + ' [%s]'%target if dyn else target + ' [Static]'
437
438 def get_fee_text(
439 self,
440 slider_pos: int,
441 dyn: bool,
442 mempool: bool,
443 fee_per_kb: Optional[int],
444 ):
445 """Returns (text, tooltip) where
446 text is what we target: static fee / num blocks to confirm in / mempool depth
447 tooltip is the corresponding estimate (e.g. num blocks for a static fee)
448
449 fee_rate is in sat/kbyte
450 """
451 if fee_per_kb is None:
452 rate_str = 'unknown'
453 fee_per_byte = None
454 else:
455 fee_per_byte = fee_per_kb/1000
456 rate_str = format_fee_satoshis(fee_per_byte) + ' sat/byte'
457
458 if dyn:
459 if mempool:
460 depth = self.depth_target(slider_pos)
461 text = self.depth_tooltip(depth)
462 else:
463 eta = self.eta_target(slider_pos)
464 text = self.eta_tooltip(eta)
465 tooltip = rate_str
466 else: # using static fees
467 assert fee_per_kb is not None
468 assert fee_per_byte is not None
469 text = rate_str
470 if mempool and self.has_fee_mempool():
471 depth = self.fee_to_depth(fee_per_byte)
472 tooltip = self.depth_tooltip(depth)
473 elif not mempool and self.has_fee_etas():
474 eta = self.fee_to_eta(fee_per_kb)
475 tooltip = self.eta_tooltip(eta)
476 else:
477 tooltip = ''
478 return text, tooltip
479
480 def get_depth_level(self):
481 maxp = len(FEE_DEPTH_TARGETS) - 1
482 return min(maxp, self.get('depth_level', 2))
483
484 def get_fee_level(self):
485 maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block"
486 return min(maxp, self.get('fee_level', 2))
487
488 def get_fee_slider(self, dyn, mempool) -> Tuple[int, int, Optional[int]]:
489 if dyn:
490 if mempool:
491 pos = self.get_depth_level()
492 maxp = len(FEE_DEPTH_TARGETS) - 1
493 fee_rate = self.depth_to_fee(pos)
494 else:
495 pos = self.get_fee_level()
496 maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block"
497 fee_rate = self.eta_to_fee(pos)
498 else:
499 fee_rate = self.fee_per_kb(dyn=False)
500 pos = self.static_fee_index(fee_rate)
501 maxp = len(FEERATE_STATIC_VALUES) - 1
502 return maxp, pos, fee_rate
503
504 def static_fee(self, i):
505 return FEERATE_STATIC_VALUES[i]
506
507 def static_fee_index(self, value) -> int:
508 if value is None:
509 raise TypeError('static fee cannot be None')
510 dist = list(map(lambda x: abs(x - value), FEERATE_STATIC_VALUES))
511 return min(range(len(dist)), key=dist.__getitem__)
512
513 def has_fee_etas(self):
514 return len(self.fee_estimates) == 4
515
516 def has_fee_mempool(self) -> bool:
517 return self.mempool_fees is not None
518
519 def has_dynamic_fees_ready(self):
520 if self.use_mempool_fees():
521 return self.has_fee_mempool()
522 else:
523 return self.has_fee_etas()
524
525 def is_dynfee(self):
526 return bool(self.get('dynamic_fees', True))
527
528 def use_mempool_fees(self):
529 return bool(self.get('mempool_fees', False))
530
531 def _feerate_from_fractional_slider_position(self, fee_level: float, dyn: bool,
532 mempool: bool) -> Union[int, None]:
533 fee_level = max(fee_level, 0)
534 fee_level = min(fee_level, 1)
535 if dyn:
536 max_pos = (len(FEE_DEPTH_TARGETS) - 1) if mempool else len(FEE_ETA_TARGETS)
537 slider_pos = round(fee_level * max_pos)
538 fee_rate = self.depth_to_fee(slider_pos) if mempool else self.eta_to_fee(slider_pos)
539 else:
540 max_pos = len(FEERATE_STATIC_VALUES) - 1
541 slider_pos = round(fee_level * max_pos)
542 fee_rate = FEERATE_STATIC_VALUES[slider_pos]
543 return fee_rate
544
545 def fee_per_kb(self, dyn: bool=None, mempool: bool=None, fee_level: float=None) -> Optional[int]:
546 """Returns sat/kvB fee to pay for a txn.
547 Note: might return None.
548
549 fee_level: float between 0.0 and 1.0, representing fee slider position
550 """
551 if constants.net is constants.BitcoinRegtest:
552 return FEERATE_REGTEST_HARDCODED
553 if dyn is None:
554 dyn = self.is_dynfee()
555 if mempool is None:
556 mempool = self.use_mempool_fees()
557 if fee_level is not None:
558 return self._feerate_from_fractional_slider_position(fee_level, dyn, mempool)
559 # there is no fee_level specified; will use config.
560 # note: 'depth_level' and 'fee_level' in config are integer slider positions,
561 # unlike fee_level here, which (when given) is a float in [0.0, 1.0]
562 if dyn:
563 if mempool:
564 fee_rate = self.depth_to_fee(self.get_depth_level())
565 else:
566 fee_rate = self.eta_to_fee(self.get_fee_level())
567 else:
568 fee_rate = self.get('fee_per_kb', FEERATE_FALLBACK_STATIC_FEE)
569 if fee_rate is not None:
570 fee_rate = int(fee_rate)
571 return fee_rate
572
573 def fee_per_byte(self):
574 """Returns sat/vB fee to pay for a txn.
575 Note: might return None.
576 """
577 fee_per_kb = self.fee_per_kb()
578 return fee_per_kb / 1000 if fee_per_kb is not None else None
579
580 def estimate_fee(self, size: Union[int, float, Decimal], *,
581 allow_fallback_to_static_rates: bool = False) -> int:
582 fee_per_kb = self.fee_per_kb()
583 if fee_per_kb is None:
584 if allow_fallback_to_static_rates:
585 fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
586 else:
587 raise NoDynamicFeeEstimates()
588 return self.estimate_fee_for_feerate(fee_per_kb, size)
589
590 @classmethod
591 def estimate_fee_for_feerate(cls, fee_per_kb: Union[int, float, Decimal],
592 size: Union[int, float, Decimal]) -> int:
593 size = Decimal(size)
594 fee_per_kb = Decimal(fee_per_kb)
595 fee_per_byte = fee_per_kb / 1000
596 # to be consistent with what is displayed in the GUI,
597 # the calculation needs to use the same precision:
598 fee_per_byte = quantize_feerate(fee_per_byte)
599 return round(fee_per_byte * size)
600
601 def update_fee_estimates(self, key, value):
602 self.fee_estimates[key] = value
603 self.fee_estimates_last_updated[key] = time.time()
604
605 def is_fee_estimates_update_required(self):
606 """Checks time since last requested and updated fee estimates.
607 Returns True if an update should be requested.
608 """
609 now = time.time()
610 return now - self.last_time_fee_estimates_requested > 60
611
612 def requested_fee_estimates(self):
613 self.last_time_fee_estimates_requested = time.time()
614
615 def get_video_device(self):
616 device = self.get("video_device", "default")
617 if device == 'default':
618 device = ''
619 return device
620
621 def get_ssl_context(self):
622 ssl_keyfile = self.get('ssl_keyfile')
623 ssl_certfile = self.get('ssl_certfile')
624 if ssl_keyfile and ssl_certfile:
625 ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
626 ssl_context.load_cert_chain(ssl_certfile, ssl_keyfile)
627 return ssl_context
628
629 def get_ssl_domain(self):
630 from .paymentrequest import check_ssl_config
631 if self.get('ssl_keyfile') and self.get('ssl_certfile'):
632 SSL_identity = check_ssl_config(self)
633 else:
634 SSL_identity = None
635 return SSL_identity
636
637 def get_netaddress(self, key: str) -> Optional[NetAddress]:
638 text = self.get(key)
639 if text:
640 try:
641 return NetAddress.from_string(text)
642 except:
643 pass
644
645 def format_amount(self, x, is_diff=False, whitespaces=False):
646 return format_satoshis(
647 x,
648 num_zeros=self.num_zeros,
649 decimal_point=self.decimal_point,
650 is_diff=is_diff,
651 whitespaces=whitespaces,
652 )
653
654 def format_amount_and_units(self, amount):
655 return self.format_amount(amount) + ' '+ self.get_base_unit()
656
657 def format_fee_rate(self, fee_rate):
658 return format_fee_satoshis(fee_rate/1000, num_zeros=self.num_zeros) + ' sat/byte'
659
660 def get_base_unit(self):
661 return decimal_point_to_base_unit_name(self.decimal_point)
662
663 def set_base_unit(self, unit):
664 assert unit in base_units.keys()
665 self.decimal_point = base_unit_name_to_decimal_point(unit)
666 self.set_key('decimal_point', self.decimal_point, True)
667
668 def get_decimal_point(self):
669 return self.decimal_point
670
671
672 def read_user_config(path):
673 """Parse and store the user config settings in electrum.conf into user_config[]."""
674 if not path:
675 return {}
676 config_path = os.path.join(path, "config")
677 if not os.path.exists(config_path):
678 return {}
679 try:
680 with open(config_path, "r", encoding='utf-8') as f:
681 data = f.read()
682 result = json.loads(data)
683 except:
684 _logger.warning(f"Cannot read config file. {config_path}")
685 return {}
686 if not type(result) is dict:
687 return {}
688 return result