obj.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2018 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. obj.py: MMGen native classes
  20. """
  21. import sys,os
  22. from decimal import *
  23. from mmgen.color import *
  24. from string import hexdigits,ascii_letters,digits
  25. def is_mmgen_seed_id(s): return SeedID(sid=s,on_fail='silent')
  26. def is_mmgen_idx(s): return AddrIdx(s,on_fail='silent')
  27. def is_mmgen_id(s): return MMGenID(s,on_fail='silent')
  28. def is_coin_addr(s): return CoinAddr(s,on_fail='silent')
  29. def is_addrlist_id(s): return AddrListID(s,on_fail='silent')
  30. def is_tw_label(s): return TwLabel(s,on_fail='silent')
  31. def is_wif(s): return WifKey(s,on_fail='silent')
  32. def is_viewkey(s): return ViewKey(s,on_fail='silent')
  33. class MMGenObject(object):
  34. # Pretty-print any object subclassed from MMGenObject, recursing into sub-objects - WIP
  35. def pmsg(self): print(self.pformat())
  36. def pdie(self): print(self.pformat()); sys.exit(0)
  37. def pformat(self,lvl=0):
  38. scalars = (str,unicode,int,float,Decimal)
  39. def do_list(out,e,lvl=0,is_dict=False):
  40. out.append('\n')
  41. for i in e:
  42. el = i if not is_dict else e[i]
  43. if is_dict:
  44. out.append('{s}{:<{l}}'.format(i,s=' '*(4*lvl+8),l=10,l2=8*(lvl+1)+8))
  45. if hasattr(el,'pformat'):
  46. out.append('{:>{l}}{}'.format('',el.pformat(lvl=lvl+1),l=(lvl+1)*8))
  47. elif type(el) in scalars:
  48. if isList(e):
  49. out.append(u'{:>{l}}{:16}\n'.format('',repr(el),l=lvl*8))
  50. else:
  51. out.append(u' {}'.format(repr(el)))
  52. elif isList(el) or isDict(el):
  53. indent = 1 if is_dict else lvl*8+4
  54. out.append(u'{:>{l}}{:16}'.format('','<'+type(el).__name__+'>',l=indent))
  55. if isList(el) and type(el[0]) in scalars: out.append('\n')
  56. do_list(out,el,lvl=lvl+1,is_dict=isDict(el))
  57. else:
  58. out.append(u'{:>{l}}{:16} {}\n'.format('','<'+type(el).__name__+'>',repr(el),l=(lvl*8)+8))
  59. out.append('\n')
  60. if not e: out.append('{}\n'.format(repr(e)))
  61. from collections import OrderedDict
  62. def isDict(obj):
  63. return issubclass(type(obj),dict) or issubclass(type(obj),OrderedDict)
  64. def isList(obj):
  65. return issubclass(type(obj),list) and type(obj) != OrderedDict
  66. def isScalar(obj):
  67. return any(issubclass(type(obj),t) for t in scalars)
  68. # print type(self)
  69. # print dir(self)
  70. # print self.__dict__
  71. # print self.__dict__.keys()
  72. # print self.keys()
  73. out = [u'<{}>{}\n'.format(type(self).__name__,' '+repr(self) if isScalar(self) else '')]
  74. if isList(self) or isDict(self):
  75. do_list(out,self,lvl=lvl,is_dict=isDict(self))
  76. # print repr(self.__dict__.keys())
  77. for k in self.__dict__:
  78. if k in ('_OrderedDict__root','_OrderedDict__map'): continue # exclude these because of recursion
  79. e = getattr(self,k)
  80. if isList(e) or isDict(e):
  81. out.append(u'{:>{l}}{:<10} {:16}'.format('',k,'<'+type(e).__name__+'>',l=(lvl*8)+4))
  82. do_list(out,e,lvl=lvl,is_dict=isDict(e))
  83. elif hasattr(e,'pformat') and type(e) != type:
  84. out.append(u'{:>{l}}{:10} {}'.format('',k,e.pformat(lvl=lvl+1),l=(lvl*8)+4))
  85. else:
  86. out.append(u'{:>{l}}{:<10} {:16} {}\n'.format(
  87. '',k,'<'+type(e).__name__+'>',repr(e),l=(lvl*8)+4))
  88. import re
  89. return re.sub('\n+','\n',''.join(out))
  90. class MMGenList(list,MMGenObject): pass
  91. class MMGenDict(dict,MMGenObject): pass
  92. class AddrListList(list,MMGenObject): pass
  93. class InitErrors(object):
  94. @staticmethod
  95. def arg_chk(cls,on_fail):
  96. assert on_fail in ('die','return','silent','raise'),'arg_chk in class {}'.format(cls.__name__)
  97. @staticmethod
  98. def init_fail(m,on_fail):
  99. if os.getenv('MMGEN_TRACEBACK'): on_fail == 'raise'
  100. from mmgen.util import die,msg
  101. if on_fail == 'silent': return None # TODO: return False instead?
  102. elif on_fail == 'raise': raise ValueError,m
  103. elif on_fail == 'die': die(1,m)
  104. elif on_fail == 'return':
  105. if m: msg(m)
  106. return None # TODO: here too?
  107. class Hilite(object):
  108. color = 'red'
  109. color_always = False
  110. width = 0
  111. trunc_ok = True
  112. @classmethod
  113. def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None,
  114. center=False,nullrepl='',app='',appcolor=False):
  115. if width == None: width = cls.width
  116. if trunc_ok == None: trunc_ok = cls.trunc_ok
  117. assert width > 0,'Width must be > 0'
  118. if s == '' and nullrepl:
  119. s,center = nullrepl,True
  120. if center: s = s.center(width)
  121. assert type(encl) is str and len(encl) in (0,2),'type(encl) must be str and len(encl) be in (0,2)'
  122. a,b = list(encl) if encl else ('','')
  123. if trunc_ok and len(s) > width: s = s[:width]
  124. if app:
  125. return cls.colorize(a+s+b,color=color) + \
  126. cls.colorize(app.ljust(width-len(a+s+b)),color=appcolor)
  127. else:
  128. return cls.colorize((a+s+b).ljust(width),color=color)
  129. def fmt(self,*args,**kwargs):
  130. assert args == () # forbid invocation w/o keywords
  131. return self.fmtc(self,*args,**kwargs)
  132. @classmethod
  133. def hlc(cls,s,color=True):
  134. return cls.colorize(s,color=color)
  135. def hl(self,color=True):
  136. return self.colorize(self,color=color)
  137. def __str__(self):
  138. return self.colorize(self,color=False)
  139. @classmethod
  140. def colorize(cls,s,color=True):
  141. k = color if type(color) is str else cls.color # hack: override color with str value
  142. return globals()[k](s) if (color or cls.color_always) else s
  143. # For attrs that are always present in the data instance
  144. # Reassignment and deletion forbidden
  145. class MMGenImmutableAttr(object): # Descriptor
  146. def __init__(self,name,dtype,typeconv=True):
  147. self.typeconv = typeconv
  148. assert type(dtype) in (str,type)
  149. self.name = name
  150. self.dtype = dtype
  151. def __get__(self,instance,owner):
  152. return instance.__dict__[self.name]
  153. # forbid all reassignment
  154. def set_attr_ok(self,instance):
  155. return not hasattr(instance,self.name)
  156. def __set__(self,instance,value):
  157. if not self.set_attr_ok(instance):
  158. m = "Attribute '{}' of {} instance cannot be reassigned"
  159. raise AttributeError(m.format(self.name,type(instance)))
  160. if self.typeconv: # convert type
  161. instance.__dict__[self.name] = \
  162. globals()[self.dtype](value,on_fail='raise') if type(self.dtype) == str else self.dtype(value)
  163. else: # check type
  164. if type(value) != self.dtype:
  165. m = "Attribute '{}' of {} instance must of type {}"
  166. raise TypeError(m.format(self.name,type(instance),self.dtype))
  167. instance.__dict__[self.name] = value
  168. def __delete__(self,instance):
  169. m = "Atribute '{}' of {} instance cannot be deleted"
  170. raise AttributeError(m.format(self.name,type(instance)))
  171. # For attrs that might not be present in the data instance
  172. # Reassignment or deletion allowed if specified
  173. class MMGenListItemAttr(MMGenImmutableAttr): # Descriptor
  174. def __init__(self,name,dtype,typeconv=True,reassign_ok=False,delete_ok=False):
  175. self.reassign_ok = reassign_ok
  176. self.delete_ok = delete_ok
  177. MMGenImmutableAttr.__init__(self,name,dtype,typeconv=typeconv)
  178. # return None if attribute doesn't exist
  179. def __get__(self,instance,owner):
  180. try: return instance.__dict__[self.name]
  181. except: return None
  182. def set_attr_ok(self,instance):
  183. return getattr(instance,self.name) == None or self.reassign_ok
  184. def __delete__(self,instance):
  185. if self.delete_ok:
  186. if self.name in instance.__dict__:
  187. del instance.__dict__[self.name]
  188. else:
  189. MMGenImmutableAttr.__delete__(self,instance)
  190. class MMGenListItem(MMGenObject):
  191. def __init__(self,*args,**kwargs):
  192. if args:
  193. raise ValueError, 'Non-keyword args not allowed'
  194. for k in kwargs:
  195. if kwargs[k] != None:
  196. setattr(self,k,kwargs[k])
  197. # prevent setting random attributes
  198. def __setattr__(self,name,value):
  199. if name not in type(self).__dict__:
  200. m = "'{}': no such attribute in class {}"
  201. raise AttributeError(m.format(name,type(self)))
  202. return object.__setattr__(self,name,value)
  203. class AddrIdx(int,InitErrors):
  204. max_digits = 7
  205. def __new__(cls,num,on_fail='die'):
  206. cls.arg_chk(cls,on_fail)
  207. try:
  208. assert type(num) is not float,'is float'
  209. me = int.__new__(cls,num)
  210. assert len(str(me)) <= cls.max_digits,'is more than {} digits'.format(cls.max_digits)
  211. assert me > 0,'is less than one'
  212. return me
  213. except Exception as e:
  214. m = "{!r}: value cannot be converted to address index ({})"
  215. return cls.init_fail(m.format(num,e[0]),on_fail)
  216. class AddrIdxList(list,InitErrors,MMGenObject):
  217. max_len = 1000000
  218. def __init__(self,fmt_str=None,idx_list=None,on_fail='die',sep=','):
  219. self.arg_chk(type(self),on_fail)
  220. try:
  221. if idx_list:
  222. return list.__init__(self,sorted(set(AddrIdx(i,on_fail='raise') for i in idx_list)))
  223. elif fmt_str:
  224. ret = []
  225. for i in (fmt_str.split(sep)):
  226. j = i.split('-')
  227. if len(j) == 1:
  228. idx = AddrIdx(i,on_fail='raise')
  229. if not idx: break
  230. ret.append(idx)
  231. elif len(j) == 2:
  232. beg = AddrIdx(j[0],on_fail='raise')
  233. if not beg: break
  234. end = AddrIdx(j[1],on_fail='raise')
  235. if not beg: break
  236. if end < beg: break
  237. ret.extend([AddrIdx(x,on_fail='raise') for x in range(beg,end+1)])
  238. else: break
  239. else:
  240. return list.__init__(self,sorted(set(ret))) # fell off end of loop - success
  241. raise ValueError,"{!r}: invalid range".format(i)
  242. except Exception as e:
  243. m = "{!r}: value cannot be converted to AddrIdxList ({})"
  244. return type(self).init_fail(m.format(idx_list or fmt_str,e[0]),on_fail)
  245. class BTCAmt(Decimal,Hilite,InitErrors):
  246. color = 'yellow'
  247. max_prec = 8
  248. max_amt = 21000000
  249. min_coin_unit = Decimal('0.00000001')
  250. def __new__(cls,num,on_fail='die'):
  251. if type(num) == cls: return num
  252. cls.arg_chk(cls,on_fail)
  253. try:
  254. assert type(num) is not float,'number is floating-point'
  255. assert type(num) is not long,'number is a long integer'
  256. me = Decimal.__new__(cls,str(num))
  257. assert me.normalize().as_tuple()[-1] >= -cls.max_prec,'too many decimal places in coin amount'
  258. assert me <= cls.max_amt,'coin amount too large (>{})'.format(cls.max_amt)
  259. assert me >= 0,'coin amount cannot be negative'
  260. return me
  261. except Exception as e:
  262. m = "{!r}: value cannot be converted to {} ({})"
  263. return cls.init_fail(m.format(num,cls.__name__,e[0]),on_fail)
  264. @classmethod
  265. def fmtc(cls):
  266. raise NotImplementedError
  267. def fmt(self,fs='4.8',color=False,suf=''):
  268. s = str(int(self)) if int(self) == self else self.normalize().__format__('f')
  269. if '.' in fs:
  270. p1,p2 = map(int,fs.split('.',1))
  271. ss = s.split('.',1)
  272. if len(ss) == 2:
  273. a,b = ss
  274. ret = a.rjust(p1) + '.' + (b+suf).ljust(p2+len(suf))
  275. else:
  276. ret = s.rjust(p1) + suf + ' ' * (p2+1)
  277. else:
  278. ret = s.ljust(int(fs))
  279. return self.colorize(ret,color=color)
  280. def hl(self,color=True):
  281. return self.__str__(color=color)
  282. def __str__(self,color=False): # format simply, no exponential notation
  283. return self.colorize(
  284. str(int(self)) if int(self) == self else self.normalize().__format__('f'),
  285. color=color)
  286. def __repr__(self):
  287. return "{}('{}')".format(type(self).__name__,self.__str__())
  288. def __add__(self,other,context=None):
  289. return type(self)(Decimal.__add__(self,other,context))
  290. __radd__ = __add__
  291. def __sub__(self,other,context=None):
  292. return type(self)(Decimal.__sub__(self,other,context))
  293. def __mul__(self,other,context=None):
  294. return type(self)('{:0.8f}'.format(Decimal.__mul__(self,Decimal(other),context)))
  295. def __div__(self,other,context=None):
  296. return type(self)('{:0.8f}'.format(Decimal.__div__(self,Decimal(other),context)))
  297. def __neg__(self,other,context=None):
  298. return type(self)(Decimal.__neg__(self,other,context))
  299. class BCHAmt(BTCAmt): pass
  300. class B2XAmt(BTCAmt): pass
  301. class LTCAmt(BTCAmt): max_amt = 84000000
  302. class CoinAddr(str,Hilite,InitErrors,MMGenObject):
  303. color = 'cyan'
  304. hex_width = 40
  305. def __new__(cls,s,on_fail='die'):
  306. if type(s) == cls: return s
  307. cls.arg_chk(cls,on_fail)
  308. from mmgen.globalvars import g
  309. try:
  310. assert set(s) <= set(ascii_letters+digits),'contains non-alphanumeric characters'
  311. me = str.__new__(cls,s)
  312. va = g.proto.verify_addr(s,hex_width=cls.hex_width,return_dict=True)
  313. assert va,'failed verification'
  314. me.addr_fmt = va['format']
  315. me.hex = va['hex']
  316. cls.width = va['width']
  317. return me
  318. except Exception as e:
  319. m = "{!r}: value cannot be converted to {} address ({})"
  320. return cls.init_fail(m.format(s,g.proto.__name__,e[0]),on_fail)
  321. @classmethod
  322. def fmtc(cls,s,**kwargs):
  323. # True -> 'cyan': use the str value override hack
  324. if 'color' in kwargs and kwargs['color'] == True:
  325. kwargs['color'] = cls.color
  326. if not 'width' in kwargs: kwargs['width'] = cls.width
  327. if kwargs['width'] < len(s):
  328. s = s[:kwargs['width']-2] + '..'
  329. return Hilite.fmtc(s,**kwargs)
  330. def is_for_chain(self,chain):
  331. from mmgen.globalvars import g
  332. vn = g.proto.get_protocol_by_chain(chain).addr_ver_num
  333. def pfx_ok(pfx):
  334. if type(pfx) == tuple:
  335. if self[0] in pfx: return True
  336. elif self[:len(pfx)] == pfx: return True
  337. return False
  338. if self.addr_fmt == 'p2sh' and 'p2sh2' in vn:
  339. return pfx_ok(vn['p2sh'][1]) or pfx_ok(vn['p2sh2'][1])
  340. else:
  341. return pfx_ok(vn[self.addr_fmt][1])
  342. def is_in_tracking_wallet(self):
  343. from mmgen.rpc import rpc_init
  344. d = rpc_init().validateaddress(self)
  345. return d['iswatchonly'] and 'account' in d
  346. class ViewKey(object):
  347. def __new__(cls,s,on_fail='die'):
  348. from mmgen.globalvars import g
  349. if g.proto.name == 'zcash':
  350. return ZcashViewKey.__new__(ZcashViewKey,s,on_fail)
  351. elif g.proto.name == 'monero':
  352. return MoneroViewKey.__new__(MoneroViewKey,s,on_fail)
  353. else:
  354. raise ValueError,'{}: protocol does not support view keys'.format(g.proto.name.capitalize())
  355. class ZcashViewKey(CoinAddr): hex_width = 128
  356. class SeedID(str,Hilite,InitErrors):
  357. color = 'blue'
  358. width = 8
  359. trunc_ok = False
  360. def __new__(cls,seed=None,sid=None,on_fail='die'):
  361. if type(sid) == cls: return sid
  362. cls.arg_chk(cls,on_fail)
  363. try:
  364. if seed:
  365. from mmgen.seed import Seed
  366. assert type(seed) == Seed,'not a Seed instance'
  367. from mmgen.util import make_chksum_8
  368. return str.__new__(cls,make_chksum_8(seed.get_data()))
  369. elif sid:
  370. assert set(sid) <= set(hexdigits.upper()),'not uppercase hex digits'
  371. assert len(sid) == cls.width,'not {} characters wide'.format(cls.width)
  372. return str.__new__(cls,sid)
  373. raise ValueError,'no arguments provided'
  374. except Exception as e:
  375. m = "{!r}: value cannot be converted to SeedID ({})"
  376. return cls.init_fail(m.format(seed or sid,e[0]),on_fail)
  377. class MMGenID(str,Hilite,InitErrors,MMGenObject):
  378. color = 'orange'
  379. width = 0
  380. trunc_ok = False
  381. def __new__(cls,s,on_fail='die'):
  382. cls.arg_chk(cls,on_fail)
  383. from mmgen.globalvars import g
  384. try:
  385. ss = str(s).split(':')
  386. assert len(ss) in (2,3),'not 2 or 3 colon-separated items'
  387. t = MMGenAddrType((ss[1],g.proto.dfl_mmtype)[len(ss)==2],on_fail='raise')
  388. me = str.__new__(cls,'{}:{}:{}'.format(ss[0],t,ss[-1]))
  389. me.sid = SeedID(sid=ss[0],on_fail='raise')
  390. me.idx = AddrIdx(ss[-1],on_fail='raise')
  391. me.mmtype = t
  392. assert t in g.proto.mmtypes,'{}: invalid address type for {}'.format(t,g.proto.__name__)
  393. me.al_id = str.__new__(AddrListID,me.sid+':'+me.mmtype) # checks already done
  394. me.sort_key = '{}:{}:{:0{w}}'.format(me.sid,me.mmtype,me.idx,w=me.idx.max_digits)
  395. return me
  396. except Exception as e:
  397. m = "{}\n{!r}: value cannot be converted to MMGenID"
  398. return cls.init_fail(m.format(e[0],s),on_fail)
  399. class TwMMGenID(str,Hilite,InitErrors,MMGenObject):
  400. color = 'orange'
  401. width = 0
  402. trunc_ok = False
  403. def __new__(cls,s,on_fail='die'):
  404. if type(s) == cls: return s
  405. cls.arg_chk(cls,on_fail)
  406. ret = None
  407. try:
  408. ret = MMGenID(s,on_fail='raise')
  409. sort_key,idtype = ret.sort_key,'mmgen'
  410. except Exception as e:
  411. try:
  412. from mmgen.globalvars import g
  413. assert s.split(':',1)[0] == g.proto.base_coin.lower(),(
  414. "not a string beginning with the prefix '{}:'".format(g.proto.base_coin.lower()))
  415. assert set(s[4:]) <= set(ascii_letters+digits),'contains non-alphanumeric characters'
  416. assert len(s) > 4,'not more that four characters long'
  417. ret,sort_key,idtype = str(s),'z_'+s,'non-mmgen'
  418. except Exception as f:
  419. m = "{}\nValue is {}\n{!r}: value cannot be converted to TwMMGenID"
  420. return cls.init_fail(m.format(e[0],f[0],s),on_fail)
  421. me = str.__new__(cls,ret)
  422. me.obj = ret
  423. me.sort_key = sort_key
  424. me.type = idtype
  425. return me
  426. # contains TwMMGenID,TwComment. Not for display
  427. class TwLabel(str,InitErrors,MMGenObject):
  428. def __new__(cls,s,on_fail='die'):
  429. if type(s) == cls: return s
  430. cls.arg_chk(cls,on_fail)
  431. try:
  432. ss = s.split(None,1)
  433. mmid = TwMMGenID(ss[0],on_fail='raise')
  434. comment = TwComment(ss[1] if len(ss) == 2 else '',on_fail='raise')
  435. me = str.__new__(cls,'{}{}'.format(mmid,' {}'.format(comment) if comment else ''))
  436. me.mmid = mmid
  437. me.comment = comment
  438. return me
  439. except Exception as e:
  440. m = u"{}\n{!r}: value cannot be converted to TwLabel"
  441. return cls.init_fail(m.format(e[0],s),on_fail)
  442. class HexStr(str,Hilite,InitErrors):
  443. color = 'red'
  444. trunc_ok = False
  445. def __new__(cls,s,on_fail='die',case='lower'):
  446. if type(s) == cls: return s
  447. assert case in ('upper','lower')
  448. cls.arg_chk(cls,on_fail)
  449. try:
  450. assert type(s) in (str,unicode,bytes),'not a string'
  451. assert set(s) <= set(getattr(hexdigits,case)()),'not {}case hexadecimal symbols'.format(case)
  452. assert not len(s) % 2,'odd-length string'
  453. return str.__new__(cls,s)
  454. except Exception as e:
  455. m = "{!r}: value cannot be converted to {} (value is {})"
  456. return cls.init_fail(m.format(s,cls.__name__,e[0]),on_fail)
  457. class HexStrWithWidth(HexStr):
  458. color = 'nocolor'
  459. trunc_ok = False
  460. hexcase = 'lower'
  461. width = None
  462. def __new__(cls,s,on_fail='die'):
  463. cls.arg_chk(cls,on_fail)
  464. try:
  465. ret = HexStr.__new__(cls,s,case=cls.hexcase,on_fail='raise')
  466. assert len(s) == cls.width,'Value is not {} characters wide'.format(cls.width)
  467. return ret
  468. except Exception as e:
  469. m = "{}\n{!r}: value cannot be converted to {}"
  470. return cls.init_fail(m.format(e[0],s,cls.__name__),on_fail)
  471. class MMGenTxID(HexStrWithWidth): color,width,hexcase = 'red',6,'upper'
  472. class MoneroViewKey(HexStrWithWidth): color,width,hexcase = 'cyan',64,'lower'
  473. class WalletPassword(HexStrWithWidth): color,width,hexcase = 'blue',32,'lower'
  474. class CoinTxID(HexStrWithWidth): color,width,hexcase = 'purple',64,'lower'
  475. class WifKey(str,Hilite,InitErrors):
  476. width = 53
  477. color = 'blue'
  478. def __new__(cls,s,on_fail='die'):
  479. if type(s) == cls: return s
  480. cls.arg_chk(cls,on_fail)
  481. try:
  482. assert set(s) <= set(ascii_letters+digits),'not an ascii string'
  483. from mmgen.globalvars import g
  484. g.proto.wif2hex(s) # raises exception on error
  485. return str.__new__(cls,s)
  486. except Exception as e:
  487. m = '{!r}: invalid value for WIF key ({})'.format(s,e[0])
  488. return cls.init_fail(m,on_fail)
  489. class PubKey(HexStr,MMGenObject): # TODO: add some real checks
  490. def __new__(cls,s,compressed,on_fail='die'):
  491. try:
  492. assert type(compressed) == bool,"'compressed' must be of type bool"
  493. me = HexStr.__new__(cls,s,case='lower',on_fail='raise')
  494. me.compressed = compressed
  495. return me
  496. except Exception as e:
  497. m = '{!r}: invalid value for pubkey ({})'.format(s,e[0])
  498. return cls.init_fail(m,on_fail)
  499. class PrivKey(str,Hilite,InitErrors,MMGenObject):
  500. color = 'red'
  501. width = 64
  502. trunc_ok = False
  503. compressed = MMGenImmutableAttr('compressed',bool,typeconv=False)
  504. wif = MMGenImmutableAttr('wif',WifKey,typeconv=False)
  505. # initialize with (priv_bin,compressed), WIF or self
  506. def __new__(cls,s=None,compressed=None,wif=None,pubkey_type=None,on_fail='die'):
  507. from mmgen.globalvars import g
  508. if type(s) == cls: return s
  509. cls.arg_chk(cls,on_fail)
  510. if wif:
  511. try:
  512. assert s == None
  513. assert set(wif) <= set(ascii_letters+digits),'not an ascii string'
  514. w2h = g.proto.wif2hex(wif) # raises exception on error
  515. me = str.__new__(cls,w2h['hex'])
  516. me.compressed = w2h['compressed']
  517. me.pubkey_type = w2h['pubkey_type']
  518. me.wif = str.__new__(WifKey,wif) # check has been done
  519. me.orig_hex = None
  520. return me
  521. except Exception as e:
  522. fs = "Value {!r} cannot be converted to {} WIF key ({})"
  523. return cls.init_fail(fs.format(wif,g.coin,e[0]),on_fail)
  524. try:
  525. assert s and type(compressed) == bool and pubkey_type,'Incorrect args for PrivKey()'
  526. assert len(s) == cls.width / 2,'Key length must be {}'.format(cls.width/2)
  527. me = str.__new__(cls,g.proto.preprocess_key(s.encode('hex'),pubkey_type))
  528. me.orig_hex = s.encode('hex') # save the non-preprocessed key
  529. me.compressed = compressed
  530. me.pubkey_type = pubkey_type
  531. if pubkey_type != 'password': # skip WIF creation for passwds
  532. me.wif = WifKey(g.proto.hex2wif(me,pubkey_type,compressed),on_fail='raise')
  533. return me
  534. except Exception as e:
  535. fs = "Key={!r}\nCompressed={}\nValue pair cannot be converted to PrivKey\n({})"
  536. return cls.init_fail(fs.format(s,compressed,e),on_fail)
  537. class AddrListID(str,Hilite,InitErrors,MMGenObject):
  538. width = 10
  539. trunc_ok = False
  540. color = 'yellow'
  541. def __new__(cls,sid,mmtype,on_fail='die'):
  542. cls.arg_chk(cls,on_fail)
  543. try:
  544. assert type(sid) == SeedID,"{!r} not a SeedID instance".format(sid)
  545. t = MMGenAddrType,MMGenPasswordType
  546. assert type(mmtype) in t,"{!r} not an instance of {}".format(mmtype,','.join([i.__name__ for i in t]))
  547. me = str.__new__(cls,sid+':'+mmtype)
  548. me.sid = sid
  549. me.mmtype = mmtype
  550. return me
  551. except Exception as e:
  552. m = "Cannot create AddrListID ({})".format(e[0])
  553. return cls.init_fail(m,on_fail)
  554. class MMGenLabel(unicode,Hilite,InitErrors):
  555. color = 'pink'
  556. allowed = []
  557. forbidden = []
  558. max_len = 0
  559. min_len = 0
  560. desc = 'label'
  561. def __new__(cls,s,on_fail='die',msg=None):
  562. if type(s) == cls: return s
  563. cls.arg_chk(cls,on_fail)
  564. for k in cls.forbidden,cls.allowed:
  565. assert type(k) == list
  566. for ch in k: assert type(ch) == unicode and len(ch) == 1
  567. try:
  568. s = s.strip()
  569. if type(s) != unicode:
  570. s = s.decode('utf8')
  571. from mmgen.util import capfirst
  572. assert len(s) <= cls.max_len, 'too long (>{} symbols)'.format(cls.max_len)
  573. assert len(s) >= cls.min_len, 'too short (<{} symbols)'.format(cls.min_len)
  574. assert not cls.allowed or set(list(s)).issubset(set(cls.allowed)),\
  575. u'contains non-allowed symbols: {}'.format(' '.join(set(list(s)) - set(cls.allowed)))
  576. assert not cls.forbidden or not any(ch in s for ch in cls.forbidden),\
  577. u"contains one of these forbidden symbols: '{}'".format("', '".join(cls.forbidden))
  578. return unicode.__new__(cls,s)
  579. except Exception as e:
  580. m = u"{!r}: value cannot be converted to {} ({})"
  581. return cls.init_fail(m.format(s,cls.__name__,e),on_fail)
  582. class MMGenWalletLabel(MMGenLabel):
  583. max_len = 48
  584. allowed = [unichr(i+32) for i in range(95)]
  585. desc = 'wallet label'
  586. class TwComment(MMGenLabel):
  587. max_len = 32
  588. allowed = [unichr(i+32) for i in range(95)]
  589. desc = 'tracking wallet comment'
  590. class MMGenTXLabel(MMGenLabel):
  591. max_len = 72
  592. desc = 'transaction label'
  593. class MMGenPWIDString(MMGenLabel):
  594. max_len = 256
  595. min_len = 1
  596. desc = 'password ID string'
  597. forbidden = list(u' :/\\')
  598. class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
  599. width = 1
  600. trunc_ok = False
  601. color = 'blue'
  602. mmtypes = { # 'name' is used to cook the seed, so it must never change!
  603. 'L': { 'name':'legacy',
  604. 'pubkey_type':'std',
  605. 'compressed':False,
  606. 'gen_method':'p2pkh',
  607. 'addr_fmt':'p2pkh',
  608. 'desc':'Legacy uncompressed address'},
  609. 'C': { 'name':'compressed',
  610. 'pubkey_type':'std',
  611. 'compressed':True,
  612. 'gen_method':'p2pkh',
  613. 'addr_fmt':'p2pkh',
  614. 'desc':'Compressed P2PKH address'},
  615. 'S': { 'name':'segwit',
  616. 'pubkey_type':'std',
  617. 'compressed':True,
  618. 'gen_method':'segwit',
  619. 'addr_fmt':'p2sh',
  620. 'desc':'Segwit P2SH-P2WPKH address' },
  621. 'E': { 'name':'ethereum',
  622. 'pubkey_type':'std',
  623. 'compressed':False,
  624. 'gen_method':'ethereum',
  625. 'addr_fmt':'ethereum',
  626. 'desc':'Ethereum address' },
  627. 'Z': { 'name':'zcash_z',
  628. 'pubkey_type':'zcash_z',
  629. 'compressed':False,
  630. 'gen_method':'zcash_z',
  631. 'addr_fmt':'zcash_z',
  632. 'desc':'Zcash z-address' },
  633. 'M': { 'name':'monero',
  634. 'pubkey_type':'monero',
  635. 'compressed':False,
  636. 'gen_method':'monero',
  637. 'addr_fmt':'monero',
  638. 'desc':'Monero address'}
  639. }
  640. def __new__(cls,s,on_fail='die',errmsg=None):
  641. if type(s) == cls: return s
  642. cls.arg_chk(cls,on_fail)
  643. from mmgen.globalvars import g
  644. try:
  645. for k,v in cls.mmtypes.items():
  646. if s in (k,v['name']):
  647. if s == v['name']: s = k
  648. me = str.__new__(cls,s)
  649. for k in ('name','pubkey_type','compressed','gen_method','addr_fmt','desc'):
  650. setattr(me,k,v[k])
  651. assert me in g.proto.mmtypes + ('P',), (
  652. "'{}': invalid address type for {}".format(me.name,g.proto.__name__))
  653. me.extra_attrs = []
  654. if me.name in ('monero','zcash_z'): me.extra_attrs += ['viewkey']
  655. if me.name == 'monero': me.extra_attrs += ['wallet_passwd']
  656. me.wif_label = ('wif:','spendkey:')[me.name=='monero']
  657. return me
  658. raise ValueError,'not found'
  659. except Exception as e:
  660. m = '{}{!r}: invalid value for {} ({})'.format(
  661. ('{!r}\n'.format(errmsg) if errmsg else ''),s,cls.__name__,e[0])
  662. return cls.init_fail(m,on_fail)
  663. @classmethod
  664. def get_names(cls):
  665. return [v['name'] for v in cls.mmtypes.values()]
  666. class MMGenPasswordType(MMGenAddrType):
  667. mmtypes = {
  668. 'P': { 'name':'password',
  669. 'pubkey_type':'password',
  670. 'compressed':False,
  671. 'gen_method':None,
  672. 'addr_fmt':None,
  673. 'desc':'Password generated from MMGen seed'}
  674. }