tool.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  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 and data for the 'mmgen-tool' utility
  20. """
  21. import binascii
  22. from collections import OrderedDict
  23. from mmgen.protocol import hash160
  24. from mmgen.common import *
  25. from mmgen.crypto import *
  26. from mmgen.tx import *
  27. from mmgen.addr import *
  28. pnm = g.proj_name
  29. cmd_data = OrderedDict([
  30. ('help', ['<tool command> [str]']),
  31. ('usage', ['<tool command> [str]']),
  32. ('strtob58', ['<string> [str-]','pad [int=0]']),
  33. ('b58tostr', ['<b58 number> [str-]']),
  34. ('hextob58', ['<hex number> [str-]','pad [int=0]']),
  35. ('hextob58chk', ['<hex number> [str-]']),
  36. ('b58tohex', ['<b58 number> [str-]','pad [int=0]']),
  37. ('b58chktohex', ['<b58 number> [str-]']),
  38. ('b58randenc', []),
  39. ('b32tohex', ['<b32 num> [str-]','pad [int=0]']),
  40. ('hextob32', ['<hex num> [str-]','pad [int=0]']),
  41. ('randhex', ['nbytes [int=32]']),
  42. ('id8', ['<infile> [str]']),
  43. ('id6', ['<infile> [str]']),
  44. ('hash160', ['<hexadecimal string> [str-]']),
  45. ('hash256', ['<str, hexstr or filename> [str]', # TODO handle stdin
  46. 'hex_input [bool=False]','file_input [bool=False]']),
  47. ('str2id6', ['<string (spaces are ignored)> [str-]']),
  48. ('hexdump', ['<infile> [str]', 'cols [int=8]', 'line_nums [bool=True]']),
  49. ('unhexdump', ['<infile> [str]']),
  50. ('hexreverse', ['<hexadecimal string> [str-]']),
  51. ('hexlify', ['<string> [str-]']),
  52. ('rand2file', ['<outfile> [str]','<nbytes> [str]','threads [int=4]','silent [bool=False]']),
  53. ('randwif', []),
  54. ('randpair', []),
  55. ('hex2wif', ['<private key in hex format> [str-]']),
  56. ('wif2hex', ['<wif> [str-]']),
  57. ('wif2addr', ['<wif> [str-]']),
  58. ('wif2segwit_pair',['<wif> [str-]']),
  59. ('pubhash2addr', ['<coin address in hex format> [str-]']),
  60. ('addr2hexaddr', ['<coin address> [str-]']),
  61. ('privhex2addr', ['<private key in hex format> [str-]']),
  62. ('privhex2pubhex',['<private key in hex format> [str-]']),
  63. ('pubhex2addr', ['<public key in hex format> [str-]']), # new
  64. ('pubhex2redeem_script',['<public key in hex format> [str-]']), # new
  65. ('wif2redeem_script', ['<private key in WIF format> [str-]']), # new
  66. ('hex2mn', ['<hexadecimal string> [str-]',"wordlist [str='electrum']"]),
  67. ('mn2hex', ['<mnemonic> [str-]', "wordlist [str='electrum']"]),
  68. ('mn_rand128', ["wordlist [str='electrum']"]),
  69. ('mn_rand192', ["wordlist [str='electrum']"]),
  70. ('mn_rand256', ["wordlist [str='electrum']"]),
  71. ('mn_stats', ["wordlist [str='electrum']"]),
  72. ('mn_printlist', ["wordlist [str='electrum']"]),
  73. ('gen_addr', ['<{} ID> [str]'.format(pnm),"wallet [str='']"]),
  74. ('gen_key', ['<{} ID> [str]'.format(pnm),"wallet [str='']"]),
  75. ('listaddress',['<{} address> [str]'.format(pnm),'minconf [int=1]','pager [bool=False]','showempty [bool=True]','showbtcaddr [bool=True]','show_age [bool=False]','show_days [bool=True]']),
  76. ('listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=True]','all_labels [bool=False]',"sort [str=''] (options: reverse, age)",'show_age [bool=False]','show_days [bool=True]']),
  77. ('getbalance', ['minconf [int=1]','quiet [bool=False]','pager [bool=False]']),
  78. ('txview', ['<{} TX file(s)> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]',"sort [str='mtime'] (options: ctime, atime)",'MARGS']),
  79. ('twview', ["sort [str='age']",'reverse [bool=False]','show_days [bool=True]','show_mmid [bool=True]','minconf [int=1]','wide [bool=False]','pager [bool=False]']),
  80. ('add_label', ['<{} or coin address> [str]'.format(pnm),'<label> [str]']),
  81. ('remove_label', ['<{} or coin address> [str]'.format(pnm)]),
  82. ('remove_address', ['<{} or coin address> [str]'.format(pnm)]),
  83. ('addrfile_chksum', ['<{} addr file> [str]'.format(pnm),"mmtype [str='']"]),
  84. ('keyaddrfile_chksum', ['<{} addr file> [str]'.format(pnm),"mmtype [str='']"]),
  85. ('passwdfile_chksum', ['<{} password file> [str]'.format(pnm)]),
  86. ('find_incog_data', ['<file or device name> [str]','<Incog ID> [str]','keep_searching [bool=False]']),
  87. ('encrypt', ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
  88. ('decrypt', ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
  89. ('bytespec', ['<bytespec> [str]']),
  90. ('keyaddrlist2monerowallets',['<{} XMR key-address file> [str]'.format(pnm),'blockheight [int=(current height)]',"addrs [str=''] (addr idx list or range)"]),
  91. ('syncmonerowallets', ['<{} XMR key-address file> [str]'.format(pnm),"addrs [str=''] (addr idx list or range)"]),
  92. ])
  93. def _usage(cmd=None,exit_val=1):
  94. for v in cmd_data.values():
  95. if v and v[0][-2:] == '-]':
  96. v[0] = v[0][:-2] + ' or STDIN]'
  97. if 'MARGS' in v: v.remove('MARGS')
  98. if not cmd:
  99. Msg('Usage information for mmgen-tool commands:')
  100. for k,v in list(cmd_data.items()):
  101. Msg(' {:18} {}'.format(k,' '.join(v)))
  102. from mmgen.main_tool import stdin_msg
  103. Msg('\n '+'\n '.join(stdin_msg.split('\n')))
  104. sys.exit(0)
  105. if cmd in cmd_data:
  106. import re
  107. from mmgen.main_tool import cmd_help
  108. for line in cmd_help.split('\n'):
  109. if re.match(r'\s+{}\s+'.format(cmd),line):
  110. c,h = line.split('-',1)
  111. Msg('MMGEN-TOOL {}: {}'.format(c.strip().upper(),h.strip()))
  112. cd = cmd_data[cmd]
  113. msg('USAGE: {} {} {}'.format(g.prog_name,cmd,' '.join(cd)))
  114. else:
  115. die(1,"'{}': no such tool command".format(cmd))
  116. sys.exit(exit_val)
  117. def _process_args(cmd,cmd_args):
  118. if 'MARGS' in cmd_data[cmd]:
  119. cmd_data[cmd].remove('MARGS')
  120. margs = True
  121. else:
  122. margs = False
  123. c_args = [[i.split(' [')[0],i.split(' [')[1][:-1]]
  124. for i in cmd_data[cmd] if '=' not in i]
  125. c_kwargs = dict([[
  126. i.split(' [')[0],
  127. [i.split(' [')[1].split('=')[0],i.split(' [')[1].split('=')[1][:-1]]
  128. ] for i in cmd_data[cmd] if '=' in i])
  129. if not margs:
  130. u_args = [a for a in cmd_args[:len(c_args)]]
  131. if c_args and c_args[0][1][-1] == '-':
  132. c_args[0][1] = c_args[0][1][:-1] # [str-] -> [str]
  133. # If we're reading from a pipe, replace '-' with output of previous command
  134. if u_args and u_args[0] == '-':
  135. if not sys.stdin.isatty():
  136. u_args[0] = sys.stdin.read().strip()
  137. if not u_args[0]:
  138. die(2,'{}: ERROR: no output from previous command in pipe'.format(cmd))
  139. if not margs and len(u_args) < len(c_args):
  140. m1 = 'Command requires exactly {} non-keyword argument{}'
  141. msg(m1.format(len(c_args),suf(c_args,'s')))
  142. _usage(cmd)
  143. extra_args = len(cmd_args) - len(c_args)
  144. u_kwargs = {}
  145. if margs:
  146. t = [a.split('=') for a in cmd_args if '=' in a]
  147. tk = [a[0] for a in t]
  148. tk_bad = [a for a in tk if a not in c_kwargs]
  149. if set(tk_bad) != set(tk[:len(tk_bad)]):
  150. die(1,"'{}': illegal keyword argument".format(tk_bad[-1]))
  151. u_kwargs = dict(t[len(tk_bad):])
  152. u_args = cmd_args[:-len(u_kwargs) or None]
  153. elif extra_args > 0:
  154. u_kwargs = dict([a.split('=') for a in cmd_args[len(c_args):] if '=' in a])
  155. if len(u_kwargs) != extra_args:
  156. msg('Command requires exactly {} non-keyword argument{}'.format(len(c_args),suf(c_args,'s')))
  157. _usage(cmd)
  158. if len(u_kwargs) > len(c_kwargs):
  159. msg('Command requires exactly {} keyword argument{}'.format(len(c_kwargs),suf(c_kwargs,'s')))
  160. _usage(cmd)
  161. # mdie(c_args,c_kwargs,u_args,u_kwargs)
  162. for k in u_kwargs:
  163. if k not in c_kwargs:
  164. msg("'{}': invalid keyword argument".format(k))
  165. _usage(cmd)
  166. def conv_type(arg,arg_name,arg_type):
  167. if arg_type == 'bytes': pdie(arg,arg_name,arg_type)
  168. if arg_type == 'bool':
  169. if arg.lower() in ('true','yes','1','on'): arg = True
  170. elif arg.lower() in ('false','no','0','off'): arg = False
  171. else:
  172. msg("'{}': invalid boolean value for keyword argument".format(arg))
  173. _usage(cmd)
  174. try:
  175. return __builtins__[arg_type](arg)
  176. except:
  177. die(1,"'{}': Invalid argument for argument {} ('{}' required)".format(arg,arg_name,arg_type))
  178. if margs:
  179. args = [conv_type(u_args[i],c_args[0][0],c_args[0][1]) for i in range(len(u_args))]
  180. else:
  181. args = [conv_type(u_args[i],c_args[i][0],c_args[i][1]) for i in range(len(c_args))]
  182. kwargs = dict([(k,conv_type(u_kwargs[k],k,c_kwargs[k][0])) for k in u_kwargs])
  183. return args,kwargs
  184. def _get_result(ret): # returns a string or string subclass
  185. if issubclass(type(ret),str):
  186. return ret
  187. elif type(ret) == tuple:
  188. return '\n'.join([r.decode() if issubclass(type(r),bytes) else r for r in ret])
  189. elif issubclass(type(ret),bytes):
  190. try: return ret.decode()
  191. except: return repr(ret)
  192. elif ret == True:
  193. return ''
  194. elif ret in (False,None):
  195. ydie(1,"tool command returned '{}'".format(ret))
  196. else:
  197. ydie(1,"tool.py: can't handle return value of type '{}'".format(type(ret).__name__))
  198. def _print_result(ret,pager):
  199. if issubclass(type(ret),str):
  200. do_pager(ret) if pager else Msg(ret)
  201. elif type(ret) == tuple:
  202. o = '\n'.join([r.decode() if issubclass(type(r),bytes) else r for r in ret])
  203. do_pager(o) if pager else Msg(o)
  204. elif issubclass(type(ret),bytes):
  205. try:
  206. o = ret.decode()
  207. do_pager(o) if pager else Msg(o)
  208. except: os.write(1,ret)
  209. elif ret == True:
  210. pass
  211. elif ret in (False,None):
  212. ydie(1,"tool command returned '{}'".format(ret))
  213. else:
  214. ydie(1,"tool.py: can't handle return value of type '{}'".format(type(ret).__name__))
  215. from mmgen.obj import MMGenAddrType
  216. at = MMGenAddrType((hasattr(opt,'type') and opt.type) or g.proto.dfl_mmtype)
  217. kg = KeyGenerator(at)
  218. ag = AddrGenerator(at)
  219. wordlists = 'electrum','tirosh'
  220. dfl_wl_id = 'electrum'
  221. class MMGenToolCmd(object):
  222. def help(self,cmd=None):
  223. _usage(cmd,exit_val=0)
  224. def usage(self,cmd=None):
  225. _usage(cmd,exit_val=0)
  226. def hexdump(self,infile,cols=8,line_nums=True):
  227. return pretty_hexdump(
  228. get_data_from_file(infile,dash=True,silent=True,binary=True),
  229. cols=cols,line_nums=line_nums)
  230. def unhexdump(self,infile):
  231. if g.platform == 'win':
  232. import msvcrt
  233. msvcrt.setmode(sys.stdout.fileno(),os.O_BINARY)
  234. hexdata = get_data_from_file(infile,dash=True,silent=True)
  235. return decode_pretty_hexdump(hexdata)
  236. def b58randenc(self):
  237. r = get_random(32)
  238. return baseconv.b58encode(r,pad=True)
  239. def randhex(self,nbytes='32'):
  240. return binascii.hexlify(get_random(int(nbytes)))
  241. def randwif(self):
  242. return PrivKey(get_random(32),pubkey_type=at.pubkey_type,compressed=at.compressed).wif
  243. def randpair(self):
  244. privhex = PrivKey(get_random(32),pubkey_type=at.pubkey_type,compressed=at.compressed)
  245. addr = ag.to_addr(kg.to_pubhex(privhex))
  246. return (privhex.wif,addr)
  247. def wif2addr(self,wif):
  248. privhex = PrivKey(wif=wif)
  249. addr = ag.to_addr(kg.to_pubhex(privhex))
  250. return addr
  251. def wif2segwit_pair(self,wif):
  252. pubhex = kg.to_pubhex(PrivKey(wif=wif))
  253. addr = ag.to_addr(pubhex)
  254. rs = ag.to_segwit_redeem_script(pubhex)
  255. return (rs,addr)
  256. def pubhash2addr(self,pubhash):
  257. if opt.type == 'bech32':
  258. return g.proto.pubhash2bech32addr(pubhash.encode())
  259. else:
  260. return g.proto.pubhash2addr(pubhash.encode(),at.addr_fmt=='p2sh')
  261. def addr2hexaddr(self,addr):
  262. return g.proto.verify_addr(addr,CoinAddr.hex_width,return_dict=True)['hex']
  263. def hash160(self,pubkeyhex):
  264. return hash160(pubkeyhex)
  265. def pubhex2addr(self,pubkeyhex):
  266. return self.pubhash2addr(hash160(pubkeyhex.encode()).decode())
  267. def wif2hex(self,wif):
  268. return PrivKey(wif=wif)
  269. def hex2wif(self,hexpriv):
  270. return g.proto.hex2wif(hexpriv.encode(),pubkey_type=at.pubkey_type,compressed=at.compressed)
  271. def privhex2addr(self,privhex,output_pubhex=False):
  272. pk = PrivKey(binascii.unhexlify(privhex),compressed=at.compressed,pubkey_type=at.pubkey_type)
  273. ph = kg.to_pubhex(pk)
  274. return ph if output_pubhex else ag.to_addr(ph)
  275. def privhex2pubhex(self,privhex): # new
  276. return self.privhex2addr(privhex,output_pubhex=True)
  277. def pubhex2redeem_script(self,pubhex): # new
  278. return g.proto.pubhex2redeem_script(pubhex)
  279. def wif2redeem_script(self,wif): # new
  280. privhex = PrivKey(wif=wif)
  281. return ag.to_segwit_redeem_script(kg.to_pubhex(privhex))
  282. def do_random_mn(self,nbytes,wordlist):
  283. hexrand = binascii.hexlify(get_random(nbytes))
  284. Vmsg('Seed: {}'.format(hexrand))
  285. for wl_id in ([wordlist],wordlists)[wordlist=='all']:
  286. if wordlist == 'all': # TODO
  287. Msg('{} mnemonic:'.format(capfirst(wl_id)))
  288. mn = baseconv.fromhex(hexrand,wl_id)
  289. return ' '.join(mn)
  290. def mn_rand128(self,wordlist=dfl_wl_id):
  291. return self.do_random_mn(16,wordlist)
  292. def mn_rand192(self,wordlist=dfl_wl_id):
  293. return self.do_random_mn(24,wordlist)
  294. def mn_rand256(self,wordlist=dfl_wl_id):
  295. return self.do_random_mn(32,wordlist)
  296. def hex2mn(self,s,wordlist=dfl_wl_id):
  297. return ' '.join(baseconv.fromhex(s.encode(),wordlist))
  298. def mn2hex(self,s,wordlist=dfl_wl_id):
  299. return baseconv.tohex(s.split(),wordlist)
  300. def strtob58(self,s,pad=None):
  301. return baseconv.fromhex(binascii.hexlify(s.encode()),'b58',pad,tostr=True)
  302. def hextob58(self,s,pad=None):
  303. return baseconv.fromhex(s.encode(),'b58',pad,tostr=True)
  304. def hextob58chk(self,s):
  305. from mmgen.protocol import _b58chk_encode
  306. return _b58chk_encode(s.encode())
  307. def hextob32(self,s,pad=None):
  308. return baseconv.fromhex(s.encode(),'b32',pad,tostr=True)
  309. def b58tostr(self,s):
  310. return binascii.unhexlify(baseconv.tohex(s,'b58'))
  311. def b58tohex(self,s,pad=None):
  312. return baseconv.tohex(s,'b58',pad)
  313. def b58chktohex(self,s):
  314. from mmgen.protocol import _b58chk_decode
  315. return _b58chk_decode(s)
  316. def b32tohex(self,s,pad=None):
  317. return baseconv.tohex(s.upper(),'b32',pad)
  318. def mn_stats(self,wordlist=dfl_wl_id):
  319. wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist))
  320. baseconv.check_wordlist(wordlist)
  321. return True
  322. def mn_printlist(self,wordlist=dfl_wl_id):
  323. wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist))
  324. return '\n'.join(baseconv.digits[wordlist])
  325. def id8(self,infile):
  326. return make_chksum_8(
  327. get_data_from_file(infile,dash=True,silent=True,binary=True))
  328. def id6(self,infile):
  329. return make_chksum_6(
  330. get_data_from_file(infile,dash=True,silent=True,binary=True))
  331. def str2id6(self,s): # retain ignoring of space for backwards compat
  332. return make_chksum_6(''.join(s.split()))
  333. def addrfile_chksum(self,infile,mmtype=''):
  334. from mmgen.addr import AddrList
  335. mmtype = None if not mmtype else MMGenAddrType(mmtype)
  336. return AddrList(infile,mmtype=mmtype).chksum
  337. def keyaddrfile_chksum(self,infile,mmtype=''):
  338. from mmgen.addr import KeyAddrList
  339. mmtype = None if not mmtype else MMGenAddrType(mmtype)
  340. return KeyAddrList(infile,mmtype=mmtype).chksum
  341. def passwdfile_chksum(self,infile):
  342. from mmgen.addr import PasswordList
  343. return PasswordList(infile=infile).chksum
  344. def hexreverse(self,s):
  345. return binascii.hexlify(binascii.unhexlify(s.strip())[::-1])
  346. def hexlify(self,s):
  347. return binascii.hexlify(s.encode())
  348. def hash256(self,s,file_input=False,hex_input=False):
  349. from hashlib import sha256
  350. if file_input: b = get_data_from_file(s,binary=True)
  351. elif hex_input: b = decode_pretty_hexdump(s)
  352. else: b = s
  353. return sha256(sha256(b.encode()).digest()).hexdigest()
  354. def encrypt(self,infile,outfile='',hash_preset=''):
  355. data = get_data_from_file(infile,'data for encryption',binary=True)
  356. enc_d = mmgen_encrypt(data,'user data',hash_preset)
  357. if not outfile:
  358. outfile = '{}.{}'.format(os.path.basename(infile),g.mmenc_ext)
  359. write_data_to_file(outfile,enc_d,'encrypted data',binary=True)
  360. return True
  361. def decrypt(self,infile,outfile='',hash_preset=''):
  362. enc_d = get_data_from_file(infile,'encrypted data',binary=True)
  363. while True:
  364. dec_d = mmgen_decrypt(enc_d,'user data',hash_preset)
  365. if dec_d: break
  366. msg('Trying again...')
  367. if not outfile:
  368. o = os.path.basename(infile)
  369. outfile = remove_extension(o,g.mmenc_ext)
  370. if outfile == o: outfile += '.dec'
  371. write_data_to_file(outfile,dec_d,'decrypted data',binary=True)
  372. return True
  373. def find_incog_data(self,filename,iv_id,keep_searching=False):
  374. ivsize,bsize,mod = g.aesctr_iv_len,4096,4096*8
  375. n,carry = 0,b' '*ivsize
  376. flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
  377. f = os.open(filename,flgs)
  378. for ch in iv_id:
  379. if ch not in '0123456789ABCDEF':
  380. die(2,"'{}': invalid Incog ID".format(iv_id))
  381. while True:
  382. d = os.read(f,bsize)
  383. if not d: break
  384. d = carry + d
  385. for i in range(bsize):
  386. if sha256(d[i:i+ivsize]).hexdigest()[:8].upper() == iv_id:
  387. if n+i < ivsize: continue
  388. msg('\rIncog data for ID {} found at offset {}'.format(iv_id,n+i-ivsize))
  389. if not keep_searching: sys.exit(0)
  390. carry = d[len(d)-ivsize:]
  391. n += bsize
  392. if not n % mod:
  393. msg_r('\rSearched: {} bytes'.format(n))
  394. msg('')
  395. os.close(f)
  396. return True
  397. def rand2file(self,outfile,nbytes,threads=4,silent=False):
  398. nbytes = parse_nbytes(nbytes)
  399. from Crypto import Random
  400. rh = Random.new()
  401. from queue import Queue
  402. from threading import Thread
  403. bsize = 2**20
  404. roll = bsize * 4
  405. if opt.outdir: outfile = make_full_path(opt.outdir,outfile)
  406. f = open(outfile,'wb')
  407. from Crypto.Cipher import AES
  408. from Crypto.Util import Counter
  409. key = get_random(32)
  410. def encrypt_worker(wid):
  411. while True:
  412. i,d = q1.get()
  413. c = AES.new(key,AES.MODE_CTR,counter=Counter.new(g.aesctr_iv_len*8,initial_value=i))
  414. enc_data = c.encrypt(d)
  415. q2.put(enc_data)
  416. q1.task_done()
  417. def output_worker():
  418. while True:
  419. data = q2.get()
  420. f.write(data)
  421. q2.task_done()
  422. q1 = Queue()
  423. for i in range(max(1,threads-2)):
  424. t = Thread(target=encrypt_worker,args=(i,))
  425. t.daemon = True
  426. t.start()
  427. q2 = Queue()
  428. t = Thread(target=output_worker)
  429. t.daemon = True
  430. t.start()
  431. i = 1; rbytes = nbytes
  432. while rbytes > 0:
  433. d = rh.read(min(bsize,rbytes))
  434. q1.put((i,d))
  435. rbytes -= bsize
  436. i += 1
  437. if not (bsize*i) % roll:
  438. msg_r('\rRead: {} bytes'.format(bsize*i))
  439. if not silent:
  440. msg('\rRead: {} bytes'.format(nbytes))
  441. qmsg("\r{} bytes of random data written to file '{}'".format(nbytes,outfile))
  442. q1.join()
  443. q2.join()
  444. f.close()
  445. return True
  446. def bytespec(self,s):
  447. return str(parse_nbytes(s))
  448. def keyaddrlist2monerowallets(self,infile,blockheight=None,addrs=None):
  449. return self.monero_wallet_ops(infile=infile,op='create',blockheight=blockheight,addrs=addrs)
  450. def syncmonerowallets(self,infile,addrs=None):
  451. return self.monero_wallet_ops(infile=infile,op='sync',addrs=addrs)
  452. def monero_wallet_ops(self,infile,op,blockheight=None,addrs=None):
  453. def run_cmd(cmd):
  454. import subprocess as sp
  455. p = sp.Popen(cmd,stdin=sp.PIPE,stdout=sp.PIPE,stderr=sp.PIPE)
  456. return p
  457. def test_rpc():
  458. p = run_cmd(['monero-wallet-cli','--version'])
  459. if not b'Monero' in p.stdout.read():
  460. die(1,"Unable to run 'monero-wallet-cli'!")
  461. p = run_cmd(['monerod','status'])
  462. import re
  463. m = re.search(r'Height: (\d+)/\d+ ',p.stdout.read().decode())
  464. if not m:
  465. die(1,'Unable to connect to monerod!')
  466. return int(m.group(1))
  467. def my_expect(p,m,s,regex=False):
  468. if m: msg_r(' {}...'.format(m))
  469. ret = (p.expect_exact,p.expect)[regex](s)
  470. vmsg("\nexpect: '{}' => {}".format(s,ret))
  471. if not (ret == 0 or (type(s) == list and ret in (0,1))):
  472. die(2,"Expect failed: '{}' (return value: {})".format(s,ret))
  473. if m: msg('OK')
  474. return ret
  475. def my_sendline(p,m,s,usr_ret):
  476. if m: msg_r(' {}...'.format(m))
  477. ret = p.sendline(s)
  478. if ret != usr_ret:
  479. die(2,"Unable to send line '{}' (return value {})".format(s,ret))
  480. if m: msg('OK')
  481. vmsg("sendline: '{}' => {}".format(s,ret))
  482. def create(n,d,fn):
  483. try: os.stat(fn)
  484. except: pass
  485. else: die(1,"Wallet '{}' already exists!".format(fn))
  486. p = pexpect.spawn('monero-wallet-cli --generate-from-spend-key {}'.format(fn))
  487. if g.debug: p.logfile = sys.stdout
  488. my_expect(p,'Awaiting initial prompt','Secret spend key: ')
  489. my_sendline(p,'',d.sec.decode(),65)
  490. my_expect(p,'','Enter.* new.* password.*: ',regex=True)
  491. my_sendline(p,'Sending password',d.wallet_passwd,33)
  492. my_expect(p,'','Confirm password: ')
  493. my_sendline(p,'Sending password again',d.wallet_passwd,33)
  494. my_expect(p,'','of your choice: ')
  495. my_sendline(p,'','1',2)
  496. my_expect(p,'monerod generating wallet','Generated new wallet: ')
  497. my_expect(p,'','\n')
  498. if d.addr not in p.before.decode():
  499. die(3,'Addresses do not match!\n MMGen: {}\n Monero: {}'.format(d.addr,p.before.decode()))
  500. my_expect(p,'','View key: ')
  501. my_expect(p,'','\n')
  502. if d.viewkey not in p.before.decode():
  503. die(3,'View keys do not match!\n MMGen: {}\n Monero: {}'.format(d.viewkey,p.before.decode()))
  504. my_expect(p,'','(YYYY-MM-DD): ')
  505. h = str(blockheight or cur_height-1)
  506. my_sendline(p,'',h,len(h)+1)
  507. ret = my_expect(p,'',['Starting refresh','Still apply restore height? (Y/Yes/N/No): '])
  508. if ret == 1:
  509. my_sendline(p,'','Y',2)
  510. m = ' Warning: {}: blockheight argument is higher than current blockheight'
  511. ymsg(m.format(blockheight))
  512. elif blockheight != None:
  513. p.logfile = sys.stderr
  514. my_expect(p,'Syncing wallet','\[wallet.*$',regex=True)
  515. p.logfile = None
  516. my_sendline(p,'Exiting','exit',5)
  517. p.read()
  518. def sync(n,d,fn):
  519. try: os.stat(fn)
  520. except: die(1,"Wallet '{}' does not exist!".format(fn))
  521. p = pexpect.spawn('monero-wallet-cli --wallet-file={}'.format(fn))
  522. if g.debug: p.logfile = sys.stdout
  523. my_expect(p,'Awaiting password prompt','Wallet password: ')
  524. my_sendline(p,'Sending password',d.wallet_passwd,33)
  525. msg(' Starting refresh...')
  526. height = None
  527. while True:
  528. ret = p.expect([r' / .*',r'\[wallet.*:.*'])
  529. if ret == 0: # TODO: coverage
  530. height = p.after
  531. msg_r('\r Block {}{}'.format(p.before.split()[-1],height))
  532. elif ret == 1:
  533. if height:
  534. height = height.split()[-1]
  535. msg('\r Block {h} / {h}'.format(h=height))
  536. else:
  537. msg(' Wallet in sync')
  538. b = [l for l in p.before.decode().splitlines() if len(l) > 7 and l[:8] == 'Balance:'][0].split()
  539. msg(' Balance: {} Unlocked balance: {}'.format(b[1],b[4]))
  540. from mmgen.obj import XMRAmt
  541. bals[fn] = ( XMRAmt(b[1][:-1]), XMRAmt(b[4]) )
  542. my_sendline(p,'Exiting','exit',5)
  543. p.read()
  544. break
  545. else:
  546. die(2,"\nExpect failed: (return value: {})".format(ret))
  547. def process_wallets():
  548. m = { 'create': ('Creat','Generat',create,False),
  549. 'sync': ('Sync', 'Sync', sync, True) }
  550. opt.accept_defaults = opt.accept_defaults or m[op][3]
  551. from mmgen.protocol import init_coin
  552. init_coin('xmr')
  553. from mmgen.addr import AddrList
  554. al = KeyAddrList(infile)
  555. data = [d for d in al.data if addrs == None or d.idx in AddrIdxList(addrs)]
  556. dl = len(data)
  557. assert dl,"No addresses in addrfile within range '{}'".format(addrs)
  558. gmsg('\n{}ing {} wallet{}'.format(m[op][0],dl,suf(dl)))
  559. for n,d in enumerate(data): # [d.sec,d.wallet_passwd,d.viewkey,d.addr]
  560. fn = os.path.join(
  561. opt.outdir or '','{}-{}-MoneroWallet{}'.format(
  562. al.al_id.sid,
  563. d.idx,
  564. '-α' if g.debug_utf8 else ''))
  565. gmsg('\n{}ing wallet {}/{} ({})'.format(m[op][1],n+1,dl,fn))
  566. m[op][2](n,d,fn)
  567. gmsg('\n{} wallet{} {}ed'.format(dl,suf(dl),m[op][0].lower()))
  568. if op == 'sync':
  569. col1_w = max(map(len,bals)) + 1
  570. fs = '{:%s} {} {}' % col1_w
  571. msg('\n'+fs.format('Wallet','Balance ','Unlocked Balance '))
  572. from mmgen.obj import XMRAmt
  573. tbals = [XMRAmt('0'),XMRAmt('0')]
  574. for bal in bals:
  575. for i in (0,1): tbals[i] += bals[bal][i]
  576. msg(fs.format(bal+':',*[XMRAmt(b).fmt(fs='5.12',color=True) for b in bals[bal]]))
  577. msg(fs.format('-'*col1_w,'-'*18,'-'*18))
  578. msg(fs.format('TOTAL:',*[XMRAmt(b).fmt(fs='5.12',color=True) for b in tbals]))
  579. os.environ['LANG'] = 'C'
  580. import pexpect
  581. if blockheight != None and int(blockheight) < 0:
  582. blockheight = 0 # TODO: non-zero coverage
  583. cur_height = test_rpc()
  584. bals = OrderedDict() # locked,unlocked
  585. try:
  586. process_wallets()
  587. except KeyboardInterrupt:
  588. rdie(1,'\nUser interrupt\n')
  589. except EOFError:
  590. rdie(2,'\nEnd of file\n')
  591. except Exception as e:
  592. try:
  593. die(1,'Error: {}'.format(e.args[0]))
  594. except:
  595. rdie(1,'Error: {!r}'.format(e.args[0]))
  596. return True
  597. # ================ RPC commands ================== #
  598. def gen_addr(self,addr,wallet='',target='addr'):
  599. addr = MMGenID(addr)
  600. sf = get_seed_file([wallet] if wallet else [],1)
  601. opt.quiet = True
  602. from mmgen.seed import SeedSource
  603. ss = SeedSource(sf)
  604. if ss.seed.sid != addr.sid:
  605. m = 'Seed ID of requested address ({}) does not match wallet ({})'
  606. die(1,m.format(addr.sid,ss.seed.sid))
  607. al = AddrList(seed=ss.seed,addr_idxs=AddrIdxList(str(addr.idx)),mmtype=addr.mmtype)
  608. d = al.data[0]
  609. ret = d.sec.wif if target=='wif' else d.addr
  610. return ret
  611. def gen_key(self,addr,wallet=''):
  612. return self.gen_addr(addr,wallet,target='wif')
  613. def listaddress(self,addr,minconf=1,pager=False,showempty=True,showbtcaddr=True,show_age=False,show_days=None):
  614. return self.listaddresses(addrs=addr,minconf=minconf,pager=pager,
  615. showempty=showempty,showbtcaddrs=showbtcaddr,show_age=show_age,show_days=show_days)
  616. def listaddresses(self,addrs='',minconf=1,
  617. showempty=False,pager=False,showbtcaddrs=True,all_labels=False,sort=None,show_age=False,show_days=None):
  618. if show_days == None: show_days = False # user-set show_days triggers show_age
  619. else: show_age = True
  620. if sort:
  621. sort = set(sort.split(','))
  622. sort_params = set(['reverse','age'])
  623. if not sort.issubset(sort_params):
  624. die(1,"The sort option takes the following parameters: '{}'".format("','".join(sort_params)))
  625. usr_addr_list = []
  626. if addrs:
  627. a = addrs.rsplit(':',1)
  628. if len(a) != 2:
  629. m = "'{}': invalid address list argument (must be in form <seed ID>:[<type>:]<idx list>)"
  630. die(1,m.format(addrs))
  631. usr_addr_list = [MMGenID('{}:{}'.format(a[0],i)) for i in AddrIdxList(a[1])]
  632. from mmgen.tw import TwAddrList
  633. al = TwAddrList(usr_addr_list,minconf,showempty,showbtcaddrs,all_labels)
  634. if not al:
  635. die(0,('No tracked addresses with balances!','No tracked addresses!')[showempty])
  636. return al.format(showbtcaddrs,sort,show_age,show_days)
  637. def getbalance(self,minconf=1,quiet=False,pager=False):
  638. from mmgen.tw import TwGetBalance
  639. return TwGetBalance(minconf,quiet).format()
  640. def txview(self,*infiles,**kwargs):
  641. from mmgen.filename import MMGenFileList
  642. terse = 'terse' in kwargs and kwargs['terse']
  643. sort_key = kwargs['sort'] if 'sort' in kwargs else 'mtime'
  644. flist = MMGenFileList(infiles,ftype=MMGenTX)
  645. flist.sort_by_age(key=sort_key) # in-place sort
  646. from mmgen.term import get_terminal_size
  647. sep = '—'*77+'\n'
  648. return sep.join([MMGenTX(fn).format_view(terse=terse) for fn in flist.names()]).rstrip()
  649. def twview(self,pager=False,reverse=False,wide=False,minconf=1,sort='age',show_days=True,show_mmid=True):
  650. rpc_init()
  651. from mmgen.tw import TwUnspentOutputs
  652. tw = TwUnspentOutputs(minconf=minconf)
  653. tw.do_sort(sort,reverse=reverse)
  654. tw.show_days = show_days
  655. tw.show_mmid = show_mmid
  656. return tw.format_for_printing(color=True) if wide else tw.format_for_display()
  657. def add_label(self,mmaddr_or_coin_addr,label):
  658. rpc_init()
  659. from mmgen.tw import TrackingWallet
  660. TrackingWallet(mode='w').add_label(mmaddr_or_coin_addr,label,on_fail='raise')
  661. return True
  662. def remove_label(self,mmaddr_or_coin_addr):
  663. self.add_label(mmaddr_or_coin_addr,'')
  664. return True
  665. def remove_address(self,mmaddr_or_coin_addr):
  666. from mmgen.tw import TrackingWallet
  667. tw = TrackingWallet(mode='w')
  668. ret = tw.remove_address(mmaddr_or_coin_addr) # returns None on failure
  669. if ret:
  670. msg("Address '{}' deleted from tracking wallet".format(ret))
  671. return ret