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