main_tool.py 11 KB

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