tool.py 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2019 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. tool.py: Routines for the 'mmgen-tool' utility
  20. """
  21. from mmgen.protocol import hash160
  22. from mmgen.common import *
  23. from mmgen.crypto import *
  24. from mmgen.addr import *
  25. NL = ('\n','\r\n')[g.platform=='win']
  26. def _create_call_sig(cmd,parsed=False):
  27. m = getattr(MMGenToolCmd,cmd)
  28. if 'varargs_call_sig' in m.__code__.co_varnames: # hack
  29. flag = 'VAR_ARGS'
  30. va = m.__defaults__[0]
  31. args,dfls,ann = va['args'],va['dfls'],va['annots']
  32. else:
  33. flag = None
  34. args = m.__code__.co_varnames[1:m.__code__.co_argcount]
  35. dfls = m.__defaults__ or ()
  36. ann = m.__annotations__
  37. nargs = len(args) - len(dfls)
  38. def get_type_from_ann(arg):
  39. return ann[arg][1:] + (' or STDIN','')[parsed] if ann[arg] == 'sstr' else ann[arg].__name__
  40. if parsed:
  41. c_args = [(a,get_type_from_ann(a)) for a in args[:nargs]]
  42. c_kwargs = [(a,dfls[n]) for n,a in enumerate(args[nargs:])]
  43. return c_args,dict(c_kwargs),'STDIN_OK' if c_args and ann[args[0]] == 'sstr' else flag
  44. else:
  45. c_args = ['{} [{}]'.format(a,get_type_from_ann(a)) for a in args[:nargs]]
  46. c_kwargs = ['"{}" [{}={!r}{}]'.format(
  47. a, type(dfls[n]).__name__, dfls[n],
  48. (' ' + ann[a] if a in ann else ''))
  49. for n,a in enumerate(args[nargs:])]
  50. return ' '.join(c_args + c_kwargs)
  51. def _usage(cmd=None,exit_val=1):
  52. m1=('USAGE INFORMATION FOR MMGEN-TOOL COMMANDS:\n\n'
  53. ' Unquoted arguments are mandatory\n'
  54. ' Quoted arguments are optional, default values will be used\n'
  55. ' Argument types and default values are shown in square brackets\n')
  56. m2=(' To force a command to read from STDIN instead of file (for commands taking\n'
  57. ' a filename as their first argument), substitute "-" for the filename.\n\n'
  58. 'EXAMPLES:\n\n'
  59. ' Generate a random Bech32 public/private keypair for LTC:\n'
  60. ' $ mmgen-tool -r0 --coin=ltc --type=bech32 randpair\n\n'
  61. ' Generate a well-known burn address:\n'
  62. ' $ mmgen-tool hextob58chk 000000000000000000000000000000000000000000\n\n'
  63. ' Generate a random 12-word seed phrase:\n'
  64. ' $ mmgen-tool -r0 mn_rand128\n\n'
  65. ' Same as above, but get additional entropy from user:\n'
  66. ' $ mmgen-tool mn_rand128\n\n'
  67. ' Convert a string to base 58:\n'
  68. ' $ mmgen-tool bytestob58 /etc/timezone pad=20\n\n'
  69. ' Reverse a hex string:\n'
  70. ' $ mmgen-tool hexreverse "deadbeefcafe"\n\n'
  71. ' Same as above, but use a pipe:\n'
  72. ' $ echo "deadbeefcafe" | mmgen-tool hexreverse -')
  73. if not cmd:
  74. Msg(m1)
  75. for bc in MMGenToolCmd.__bases__:
  76. cls_info = bc.__doc__.strip().split('\n')[0]
  77. Msg(' {}{}\n'.format(cls_info[0].upper(),cls_info[1:]))
  78. ucmds = bc._user_commands()
  79. max_w = max(map(len,ucmds))
  80. for cmd in ucmds:
  81. if getattr(MMGenToolCmd,cmd).__doc__:
  82. Msg(' {:{w}} {}'.format(cmd,_create_call_sig(cmd),w=max_w))
  83. Msg('')
  84. Msg(m2)
  85. elif cmd in MMGenToolCmd._user_commands():
  86. docstr = getattr(MMGenToolCmd,cmd).__doc__.strip()
  87. msg('{}'.format(capfirst(docstr)))
  88. msg('USAGE: {} {} {}'.format(g.prog_name,cmd,_create_call_sig(cmd)))
  89. else:
  90. die(1,"'{}': no such tool command".format(cmd))
  91. sys.exit(exit_val)
  92. def _process_args(cmd,cmd_args):
  93. c_args,c_kwargs,flag = _create_call_sig(cmd,parsed=True)
  94. have_stdin_input = False
  95. if flag != 'VAR_ARGS':
  96. if len(cmd_args) < len(c_args):
  97. m1 = 'Command requires exactly {} non-keyword argument{}'
  98. msg(m1.format(len(c_args),suf(c_args)))
  99. _usage(cmd)
  100. u_args = cmd_args[:len(c_args)]
  101. # If we're reading from a pipe, replace '-' with output of previous command
  102. if flag == 'STDIN_OK' and u_args and u_args[0] == '-':
  103. if sys.stdin.isatty():
  104. raise BadFilename("Standard input is a TTY. Can't use '-' as a filename")
  105. else:
  106. max_dlen_spec = '10kB' # limit input to 10KB for now
  107. max_dlen = MMGenToolCmdUtil().bytespec(max_dlen_spec)
  108. u_args[0] = os.read(0,max_dlen)
  109. # try: u_args[0] = u_args[0].decode()
  110. # except: pass
  111. have_stdin_input = True
  112. if len(u_args[0]) >= max_dlen:
  113. die(2,'Maximum data input for this command is {}'.format(max_dlen_spec))
  114. if not u_args[0]:
  115. die(2,'{}: ERROR: no output from previous command in pipe'.format(cmd))
  116. u_nkwargs = len(cmd_args) - len(c_args)
  117. u_kwargs = {}
  118. if flag == 'VAR_ARGS':
  119. t = [a.split('=',1) for a in cmd_args if '=' in a]
  120. tk = [a[0] for a in t]
  121. tk_bad = [a for a in tk if a not in c_kwargs]
  122. if set(tk_bad) != set(tk[:len(tk_bad)]): # permit non-kw args to contain '='
  123. die(1,"'{}': illegal keyword argument".format(tk_bad[-1]))
  124. u_kwargs = dict(t[len(tk_bad):])
  125. u_args = cmd_args[:-len(u_kwargs) or None]
  126. elif u_nkwargs > 0:
  127. u_kwargs = dict([a.split('=',1) for a in cmd_args[len(c_args):] if '=' in a])
  128. if len(u_kwargs) != u_nkwargs:
  129. msg('Command requires exactly {} non-keyword argument{}'.format(len(c_args),suf(c_args)))
  130. _usage(cmd)
  131. if len(u_kwargs) > len(c_kwargs):
  132. msg('Command accepts no more than {} keyword argument{}'.format(len(c_kwargs),suf(c_kwargs)))
  133. _usage(cmd)
  134. for k in u_kwargs:
  135. if k not in c_kwargs:
  136. msg("'{}': invalid keyword argument".format(k))
  137. _usage(cmd)
  138. def conv_type(arg,arg_name,arg_type):
  139. if arg_type == 'bytes' and type(arg) != bytes:
  140. die(1,"'Binary input data must be supplied via STDIN")
  141. if have_stdin_input and arg_type == 'str' and isinstance(arg,bytes):
  142. arg = arg.decode()
  143. if arg[-len(NL):] == NL: # rstrip one newline
  144. arg = arg[:-len(NL)]
  145. if arg_type == 'bool':
  146. if arg.lower() in ('true','yes','1','on'): arg = True
  147. elif arg.lower() in ('false','no','0','off'): arg = False
  148. else:
  149. msg("'{}': invalid boolean value for keyword argument".format(arg))
  150. _usage(cmd)
  151. try:
  152. return __builtins__[arg_type](arg)
  153. except:
  154. die(1,"'{}': Invalid argument for argument {} ('{}' required)".format(arg,arg_name,arg_type))
  155. if flag == 'VAR_ARGS':
  156. args = [conv_type(u_args[i],c_args[0][0],c_args[0][1]) for i in range(len(u_args))]
  157. else:
  158. args = [conv_type(u_args[i],c_args[i][0],c_args[i][1]) for i in range(len(c_args))]
  159. kwargs = {k:conv_type(u_kwargs[k],k,type(c_kwargs[k]).__name__) for k in u_kwargs}
  160. return args,kwargs
  161. def _process_result(ret,pager=False,print_result=False):
  162. """
  163. Convert result to something suitable for output to screen and return it.
  164. If result is bytes and not convertible to utf8, output as binary using os.write().
  165. If 'print_result' is True, send the converted result directly to screen or
  166. pager instead of returning it.
  167. """
  168. def triage_result(o):
  169. return o if not print_result else do_pager(o) if pager else Msg(o)
  170. if ret == True:
  171. return True
  172. elif ret in (False,None):
  173. ydie(1,"tool command returned '{}'".format(ret))
  174. elif isinstance(ret,str):
  175. return triage_result(ret)
  176. elif isinstance(ret,int):
  177. return triage_result(str(ret))
  178. elif isinstance(ret,tuple):
  179. return triage_result('\n'.join([r.decode() if isinstance(r,bytes) else r for r in ret]))
  180. elif isinstance(ret,bytes):
  181. try:
  182. o = ret.decode()
  183. return o if not print_result else do_pager(o) if pager else Msg(o)
  184. except:
  185. # don't add NL to binary data if it can't be converted to utf8
  186. return ret if not print_result else os.write(1,ret)
  187. else:
  188. ydie(1,"tool.py: can't handle return value of type '{}'".format(type(ret).__name__))
  189. from mmgen.obj import MMGenAddrType
  190. def init_generators(arg=None):
  191. global at,kg,ag
  192. at = MMGenAddrType((hasattr(opt,'type') and opt.type) or g.proto.dfl_mmtype)
  193. if arg != 'at':
  194. kg = KeyGenerator(at)
  195. ag = AddrGenerator(at)
  196. def conv_cls_bip39():
  197. from mmgen.bip39 import bip39
  198. return bip39
  199. dfl_mnemonic_fmt = 'mmgen'
  200. mnemonic_fmts = {
  201. 'mmgen': { 'fmt': 'words', 'conv_cls': lambda: baseconv },
  202. 'bip39': { 'fmt': 'bip39', 'conv_cls': conv_cls_bip39 },
  203. }
  204. mn_opts_disp = "(valid options: '{}')".format("', '".join(mnemonic_fmts))
  205. class MMGenToolCmdBase(object):
  206. @classmethod
  207. def _user_commands(cls):
  208. return [e for e in dir(cls) if e[0] != '_' and getattr(cls,e).__doc__]
  209. class MMGenToolCmdMisc(MMGenToolCmdBase):
  210. "miscellaneous commands"
  211. def help(self,command_name=''):
  212. "display usage information for a single command or all commands"
  213. _usage(command_name,exit_val=0)
  214. usage = help
  215. class MMGenToolCmdUtil(MMGenToolCmdBase):
  216. "general string conversion and hashing utilities"
  217. def bytespec(self,dd_style_byte_specifier:str):
  218. "convert a byte specifier such as '1GB' into an integer"
  219. return parse_bytespec(dd_style_byte_specifier)
  220. def randhex(self,nbytes='32'):
  221. "print 'n' bytes (default 32) of random data in hex format"
  222. return get_random(int(nbytes)).hex()
  223. def hexreverse(self,hexstr:'sstr'):
  224. "reverse bytes of a hexadecimal string"
  225. return bytes.fromhex(hexstr.strip())[::-1].hex()
  226. def hexlify(self,infile:str):
  227. "convert bytes in file to hexadecimal (use '-' for stdin)"
  228. data = get_data_from_file(infile,dash=True,quiet=True,binary=True)
  229. return data.hex()
  230. def unhexlify(self,hexstr:'sstr'):
  231. "convert hexadecimal value to bytes (warning: outputs binary data)"
  232. return bytes.fromhex(hexstr)
  233. def hexdump(self,infile:str,cols=8,line_nums=True):
  234. "create hexdump of data from file (use '-' for stdin)"
  235. data = get_data_from_file(infile,dash=True,quiet=True,binary=True)
  236. return pretty_hexdump(data,cols=cols,line_nums=line_nums).rstrip()
  237. def unhexdump(self,infile:str):
  238. "decode hexdump from file (use '-' for stdin) (warning: outputs binary data)"
  239. if g.platform == 'win':
  240. import msvcrt
  241. msvcrt.setmode(sys.stdout.fileno(),os.O_BINARY)
  242. hexdata = get_data_from_file(infile,dash=True,quiet=True)
  243. return decode_pretty_hexdump(hexdata)
  244. def hash160(self,hexstr:'sstr'):
  245. "compute ripemd160(sha256(data)) (convert hex pubkey to hex addr)"
  246. return hash160(hexstr)
  247. def hash256(self,string_or_bytes:str,file_input=False,hex_input=False): # TODO: handle stdin
  248. "compute sha256(sha256(data)) (double sha256)"
  249. from hashlib import sha256
  250. if file_input: b = get_data_from_file(string_or_bytes,binary=True)
  251. elif hex_input: b = decode_pretty_hexdump(string_or_bytes)
  252. else: b = string_or_bytes
  253. return sha256(sha256(b.encode()).digest()).hexdigest()
  254. def id6(self,infile:str):
  255. "generate 6-character MMGen ID for a file (use '-' for stdin)"
  256. return make_chksum_6(
  257. get_data_from_file(infile,dash=True,quiet=True,binary=True))
  258. def str2id6(self,string:'sstr'): # retain ignoring of space for backwards compat
  259. "generate 6-character MMGen ID for a string, ignoring spaces"
  260. return make_chksum_6(''.join(string.split()))
  261. def id8(self,infile:str):
  262. "generate 8-character MMGen ID for a file (use '-' for stdin)"
  263. return make_chksum_8(
  264. get_data_from_file(infile,dash=True,quiet=True,binary=True))
  265. def randb58(self,nbytes=32,pad=True):
  266. "generate random data (default: 32 bytes) and convert it to base 58"
  267. return baseconv.b58encode(get_random(nbytes),pad=pad)
  268. def bytestob58(self,infile:str,pad=0):
  269. "convert bytes to base 58 (supply data via STDIN)"
  270. data = get_data_from_file(infile,dash=True,quiet=True,binary=True)
  271. return baseconv.fromhex(data.hex(),'b58',pad=pad,tostr=True)
  272. def b58tobytes(self,b58num:'sstr',pad=0):
  273. "convert a base 58 number to bytes (warning: outputs binary data)"
  274. return bytes.fromhex(baseconv.tohex(b58num,'b58',pad=pad))
  275. def hextob58(self,hexstr:'sstr',pad=0):
  276. "convert a hexadecimal number to base 58"
  277. return baseconv.fromhex(hexstr,'b58',pad=pad,tostr=True)
  278. def b58tohex(self,b58num:'sstr',pad=0):
  279. "convert a base 58 number to hexadecimal"
  280. return baseconv.tohex(b58num,'b58',pad=pad)
  281. def hextob58chk(self,hexstr:'sstr'):
  282. "convert a hexadecimal number to base58-check encoding"
  283. from mmgen.protocol import _b58chk_encode
  284. return _b58chk_encode(hexstr)
  285. def b58chktohex(self,b58chk_num:'sstr'):
  286. "convert a base58-check encoded number to hexadecimal"
  287. from mmgen.protocol import _b58chk_decode
  288. return _b58chk_decode(b58chk_num)
  289. def hextob32(self,hexstr:'sstr',pad=0):
  290. "convert a hexadecimal number to MMGen's flavor of base 32"
  291. return baseconv.fromhex(hexstr,'b32',pad,tostr=True)
  292. def b32tohex(self,b32num:'sstr',pad=0):
  293. "convert an MMGen-flavor base 32 number to hexadecimal"
  294. return baseconv.tohex(b32num.upper(),'b32',pad)
  295. class MMGenToolCmdCoin(MMGenToolCmdBase):
  296. """
  297. cryptocoin key/address utilities
  298. May require use of the '--coin', '--type' and/or '--testnet' options
  299. Examples:
  300. mmgen-tool --coin=ltc --type=bech32 wif2addr <wif key>
  301. mmgen-tool --coin=zec --type=zcash_z randpair
  302. """
  303. def randwif(self):
  304. "generate a random private key in WIF format"
  305. init_generators('at')
  306. return PrivKey(get_random(32),pubkey_type=at.pubkey_type,compressed=at.compressed).wif
  307. def randpair(self):
  308. "generate a random private key/address pair"
  309. init_generators()
  310. privhex = PrivKey(get_random(32),pubkey_type=at.pubkey_type,compressed=at.compressed)
  311. addr = ag.to_addr(kg.to_pubhex(privhex))
  312. return (privhex.wif,addr)
  313. def wif2hex(self,wifkey:'sstr'):
  314. "convert a private key from WIF to hex format"
  315. return PrivKey(wif=wifkey)
  316. def hex2wif(self,privhex:'sstr'):
  317. "convert a private key from hex to WIF format"
  318. init_generators('at')
  319. return g.proto.hex2wif(privhex,pubkey_type=at.pubkey_type,compressed=at.compressed)
  320. def wif2addr(self,wifkey:'sstr'):
  321. "generate a coin address from a key in WIF format"
  322. init_generators()
  323. privhex = PrivKey(wif=wifkey)
  324. addr = ag.to_addr(kg.to_pubhex(privhex))
  325. return addr
  326. def wif2redeem_script(self,wifkey:'sstr'): # new
  327. "convert a WIF private key to a Segwit P2SH-P2WPKH redeem script"
  328. assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
  329. init_generators()
  330. privhex = PrivKey(wif=wifkey)
  331. return ag.to_segwit_redeem_script(kg.to_pubhex(privhex))
  332. def wif2segwit_pair(self,wifkey:'sstr'):
  333. "generate both a Segwit P2SH-P2WPKH redeem script and address from WIF"
  334. assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
  335. init_generators()
  336. pubhex = kg.to_pubhex(PrivKey(wif=wifkey))
  337. addr = ag.to_addr(pubhex)
  338. rs = ag.to_segwit_redeem_script(pubhex)
  339. return (rs,addr)
  340. def privhex2addr(self,privhex:'sstr',output_pubhex=False):
  341. "generate coin address from private key in hex format"
  342. init_generators()
  343. pk = PrivKey(bytes.fromhex(privhex),compressed=at.compressed,pubkey_type=at.pubkey_type)
  344. ph = kg.to_pubhex(pk)
  345. return ph if output_pubhex else ag.to_addr(ph)
  346. def privhex2pubhex(self,privhex:'sstr'): # new
  347. "generate a hex public key from a hex private key"
  348. return self.privhex2addr(privhex,output_pubhex=True)
  349. def pubhex2addr(self,pubkeyhex:'sstr'):
  350. "convert a hex pubkey to an address"
  351. if opt.type == 'segwit':
  352. return g.proto.pubhex2segwitaddr(pubkeyhex)
  353. else:
  354. return self.pubhash2addr(hash160(pubkeyhex))
  355. def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new
  356. "convert a hex pubkey to a Segwit P2SH-P2WPKH redeem script"
  357. assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
  358. return g.proto.pubhex2redeem_script(pubkeyhex)
  359. def redeem_script2addr(self,redeem_scripthex:'sstr'): # new
  360. "convert a Segwit P2SH-P2WPKH redeem script to an address"
  361. assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
  362. assert redeem_scripthex[:4] == '0014','{!r}: invalid redeem script'.format(redeem_scripthex)
  363. assert len(redeem_scripthex) == 44,'{} bytes: invalid redeem script length'.format(len(redeem_scripthex)//2)
  364. return self.pubhash2addr(self.hash160(redeem_scripthex))
  365. def pubhash2addr(self,pubhashhex:'sstr'):
  366. "convert public key hash to address"
  367. if opt.type == 'bech32':
  368. return g.proto.pubhash2bech32addr(pubhashhex)
  369. else:
  370. init_generators('at')
  371. return g.proto.pubhash2addr(pubhashhex,at.addr_fmt=='p2sh')
  372. def addr2pubhash(self,addr:'sstr'):
  373. "convert coin address to public key hash"
  374. from mmgen.tx import addr2pubhash
  375. return addr2pubhash(CoinAddr(addr))
  376. def addr2scriptpubkey(self,addr:'sstr'):
  377. "convert coin address to scriptPubKey"
  378. from mmgen.tx import addr2scriptPubKey
  379. return addr2scriptPubKey(CoinAddr(addr))
  380. def scriptpubkey2addr(self,hexstr:'sstr'):
  381. "convert scriptPubKey to coin address"
  382. from mmgen.tx import scriptPubKey2addr
  383. return scriptPubKey2addr(hexstr)[0]
  384. class MMGenToolCmdMnemonic(MMGenToolCmdBase):
  385. """
  386. seed phrase utilities (valid formats: 'mmgen' (default), 'bip39')
  387. IMPORTANT NOTE: MMGen's default seed phrase format uses the Electrum
  388. wordlist, however seed phrases are computed using a different algorithm
  389. and are NOT Electrum-compatible!
  390. BIP39 support is fully compatible with the standard, allowing users to
  391. import and export seed entropy from BIP39-compatible wallets. However,
  392. users should be aware that BIP39 support does not imply BIP32 support!
  393. MMGen uses its own key derivation scheme differing from the one described
  394. by the BIP32 protocol.
  395. """
  396. def _do_random_mn(self,nbytes:int,fmt:str):
  397. assert nbytes in (16,24,32), 'nbytes must be 16, 24 or 32'
  398. hexrand = get_random(nbytes).hex()
  399. Vmsg('Seed: {}'.format(hexrand))
  400. return self.hex2mn(hexrand,fmt=fmt)
  401. def mn_rand128(self, fmt:mn_opts_disp = dfl_mnemonic_fmt ):
  402. "generate random 128-bit mnemonic seed phrase"
  403. return self._do_random_mn(16,fmt)
  404. def mn_rand192(self, fmt:mn_opts_disp = dfl_mnemonic_fmt ):
  405. "generate random 192-bit mnemonic seed phrase"
  406. return self._do_random_mn(24,fmt)
  407. def mn_rand256(self, fmt:mn_opts_disp = dfl_mnemonic_fmt ):
  408. "generate random 256-bit mnemonic seed phrase"
  409. return self._do_random_mn(32,fmt)
  410. def _get_mnemonic_fmt(self,fmt):
  411. if fmt not in mnemonic_fmts:
  412. m = '{!r}: invalid format (valid options: {})'
  413. die(1,m.format(fmt,', '.join(mnemonic_fmts)))
  414. return mnemonic_fmts[fmt]['fmt']
  415. def hex2mn( self, hexstr:'sstr', fmt:mn_opts_disp = dfl_mnemonic_fmt ):
  416. "convert a 16, 24 or 32-byte hexadecimal number to a mnemonic seed phrase"
  417. opt.out_fmt = self._get_mnemonic_fmt(fmt)
  418. from mmgen.seed import SeedSource
  419. s = SeedSource(seed_bin=bytes.fromhex(hexstr))
  420. s._format()
  421. return ' '.join(s.ssdata.mnemonic)
  422. def mn2hex( self, seed_mnemonic:'sstr', fmt:mn_opts_disp = dfl_mnemonic_fmt ):
  423. "convert a 12, 18 or 24-word mnemonic seed phrase to a hexadecimal number"
  424. in_fmt = self._get_mnemonic_fmt(fmt)
  425. opt.quiet = True
  426. from mmgen.seed import SeedSource
  427. return SeedSource(in_data=seed_mnemonic,in_fmt=in_fmt).seed.hexdata
  428. def mn_stats(self, fmt:mn_opts_disp = dfl_mnemonic_fmt ):
  429. "show stats for mnemonic wordlist"
  430. conv_cls = mnemonic_fmts[fmt]['conv_cls']()
  431. conv_cls.check_wordlist(fmt)
  432. return True
  433. def mn_printlist( self, fmt:mn_opts_disp = dfl_mnemonic_fmt, enum=False, pager=False ):
  434. "print mnemonic wordlist"
  435. conv_cls = mnemonic_fmts[fmt]['conv_cls']()
  436. ret = conv_cls.get_wordlist(fmt)
  437. if enum:
  438. ret = ['{:>4} {}'.format(n,e) for n,e in enumerate(ret)]
  439. return '\n'.join(ret)
  440. class MMGenToolCmdFile(MMGenToolCmdBase):
  441. "utilities for viewing/checking MMGen address and transaction files"
  442. def addrfile_chksum(self,mmgen_addrfile:str):
  443. "compute checksum for MMGen address file"
  444. opt.yes = True
  445. opt.quiet = True
  446. from mmgen.addr import AddrList
  447. return AddrList(mmgen_addrfile).chksum
  448. def keyaddrfile_chksum(self,mmgen_keyaddrfile:str):
  449. "compute checksum for MMGen key-address file"
  450. opt.yes = True
  451. opt.quiet = True
  452. from mmgen.addr import KeyAddrList
  453. return KeyAddrList(mmgen_keyaddrfile).chksum
  454. def passwdfile_chksum(self,mmgen_passwdfile:str):
  455. "compute checksum for MMGen password file"
  456. from mmgen.addr import PasswordList
  457. return PasswordList(infile=mmgen_passwdfile).chksum
  458. def txview( varargs_call_sig = { # hack to allow for multiple filenames
  459. 'args': (
  460. 'mmgen_tx_file(s)',
  461. 'pager',
  462. 'terse',
  463. 'sort',
  464. 'filesort' ),
  465. 'dfls': ( False, False, 'addr', 'mtime' ),
  466. 'annots': {
  467. 'mmgen_tx_file(s)': str,
  468. 'sort': '(valid options: addr,raw)',
  469. 'filesort': '(valid options: mtime,ctime,atime)'
  470. } },
  471. *infiles,**kwargs):
  472. "show raw/signed MMGen transaction in human-readable form"
  473. terse = bool(kwargs.get('terse'))
  474. tx_sort = kwargs.get('sort') or 'addr'
  475. file_sort = kwargs.get('filesort') or 'mtime'
  476. from mmgen.filename import MMGenFileList
  477. from mmgen.tx import MMGenTX
  478. flist = MMGenFileList(infiles,ftype=MMGenTX)
  479. flist.sort_by_age(key=file_sort) # in-place sort
  480. sep = '—'*77+'\n'
  481. return sep.join(
  482. [MMGenTX(fn,offline=True).format_view(terse=terse,sort=tx_sort) for fn in flist.names()]
  483. ).rstrip()
  484. class MMGenToolCmdFileCrypt(MMGenToolCmdBase):
  485. """
  486. file encryption and decryption
  487. MMGen encryption suite:
  488. * Key: Scrypt (user-configurable hash parameters, 32-byte salt)
  489. * Enc: AES256_CTR, 16-byte rand IV, sha256 hash + 32-byte nonce + data
  490. * The encrypted file is indistinguishable from random data
  491. """
  492. def encrypt(self,infile:str,outfile='',hash_preset=''):
  493. "encrypt a file"
  494. data = get_data_from_file(infile,'data for encryption',binary=True)
  495. enc_d = mmgen_encrypt(data,'user data',hash_preset)
  496. if not outfile:
  497. outfile = '{}.{}'.format(os.path.basename(infile),g.mmenc_ext)
  498. write_data_to_file(outfile,enc_d,'encrypted data',binary=True)
  499. return True
  500. def decrypt(self,infile:str,outfile='',hash_preset=''):
  501. "decrypt a file"
  502. enc_d = get_data_from_file(infile,'encrypted data',binary=True)
  503. while True:
  504. dec_d = mmgen_decrypt(enc_d,'user data',hash_preset)
  505. if dec_d: break
  506. msg('Trying again...')
  507. if not outfile:
  508. o = os.path.basename(infile)
  509. outfile = remove_extension(o,g.mmenc_ext)
  510. if outfile == o: outfile += '.dec'
  511. write_data_to_file(outfile,dec_d,'decrypted data',binary=True)
  512. return True
  513. class MMGenToolCmdFileUtil(MMGenToolCmdBase):
  514. "file utilities"
  515. def find_incog_data(self,filename:str,incog_id:str,keep_searching=False):
  516. "Use an Incog ID to find hidden incognito wallet data"
  517. ivsize,bsize,mod = g.aesctr_iv_len,4096,4096*8
  518. n,carry = 0,b' '*ivsize
  519. flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
  520. f = os.open(filename,flgs)
  521. for ch in incog_id:
  522. if ch not in '0123456789ABCDEF':
  523. die(2,"'{}': invalid Incog ID".format(incog_id))
  524. while True:
  525. d = os.read(f,bsize)
  526. if not d: break
  527. d = carry + d
  528. for i in range(bsize):
  529. if sha256(d[i:i+ivsize]).hexdigest()[:8].upper() == incog_id:
  530. if n+i < ivsize: continue
  531. msg('\rIncog data for ID {} found at offset {}'.format(incog_id,n+i-ivsize))
  532. if not keep_searching: sys.exit(0)
  533. carry = d[len(d)-ivsize:]
  534. n += bsize
  535. if not n % mod:
  536. msg_r('\rSearched: {} bytes'.format(n))
  537. msg('')
  538. os.close(f)
  539. return True
  540. def rand2file(self,outfile:str,nbytes:str,threads=4,silent=False):
  541. "write 'n' bytes of random data to specified file"
  542. from threading import Thread
  543. from queue import Queue
  544. from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
  545. from cryptography.hazmat.backends import default_backend
  546. def encrypt_worker(wid):
  547. ctr_init_val = os.urandom(g.aesctr_iv_len)
  548. c = Cipher(algorithms.AES(key),modes.CTR(ctr_init_val),backend=default_backend())
  549. encryptor = c.encryptor()
  550. while True:
  551. q2.put(encryptor.update(q1.get()))
  552. q1.task_done()
  553. def output_worker():
  554. while True:
  555. f.write(q2.get())
  556. q2.task_done()
  557. nbytes = parse_bytespec(nbytes)
  558. if opt.outdir:
  559. outfile = make_full_path(opt.outdir,outfile)
  560. f = open(outfile,'wb')
  561. key = get_random(32)
  562. q1,q2 = Queue(),Queue()
  563. for i in range(max(1,threads-2)):
  564. t = Thread(target=encrypt_worker,args=[i])
  565. t.daemon = True
  566. t.start()
  567. t = Thread(target=output_worker)
  568. t.daemon = True
  569. t.start()
  570. blk_size = 1024 * 1024
  571. for i in range(nbytes // blk_size):
  572. if not i % 4:
  573. msg_r('\rRead: {} bytes'.format(i * blk_size))
  574. q1.put(os.urandom(blk_size))
  575. if nbytes % blk_size:
  576. q1.put(os.urandom(nbytes % blk_size))
  577. q1.join()
  578. q2.join()
  579. f.close()
  580. fsize = os.stat(outfile).st_size
  581. if fsize != nbytes:
  582. die(3,'{}: incorrect random file size (should be {})'.format(fsize,nbytes))
  583. if not silent:
  584. msg('\rRead: {} bytes'.format(nbytes))
  585. qmsg("\r{} byte{} of random data written to file '{}'".format(nbytes,suf(nbytes),outfile))
  586. return True
  587. class MMGenToolCmdWallet(MMGenToolCmdBase):
  588. "key, address or subseed generation from an MMGen wallet"
  589. def get_subseed(self,subseed_idx:str,wallet=''):
  590. "get the Seed ID of a single subseed by Subseed Index for default or specified wallet"
  591. opt.quiet = True
  592. sf = get_seed_file([wallet] if wallet else [],1)
  593. from mmgen.seed import SeedSource
  594. return SeedSource(sf).seed.subseed(subseed_idx).sid
  595. def get_subseed_by_seed_id(self,seed_id:str,wallet='',last_idx=g.subseeds):
  596. "get the Subseed Index of a single subseed by Seed ID for default or specified wallet"
  597. opt.quiet = True
  598. sf = get_seed_file([wallet] if wallet else [],1)
  599. from mmgen.seed import SeedSource
  600. ret = SeedSource(sf).seed.subseed_by_seed_id(seed_id,last_idx)
  601. return ret.ss_idx if ret else None
  602. def list_subseeds(self,subseed_idx_range:str,wallet=''):
  603. "list a range of subseed Seed IDs for default or specified wallet"
  604. opt.quiet = True
  605. sf = get_seed_file([wallet] if wallet else [],1)
  606. from mmgen.seed import SeedSource
  607. return SeedSource(sf).seed.subseeds.format(*SubSeedIdxRange(subseed_idx_range))
  608. def gen_key(self,mmgen_addr:str,wallet=''):
  609. "generate a single MMGen WIF key from default or specified wallet"
  610. return self.gen_addr(mmgen_addr,wallet,target='wif')
  611. def gen_addr(self,mmgen_addr:str,wallet='',target='addr'):
  612. "generate a single MMGen address from default or specified wallet"
  613. addr = MMGenID(mmgen_addr)
  614. opt.quiet = True
  615. sf = get_seed_file([wallet] if wallet else [],1)
  616. from mmgen.seed import SeedSource
  617. ss = SeedSource(sf)
  618. if ss.seed.sid != addr.sid:
  619. m = 'Seed ID of requested address ({}) does not match wallet ({})'
  620. die(1,m.format(addr.sid,ss.seed.sid))
  621. al = AddrList(seed=ss.seed,addr_idxs=AddrIdxList(str(addr.idx)),mmtype=addr.mmtype)
  622. d = al.data[0]
  623. ret = d.sec.wif if target=='wif' else d.addr
  624. return ret
  625. class MMGenToolCmdRPC(MMGenToolCmdBase):
  626. "tracking wallet commands using the JSON-RPC interface"
  627. def getbalance(self,minconf=1,quiet=False,pager=False):
  628. "list confirmed/unconfirmed, spendable/unspendable balances in tracking wallet"
  629. from mmgen.tw import TwGetBalance
  630. return TwGetBalance(minconf,quiet).format()
  631. def listaddress(self,
  632. mmgen_addr:str,
  633. minconf = 1,
  634. pager = False,
  635. showempty = True,
  636. showbtcaddr = True,
  637. age_fmt:'(valid options: days,confs)' = ''):
  638. "list the specified MMGen address and its balance"
  639. return self.listaddresses( mmgen_addrs = mmgen_addr,
  640. minconf = minconf,
  641. pager = pager,
  642. showempty = showempty,
  643. showbtcaddrs = showbtcaddr,
  644. age_fmt = age_fmt)
  645. def listaddresses( self,
  646. mmgen_addrs:'(range or list)' = '',
  647. minconf = 1,
  648. showempty = False,
  649. pager = False,
  650. showbtcaddrs = True,
  651. all_labels = False,
  652. sort:'(valid options: reverse,age)' = '',
  653. age_fmt:'(valid options: days,confs)' = ''):
  654. "list MMGen addresses and their balances"
  655. show_age = bool(age_fmt)
  656. if sort:
  657. sort = set(sort.split(','))
  658. sort_params = {'reverse','age'}
  659. if not sort.issubset(sort_params):
  660. die(1,"The sort option takes the following parameters: '{}'".format("','".join(sort_params)))
  661. usr_addr_list = []
  662. if mmgen_addrs:
  663. a = mmgen_addrs.rsplit(':',1)
  664. if len(a) != 2:
  665. m = "'{}': invalid address list argument (must be in form <seed ID>:[<type>:]<idx list>)"
  666. die(1,m.format(mmgen_addrs))
  667. usr_addr_list = [MMGenID('{}:{}'.format(a[0],i)) for i in AddrIdxList(a[1])]
  668. rpc_init()
  669. from mmgen.tw import TwAddrList
  670. al = TwAddrList(usr_addr_list,minconf,showempty,showbtcaddrs,all_labels)
  671. if not al:
  672. die(0,('No tracked addresses with balances!','No tracked addresses!')[showempty])
  673. return al.format(showbtcaddrs,sort,show_age,age_fmt or 'days')
  674. def twview( self,
  675. pager = False,
  676. reverse = False,
  677. wide = False,
  678. minconf = 1,
  679. sort = 'age',
  680. age_fmt:'(valid options: days,confs)' = 'days',
  681. show_mmid = True):
  682. "view tracking wallet"
  683. rpc_init()
  684. from mmgen.tw import TwUnspentOutputs
  685. twuo = TwUnspentOutputs(minconf=minconf)
  686. twuo.do_sort(sort,reverse=reverse)
  687. twuo.age_fmt = age_fmt
  688. twuo.show_mmid = show_mmid
  689. ret = twuo.format_for_printing(color=True) if wide else twuo.format_for_display()
  690. del twuo.wallet
  691. return ret
  692. def add_label(self,mmgen_or_coin_addr:str,label:str):
  693. "add descriptive label for address in tracking wallet"
  694. rpc_init()
  695. from mmgen.tw import TrackingWallet
  696. TrackingWallet(mode='w').add_label(mmgen_or_coin_addr,label,on_fail='raise')
  697. return True
  698. def remove_label(self,mmgen_or_coin_addr:str):
  699. "remove descriptive label for address in tracking wallet"
  700. self.add_label(mmgen_or_coin_addr,'')
  701. return True
  702. def remove_address(self,mmgen_or_coin_addr:str):
  703. "remove an address from tracking wallet"
  704. from mmgen.tw import TrackingWallet
  705. tw = TrackingWallet(mode='w')
  706. ret = tw.remove_address(mmgen_or_coin_addr) # returns None on failure
  707. if ret:
  708. msg("Address '{}' deleted from tracking wallet".format(ret))
  709. return ret
  710. class MMGenToolCmdMonero(MMGenToolCmdBase):
  711. "Monero wallet utilities"
  712. def keyaddrlist2monerowallets( self,
  713. xmr_keyaddrfile:str,
  714. blockheight:'(default: current height)' = 0,
  715. addrs:'(integer range or list)' = ''):
  716. "create Monero wallets from key-address list"
  717. return self.monero_wallet_ops( infile = xmr_keyaddrfile,
  718. op = 'create',
  719. blockheight = blockheight,
  720. addrs = addrs)
  721. def syncmonerowallets(self,xmr_keyaddrfile:str,addrs:'(integer range or list)'=''):
  722. "sync Monero wallets from key-address list"
  723. return self.monero_wallet_ops(infile=xmr_keyaddrfile,op='sync',addrs=addrs)
  724. def monero_wallet_ops(self,infile:str,op:str,blockheight=0,addrs=''):
  725. exit_if_mswin('Monero wallet operations')
  726. def run_cmd(cmd):
  727. import subprocess as sp
  728. p = sp.Popen(cmd,stdin=sp.PIPE,stdout=sp.PIPE,stderr=sp.PIPE)
  729. return p
  730. def test_rpc():
  731. p = run_cmd(['monero-wallet-cli','--version'])
  732. if not b'Monero' in p.stdout.read():
  733. die(1,"Unable to run 'monero-wallet-cli'!")
  734. p = run_cmd(['monerod','status'])
  735. import re
  736. m = re.search(r'Height: (\d+)/\d+ ',p.stdout.read().decode())
  737. if not m:
  738. die(1,'Unable to connect to monerod!')
  739. return int(m.group(1))
  740. def my_expect(p,m,s,regex=False):
  741. if m: msg_r(' {}...'.format(m))
  742. ret = (p.expect_exact,p.expect)[regex](s)
  743. vmsg("\nexpect: '{}' => {}".format(s,ret))
  744. if g.debug:
  745. pmsg('p.before:',p.before)
  746. pmsg('p.after:',p.after)
  747. if not (ret == 0 or (type(s) == list and ret in (0,1))):
  748. die(2,"Expect failed: '{}' (return value: {})".format(s,ret))
  749. if m: msg('OK')
  750. return ret
  751. def my_sendline(p,m,s,usr_ret):
  752. if m: msg_r(' {}...'.format(m))
  753. ret = p.sendline(s)
  754. if g.debug:
  755. pmsg('p.before:',p.before)
  756. pmsg('p.after:',p.after)
  757. if ret != usr_ret:
  758. die(2,"Unable to send line '{}' (return value {})".format(s,ret))
  759. if m: msg('OK')
  760. vmsg("sendline: '{}' => {}".format(s,ret))
  761. def create(n,d,fn):
  762. try: os.stat(fn)
  763. except: pass
  764. else: die(1,"Wallet '{}' already exists!".format(fn))
  765. p = pexpect.spawn('monero-wallet-cli --generate-from-spend-key {}'.format(fn))
  766. # if g.debug: p.logfile = sys.stdout # TODO: Error: 'write() argument must be str, not bytes'
  767. my_expect(p,'Awaiting initial prompt','Secret spend key: ')
  768. my_sendline(p,'',d.sec,65)
  769. my_expect(p,'','Enter.* new.* password.*: ',regex=True)
  770. my_sendline(p,'Sending password',d.wallet_passwd,33)
  771. my_expect(p,'','Confirm password: ')
  772. my_sendline(p,'Sending password again',d.wallet_passwd,33)
  773. my_expect(p,'','of your choice: ')
  774. my_sendline(p,'','1',2)
  775. my_expect(p,'monerod generating wallet','Generated new wallet: ')
  776. my_expect(p,'','\n')
  777. if d.addr not in p.before.decode():
  778. die(3,'Addresses do not match!\n MMGen: {}\n Monero: {}'.format(d.addr,p.before.decode()))
  779. my_expect(p,'','View key: ')
  780. my_expect(p,'','\n')
  781. if d.viewkey not in p.before.decode():
  782. die(3,'View keys do not match!\n MMGen: {}\n Monero: {}'.format(d.viewkey,p.before.decode()))
  783. my_expect(p,'','(YYYY-MM-DD): ')
  784. h = str(blockheight or cur_height-1)
  785. my_sendline(p,'',h,len(h)+1)
  786. ret = my_expect(p,'',['Starting refresh','Still apply restore height? (Y/Yes/N/No): '])
  787. if ret == 1:
  788. my_sendline(p,'','Y',2)
  789. m = ' Warning: {}: blockheight argument is higher than current blockheight'
  790. ymsg(m.format(blockheight))
  791. elif blockheight:
  792. p.logfile = sys.stderr
  793. my_expect(p,'Syncing wallet','\[wallet.*$',regex=True)
  794. p.logfile = None
  795. my_sendline(p,'Exiting','exit',5)
  796. p.read()
  797. def sync(n,d,fn):
  798. import time
  799. try: os.stat(fn)
  800. except: die(1,"Wallet '{}' does not exist!".format(fn))
  801. p = pexpect.spawn('monero-wallet-cli --wallet-file={}'.format(fn))
  802. # if g.debug: p.logfile = sys.stdout # TODO: Error: 'write() argument must be str, not bytes'
  803. my_expect(p,'Awaiting password prompt','Wallet password: ')
  804. my_sendline(p,'Sending password',d.wallet_passwd,33)
  805. msg(' Starting refresh...')
  806. height = None
  807. while True:
  808. ret = p.expect([r'Height\s+\S+\s+/\s+\S+',r'\[wallet.*:.*'])
  809. if ret == 0: # TODO: coverage
  810. d = p.after.decode().split()
  811. msg_r('\r Block {} / {}'.format(d[1],d[3]))
  812. height = d[3]
  813. time.sleep(0.5)
  814. elif ret == 1:
  815. if height:
  816. msg('\r Block {h} / {h} (wallet in sync)'.format(h=height))
  817. else:
  818. msg(' Wallet in sync')
  819. my_sendline(p,'Requesting account info','account',8)
  820. my_expect(p,'Getting totals','Total\s+.*\n',regex=True)
  821. b = p.after.decode().strip().split()[1:]
  822. msg(' Balance: {} Unlocked balance: {}'.format(*b))
  823. from mmgen.obj import XMRAmt
  824. bals[fn] = tuple(map(XMRAmt,b))
  825. my_sendline(p,'Exiting','exit',5)
  826. p.read()
  827. break
  828. else:
  829. die(2,"\nExpect failed: (return value: {})".format(ret))
  830. def process_wallets():
  831. m = { 'create': ('Creat','Generat',create,False),
  832. 'sync': ('Sync', 'Sync', sync, True) }
  833. opt.accept_defaults = opt.accept_defaults or m[op][3]
  834. from mmgen.protocol import init_coin
  835. init_coin('xmr')
  836. from mmgen.addr import AddrList
  837. al = KeyAddrList(infile)
  838. data = [d for d in al.data if addrs == '' or d.idx in AddrIdxList(addrs)]
  839. dl = len(data)
  840. assert dl,"No addresses in addrfile within range '{}'".format(addrs)
  841. gmsg('\n{}ing {} wallet{}'.format(m[op][0],dl,suf(dl)))
  842. for n,d in enumerate(data): # [d.sec,d.wallet_passwd,d.viewkey,d.addr]
  843. fn = os.path.join(
  844. opt.outdir or '','{}-{}-MoneroWallet{}'.format(
  845. al.al_id.sid,
  846. d.idx,
  847. '-α' if g.debug_utf8 else ''))
  848. gmsg('\n{}ing wallet {}/{} ({})'.format(m[op][1],n+1,dl,fn))
  849. m[op][2](n,d,fn)
  850. gmsg('\n{} wallet{} {}ed'.format(dl,suf(dl),m[op][0].lower()))
  851. if op == 'sync':
  852. col1_w = max(map(len,bals)) + 1
  853. fs = '{:%s} {} {}' % col1_w
  854. msg('\n'+fs.format('Wallet','Balance ','Unlocked Balance '))
  855. from mmgen.obj import XMRAmt
  856. tbals = [XMRAmt('0'),XMRAmt('0')]
  857. for bal in bals:
  858. for i in (0,1): tbals[i] += bals[bal][i]
  859. msg(fs.format(bal+':',*[XMRAmt(b).fmt(fs='5.12',color=True) for b in bals[bal]]))
  860. msg(fs.format('-'*col1_w,'-'*18,'-'*18))
  861. msg(fs.format('TOTAL:',*[XMRAmt(b).fmt(fs='5.12',color=True) for b in tbals]))
  862. os.environ['LANG'] = 'C'
  863. import pexpect
  864. if blockheight < 0:
  865. blockheight = 0 # TODO: handle the non-zero case
  866. cur_height = test_rpc() # empty blockchain returns 1
  867. from collections import OrderedDict
  868. bals = OrderedDict() # locked,unlocked
  869. try:
  870. process_wallets()
  871. except KeyboardInterrupt:
  872. rdie(1,'\nUser interrupt\n')
  873. except EOFError:
  874. rdie(2,'\nEnd of file\n')
  875. except Exception as e:
  876. try:
  877. die(1,'Error: {}'.format(e.args[0]))
  878. except:
  879. rdie(1,'Error: {!r}'.format(e.args[0]))
  880. return True
  881. class MMGenToolCmd(
  882. MMGenToolCmdMisc,
  883. MMGenToolCmdUtil,
  884. MMGenToolCmdCoin,
  885. MMGenToolCmdMnemonic,
  886. MMGenToolCmdFile,
  887. MMGenToolCmdFileCrypt,
  888. MMGenToolCmdFileUtil,
  889. MMGenToolCmdWallet,
  890. MMGenToolCmdRPC,
  891. MMGenToolCmdMonero,
  892. ): pass