obj.py 30 KB

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