tool.py 26 KB

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