cfg.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. cfg: Configuration classes for the MMGen suite
  20. """
  21. import sys,os
  22. from collections import namedtuple
  23. from .base_obj import Lockable
  24. def die(*args,**kwargs):
  25. from .util import die
  26. die(*args,**kwargs)
  27. def die2(exit_val,s):
  28. sys.stderr.write(s+'\n')
  29. sys.exit(exit_val)
  30. class GlobalConstants(Lockable):
  31. """
  32. These values are non-configurable. They’re constant for a given machine,
  33. user, executable and MMGen release.
  34. """
  35. _autolock = True
  36. proj_name = 'MMGen'
  37. proj_url = 'https://github.com/mmgen/mmgen-wallet'
  38. author = 'The MMGen Project'
  39. email = '<mmgen@tuta.io>'
  40. Cdates = '2013-2024'
  41. dfl_hash_preset = '3'
  42. passwd_max_tries = 5
  43. min_screen_width = 80
  44. min_time_precision = 18
  45. # must match CoinProtocol.coins
  46. core_coins = ('btc','bch','ltc','eth','etc','zec','xmr')
  47. prog_name = os.path.basename(sys.argv[0])
  48. is_txprog = prog_name == 'mmgen-regtest' or prog_name.startswith('mmgen-tx')
  49. if sys.platform not in ('linux', 'win32', 'darwin'):
  50. die2(1,f'{sys.platform!r}: platform not supported by {proj_name}')
  51. if os.getenv('HOME'): # Linux, MSYS2, or macOS
  52. home_dir = os.getenv('HOME')
  53. elif sys.platform == 'win32': # Windows without MSYS2 - not supported
  54. die2(1,f'$HOME not set! {proj_name} for Windows must be run in MSYS2 environment')
  55. else:
  56. die2(2,'$HOME is not set! Unable to determine home directory')
  57. def get_mmgen_data_file(self,filename,package='mmgen'):
  58. """
  59. this is an expensive import, so do only when required
  60. """
  61. # Resource will be unpacked and then cleaned up if necessary, see:
  62. # https://docs.python.org/3/library/importlib.html:
  63. # Note: This module provides functionality similar to pkg_resources Basic
  64. # Resource Access without the performance overhead of that package.
  65. # https://importlib-resources.readthedocs.io/en/latest/migration.html
  66. # https://setuptools.readthedocs.io/en/latest/pkg_resources.html
  67. # TODO: remove try..except (workaround for Python 3.9 pylint bug)
  68. try:
  69. from importlib.resources import files # Python 3.9 and above
  70. except ImportError:
  71. from importlib_resources import files
  72. return files(package).joinpath('data',filename).read_text()
  73. @property
  74. def version(self):
  75. return self.get_mmgen_data_file(
  76. filename = 'version',
  77. package = 'mmgen_node_tools' if self.prog_name.startswith('mmnode-') else 'mmgen'
  78. ).strip()
  79. @property
  80. def release_date(self):
  81. return self.get_mmgen_data_file(filename='release_date').strip()
  82. gc = GlobalConstants()
  83. class GlobalVars:
  84. """
  85. These are used only by the test suite to redirect msg() and friends to /dev/null
  86. """
  87. stdout = sys.stdout
  88. stderr = sys.stderr
  89. gv = GlobalVars()
  90. class Config(Lockable):
  91. """
  92. These values are configurable - RHS values are defaults
  93. Globals are overridden with the following precedence:
  94. 1 - command line
  95. 2 - environmental vars
  96. 3 - config file
  97. """
  98. _autolock = False
  99. _set_ok = ('usr_randchars','_proto')
  100. _reset_ok = ('accept_defaults',)
  101. _use_class_attr = True
  102. _default_to_none = True
  103. # general
  104. coin = 'BTC'
  105. token = ''
  106. outdir = ''
  107. passwd_file = ''
  108. network = 'mainnet'
  109. testnet = False
  110. regtest = False
  111. # verbosity / prompting behavior
  112. quiet = False
  113. verbose = False
  114. yes = False
  115. accept_defaults = False
  116. no_license = False
  117. # limits
  118. http_timeout = 60
  119. usr_randchars = 30
  120. fee_adjust = 1.0
  121. fee_estimate_confs = 3
  122. minconf = 1
  123. max_tx_file_size = 100000
  124. max_input_size = 1024 * 1024
  125. min_urandchars = 10
  126. max_urandchars = 80
  127. macos_autosign_ramdisk_size = 10 # see MacOSRamDisk
  128. # debug
  129. debug = False
  130. debug_daemon = False
  131. debug_opts = False
  132. debug_rpc = False
  133. debug_addrlist = False
  134. debug_subseed = False
  135. debug_tw = False
  136. devtools = False
  137. traceback = False
  138. # rpc:
  139. rpc_host = ''
  140. rpc_port = 0
  141. rpc_user = ''
  142. rpc_password = ''
  143. aiohttp_rpc_queue_len = 16
  144. cached_balances = False
  145. # daemons
  146. daemon_data_dir = '' # set by user
  147. daemon_id = ''
  148. blacklisted_daemons = ''
  149. ignore_daemon_version = False
  150. # display:
  151. test_suite_enable_color = False # placeholder
  152. force_256_color = False
  153. scroll = False
  154. pager = False
  155. columns = 0
  156. color = bool(
  157. ( sys.stdout.isatty() and not os.getenv('MMGEN_TEST_SUITE_PEXPECT') ) or
  158. os.getenv('MMGEN_TEST_SUITE_ENABLE_COLOR')
  159. )
  160. # miscellaneous features:
  161. use_internal_keccak_module = False
  162. force_standalone_scrypt_module = False
  163. enable_erigon = False
  164. autochg_ignore_labels = False
  165. # regtest:
  166. bob = False
  167. alice = False
  168. carol = False
  169. regtest_user = ''
  170. # Monero:
  171. monero_wallet_rpc_user = 'monero'
  172. monero_wallet_rpc_password = ''
  173. priority = 0
  174. # test suite:
  175. bogus_send = False
  176. bogus_unspent_data = ''
  177. debug_utf8 = False
  178. exec_wrapper = False
  179. ignore_test_py_exception = False
  180. test_suite = False
  181. test_suite_autosign_led_simulate = False
  182. test_suite_autosign_threaded = False
  183. test_suite_xmr_autosign = False
  184. test_suite_cfgtest = False
  185. test_suite_deterministic = False
  186. test_suite_pexpect = False
  187. test_suite_popen_spawn = False
  188. test_suite_root_pfx = ''
  189. hold_protect_disable = False
  190. no_daemon_autostart = False
  191. names = False
  192. no_timings = False
  193. exit_after = ''
  194. resuming = False
  195. skipping_deps = False
  196. test_datadir = os.path.join('test','data_dir' + ('','-α')[bool(os.getenv('MMGEN_DEBUG_UTF8'))])
  197. mnemonic_entry_modes = {}
  198. # external use:
  199. _opts = None
  200. _proto = None
  201. # internal use:
  202. _use_cfg_file = False
  203. _use_env = False
  204. _forbidden_opts = (
  205. 'data_dir_root',
  206. )
  207. _incompatible_opts = (
  208. ('help','longhelp'),
  209. ('bob','alice','carol'),
  210. ('label','keep_label'),
  211. ('tx_id','info'),
  212. ('tx_id','terse_info'),
  213. ('autosign','outdir'),
  214. )
  215. _cfg_file_opts = (
  216. 'autochg_ignore_labels',
  217. 'color',
  218. 'daemon_data_dir',
  219. 'debug',
  220. 'fee_adjust',
  221. 'force_256_color',
  222. 'hash_preset',
  223. 'http_timeout',
  224. 'macos_autosign_ramdisk_size',
  225. 'max_input_size',
  226. 'max_tx_file_size',
  227. 'mnemonic_entry_modes',
  228. 'monero_wallet_rpc_password',
  229. 'monero_wallet_rpc_user',
  230. 'no_license',
  231. 'quiet',
  232. 'regtest',
  233. 'rpc_host',
  234. 'rpc_password',
  235. 'rpc_port',
  236. 'rpc_user',
  237. 'scroll',
  238. 'subseeds',
  239. 'testnet',
  240. 'usr_randchars',
  241. 'bch_max_tx_fee',
  242. 'btc_max_tx_fee',
  243. 'eth_max_tx_fee',
  244. 'ltc_max_tx_fee',
  245. 'bch_ignore_daemon_version',
  246. 'btc_ignore_daemon_version',
  247. 'etc_ignore_daemon_version',
  248. 'eth_ignore_daemon_version',
  249. 'ltc_ignore_daemon_version',
  250. 'xmr_ignore_daemon_version',
  251. 'eth_mainnet_chain_names',
  252. 'eth_testnet_chain_names' )
  253. # Supported environmental vars
  254. # The corresponding attributes (lowercase, without 'mmgen_') must exist in the class.
  255. # The 'MMGEN_DISABLE_' prefix sets the corresponding attribute to False.
  256. _env_opts = (
  257. 'MMGEN_DEBUG_ALL', # special: there is no `debug_all` attribute
  258. 'MMGEN_COLUMNS',
  259. 'MMGEN_TEST_SUITE',
  260. 'MMGEN_TEST_SUITE_AUTOSIGN_LED_SIMULATE',
  261. 'MMGEN_TEST_SUITE_AUTOSIGN_THREADED',
  262. 'MMGEN_TEST_SUITE_XMR_AUTOSIGN',
  263. 'MMGEN_TEST_SUITE_CFGTEST',
  264. 'MMGEN_TEST_SUITE_DETERMINISTIC',
  265. 'MMGEN_TEST_SUITE_ENABLE_COLOR',
  266. 'MMGEN_TEST_SUITE_PEXPECT',
  267. 'MMGEN_TEST_SUITE_POPEN_SPAWN',
  268. 'MMGEN_TEST_SUITE_ROOT_PFX',
  269. 'MMGEN_TRACEBACK',
  270. 'MMGEN_BLACKLIST_DAEMONS',
  271. 'MMGEN_BOGUS_SEND',
  272. 'MMGEN_BOGUS_UNSPENT_DATA',
  273. 'MMGEN_DEBUG',
  274. 'MMGEN_DEBUG_DAEMON',
  275. 'MMGEN_DEBUG_OPTS',
  276. 'MMGEN_DEBUG_RPC',
  277. 'MMGEN_DEBUG_ADDRLIST',
  278. 'MMGEN_DEBUG_TW',
  279. 'MMGEN_DEBUG_UTF8',
  280. 'MMGEN_DEBUG_SUBSEED',
  281. 'MMGEN_DEVTOOLS',
  282. 'MMGEN_FORCE_256_COLOR',
  283. 'MMGEN_HOLD_PROTECT_DISABLE',
  284. 'MMGEN_HTTP_TIMEOUT',
  285. 'MMGEN_QUIET',
  286. 'MMGEN_NO_LICENSE',
  287. 'MMGEN_RPC_HOST',
  288. 'MMGEN_RPC_FAIL_ON_COMMAND',
  289. 'MMGEN_TESTNET',
  290. 'MMGEN_REGTEST',
  291. 'MMGEN_EXEC_WRAPPER',
  292. 'MMGEN_IGNORE_TEST_PY_EXCEPTION',
  293. 'MMGEN_RPC_BACKEND',
  294. 'MMGEN_IGNORE_DAEMON_VERSION',
  295. 'MMGEN_USE_STANDALONE_SCRYPT_MODULE',
  296. 'MMGEN_ENABLE_ERIGON',
  297. 'MMGEN_DISABLE_COLOR',
  298. )
  299. _infile_opts = (
  300. 'keys_from_file',
  301. 'mmgen_keys_from_file',
  302. 'passwd_file',
  303. 'keysforaddrs',
  304. 'comment_file',
  305. 'contract_data',
  306. )
  307. # Auto-typechecked and auto-set opts - first value in list is the default
  308. _ov = namedtuple('autoset_opt_info',['type','choices'])
  309. _autoset_opts = {
  310. 'fee_estimate_mode': _ov('nocase_pfx', ['conservative','economical']),
  311. 'rpc_backend': _ov('nocase_pfx', ['auto','httplib','curl','aiohttp','requests']),
  312. }
  313. _auto_typeset_opts = {
  314. 'seed_len': int,
  315. 'subseeds': int,
  316. 'vsize_adj': float,
  317. }
  318. # test suite:
  319. err_disp_timeout = 0.7
  320. short_disp_timeout = 0.3
  321. stdin_tty = sys.stdin.isatty()
  322. if os.getenv('MMGEN_TEST_SUITE'):
  323. min_urandchars = 3
  324. err_disp_timeout = 0.1
  325. short_disp_timeout = 0.1
  326. if os.getenv('MMGEN_TEST_SUITE_POPEN_SPAWN'):
  327. stdin_tty = True
  328. if gc.prog_name == 'unit_tests.py':
  329. _set_ok += ('debug_subseed',)
  330. _reset_ok += ('force_standalone_scrypt_module',)
  331. if os.getenv('MMGEN_DEBUG_ALL'):
  332. for name in _env_opts:
  333. if name[:11] == 'MMGEN_DEBUG':
  334. os.environ[name] = '1'
  335. @property
  336. def data_dir_root(self):
  337. """
  338. location of mmgen.cfg
  339. """
  340. if not hasattr(self,'_data_dir_root'):
  341. if self._data_dir_root_override:
  342. self._data_dir_root = os.path.normpath(os.path.abspath(self._data_dir_root_override))
  343. elif self.test_suite:
  344. self._data_dir_root = self.test_datadir
  345. else:
  346. self._data_dir_root = os.path.join(gc.home_dir,'.'+gc.proj_name.lower())
  347. return self._data_dir_root
  348. @property
  349. def data_dir(self):
  350. """
  351. location of wallet and other data - same as data_dir_root for mainnet
  352. """
  353. if not hasattr(self,'_data_dir'):
  354. self._data_dir = os.path.normpath(os.path.join(*{
  355. 'regtest': (self.data_dir_root, 'regtest', self.coin.lower(), (self.regtest_user or 'none') ),
  356. 'testnet': (self.data_dir_root, 'testnet'),
  357. 'mainnet': (self.data_dir_root,),
  358. }[self.network] ))
  359. return self._data_dir
  360. def __init__(
  361. self,
  362. cfg = None,
  363. opts_data = None,
  364. init_opts = None,
  365. opt_filter = None,
  366. parse_only = False,
  367. parsed_opts = None,
  368. need_proto = True,
  369. need_amt = True,
  370. do_post_init = False,
  371. process_opts = False ):
  372. # Step 1: get user-supplied configuration data from
  373. # a) command line, or
  374. # b) first argument to constructor;
  375. # save to self._uopts:
  376. self._cloned = {}
  377. if opts_data or parsed_opts or process_opts:
  378. assert cfg is None, (
  379. 'Config(): ‘cfg’ cannot be used simultaneously with ' +
  380. '‘opts_data’, ‘parsed_opts’ or ‘process_opts’' )
  381. from .opts import UserOpts
  382. UserOpts(
  383. cfg = self,
  384. opts_data = opts_data,
  385. init_opts = init_opts,
  386. opt_filter = opt_filter,
  387. parse_only = parse_only,
  388. parsed_opts = parsed_opts )
  389. self._uopt_desc = 'command-line option'
  390. else:
  391. if cfg is None:
  392. self._uopts = {}
  393. else:
  394. if '_clone' in cfg:
  395. assert isinstance( cfg['_clone'], Config )
  396. self._cloned = cfg['_clone'].__dict__
  397. for k,v in self._cloned.items():
  398. if not k.startswith('_'):
  399. setattr(self,k,v)
  400. del cfg['_clone']
  401. self._uopts = cfg
  402. self._uopt_desc = 'configuration option'
  403. self._data_dir_root_override = self._cloned.pop(
  404. '_data_dir_root_override',
  405. self._uopts.pop('data_dir',None))
  406. if parse_only and not any(k in self._uopts for k in ['help','longhelp']):
  407. return
  408. # Step 2: set cfg from user-supplied data, skipping auto opts; set type from corresponding
  409. # class attribute, if it exists:
  410. auto_opts = tuple(self._autoset_opts) + tuple(self._auto_typeset_opts)
  411. for key,val in self._uopts.items():
  412. assert key.isascii() and key.isidentifier() and key[0] != '_', '{key!r}: malformed configuration option'
  413. assert key not in self._forbidden_opts, '{key!r}: forbidden configuration option'
  414. if key not in auto_opts:
  415. if hasattr(type(self), key):
  416. setattr(
  417. self,
  418. key,
  419. getattr(type(self), key) if val is None else
  420. conv_type(key, val, getattr(type(self), key), self._uopt_desc))
  421. elif val is None:
  422. if hasattr(self, key):
  423. delattr(self, key)
  424. else:
  425. setattr(self, key, val)
  426. # Step 3: set cfg from environment, skipping already-set opts; save names set from environment:
  427. self._envopts = tuple(self._set_cfg_from_env()) if self._use_env else ()
  428. from .term import init_term
  429. init_term(self) # requires ‘hold_protect_disable’ (set from env)
  430. from .fileutil import check_or_create_dir
  431. check_or_create_dir(self.data_dir_root)
  432. from .util import wrap_ripemd160
  433. wrap_ripemd160() # ripemd160 required by mmgen_cfg_file() in _set_cfg_from_cfg_file()
  434. # Step 4: set cfg from cfgfile, skipping already-set opts and auto opts; save set opts and auto
  435. # opts to be set:
  436. # requires ‘data_dir_root’, ‘test_suite_cfgtest’
  437. self._cfgfile_opts = self._set_cfg_from_cfg_file( self._envopts, need_proto )
  438. # Step 5: set autoset opts from user-supplied data, cfgfile data, or default values, in that order:
  439. self._set_autoset_opts( self._cfgfile_opts.autoset )
  440. # Step 6: set auto typeset opts from user-supplied data or cfgfile data, in that order:
  441. self._set_auto_typeset_opts( self._cfgfile_opts.auto_typeset )
  442. if self.regtest or self.bob or self.alice or self.carol or gc.prog_name == 'mmgen-regtest':
  443. self.network = 'regtest'
  444. self.regtest_user = 'bob' if self.bob else 'alice' if self.alice else 'carol' if self.carol else None
  445. else:
  446. self.network = 'testnet' if self.testnet else 'mainnet'
  447. self.coin = self.coin.upper()
  448. self.token = self.token.upper() if self.token else None
  449. # self.color is finalized, so initialize color:
  450. if self.color: # MMGEN_DISABLE_COLOR sets this to False
  451. from .color import init_color
  452. init_color(num_colors=256 if self.force_256_color else 'auto')
  453. self._die_on_incompatible_opts()
  454. check_or_create_dir(self.data_dir)
  455. if self.debug and gc.prog_name != 'cmdtest.py':
  456. self.verbose = True
  457. self.quiet = False
  458. if self.debug_opts:
  459. opt_postproc_debug(self)
  460. from .util import Util
  461. self._util = Util(self)
  462. del self._cloned
  463. self._lock()
  464. if need_proto:
  465. from .protocol import warn_trustlevel,init_proto_from_cfg
  466. warn_trustlevel(self)
  467. # requires the default-to-none behavior, so do after the lock:
  468. self._proto = init_proto_from_cfg(self,need_amt=need_amt)
  469. if self._opts and not do_post_init:
  470. self._opts.init_bottom(self)
  471. # Check user-set opts without modifying them
  472. check_opts(self)
  473. def _set_cfg_from_env(self):
  474. for name,val in ((k,v) for k,v in os.environ.items() if k.startswith('MMGEN_')):
  475. if name == 'MMGEN_DEBUG_ALL':
  476. continue
  477. if name in self._env_opts:
  478. if val: # ignore empty string values; string value of '0' or 'false' sets variable to False
  479. disable = name.startswith('MMGEN_DISABLE_')
  480. gname = name[(6,14)[disable]:].lower()
  481. if gname in self._uopts: # don’t touch attr if already set by user
  482. continue
  483. if hasattr(self,gname):
  484. setattr(
  485. self,
  486. gname,
  487. conv_type( name, val, getattr(self,gname), 'environment var', invert_bool=disable ))
  488. yield gname
  489. else:
  490. raise ValueError(f'Name {gname!r} not present in globals')
  491. else:
  492. raise ValueError(f'{name!r} is not a valid MMGen environment variable')
  493. def _set_cfg_from_cfg_file(
  494. self,
  495. env_cfg,
  496. need_proto ):
  497. _ret = namedtuple('cfgfile_opts',['non_auto','autoset','auto_typeset'])
  498. if not self._use_cfg_file:
  499. return _ret( (), {}, {} )
  500. # check for changes in system template file (term must be initialized)
  501. from .cfgfile import mmgen_cfg_file
  502. mmgen_cfg_file(self,'sample')
  503. ucfg = mmgen_cfg_file(self,'usr')
  504. self._cfgfile_fn = ucfg.fn
  505. if need_proto:
  506. from .protocol import init_proto
  507. autoset_opts = {}
  508. auto_typeset_opts = {}
  509. non_auto_opts = []
  510. already_set = tuple(self._uopts) + env_cfg
  511. for d in ucfg.get_lines():
  512. if d.name in self._cfg_file_opts:
  513. ns = d.name.split('_')
  514. if ns[0] in gc.core_coins:
  515. if not need_proto:
  516. continue
  517. nse,tn = (
  518. (ns[2:],ns[1]=='testnet') if len(ns) > 2 and ns[1] in ('mainnet','testnet') else
  519. (ns[1:],False)
  520. )
  521. # no instance yet, so override _class_ attr:
  522. cls = init_proto(self, ns[0], tn, need_amt=True, return_cls=True)
  523. attr = '_'.join(nse)
  524. else:
  525. cls = self
  526. attr = d.name
  527. refval = getattr(cls,attr)
  528. val = ucfg.parse_value(d.value,refval)
  529. if not val:
  530. die( 'CfgFileParseError', f'Parse error in file {ucfg.fn!r}, line {d.lineno}' )
  531. val_conv = conv_type( attr, val, refval, 'configuration file option', src=ucfg.fn )
  532. if not attr in already_set:
  533. setattr(cls,attr,val_conv)
  534. non_auto_opts.append(attr)
  535. elif d.name in self._autoset_opts:
  536. autoset_opts[d.name] = d.value
  537. elif d.name in self._auto_typeset_opts:
  538. auto_typeset_opts[d.name] = d.value
  539. else:
  540. die( 'CfgFileParseError', f'{d.name!r}: unrecognized option in {ucfg.fn!r}, line {d.lineno}' )
  541. return _ret( tuple(non_auto_opts), autoset_opts, auto_typeset_opts )
  542. def _set_autoset_opts(self,cfgfile_autoset_opts):
  543. def get_autoset_opt(key,val,src):
  544. def die_on_err(desc):
  545. from .util import fmt_list
  546. die(
  547. 'UserOptError',
  548. '{a!r}: invalid {b} (not {c}: {d})'.format(
  549. a = val,
  550. b = {
  551. 'cmdline': f'parameter for option --{key.replace("_","-")}',
  552. 'cfgfile': f'value for cfg file option {key!r}'
  553. }[src],
  554. c = desc,
  555. d = fmt_list(data.choices) ))
  556. class opt_type:
  557. def nocase_str():
  558. if val.lower() in data.choices:
  559. return val.lower()
  560. else:
  561. die_on_err('one of')
  562. def nocase_pfx():
  563. cs = [s for s in data.choices if s.startswith(val.lower())]
  564. if len(cs) == 1:
  565. return cs[0]
  566. else:
  567. die_on_err('unique substring of')
  568. data = self._autoset_opts[key]
  569. return getattr(opt_type,data.type)()
  570. # Check autoset opts, setting if unset
  571. for key in self._autoset_opts:
  572. if key in self._cloned:
  573. continue
  574. assert not hasattr(self,key), f'autoset opt {key!r} is already set, but it shouldn’t be!'
  575. if key in self._uopts:
  576. val,src = (self._uopts[key],'cmdline')
  577. elif key in cfgfile_autoset_opts:
  578. val,src = (cfgfile_autoset_opts[key],'cfgfile')
  579. else:
  580. val = None
  581. if val is None:
  582. setattr(self, key, self._autoset_opts[key].choices[0])
  583. else:
  584. setattr(self, key, get_autoset_opt(key,val,src=src))
  585. def _set_auto_typeset_opts(self,cfgfile_auto_typeset_opts):
  586. def do_set(key,val,ref_type):
  587. assert not hasattr(self,key), f'{key!r} is in cfg!'
  588. setattr(self,key,None if val is None else ref_type(val))
  589. for key,ref_type in self._auto_typeset_opts.items():
  590. if key in self._uopts:
  591. do_set(key, self._uopts[key], ref_type)
  592. elif key in cfgfile_auto_typeset_opts:
  593. do_set(key, cfgfile_auto_typeset_opts[key], ref_type)
  594. def _post_init(self):
  595. return self._opts.init_bottom(self)
  596. def _die_on_incompatible_opts(self):
  597. for group in self._incompatible_opts:
  598. bad = [k for k in self.__dict__ if k in group and getattr(self,k) is not None]
  599. if len(bad) > 1:
  600. die(1,'Conflicting options: {}'.format(', '.join(map(fmt_opt,bad))))
  601. def _set_quiet(self,val):
  602. from .util import Util
  603. self.__dict__['quiet'] = val
  604. self.__dict__['_util'] = Util(self) # qmsg, qmsg_r
  605. def check_opts(cfg): # Raises exception if any check fails
  606. from .util import is_int,Msg
  607. def get_desc(desc_pfx=''):
  608. return (
  609. (desc_pfx + ' ' if desc_pfx else '')
  610. + (
  611. f'parameter for command-line option {fmt_opt(name)!r}'
  612. if name in cfg._uopts and 'command-line' in cfg._uopt_desc else
  613. f'value for configuration option {name!r}'
  614. )
  615. + ( ' from environment' if name in cfg._envopts else '')
  616. + (f' in {cfg._cfgfile_fn!r}' if name in cfg._cfgfile_opts.non_auto else '')
  617. )
  618. def display_opt(name,val='',beg='For selected',end=':\n'):
  619. from .util import msg_r
  620. msg_r('{} option {!r}{}'.format(
  621. beg,
  622. f'{fmt_opt(name)}={val}' if val else fmt_opt(name),
  623. end ))
  624. def opt_compares(val,op_str,target):
  625. import operator
  626. if not {
  627. '<': operator.lt,
  628. '<=': operator.le,
  629. '>': operator.gt,
  630. '>=': operator.ge,
  631. '=': operator.eq,
  632. }[op_str](val,target):
  633. die( 'UserOptError', f'{val}: invalid {get_desc()} (not {op_str} {target})' )
  634. def opt_is_int(val,desc_pfx=''):
  635. if not is_int(val):
  636. die( 'UserOptError', f'{val!r}: invalid {get_desc(desc_pfx)} (not an integer)' )
  637. def opt_is_in_list(val,tlist,desc_pfx=''):
  638. if val not in tlist:
  639. q,sep = (('',','),("'","','"))[isinstance(tlist[0],str)]
  640. die( 'UserOptError', '{q}{v}{q}: invalid {w}\nValid choices: {q}{o}{q}'.format(
  641. v = val,
  642. w = get_desc(desc_pfx),
  643. q = q,
  644. o = sep.join(map(str,sorted(tlist))) ))
  645. def opt_unrecognized():
  646. die( 'UserOptError', f'{val!r}: unrecognized {get_desc()}' )
  647. class check_funcs:
  648. def in_fmt():
  649. from .wallet import get_wallet_data
  650. wd = get_wallet_data(fmt_code=val)
  651. if not wd:
  652. opt_unrecognized()
  653. if name == 'out_fmt':
  654. p = 'hidden_incog_output_params'
  655. if wd.type == 'incog_hidden' and not getattr(cfg,p):
  656. die( 'UserOptError',
  657. 'Hidden incog format output requested. ' +
  658. f'You must supply a file and offset with the {fmt_opt(p)!r} option' )
  659. if wd.base_type == 'incog_base' and cfg.old_incog_fmt:
  660. display_opt(name,val,beg='Selected',end=' ')
  661. display_opt('old_incog_fmt',beg='conflicts with',end=':\n')
  662. die( 'UserOptError', 'Export to old incog wallet format unsupported' )
  663. elif wd.type == 'brain':
  664. die( 'UserOptError', 'Output to brainwallet format unsupported' )
  665. out_fmt = in_fmt
  666. def hidden_incog_input_params():
  667. a = val.rsplit(',',1) # permit comma in filename
  668. if len(a) != 2:
  669. display_opt(name,val)
  670. die( 'UserOptError', 'Option requires two comma-separated arguments' )
  671. fn,offset = a
  672. opt_is_int(offset)
  673. from .fileutil import check_infile,check_outdir,check_outfile
  674. if name == 'hidden_incog_input_params':
  675. check_infile(fn,blkdev_ok=True)
  676. key2 = 'in_fmt'
  677. else:
  678. try:
  679. os.stat(fn)
  680. except:
  681. b = os.path.dirname(fn)
  682. if b:
  683. check_outdir(b)
  684. else:
  685. check_outfile(fn,blkdev_ok=True)
  686. key2 = 'out_fmt'
  687. if hasattr(cfg,key2):
  688. val2 = getattr(cfg,key2)
  689. from .wallet import get_wallet_data
  690. wd = get_wallet_data('incog_hidden')
  691. if val2 and val2 not in wd.fmt_codes:
  692. die( 'UserOptError', f'Option conflict:\n {fmt_opt(name)}, with\n {fmt_opt(key2)}={val2}' )
  693. hidden_incog_output_params = hidden_incog_input_params
  694. def subseeds():
  695. from .subseed import SubSeedIdxRange
  696. opt_compares(val,'>=',SubSeedIdxRange.min_idx)
  697. opt_compares(val,'<=',SubSeedIdxRange.max_idx)
  698. def seed_len():
  699. from .seed import Seed
  700. opt_is_in_list(int(val),Seed.lens)
  701. def hash_preset():
  702. from .crypto import Crypto
  703. opt_is_in_list(val,list(Crypto.hash_presets.keys()))
  704. def brain_params():
  705. a = val.split(',')
  706. if len(a) != 2:
  707. display_opt(name,val)
  708. die( 'UserOptError', 'Option requires two comma-separated arguments' )
  709. opt_is_int( a[0], desc_pfx='seed length' )
  710. from .seed import Seed
  711. opt_is_in_list( int(a[0]), Seed.lens, desc_pfx='seed length' )
  712. from .crypto import Crypto
  713. opt_is_in_list( a[1], list(Crypto.hash_presets.keys()), desc_pfx='hash preset' )
  714. def usr_randchars():
  715. if val != 0:
  716. opt_compares(val,'>=',cfg.min_urandchars)
  717. opt_compares(val,'<=',cfg.max_urandchars)
  718. def tx_confs():
  719. opt_is_int(val)
  720. opt_compares(int(val),'>=',1)
  721. def vsize_adj():
  722. from .util import ymsg
  723. ymsg(f'Adjusting transaction vsize by a factor of {val:1.2f}')
  724. def daemon_id():
  725. from .daemon import CoinDaemon
  726. opt_is_in_list(val,CoinDaemon.all_daemon_ids())
  727. def locktime():
  728. opt_is_int(val)
  729. opt_compares(int(val),'>',0)
  730. def columns():
  731. opt_compares(val,'>',10)
  732. # TODO: add checks for token, rbf, tx_fee
  733. check_funcs_names = tuple(check_funcs.__dict__)
  734. for name in tuple(cfg._uopts) + cfg._envopts + cfg._cfgfile_opts.non_auto:
  735. val = getattr(cfg,name)
  736. if name in cfg._infile_opts:
  737. from .fileutil import check_infile
  738. check_infile(val) # file exists and is readable - dies on error
  739. elif name == 'outdir':
  740. from .fileutil import check_outdir
  741. check_outdir(val) # dies on error
  742. elif name in check_funcs_names:
  743. getattr(check_funcs,name)()
  744. elif cfg.debug:
  745. Msg(f'check_opts(): No test for config opt {name!r}')
  746. def fmt_opt(o):
  747. return '--' + o.replace('_','-')
  748. def opt_postproc_debug(cfg):
  749. none_opts = [k for k in dir(cfg) if k[:2] != '__' and getattr(cfg,k) is None]
  750. from .util import Msg
  751. Msg('\n Configuration opts:')
  752. for e in [d for d in dir(cfg) if d[:2] != '__']:
  753. Msg(f' {e:<20}: {getattr(cfg,e)}')
  754. Msg(" Configuration opts set to 'None':")
  755. Msg(' {}\n'.format('\n '.join(none_opts)))
  756. Msg('\n=== end opts.py debug ===\n')
  757. def conv_type(
  758. name,
  759. val,
  760. refval,
  761. desc,
  762. invert_bool = False,
  763. src = None ):
  764. def do_fail():
  765. die(1,'{a!r}: invalid value for {b} {c!r}{d} (must be of type {e!r})'.format(
  766. a = val,
  767. b = desc,
  768. c = fmt_opt(name) if 'command-line' in desc else name,
  769. d = f' in {src!r}' if src else '',
  770. e = type(refval).__name__ ))
  771. if type(refval) is bool:
  772. v = str(val).lower()
  773. ret = (
  774. True if v in ('true','yes','1','on') else
  775. False if v in ('false','no','none','0','off','') else
  776. None
  777. )
  778. return do_fail() if ret is None else (not ret) if invert_bool else ret
  779. else:
  780. try:
  781. return type(refval)(not val if invert_bool else val)
  782. except:
  783. do_fail()