main_tool.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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. mmgen-tool: Perform various MMGen- and cryptocoin-related operations.
  20. Part of the MMGen suite
  21. """
  22. import sys, os, importlib
  23. from .cfg import gc, Config
  24. from .util import msg, Msg, die, capfirst, suf, async_run, isAsync
  25. opts_data = {
  26. 'filter_codes': ['-'],
  27. 'text': {
  28. 'desc': f'Perform various {gc.proj_name}- and cryptocoin-related operations',
  29. 'usage': '[opts] <command> <command args>',
  30. 'options': """
  31. -- -d, --outdir= d Specify an alternate directory 'd' for output
  32. -- -h, --help Print this help message
  33. -- --, --longhelp Print help message for long (global) options
  34. -- -e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry
  35. -- -k, --use-internal-keccak-module Force use of the internal keccak module
  36. -- -K, --keygen-backend=n Use backend 'n' for public key generation. Options
  37. + for {coin_id}: {kgs}
  38. -- -l, --list List available commands
  39. -- -p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
  40. + for password hashing (default: '{gc.dfl_hash_preset}')
  41. -- -P, --passwd-file= f Get passphrase from file 'f'.
  42. -- -q, --quiet Produce quieter output
  43. -- -r, --usr-randchars=n Get 'n' characters of additional randomness from
  44. + user (min={cfg.min_urandchars}, max={cfg.max_urandchars})
  45. -- -t, --type=t Specify address type (valid choices: 'legacy',
  46. + 'compressed', 'segwit', 'bech32', 'zcash_z')
  47. -- -v, --verbose Produce more verbose output
  48. -- -x, --proxy=P Proxy HTTP connections via SOCKS5h proxy ‘P’ (host:port).
  49. + Use special value ‘env’ to honor *_PROXY environment
  50. + vars instead.
  51. e- -X, --cached-balances Use cached balances
  52. -- -y, --yes Answer 'yes' to prompts, suppress non-essential output
  53. """,
  54. 'notes': """
  55. COMMANDS
  56. {ch}
  57. Type ‘{pn} help <command>’ for help on a particular command
  58. """
  59. },
  60. 'code': {
  61. 'options': lambda cfg, s, help_notes: s.format(
  62. kgs = help_notes('keygen_backends'),
  63. coin_id = help_notes('coin_id'),
  64. cfg = cfg,
  65. gc = gc,
  66. ),
  67. 'notes': lambda cfg, s, help_notes: s.format(
  68. ch = help_notes('tool_help'),
  69. pn = gc.prog_name)
  70. }
  71. }
  72. # NB: Command groups and commands are displayed on the help screen in the following order,
  73. # so keep the command names sorted
  74. mods = {
  75. 'help': (
  76. 'help',
  77. 'usage',
  78. ),
  79. 'util': (
  80. 'b32tohex',
  81. 'b58chktohex',
  82. 'b58tobytes',
  83. 'b58tohex',
  84. 'b6dtohex',
  85. 'bytespec',
  86. 'bytestob58',
  87. 'hash160',
  88. 'hash256',
  89. 'hexdump',
  90. 'hexlify',
  91. 'hexreverse',
  92. 'hextob32',
  93. 'hextob58',
  94. 'hextob58chk',
  95. 'hextob6d',
  96. 'id6',
  97. 'id8',
  98. 'randb58',
  99. 'randhex',
  100. 'str2id6',
  101. 'to_bytespec',
  102. 'unhexdump',
  103. 'unhexlify',
  104. ),
  105. 'coin': (
  106. 'addr2pubhash',
  107. 'addr2scriptpubkey',
  108. 'eth_checksummed_addr',
  109. 'hex2wif',
  110. 'privhex2addr',
  111. 'privhex2pubhex',
  112. 'pubhash2addr',
  113. 'pubhex2addr',
  114. 'pubhex2redeem_script',
  115. 'privhex2pair',
  116. 'randpair',
  117. 'randwif',
  118. 'redeem_script2addr',
  119. 'scriptpubkey2addr',
  120. 'wif2addr',
  121. 'wif2hex',
  122. 'wif2redeem_script',
  123. 'wif2segwit_pair',
  124. ),
  125. 'mnemonic': (
  126. 'hex2mn',
  127. 'mn2hex',
  128. 'mn2hex_interactive',
  129. 'mn_printlist',
  130. 'mn_rand128',
  131. 'mn_rand192',
  132. 'mn_rand256',
  133. 'mn_stats',
  134. ),
  135. 'file': (
  136. 'addrfile_chksum',
  137. 'keyaddrfile_chksum',
  138. 'viewkeyaddrfile_chksum',
  139. 'passwdfile_chksum',
  140. 'txview',
  141. ),
  142. 'filecrypt': (
  143. 'decrypt',
  144. 'encrypt',
  145. ),
  146. 'fileutil': (
  147. 'decrypt_keystore',
  148. 'decrypt_geth_keystore',
  149. 'find_incog_data',
  150. 'rand2file',
  151. ),
  152. 'wallet': (
  153. 'gen_addr',
  154. 'gen_key',
  155. 'get_subseed',
  156. 'get_subseed_by_seed_id',
  157. 'list_shares',
  158. 'list_subseeds',
  159. ),
  160. 'rpc': (
  161. 'add_label',
  162. 'daemon_version',
  163. 'getbalance',
  164. 'listaddress',
  165. 'listaddresses',
  166. 'remove_address',
  167. 'remove_label',
  168. 'rescan_address',
  169. 'rescan_blockchain',
  170. 'resolve_address',
  171. 'twexport',
  172. 'twimport',
  173. 'twview',
  174. 'txhist',
  175. ),
  176. }
  177. def get_cmds():
  178. return [cmd for mod, cmds in mods.items() if mod != 'help' for cmd in cmds]
  179. def create_call_sig(cmd, cls, *, as_string=False):
  180. m = getattr(cls, cmd)
  181. if 'varargs_call_sig' in m.__code__.co_varnames: # hack
  182. flag = 'VAR_ARGS'
  183. va = m.__defaults__[0]
  184. args, dfls, ann = va['args'], va['dfls'], va['annots']
  185. else:
  186. flag = None
  187. c = m.__code__
  188. args = c.co_varnames[1:c.co_argcount + c.co_posonlyargcount + c.co_kwonlyargcount]
  189. dfls = (
  190. (m.__defaults__ or ()) +
  191. tuple(m.__kwdefaults__[k] for k in args if k in (m.__kwdefaults__ or ())))
  192. ann = m.__annotations__
  193. nargs = len(args) - len(dfls)
  194. dfl_types = tuple(
  195. ann[a] if a in ann and isinstance(ann[a], type) else type(dfls[i])
  196. for i, a in enumerate(args[nargs:]))
  197. if as_string:
  198. get_type_from_ann = lambda x: 'str or STDIN' if ann[x] == 'sstr' else ann[x].__name__
  199. return ' '.join(
  200. [f'{a} [{get_type_from_ann(a)}]' for a in args[:nargs]] +
  201. [f'{a} [{dfl_types[n].__name__}={dfls[n]!r}]' for n, a in enumerate(args[nargs:])])
  202. else:
  203. get_type_from_ann = lambda x: 'str' if ann[x] == 'sstr' else ann[x].__name__
  204. return (
  205. [(a, get_type_from_ann(a)) for a in args[:nargs]], # c_args
  206. {a:dfls[n] for n, a in enumerate(args[nargs:])}, # c_kwargs
  207. {a:dfl_types[n] for n, a in enumerate(args[nargs:])}, # c_kwargs_types
  208. ('STDIN_OK' if nargs and ann[args[0]] == 'sstr' else flag), # flag
  209. ann) # ann
  210. def process_args(cmd, cmd_args, cls):
  211. c_args, c_kwargs, c_kwargs_types, flag, _ = create_call_sig(cmd, cls)
  212. have_stdin_input = False
  213. def usage_die(s):
  214. msg(s)
  215. from .tool.help import usage
  216. usage(cmd)
  217. if flag != 'VAR_ARGS':
  218. if len(cmd_args) < len(c_args):
  219. usage_die(f'Command requires exactly {len(c_args)} non-keyword argument{suf(c_args)}')
  220. u_args = cmd_args[:len(c_args)]
  221. # If we're reading from a pipe, replace '-' with output of previous command
  222. if flag == 'STDIN_OK' and u_args and u_args[0] == '-':
  223. if sys.stdin.isatty():
  224. die('BadFilename', "Standard input is a TTY. Can't use '-' as a filename")
  225. else:
  226. from .util2 import parse_bytespec
  227. max_dlen_spec = '10kB' # limit input to 10KB for now
  228. max_dlen = parse_bytespec(max_dlen_spec)
  229. u_args[0] = os.read(0, max_dlen)
  230. have_stdin_input = True
  231. if len(u_args[0]) >= max_dlen:
  232. die(2, f'Maximum data input for this command is {max_dlen_spec}')
  233. if not u_args[0]:
  234. die(2, f'{cmd}: ERROR: no output from previous command in pipe')
  235. u_nkwargs = len(cmd_args) - len(c_args)
  236. u_kwargs = {}
  237. if flag == 'VAR_ARGS':
  238. cmd_args = ['dummy_arg'] + cmd_args
  239. t = [a.split('=', 1) for a in cmd_args if '=' in a]
  240. tk = [a[0] for a in t]
  241. tk_bad = [a for a in tk if a not in c_kwargs]
  242. if set(tk_bad) != set(tk[:len(tk_bad)]): # permit non-kw args to contain '='
  243. die(1, f'{tk_bad[-1]!r}: illegal keyword argument')
  244. u_kwargs = dict(t[len(tk_bad):])
  245. u_args = cmd_args[:-len(u_kwargs) or None]
  246. elif u_nkwargs > 0:
  247. u_kwargs = dict([a.split('=', 1) for a in cmd_args[len(c_args):] if '=' in a])
  248. if len(u_kwargs) != u_nkwargs:
  249. usage_die(f'Command requires exactly {len(c_args)} non-keyword argument{suf(c_args)}')
  250. if len(u_kwargs) > len(c_kwargs):
  251. usage_die(f'Command accepts no more than {len(c_kwargs)} keyword argument{suf(c_kwargs)}')
  252. for k in u_kwargs:
  253. if k not in c_kwargs:
  254. usage_die(f'{k!r}: invalid keyword argument')
  255. def conv_type(arg, arg_name, arg_type):
  256. if arg_type == 'bytes' and not isinstance(arg, bytes):
  257. die(1, "'Binary input data must be supplied via STDIN")
  258. if have_stdin_input and arg_type == 'str' and isinstance(arg, bytes):
  259. NL = '\r\n' if sys.platform == 'win32' else '\n'
  260. arg = arg.decode()
  261. if arg[-len(NL):] == NL: # rstrip one newline
  262. arg = arg[:-len(NL)]
  263. if arg_type == 'bool':
  264. if arg.lower() in ('true', 'yes', '1', 'on'):
  265. arg = True
  266. elif arg.lower() in ('false', 'no', '0', 'off'):
  267. arg = False
  268. else:
  269. usage_die(f'{arg!r}: invalid boolean value for keyword argument')
  270. try:
  271. return __builtins__[arg_type](arg)
  272. except:
  273. die(1, f'{arg!r}: Invalid argument for argument {arg_name} ({arg_type!r} required)')
  274. if flag == 'VAR_ARGS':
  275. args = [conv_type(u_args[i], c_args[0][0], c_args[0][1]) for i in range(len(u_args))]
  276. else:
  277. args = [conv_type(u_args[i], c_args[i][0], c_args[i][1]) for i in range(len(c_args))]
  278. kwargs = {k:conv_type(v, k, c_kwargs_types[k].__name__) for k, v in u_kwargs.items()}
  279. return (args, kwargs)
  280. def process_result(ret, *, pager=False, print_result=False):
  281. """
  282. Convert result to something suitable for output to screen and return it.
  283. If result is bytes and not convertible to utf8, output as binary using os.write().
  284. If 'print_result' is True, send the converted result directly to screen or
  285. pager instead of returning it.
  286. """
  287. def triage_result(o):
  288. if print_result:
  289. if pager:
  290. from .ui import do_pager
  291. do_pager(o)
  292. else:
  293. Msg(o)
  294. else:
  295. return o
  296. match ret:
  297. case True:
  298. return True
  299. case False | None:
  300. die(2, f'tool command returned {ret!r}')
  301. case str():
  302. return triage_result(ret)
  303. case int():
  304. return triage_result(str(ret))
  305. case tuple():
  306. return triage_result('\n'.join([r.decode() if isinstance(r, bytes) else r for r in ret]))
  307. case bytes():
  308. try:
  309. return triage_result(ret.decode())
  310. except:
  311. # don't add NL to binary data if it can't be converted to utf8
  312. return os.write(1, ret) if print_result else ret
  313. case _:
  314. die(2, f'tool.py: can’t handle return value of type {type(ret).__name__!r}')
  315. def get_cmd_cls(cmd):
  316. for modname, cmdlist in mods.items():
  317. if cmd in cmdlist:
  318. return getattr(importlib.import_module(f'mmgen.tool.{modname}'), 'tool_cmd')
  319. return False
  320. def get_mod_cls(modname):
  321. return getattr(importlib.import_module(f'mmgen.tool.{modname}'), 'tool_cmd')
  322. if gc.prog_name.endswith('-tool'):
  323. cfg = Config(opts_data=opts_data, parse_only=True)
  324. po = cfg._parsed_opts
  325. if po.user_opts.get('list'):
  326. def gen():
  327. for mod, cmdlist in mods.items():
  328. if mod == 'help':
  329. continue
  330. yield capfirst(get_mod_cls(mod).__doc__.lstrip().split('\n')[0]) + ':'
  331. for cmd in cmdlist:
  332. yield ' ' + cmd
  333. yield ''
  334. Msg('\n'.join(gen()).rstrip())
  335. sys.exit(0)
  336. if len(po.cmd_args) < 1:
  337. cfg._usage()
  338. cmd = po.cmd_args[0]
  339. cls = get_cmd_cls(cmd)
  340. if not cls:
  341. die(1, f'{cmd!r}: no such command')
  342. cfg = Config(
  343. opts_data = opts_data,
  344. parsed_opts = po,
  345. need_proto = cls.need_proto,
  346. init_opts = {'rpc_backend':'aiohttp'} if cmd == 'twimport' else None,
  347. process_opts = True)
  348. cmd, *args = cfg._args
  349. if cmd in ('help', 'usage') and args:
  350. args[0] = 'command_name=' + args[0]
  351. args, kwargs = process_args(cmd, args, cls)
  352. func = getattr(cls(cfg, cmdname=cmd), cmd)
  353. ret = async_run(cfg, func, args=args, kwargs=kwargs) if isAsync(func) else func(*args, **kwargs)
  354. process_result(
  355. ret,
  356. pager = kwargs.get('pager'),
  357. print_result = True)