cfg.py 30 KB

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