cfg.py 31 KB

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