util.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962
  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. util.py: Low-level routines imported by other modules in the MMGen suite
  20. """
  21. import sys,os,time,stat,re,unicodedata
  22. from hashlib import sha256
  23. from binascii import hexlify,unhexlify
  24. from string import hexdigits,digits
  25. from mmgen.color import *
  26. from mmgen.exception import *
  27. def msg(s): os.write(g.stderr_fileno,s.encode() + b'\n')
  28. def msg_r(s): os.write(g.stderr_fileno,s.encode())
  29. def Msg(s): os.write(g.stdout_fileno,s.encode() + b'\n')
  30. def Msg_r(s): os.write(g.stdout_fileno,s.encode())
  31. def msgred(s): msg(red(s))
  32. def rmsg(s): msg(red(s))
  33. def rmsg_r(s): msg_r(red(s))
  34. def ymsg(s): msg(yellow(s))
  35. def ymsg_r(s): msg_r(yellow(s))
  36. def gmsg(s): msg(green(s))
  37. def gmsg_r(s): msg_r(green(s))
  38. def mmsg(*args):
  39. for d in args: Msg(repr(d))
  40. def mdie(*args):
  41. mmsg(*args); sys.exit(0)
  42. def die_wait(delay,ev=0,s=''):
  43. assert type(delay) == int
  44. assert type(ev) == int
  45. if s: msg(s)
  46. time.sleep(delay)
  47. sys.exit(ev)
  48. def die_pause(ev=0,s=''):
  49. assert type(ev) == int
  50. if s: msg(s)
  51. input('Press ENTER to exit')
  52. sys.exit(ev)
  53. def die(ev=0,s=''):
  54. assert type(ev) == int
  55. if s: msg(s)
  56. sys.exit(ev)
  57. def Die(ev=0,s=''):
  58. assert type(ev) == int
  59. if s: Msg(s)
  60. sys.exit(ev)
  61. def rdie(ev=0,s=''): die(ev,red(s))
  62. def ydie(ev=0,s=''): die(ev,yellow(s))
  63. def hi(): ymsg('hi')
  64. def pformat(d):
  65. import pprint
  66. return pprint.PrettyPrinter(indent=4).pformat(d)
  67. def pmsg(*args):
  68. if not args: return
  69. msg(pformat(args if len(args) > 1 else args[0]))
  70. def pdie(*args,exit_val=1):
  71. if not args: sys.exit(1)
  72. die(exit_val,(pformat(args if len(args) > 1 else args[0])))
  73. def pdie2(*args): pdie(*args,exit_val=0)
  74. def set_for_type(val,refval,desc,invert_bool=False,src=None):
  75. src_str = (''," in '{}'".format(src))[bool(src)]
  76. if type(refval) == bool:
  77. v = str(val).lower()
  78. if v in ('true','yes','1'): ret = True
  79. elif v in ('false','no','none','0'): ret = False
  80. else: die(1,"'{}': invalid value for '{}'{} (must be of type '{}')".format(
  81. val,desc,src_str,'bool'))
  82. if invert_bool: ret = not ret
  83. else:
  84. try:
  85. ret = type(refval)((val,not val)[invert_bool])
  86. except:
  87. die(1,"'{}': invalid value for '{}'{} (must be of type '{}')".format(
  88. val,desc,src_str,type(refval).__name__))
  89. return ret
  90. # From 'man dd':
  91. # c=1, w=2, b=512, kB=1000, K=1024, MB=1000*1000, M=1024*1024,
  92. # GB=1000*1000*1000, G=1024*1024*1024, and so on for T, P, E, Z, Y.
  93. def parse_nbytes(nbytes):
  94. import re
  95. m = re.match(r'([0123456789]+)(.*)',nbytes)
  96. smap = ('c',1),('w',2),('b',512),('kB',1000),('K',1024),('MB',1000*1000),\
  97. ('M',1024*1024),('GB',1000*1000*1000),('G',1024*1024*1024)
  98. if m:
  99. if m.group(2):
  100. for k,v in smap:
  101. if k == m.group(2):
  102. return int(m.group(1)) * v
  103. else:
  104. msg("Valid byte specifiers: '{}'".format("' '".join([i[0] for i in smap])))
  105. else:
  106. return int(nbytes)
  107. die(1,"'{}': invalid byte specifier".format(nbytes))
  108. def check_or_create_dir(path):
  109. try:
  110. os.listdir(path)
  111. except:
  112. try:
  113. os.makedirs(path,0o700)
  114. except:
  115. die(2,"ERROR: unable to read or create path '{}'".format(path))
  116. from mmgen.opts import opt
  117. def qmsg(s,alt=None):
  118. if opt.quiet:
  119. if alt != None: msg(alt)
  120. else: msg(s)
  121. def qmsg_r(s,alt=None):
  122. if opt.quiet:
  123. if alt != None: msg_r(alt)
  124. else: msg_r(s)
  125. def vmsg(s,force=False):
  126. if opt.verbose or force: msg(s)
  127. def vmsg_r(s,force=False):
  128. if opt.verbose or force: msg_r(s)
  129. def Vmsg(s,force=False):
  130. if opt.verbose or force: Msg(s)
  131. def Vmsg_r(s,force=False):
  132. if opt.verbose or force: Msg_r(s)
  133. def dmsg(s):
  134. if opt.debug: msg(s)
  135. def suf(arg,suf_type='s'):
  136. suf_types = { 's': ('s',''), 'es': ('es','') }
  137. assert suf_type in suf_types
  138. t = type(arg)
  139. if t == int:
  140. n = arg
  141. elif any(issubclass(t,c) for c in (list,tuple,set,dict)):
  142. n = len(arg)
  143. else:
  144. die(2,'{}: invalid parameter for suf()'.format(arg))
  145. return suf_types[suf_type][n==1]
  146. def get_extension(f):
  147. a,b = os.path.splitext(f)
  148. return ('',b[1:])[len(b) > 1]
  149. def remove_extension(f,e):
  150. a,b = os.path.splitext(f)
  151. return (f,a)[len(b)>1 and b[1:]==e]
  152. def make_chksum_N(s,nchars,sep=False):
  153. if type(s) == str: s = s.encode()
  154. if nchars%4 or not (4 <= nchars <= 64): return False
  155. s = sha256(sha256(s).digest()).hexdigest().upper()
  156. sep = ('',' ')[bool(sep)]
  157. return sep.join([s[i*4:i*4+4] for i in range(nchars//4)])
  158. def make_chksum_8(s,sep=False):
  159. from mmgen.obj import HexStr
  160. s = HexStr(sha256(sha256(s).digest()).hexdigest()[:8].upper(),case='upper')
  161. return '{} {}'.format(s[:4],s[4:]) if sep else s
  162. def make_chksum_6(s):
  163. from mmgen.obj import HexStr
  164. if type(s) == str: s = s.encode()
  165. return HexStr(sha256(s).hexdigest()[:6])
  166. def is_chksum_6(s): return len(s) == 6 and is_hex_str_lc(s)
  167. def make_iv_chksum(s): return sha256(s).hexdigest()[:8].upper()
  168. def splitN(s,n,sep=None): # always return an n-element list
  169. ret = s.split(sep,n-1)
  170. return ret + ['' for i in range(n-len(ret))]
  171. def split2(s,sep=None): return splitN(s,2,sep) # always return a 2-element list
  172. def split3(s,sep=None): return splitN(s,3,sep) # always return a 3-element list
  173. def split_into_cols(col_wid,s):
  174. return ' '.join([s[col_wid*i:col_wid*(i+1)] for i in range(len(s)//col_wid+1)]).rstrip()
  175. def screen_width(s):
  176. return len(s) + len([1 for ch in s if unicodedata.east_asian_width(ch) in ('F','W')])
  177. def capfirst(s): # different from str.capitalize() - doesn't downcase any uc in string
  178. return s if len(s) == 0 else s[0].upper() + s[1:]
  179. def decode_timestamp(s):
  180. # with open('/etc/timezone') as f:
  181. # tz_save = f.read().rstrip()
  182. os.environ['TZ'] = 'UTC'
  183. ts = time.strptime(s,'%Y%m%d_%H%M%S')
  184. t = time.mktime(ts)
  185. # os.environ['TZ'] = tz_save
  186. return int(t)
  187. def make_timestamp(secs=None):
  188. t = int(secs) if secs else time.time()
  189. tv = time.gmtime(t)[:6]
  190. return '{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}'.format(*tv)
  191. def make_timestr(secs=None):
  192. t = int(secs) if secs else time.time()
  193. tv = time.gmtime(t)[:6]
  194. return '{:04d}/{:02d}/{:02d} {:02d}:{:02d}:{:02d}'.format(*tv)
  195. def secs_to_dhms(secs):
  196. dsecs = secs//3600
  197. return '{}{:02d}:{:02d}:{:02d}'.format(
  198. ('','{} day{}, '.format(dsecs//24,suf(dsecs//24)))[dsecs > 24],
  199. dsecs % 24, (secs//60) % 60, secs % 60)
  200. def secs_to_hms(secs):
  201. return '{:02d}:{:02d}:{:02d}'.format(secs//3600, (secs//60) % 60, secs % 60)
  202. def secs_to_ms(secs):
  203. return '{:02d}:{:02d}'.format(secs//60, secs % 60)
  204. def is_digits(s): return set(list(s)) <= set(list(digits))
  205. def is_int(s):
  206. try:
  207. int(str(s))
  208. return True
  209. except:
  210. return False
  211. # https://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet
  212. # https://tools.ietf.org/html/rfc4648
  213. def is_hex_bytes(s): return set(list(s.lower())) <= set(list(hexdigits.lower().encode()))
  214. def is_hex_str(s): return set(list(s.lower())) <= set(list(hexdigits.lower()))
  215. def is_hex_str_lc(s): return set(list(s)) <= set(list(hexdigits.lower()))
  216. def is_hex_str_uc(s): return set(list(s)) <= set(list(hexdigits.upper()))
  217. def is_b58_str(s): return set(list(s)) <= set(baseconv.digits['b58'])
  218. def is_b32_str(s): return set(list(s)) <= set(baseconv.digits['b32'])
  219. def is_ascii(s,enc='ascii'):
  220. try: s.decode(enc)
  221. except: return False
  222. else: return True
  223. def is_utf8(s): return is_ascii(s,enc='utf8')
  224. class baseconv(object):
  225. mn_base = 1626 # tirosh list is 1633 words long!
  226. digits = {
  227. 'electrum': tuple(__import__('mmgen.mn_electrum',fromlist=['words']).words.split()),
  228. 'tirosh': tuple(__import__('mmgen.mn_tirosh',fromlist=['words']).words.split()[:mn_base]),
  229. 'b58': tuple('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'),
  230. 'b32': tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'),
  231. 'b16': tuple('0123456789abcdef'),
  232. 'b10': tuple('0123456789'),
  233. 'b8': tuple('01234567'),
  234. }
  235. wl_chksums = {
  236. 'electrum': '5ca31424',
  237. 'tirosh': '48f05e1f', # tirosh truncated to mn_base (1626)
  238. # 'tirosh1633': '1a5faeff'
  239. }
  240. b58pad_lens = [(16,22), (24,33), (32,44)]
  241. b58pad_lens_rev = [(v,k) for k,v in b58pad_lens]
  242. @classmethod
  243. def b58encode(cls,s,pad=None):
  244. pad = cls.get_pad(s,pad,'en',cls.b58pad_lens,[bytes])
  245. return cls.fromhex(hexlify(s),'b58',pad=pad,tostr=True)
  246. @classmethod
  247. def b58decode(cls,s,pad=None):
  248. pad = cls.get_pad(s,pad,'de',cls.b58pad_lens_rev,[bytes,str])
  249. return unhexlify(cls.tohex(s,'b58',pad=pad*2 if pad else None))
  250. @staticmethod
  251. def get_pad(s,pad,op,pad_map,ok_types):
  252. m = "b58{}code() input must be one of {}, not '{}'"
  253. assert type(s) in ok_types, m.format(op,repr([t.__name__ for t in ok_types]),type(s).__name__)
  254. if pad:
  255. assert type(pad) == bool, "'pad' must be boolean type"
  256. d = dict(pad_map)
  257. assert len(s) in d, 'Invalid data length for b58{}code(pad=True)'.format(op)
  258. return d[len(s)]
  259. else:
  260. return None
  261. @classmethod
  262. def get_wordlist_chksum(cls,wl_id):
  263. return sha256(' '.join(cls.digits[wl_id]).encode()).hexdigest()[:8]
  264. @classmethod
  265. def check_wordlists(cls):
  266. for k,v in list(cls.wl_chksums.items()): assert cls.get_wordlist_chksum(k) == v
  267. @classmethod
  268. def check_wordlist(cls,wl_id):
  269. wl = baseconv.digits[wl_id]
  270. Msg('Wordlist: {}\nLength: {} words'.format(capfirst(wl_id),len(wl)))
  271. new_chksum = cls.get_wordlist_chksum(wl_id)
  272. a,b = 'generated checksum','saved checksum'
  273. compare_chksums(new_chksum,a,cls.wl_chksums[wl_id],b,die_on_fail=True)
  274. Msg('Checksum {} matches'.format(new_chksum))
  275. Msg('List is sorted') if tuple(sorted(wl)) == wl else die(3,'ERROR: List is not sorted!')
  276. @classmethod
  277. def tohex(cls,words_arg,wl_id,pad=None):
  278. words = words_arg if type(words_arg) in (list,tuple) else tuple(words_arg.strip())
  279. wl = cls.digits[wl_id]
  280. base = len(wl)
  281. if not set(words) <= set(wl):
  282. die(2,'{} is not in {} (base{}) format'.format(repr(words_arg),wl_id,base))
  283. deconv = [wl.index(words[::-1][i])*(base**i) for i in range(len(words))]
  284. ret = ('{:0{w}x}'.format(sum(deconv),w=pad or 0))
  285. return ('','0')[len(ret) % 2] + ret
  286. @classmethod
  287. def fromhex(cls,hexnum,wl_id,pad=None,tostr=False):
  288. if not is_hex_str(hexnum.decode()):
  289. die(2,"'{}': not a hexadecimal number".format(hexnum.decode()))
  290. wl = cls.digits[wl_id]
  291. base = len(wl)
  292. num,ret = int(hexnum,16),[]
  293. while num:
  294. ret.append(num % base)
  295. num //= base
  296. o = [wl[n] for n in [0] * ((pad or 0)-len(ret)) + ret[::-1]]
  297. return ''.join(o) if tostr else o
  298. baseconv.check_wordlists()
  299. def match_ext(addr,ext):
  300. return addr.split('.')[-1] == ext
  301. def file_exists(f):
  302. try:
  303. os.stat(f)
  304. return True
  305. except:
  306. return False
  307. def file_is_readable(f):
  308. from stat import S_IREAD
  309. try:
  310. assert os.stat(f).st_mode & S_IREAD
  311. except:
  312. return False
  313. else:
  314. return True
  315. def get_from_brain_opt_params():
  316. l,p = opt.from_brain.split(',')
  317. return(int(l),p)
  318. def pretty_hexdump(data,gw=2,cols=8,line_nums=False):
  319. r = (0,1)[bool(len(data) % gw)]
  320. return ''.join(
  321. [
  322. ('' if (line_nums == False or i % cols) else '{:06x}: '.format(i*gw)) +
  323. hexlify(data[i*gw:i*gw+gw]).decode() + ('\n',' ')[bool((i+1) % cols)]
  324. for i in range(len(data)//gw + r)
  325. ]
  326. ).rstrip() + '\n'
  327. def decode_pretty_hexdump(data):
  328. from string import hexdigits
  329. pat = r'^[{}]+:\s+'.format(hexdigits)
  330. lines = [re.sub(pat,'',l) for l in data.splitlines()]
  331. try:
  332. return unhexlify(''.join((''.join(lines).split())))
  333. except:
  334. msg('Data not in hexdump format')
  335. return False
  336. def strip_comments(line):
  337. return re.sub(r'\s+$','',re.sub(r'#.*','',line,1))
  338. def remove_comments(lines):
  339. return [m for m in [strip_comments(l) for l in lines] if m != '']
  340. from mmgen.globalvars import g
  341. def start_mscolor():
  342. try:
  343. import colorama
  344. colorama.init(strip=True,convert=True)
  345. except:
  346. msg('Import of colorama module failed')
  347. def get_hash_params(hash_preset):
  348. if hash_preset in g.hash_presets:
  349. return g.hash_presets[hash_preset] # N,p,r,buflen
  350. else: # Shouldn't be here
  351. die(3,"{}: invalid 'hash_preset' value".format(hash_preset))
  352. def compare_chksums(chk1,desc1,chk2,desc2,hdr='',die_on_fail=False,verbose=False):
  353. if not chk1 == chk2:
  354. fs = "{} ERROR: {} checksum ({}) doesn't match {} checksum ({})"
  355. m = fs.format((hdr+':\n ' if hdr else 'CHECKSUM'),desc2,chk2,desc1,chk1)
  356. if die_on_fail:
  357. die(3,m)
  358. else:
  359. vmsg(m,force=verbose)
  360. return False
  361. vmsg('{} checksum OK ({})'.format(capfirst(desc1),chk1))
  362. return True
  363. def compare_or_die(val1, desc1, val2, desc2, e='Error'):
  364. if val1 != val2:
  365. die(3,"{}: {} ({}) doesn't match {} ({})".format(e,desc2,val2,desc1,val1))
  366. dmsg('{} OK ({})'.format(capfirst(desc2),val2))
  367. return True
  368. def open_file_or_exit(filename,mode,silent=False):
  369. try:
  370. f = open(filename, mode)
  371. except:
  372. op = ('writing','reading')['r' in mode]
  373. die(2,("Unable to open file '{}' for {}".format(filename,op),'')[silent])
  374. return f
  375. def check_file_type_and_access(fname,ftype,blkdev_ok=False):
  376. a = ((os.R_OK,'read'),(os.W_OK,'writ'))
  377. access,m = a[ftype in ('output file','output directory')]
  378. ok_types = [
  379. (stat.S_ISREG,'regular file'),
  380. (stat.S_ISLNK,'symbolic link')
  381. ]
  382. if blkdev_ok: ok_types.append((stat.S_ISBLK,'block device'))
  383. if ftype == 'output directory': ok_types = [(stat.S_ISDIR, 'output directory')]
  384. try: mode = os.stat(fname).st_mode
  385. except:
  386. die(1,"Unable to stat requested {} '{}'".format(ftype,fname))
  387. for t in ok_types:
  388. if t[0](mode): break
  389. else:
  390. die(1,"Requested {} '{}' is not a {}".format(ftype,fname,' or '.join([t[1] for t in ok_types])))
  391. if not os.access(fname, access):
  392. die(1,"Requested {} '{}' is not {}able by you".format(ftype,fname,m))
  393. return True
  394. def check_infile(f,blkdev_ok=False):
  395. return check_file_type_and_access(f,'input file',blkdev_ok=blkdev_ok)
  396. def check_outfile(f,blkdev_ok=False):
  397. return check_file_type_and_access(f,'output file',blkdev_ok=blkdev_ok)
  398. def check_outdir(f):
  399. return check_file_type_and_access(f,'output directory')
  400. def make_full_path(outdir,outfile):
  401. return os.path.normpath(os.path.join(outdir, os.path.basename(outfile)))
  402. def get_seed_file(cmd_args,nargs,invoked_as=None):
  403. from mmgen.filename import find_file_in_dir
  404. from mmgen.seed import Wallet
  405. wf = find_file_in_dir(Wallet,g.data_dir)
  406. wd_from_opt = bool(opt.hidden_incog_input_params or opt.in_fmt) # have wallet data from opt?
  407. import mmgen.opts as opts
  408. if len(cmd_args) + (wd_from_opt or bool(wf)) < nargs:
  409. if not wf:
  410. msg('No default wallet found, and no other seed source was specified')
  411. opts.usage()
  412. elif len(cmd_args) > nargs:
  413. opts.usage()
  414. elif len(cmd_args) == nargs and wf and invoked_as != 'gen':
  415. qmsg('Warning: overriding default wallet with user-supplied wallet')
  416. if cmd_args or wf:
  417. check_infile(cmd_args[0] if cmd_args else wf)
  418. return cmd_args[0] if cmd_args else (wf,None)[wd_from_opt]
  419. def get_new_passphrase(desc,passchg=False):
  420. w = '{}passphrase for {}'.format(('','new ')[bool(passchg)], desc)
  421. if opt.passwd_file:
  422. pw = ' '.join(get_words_from_file(opt.passwd_file,w))
  423. elif opt.echo_passphrase:
  424. pw = ' '.join(get_words_from_user('Enter {}: '.format(w)))
  425. else:
  426. for i in range(g.passwd_max_tries):
  427. pw = ' '.join(get_words_from_user('Enter {}: '.format(w)))
  428. pw2 = ' '.join(get_words_from_user('Repeat passphrase: '))
  429. dmsg('Passphrases: [{}] [{}]'.format(pw,pw2))
  430. if pw == pw2:
  431. vmsg('Passphrases match'); break
  432. else: msg('Passphrases do not match. Try again.')
  433. else:
  434. die(2,'User failed to duplicate passphrase in {} attempts'.format(g.passwd_max_tries))
  435. if pw == '': qmsg('WARNING: Empty passphrase')
  436. return pw
  437. def confirm_or_raise(message,q,expect='YES',exit_msg='Exiting at user request'):
  438. m = message.strip()
  439. if m: msg(m)
  440. a = q+' ' if q[0].isupper() else 'Are you sure you want to {}?\n'.format(q)
  441. b = "Type uppercase '{}' to confirm: ".format(expect)
  442. if my_raw_input(a+b).strip() != expect:
  443. raise UserNonConfirmation(exit_msg)
  444. def write_data_to_file( outfile,data,desc='data',
  445. ask_write=False,
  446. ask_write_prompt='',
  447. ask_write_default_yes=True,
  448. ask_overwrite=True,
  449. ask_tty=True,
  450. no_tty=False,
  451. silent=False,
  452. binary=False,
  453. ignore_opt_outdir=False,
  454. check_data=False,
  455. cmp_data=None):
  456. if silent: ask_tty = ask_overwrite = False
  457. if opt.quiet: ask_overwrite = False
  458. if ask_write_default_yes == False or ask_write_prompt:
  459. ask_write = True
  460. def do_stdout():
  461. qmsg('Output to STDOUT requested')
  462. if sys.stdout.isatty():
  463. if no_tty:
  464. die(2,'Printing {} to screen is not allowed'.format(desc))
  465. if (ask_tty and not opt.quiet) or binary:
  466. confirm_or_raise('','output {} to screen'.format(desc))
  467. else:
  468. try: of = os.readlink('/proc/{}/fd/1'.format(os.getpid())) # Linux
  469. except: of = None # Windows
  470. if of:
  471. if of[:5] == 'pipe:':
  472. if no_tty:
  473. die(2,'Writing {} to pipe is not allowed'.format(desc))
  474. if ask_tty and not opt.quiet:
  475. confirm_or_raise('','output {} to pipe'.format(desc))
  476. msg('')
  477. of2,pd = os.path.relpath(of),os.path.pardir
  478. msg("Redirecting output to file '{}'".format((of2,of)[of2[:len(pd)] == pd]))
  479. else:
  480. msg('Redirecting output to file')
  481. if binary and g.platform == 'win':
  482. import msvcrt
  483. msvcrt.setmode(sys.stdout.fileno(),os.O_BINARY)
  484. sys.stdout.write(data.decode() if issubclass(type(data),bytes) else data)
  485. def do_file(outfile,ask_write_prompt):
  486. if opt.outdir and not ignore_opt_outdir and not os.path.isabs(outfile):
  487. outfile = make_full_path(opt.outdir,outfile)
  488. if ask_write:
  489. if not ask_write_prompt: ask_write_prompt = 'Save {}?'.format(desc)
  490. if not keypress_confirm(ask_write_prompt,
  491. default_yes=ask_write_default_yes):
  492. die(1,'{} not saved'.format(capfirst(desc)))
  493. hush = False
  494. if file_exists(outfile) and ask_overwrite:
  495. q = "File '{}' already exists\nOverwrite?".format(outfile)
  496. confirm_or_raise('',q)
  497. msg("Overwriting file '{}'".format(outfile))
  498. hush = True
  499. # not atomic, but better than nothing
  500. # if cmp_data is empty, file can be either empty or non-existent
  501. if check_data:
  502. try:
  503. d = open(outfile,('r','rb')[bool(binary)]).read()
  504. except:
  505. d = ''
  506. finally:
  507. if d != cmp_data:
  508. m = "{} in file '{}' has been altered by some other program! Aborting file write"
  509. die(3,m.format(desc,outfile))
  510. f = open_file_or_exit(outfile,('w','wb')[bool(binary)])
  511. try:
  512. f.write(data)
  513. except:
  514. die(2,"Failed to write {} to file '{}'".format(desc,outfile))
  515. f.close
  516. if not (hush or silent):
  517. msg("{} written to file '{}'".format(capfirst(desc),outfile))
  518. return True
  519. if opt.stdout or outfile in ('','-'):
  520. do_stdout()
  521. elif sys.stdin.isatty() and not sys.stdout.isatty():
  522. do_stdout()
  523. else:
  524. do_file(outfile,ask_write_prompt)
  525. def get_words_from_user(prompt):
  526. # split() also strips
  527. words = my_raw_input(prompt, echo=opt.echo_passphrase).split()
  528. dmsg('Sanitized input: [{}]'.format(' '.join(words)))
  529. return words
  530. def get_words_from_file(infile,desc,silent=False):
  531. if not silent:
  532. qmsg("Getting {} from file '{}'".format(desc,infile))
  533. f = open_file_or_exit(infile, 'r')
  534. try: words = f.read().split() # split() also strips
  535. except: die(1,'{} data must be UTF-8 encoded.'.format(capfirst(desc)))
  536. f.close()
  537. dmsg('Sanitized input: [{}]'.format(' '.join(words)))
  538. return words
  539. def get_words(infile,desc,prompt):
  540. if infile:
  541. return get_words_from_file(infile,desc)
  542. else:
  543. return get_words_from_user(prompt)
  544. def mmgen_decrypt_file_maybe(fn,desc='',silent=False):
  545. d = get_data_from_file(fn,desc,binary=True,silent=silent)
  546. have_enc_ext = get_extension(fn) == g.mmenc_ext
  547. if have_enc_ext or not is_utf8(d):
  548. m = ('Attempting to decrypt','Decrypting')[have_enc_ext]
  549. msg("{} {} '{}'".format(m,desc,fn))
  550. from mmgen.crypto import mmgen_decrypt_retry
  551. d = mmgen_decrypt_retry(d,desc)
  552. return d
  553. def get_lines_from_file(fn,desc='',trim_comments=False,silent=False):
  554. dec = mmgen_decrypt_file_maybe(fn,desc,silent=silent)
  555. ret = dec.decode('utf8').splitlines() # DOS-safe
  556. if trim_comments: ret = remove_comments(ret)
  557. dmsg("Got {} lines from file '{}'".format(len(ret),fn))
  558. return ret
  559. def get_data_from_user(desc='data',silent=False): # user input MUST be UTF-8
  560. p = ('','Enter {}: '.format(desc))[g.stdin_tty]
  561. data = my_raw_input(p,echo=opt.echo_passphrase)
  562. dmsg('User input: [{}]'.format(data))
  563. return data
  564. def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False,require_utf8=False):
  565. if dash and infile == '-': return sys.stdin.read()
  566. if not opt.quiet and not silent and desc:
  567. qmsg("Getting {} from file '{}'".format(desc,infile))
  568. f = open_file_or_exit(infile,('r','rb')[bool(binary)],silent=silent)
  569. data = f.read()
  570. f.close()
  571. if require_utf8:
  572. try:
  573. if binary: data = data.decode()
  574. else: data.encode()
  575. except: die(1,'{} data must be UTF-8 encoded.'.format(capfirst(desc)))
  576. return data
  577. def pwfile_reuse_warning():
  578. if 'passwd_file_used' in globals():
  579. qmsg("Reusing passphrase from file '{}' at user request".format(opt.passwd_file))
  580. return True
  581. globals()['passwd_file_used'] = True
  582. return False
  583. def get_mmgen_passphrase(desc,passchg=False):
  584. prompt ='Enter {}passphrase for {}: '.format(('','old ')[bool(passchg)],desc)
  585. if opt.passwd_file:
  586. pwfile_reuse_warning()
  587. return ' '.join(get_words_from_file(opt.passwd_file,'passphrase'))
  588. else:
  589. return ' '.join(get_words_from_user(prompt))
  590. def my_raw_input(prompt,echo=True,insert_txt='',use_readline=True):
  591. try: import readline
  592. except: use_readline = False # Windows
  593. if use_readline and sys.stdout.isatty():
  594. def st_hook(): readline.insert_text(insert_txt)
  595. readline.set_startup_hook(st_hook)
  596. else:
  597. msg_r(prompt)
  598. prompt = ''
  599. from mmgen.term import kb_hold_protect
  600. kb_hold_protect()
  601. if echo or not sys.stdin.isatty():
  602. reply = input(prompt)
  603. else:
  604. from getpass import getpass
  605. reply = getpass(prompt)
  606. kb_hold_protect()
  607. try:
  608. return reply.strip()
  609. except:
  610. die(1,'User input must be UTF-8 encoded.')
  611. def keypress_confirm(prompt,default_yes=False,verbose=False,no_nl=False):
  612. from mmgen.term import get_char
  613. q = ('(y/N)','(Y/n)')[bool(default_yes)]
  614. p = '{} {}: '.format(prompt,q)
  615. nl = ('\n','\r{}\r'.format(' '*len(p)))[no_nl]
  616. if opt.accept_defaults:
  617. msg(p)
  618. return (False,True)[default_yes]
  619. while True:
  620. r = get_char(p).strip(b'\n\r')
  621. if not r:
  622. if default_yes: msg_r(nl); return True
  623. else: msg_r(nl); return False
  624. elif r in b'yY': msg_r(nl); return True
  625. elif r in b'nN': msg_r(nl); return False
  626. else:
  627. if verbose: msg('\nInvalid reply')
  628. else: msg_r('\r')
  629. def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False):
  630. from mmgen.term import get_char
  631. while True:
  632. reply = get_char('{}: '.format(prompt)).strip(b'\n\r')
  633. if reply in chars.encode() or (enter_ok and not reply):
  634. msg('')
  635. return reply.decode()
  636. if verbose: msg('\nInvalid reply')
  637. else: msg_r('\r')
  638. def do_pager(text):
  639. pagers = ['less','more']
  640. end_msg = '\n(end of text)\n\n'
  641. # --- Non-MSYS Windows code deleted ---
  642. # raw, chop, horiz scroll 8 chars, disable buggy line chopping in MSYS
  643. os.environ['LESS'] = (('--shift 8 -RS'),('-cR -#1'))[g.platform=='win']
  644. if 'PAGER' in os.environ and os.environ['PAGER'] != pagers[0]:
  645. pagers = [os.environ['PAGER']] + pagers
  646. for pager in pagers:
  647. try:
  648. from subprocess import Popen,PIPE
  649. p = Popen([pager],stdin=PIPE,shell=False)
  650. except: pass
  651. else:
  652. p.communicate((text+(end_msg,'')[pager=='less']).encode())
  653. msg_r('\r')
  654. break
  655. else: Msg(text+end_msg)
  656. def do_license_msg(immed=False):
  657. if opt.quiet or g.no_license or opt.yes or not g.stdin_tty: return
  658. import mmgen.license as gpl
  659. p = "Press 'w' for conditions and warranty info, or 'c' to continue:"
  660. msg(gpl.warning)
  661. prompt = '{} '.format(p.strip())
  662. from mmgen.term import get_char
  663. while True:
  664. reply = get_char(prompt, immed_chars=('','wc')[bool(immed)])
  665. if reply == b'w':
  666. do_pager(gpl.conditions)
  667. elif reply == b'c':
  668. msg(''); break
  669. else:
  670. msg_r('\r')
  671. msg('')
  672. def get_daemon_cfg_options(cfg_keys):
  673. cfg_file = os.path.join(g.proto.daemon_data_dir,g.proto.name+'.conf')
  674. try:
  675. lines = get_lines_from_file(cfg_file,'',silent=bool(opt.quiet))
  676. kv_pairs = [l.split('=') for l in lines]
  677. cfg = dict([(k,v) for k,v in kv_pairs if k in cfg_keys])
  678. except:
  679. vmsg("Warning: '{}' does not exist or is unreadable".format(cfg_file))
  680. cfg = {}
  681. for k in set(cfg_keys) - set(cfg.keys()): cfg[k] = ''
  682. return cfg
  683. def get_coin_daemon_auth_cookie():
  684. f = os.path.join(g.proto.daemon_data_dir,g.proto.daemon_data_subdir,'.cookie')
  685. return get_lines_from_file(f,'')[0] if file_is_readable(f) else ''
  686. def rpc_init_parity():
  687. def resolve_token_arg(token_arg):
  688. from mmgen.tw import TrackingWallet
  689. from mmgen.obj import CoinAddr
  690. from mmgen.altcoins.eth.contract import Token
  691. try: addr = CoinAddr(token_arg,on_fail='raise')
  692. except: addr = TrackingWallet().sym2addr(token_arg)
  693. else: Token(addr) # test for presence in blockchain
  694. if not addr:
  695. m = "'{}': unrecognized token symbol"
  696. raise UnrecognizedTokenSymbol(m.format(token_arg))
  697. sym = Token(addr).symbol().upper()
  698. vmsg('ERC20 token resolved: {} ({})'.format(addr,sym))
  699. return addr,sym
  700. from mmgen.rpc import EthereumRPCConnection
  701. g.rpch = EthereumRPCConnection(
  702. g.rpc_host or 'localhost',
  703. g.rpc_port or g.proto.rpc_port)
  704. g.rpch.daemon_version = g.rpch.parity_versionInfo()['version'] # fail immediately if daemon is geth
  705. g.rpch.coin_amt_type = str
  706. g.chain = g.rpch.parity_chain().replace(' ','_')
  707. if g.token:
  708. (g.token,g.dcoin) = resolve_token_arg(g.token)
  709. g.rpch.caps = ()
  710. try:
  711. g.rpch.request('eth_chainId')
  712. g.rpch.caps += ('eth_chainId',)
  713. except RPCFailure:
  714. pass
  715. return g.rpch
  716. def rpc_init_bitcoind():
  717. def check_chainfork_mismatch(conn):
  718. block0 = conn.getblockhash(0)
  719. latest = conn.getblockcount()
  720. try:
  721. assert block0 == g.proto.block0,'Incorrect Genesis block for {}'.format(g.proto.__name__)
  722. for fork in g.proto.forks:
  723. if fork[0] == None or latest < fork[0]: break
  724. assert conn.getblockhash(fork[0]) == fork[1], (
  725. 'Bad block hash at fork block {}. Is this the {} chain?'.format(fork[0],fork[2].upper()))
  726. except Exception as e:
  727. die(2,"{}\n'{c}' requested, but this is not the {c} chain!".format(e.args[0],c=g.coin))
  728. def check_chaintype_mismatch():
  729. try:
  730. if g.regtest: assert g.chain == 'regtest','--regtest option selected, but chain is not regtest'
  731. if g.testnet: assert g.chain != 'mainnet','--testnet option selected, but chain is mainnet'
  732. if not g.testnet: assert g.chain == 'mainnet','mainnet selected, but chain is not mainnet'
  733. except Exception as e:
  734. die(1,'{}\nChain is {}!'.format(e.args[0],g.chain))
  735. cfg = get_daemon_cfg_options(('rpcuser','rpcpassword'))
  736. from mmgen.rpc import CoinDaemonRPCConnection
  737. conn = CoinDaemonRPCConnection(
  738. g.rpc_host or 'localhost',
  739. g.rpc_port or g.proto.rpc_port,
  740. g.rpc_user or cfg['rpcuser'], # MMGen's rpcuser,rpcpassword override coin daemon's
  741. g.rpc_password or cfg['rpcpassword'],
  742. auth_cookie=get_coin_daemon_auth_cookie())
  743. if g.bob or g.alice:
  744. from . import regtest as rt
  745. rt.user(('alice','bob')[g.bob],quiet=True)
  746. conn.daemon_version = int(conn.getnetworkinfo()['version'])
  747. conn.coin_amt_type = (float,str)[conn.daemon_version>=120000]
  748. g.chain = conn.getblockchaininfo()['chain']
  749. if g.chain != 'regtest': g.chain += 'net'
  750. assert g.chain in g.chains
  751. check_chaintype_mismatch()
  752. if g.chain == 'mainnet': # skip this for testnet, as Genesis block may change
  753. check_chainfork_mismatch(conn)
  754. conn.caps = ()
  755. for func,cap in (
  756. ('setlabel','label_api'),
  757. ('signrawtransactionwithkey','sign_with_key') ):
  758. if len(conn.request('help',func).split('\n')) > 3:
  759. conn.caps += (cap,)
  760. return conn
  761. def rpc_init(reinit=False):
  762. if not 'rpc' in g.proto.mmcaps:
  763. die(1,'Coin daemon operations not supported for coin {}!'.format(g.coin))
  764. if g.rpch != None and not reinit: return g.rpch
  765. g.rpch = globals()['rpc_init_'+g.proto.daemon_family]()
  766. return g.rpch
  767. def format_par(s,indent=0,width=80,as_list=False):
  768. words,lines = s.split(),[]
  769. assert width >= indent + 4,'width must be >= indent + 4'
  770. while words:
  771. line = ''
  772. while len(line) <= (width-indent) and words:
  773. if line and len(line) + len(words[0]) + 1 > width-indent: break
  774. line += ('',' ')[bool(line)] + words.pop(0)
  775. lines.append(' '*indent + line)
  776. return lines if as_list else '\n'.join(lines) + '\n'
  777. # module loading magic for tx.py and tw.py
  778. def altcoin_subclass(cls,mod_id,cls_name):
  779. if cls.__name__ != cls_name: return cls
  780. mod_dir = g.proto.base_coin.lower()
  781. pname = g.proto.class_pfx if hasattr(g.proto,'class_pfx') else capfirst(g.proto.name)
  782. tname = 'Token' if g.token else ''
  783. e1 = 'from mmgen.altcoins.{}.{} import {}{}{}'.format(mod_dir,mod_id,pname,tname,cls_name)
  784. e2 = 'alt_cls = {}{}{}'.format(pname,tname,cls_name)
  785. gl = globals()
  786. try:
  787. exec(e1,gl,gl)
  788. exec(e2,gl,gl)
  789. return alt_cls
  790. except ImportError:
  791. return cls
  792. # decorator for TrackingWallet
  793. def write_mode(orig_func):
  794. def f(self,*args,**kwargs):
  795. if self.mode != 'w':
  796. m = '{} opened in read-only mode: cannot execute method {}()'
  797. die(1,m.format(type(self).__name__,locals()['orig_func'].__name__))
  798. return orig_func(self,*args,**kwargs)
  799. return f