tool.py 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2020 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 collections import namedtuple
  22. from .protocol import hash160
  23. from .common import *
  24. from .crypto import *
  25. from .addr import *
  26. NL = ('\n','\r\n')[g.platform=='win']
  27. def _options_annot_str(l):
  28. return "(valid options: '{}')".format("','".join(l))
  29. def _create_call_sig(cmd,parsed=False):
  30. m = MMGenToolCmds[cmd]
  31. if 'varargs_call_sig' in m.__code__.co_varnames: # hack
  32. flag = 'VAR_ARGS'
  33. va = m.__defaults__[0]
  34. args,dfls,ann = va['args'],va['dfls'],va['annots']
  35. else:
  36. flag = None
  37. args = m.__code__.co_varnames[1:m.__code__.co_argcount]
  38. dfls = m.__defaults__ or ()
  39. ann = m.__annotations__
  40. nargs = len(args) - len(dfls)
  41. def get_type_from_ann(arg):
  42. return ann[arg][1:] + (' or STDIN','')[parsed] if ann[arg] == 'sstr' else ann[arg].__name__
  43. if parsed:
  44. c_args = [(a,get_type_from_ann(a)) for a in args[:nargs]]
  45. c_kwargs = [(a,dfls[n]) for n,a in enumerate(args[nargs:])]
  46. return c_args,dict(c_kwargs),'STDIN_OK' if c_args and ann[args[0]] == 'sstr' else flag
  47. else:
  48. c_args = ['{} [{}]'.format(a,get_type_from_ann(a)) for a in args[:nargs]]
  49. c_kwargs = ['"{}" [{}={!r}{}]'.format(
  50. a, type(dfls[n]).__name__, dfls[n],
  51. (' ' + ann[a] if a in ann else ''))
  52. for n,a in enumerate(args[nargs:])]
  53. return ' '.join(c_args + c_kwargs)
  54. def _usage(cmd=None,exit_val=1):
  55. m1=('USAGE INFORMATION FOR MMGEN-TOOL COMMANDS:\n\n'
  56. ' Unquoted arguments are mandatory\n'
  57. ' Quoted arguments are optional, default values will be used\n'
  58. ' Argument types and default values are shown in square brackets\n')
  59. m2=(' To force a command to read from STDIN instead of file (for commands taking\n'
  60. ' a filename as their first argument), substitute "-" for the filename.\n\n'
  61. 'EXAMPLES:\n\n'
  62. ' Generate a random Bech32 public/private keypair for LTC:\n'
  63. ' $ mmgen-tool -r0 --coin=ltc --type=bech32 randpair\n\n'
  64. ' Generate a DASH compressed public key address from the supplied WIF key:\n'
  65. ' $ mmgen-tool --coin=dash --type=compressed wif2addr XJkVRC3eGKurc9Uzx1wfQoio3yqkmaXVqLMTa6y7s3M3jTBnmxfw\n\n'
  66. ' Generate a well-known burn address:\n'
  67. ' $ mmgen-tool hextob58chk 000000000000000000000000000000000000000000\n\n'
  68. ' Generate a random 12-word seed phrase:\n'
  69. ' $ mmgen-tool -r0 mn_rand128\n\n'
  70. ' Same as above, but get additional entropy from user:\n'
  71. ' $ mmgen-tool mn_rand128\n\n'
  72. ' Encode bytes from a file to base 58:\n'
  73. ' $ mmgen-tool bytestob58 /etc/timezone pad=20\n\n'
  74. ' Reverse a hex string:\n'
  75. ' $ mmgen-tool hexreverse "deadbeefcafe"\n\n'
  76. ' Same as above, but use a pipe:\n'
  77. ' $ echo "deadbeefcafe" | mmgen-tool hexreverse -')
  78. if not cmd:
  79. Msg(m1)
  80. for bc in MMGenToolCmds.classes.values():
  81. cls_info = bc.__doc__.strip().split('\n')[0]
  82. Msg(' {}{}\n'.format(cls_info[0].upper(),cls_info[1:]))
  83. max_w = max(map(len,bc.user_commands))
  84. for cmd in sorted(bc.user_commands):
  85. Msg(' {:{w}} {}'.format(cmd,_create_call_sig(cmd),w=max_w))
  86. Msg('')
  87. Msg(m2)
  88. elif cmd in MMGenToolCmds:
  89. msg('{}'.format(capfirst(MMGenToolCmds[cmd].__doc__.strip())))
  90. msg('USAGE: {} {} {}'.format(g.prog_name,cmd,_create_call_sig(cmd)))
  91. else:
  92. die(1,"'{}': no such tool command".format(cmd))
  93. sys.exit(exit_val)
  94. def _process_args(cmd,cmd_args):
  95. c_args,c_kwargs,flag = _create_call_sig(cmd,parsed=True)
  96. have_stdin_input = False
  97. if flag != 'VAR_ARGS':
  98. if len(cmd_args) < len(c_args):
  99. m1 = 'Command requires exactly {} non-keyword argument{}'
  100. msg(m1.format(len(c_args),suf(c_args)))
  101. _usage(cmd)
  102. u_args = cmd_args[:len(c_args)]
  103. # If we're reading from a pipe, replace '-' with output of previous command
  104. if flag == 'STDIN_OK' and u_args and u_args[0] == '-':
  105. if sys.stdin.isatty():
  106. raise BadFilename("Standard input is a TTY. Can't use '-' as a filename")
  107. else:
  108. max_dlen_spec = '10kB' # limit input to 10KB for now
  109. max_dlen = MMGenToolCmdUtil().bytespec(max_dlen_spec)
  110. u_args[0] = os.read(0,max_dlen)
  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 .obj import MMGenAddrType
  190. def conv_cls_bip39():
  191. from .bip39 import bip39
  192. return bip39
  193. dfl_mnemonic_fmt = 'mmgen'
  194. mnemonic_fmts = {
  195. 'mmgen': { 'fmt': 'words', 'conv_cls': lambda: baseconv },
  196. 'bip39': { 'fmt': 'bip39', 'conv_cls': conv_cls_bip39 },
  197. 'xmrseed': { 'fmt': 'xmrseed','conv_cls': lambda: baseconv },
  198. }
  199. mn_opts_disp = _options_annot_str(mnemonic_fmts)
  200. class MMGenToolCmdMeta(type):
  201. classes = {}
  202. methods = {}
  203. def __new__(mcls,name,bases,namespace):
  204. methods = {k:v for k,v in namespace.items() if k[0] != '_' and callable(v) and v.__doc__}
  205. if g.test_suite:
  206. if name in mcls.classes:
  207. raise ValueError(f'Class {name!r} already defined!')
  208. for m in methods:
  209. if m in mcls.methods:
  210. raise ValueError(f'Method {m!r} already defined!')
  211. if not getattr(m,'__doc__',None):
  212. raise ValueError(f'Method {m!r} has no doc string!')
  213. cls = super().__new__(mcls,name,bases,namespace)
  214. if bases and name != 'tool_api':
  215. mcls.classes[name] = cls
  216. mcls.methods.update(methods)
  217. return cls
  218. def __iter__(cls):
  219. return cls.methods.__iter__()
  220. def __getitem__(cls,val):
  221. return cls.methods.__getitem__(val)
  222. def __contains__(cls,val):
  223. return cls.methods.__contains__(val)
  224. def classname(cls,cmd_name):
  225. return cls.methods[cmd_name].__qualname__.split('.')[0]
  226. def call(cls,cmd_name,*args,**kwargs):
  227. return getattr(cls.classes[cls.classname(cmd_name)](),cmd_name)(*args,**kwargs)
  228. @property
  229. def user_commands(cls):
  230. return {k:v for k,v in cls.__dict__.items() if k in cls.methods}
  231. class MMGenToolCmds(metaclass=MMGenToolCmdMeta):
  232. def __init__(self,proto=None,mmtype=None):
  233. from .protocol import init_proto_from_opts
  234. self.proto = proto or init_proto_from_opts()
  235. self.mmtype = mmtype or getattr(opt,'type',None) or self.proto.dfl_mmtype
  236. if g.token:
  237. self.proto.tokensym = g.token.upper()
  238. def init_generators(self,arg=None):
  239. gd = namedtuple('generator_data',['at','kg','ag'])
  240. at = MMGenAddrType(
  241. proto = self.proto,
  242. id_str = self.mmtype )
  243. if arg == 'addrtype_only':
  244. return gd(at,None,None)
  245. else:
  246. return gd(
  247. at,
  248. KeyGenerator(self.proto,at),
  249. AddrGenerator(self.proto,at),
  250. )
  251. class MMGenToolCmdMisc(MMGenToolCmds):
  252. "miscellaneous commands"
  253. def help(self,command_name=''):
  254. "display usage information for a single command or all commands"
  255. _usage(command_name,exit_val=0)
  256. usage = help
  257. class MMGenToolCmdUtil(MMGenToolCmds):
  258. "general string conversion and hashing utilities"
  259. def bytespec(self,dd_style_byte_specifier:str):
  260. "convert a byte specifier such as '1GB' into an integer"
  261. return parse_bytespec(dd_style_byte_specifier)
  262. def randhex(self,nbytes='32'):
  263. "print 'n' bytes (default 32) of random data in hex format"
  264. return get_random(int(nbytes)).hex()
  265. def hexreverse(self,hexstr:'sstr'):
  266. "reverse bytes of a hexadecimal string"
  267. return bytes.fromhex(hexstr.strip())[::-1].hex()
  268. def hexlify(self,infile:str):
  269. "convert bytes in file to hexadecimal (use '-' for stdin)"
  270. data = get_data_from_file(infile,dash=True,quiet=True,binary=True)
  271. return data.hex()
  272. def unhexlify(self,hexstr:'sstr'):
  273. "convert hexadecimal value to bytes (warning: outputs binary data)"
  274. return bytes.fromhex(hexstr)
  275. def hexdump(self,infile:str,cols=8,line_nums='hex'):
  276. "create hexdump of data from file (use '-' for stdin)"
  277. data = get_data_from_file(infile,dash=True,quiet=True,binary=True)
  278. return pretty_hexdump(data,cols=cols,line_nums=line_nums).rstrip()
  279. def unhexdump(self,infile:str):
  280. "decode hexdump from file (use '-' for stdin) (warning: outputs binary data)"
  281. if g.platform == 'win':
  282. import msvcrt
  283. msvcrt.setmode(sys.stdout.fileno(),os.O_BINARY)
  284. hexdata = get_data_from_file(infile,dash=True,quiet=True)
  285. return decode_pretty_hexdump(hexdata)
  286. def hash160(self,hexstr:'sstr'):
  287. "compute ripemd160(sha256(data)) (convert hex pubkey to hex addr)"
  288. return hash160(hexstr)
  289. def hash256(self,string_or_bytes:str,file_input=False,hex_input=False): # TODO: handle stdin
  290. "compute sha256(sha256(data)) (double sha256)"
  291. from hashlib import sha256
  292. if file_input: b = get_data_from_file(string_or_bytes,binary=True)
  293. elif hex_input: b = decode_pretty_hexdump(string_or_bytes)
  294. else: b = string_or_bytes
  295. return sha256(sha256(b.encode()).digest()).hexdigest()
  296. def id6(self,infile:str):
  297. "generate 6-character MMGen ID for a file (use '-' for stdin)"
  298. return make_chksum_6(
  299. get_data_from_file(infile,dash=True,quiet=True,binary=True))
  300. def str2id6(self,string:'sstr'): # retain ignoring of space for backwards compat
  301. "generate 6-character MMGen ID for a string, ignoring spaces"
  302. return make_chksum_6(''.join(string.split()))
  303. def id8(self,infile:str):
  304. "generate 8-character MMGen ID for a file (use '-' for stdin)"
  305. return make_chksum_8(
  306. get_data_from_file(infile,dash=True,quiet=True,binary=True))
  307. def randb58(self,nbytes=32,pad=0):
  308. "generate random data (default: 32 bytes) and convert it to base 58"
  309. return baseconv.frombytes(get_random(nbytes),'b58',pad=pad,tostr=True)
  310. def bytestob58(self,infile:str,pad=0):
  311. "convert bytes to base 58 (supply data via STDIN)"
  312. data = get_data_from_file(infile,dash=True,quiet=True,binary=True)
  313. return baseconv.frombytes(data,'b58',pad=pad,tostr=True)
  314. def b58tobytes(self,b58num:'sstr',pad=0):
  315. "convert a base 58 number to bytes (warning: outputs binary data)"
  316. return baseconv.tobytes(b58num,'b58',pad=pad)
  317. def hextob58(self,hexstr:'sstr',pad=0):
  318. "convert a hexadecimal number to base 58"
  319. return baseconv.fromhex(hexstr,'b58',pad=pad,tostr=True)
  320. def b58tohex(self,b58num:'sstr',pad=0):
  321. "convert a base 58 number to hexadecimal"
  322. return baseconv.tohex(b58num,'b58',pad=pad)
  323. def hextob58chk(self,hexstr:'sstr'):
  324. "convert a hexadecimal number to base58-check encoding"
  325. from .protocol import _b58chk_encode
  326. return _b58chk_encode(bytes.fromhex(hexstr))
  327. def b58chktohex(self,b58chk_num:'sstr'):
  328. "convert a base58-check encoded number to hexadecimal"
  329. from .protocol import _b58chk_decode
  330. return _b58chk_decode(b58chk_num).hex()
  331. def hextob32(self,hexstr:'sstr',pad=0):
  332. "convert a hexadecimal number to MMGen's flavor of base 32"
  333. return baseconv.fromhex(hexstr,'b32',pad,tostr=True)
  334. def b32tohex(self,b32num:'sstr',pad=0):
  335. "convert an MMGen-flavor base 32 number to hexadecimal"
  336. return baseconv.tohex(b32num.upper(),'b32',pad)
  337. def hextob6d(self,hexstr:'sstr',pad=0,add_spaces=True):
  338. "convert a hexadecimal number to die roll base6 (base6d)"
  339. ret = baseconv.fromhex(hexstr,'b6d',pad,tostr=True)
  340. return block_format(ret,gw=5,cols=None).strip() if add_spaces else ret
  341. def b6dtohex(self,b6d_num:'sstr',pad=0):
  342. "convert a die roll base6 (base6d) number to hexadecimal"
  343. return baseconv.tohex(remove_whitespace(b6d_num),'b6d',pad)
  344. class MMGenToolCmdCoin(MMGenToolCmds):
  345. """
  346. cryptocoin key/address utilities
  347. May require use of the '--coin', '--type' and/or '--testnet' options
  348. Examples:
  349. mmgen-tool --coin=ltc --type=bech32 wif2addr <wif key>
  350. mmgen-tool --coin=zec --type=zcash_z randpair
  351. """
  352. def randwif(self):
  353. "generate a random private key in WIF format"
  354. gd = self.init_generators('addrtype_only')
  355. return PrivKey(
  356. self.proto,
  357. get_random(32),
  358. pubkey_type = gd.at.pubkey_type,
  359. compressed = gd.at.compressed ).wif
  360. def randpair(self):
  361. "generate a random private key/address pair"
  362. gd = self.init_generators()
  363. privhex = PrivKey(
  364. self.proto,
  365. get_random(32),
  366. pubkey_type = gd.at.pubkey_type,
  367. compressed = gd.at.compressed )
  368. addr = gd.ag.to_addr(gd.kg.to_pubhex(privhex))
  369. return (privhex.wif,addr)
  370. def wif2hex(self,wifkey:'sstr'):
  371. "convert a private key from WIF to hex format"
  372. return PrivKey(
  373. self.proto,
  374. wif = wifkey )
  375. def hex2wif(self,privhex:'sstr'):
  376. "convert a private key from hex to WIF format"
  377. gd = self.init_generators('addrtype_only')
  378. return PrivKey(
  379. self.proto,
  380. bytes.fromhex(privhex),
  381. pubkey_type = gd.at.pubkey_type,
  382. compressed = gd.at.compressed ).wif
  383. def wif2addr(self,wifkey:'sstr'):
  384. "generate a coin address from a key in WIF format"
  385. gd = self.init_generators()
  386. privhex = PrivKey(
  387. self.proto,
  388. wif = wifkey )
  389. addr = gd.ag.to_addr(gd.kg.to_pubhex(privhex))
  390. return addr
  391. def wif2redeem_script(self,wifkey:'sstr'): # new
  392. "convert a WIF private key to a Segwit P2SH-P2WPKH redeem script"
  393. assert self.mmtype == 'segwit','This command is meaningful only for --type=segwit'
  394. gd = self.init_generators()
  395. privhex = PrivKey(
  396. self.proto,
  397. wif = wifkey )
  398. return gd.ag.to_segwit_redeem_script(gd.kg.to_pubhex(privhex))
  399. def wif2segwit_pair(self,wifkey:'sstr'):
  400. "generate both a Segwit P2SH-P2WPKH redeem script and address from WIF"
  401. assert self.mmtype == 'segwit','This command is meaningful only for --type=segwit'
  402. gd = self.init_generators()
  403. pubhex = gd.kg.to_pubhex(PrivKey(
  404. self.proto,
  405. wif = wifkey ))
  406. addr = gd.ag.to_addr(pubhex)
  407. rs = gd.ag.to_segwit_redeem_script(pubhex)
  408. return (rs,addr)
  409. def privhex2addr(self,privhex:'sstr',output_pubhex=False):
  410. "generate coin address from raw private key data in hexadecimal format"
  411. gd = self.init_generators()
  412. pk = PrivKey(
  413. self.proto,
  414. bytes.fromhex(privhex),
  415. compressed = gd.at.compressed,
  416. pubkey_type = gd.at.pubkey_type )
  417. ph = gd.kg.to_pubhex(pk)
  418. return ph if output_pubhex else gd.ag.to_addr(ph)
  419. def privhex2pubhex(self,privhex:'sstr'): # new
  420. "generate a hex public key from a hex private key"
  421. return self.privhex2addr(privhex,output_pubhex=True)
  422. def pubhex2addr(self,pubkeyhex:'sstr'):
  423. "convert a hex pubkey to an address"
  424. if self.mmtype == 'segwit':
  425. return self.proto.pubhex2segwitaddr(pubkeyhex)
  426. else:
  427. return self.pubhash2addr(hash160(pubkeyhex))
  428. def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new
  429. "convert a hex pubkey to a Segwit P2SH-P2WPKH redeem script"
  430. assert self.mmtype == 'segwit','This command is meaningful only for --type=segwit'
  431. return self.proto.pubhex2redeem_script(pubkeyhex)
  432. def redeem_script2addr(self,redeem_scripthex:'sstr'): # new
  433. "convert a Segwit P2SH-P2WPKH redeem script to an address"
  434. assert self.mmtype == 'segwit','This command is meaningful only for --type=segwit'
  435. assert redeem_scripthex[:4] == '0014','{!r}: invalid redeem script'.format(redeem_scripthex)
  436. assert len(redeem_scripthex) == 44,'{} bytes: invalid redeem script length'.format(len(redeem_scripthex)//2)
  437. return self.pubhash2addr(hash160(redeem_scripthex))
  438. def pubhash2addr(self,pubhashhex:'sstr'):
  439. "convert public key hash to address"
  440. if self.mmtype == 'bech32':
  441. return self.proto.pubhash2bech32addr(pubhashhex)
  442. else:
  443. gd = self.init_generators('addrtype_only')
  444. return self.proto.pubhash2addr(pubhashhex,gd.at.addr_fmt=='p2sh')
  445. def addr2pubhash(self,addr:'sstr'):
  446. "convert coin address to public key hash"
  447. from .tx import addr2pubhash
  448. return addr2pubhash(self.proto,CoinAddr(self.proto,addr))
  449. def addr2scriptpubkey(self,addr:'sstr'):
  450. "convert coin address to scriptPubKey"
  451. from .tx import addr2scriptPubKey
  452. return addr2scriptPubKey(self.proto,CoinAddr(self.proto,addr))
  453. def scriptpubkey2addr(self,hexstr:'sstr'):
  454. "convert scriptPubKey to coin address"
  455. from .tx import scriptPubKey2addr
  456. return scriptPubKey2addr(self.proto,hexstr)[0]
  457. class MMGenToolCmdMnemonic(MMGenToolCmds):
  458. """
  459. seed phrase utilities (valid formats: 'mmgen' (default), 'bip39', 'xmrseed')
  460. IMPORTANT NOTE: MMGen's default seed phrase format uses the Electrum
  461. wordlist, however seed phrases are computed using a different algorithm
  462. and are NOT Electrum-compatible!
  463. BIP39 support is fully compatible with the standard, allowing users to
  464. import and export seed entropy from BIP39-compatible wallets. However,
  465. users should be aware that BIP39 support does not imply BIP32 support!
  466. MMGen uses its own key derivation scheme differing from the one described
  467. by the BIP32 protocol.
  468. For Monero ('xmrseed') seed phrases, input data is reduced to a spendkey
  469. before conversion so that a canonical seed phrase is produced. This is
  470. required because Monero seeds, unlike ordinary wallet seeds, are tied
  471. to a concrete key/address pair. To manually generate a Monero spendkey,
  472. use the 'hex2wif' command.
  473. """
  474. @staticmethod
  475. def _xmr_reduce(bytestr):
  476. from .protocol import init_proto
  477. proto = init_proto('xmr')
  478. if len(bytestr) != proto.privkey_len:
  479. m = '{!r}: invalid bit length for Monero private key (must be {})'
  480. die(1,m.format(len(bytestr*8),proto.privkey_len*8))
  481. return proto.preprocess_key(bytestr,None)
  482. def _do_random_mn(self,nbytes:int,fmt:str):
  483. assert nbytes in (16,24,32), 'nbytes must be 16, 24 or 32'
  484. randbytes = get_random(nbytes)
  485. if fmt == 'xmrseed':
  486. randbytes = self._xmr_reduce(randbytes)
  487. if opt.verbose:
  488. msg('Seed: {}'.format(randbytes.hex()))
  489. return self.hex2mn(randbytes.hex(),fmt=fmt)
  490. def mn_rand128(self, fmt:mn_opts_disp = dfl_mnemonic_fmt ):
  491. "generate random 128-bit mnemonic seed phrase"
  492. return self._do_random_mn(16,fmt)
  493. def mn_rand192(self, fmt:mn_opts_disp = dfl_mnemonic_fmt ):
  494. "generate random 192-bit mnemonic seed phrase"
  495. return self._do_random_mn(24,fmt)
  496. def mn_rand256(self, fmt:mn_opts_disp = dfl_mnemonic_fmt ):
  497. "generate random 256-bit mnemonic seed phrase"
  498. return self._do_random_mn(32,fmt)
  499. def hex2mn( self, hexstr:'sstr', fmt:mn_opts_disp = dfl_mnemonic_fmt ):
  500. "convert a 16, 24 or 32-byte hexadecimal number to a mnemonic seed phrase"
  501. if fmt == 'bip39':
  502. from .bip39 import bip39
  503. return ' '.join(bip39.fromhex(hexstr,fmt))
  504. else:
  505. bytestr = bytes.fromhex(hexstr)
  506. if fmt == 'xmrseed':
  507. bytestr = self._xmr_reduce(bytestr)
  508. return baseconv.frombytes(bytestr,fmt,'seed',tostr=True)
  509. def mn2hex( self, seed_mnemonic:'sstr', fmt:mn_opts_disp = dfl_mnemonic_fmt ):
  510. "convert a mnemonic seed phrase to a hexadecimal number"
  511. if fmt == 'bip39':
  512. from .bip39 import bip39
  513. return bip39.tohex(seed_mnemonic.split(),fmt)
  514. else:
  515. return baseconv.tohex(seed_mnemonic.split(),fmt,'seed')
  516. def mn2hex_interactive( self, fmt:mn_opts_disp = dfl_mnemonic_fmt, mn_len=24, print_mn=False ):
  517. "convert an interactively supplied mnemonic seed phrase to a hexadecimal number"
  518. from .mn_entry import mn_entry
  519. mn = mn_entry(fmt).get_mnemonic_from_user(25 if fmt == 'xmrseed' else mn_len,validate=False)
  520. if print_mn:
  521. msg(mn)
  522. return self.mn2hex(seed_mnemonic=mn,fmt=fmt)
  523. def mn_stats(self, fmt:mn_opts_disp = dfl_mnemonic_fmt ):
  524. "show stats for mnemonic wordlist"
  525. conv_cls = mnemonic_fmts[fmt]['conv_cls']()
  526. return conv_cls.check_wordlist(fmt)
  527. def mn_printlist( self, fmt:mn_opts_disp = dfl_mnemonic_fmt, enum=False, pager=False ):
  528. "print mnemonic wordlist"
  529. conv_cls = mnemonic_fmts[fmt]['conv_cls']()
  530. ret = conv_cls.get_wordlist(fmt)
  531. if enum:
  532. ret = ['{:>4} {}'.format(n,e) for n,e in enumerate(ret)]
  533. return '\n'.join(ret)
  534. class MMGenToolCmdFile(MMGenToolCmds):
  535. "utilities for viewing/checking MMGen address and transaction files"
  536. def _file_chksum(self,mmgen_addrfile,objname):
  537. verbose,yes,quiet = [bool(i) for i in (opt.verbose,opt.yes,opt.quiet)]
  538. opt.verbose,opt.yes,opt.quiet = (False,True,True)
  539. ret = globals()[objname](self.proto,mmgen_addrfile)
  540. opt.verbose,opt.yes,opt.quiet = (verbose,yes,quiet)
  541. if verbose:
  542. if ret.al_id.mmtype.name == 'password':
  543. msg('Passwd fmt: {}\nPasswd len: {}\nID string: {}'.format(
  544. capfirst(ret.pw_info[ret.pw_fmt].desc),
  545. ret.pw_len,
  546. ret.pw_id_str ))
  547. else:
  548. msg(f'Base coin: {ret.base_coin} {capfirst(ret.network)}')
  549. msg(f'MMType: {capfirst(ret.al_id.mmtype.name)}')
  550. msg( f'List length: {len(ret.data)}')
  551. return ret.chksum
  552. def addrfile_chksum(self,mmgen_addrfile:str):
  553. "compute checksum for MMGen address file"
  554. return self._file_chksum(mmgen_addrfile,'AddrList')
  555. def keyaddrfile_chksum(self,mmgen_keyaddrfile:str):
  556. "compute checksum for MMGen key-address file"
  557. return self._file_chksum(mmgen_keyaddrfile,'KeyAddrList')
  558. def passwdfile_chksum(self,mmgen_passwdfile:str):
  559. "compute checksum for MMGen password file"
  560. return self._file_chksum(mmgen_passwdfile,'PasswordList')
  561. async def txview( varargs_call_sig = { # hack to allow for multiple filenames
  562. 'args': (
  563. 'mmgen_tx_file(s)',
  564. 'pager',
  565. 'terse',
  566. 'sort',
  567. 'filesort' ),
  568. 'dfls': ( False, False, 'addr', 'mtime' ),
  569. 'annots': {
  570. 'mmgen_tx_file(s)': str,
  571. 'sort': _options_annot_str(['addr','raw']),
  572. 'filesort': _options_annot_str(['mtime','ctime','atime']),
  573. } },
  574. *infiles,**kwargs):
  575. "show raw/signed MMGen transaction in human-readable form"
  576. terse = bool(kwargs.get('terse'))
  577. tx_sort = kwargs.get('sort') or 'addr'
  578. file_sort = kwargs.get('filesort') or 'mtime'
  579. from .filename import MMGenFileList
  580. from .tx import MMGenTX
  581. flist = MMGenFileList(infiles,ftype=MMGenTX)
  582. flist.sort_by_age(key=file_sort) # in-place sort
  583. async def process_file(fn):
  584. if fn.endswith(MMGenTX.Signed.ext):
  585. tx = MMGenTX.Signed(
  586. filename = fn,
  587. quiet_open = True,
  588. tw = await MMGenTX.Signed.get_tracking_wallet(fn) )
  589. else:
  590. tx = MMGenTX.Unsigned(
  591. filename = fn,
  592. quiet_open = True )
  593. return tx.format_view(terse=terse,sort=tx_sort)
  594. return ('—'*77+'\n').join([await process_file(fn) for fn in flist.names()]).rstrip()
  595. class MMGenToolCmdFileCrypt(MMGenToolCmds):
  596. """
  597. file encryption and decryption
  598. MMGen encryption suite:
  599. * Key: Scrypt (user-configurable hash parameters, 32-byte salt)
  600. * Enc: AES256_CTR, 16-byte rand IV, sha256 hash + 32-byte nonce + data
  601. * The encrypted file is indistinguishable from random data
  602. """
  603. def encrypt(self,infile:str,outfile='',hash_preset=''):
  604. "encrypt a file"
  605. data = get_data_from_file(infile,'data for encryption',binary=True)
  606. enc_d = mmgen_encrypt(data,'user data',hash_preset)
  607. if not outfile:
  608. outfile = '{}.{}'.format(os.path.basename(infile),g.mmenc_ext)
  609. write_data_to_file(outfile,enc_d,'encrypted data',binary=True)
  610. return True
  611. def decrypt(self,infile:str,outfile='',hash_preset=''):
  612. "decrypt a file"
  613. enc_d = get_data_from_file(infile,'encrypted data',binary=True)
  614. while True:
  615. dec_d = mmgen_decrypt(enc_d,'user data',hash_preset)
  616. if dec_d: break
  617. msg('Trying again...')
  618. if not outfile:
  619. o = os.path.basename(infile)
  620. outfile = remove_extension(o,g.mmenc_ext)
  621. if outfile == o: outfile += '.dec'
  622. write_data_to_file(outfile,dec_d,'decrypted data',binary=True)
  623. return True
  624. class MMGenToolCmdFileUtil(MMGenToolCmds):
  625. "file utilities"
  626. def find_incog_data(self,filename:str,incog_id:str,keep_searching=False):
  627. "Use an Incog ID to find hidden incognito wallet data"
  628. ivsize,bsize,mod = g.aesctr_iv_len,4096,4096*8
  629. n,carry = 0,b' '*ivsize
  630. flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
  631. f = os.open(filename,flgs)
  632. for ch in incog_id:
  633. if ch not in '0123456789ABCDEF':
  634. die(2,"'{}': invalid Incog ID".format(incog_id))
  635. while True:
  636. d = os.read(f,bsize)
  637. if not d: break
  638. d = carry + d
  639. for i in range(bsize):
  640. if sha256(d[i:i+ivsize]).hexdigest()[:8].upper() == incog_id:
  641. if n+i < ivsize: continue
  642. msg('\rIncog data for ID {} found at offset {}'.format(incog_id,n+i-ivsize))
  643. if not keep_searching: sys.exit(0)
  644. carry = d[len(d)-ivsize:]
  645. n += bsize
  646. if not n % mod:
  647. msg_r('\rSearched: {} bytes'.format(n))
  648. msg('')
  649. os.close(f)
  650. return True
  651. def rand2file(self,outfile:str,nbytes:str,threads=4,silent=False):
  652. "write 'n' bytes of random data to specified file"
  653. from threading import Thread
  654. from queue import Queue
  655. from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
  656. from cryptography.hazmat.backends import default_backend
  657. def encrypt_worker(wid):
  658. ctr_init_val = os.urandom(g.aesctr_iv_len)
  659. c = Cipher(algorithms.AES(key),modes.CTR(ctr_init_val),backend=default_backend())
  660. encryptor = c.encryptor()
  661. while True:
  662. q2.put(encryptor.update(q1.get()))
  663. q1.task_done()
  664. def output_worker():
  665. while True:
  666. f.write(q2.get())
  667. q2.task_done()
  668. nbytes = parse_bytespec(nbytes)
  669. if opt.outdir:
  670. outfile = make_full_path(opt.outdir,outfile)
  671. f = open(outfile,'wb')
  672. key = get_random(32)
  673. q1,q2 = Queue(),Queue()
  674. for i in range(max(1,threads-2)):
  675. t = Thread(target=encrypt_worker,args=[i])
  676. t.daemon = True
  677. t.start()
  678. t = Thread(target=output_worker)
  679. t.daemon = True
  680. t.start()
  681. blk_size = 1024 * 1024
  682. for i in range(nbytes // blk_size):
  683. if not i % 4:
  684. msg_r('\rRead: {} bytes'.format(i * blk_size))
  685. q1.put(os.urandom(blk_size))
  686. if nbytes % blk_size:
  687. q1.put(os.urandom(nbytes % blk_size))
  688. q1.join()
  689. q2.join()
  690. f.close()
  691. fsize = os.stat(outfile).st_size
  692. if fsize != nbytes:
  693. die(3,'{}: incorrect random file size (should be {})'.format(fsize,nbytes))
  694. if not silent:
  695. msg('\rRead: {} bytes'.format(nbytes))
  696. qmsg("\r{} byte{} of random data written to file '{}'".format(nbytes,suf(nbytes),outfile))
  697. return True
  698. class MMGenToolCmdWallet(MMGenToolCmds):
  699. "key, address or subseed generation from an MMGen wallet"
  700. def get_subseed(self,subseed_idx:str,wallet=''):
  701. "get the Seed ID of a single subseed by Subseed Index for default or specified wallet"
  702. opt.quiet = True
  703. sf = get_seed_file([wallet] if wallet else [],1)
  704. from .wallet import Wallet
  705. return Wallet(sf).seed.subseed(subseed_idx).sid
  706. def get_subseed_by_seed_id(self,seed_id:str,wallet='',last_idx=g.subseeds):
  707. "get the Subseed Index of a single subseed by Seed ID for default or specified wallet"
  708. opt.quiet = True
  709. sf = get_seed_file([wallet] if wallet else [],1)
  710. from .wallet import Wallet
  711. ret = Wallet(sf).seed.subseed_by_seed_id(seed_id,last_idx)
  712. return ret.ss_idx if ret else None
  713. def list_subseeds(self,subseed_idx_range:str,wallet=''):
  714. "list a range of subseed Seed IDs for default or specified wallet"
  715. opt.quiet = True
  716. sf = get_seed_file([wallet] if wallet else [],1)
  717. from .wallet import Wallet
  718. return Wallet(sf).seed.subseeds.format(*SubSeedIdxRange(subseed_idx_range))
  719. def list_shares(self,
  720. share_count:int,
  721. id_str='default',
  722. master_share:"(min:1, max:{}, 0=no master share)".format(MasterShareIdx.max_val)=0,
  723. wallet=''):
  724. "list the Seed IDs of the shares resulting from a split of default or specified wallet"
  725. opt.quiet = True
  726. sf = get_seed_file([wallet] if wallet else [],1)
  727. from .wallet import Wallet
  728. return Wallet(sf).seed.split(share_count,id_str,master_share).format()
  729. def gen_key(self,mmgen_addr:str,wallet=''):
  730. "generate a single MMGen WIF key from default or specified wallet"
  731. return self.gen_addr(mmgen_addr,wallet,target='wif')
  732. def gen_addr(self,mmgen_addr:str,wallet='',target='addr'):
  733. "generate a single MMGen address from default or specified wallet"
  734. addr = MMGenID(self.proto,mmgen_addr)
  735. opt.quiet = True
  736. sf = get_seed_file([wallet] if wallet else [],1)
  737. from .wallet import Wallet
  738. ss = Wallet(sf)
  739. if ss.seed.sid != addr.sid:
  740. m = 'Seed ID of requested address ({}) does not match wallet ({})'
  741. die(1,m.format(addr.sid,ss.seed.sid))
  742. al = AddrList(
  743. proto = self.proto,
  744. seed = ss.seed,
  745. addr_idxs = AddrIdxList(str(addr.idx)),
  746. mmtype = addr.mmtype )
  747. d = al.data[0]
  748. ret = d.sec.wif if target=='wif' else d.addr
  749. return ret
  750. from .tw import TwAddrList,TwUnspentOutputs
  751. class MMGenToolCmdRPC(MMGenToolCmds):
  752. "tracking wallet commands using the JSON-RPC interface"
  753. async def getbalance(self,minconf=1,quiet=False,pager=False):
  754. "list confirmed/unconfirmed, spendable/unspendable balances in tracking wallet"
  755. from .tw import TwGetBalance
  756. return (await TwGetBalance(self.proto,minconf,quiet)).format()
  757. async def listaddress(self,
  758. mmgen_addr:str,
  759. minconf = 1,
  760. pager = False,
  761. showempty = True,
  762. showbtcaddr = True,
  763. age_fmt: _options_annot_str(TwAddrList.age_fmts) = 'confs',
  764. ):
  765. "list the specified MMGen address and its balance"
  766. return await self.listaddresses( mmgen_addrs = mmgen_addr,
  767. minconf = minconf,
  768. pager = pager,
  769. showempty = showempty,
  770. showbtcaddrs = showbtcaddr,
  771. age_fmt = age_fmt,
  772. )
  773. async def listaddresses( self,
  774. mmgen_addrs:'(range or list)' = '',
  775. minconf = 1,
  776. showempty = False,
  777. pager = False,
  778. showbtcaddrs = True,
  779. all_labels = False,
  780. sort: _options_annot_str(['reverse','age']) = '',
  781. age_fmt: _options_annot_str(TwAddrList.age_fmts) = 'confs',
  782. ):
  783. "list MMGen addresses and their balances"
  784. show_age = bool(age_fmt)
  785. if sort:
  786. sort = set(sort.split(','))
  787. sort_params = {'reverse','age'}
  788. if not sort.issubset(sort_params):
  789. die(1,"The sort option takes the following parameters: '{}'".format("','".join(sort_params)))
  790. usr_addr_list = []
  791. if mmgen_addrs:
  792. a = mmgen_addrs.rsplit(':',1)
  793. if len(a) != 2:
  794. m = "'{}': invalid address list argument (must be in form <seed ID>:[<type>:]<idx list>)"
  795. die(1,m.format(mmgen_addrs))
  796. usr_addr_list = [MMGenID(self.proto,f'{a[0]}:{i}') for i in AddrIdxList(a[1])]
  797. al = await TwAddrList(self.proto,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels)
  798. if not al:
  799. die(0,('No tracked addresses with balances!','No tracked addresses!')[showempty])
  800. return await al.format(showbtcaddrs,sort,show_age,age_fmt or 'confs')
  801. async def twview( self,
  802. pager = False,
  803. reverse = False,
  804. wide = False,
  805. minconf = 1,
  806. sort = 'age',
  807. age_fmt: _options_annot_str(TwUnspentOutputs.age_fmts) = 'confs',
  808. show_mmid = True,
  809. wide_show_confs = True):
  810. "view tracking wallet"
  811. twuo = await TwUnspentOutputs(self.proto,minconf=minconf)
  812. await twuo.get_unspent_data(reverse_sort=reverse)
  813. twuo.age_fmt = age_fmt
  814. twuo.show_mmid = show_mmid
  815. if wide:
  816. ret = twuo.format_for_printing(color=True,show_confs=wide_show_confs)
  817. else:
  818. ret = twuo.format_for_display()
  819. del twuo.wallet
  820. return await ret
  821. async def add_label(self,mmgen_or_coin_addr:str,label:str):
  822. "add descriptive label for address in tracking wallet"
  823. from .tw import TrackingWallet
  824. await (await TrackingWallet(self.proto,mode='w')).add_label(mmgen_or_coin_addr,label,on_fail='raise')
  825. return True
  826. async def remove_label(self,mmgen_or_coin_addr:str):
  827. "remove descriptive label for address in tracking wallet"
  828. await self.add_label(mmgen_or_coin_addr,'')
  829. return True
  830. async def remove_address(self,mmgen_or_coin_addr:str):
  831. "remove an address from tracking wallet"
  832. from .tw import TrackingWallet
  833. ret = await (await TrackingWallet(self.proto,mode='w')).remove_address(mmgen_or_coin_addr) # returns None on failure
  834. if ret:
  835. msg("Address '{}' deleted from tracking wallet".format(ret))
  836. return ret
  837. class MMGenToolCmdMonero(MMGenToolCmds):
  838. """
  839. Monero wallet utilities
  840. Note that the use of these commands requires private data to be exposed on
  841. a network-connected machine in order to unlock the Monero wallets. This is
  842. a violation of good security practice.
  843. """
  844. _monero_chain_height = None
  845. monerod_args = []
  846. @property
  847. def monero_chain_height(self):
  848. if self._monero_chain_height == None:
  849. from .daemon import CoinDaemon
  850. port = CoinDaemon('xmr',test_suite=g.test_suite).rpc_port
  851. cmd = ['monerod','--rpc-bind-port={}'.format(port)] + self.monerod_args + ['status']
  852. from subprocess import run,PIPE,DEVNULL
  853. cp = run(cmd,stdout=PIPE,stderr=DEVNULL,check=True)
  854. import re
  855. m = re.search(r'Height: (\d+)/\d+ ',cp.stdout.decode())
  856. if not m:
  857. die(1,'Unable to connect to monerod!')
  858. self._monero_chain_height = int(m.group(1))
  859. msg('Chain height: {}'.format(self._monero_chain_height))
  860. return self._monero_chain_height
  861. def keyaddrlist2monerowallets( self,
  862. xmr_keyaddrfile:str,
  863. blockheight:'(default: current height)' = 0,
  864. addrs:'(integer range or list)' = ''):
  865. "create Monero wallets from a key-address list"
  866. return self.monero_wallet_ops( infile = xmr_keyaddrfile,
  867. op = 'create',
  868. blockheight = blockheight,
  869. addrs = addrs)
  870. def syncmonerowallets(self,xmr_keyaddrfile:str,addrs:'(integer range or list)'=''):
  871. "sync Monero wallets from a key-address list"
  872. return self.monero_wallet_ops(infile=xmr_keyaddrfile,op='sync',addrs=addrs)
  873. def monero_wallet_ops(self,infile:str,op:str,blockheight=0,addrs='',monerod_args=[]):
  874. if monerod_args:
  875. self.monerod_args = monerod_args
  876. async def create(n,d,fn,c,m):
  877. try: os.stat(fn)
  878. except: pass
  879. else:
  880. ymsg("Wallet '{}' already exists!".format(fn))
  881. return False
  882. gmsg(m)
  883. from .baseconv import baseconv
  884. ret = await c.call(
  885. 'restore_deterministic_wallet',
  886. filename = os.path.basename(fn),
  887. password = d.wallet_passwd,
  888. seed = baseconv.fromhex(d.sec,'xmrseed',tostr=True),
  889. restore_height = blockheight,
  890. language = 'English' )
  891. pp_msg(ret) if opt.debug else msg(' Address: {}'.format(ret['address']))
  892. return True
  893. async def sync(n,d,fn,c,m):
  894. try:
  895. os.stat(fn)
  896. except:
  897. ymsg("Wallet '{}' does not exist!".format(fn))
  898. return False
  899. chain_height = self.monero_chain_height
  900. gmsg(m)
  901. import time
  902. t_start = time.time()
  903. msg_r(' Opening wallet...')
  904. await c.call(
  905. 'open_wallet',
  906. filename=os.path.basename(fn),
  907. password=d.wallet_passwd )
  908. msg('done')
  909. msg_r(' Getting wallet height...')
  910. wallet_height = (await c.call('get_height'))['height']
  911. msg('\r Wallet height: {} '.format(wallet_height))
  912. behind = chain_height - wallet_height
  913. if behind > 1000:
  914. m = ' Wallet is {} blocks behind chain tip. Please be patient. Syncing...'
  915. msg_r(m.format(behind))
  916. ret = await c.call('refresh')
  917. if behind > 1000:
  918. msg('done')
  919. if ret['received_money']:
  920. msg(' Wallet has received funds')
  921. t_elapsed = int(time.time() - t_start)
  922. ret = await c.call('get_balance') # account_index=0, address_indices=[0,1]
  923. from .obj import XMRAmt
  924. bals[fn] = tuple([XMRAmt(ret[k],from_unit='min_coin_unit') for k in ('balance','unlocked_balance')])
  925. if opt.debug:
  926. pp_msg(ret)
  927. else:
  928. msg(' Balance: {} Unlocked balance: {}'.format(*[b.hl() for b in bals[fn]]))
  929. msg(' Wallet height: {}'.format((await c.call('get_height'))['height']))
  930. msg(' Sync time: {:02}:{:02}'.format(t_elapsed//60,t_elapsed%60))
  931. await c.call('close_wallet')
  932. return True
  933. async def process_wallets(op):
  934. g.accept_defaults = g.accept_defaults or op.accept_defaults
  935. from .protocol import init_proto
  936. proto = init_proto('xmr',network='mainnet')
  937. al = KeyAddrList(proto,infile)
  938. data = [d for d in al.data if addrs == '' or d.idx in AddrIdxList(addrs)]
  939. dl = len(data)
  940. assert dl,"No addresses in addrfile within range '{}'".format(addrs)
  941. gmsg('\n{}ing {} wallet{}'.format(op.desc,dl,suf(dl)))
  942. from .daemon import MoneroWalletDaemon
  943. wd = MoneroWalletDaemon(
  944. wallet_dir = opt.outdir or '.',
  945. test_suite = g.test_suite )
  946. wd.restart()
  947. from .rpc import MoneroWalletRPCClient
  948. c = MoneroWalletRPCClient(
  949. host = wd.host,
  950. port = wd.rpc_port,
  951. user = wd.user,
  952. passwd = wd.passwd )
  953. wallets_processed = 0
  954. for n,d in enumerate(data): # [d.sec,d.wallet_passwd,d.viewkey,d.addr]
  955. fn = os.path.join(
  956. opt.outdir or '.','{}-{}-MoneroWallet{}'.format(
  957. al.al_id.sid,
  958. d.idx,
  959. '-α' if g.debug_utf8 else ''))
  960. info = '\n{}ing wallet {}/{} ({})'.format(op.action,n+1,dl,fn)
  961. wallets_processed += await op.func(n,d,fn,c,info)
  962. wd.stop()
  963. gmsg('\n{} wallet{} {}ed'.format(wallets_processed,suf(wallets_processed),op.desc.lower()))
  964. if wallets_processed and op.name == 'sync':
  965. col1_w = max(map(len,bals)) + 1
  966. fs = '{:%s} {} {}' % col1_w
  967. msg('\n'+fs.format('Wallet','Balance ','Unlocked Balance '))
  968. from .obj import XMRAmt
  969. tbals = [XMRAmt('0'),XMRAmt('0')]
  970. for bal in bals:
  971. for i in (0,1): tbals[i] += bals[bal][i]
  972. msg(fs.format(bal+':',*[XMRAmt(b).fmt(fs='5.12',color=True) for b in bals[bal]]))
  973. msg(fs.format('-'*col1_w,'-'*18,'-'*18))
  974. msg(fs.format('TOTAL:',*[XMRAmt(b).fmt(fs='5.12',color=True) for b in tbals]))
  975. if blockheight < 0:
  976. blockheight = 0 # TODO: handle the non-zero case
  977. bals = {} # locked,unlocked
  978. wo = namedtuple('mwo',['name','desc','action','func','accept_defaults'])
  979. op = { # reusing name!
  980. 'create': wo('create', 'Creat', 'Generat', create, False),
  981. 'sync': wo('sync', 'Sync', 'Sync', sync, True) }[op]
  982. try:
  983. run_session(process_wallets(op))
  984. except KeyboardInterrupt:
  985. rdie(1,'\nUser interrupt\n')
  986. except EOFError:
  987. rdie(2,'\nEnd of file\n')
  988. except Exception as e:
  989. try:
  990. die(1,'Error: {}'.format(e.args[0]))
  991. except:
  992. rdie(1,'Error: {!r}'.format(e.args[0]))
  993. return True
  994. class tool_api(
  995. MMGenToolCmdUtil,
  996. MMGenToolCmdCoin,
  997. MMGenToolCmdMnemonic,
  998. ):
  999. """
  1000. API providing access to a subset of methods from the mmgen.tool module
  1001. Example:
  1002. from mmgen.tool import tool_api
  1003. tool = tool_api()
  1004. # Set the coin and network:
  1005. tool.init_coin('btc','mainnet')
  1006. # Print available address types:
  1007. tool.print_addrtypes()
  1008. # Set the address type:
  1009. tool.addrtype = 'segwit'
  1010. # Disable user entropy gathering (optional, reduces security):
  1011. tool.usr_randchars = 0
  1012. # Generate a random BTC segwit keypair:
  1013. wif,addr = tool.randpair()
  1014. # Set coin, network and address type:
  1015. tool.init_coin('ltc','testnet')
  1016. tool.addrtype = 'bech32'
  1017. # Generate a random LTC testnet Bech32 keypair:
  1018. wif,addr = tool.randpair()
  1019. """
  1020. def __init__(self):
  1021. """
  1022. Initializer - takes no arguments
  1023. """
  1024. super().__init__()
  1025. if not hasattr(opt,'version'):
  1026. opts.init()
  1027. self.mmtype = self.proto.dfl_mmtype
  1028. def init_coin(self,coinsym,network):
  1029. """
  1030. Initialize a coin/network pair
  1031. Valid choices for coins: one of the symbols returned by the 'coins' attribute
  1032. Valid choices for network: 'mainnet','testnet','regtest'
  1033. """
  1034. from .protocol import init_proto,init_genonly_altcoins
  1035. altcoin_trust_level = init_genonly_altcoins(coinsym,testnet=network in ('testnet','regtest'))
  1036. warn_altcoins(coinsym,altcoin_trust_level)
  1037. self.proto = init_proto(coinsym,network=network) # FIXME
  1038. return self.proto
  1039. @property
  1040. def coins(self):
  1041. """The available coins"""
  1042. from .protocol import CoinProtocol
  1043. from .altcoin import CoinInfo
  1044. return sorted(set(
  1045. [c.upper() for c in CoinProtocol.coins]
  1046. + [c.symbol for c in CoinInfo.get_supported_coins(self.proto.network)]
  1047. ))
  1048. @property
  1049. def coin(self):
  1050. """The currently configured coin"""
  1051. return self.proto.coin
  1052. @property
  1053. def network(self):
  1054. """The currently configured network"""
  1055. return self.proto.network
  1056. @property
  1057. def addrtypes(self):
  1058. """
  1059. The available address types for current coin/network pair. The
  1060. first-listed is the default
  1061. """
  1062. return [MMGenAddrType(proto=proto,id_str=id_str).name for id_str in self.proto.mmtypes]
  1063. def print_addrtypes(self):
  1064. """
  1065. Print the available address types for current coin/network pair along with
  1066. a description. The first-listed is the default
  1067. """
  1068. for t in [MMGenAddrType(proto=proto,id_str=id_str).name for id_str in self.proto.mmtypes]:
  1069. print('{:<12} - {}'.format(t.name,t.desc))
  1070. @property
  1071. def addrtype(self):
  1072. """The currently configured address type (is assignable)"""
  1073. return self.mmtype
  1074. @addrtype.setter
  1075. def addrtype(self,val):
  1076. self.mmtype = val
  1077. @property
  1078. def usr_randchars(self):
  1079. """
  1080. The number of keystrokes of entropy to be gathered from the user.
  1081. Setting to zero disables user entropy gathering.
  1082. """
  1083. return opt.usr_randchars
  1084. @usr_randchars.setter
  1085. def usr_randchars(self,val):
  1086. opt.usr_randchars = val