obj.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2022 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,re,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. from .objmethods import *
  28. def get_obj(objname,*args,**kwargs):
  29. """
  30. Wrapper for data objects
  31. - If the object throws an exception on instantiation, return False, otherwise return the object.
  32. - If silent is True, suppress display of the exception.
  33. - If return_bool is True, return True instead of the object.
  34. Only keyword args are accepted.
  35. """
  36. assert args == (), 'get_obj_chk1'
  37. silent,return_bool = (False,False)
  38. if 'silent' in kwargs:
  39. silent = kwargs['silent']
  40. del kwargs['silent']
  41. if 'return_bool' in kwargs:
  42. return_bool = kwargs['return_bool']
  43. del kwargs['return_bool']
  44. try:
  45. ret = objname(**kwargs)
  46. except Exception as e:
  47. if not silent:
  48. from .util import msg
  49. msg(f'{e!s}')
  50. return False
  51. else:
  52. return True if return_bool else ret
  53. def is_addr_idx(s): return get_obj(AddrIdx, n=s, silent=True,return_bool=True)
  54. def is_addrlist_id(s): return get_obj(AddrListID, sid=s, silent=True,return_bool=True)
  55. def is_mmgen_id(proto,s): return get_obj(MMGenID, proto=proto, id_str=s, silent=True,return_bool=True)
  56. def is_coin_addr(proto,s): return get_obj(CoinAddr, proto=proto, addr=s, silent=True,return_bool=True)
  57. def is_wif(proto,s): return get_obj(WifKey, proto=proto, wif=s, silent=True,return_bool=True)
  58. # dict that keeps a list of keys for efficient lookup by index
  59. class IndexedDict(dict):
  60. def __init__(self,*args,**kwargs):
  61. if args or kwargs:
  62. self.die('initializing values via constructor')
  63. self.__keylist = []
  64. return dict.__init__(self,*args,**kwargs)
  65. def __setitem__(self,key,value):
  66. if key in self:
  67. self.die('reassignment to existing key')
  68. self.__keylist.append(key)
  69. return dict.__setitem__(self,key,value)
  70. @property
  71. def keys(self):
  72. return self.__keylist
  73. def key(self,idx):
  74. return self.__keylist[idx]
  75. def __delitem__(self,*args): self.die('item deletion')
  76. def move_to_end(self,*args): self.die('item moving')
  77. def clear(self,*args): self.die('clearing')
  78. def update(self,*args): self.die('updating')
  79. def die(self,desc):
  80. raise NotImplementedError(f'{desc} not implemented for type {type(self).__name__}')
  81. class MMGenList(list,MMGenObject): pass
  82. class MMGenDict(dict,MMGenObject): pass
  83. class AddrListData(list,MMGenObject): pass
  84. class Str(str,Hilite): pass
  85. class Int(int,Hilite,InitErrors):
  86. min_val = None
  87. max_val = None
  88. max_digits = None
  89. color = 'red'
  90. def __new__(cls,n,base=10):
  91. if type(n) == cls:
  92. return n
  93. try:
  94. me = int.__new__(cls,str(n),base)
  95. if cls.min_val != None:
  96. assert me >= cls.min_val, f'is less than cls.min_val ({cls.min_val})'
  97. if cls.max_val != None:
  98. assert me <= cls.max_val, f'is greater than cls.max_val ({cls.max_val})'
  99. if cls.max_digits != None:
  100. assert len(str(me)) <= cls.max_digits, f'has more than {cls.max_digits} digits'
  101. return me
  102. except Exception as e:
  103. return cls.init_fail(e,n)
  104. @classmethod
  105. def fmtc(cls,*args,**kwargs):
  106. cls.method_not_implemented()
  107. @classmethod
  108. def colorize(cls,n,color=True):
  109. return super().colorize(repr(n),color=color)
  110. class ImmutableAttr: # Descriptor
  111. """
  112. For attributes that are always present in the data instance
  113. Reassignment and deletion forbidden
  114. """
  115. ok_dtypes = (str,type,type(None),type(lambda:0))
  116. def __init__(self,dtype,typeconv=True,set_none_ok=False,include_proto=False):
  117. assert isinstance(dtype,self.ok_dtypes), 'ImmutableAttr_check1'
  118. if include_proto:
  119. assert typeconv and type(dtype) == str, 'ImmutableAttr_check2'
  120. if set_none_ok:
  121. assert typeconv and type(dtype) != str, 'ImmutableAttr_check3'
  122. if dtype is None:
  123. 'use instance-defined conversion function for this attribute'
  124. self.conv = lambda instance,value: getattr(instance.conv_funcs,self.name)(instance,value)
  125. elif typeconv:
  126. "convert this attribute's type"
  127. if type(dtype) == str:
  128. if include_proto:
  129. self.conv = lambda instance,value: globals()[dtype](instance.proto,value)
  130. else:
  131. self.conv = lambda instance,value: globals()[dtype](value)
  132. else:
  133. if set_none_ok:
  134. self.conv = lambda instance,value: None if value is None else dtype(value)
  135. else:
  136. self.conv = lambda instance,value: dtype(value)
  137. else:
  138. "check this attribute's type"
  139. def assign_with_check(instance,value):
  140. if type(value) == dtype:
  141. return value
  142. raise TypeError('Attribute {!r} of {} instance must of type {}'.format(
  143. self.name,
  144. type(instance).__name__,
  145. dtype ))
  146. self.conv = assign_with_check
  147. def __set_name__(self,owner,name):
  148. self.name = name
  149. def __get__(self,instance,owner):
  150. return instance.__dict__[self.name]
  151. def setattr_condition(self,instance):
  152. 'forbid all reassignment'
  153. return not self.name in instance.__dict__
  154. def __set__(self,instance,value):
  155. if not self.setattr_condition(instance):
  156. raise AttributeError(f'Attribute {self.name!r} of {type(instance)} instance cannot be reassigned')
  157. instance.__dict__[self.name] = self.conv(instance,value)
  158. def __delete__(self,instance):
  159. raise AttributeError(
  160. f'Attribute {self.name!r} of {type(instance).__name__} instance cannot be deleted')
  161. class ListItemAttr(ImmutableAttr):
  162. """
  163. For attributes that might not be present in the data instance
  164. Reassignment or deletion allowed if specified
  165. """
  166. def __init__(self,dtype,typeconv=True,include_proto=False,reassign_ok=False,delete_ok=False):
  167. self.reassign_ok = reassign_ok
  168. self.delete_ok = delete_ok
  169. ImmutableAttr.__init__(self,dtype,typeconv=typeconv,include_proto=include_proto)
  170. def __get__(self,instance,owner):
  171. "return None if attribute doesn't exist"
  172. try: return instance.__dict__[self.name]
  173. except: return None
  174. def setattr_condition(self,instance):
  175. return getattr(instance,self.name) == None or self.reassign_ok
  176. def __delete__(self,instance):
  177. if self.delete_ok:
  178. if self.name in instance.__dict__:
  179. del instance.__dict__[self.name]
  180. else:
  181. ImmutableAttr.__delete__(self,instance)
  182. class MMGenListItem(MMGenObject):
  183. valid_attrs = set()
  184. valid_attrs_extra = set()
  185. invalid_attrs = {
  186. 'pfmt',
  187. 'pmsg',
  188. 'pdie',
  189. 'valid_attrs',
  190. 'valid_attrs_extra',
  191. 'invalid_attrs',
  192. 'immutable_attr_init_check',
  193. 'conv_funcs',
  194. '_asdict',
  195. }
  196. def __init__(self,*args,**kwargs):
  197. # generate valid_attrs, or use the class valid_attrs if set
  198. self.__dict__['valid_attrs'] = self.valid_attrs or (
  199. ( {e for e in dir(self) if e[:2] != '__'} | self.valid_attrs_extra )
  200. - MMGenListItem.invalid_attrs
  201. - self.invalid_attrs
  202. )
  203. if args:
  204. raise ValueError(f'Non-keyword args not allowed in {type(self).__name__!r} constructor')
  205. for k,v in kwargs.items():
  206. if v != None:
  207. setattr(self,k,v)
  208. # Require all immutables to be initialized. Check performed only when testing.
  209. self.immutable_attr_init_check()
  210. # allow only valid attributes to be set
  211. def __setattr__(self,name,value):
  212. if name not in self.valid_attrs:
  213. raise AttributeError(f'{name!r}: no such attribute in class {type(self)}')
  214. return object.__setattr__(self,name,value)
  215. def _asdict(self):
  216. return dict((k,v) for k,v in self.__dict__.items() if k in self.valid_attrs)
  217. class MMGenIdx(Int): min_val = 1
  218. class AddrIdx(MMGenIdx): max_digits = 7
  219. class AddrIdxList(list,InitErrors,MMGenObject):
  220. max_len = 1000000
  221. def __init__(self,fmt_str=None,idx_list=None,sep=','):
  222. try:
  223. if idx_list:
  224. return list.__init__(self,sorted({AddrIdx(i) for i in idx_list}))
  225. elif fmt_str:
  226. ret = []
  227. for i in (fmt_str.split(sep)):
  228. j = i.split('-')
  229. if len(j) == 1:
  230. idx = AddrIdx(i)
  231. if not idx:
  232. break
  233. ret.append(idx)
  234. elif len(j) == 2:
  235. beg = AddrIdx(j[0])
  236. if not beg:
  237. break
  238. end = AddrIdx(j[1])
  239. if not beg or (end < beg):
  240. break
  241. ret.extend([AddrIdx(x) for x in range(beg,end+1)])
  242. else: break
  243. else:
  244. return list.__init__(self,sorted(set(ret))) # fell off end of loop - success
  245. raise ValueError(f'{i!r}: invalid range')
  246. except Exception as e:
  247. return type(self).init_fail(e,idx_list or fmt_str)
  248. class MMGenRange(tuple,InitErrors,MMGenObject):
  249. min_idx = None
  250. max_idx = None
  251. def __new__(cls,*args):
  252. try:
  253. if len(args) == 1:
  254. s = args[0]
  255. if type(s) == cls:
  256. return s
  257. assert isinstance(s,str),'not a string or string subclass'
  258. ss = s.split('-',1)
  259. first = int(ss[0])
  260. last = int(ss.pop())
  261. else:
  262. s = repr(args) # needed if exception occurs
  263. assert len(args) == 2,'one format string arg or two start,stop args required'
  264. first,last = args
  265. assert first <= last, 'start of range greater than end of range'
  266. if cls.min_idx is not None:
  267. assert first >= cls.min_idx, f'start of range < {cls.min_idx:,}'
  268. if cls.max_idx is not None:
  269. assert last <= cls.max_idx, f'end of range > {cls.max_idx:,}'
  270. return tuple.__new__(cls,(first,last))
  271. except Exception as e:
  272. return cls.init_fail(e,s)
  273. @property
  274. def first(self):
  275. return self[0]
  276. @property
  277. def last(self):
  278. return self[1]
  279. def iterate(self):
  280. return range(self[0],self[1]+1)
  281. @property
  282. def items(self):
  283. return list(self.iterate())
  284. class CoinAddr(str,Hilite,InitErrors,MMGenObject):
  285. color = 'cyan'
  286. hex_width = 40
  287. width = 1
  288. trunc_ok = False
  289. def __new__(cls,proto,addr):
  290. if type(addr) == cls:
  291. return addr
  292. try:
  293. assert set(addr) <= set(ascii_letters+digits),'contains non-alphanumeric characters'
  294. me = str.__new__(cls,addr)
  295. ap = proto.parse_addr(addr)
  296. assert ap, f'coin address {addr!r} could not be parsed'
  297. me.addr_fmt = ap.fmt
  298. me.hex = ap.bytes.hex()
  299. me.proto = proto
  300. return me
  301. except Exception as e:
  302. return cls.init_fail(e,addr,objname=f'{proto.cls_name} address')
  303. @classmethod
  304. def fmtc(cls,addr,**kwargs):
  305. w = kwargs['width'] or cls.width
  306. return super().fmtc(addr[:w-2]+'..' if w < len(addr) else addr, **kwargs)
  307. class TokenAddr(CoinAddr):
  308. color = 'blue'
  309. class ViewKey(object):
  310. def __new__(cls,proto,viewkey):
  311. if proto.name == 'Zcash':
  312. return ZcashViewKey.__new__(ZcashViewKey,proto,viewkey)
  313. elif proto.name == 'Monero':
  314. return MoneroViewKey.__new__(MoneroViewKey,viewkey)
  315. else:
  316. raise ValueError(f'{proto.name}: protocol does not support view keys')
  317. class ZcashViewKey(CoinAddr): hex_width = 128
  318. class MMGenID(str,Hilite,InitErrors,MMGenObject):
  319. color = 'orange'
  320. width = 0
  321. trunc_ok = False
  322. def __new__(cls,proto,id_str):
  323. from .seed import SeedID
  324. try:
  325. ss = str(id_str).split(':')
  326. assert len(ss) in (2,3),'not 2 or 3 colon-separated items'
  327. t = proto.addr_type((ss[1],proto.dfl_mmtype)[len(ss)==2])
  328. me = str.__new__(cls,'{}:{}:{}'.format(ss[0],t,ss[-1]))
  329. me.sid = SeedID(sid=ss[0])
  330. me.idx = AddrIdx(ss[-1])
  331. me.mmtype = t
  332. assert t in proto.mmtypes, f'{t}: invalid address type for {proto.cls_name}'
  333. me.al_id = str.__new__(AddrListID,me.sid+':'+me.mmtype) # checks already done
  334. me.sort_key = '{}:{}:{:0{w}}'.format(me.sid,me.mmtype,me.idx,w=me.idx.max_digits)
  335. me.proto = proto
  336. return me
  337. except Exception as e:
  338. return cls.init_fail(e,id_str)
  339. class TwMMGenID(str,Hilite,InitErrors,MMGenObject):
  340. color = 'orange'
  341. width = 0
  342. trunc_ok = False
  343. def __new__(cls,proto,id_str):
  344. if type(id_str) == cls:
  345. return id_str
  346. ret = None
  347. try:
  348. ret = MMGenID(proto,id_str)
  349. sort_key,idtype = ret.sort_key,'mmgen'
  350. except Exception as e:
  351. try:
  352. assert id_str.split(':',1)[0] == proto.base_coin.lower(),(
  353. f'not a string beginning with the prefix {proto.base_coin.lower()!r}:' )
  354. assert set(id_str[4:]) <= set(ascii_letters+digits),'contains non-alphanumeric characters'
  355. assert len(id_str) > 4,'not more that four characters long'
  356. ret,sort_key,idtype = str(id_str),'z_'+id_str,'non-mmgen'
  357. except Exception as e2:
  358. return cls.init_fail(e,id_str,e2=e2)
  359. me = str.__new__(cls,ret)
  360. me.obj = ret
  361. me.sort_key = sort_key
  362. me.type = idtype
  363. me.proto = proto
  364. return me
  365. # non-displaying container for TwMMGenID,TwComment
  366. class TwLabel(str,InitErrors,MMGenObject):
  367. exc = BadTwLabel
  368. passthru_excs = (BadTwComment,)
  369. def __new__(cls,proto,text):
  370. if type(text) == cls:
  371. return text
  372. try:
  373. ts = text.split(None,1)
  374. mmid = TwMMGenID(proto,ts[0])
  375. comment = TwComment(ts[1] if len(ts) == 2 else '')
  376. me = str.__new__( cls, mmid + (' ' + comment if comment else '') )
  377. me.mmid = mmid
  378. me.comment = comment
  379. me.proto = proto
  380. return me
  381. except Exception as e:
  382. return cls.init_fail(e,text)
  383. class HexStr(str,Hilite,InitErrors):
  384. color = 'red'
  385. width = None
  386. hexcase = 'lower'
  387. trunc_ok = False
  388. def __new__(cls,s,case=None):
  389. if type(s) == cls:
  390. return s
  391. if case == None:
  392. case = cls.hexcase
  393. try:
  394. assert isinstance(s,str),'not a string or string subclass'
  395. assert case in ('upper','lower'), f'{case!r} incorrect case specifier'
  396. assert set(s) <= set(getattr(hexdigits,case)()), f'not {case}case hexadecimal symbols'
  397. assert not len(s) % 2,'odd-length string'
  398. if cls.width:
  399. assert len(s) == cls.width, f'Value is not {cls.width} characters wide'
  400. return str.__new__(cls,s)
  401. except Exception as e:
  402. return cls.init_fail(e,s)
  403. class CoinTxID(HexStr): color,width,hexcase = 'purple',64,'lower'
  404. class WalletPassword(HexStr): color,width,hexcase = 'blue',32,'lower'
  405. class MoneroViewKey(HexStr): color,width,hexcase = 'cyan',64,'lower' # FIXME - no checking performed
  406. class MMGenTxID(HexStr): color,width,hexcase = 'red',6,'upper'
  407. class WifKey(str,Hilite,InitErrors):
  408. """
  409. Initialize a WIF key, checking its well-formedness.
  410. The numeric validity of the private key it encodes is not checked.
  411. """
  412. width = 53
  413. color = 'blue'
  414. def __new__(cls,proto,wif):
  415. if type(wif) == cls:
  416. return wif
  417. try:
  418. assert set(wif) <= set(ascii_letters+digits),'not an ascii alphanumeric string'
  419. proto.parse_wif(wif) # raises exception on error
  420. return str.__new__(cls,wif)
  421. except Exception as e:
  422. return cls.init_fail(e,wif)
  423. class PubKey(HexStr,MMGenObject): # TODO: add some real checks
  424. def __new__(cls,s,privkey):
  425. try:
  426. me = HexStr.__new__(cls,s,case='lower')
  427. me.privkey = privkey
  428. me.compressed = privkey.compressed
  429. return me
  430. except Exception as e:
  431. return cls.init_fail(e,s)
  432. class PrivKey(str,Hilite,InitErrors,MMGenObject):
  433. """
  434. Input: a) raw, non-preprocessed bytes; or b) WIF key.
  435. Output: preprocessed hexadecimal key, plus WIF key in 'wif' attribute
  436. For coins without a WIF format, 'wif' contains the preprocessed hex.
  437. The numeric validity of the resulting key is always checked.
  438. """
  439. color = 'red'
  440. width = 64
  441. trunc_ok = False
  442. compressed = ImmutableAttr(bool,typeconv=False)
  443. wif = ImmutableAttr(WifKey,typeconv=False)
  444. # initialize with (priv_bin,compressed), WIF or self
  445. def __new__(cls,proto,s=None,compressed=None,wif=None,pubkey_type=None):
  446. if type(s) == cls:
  447. return s
  448. if wif:
  449. try:
  450. assert s == None,"'wif' and key hex args are mutually exclusive"
  451. assert set(wif) <= set(ascii_letters+digits),'not an ascii alphanumeric string'
  452. k = proto.parse_wif(wif) # raises exception on error
  453. me = str.__new__(cls,k.sec.hex())
  454. me.compressed = k.compressed
  455. me.pubkey_type = k.pubkey_type
  456. me.wif = str.__new__(WifKey,wif) # check has been done
  457. me.orig_hex = None
  458. if k.sec != proto.preprocess_key(k.sec,k.pubkey_type):
  459. raise PrivateKeyError(
  460. f'{proto.cls_name} WIF key {me.wif!r} encodes private key with invalid value {me}')
  461. me.proto = proto
  462. return me
  463. except Exception as e:
  464. return cls.init_fail(e,s,objname=f'{proto.coin} WIF key')
  465. else:
  466. try:
  467. assert s,'private key bytes data missing'
  468. assert pubkey_type is not None,"'pubkey_type' arg missing"
  469. assert len(s) == cls.width // 2, f'key length must be {cls.width // 2} bytes'
  470. if pubkey_type == 'password': # skip WIF creation and pre-processing for passwds
  471. me = str.__new__(cls,s.hex())
  472. else:
  473. assert compressed is not None, "'compressed' arg missing"
  474. assert type(compressed) == bool,(
  475. f"'compressed' must be of type bool, not {type(compressed).__name__}" )
  476. me = str.__new__(cls,proto.preprocess_key(s,pubkey_type).hex())
  477. me.wif = WifKey(proto,proto.hex2wif(me,pubkey_type,compressed))
  478. me.compressed = compressed
  479. me.pubkey_type = pubkey_type
  480. me.orig_hex = s.hex() # save the non-preprocessed key
  481. me.proto = proto
  482. return me
  483. except Exception as e:
  484. return cls.init_fail(e,s)
  485. class AddrListID(str,Hilite,InitErrors,MMGenObject):
  486. width = 10
  487. trunc_ok = False
  488. color = 'yellow'
  489. def __new__(cls,sid,mmtype):
  490. from .seed import SeedID
  491. try:
  492. assert type(sid) == SeedID, f'{sid!r} not a SeedID instance'
  493. if not isinstance(mmtype,(MMGenAddrType,MMGenPasswordType)):
  494. raise ValueError(f'{mmtype!r}: not an instance of MMGenAddrType or MMGenPasswordType')
  495. me = str.__new__(cls,sid+':'+mmtype)
  496. me.sid = sid
  497. me.mmtype = mmtype
  498. return me
  499. except Exception as e:
  500. return cls.init_fail(e, f'sid={sid}, mmtype={mmtype}')
  501. class MMGenLabel(str,Hilite,InitErrors):
  502. color = 'pink'
  503. allowed = []
  504. forbidden = []
  505. max_len = 0
  506. min_len = 0
  507. max_screen_width = 0 # if != 0, overrides max_len
  508. desc = 'label'
  509. def __new__(cls,s,msg=None):
  510. if type(s) == cls:
  511. return s
  512. for k in cls.forbidden,cls.allowed:
  513. assert type(k) == list
  514. for ch in k: assert type(ch) == str and len(ch) == 1
  515. try:
  516. s = s.strip()
  517. for ch in s:
  518. # Allow: (L)etter,(N)umber,(P)unctuation,(S)ymbol,(Z)space
  519. # Disallow: (C)ontrol,(M)combining
  520. # Combining characters create width formatting issues, so disallow them for now
  521. if unicodedata.category(ch)[0] in ('C','M'):
  522. raise ValueError('{!a}: {} characters not allowed'.format(ch,
  523. { 'C':'control', 'M':'combining' }[unicodedata.category(ch)[0]] ))
  524. me = str.__new__(cls,s)
  525. if cls.max_screen_width:
  526. me.screen_width = len(s) + len([1 for ch in s if unicodedata.east_asian_width(ch) in ('F','W')])
  527. assert me.screen_width <= cls.max_screen_width, f'too wide (>{cls.max_screen_width} screen width)'
  528. else:
  529. assert len(s) <= cls.max_len, f'too long (>{cls.max_len} symbols)'
  530. assert len(s) >= cls.min_len, f'too short (<{cls.min_len} symbols)'
  531. if cls.allowed and not set(list(s)).issubset(set(cls.allowed)):
  532. raise ValueError('contains non-allowed symbols: ' + ' '.join(set(list(s)) - set(cls.allowed)) )
  533. if cls.forbidden and any(ch in s for ch in cls.forbidden):
  534. raise ValueError('contains one of these forbidden symbols: ' + ' '.join(cls.forbidden) )
  535. return me
  536. except Exception as e:
  537. return cls.init_fail(e,s)
  538. class MMGenWalletLabel(MMGenLabel):
  539. max_len = 48
  540. desc = 'wallet label'
  541. class TwComment(MMGenLabel):
  542. max_screen_width = 80
  543. desc = 'tracking wallet comment'
  544. exc = BadTwComment
  545. class MMGenTxLabel(MMGenLabel):
  546. max_len = 72
  547. desc = 'transaction label'
  548. class MMGenPWIDString(MMGenLabel):
  549. max_len = 256
  550. min_len = 1
  551. desc = 'password ID string'
  552. forbidden = list(' :/\\')
  553. trunc_ok = False
  554. class IPPort(str,Hilite,InitErrors,MMGenObject):
  555. color = 'yellow'
  556. width = 0
  557. trunc_ok = False
  558. min_len = 9 # 0.0.0.0:0
  559. max_len = 21 # 255.255.255.255:65535
  560. def __new__(cls,s):
  561. if type(s) == cls:
  562. return s
  563. try:
  564. m = re.fullmatch(r'{q}\.{q}\.{q}\.{q}:(\d{{1,10}})'.format(q=r'([0-9]{1,3})'),s)
  565. assert m is not None, f'{s!r}: invalid IP:HOST specifier'
  566. for e in m.groups():
  567. if len(e) != 1 and e[0] == '0':
  568. raise ValueError(f'{e}: leading zeroes not permitted in dotted decimal element or port number')
  569. res = [int(e) for e in m.groups()]
  570. for e in res[:4]:
  571. assert e <= 255, f'{e}: dotted decimal element > 255'
  572. assert res[4] <= 65535, f'{res[4]}: port number > 65535'
  573. me = str.__new__(cls,s)
  574. me.ip = '{}.{}.{}.{}'.format(*res)
  575. me.ip_num = sum( res[i] * ( 2 ** (-(i-3)*8) ) for i in range(4) )
  576. me.port = res[4]
  577. return me
  578. except Exception as e:
  579. return cls.init_fail(e,s)
  580. from collections import namedtuple
  581. ati = namedtuple('addrtype_info',
  582. ['name','pubkey_type','compressed','gen_method','addr_fmt','wif_label','extra_attrs','desc'])
  583. class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
  584. width = 1
  585. trunc_ok = False
  586. color = 'blue'
  587. name = ImmutableAttr(str)
  588. pubkey_type = ImmutableAttr(str)
  589. compressed = ImmutableAttr(bool,set_none_ok=True)
  590. gen_method = ImmutableAttr(str,set_none_ok=True)
  591. addr_fmt = ImmutableAttr(str,set_none_ok=True)
  592. wif_label = ImmutableAttr(str,set_none_ok=True)
  593. extra_attrs = ImmutableAttr(tuple,set_none_ok=True)
  594. desc = ImmutableAttr(str)
  595. mmtypes = {
  596. 'L': ati('legacy', 'std', False,'p2pkh', 'p2pkh', 'wif', (), 'Legacy uncompressed address'),
  597. 'C': ati('compressed','std', True, 'p2pkh', 'p2pkh', 'wif', (), 'Compressed P2PKH address'),
  598. 'S': ati('segwit', 'std', True, 'segwit', 'p2sh', 'wif', (), 'Segwit P2SH-P2WPKH address'),
  599. 'B': ati('bech32', 'std', True, 'bech32', 'bech32', 'wif', (), 'Native Segwit (Bech32) address'),
  600. 'E': ati('ethereum', 'std', False,'ethereum','ethereum','privkey', ('wallet_passwd',),'Ethereum address'),
  601. 'Z': ati('zcash_z','zcash_z',False,'zcash_z', 'zcash_z', 'wif', ('viewkey',), 'Zcash z-address'),
  602. 'M': ati('monero', 'monero', False,'monero', 'monero', 'spendkey',('viewkey','wallet_passwd'),'Monero address'),
  603. }
  604. def __new__(cls,proto,id_str,errmsg=None):
  605. if isinstance(id_str,cls):
  606. return id_str
  607. try:
  608. for k,v in cls.mmtypes.items():
  609. if id_str in (k,v.name):
  610. if id_str == v.name:
  611. id_str = k
  612. me = str.__new__(cls,id_str)
  613. for k in v._fields:
  614. setattr(me,k,getattr(v,k))
  615. if me not in proto.mmtypes + ('P',):
  616. raise ValueError(f'{me.name!r}: invalid address type for {proto.name} protocol')
  617. me.proto = proto
  618. return me
  619. raise ValueError(f'{id_str}: unrecognized address type for protocol {proto.name}')
  620. except Exception as e:
  621. return cls.init_fail( e,
  622. f"{errmsg or ''}{id_str!r}: invalid value for {cls.__name__} ({e!s})",
  623. preformat = True )
  624. @classmethod
  625. def get_names(cls):
  626. return [v.name for v in cls.mmtypes.values()]
  627. class MMGenPasswordType(MMGenAddrType):
  628. mmtypes = {
  629. 'P': ati('password', 'password', None, None, None, None, None, 'Password generated from MMGen seed')
  630. }