#!/usr/bin/env python3 # # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution # Copyright (C)2013-2024 The MMGen Project # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ cfg: Configuration classes for the MMGen suite """ import sys,os from collections import namedtuple from .base_obj import Lockable def die(*args,**kwargs): from .util import die die(*args,**kwargs) def die2(exit_val,s): sys.stderr.write(s+'\n') sys.exit(exit_val) class GlobalConstants(Lockable): """ These values are non-configurable. They’re constant for a given machine, user, executable and MMGen release. """ _autolock = True proj_name = 'MMGen' proj_url = 'https://github.com/mmgen/mmgen-wallet' author = 'The MMGen Project' email = '' Cdates = '2013-2024' dfl_hash_preset = '3' passwd_max_tries = 5 min_screen_width = 80 min_time_precision = 18 # must match CoinProtocol.coins core_coins = ('btc','bch','ltc','eth','etc','zec','xmr') prog_name = os.path.basename(sys.argv[0]) is_txprog = prog_name == 'mmgen-regtest' or prog_name.startswith('mmgen-tx') if sys.platform not in ('linux','win32'): die2(1,f'{sys.platform!r}: platform not supported by {proj_name}') if os.getenv('HOME'): # Linux or MSYS2 home_dir = os.getenv('HOME') elif sys.platform == 'win32': # Windows without MSYS2 - not supported die2(1,f'$HOME not set! {proj_name} for Windows must be run in MSYS2 environment') else: die2(2,'$HOME is not set! Unable to determine home directory') def get_mmgen_data_file(self,filename,package='mmgen'): """ this is an expensive import, so do only when required """ # Resource will be unpacked and then cleaned up if necessary, see: # https://docs.python.org/3/library/importlib.html: # Note: This module provides functionality similar to pkg_resources Basic # Resource Access without the performance overhead of that package. # https://importlib-resources.readthedocs.io/en/latest/migration.html # https://setuptools.readthedocs.io/en/latest/pkg_resources.html # TODO: remove try..except (workaround for Python 3.9 pylint bug) try: from importlib.resources import files # Python 3.9 and above except ImportError: from importlib_resources import files return files(package).joinpath('data',filename).read_text() @property def version(self): return self.get_mmgen_data_file( filename = 'version', package = 'mmgen_node_tools' if self.prog_name.startswith('mmnode-') else 'mmgen' ).strip() @property def release_date(self): return self.get_mmgen_data_file(filename='release_date').strip() gc = GlobalConstants() class GlobalVars: """ These are used only by the test suite to redirect msg() and friends to /dev/null """ stdout = sys.stdout stderr = sys.stderr gv = GlobalVars() class Config(Lockable): """ These values are configurable - RHS values are defaults Globals are overridden with the following precedence: 1 - command line 2 - environmental vars 3 - config file """ _autolock = False _set_ok = ('usr_randchars','_proto') _reset_ok = ('accept_defaults','quiet','verbose','yes') _use_class_attr = True _default_to_none = True # general coin = 'BTC' token = '' outdir = '' passwd_file = '' network = 'mainnet' testnet = False regtest = False # verbosity / prompting behavior quiet = False verbose = False yes = False accept_defaults = False no_license = False # limits http_timeout = 60 usr_randchars = 30 fee_adjust = 1.0 fee_estimate_confs = 3 minconf = 1 max_tx_file_size = 100000 max_input_size = 1024 * 1024 min_urandchars = 10 max_urandchars = 80 # debug debug = False debug_daemon = False debug_opts = False debug_rpc = False debug_addrlist = False debug_subseed = False debug_tw = False devtools = False # rpc: rpc_host = '' rpc_port = 0 rpc_user = '' rpc_password = '' aiohttp_rpc_queue_len = 16 cached_balances = False # daemons daemon_data_dir = '' # set by user daemon_id = '' blacklisted_daemons = '' ignore_daemon_version = False # display: test_suite_enable_color = False # placeholder force_256_color = False scroll = False pager = False columns = 0 color = bool( ( sys.stdout.isatty() and not os.getenv('MMGEN_TEST_SUITE_PEXPECT') ) or os.getenv('MMGEN_TEST_SUITE_ENABLE_COLOR') ) # miscellaneous features: use_internal_keccak_module = False force_standalone_scrypt_module = False enable_erigon = False autochg_ignore_labels = False # regtest: bob = False alice = False carol = False regtest_user = '' # Monero: monero_wallet_rpc_user = 'monero' monero_wallet_rpc_password = '' priority = 0 # test suite: bogus_send = False bogus_unspent_data = '' debug_utf8 = False exec_wrapper = False ignore_test_py_exception = False test_suite = False test_suite_autosign_led_simulate = False test_suite_autosign_threaded = False test_suite_xmr_autosign = False test_suite_cfgtest = False test_suite_deterministic = False test_suite_pexpect = False test_suite_popen_spawn = False test_suite_root_pfx = '' hold_protect_disable = False no_daemon_autostart = False names = False no_timings = False exit_after = '' resuming = False skipping_deps = False test_datadir = os.path.join('test','data_dir' + ('','-α')[bool(os.getenv('MMGEN_DEBUG_UTF8'))]) mnemonic_entry_modes = {} # external use: _opts = None _proto = None # internal use: _use_cfg_file = False _use_env = False _forbidden_opts = ( 'data_dir_root', ) _incompatible_opts = ( ('help','longhelp'), ('bob','alice','carol'), ('label','keep_label'), ('tx_id','info'), ('tx_id','terse_info'), ('autosign','outdir'), ) _cfg_file_opts = ( 'autochg_ignore_labels', 'color', 'daemon_data_dir', 'debug', 'fee_adjust', 'force_256_color', 'hash_preset', 'http_timeout', 'max_input_size', 'max_tx_file_size', 'mnemonic_entry_modes', 'monero_wallet_rpc_password', 'monero_wallet_rpc_user', 'no_license', 'quiet', 'regtest', 'rpc_host', 'rpc_password', 'rpc_port', 'rpc_user', 'scroll', 'subseeds', 'testnet', 'usr_randchars', 'bch_max_tx_fee', 'btc_max_tx_fee', 'eth_max_tx_fee', 'ltc_max_tx_fee', 'bch_ignore_daemon_version', 'btc_ignore_daemon_version', 'etc_ignore_daemon_version', 'eth_ignore_daemon_version', 'ltc_ignore_daemon_version', 'xmr_ignore_daemon_version', 'eth_mainnet_chain_names', 'eth_testnet_chain_names' ) # Supported environmental vars # The corresponding attributes (lowercase, without 'mmgen_') must exist in the class. # The 'MMGEN_DISABLE_' prefix sets the corresponding attribute to False. _env_opts = ( 'MMGEN_DEBUG_ALL', # special: there is no `debug_all` attribute 'MMGEN_COLUMNS', 'MMGEN_TEST_SUITE', 'MMGEN_TEST_SUITE_AUTOSIGN_LED_SIMULATE', 'MMGEN_TEST_SUITE_AUTOSIGN_THREADED', 'MMGEN_TEST_SUITE_XMR_AUTOSIGN', 'MMGEN_TEST_SUITE_CFGTEST', 'MMGEN_TEST_SUITE_DETERMINISTIC', 'MMGEN_TEST_SUITE_ENABLE_COLOR', 'MMGEN_TEST_SUITE_PEXPECT', 'MMGEN_TEST_SUITE_POPEN_SPAWN', 'MMGEN_TEST_SUITE_ROOT_PFX', 'MMGEN_BLACKLIST_DAEMONS', 'MMGEN_BOGUS_SEND', 'MMGEN_BOGUS_UNSPENT_DATA', 'MMGEN_DEBUG', 'MMGEN_DEBUG_DAEMON', 'MMGEN_DEBUG_OPTS', 'MMGEN_DEBUG_RPC', 'MMGEN_DEBUG_ADDRLIST', 'MMGEN_DEBUG_TW', 'MMGEN_DEBUG_UTF8', 'MMGEN_DEBUG_SUBSEED', 'MMGEN_DEVTOOLS', 'MMGEN_FORCE_256_COLOR', 'MMGEN_HOLD_PROTECT_DISABLE', 'MMGEN_QUIET', 'MMGEN_NO_LICENSE', 'MMGEN_RPC_HOST', 'MMGEN_RPC_FAIL_ON_COMMAND', 'MMGEN_TESTNET', 'MMGEN_REGTEST', 'MMGEN_EXEC_WRAPPER', 'MMGEN_IGNORE_TEST_PY_EXCEPTION', 'MMGEN_RPC_BACKEND', 'MMGEN_IGNORE_DAEMON_VERSION', 'MMGEN_USE_STANDALONE_SCRYPT_MODULE', 'MMGEN_ENABLE_ERIGON', 'MMGEN_DISABLE_COLOR', ) _infile_opts = ( 'keys_from_file', 'mmgen_keys_from_file', 'passwd_file', 'keysforaddrs', 'comment_file', 'contract_data', ) # Auto-typechecked and auto-set opts - first value in list is the default _ov = namedtuple('autoset_opt_info',['type','choices']) _autoset_opts = { 'fee_estimate_mode': _ov('nocase_pfx', ['conservative','economical']), 'rpc_backend': _ov('nocase_pfx', ['auto','httplib','curl','aiohttp','requests']), } _auto_typeset_opts = { 'seed_len': int, 'subseeds': int, 'vsize_adj': float, } # test suite: err_disp_timeout = 0.7 short_disp_timeout = 0.3 stdin_tty = sys.stdin.isatty() if os.getenv('MMGEN_TEST_SUITE'): min_urandchars = 3 err_disp_timeout = 0.1 short_disp_timeout = 0.1 if os.getenv('MMGEN_TEST_SUITE_POPEN_SPAWN'): stdin_tty = True if gc.prog_name == 'unit_tests.py': _set_ok += ('debug_subseed',) _reset_ok += ('force_standalone_scrypt_module',) if os.getenv('MMGEN_DEBUG_ALL'): for name in _env_opts: if name[:11] == 'MMGEN_DEBUG': os.environ[name] = '1' @property def data_dir_root(self): """ location of mmgen.cfg """ if not hasattr(self,'_data_dir_root'): if self._data_dir_root_override: self._data_dir_root = os.path.normpath(os.path.abspath(self._data_dir_root_override)) elif self.test_suite: self._data_dir_root = self.test_datadir else: self._data_dir_root = os.path.join(gc.home_dir,'.'+gc.proj_name.lower()) return self._data_dir_root @property def data_dir(self): """ location of wallet and other data - same as data_dir_root for mainnet """ if not hasattr(self,'_data_dir'): self._data_dir = os.path.normpath(os.path.join(*{ 'regtest': (self.data_dir_root, 'regtest', self.coin.lower(), (self.regtest_user or 'none') ), 'testnet': (self.data_dir_root, 'testnet'), 'mainnet': (self.data_dir_root,), }[self.network] )) return self._data_dir def __init__( self, cfg = None, opts_data = None, init_opts = None, opt_filter = None, parse_only = False, parsed_opts = None, need_proto = True, need_amt = True, do_post_init = False, process_opts = False ): # Step 1: get user-supplied configuration data from # a) command line, or # b) first argument to constructor; # save to self._uopts: self._cloned = {} if opts_data or parsed_opts or process_opts: assert cfg is None, ( 'Config(): ‘cfg’ cannot be used simultaneously with ' + '‘opts_data’, ‘parsed_opts’ or ‘process_opts’' ) from .opts import UserOpts UserOpts( cfg = self, opts_data = opts_data, init_opts = init_opts, opt_filter = opt_filter, parse_only = parse_only, parsed_opts = parsed_opts ) self._uopt_desc = 'command-line option' else: if cfg is None: self._uopts = {} else: if '_clone' in cfg: assert isinstance( cfg['_clone'], Config ) self._cloned = cfg['_clone'].__dict__ for k,v in self._cloned.items(): if not k.startswith('_'): setattr(self,k,v) del cfg['_clone'] self._uopts = cfg self._uopt_desc = 'configuration option' self._data_dir_root_override = self._cloned.pop( '_data_dir_root_override', self._uopts.pop('data_dir',None)) if parse_only and not any(k in self._uopts for k in ['help','longhelp']): return # Step 2: set cfg from user-supplied data, skipping auto opts; set type from corresponding # class attribute, if it exists: auto_opts = tuple(self._autoset_opts) + tuple(self._auto_typeset_opts) for key,val in self._uopts.items(): assert key.isascii() and key.isidentifier() and key[0] != '_', '{key!r}: malformed configuration option' assert key not in self._forbidden_opts, '{key!r}: forbidden configuration option' if key not in auto_opts: if hasattr(type(self), key): setattr( self, key, getattr(type(self), key) if val is None else conv_type(key, val, getattr(type(self), key), self._uopt_desc)) elif val is None: if hasattr(self, key): delattr(self, key) else: setattr(self, key, val) # Step 3: set cfg from environment, skipping already-set opts; save names set from environment: self._envopts = tuple(self._set_cfg_from_env()) if self._use_env else () from .term import init_term init_term(self) # requires ‘hold_protect_disable’ (set from env) from .fileutil import check_or_create_dir check_or_create_dir(self.data_dir_root) from .util import wrap_ripemd160 wrap_ripemd160() # ripemd160 required by mmgen_cfg_file() in _set_cfg_from_cfg_file() # Step 4: set cfg from cfgfile, skipping already-set opts and auto opts; save set opts and auto # opts to be set: # requires ‘data_dir_root’, ‘test_suite_cfgtest’ self._cfgfile_opts = self._set_cfg_from_cfg_file( self._envopts, need_proto ) # Step 5: set autoset opts from user-supplied data, cfgfile data, or default values, in that order: self._set_autoset_opts( self._cfgfile_opts.autoset ) # Step 6: set auto typeset opts from user-supplied data or cfgfile data, in that order: self._set_auto_typeset_opts( self._cfgfile_opts.auto_typeset ) if self.regtest or self.bob or self.alice or self.carol or gc.prog_name == 'mmgen-regtest': self.network = 'regtest' self.regtest_user = 'bob' if self.bob else 'alice' if self.alice else 'carol' if self.carol else None else: self.network = 'testnet' if self.testnet else 'mainnet' self.coin = self.coin.upper() self.token = self.token.upper() if self.token else None # self.color is finalized, so initialize color: if self.color: # MMGEN_DISABLE_COLOR sets this to False from .color import init_color init_color(num_colors=256 if self.force_256_color else 'auto') self._die_on_incompatible_opts() check_or_create_dir(self.data_dir) if self.debug and gc.prog_name != 'cmdtest.py': self.verbose = True self.quiet = False if self.debug_opts: opt_postproc_debug(self) from .util import Util self._util = Util(self) del self._cloned self._lock() if need_proto: from .protocol import warn_trustlevel,init_proto_from_cfg warn_trustlevel(self) # requires the default-to-none behavior, so do after the lock: self._proto = init_proto_from_cfg(self,need_amt=need_amt) if self._opts and not do_post_init: self._opts.init_bottom(self) # Check user-set opts without modifying them check_opts(self) def _set_cfg_from_env(self): for name,val in ((k,v) for k,v in os.environ.items() if k.startswith('MMGEN_')): if name == 'MMGEN_DEBUG_ALL': continue elif name in self._env_opts: if val: # ignore empty string values; string value of '0' or 'false' sets variable to False disable = name.startswith('MMGEN_DISABLE_') gname = name[(6,14)[disable]:].lower() if gname in self._uopts: # don’t touch attr if already set by user continue elif hasattr(self,gname): setattr( self, gname, conv_type( name, val, getattr(self,gname), 'environment var', invert_bool=disable )) yield gname else: raise ValueError(f'Name {gname!r} not present in globals') else: raise ValueError(f'{name!r} is not a valid MMGen environment variable') def _set_cfg_from_cfg_file( self, env_cfg, need_proto ): _ret = namedtuple('cfgfile_opts',['non_auto','autoset','auto_typeset']) if not self._use_cfg_file: return _ret( (), {}, {} ) # check for changes in system template file (term must be initialized) from .cfgfile import mmgen_cfg_file mmgen_cfg_file(self,'sample') ucfg = mmgen_cfg_file(self,'usr') self._cfgfile_fn = ucfg.fn if need_proto: from .protocol import init_proto autoset_opts = {} auto_typeset_opts = {} non_auto_opts = [] already_set = tuple(self._uopts) + env_cfg for d in ucfg.get_lines(): if d.name in self._cfg_file_opts: ns = d.name.split('_') if ns[0] in gc.core_coins: if not need_proto: continue nse,tn = ( (ns[2:],ns[1]=='testnet') if len(ns) > 2 and ns[1] in ('mainnet','testnet') else (ns[1:],False) ) # no instance yet, so override _class_ attr: cls = init_proto(self, ns[0], tn, need_amt=True, return_cls=True) attr = '_'.join(nse) else: cls = self attr = d.name refval = getattr(cls,attr) val = ucfg.parse_value(d.value,refval) if not val: die( 'CfgFileParseError', f'Parse error in file {ucfg.fn!r}, line {d.lineno}' ) val_conv = conv_type( attr, val, refval, 'configuration file option', src=ucfg.fn ) if not attr in already_set: setattr(cls,attr,val_conv) non_auto_opts.append(attr) elif d.name in self._autoset_opts: autoset_opts[d.name] = d.value elif d.name in self._auto_typeset_opts: auto_typeset_opts[d.name] = d.value else: die( 'CfgFileParseError', f'{d.name!r}: unrecognized option in {ucfg.fn!r}, line {d.lineno}' ) return _ret( tuple(non_auto_opts), autoset_opts, auto_typeset_opts ) def _set_autoset_opts(self,cfgfile_autoset_opts): def get_autoset_opt(key,val,src): def die_on_err(desc): from .util import fmt_list die( 'UserOptError', '{a!r}: invalid {b} (not {c}: {d})'.format( a = val, b = { 'cmdline': f'parameter for option --{key.replace("_","-")}', 'cfgfile': f'value for cfg file option {key!r}' }[src], c = desc, d = fmt_list(data.choices) )) class opt_type: def nocase_str(): if val.lower() in data.choices: return val.lower() else: die_on_err('one of') def nocase_pfx(): cs = [s for s in data.choices if s.startswith(val.lower())] if len(cs) == 1: return cs[0] else: die_on_err('unique substring of') data = self._autoset_opts[key] return getattr(opt_type,data.type)() # Check autoset opts, setting if unset for key in self._autoset_opts: if key in self._cloned: continue assert not hasattr(self,key), f'autoset opt {key!r} is already set, but it shouldn’t be!' if key in self._uopts: val,src = (self._uopts[key],'cmdline') elif key in cfgfile_autoset_opts: val,src = (cfgfile_autoset_opts[key],'cfgfile') else: val = None if val is None: setattr(self, key, self._autoset_opts[key].choices[0]) else: setattr(self, key, get_autoset_opt(key,val,src=src)) def _set_auto_typeset_opts(self,cfgfile_auto_typeset_opts): def do_set(key,val,ref_type): assert not hasattr(self,key), f'{key!r} is in cfg!' setattr(self,key,None if val is None else ref_type(val)) for key,ref_type in self._auto_typeset_opts.items(): if key in self._uopts: do_set(key, self._uopts[key], ref_type) elif key in cfgfile_auto_typeset_opts: do_set(key, cfgfile_auto_typeset_opts[key], ref_type) def _post_init(self): return self._opts.init_bottom(self) def _die_on_incompatible_opts(self): for group in self._incompatible_opts: bad = [k for k in self.__dict__ if k in group and getattr(self,k) is not None] if len(bad) > 1: die(1,'Conflicting options: {}'.format(', '.join(map(fmt_opt,bad)))) def check_opts(cfg): # Raises exception if any check fails from .util import is_int,Msg def get_desc(desc_pfx=''): return ( (desc_pfx + ' ' if desc_pfx else '') + ( f'parameter for command-line option {fmt_opt(name)!r}' if name in cfg._uopts and 'command-line' in cfg._uopt_desc else f'value for configuration option {name!r}' ) + ( ' from environment' if name in cfg._envopts else '') + (f' in {cfg._cfgfile_fn!r}' if name in cfg._cfgfile_opts.non_auto else '') ) def display_opt(name,val='',beg='For selected',end=':\n'): from .util import msg_r msg_r('{} option {!r}{}'.format( beg, f'{fmt_opt(name)}={val}' if val else fmt_opt(name), end )) def opt_compares(val,op_str,target): import operator if not { '<': operator.lt, '<=': operator.le, '>': operator.gt, '>=': operator.ge, '=': operator.eq, }[op_str](val,target): die( 'UserOptError', f'{val}: invalid {get_desc()} (not {op_str} {target})' ) def opt_is_int(val,desc_pfx=''): if not is_int(val): die( 'UserOptError', f'{val!r}: invalid {get_desc(desc_pfx)} (not an integer)' ) def opt_is_in_list(val,tlist,desc_pfx=''): if val not in tlist: q,sep = (('',','),("'","','"))[isinstance(tlist[0],str)] die( 'UserOptError', '{q}{v}{q}: invalid {w}\nValid choices: {q}{o}{q}'.format( v = val, w = get_desc(desc_pfx), q = q, o = sep.join(map(str,sorted(tlist))) )) def opt_unrecognized(): die( 'UserOptError', f'{val!r}: unrecognized {get_desc()}' ) class check_funcs: def in_fmt(): from .wallet import get_wallet_data wd = get_wallet_data(fmt_code=val) if not wd: opt_unrecognized() if name == 'out_fmt': p = 'hidden_incog_output_params' if wd.type == 'incog_hidden' and not getattr(cfg,p): die( 'UserOptError', 'Hidden incog format output requested. ' + f'You must supply a file and offset with the {fmt_opt(p)!r} option' ) if wd.base_type == 'incog_base' and cfg.old_incog_fmt: display_opt(name,val,beg='Selected',end=' ') display_opt('old_incog_fmt',beg='conflicts with',end=':\n') die( 'UserOptError', 'Export to old incog wallet format unsupported' ) elif wd.type == 'brain': die( 'UserOptError', 'Output to brainwallet format unsupported' ) out_fmt = in_fmt def hidden_incog_input_params(): a = val.rsplit(',',1) # permit comma in filename if len(a) != 2: display_opt(name,val) die( 'UserOptError', 'Option requires two comma-separated arguments' ) fn,offset = a opt_is_int(offset) from .fileutil import check_infile,check_outdir,check_outfile if name == 'hidden_incog_input_params': check_infile(fn,blkdev_ok=True) key2 = 'in_fmt' else: try: os.stat(fn) except: b = os.path.dirname(fn) if b: check_outdir(b) else: check_outfile(fn,blkdev_ok=True) key2 = 'out_fmt' if hasattr(cfg,key2): val2 = getattr(cfg,key2) from .wallet import get_wallet_data wd = get_wallet_data('incog_hidden') if val2 and val2 not in wd.fmt_codes: die( 'UserOptError', f'Option conflict:\n {fmt_opt(name)}, with\n {fmt_opt(key2)}={val2}' ) hidden_incog_output_params = hidden_incog_input_params def subseeds(): from .subseed import SubSeedIdxRange opt_compares(val,'>=',SubSeedIdxRange.min_idx) opt_compares(val,'<=',SubSeedIdxRange.max_idx) def seed_len(): from .seed import Seed opt_is_in_list(int(val),Seed.lens) def hash_preset(): from .crypto import Crypto opt_is_in_list(val,list(Crypto.hash_presets.keys())) def brain_params(): a = val.split(',') if len(a) != 2: display_opt(name,val) die( 'UserOptError', 'Option requires two comma-separated arguments' ) opt_is_int( a[0], desc_pfx='seed length' ) from .seed import Seed opt_is_in_list( int(a[0]), Seed.lens, desc_pfx='seed length' ) from .crypto import Crypto opt_is_in_list( a[1], list(Crypto.hash_presets.keys()), desc_pfx='hash preset' ) def usr_randchars(): if val != 0: opt_compares(val,'>=',cfg.min_urandchars) opt_compares(val,'<=',cfg.max_urandchars) def tx_confs(): opt_is_int(val) opt_compares(int(val),'>=',1) def vsize_adj(): from .util import ymsg ymsg(f'Adjusting transaction vsize by a factor of {val:1.2f}') def daemon_id(): from .daemon import CoinDaemon opt_is_in_list(val,CoinDaemon.all_daemon_ids()) def locktime(): opt_is_int(val) opt_compares(int(val),'>',0) def columns(): opt_compares(val,'>',10) # TODO: add checks for token, rbf, tx_fee check_funcs_names = tuple(check_funcs.__dict__) for name in tuple(cfg._uopts) + cfg._envopts + cfg._cfgfile_opts.non_auto: val = getattr(cfg,name) if name in cfg._infile_opts: from .fileutil import check_infile check_infile(val) # file exists and is readable - dies on error elif name == 'outdir': from .fileutil import check_outdir check_outdir(val) # dies on error elif name in check_funcs_names: getattr(check_funcs,name)() elif cfg.debug: Msg(f'check_opts(): No test for config opt {name!r}') def fmt_opt(o): return '--' + o.replace('_','-') def opt_postproc_debug(cfg): none_opts = [k for k in dir(cfg) if k[:2] != '__' and getattr(cfg,k) is None] from .util import Msg Msg('\n Configuration opts:') for e in [d for d in dir(cfg) if d[:2] != '__']: Msg(f' {e:<20}: {getattr(cfg,e)}') Msg(" Configuration opts set to 'None':") Msg(' {}\n'.format('\n '.join(none_opts))) Msg('\n=== end opts.py debug ===\n') def conv_type( name, val, refval, desc, invert_bool = False, src = None ): def do_fail(): die(1,'{a!r}: invalid value for {b} {c!r}{d} (must be of type {e!r})'.format( a = val, b = desc, c = fmt_opt(name) if 'command-line' in desc else name, d = f' in {src!r}' if src else '', e = type(refval).__name__ )) if type(refval) is bool: v = str(val).lower() ret = ( True if v in ('true','yes','1','on') else False if v in ('false','no','none','0','off','') else None ) return do_fail() if ret is None else (not ret) if invert_bool else ret else: try: return type(refval)(not val if invert_bool else val) except: do_fail()