obj.py 30 KB

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