obj.py 30 KB

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