obj.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2024 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: MMGen native classes
  20. """
  21. import unicodedata
  22. from .objmethods import MMGenObject,Hilite,HiliteStr,InitErrors
  23. def get_obj(objname,*args,**kwargs):
  24. """
  25. Wrapper for data objects
  26. - If the object throws an exception on instantiation, return False, otherwise return the object.
  27. - If silent is True, suppress display of the exception.
  28. - If return_bool is True, return True instead of the object.
  29. Only keyword args are accepted.
  30. """
  31. assert not args, 'get_obj_chk1'
  32. silent,return_bool = (False,False)
  33. if 'silent' in kwargs:
  34. silent = kwargs['silent']
  35. del kwargs['silent']
  36. if 'return_bool' in kwargs:
  37. return_bool = kwargs['return_bool']
  38. del kwargs['return_bool']
  39. try:
  40. ret = objname(**kwargs)
  41. except Exception as e:
  42. if not silent:
  43. from .util import rmsg
  44. rmsg(f'{e!s}')
  45. return False
  46. else:
  47. return True if return_bool else ret
  48. # dict that keeps a list of keys for efficient lookup by index
  49. class IndexedDict(dict):
  50. def __init__(self,*args,**kwargs):
  51. if args or kwargs:
  52. self.die('initializing values via constructor')
  53. self.__keylist = []
  54. dict.__init__(self,*args,**kwargs)
  55. def __setitem__(self,key,value):
  56. if key in self:
  57. self.die('reassignment to existing key')
  58. self.__keylist.append(key)
  59. return dict.__setitem__(self,key,value)
  60. @property
  61. def keys(self):
  62. return self.__keylist
  63. def key(self,idx):
  64. return self.__keylist[idx]
  65. def __delitem__(self,*args):
  66. self.die('item deletion')
  67. def move_to_end(self,*args):
  68. self.die('item moving')
  69. def clear(self,*args):
  70. self.die('clearing')
  71. def update(self,*args):
  72. self.die('updating')
  73. def die(self,desc):
  74. raise NotImplementedError(f'{desc} not implemented for type {type(self).__name__}')
  75. class MMGenList(list,MMGenObject):
  76. pass
  77. class MMGenDict(dict,MMGenObject):
  78. pass
  79. class ImmutableAttr: # Descriptor
  80. """
  81. For attributes that are always present in the data instance
  82. Reassignment and deletion forbidden
  83. """
  84. ok_dtypes = (type,type(None),type(lambda:0))
  85. def __init__(self,dtype,typeconv=True,set_none_ok=False,include_proto=False):
  86. self.set_none_ok = set_none_ok
  87. self.typeconv = typeconv
  88. assert isinstance(dtype,self.ok_dtypes), 'ImmutableAttr_check1'
  89. if include_proto:
  90. assert typeconv, 'ImmutableAttr_check2'
  91. if set_none_ok:
  92. assert typeconv and not isinstance(dtype,str), 'ImmutableAttr_check3'
  93. if typeconv:
  94. # convert this attribute's type
  95. if set_none_ok:
  96. self.conv = lambda instance,value: None if value is None else dtype(value)
  97. elif include_proto:
  98. self.conv = lambda instance,value: dtype(instance.proto,value)
  99. else:
  100. self.conv = lambda instance,value: dtype(value)
  101. else:
  102. # check this attribute's type
  103. def assign_with_check(instance,value):
  104. if type(value) is dtype:
  105. return value
  106. raise TypeError('Attribute {!r} of {} instance must of type {}'.format(
  107. self.name,
  108. type(instance).__name__,
  109. dtype ))
  110. self.conv = assign_with_check
  111. def __set_name__(self,owner,name):
  112. self.name = name
  113. def __get__(self,instance,owner):
  114. return instance.__dict__[self.name]
  115. def setattr_condition(self,instance):
  116. 'forbid all reassignment'
  117. return not self.name in instance.__dict__
  118. def __set__(self,instance,value):
  119. if not self.setattr_condition(instance):
  120. raise AttributeError(f'Attribute {self.name!r} of {type(instance)} instance cannot be reassigned')
  121. instance.__dict__[self.name] = self.conv(instance,value)
  122. def __delete__(self,instance):
  123. raise AttributeError(
  124. f'Attribute {self.name!r} of {type(instance).__name__} instance cannot be deleted')
  125. class ListItemAttr(ImmutableAttr):
  126. """
  127. For attributes that might not be present in the data instance
  128. Reassignment or deletion allowed if specified
  129. """
  130. def __init__(self,dtype,typeconv=True,include_proto=False,reassign_ok=False,delete_ok=False):
  131. self.reassign_ok = reassign_ok
  132. self.delete_ok = delete_ok
  133. ImmutableAttr.__init__(self,dtype,typeconv=typeconv,include_proto=include_proto)
  134. def __get__(self,instance,owner):
  135. "return None if attribute doesn't exist"
  136. try:
  137. return instance.__dict__[self.name]
  138. except:
  139. return None
  140. def setattr_condition(self,instance):
  141. return getattr(instance,self.name) is None or self.reassign_ok
  142. def __delete__(self,instance):
  143. if self.delete_ok:
  144. if self.name in instance.__dict__:
  145. del instance.__dict__[self.name]
  146. else:
  147. ImmutableAttr.__delete__(self,instance)
  148. class MMGenListItem(MMGenObject):
  149. valid_attrs = set()
  150. invalid_attrs = {
  151. 'pfmt',
  152. 'pmsg',
  153. 'pdie',
  154. 'pexit',
  155. 'valid_attrs',
  156. 'invalid_attrs',
  157. 'immutable_attr_init_check',
  158. }
  159. def __init__(self,*args,**kwargs):
  160. # generate valid_attrs, or use the class valid_attrs if set
  161. self.__dict__['valid_attrs'] = self.valid_attrs or (
  162. {e for e in dir(self) if e[0] != '_'}
  163. - MMGenListItem.invalid_attrs
  164. - self.invalid_attrs
  165. )
  166. if args:
  167. raise ValueError(f'Non-keyword args not allowed in {type(self).__name__!r} constructor')
  168. for k,v in kwargs.items():
  169. if v is not None:
  170. setattr(self,k,v)
  171. # Require all immutables to be initialized. Check performed only when testing.
  172. self.immutable_attr_init_check()
  173. # allow only valid attributes to be set
  174. def __setattr__(self,name,value):
  175. if name not in self.valid_attrs:
  176. raise AttributeError(f'{name!r}: no such attribute in class {type(self)}')
  177. return object.__setattr__(self,name,value)
  178. def _asdict(self):
  179. return dict((k,v) for k,v in self.__dict__.items() if k in self.valid_attrs)
  180. class MMGenRange(tuple,InitErrors,MMGenObject):
  181. min_idx = None
  182. max_idx = None
  183. def __new__(cls,*args):
  184. try:
  185. if len(args) == 1:
  186. s = args[0]
  187. if isinstance(s,cls):
  188. return s
  189. assert isinstance(s,str),'not a string or string subclass'
  190. ss = s.split('-',1)
  191. first = int(ss[0])
  192. last = int(ss.pop())
  193. else:
  194. s = repr(args) # needed if exception occurs
  195. assert len(args) == 2,'one format string arg or two start,stop args required'
  196. first,last = args
  197. assert first <= last, 'start of range greater than end of range'
  198. if cls.min_idx is not None:
  199. assert first >= cls.min_idx, f'start of range < {cls.min_idx:,}'
  200. if cls.max_idx is not None:
  201. assert last <= cls.max_idx, f'end of range > {cls.max_idx:,}'
  202. return tuple.__new__(cls,(first,last))
  203. except Exception as e:
  204. return cls.init_fail(e,s)
  205. @property
  206. def first(self):
  207. return self[0]
  208. @property
  209. def last(self):
  210. return self[1]
  211. def iterate(self):
  212. return range(self[0],self[1]+1)
  213. @property
  214. def items(self):
  215. return list(self.iterate())
  216. class Int(int,Hilite,InitErrors):
  217. min_val = None
  218. max_val = None
  219. max_digits = None
  220. color = 'red'
  221. def __new__(cls,n,base=10):
  222. if isinstance(n,cls):
  223. return n
  224. try:
  225. me = int.__new__(cls,str(n),base)
  226. if cls.min_val is not None:
  227. assert me >= cls.min_val, f'is less than cls.min_val ({cls.min_val})'
  228. if cls.max_val is not None:
  229. assert me <= cls.max_val, f'is greater than cls.max_val ({cls.max_val})'
  230. if cls.max_digits is not None:
  231. assert len(str(me)) <= cls.max_digits, f'has more than {cls.max_digits} digits'
  232. return me
  233. except Exception as e:
  234. return cls.init_fail(e,n)
  235. @classmethod
  236. def fmtc(cls,s,width,color=False):
  237. return super().fmtc(str(s), width=width, color=color)
  238. def fmt(self,width,color=False):
  239. return super().fmtc(str(self), width=width, color=color)
  240. def hl(self,**kwargs):
  241. return super().colorize(str(self), **kwargs)
  242. class NonNegativeInt(Int):
  243. min_val = 0
  244. class MMGenIdx(Int):
  245. min_val = 1
  246. class ETHNonce(Int):
  247. min_val = 0
  248. class Str(HiliteStr):
  249. pass
  250. class HexStr(HiliteStr,InitErrors):
  251. color = 'red'
  252. width = None
  253. hexcase = 'lower'
  254. trunc_ok = False
  255. def __new__(cls,s,case=None):
  256. if isinstance(s,cls):
  257. return s
  258. if case is None:
  259. case = cls.hexcase
  260. from .util import hexdigits_lc,hexdigits_uc
  261. try:
  262. assert isinstance(s,str),'not a string or string subclass'
  263. assert case in ('upper','lower'), f'{case!r} incorrect case specifier'
  264. assert set(s) <= set(hexdigits_lc if case == 'lower' else hexdigits_uc), (
  265. f'not {case}case hexadecimal symbols' )
  266. assert not len(s) % 2,'odd-length string'
  267. if cls.width:
  268. assert len(s) == cls.width, f'Value is not {cls.width} characters wide'
  269. return str.__new__(cls,s)
  270. except Exception as e:
  271. return cls.init_fail(e,s)
  272. def truncate(self,width,color=True):
  273. return self.colorize(
  274. self if width >= self.width else self[:width-2] + '..',
  275. color = color )
  276. class CoinTxID(HexStr):
  277. color,width,hexcase = ('purple',64,'lower')
  278. class WalletPassword(HexStr):
  279. color,width,hexcase = ('blue',32,'lower')
  280. class MMGenTxID(HexStr):
  281. color,width,hexcase = ('red',6,'upper')
  282. class MMGenLabel(HiliteStr,InitErrors):
  283. color = 'pink'
  284. allowed = []
  285. forbidden = []
  286. first_char = []
  287. max_len = 0
  288. min_len = 0
  289. max_screen_width = 0 # if != 0, overrides max_len
  290. desc = 'label'
  291. def __new__(cls,s):
  292. if isinstance(s,cls):
  293. return s
  294. try:
  295. s = s.strip()
  296. if not cls.allowed:
  297. for ch in s:
  298. # Allow: (L)etter,(N)umber,(P)unctuation,(S)ymbol,(Z)space
  299. # Disallow: (C)ontrol,(M)combining
  300. # Combining characters create width formatting issues, so disallow them for now
  301. if unicodedata.category(ch)[0] in ('C','M'):
  302. raise ValueError(
  303. '{!a}: {} characters not allowed'.format(
  304. ch,
  305. { 'C':'control', 'M':'combining' }[unicodedata.category(ch)[0]] ))
  306. me = str.__new__(cls,s)
  307. if cls.max_screen_width:
  308. me.screen_width = len(s) + len([1 for ch in s if unicodedata.east_asian_width(ch) in ('F','W')])
  309. assert me.screen_width <= cls.max_screen_width, f'too wide (>{cls.max_screen_width} screen width)'
  310. else:
  311. assert len(s) <= cls.max_len, f'too long (>{cls.max_len} symbols)'
  312. assert len(s) >= cls.min_len, f'too short (<{cls.min_len} symbols)'
  313. if cls.first_char and s and not s[0] in cls.first_char:
  314. raise ValueError('first character not in set ' + ' '.join(cls.first_char))
  315. if cls.allowed and not set(list(s)).issubset(set(cls.allowed)):
  316. raise ValueError('contains symbols not in set: ' + ' '.join(cls.allowed))
  317. if cls.forbidden and any(ch in s for ch in cls.forbidden):
  318. raise ValueError('contains one of these forbidden symbols: ' + ' '.join(cls.forbidden))
  319. return me
  320. except Exception as e:
  321. return cls.init_fail(e,s)
  322. class MMGenWalletLabel(MMGenLabel):
  323. max_len = 48
  324. desc = 'wallet label'
  325. class TrackingWalletName(MMGenLabel):
  326. max_len = 40
  327. desc = 'tracking wallet name'
  328. allowed = 'abcdefghijklmnopqrstuvwxyz0123456789-_'
  329. first_char = 'abcdefghijklmnopqrstuvwxyz0123456789'
  330. class TwComment(MMGenLabel):
  331. max_screen_width = 80
  332. desc = 'tracking wallet comment'
  333. exc = 'BadTwComment'
  334. class MMGenTxComment(MMGenLabel):
  335. max_len = 72
  336. desc = 'transaction label'
  337. class MMGenPWIDString(MMGenLabel):
  338. max_len = 256
  339. min_len = 1
  340. desc = 'password ID string'
  341. forbidden = list(' :/\\')
  342. trunc_ok = False