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
  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. 'randpair',
  116. 'randwif',
  117. 'redeem_script2addr',
  118. 'scriptpubkey2addr',
  119. 'wif2addr',
  120. 'wif2hex',
  121. 'wif2redeem_script',
  122. 'wif2segwit_pair',
  123. ),
  124. 'mnemonic': (
  125. 'hex2mn',
  126. 'mn2hex',
  127. 'mn2hex_interactive',
  128. 'mn_printlist',
  129. 'mn_rand128',
  130. 'mn_rand192',
  131. 'mn_rand256',
  132. 'mn_stats',
  133. ),
  134. 'file': (
  135. 'addrfile_chksum',
  136. 'keyaddrfile_chksum',
  137. 'viewkeyaddrfile_chksum',
  138. 'passwdfile_chksum',
  139. 'txview',
  140. ),
  141. 'filecrypt': (
  142. 'decrypt',
  143. 'encrypt',
  144. ),
  145. 'fileutil': (
  146. 'decrypt_keystore',
  147. 'decrypt_geth_keystore',
  148. 'find_incog_data',
  149. 'rand2file',
  150. ),
  151. 'wallet': (
  152. 'gen_addr',
  153. 'gen_key',
  154. 'get_subseed',
  155. 'get_subseed_by_seed_id',
  156. 'list_shares',
  157. 'list_subseeds',
  158. ),
  159. 'rpc': (
  160. 'add_label',
  161. 'daemon_version',
  162. 'getbalance',
  163. 'listaddress',
  164. 'listaddresses',
  165. 'remove_address',
  166. 'remove_label',
  167. 'rescan_address',
  168. 'rescan_blockchain',
  169. 'resolve_address',
  170. 'twexport',
  171. 'twimport',
  172. 'twview',
  173. 'txhist',
  174. ),
  175. }
  176. def get_cmds():
  177. return [cmd for mod, cmds in mods.items() if mod != 'help' for cmd in cmds]
  178. def create_call_sig(cmd, cls, *, as_string=False):
  179. m = getattr(cls, cmd)
  180. if 'varargs_call_sig' in m.__code__.co_varnames: # hack
  181. flag = 'VAR_ARGS'
  182. va = m.__defaults__[0]
  183. args, dfls, ann = va['args'], va['dfls'], va['annots']
  184. else:
  185. flag = None
  186. c = m.__code__
  187. args = c.co_varnames[1:c.co_argcount + c.co_posonlyargcount + c.co_kwonlyargcount]
  188. dfls = (
  189. (m.__defaults__ or ()) +
  190. tuple(m.__kwdefaults__[k] for k in args if k in (m.__kwdefaults__ or ())))
  191. ann = m.__annotations__
  192. nargs = len(args) - len(dfls)
  193. dfl_types = tuple(
  194. ann[a] if a in ann and isinstance(ann[a], type) else type(dfls[i])
  195. for i, a in enumerate(args[nargs:]))
  196. if as_string:
  197. get_type_from_ann = lambda x: 'str or STDIN' if ann[x] == 'sstr' else ann[x].__name__
  198. return ' '.join(
  199. [f'{a} [{get_type_from_ann(a)}]' for a in args[:nargs]] +
  200. [f'{a} [{dfl_types[n].__name__}={dfls[n]!r}]' for n, a in enumerate(args[nargs:])])
  201. else:
  202. get_type_from_ann = lambda x: 'str' if ann[x] == 'sstr' else ann[x].__name__
  203. return (
  204. [(a, get_type_from_ann(a)) for a in args[:nargs]], # c_args
  205. {a:dfls[n] for n, a in enumerate(args[nargs:])}, # c_kwargs
  206. {a:dfl_types[n] for n, a in enumerate(args[nargs:])}, # c_kwargs_types
  207. ('STDIN_OK' if nargs and ann[args[0]] == 'sstr' else flag), # flag
  208. ann) # ann
  209. def process_args(cmd, cmd_args, cls):
  210. c_args, c_kwargs, c_kwargs_types, flag, _ = create_call_sig(cmd, cls)
  211. have_stdin_input = False
  212. def usage_die(s):
  213. msg(s)
  214. from .tool.help import usage
  215. usage(cmd)
  216. if flag != 'VAR_ARGS':
  217. if len(cmd_args) < len(c_args):
  218. usage_die(f'Command requires exactly {len(c_args)} non-keyword argument{suf(c_args)}')
  219. u_args = cmd_args[:len(c_args)]
  220. # If we're reading from a pipe, replace '-' with output of previous command
  221. if flag == 'STDIN_OK' and u_args and u_args[0] == '-':
  222. if sys.stdin.isatty():
  223. die('BadFilename', "Standard input is a TTY. Can't use '-' as a filename")
  224. else:
  225. from .util2 import parse_bytespec
  226. max_dlen_spec = '10kB' # limit input to 10KB for now
  227. max_dlen = parse_bytespec(max_dlen_spec)
  228. u_args[0] = os.read(0, max_dlen)
  229. have_stdin_input = True
  230. if len(u_args[0]) >= max_dlen:
  231. die(2, f'Maximum data input for this command is {max_dlen_spec}')
  232. if not u_args[0]:
  233. die(2, f'{cmd}: ERROR: no output from previous command in pipe')
  234. u_nkwargs = len(cmd_args) - len(c_args)
  235. u_kwargs = {}
  236. if flag == 'VAR_ARGS':
  237. cmd_args = ['dummy_arg'] + cmd_args
  238. t = [a.split('=', 1) for a in cmd_args if '=' in a]
  239. tk = [a[0] for a in t]
  240. tk_bad = [a for a in tk if a not in c_kwargs]
  241. if set(tk_bad) != set(tk[:len(tk_bad)]): # permit non-kw args to contain '='
  242. die(1, f'{tk_bad[-1]!r}: illegal keyword argument')
  243. u_kwargs = dict(t[len(tk_bad):])
  244. u_args = cmd_args[:-len(u_kwargs) or None]
  245. elif u_nkwargs > 0:
  246. u_kwargs = dict([a.split('=', 1) for a in cmd_args[len(c_args):] if '=' in a])
  247. if len(u_kwargs) != u_nkwargs:
  248. usage_die(f'Command requires exactly {len(c_args)} non-keyword argument{suf(c_args)}')
  249. if len(u_kwargs) > len(c_kwargs):
  250. usage_die(f'Command accepts no more than {len(c_kwargs)} keyword argument{suf(c_kwargs)}')
  251. for k in u_kwargs:
  252. if k not in c_kwargs:
  253. usage_die(f'{k!r}: invalid keyword argument')
  254. def conv_type(arg, arg_name, arg_type):
  255. if arg_type == 'bytes' and not isinstance(arg, bytes):
  256. die(1, "'Binary input data must be supplied via STDIN")
  257. if have_stdin_input and arg_type == 'str' and isinstance(arg, bytes):
  258. NL = '\r\n' if sys.platform == 'win32' else '\n'
  259. arg = arg.decode()
  260. if arg[-len(NL):] == NL: # rstrip one newline
  261. arg = arg[:-len(NL)]
  262. if arg_type == 'bool':
  263. if arg.lower() in ('true', 'yes', '1', 'on'):
  264. arg = True
  265. elif arg.lower() in ('false', 'no', '0', 'off'):
  266. arg = False
  267. else:
  268. usage_die(f'{arg!r}: invalid boolean value for keyword argument')
  269. try:
  270. return __builtins__[arg_type](arg)
  271. except:
  272. die(1, f'{arg!r}: Invalid argument for argument {arg_name} ({arg_type!r} required)')
  273. if flag == 'VAR_ARGS':
  274. args = [conv_type(u_args[i], c_args[0][0], c_args[0][1]) for i in range(len(u_args))]
  275. else:
  276. args = [conv_type(u_args[i], c_args[i][0], c_args[i][1]) for i in range(len(c_args))]
  277. kwargs = {k:conv_type(v, k, c_kwargs_types[k].__name__) for k, v in u_kwargs.items()}
  278. return (args, kwargs)
  279. def process_result(ret, *, pager=False, print_result=False):
  280. """
  281. Convert result to something suitable for output to screen and return it.
  282. If result is bytes and not convertible to utf8, output as binary using os.write().
  283. If 'print_result' is True, send the converted result directly to screen or
  284. pager instead of returning it.
  285. """
  286. def triage_result(o):
  287. if print_result:
  288. if pager:
  289. from .ui import do_pager
  290. do_pager(o)
  291. else:
  292. Msg(o)
  293. else:
  294. return o
  295. if ret is True:
  296. return True
  297. elif ret in (False, None):
  298. die(2, f'tool command returned {ret!r}')
  299. elif isinstance(ret, str):
  300. return triage_result(ret)
  301. elif isinstance(ret, int):
  302. return triage_result(str(ret))
  303. elif isinstance(ret, tuple):
  304. return triage_result('\n'.join([r.decode() if isinstance(r, bytes) else r for r in ret]))
  305. elif isinstance(ret, bytes):
  306. try:
  307. return triage_result(ret.decode())
  308. except:
  309. # don't add NL to binary data if it can't be converted to utf8
  310. return os.write(1, ret) if print_result else ret
  311. else:
  312. die(2, f'tool.py: can’t handle return value of type {type(ret).__name__!r}')
  313. def get_cmd_cls(cmd):
  314. for modname, cmdlist in mods.items():
  315. if cmd in cmdlist:
  316. return getattr(importlib.import_module(f'mmgen.tool.{modname}'), 'tool_cmd')
  317. return False
  318. def get_mod_cls(modname):
  319. return getattr(importlib.import_module(f'mmgen.tool.{modname}'), 'tool_cmd')
  320. if gc.prog_name.endswith('-tool'):
  321. cfg = Config(opts_data=opts_data, parse_only=True)
  322. po = cfg._parsed_opts
  323. if po.user_opts.get('list'):
  324. def gen():
  325. for mod, cmdlist in mods.items():
  326. if mod == 'help':
  327. continue
  328. yield capfirst(get_mod_cls(mod).__doc__.lstrip().split('\n')[0]) + ':'
  329. for cmd in cmdlist:
  330. yield ' ' + cmd
  331. yield ''
  332. Msg('\n'.join(gen()).rstrip())
  333. sys.exit(0)
  334. if len(po.cmd_args) < 1:
  335. cfg._usage()
  336. cmd = po.cmd_args[0]
  337. cls = get_cmd_cls(cmd)
  338. if not cls:
  339. die(1, f'{cmd!r}: no such command')
  340. cfg = Config(
  341. opts_data = opts_data,
  342. parsed_opts = po,
  343. need_proto = cls.need_proto,
  344. init_opts = {'rpc_backend':'aiohttp'} if cmd == 'twimport' else None,
  345. process_opts = True)
  346. cmd, *args = cfg._args
  347. if cmd in ('help', 'usage') and args:
  348. args[0] = 'command_name=' + args[0]
  349. args, kwargs = process_args(cmd, args, cls)
  350. ret = getattr(cls(cfg, cmdname=cmd), cmd)(*args, **kwargs)
  351. if type(ret).__name__ == 'coroutine':
  352. ret = async_run(ret)
  353. process_result(
  354. ret,
  355. pager = kwargs.get('pager'),
  356. print_result = True)