obj.py 28 KB

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