123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888 |
- #!/usr/bin/env python3
- #
- # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
- # Copyright (C)2013-2023 The MMGen Project <mmgen@tuta.io>
- #
- # 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 <http://www.gnu.org/licenses/>.
- """
- 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 = '<mmgen@tuta.io>'
- Cdates = '2013-2023'
- 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
- try:
- from importlib.resources import files # Python 3.9
- 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 = ''
- monero_wallet_rpc_user = 'monero'
- monero_wallet_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 = ''
- # 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_xmr_autosign = False
- test_suite_cfgtest = False
- test_suite_deterministic = False
- test_suite_pexpect = False
- test_suite_popen_spawn = False
- 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'),
- )
- _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_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_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:
- setattr(
- self,
- key,
- conv_type(key, val, getattr(self,key), self._uopt_desc ) if hasattr(self,key) else 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()
|