obj.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  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. obj.py: MMGen native classes
  20. """
  21. import sys,os,unicodedata
  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. def truncate_str(s,width): # width = screen width
  34. wide_count = 0
  35. for i in range(len(s)):
  36. wide_count += unicodedata.east_asian_width(s[i]) in ('F','W')
  37. if wide_count + i >= width:
  38. return s[:i] + ('',' ')[
  39. unicodedata.east_asian_width(s[i]) in ('F','W')
  40. and wide_count + i == width]
  41. else: # pad the string to width if necessary
  42. return s + ' '*(width-len(s)-wide_count)
  43. class MMGenObject(object):
  44. # Pretty-print any object subclassed from MMGenObject, recursing into sub-objects - WIP
  45. def pmsg(self): print(self.pformat())
  46. def pdie(self): print(self.pformat()); sys.exit(0)
  47. def pformat(self,lvl=0,id_list=[]):
  48. scalars = (str,int,float,Decimal)
  49. def do_list(out,e,lvl=0,is_dict=False):
  50. out.append('\n')
  51. for i in e:
  52. el = i if not is_dict else e[i]
  53. if is_dict:
  54. out.append('{s}{:<{l}}'.format(i,s=' '*(4*lvl+8),l=10,l2=8*(lvl+1)+8))
  55. if hasattr(el,'pformat'):
  56. out.append('{:>{l}}{}'.format('',el.pformat(lvl=lvl+1,id_list=id_list+[id(self)]),l=(lvl+1)*8))
  57. elif type(el) in scalars:
  58. if isList(e):
  59. out.append('{:>{l}}{:16}\n'.format('',repr(el),l=lvl*8))
  60. else:
  61. out.append(' {}'.format(repr(el)))
  62. elif isList(el) or isDict(el):
  63. indent = 1 if is_dict else lvl*8+4
  64. out.append('{:>{l}}{:16}'.format('','<'+type(el).__name__+'>',l=indent))
  65. if isList(el) and type(el[0]) in scalars: out.append('\n')
  66. do_list(out,el,lvl=lvl+1,is_dict=isDict(el))
  67. else:
  68. out.append('{:>{l}}{:16} {}\n'.format('','<'+type(el).__name__+'>',repr(el),l=(lvl*8)+8))
  69. out.append('\n')
  70. if not e: out.append('{}\n'.format(repr(e)))
  71. from collections import OrderedDict
  72. def isDict(obj):
  73. return issubclass(type(obj),dict) or issubclass(type(obj),OrderedDict)
  74. def isList(obj):
  75. return issubclass(type(obj),list) and type(obj) != OrderedDict
  76. def isScalar(obj):
  77. return any(issubclass(type(obj),t) for t in scalars)
  78. # print type(self)
  79. # print dir(self)
  80. # print self.__dict__
  81. # print self.__dict__.keys()
  82. # print self.keys()
  83. out = ['<{}>{}\n'.format(type(self).__name__,' '+repr(self) if isScalar(self) else '')]
  84. if id(self) in id_list:
  85. return out[-1].rstrip() + ' [RECURSION]\n'
  86. if isList(self) or isDict(self):
  87. do_list(out,self,lvl=lvl,is_dict=isDict(self))
  88. # print repr(self.__dict__.keys())
  89. for k in self.__dict__:
  90. if k in ('_OrderedDict__root','_OrderedDict__map'): continue # exclude these because of recursion
  91. e = getattr(self,k)
  92. if isList(e) or isDict(e):
  93. out.append('{:>{l}}{:<10} {:16}'.format('',k,'<'+type(e).__name__+'>',l=(lvl*8)+4))
  94. do_list(out,e,lvl=lvl,is_dict=isDict(e))
  95. elif hasattr(e,'pformat') and type(e) != type:
  96. out.append('{:>{l}}{:10} {}'.format('',k,e.pformat(lvl=lvl+1,id_list=id_list+[id(self)]),l=(lvl*8)+4))
  97. else:
  98. out.append('{:>{l}}{:<10} {:16} {}\n'.format(
  99. '',k,'<'+type(e).__name__+'>',repr(e),l=(lvl*8)+4))
  100. import re
  101. return re.sub('\n+','\n',''.join(out))
  102. # dict that keeps a list of keys for efficient lookup by index
  103. class IndexedDict(dict):
  104. def __init__(self,*args,**kwargs):
  105. if args or kwargs:
  106. self.die('initializing values via constructor')
  107. self.__keylist = []
  108. return dict.__init__(self,*args,**kwargs)
  109. def __setitem__(self,key,value):
  110. if key in self:
  111. self.die('reassignment to existing key')
  112. self.__keylist.append(key)
  113. return dict.__setitem__(self,key,value)
  114. @property
  115. def keys(self):
  116. return self.__keylist
  117. def key(self,idx):
  118. return self.__keylist[idx]
  119. def __delitem__(self,*args): self.die('item deletion')
  120. def move_to_end(self,*args): self.die('item moving')
  121. def clear(self,*args): self.die('clearing')
  122. def update(self,*args): self.die('updating')
  123. def die(self,desc):
  124. raise NotImplementedError('{} not implemented for type {}'.format(desc,type(self).__name__))
  125. class MMGenList(list,MMGenObject): pass
  126. class MMGenDict(dict,MMGenObject): pass
  127. class AddrListList(list,MMGenObject): pass
  128. class InitErrors(object):
  129. on_fail='die'
  130. @classmethod
  131. def arg_chk(cls,on_fail):
  132. cls.on_fail = on_fail
  133. assert on_fail in ('die','return','silent','raise'),(
  134. "'{}': invalid value for 'on_fail' in class {}".format(on_fail,cls.__name__) )
  135. @classmethod
  136. def init_fail(cls,e,m,e2=None,m2=None,objname=None,preformat=False):
  137. if preformat:
  138. errmsg = m
  139. else:
  140. fs = "{!r}: value cannot be converted to {} {}({})"
  141. e2_fmt = '({}) '.format(e2.args[0]) if e2 else ''
  142. errmsg = fs.format(m,objname or cls.__name__,e2_fmt,e.args[0])
  143. if m2: errmsg = '{!r}\n{}'.format(m2,errmsg)
  144. from mmgen.globalvars import g
  145. if g.traceback: cls.on_fail == 'raise'
  146. from mmgen.util import die,msg
  147. if cls.on_fail == 'silent': return None # TODO: return False instead?
  148. elif cls.on_fail == 'raise': raise ValueError(errmsg)
  149. elif cls.on_fail == 'die': die(1,errmsg)
  150. elif cls.on_fail == 'return':
  151. if errmsg: msg(errmsg)
  152. return None # TODO: here too?
  153. class Hilite(object):
  154. color = 'red'
  155. color_always = False
  156. width = 0
  157. trunc_ok = True
  158. dtype = str
  159. @classmethod
  160. # 'width' is screen width (greater than len(s) for CJK strings)
  161. # 'append_chars' and 'encl' must consist of single-width chars only
  162. def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None,
  163. center=False,nullrepl='',append_chars='',append_color=False):
  164. if cls.dtype == bytes: s = s.decode()
  165. s_wide_count = len([1 for ch in s if unicodedata.east_asian_width(ch) in ('F','W')])
  166. assert type(encl) is str and len(encl) in (0,2),"'encl' must be 2-character str"
  167. a,b = list(encl) if encl else ('','')
  168. add_len = len(a) + len(b) + len(append_chars)
  169. if width == None: width = cls.width
  170. if trunc_ok == None: trunc_ok = cls.trunc_ok
  171. assert width >= 2 + add_len,( # 2 because CJK
  172. "'{!r}': invalid width ({}) (width must be at least 2)".format(s,width))
  173. if len(s) + s_wide_count + add_len > width:
  174. assert trunc_ok, "If 'trunc_ok' is false, 'width' must be >= screen width of string"
  175. s = truncate_str(s,width-add_len)
  176. if s == '' and nullrepl:
  177. s = nullrepl.center(width)
  178. else:
  179. s = a+s+b
  180. if center: s = s.center(width)
  181. if append_chars:
  182. return cls.colorize(s,color=color) + \
  183. cls.colorize(append_chars.ljust(width-len(s)-s_wide_count),color=append_color)
  184. else:
  185. return cls.colorize(s.ljust(width-s_wide_count),color=color)
  186. @classmethod
  187. def colorize(cls,s,color=True):
  188. if cls.dtype == bytes: s = s.decode()
  189. k = color if type(color) is str else cls.color # hack: override color with str value
  190. return globals()[k](s) if (color or cls.color_always) else s
  191. def fmt(self,*args,**kwargs):
  192. assert args == () # forbid invocation w/o keywords
  193. return self.fmtc(self,*args,**kwargs)
  194. @classmethod
  195. def hlc(cls,s,color=True):
  196. return cls.colorize(s,color=color)
  197. def hl(self,color=True):
  198. return self.colorize(self,color=color)
  199. def __str__(self):
  200. return self.colorize(self,color=False)
  201. class Str(str,Hilite): pass
  202. class Int(int,Hilite): pass
  203. # For attrs that are always present in the data instance
  204. # Reassignment and deletion forbidden
  205. class MMGenImmutableAttr(object): # Descriptor
  206. def __init__(self,name,dtype,typeconv=True,no_type_check=False):
  207. self.typeconv = typeconv
  208. self.no_type_check = no_type_check
  209. assert type(dtype) in (str,type) or dtype is None
  210. self.name = name
  211. self.dtype = dtype
  212. def __get__(self,instance,owner):
  213. return instance.__dict__[self.name]
  214. # forbid all reassignment
  215. def set_attr_ok(self,instance):
  216. return not self.name in instance.__dict__
  217. # return not hasattr(instance,self.name)
  218. def __set__(self,instance,value):
  219. if not self.set_attr_ok(instance):
  220. m = "Attribute '{}' of {} instance cannot be reassigned"
  221. raise AttributeError(m.format(self.name,type(instance)))
  222. if self.typeconv: # convert type
  223. instance.__dict__[self.name] = \
  224. globals()[self.dtype](value,on_fail='raise') if type(self.dtype) == str else self.dtype(value)
  225. else: # check type
  226. if type(value) == self.dtype or self.no_type_check:
  227. instance.__dict__[self.name] = value
  228. else:
  229. m = "Attribute '{}' of {} instance must of type {}"
  230. raise TypeError(m.format(self.name,type(instance),self.dtype))
  231. def __delete__(self,instance):
  232. m = "Atribute '{}' of {} instance cannot be deleted"
  233. raise AttributeError(m.format(self.name,type(instance)))
  234. # For attrs that might not be present in the data instance
  235. # Reassignment or deletion allowed if specified
  236. class MMGenListItemAttr(MMGenImmutableAttr): # Descriptor
  237. def __init__(self,name,dtype,typeconv=True,reassign_ok=False,delete_ok=False):
  238. self.reassign_ok = reassign_ok
  239. self.delete_ok = delete_ok
  240. MMGenImmutableAttr.__init__(self,name,dtype,typeconv=typeconv)
  241. # return None if attribute doesn't exist
  242. def __get__(self,instance,owner):
  243. try: return instance.__dict__[self.name]
  244. except: return None
  245. def set_attr_ok(self,instance):
  246. return getattr(instance,self.name) == None or self.reassign_ok
  247. def __delete__(self,instance):
  248. if self.delete_ok:
  249. if self.name in instance.__dict__:
  250. del instance.__dict__[self.name]
  251. else:
  252. MMGenImmutableAttr.__delete__(self,instance)
  253. class MMGenListItem(MMGenObject):
  254. valid_attrs = None
  255. valid_attrs_extra = set()
  256. def __init__(self,*args,**kwargs):
  257. if self.valid_attrs == None:
  258. type(self).valid_attrs = (
  259. ( {e for e in dir(self) if e[:2] != '__'} | self.valid_attrs_extra ) -
  260. {'pformat','pmsg','pdie','valid_attrs','valid_attrs_extra'} )
  261. if args:
  262. raise ValueError('Non-keyword args not allowed')
  263. for k in kwargs:
  264. if kwargs[k] != None:
  265. setattr(self,k,kwargs[k])
  266. # allow only valid attributes to be set
  267. def __setattr__(self,name,value):
  268. if name not in self.valid_attrs:
  269. m = "'{}': no such attribute in class {}"
  270. raise AttributeError(m.format(name,type(self)))
  271. return object.__setattr__(self,name,value)
  272. class MMGenIdx(int,InitErrors):
  273. min_val = 1
  274. max_val = None
  275. max_digits = None
  276. def __new__(cls,num,on_fail='die'):
  277. cls.arg_chk(on_fail)
  278. try:
  279. assert type(num) is not float,'is float'
  280. me = int.__new__(cls,num)
  281. if cls.max_digits:
  282. assert len(str(me)) <= cls.max_digits,'has more than {} digits'.format(cls.max_digits)
  283. if cls.max_val:
  284. assert me <= cls.max_val,'is greater than {}'.format(cls.max_val)
  285. assert me >= cls.min_val,'is less than {}'.format(cls.min_val)
  286. return me
  287. except Exception as e:
  288. return cls.init_fail(e,num)
  289. class SeedShareIdx(MMGenIdx): max_val = 1024
  290. class SeedShareCount(SeedShareIdx): min_val = 2
  291. class MasterShareIdx(MMGenIdx): max_val = 1024
  292. class AddrIdx(MMGenIdx): max_digits = 7
  293. class AddrIdxList(list,InitErrors,MMGenObject):
  294. max_len = 1000000
  295. def __init__(self,fmt_str=None,idx_list=None,on_fail='die',sep=','):
  296. type(self).arg_chk(on_fail)
  297. try:
  298. if idx_list:
  299. return list.__init__(self,sorted({AddrIdx(i,on_fail='raise') for i in idx_list}))
  300. elif fmt_str:
  301. ret = []
  302. for i in (fmt_str.split(sep)):
  303. j = i.split('-')
  304. if len(j) == 1:
  305. idx = AddrIdx(i,on_fail='raise')
  306. if not idx: break
  307. ret.append(idx)
  308. elif len(j) == 2:
  309. beg = AddrIdx(j[0],on_fail='raise')
  310. if not beg: break
  311. end = AddrIdx(j[1],on_fail='raise')
  312. if not beg: break
  313. if end < beg: break
  314. ret.extend([AddrIdx(x,on_fail='raise') for x in range(beg,end+1)])
  315. else: break
  316. else:
  317. return list.__init__(self,sorted(set(ret))) # fell off end of loop - success
  318. raise ValueError("{!r}: invalid range".format(i))
  319. except Exception as e:
  320. return type(self).init_fail(e,idx_list or fmt_str)
  321. class MMGenRange(tuple,InitErrors,MMGenObject):
  322. min_idx = None
  323. max_idx = None
  324. def __new__(cls,*args,on_fail='die'):
  325. cls.arg_chk(on_fail)
  326. try:
  327. if len(args) == 1:
  328. s = args[0]
  329. if type(s) == cls: return s
  330. assert issubclass(type(s),str),'not a string or string subclass'
  331. ss = s.split('-',1)
  332. first = int(ss[0])
  333. last = int(ss.pop())
  334. else:
  335. s = repr(args) # needed if exception occurs
  336. assert len(args) == 2,'one format string arg or two start,stop args required'
  337. first,last = args
  338. assert first <= last, 'start of range greater than end of range'
  339. if cls.min_idx is not None:
  340. assert first >= cls.min_idx, 'start of range < {:,}'.format(cls.min_idx)
  341. if cls.max_idx is not None:
  342. assert last <= cls.max_idx, 'end of range > {:,}'.format(cls.max_idx)
  343. return tuple.__new__(cls,(first,last))
  344. except Exception as e:
  345. return cls.init_fail(e,s)
  346. @property
  347. def first(self):
  348. return self[0]
  349. @property
  350. def last(self):
  351. return self[1]
  352. def iterate(self):
  353. return range(self[0],self[1]+1)
  354. @property
  355. def items(self):
  356. return list(self.iterate())
  357. class SubSeedIdxRange(MMGenRange):
  358. min_idx = 1
  359. max_idx = 1000000
  360. class UnknownCoinAmt(Decimal): pass
  361. class BTCAmt(Decimal,Hilite,InitErrors):
  362. color = 'yellow'
  363. max_prec = 8
  364. max_amt = 21000000
  365. satoshi = Decimal('0.00000001')
  366. min_coin_unit = satoshi
  367. amt_fs = '4.8'
  368. units = ('satoshi',)
  369. forbidden_types = (float,int)
  370. # NB: 'from_decimal' rounds down to precision of 'min_coin_unit'
  371. def __new__(cls,num,from_unit=None,from_decimal=False,on_fail='die'):
  372. if type(num) == cls: return num
  373. cls.arg_chk(on_fail)
  374. try:
  375. if from_unit:
  376. assert from_unit in cls.units,(
  377. "'{}': unrecognized denomination for {}".format(from_unit,cls.__name__))
  378. assert type(num) == int,'value is not an integer or long integer'
  379. me = Decimal.__new__(cls,num * getattr(cls,from_unit))
  380. elif from_decimal:
  381. assert type(num) == Decimal,(
  382. "number is not of type Decimal (type is {!r})".format(type(num).__name__))
  383. me = Decimal.__new__(cls,num).quantize(cls.min_coin_unit)
  384. else:
  385. for t in cls.forbidden_types:
  386. assert type(num) is not t,"number is of forbidden type '{}'".format(t.__name__)
  387. me = Decimal.__new__(cls,str(num))
  388. assert me.normalize().as_tuple()[-1] >= -cls.max_prec,'too many decimal places in coin amount'
  389. if cls.max_amt:
  390. assert me <= cls.max_amt,'{}: coin amount too large (>{})'.format(me,cls.max_amt)
  391. assert me >= 0,'coin amount cannot be negative'
  392. return me
  393. except Exception as e:
  394. return cls.init_fail(e,num)
  395. def toSatoshi(self):
  396. return int(Decimal(self) // self.satoshi)
  397. def to_unit(self,unit,show_decimal=False):
  398. ret = Decimal(self) // getattr(self,unit)
  399. if show_decimal and ret < 1:
  400. return '{:.8f}'.format(ret).rstrip('0')
  401. return int(ret)
  402. @classmethod
  403. def fmtc(cls):
  404. raise NotImplementedError
  405. def fmt(self,fs=None,color=False,suf='',prec=1000):
  406. if fs == None: fs = self.amt_fs
  407. s = str(int(self)) if int(self) == self else self.normalize().__format__('f')
  408. if '.' in fs:
  409. p1,p2 = list(map(int,fs.split('.',1)))
  410. ss = s.split('.',1)
  411. if len(ss) == 2:
  412. a,b = ss
  413. ret = a.rjust(p1) + '.' + ((b+suf).ljust(p2+len(suf)))[:prec]
  414. else:
  415. ret = s.rjust(p1) + suf + (' ' * (p2+1))[:prec+1-len(suf)]
  416. else:
  417. ret = s.ljust(int(fs))
  418. return self.colorize(ret,color=color)
  419. def hl(self,color=True):
  420. return self.__str__(color=color)
  421. def __str__(self,color=False): # format simply, no exponential notation
  422. return self.colorize(
  423. str(int(self)) if int(self) == self else self.normalize().__format__('f'),
  424. color=color)
  425. def __repr__(self):
  426. return "{}('{}')".format(type(self).__name__,self.__str__())
  427. def __add__(self,other):
  428. return type(self)(Decimal.__add__(self,other))
  429. __radd__ = __add__
  430. def __sub__(self,other):
  431. return type(self)(Decimal.__sub__(self,other))
  432. def __mul__(self,other):
  433. return type(self)('{:0.8f}'.format(Decimal.__mul__(self,Decimal(other))))
  434. def __div__(self,other):
  435. return type(self)('{:0.8f}'.format(Decimal.__div__(self,Decimal(other))))
  436. def __neg__(self,other):
  437. return type(self)(Decimal.__neg__(self,other))
  438. class BCHAmt(BTCAmt): pass
  439. class B2XAmt(BTCAmt): pass
  440. class LTCAmt(BTCAmt): max_amt = 84000000
  441. class XMRAmt(BTCAmt): min_coin_unit = Decimal('0.000000000001')
  442. from mmgen.altcoins.eth.obj import ETHAmt,ETHNonce
  443. class CoinAddr(str,Hilite,InitErrors,MMGenObject):
  444. color = 'cyan'
  445. hex_width = 40
  446. width = 1
  447. trunc_ok = False
  448. def __new__(cls,s,on_fail='die'):
  449. if type(s) == cls: return s
  450. cls.arg_chk(on_fail)
  451. from mmgen.globalvars import g
  452. try:
  453. assert set(s) <= set(ascii_letters+digits),'contains non-alphanumeric characters'
  454. me = str.__new__(cls,s)
  455. va = g.proto.verify_addr(s,hex_width=cls.hex_width,return_dict=True)
  456. assert va,'failed verification'
  457. me.addr_fmt = va['format']
  458. me.hex = va['hex']
  459. return me
  460. except Exception as e:
  461. return cls.init_fail(e,s,objname='{} address'.format(g.proto.__name__))
  462. @classmethod
  463. def fmtc(cls,s,**kwargs):
  464. # True -> 'cyan': use the str value override hack
  465. if 'color' in kwargs and kwargs['color'] == True:
  466. kwargs['color'] = cls.color
  467. if not 'width' in kwargs: kwargs['width'] = cls.width
  468. if kwargs['width'] < len(s):
  469. s = s[:kwargs['width']-2] + '..'
  470. return Hilite.fmtc(s,**kwargs)
  471. def is_for_chain(self,chain):
  472. from mmgen.globalvars import g
  473. if g.proto.__name__[:8] == 'Ethereum':
  474. return True
  475. def pfx_ok(pfx):
  476. if type(pfx) == tuple:
  477. if self[0] in pfx: return True
  478. elif self[:len(pfx)] == pfx: return True
  479. return False
  480. proto = g.proto.get_protocol_by_chain(chain)
  481. vn = proto.addr_ver_num
  482. if self.addr_fmt == 'bech32':
  483. return self[:len(proto.bech32_hrp)] == proto.bech32_hrp
  484. elif self.addr_fmt == 'p2sh' and 'p2sh2' in vn:
  485. return pfx_ok(vn['p2sh'][1]) or pfx_ok(vn['p2sh2'][1])
  486. else:
  487. return pfx_ok(vn[self.addr_fmt][1])
  488. class TokenAddr(CoinAddr):
  489. color = 'blue'
  490. class ViewKey(object):
  491. def __new__(cls,s,on_fail='die'):
  492. from mmgen.globalvars import g
  493. if g.proto.name == 'zcash':
  494. return ZcashViewKey.__new__(ZcashViewKey,s,on_fail)
  495. elif g.proto.name == 'monero':
  496. return MoneroViewKey.__new__(MoneroViewKey,s,on_fail)
  497. else:
  498. raise ValueError('{}: protocol does not support view keys'.format(g.proto.name.capitalize()))
  499. class ZcashViewKey(CoinAddr): hex_width = 128
  500. class SeedID(str,Hilite,InitErrors):
  501. color = 'blue'
  502. width = 8
  503. trunc_ok = False
  504. def __new__(cls,seed=None,sid=None,on_fail='die'):
  505. if type(sid) == cls: return sid
  506. cls.arg_chk(on_fail)
  507. try:
  508. if seed:
  509. from mmgen.seed import SeedBase
  510. assert issubclass(type(seed),SeedBase),'not a subclass of SeedBase'
  511. from mmgen.util import make_chksum_8
  512. return str.__new__(cls,make_chksum_8(seed.data))
  513. elif sid:
  514. assert set(sid) <= set(hexdigits.upper()),'not uppercase hex digits'
  515. assert len(sid) == cls.width,'not {} characters wide'.format(cls.width)
  516. return str.__new__(cls,sid)
  517. raise ValueError('no arguments provided')
  518. except Exception as e:
  519. return cls.init_fail(e,seed or sid)
  520. class SubSeedIdx(str,Hilite,InitErrors):
  521. color = 'red'
  522. trunc_ok = False
  523. def __new__(cls,s,on_fail='die'):
  524. if type(s) == cls: return s
  525. cls.arg_chk(on_fail)
  526. try:
  527. assert issubclass(type(s),str),'not a string or string subclass'
  528. idx = s[:-1] if s[-1] in 'SsLl' else s
  529. from mmgen.util import is_int
  530. assert is_int(idx),"valid format: an integer, plus optional letter 'S','s','L' or 'l'"
  531. idx = int(idx)
  532. assert idx >= SubSeedIdxRange.min_idx, 'subseed index < {:,}'.format(SubSeedIdxRange.min_idx)
  533. assert idx <= SubSeedIdxRange.max_idx, 'subseed index > {:,}'.format(SubSeedIdxRange.max_idx)
  534. sstype,ltr = ('short','S') if s[-1] in 'Ss' else ('long','L')
  535. me = str.__new__(cls,str(idx)+ltr)
  536. me.idx = idx
  537. me.type = sstype
  538. return me
  539. except Exception as e:
  540. return cls.init_fail(e,s)
  541. class MMGenID(str,Hilite,InitErrors,MMGenObject):
  542. color = 'orange'
  543. width = 0
  544. trunc_ok = False
  545. def __new__(cls,s,on_fail='die'):
  546. cls.arg_chk(on_fail)
  547. from mmgen.globalvars import g
  548. try:
  549. ss = str(s).split(':')
  550. assert len(ss) in (2,3),'not 2 or 3 colon-separated items'
  551. t = MMGenAddrType((ss[1],g.proto.dfl_mmtype)[len(ss)==2],on_fail='raise')
  552. me = str.__new__(cls,'{}:{}:{}'.format(ss[0],t,ss[-1]))
  553. me.sid = SeedID(sid=ss[0],on_fail='raise')
  554. me.idx = AddrIdx(ss[-1],on_fail='raise')
  555. me.mmtype = t
  556. assert t in g.proto.mmtypes,'{}: invalid address type for {}'.format(t,g.proto.__name__)
  557. me.al_id = str.__new__(AddrListID,me.sid+':'+me.mmtype) # checks already done
  558. me.sort_key = '{}:{}:{:0{w}}'.format(me.sid,me.mmtype,me.idx,w=me.idx.max_digits)
  559. return me
  560. except Exception as e:
  561. return cls.init_fail(e,s)
  562. class TwMMGenID(str,Hilite,InitErrors,MMGenObject):
  563. color = 'orange'
  564. width = 0
  565. trunc_ok = False
  566. def __new__(cls,s,on_fail='die'):
  567. if type(s) == cls: return s
  568. cls.arg_chk(on_fail)
  569. ret = None
  570. try:
  571. ret = MMGenID(s,on_fail='raise')
  572. sort_key,idtype = ret.sort_key,'mmgen'
  573. except Exception as e:
  574. try:
  575. from mmgen.globalvars import g
  576. assert s.split(':',1)[0] == g.proto.base_coin.lower(),(
  577. "not a string beginning with the prefix '{}:'".format(g.proto.base_coin.lower()))
  578. assert set(s[4:]) <= set(ascii_letters+digits),'contains non-alphanumeric characters'
  579. assert len(s) > 4,'not more that four characters long'
  580. ret,sort_key,idtype = str(s),'z_'+s,'non-mmgen'
  581. except Exception as e2:
  582. return cls.init_fail(e,s,e2=e2)
  583. me = str.__new__(cls,ret)
  584. me.obj = ret
  585. me.sort_key = sort_key
  586. me.type = idtype
  587. return me
  588. # contains TwMMGenID,TwComment. Not for display
  589. class TwLabel(str,InitErrors,MMGenObject):
  590. def __new__(cls,s,on_fail='die'):
  591. if type(s) == cls: return s
  592. cls.arg_chk(on_fail)
  593. try:
  594. ss = s.split(None,1)
  595. mmid = TwMMGenID(ss[0],on_fail='raise')
  596. comment = TwComment(ss[1] if len(ss) == 2 else '',on_fail='raise')
  597. me = str.__new__(cls,'{}{}'.format(mmid,' {}'.format(comment) if comment else ''))
  598. me.mmid = mmid
  599. me.comment = comment
  600. return me
  601. except Exception as e:
  602. return cls.init_fail(e,s)
  603. class HexStr(str,Hilite,InitErrors):
  604. color = 'red'
  605. width = None
  606. hexcase = 'lower'
  607. trunc_ok = False
  608. dtype = str
  609. def __new__(cls,s,on_fail='die',case=None):
  610. if type(s) == cls: return s
  611. cls.arg_chk(on_fail)
  612. if case == None: case = cls.hexcase
  613. try:
  614. assert issubclass(type(s),str),'not a string or string subclass'
  615. assert case in ('upper','lower'),"'{}' incorrect case specifier".format(case)
  616. assert set(s) <= set(getattr(hexdigits,case)()),'not {}case hexadecimal symbols'.format(case)
  617. assert not len(s) % 2,'odd-length string'
  618. if cls.width:
  619. assert len(s) == cls.width,'Value is not {} characters wide'.format(cls.width)
  620. return cls.dtype.__new__(cls,s)
  621. except Exception as e:
  622. return cls.init_fail(e,s)
  623. class CoinTxID(HexStr): color,width,hexcase = 'purple',64,'lower'
  624. class WalletPassword(HexStr): color,width,hexcase = 'blue',32,'lower'
  625. class MoneroViewKey(HexStr): color,width,hexcase = 'cyan',64,'lower'
  626. class MMGenTxID(HexStr): color,width,hexcase = 'red',6,'upper'
  627. class WifKey(str,Hilite,InitErrors):
  628. width = 53
  629. color = 'blue'
  630. def __new__(cls,s,on_fail='die'):
  631. if type(s) == cls: return s
  632. cls.arg_chk(on_fail)
  633. try:
  634. assert set(s) <= set(ascii_letters+digits),'not an ascii alphanumeric string'
  635. from mmgen.globalvars import g
  636. g.proto.wif2hex(s) # raises exception on error
  637. return str.__new__(cls,s)
  638. except Exception as e:
  639. return cls.init_fail(e,s)
  640. class PubKey(HexStr,MMGenObject): # TODO: add some real checks
  641. def __new__(cls,s,compressed,on_fail='die'):
  642. try:
  643. assert type(compressed) == bool,"'compressed' must be of type bool"
  644. except Exception as e:
  645. return cls.init_fail(e,s)
  646. me = HexStr.__new__(cls,s,case='lower',on_fail=on_fail)
  647. if me:
  648. me.compressed = compressed
  649. return me
  650. class PrivKey(str,Hilite,InitErrors,MMGenObject):
  651. color = 'red'
  652. width = 64
  653. trunc_ok = False
  654. compressed = MMGenImmutableAttr('compressed',bool,typeconv=False)
  655. wif = MMGenImmutableAttr('wif',WifKey,typeconv=False)
  656. # initialize with (priv_bin,compressed), WIF or self
  657. def __new__(cls,s=None,compressed=None,wif=None,pubkey_type=None,on_fail='die'):
  658. from mmgen.globalvars import g
  659. if type(s) == cls: return s
  660. cls.arg_chk(on_fail)
  661. if wif:
  662. try:
  663. assert s == None,"'wif' and key hex args are mutually exclusive"
  664. assert set(wif) <= set(ascii_letters+digits),'not an ascii alphanumeric string'
  665. w2h = g.proto.wif2hex(wif) # raises exception on error
  666. me = str.__new__(cls,w2h['hex'])
  667. me.compressed = w2h['compressed']
  668. me.pubkey_type = w2h['pubkey_type']
  669. me.wif = str.__new__(WifKey,wif) # check has been done
  670. me.orig_hex = None
  671. return me
  672. except Exception as e:
  673. return cls.init_fail(e,s,objname='{} WIF key'.format(g.coin))
  674. else:
  675. try:
  676. assert s,'private key hex data missing'
  677. assert compressed is not None, "'compressed' arg missing"
  678. assert pubkey_type is not None,"'pubkey_type' arg missing"
  679. assert type(compressed) == bool,"{!r}: 'compressed' not of type 'bool'".format(compressed)
  680. assert len(s) == cls.width // 2,'key length must be {}'.format(cls.width // 2)
  681. if pubkey_type == 'password': # skip WIF creation and pre-processing for passwds
  682. me = str.__new__(cls,s.hex())
  683. else:
  684. me = str.__new__(cls,g.proto.preprocess_key(s.hex(),pubkey_type))
  685. me.wif = WifKey(g.proto.hex2wif(me,pubkey_type,compressed),on_fail='raise')
  686. me.compressed = compressed
  687. me.pubkey_type = pubkey_type
  688. me.orig_hex = s.hex() # save the non-preprocessed key
  689. return me
  690. except Exception as e:
  691. return cls.init_fail(e,s)
  692. class AddrListID(str,Hilite,InitErrors,MMGenObject):
  693. width = 10
  694. trunc_ok = False
  695. color = 'yellow'
  696. def __new__(cls,sid,mmtype,on_fail='die'):
  697. cls.arg_chk(on_fail)
  698. try:
  699. assert type(sid) == SeedID,"{!r} not a SeedID instance".format(sid)
  700. t = MMGenAddrType,MMGenPasswordType
  701. assert type(mmtype) in t,"{!r} not an instance of {}".format(mmtype,','.join([i.__name__ for i in t]))
  702. me = str.__new__(cls,sid+':'+mmtype)
  703. me.sid = sid
  704. me.mmtype = mmtype
  705. return me
  706. except Exception as e:
  707. return cls.init_fail(e,'sid={}, mmtype={}'.format(sid,mmtype))
  708. class MMGenLabel(str,Hilite,InitErrors):
  709. color = 'pink'
  710. allowed = []
  711. forbidden = []
  712. max_len = 0
  713. min_len = 0
  714. max_screen_width = 0 # if != 0, overrides max_len
  715. desc = 'label'
  716. def __new__(cls,s,on_fail='die',msg=None):
  717. if type(s) == cls: return s
  718. cls.arg_chk(on_fail)
  719. for k in cls.forbidden,cls.allowed:
  720. assert type(k) == list
  721. for ch in k: assert type(ch) == str and len(ch) == 1
  722. try:
  723. s = s.strip()
  724. if type(s) != str:
  725. s = s.decode('utf8')
  726. for ch in s:
  727. # Allow: (L)etter,(N)umber,(P)unctuation,(S)ymbol,(Z)space
  728. # Disallow: (C)ontrol,(M)combining
  729. # Combining characters create width formatting issues, so disallow them for now
  730. if unicodedata.category(ch)[0] in 'CM':
  731. t = { 'C':'control', 'M':'combining' }[unicodedata.category(ch)[0]]
  732. raise ValueError('{}: {} characters not allowed'.format(ascii(ch),t))
  733. me = str.__new__(cls,s)
  734. if cls.max_screen_width:
  735. me.screen_width = len(s) + len([1 for ch in s if unicodedata.east_asian_width(ch) in ('F','W')])
  736. assert me.screen_width <= cls.max_screen_width,(
  737. 'too wide (>{} screen width)'.format(cls.max_screen_width))
  738. else:
  739. assert len(s) <= cls.max_len, 'too long (>{} symbols)'.format(cls.max_len)
  740. assert len(s) >= cls.min_len, 'too short (<{} symbols)'.format(cls.min_len)
  741. assert not cls.allowed or set(list(s)).issubset(set(cls.allowed)),\
  742. 'contains non-allowed symbols: {}'.format(' '.join(set(list(s)) - set(cls.allowed)))
  743. assert not cls.forbidden or not any(ch in s for ch in cls.forbidden),\
  744. "contains one of these forbidden symbols: '{}'".format("', '".join(cls.forbidden))
  745. return me
  746. except Exception as e:
  747. return cls.init_fail(e,s)
  748. class MMGenWalletLabel(MMGenLabel):
  749. max_len = 48
  750. desc = 'wallet label'
  751. class TwComment(MMGenLabel):
  752. max_screen_width = 80
  753. desc = 'tracking wallet comment'
  754. class MMGenTXLabel(MMGenLabel):
  755. max_len = 72
  756. desc = 'transaction label'
  757. class MMGenPWIDString(MMGenLabel):
  758. max_len = 256
  759. min_len = 1
  760. desc = 'password ID string'
  761. forbidden = list(' :/\\')
  762. class SeedShareIDString(MMGenPWIDString):
  763. desc = 'seed share ID string'
  764. class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
  765. width = 1
  766. trunc_ok = False
  767. color = 'blue'
  768. mmtypes = { # 'name' is used to cook the seed, so it must never change!
  769. 'L': { 'name':'legacy',
  770. 'pubkey_type':'std',
  771. 'compressed':False,
  772. 'gen_method':'p2pkh',
  773. 'addr_fmt':'p2pkh',
  774. 'desc':'Legacy uncompressed address'},
  775. 'C': { 'name':'compressed',
  776. 'pubkey_type':'std',
  777. 'compressed':True,
  778. 'gen_method':'p2pkh',
  779. 'addr_fmt':'p2pkh',
  780. 'desc':'Compressed P2PKH address'},
  781. 'S': { 'name':'segwit',
  782. 'pubkey_type':'std',
  783. 'compressed':True,
  784. 'gen_method':'segwit',
  785. 'addr_fmt':'p2sh',
  786. 'desc':'Segwit P2SH-P2WPKH address' },
  787. 'B': { 'name':'bech32',
  788. 'pubkey_type':'std',
  789. 'compressed':True,
  790. 'gen_method':'bech32',
  791. 'addr_fmt':'bech32',
  792. 'desc':'Native Segwit (Bech32) address' },
  793. 'E': { 'name':'ethereum',
  794. 'pubkey_type':'std',
  795. 'compressed':False,
  796. 'gen_method':'ethereum',
  797. 'addr_fmt':'ethereum',
  798. 'wif_label':'privkey:',
  799. 'extra_attrs': ('wallet_passwd',),
  800. 'desc':'Ethereum address' },
  801. 'Z': { 'name':'zcash_z',
  802. 'pubkey_type':'zcash_z',
  803. 'compressed':False,
  804. 'gen_method':'zcash_z',
  805. 'addr_fmt':'zcash_z',
  806. 'extra_attrs': ('viewkey',),
  807. 'desc':'Zcash z-address' },
  808. 'M': { 'name':'monero',
  809. 'pubkey_type':'monero',
  810. 'compressed':False,
  811. 'gen_method':'monero',
  812. 'addr_fmt':'monero',
  813. 'wif_label':'spendkey:',
  814. 'extra_attrs': ('viewkey','wallet_passwd'),
  815. 'desc':'Monero address'}
  816. }
  817. def __new__(cls,s,on_fail='die',errmsg=None):
  818. if type(s) == cls: return s
  819. cls.arg_chk(on_fail)
  820. from mmgen.globalvars import g
  821. try:
  822. for k,v in list(cls.mmtypes.items()):
  823. if s in (k,v['name']):
  824. if s == v['name']: s = k
  825. me = str.__new__(cls,s)
  826. for k in ('name','pubkey_type','compressed','gen_method','addr_fmt','desc'):
  827. setattr(me,k,v[k])
  828. assert me in g.proto.mmtypes + ('P',), (
  829. "'{}': invalid address type for {}".format(me.name,g.proto.__name__))
  830. me.extra_attrs = v['extra_attrs'] if 'extra_attrs' in v else ()
  831. me.wif_label = v['wif_label'] if 'wif_label' in v else 'wif:'
  832. return me
  833. raise ValueError('not found')
  834. except Exception as e:
  835. m = '{}{!r}: invalid value for {} ({})'.format(
  836. ('{!r}\n'.format(errmsg) if errmsg else ''),s,cls.__name__,e.args[0])
  837. return cls.init_fail(e,m,preformat=True)
  838. @classmethod
  839. def get_names(cls):
  840. return [v['name'] for v in cls.mmtypes.values()]
  841. class MMGenPasswordType(MMGenAddrType):
  842. mmtypes = {
  843. 'P': { 'name':'password',
  844. 'pubkey_type':'password',
  845. 'compressed':False,
  846. 'gen_method':None,
  847. 'addr_fmt':None,
  848. 'desc':'Password generated from MMGen seed'}
  849. }