obj.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2016 Philemon <mmgen-py@yandex.com>
  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: The MMGenObject class and methods
  20. """
  21. from decimal import *
  22. lvl = 0
  23. class MMGenObject(object):
  24. # Pretty-print any object of type MMGenObject, recursing into sub-objects
  25. def __str__(self):
  26. global lvl
  27. indent = lvl * ' '
  28. def fix_linebreaks(v,fixed_indent=None):
  29. if '\n' in v:
  30. i = indent+' ' if fixed_indent == None else fixed_indent*' '
  31. return '\n'+i + v.replace('\n','\n'+i)
  32. else: return repr(v)
  33. def conv(v,col_w):
  34. vret = ''
  35. if type(v) in (str,unicode):
  36. from string import printable
  37. if not (set(list(v)) <= set(list(printable))):
  38. vret = repr(v)
  39. else:
  40. vret = fix_linebreaks(v,fixed_indent=0)
  41. elif type(v) in (int,long,BTCAmt):
  42. vret = str(v)
  43. elif type(v) == dict:
  44. sep = '\n{}{}'.format(indent,' '*4)
  45. cw = (max(len(k) for k in v) if v else 0) + 2
  46. t = sep.join(['{:<{w}}: {}'.format(
  47. repr(k),
  48. (fix_linebreaks(v[k],fixed_indent=0) if type(v[k]) == str else v[k]),
  49. w=cw)
  50. for k in sorted(v)])
  51. vret = '{' + sep + t + '\n' + indent + '}'
  52. elif type(v) in (list,tuple):
  53. sep = '\n{}{}'.format(indent,' '*4)
  54. t = ' '.join([repr(e) for e in sorted(v)])
  55. o,c = (('(',')'),('[',']'))[type(v)==list]
  56. vret = o + sep + t + '\n' + indent + c
  57. elif repr(v)[:14] == '<bound method ':
  58. vret = ' '.join(repr(v).split()[0:3]) + '>'
  59. # vret = repr(v)
  60. return vret or type(v)
  61. out = []
  62. def f(k): return k[:2] != '__'
  63. keys = filter(f, self.__dict__.keys())
  64. col_w = max(len(k) for k in keys) if keys else 1
  65. fs = '{}%-{}s: %s'.format(indent,col_w)
  66. methods = [k for k in keys if repr(getattr(self,k))[:14] == '<bound method ']
  67. def f(k): return repr(getattr(self,k))[:14] == '<bound method '
  68. methods = filter(f,keys)
  69. def f(k): return repr(getattr(self,k))[:7] == '<mmgen.'
  70. objects = filter(f,keys)
  71. other = list(set(keys) - set(methods) - set(objects))
  72. for k in sorted(methods) + sorted(other) + sorted(objects):
  73. val = getattr(self,k)
  74. if str(type(val))[:13] == "<class 'mmgen": # recurse into sub-objects
  75. out.append('\n%s%s (%s):' % (indent,k,type(val)))
  76. lvl += 1
  77. out.append(unicode(getattr(self,k))+'\n')
  78. lvl -= 1
  79. else:
  80. out.append(fs % (k, conv(val,col_w)))
  81. return repr(self) + '\n ' + '\n '.join(out)
  82. class MMGenListItemAttr(object):
  83. def __init__(self,name,dtype):
  84. self.name = name
  85. self.dtype = dtype
  86. def __get__(self,instance,owner):
  87. return instance.__dict__[self.name]
  88. def __set__(self,instance,value):
  89. # if self.name == 'mmid': print repr(instance), repr(value) # DEBUG
  90. instance.__dict__[self.name] = globals()[self.dtype](value)
  91. def __delete__(self,instance):
  92. del instance.__dict__[self.name]
  93. class MMGenListItem(MMGenObject):
  94. addr = MMGenListItemAttr('addr','BTCAddr')
  95. amt = MMGenListItemAttr('amt','BTCAmt')
  96. mmid = MMGenListItemAttr('mmid','MMGenID')
  97. label = MMGenListItemAttr('label','MMGenLabel')
  98. attrs = ()
  99. attrs_priv = ()
  100. attrs_reassign = 'label',
  101. def attr_error(self,arg):
  102. raise AttributeError, "'{}': invalid attribute for {}".format(arg,type(self).__name__)
  103. def set_error(self,attr,val):
  104. raise ValueError, \
  105. "'{}': attribute '{}' in instance of class '{}' cannot be reassigned".format(
  106. val,attr,type(self).__name__)
  107. attrs_base = ('attrs','attrs_priv','attrs_reassign','attrs_base','attr_error','set_error','__dict__')
  108. def __init__(self,*args,**kwargs):
  109. if args:
  110. raise ValueError, 'Non-keyword args not allowed'
  111. for k in kwargs:
  112. if kwargs[k] != None:
  113. setattr(self,k,kwargs[k])
  114. def __getattribute__(self,name):
  115. ga = object.__getattribute__
  116. if name in ga(self,'attrs') + ga(self,'attrs_priv') + ga(self,'attrs_base'):
  117. try:
  118. return ga(self,name)
  119. except:
  120. return None
  121. else:
  122. self.attr_error(name)
  123. def __setattr__(self,name,val):
  124. if name in (self.attrs + self.attrs_priv + self.attrs_base):
  125. if getattr(self,name) == None or name in self.attrs_reassign:
  126. object.__setattr__(self,name,val)
  127. else:
  128. # object.__setattr__(self,name,val) # DEBUG
  129. self.set_error(name,val)
  130. else:
  131. self.attr_error(name)
  132. def __delattr__(self,name):
  133. if name in (self.attrs + self.attrs_priv + self.attrs_base):
  134. try: # don't know why this is necessary
  135. object.__delattr__(self,name)
  136. except:
  137. pass
  138. else:
  139. self.attr_error(name)
  140. class InitErrors(object):
  141. @staticmethod
  142. def arg_chk(cls,on_fail):
  143. assert on_fail in ('die','return','silent','raise'),"'on_fail' in class %s" % cls.__name__
  144. @staticmethod
  145. def init_fail(m,on_fail,silent=False):
  146. if silent: m = ''
  147. from mmgen.util import die,msg
  148. if on_fail == 'die': die(1,m)
  149. elif on_fail == 'return':
  150. if m: msg(m)
  151. return None
  152. elif on_fail == 'silent': return None
  153. elif on_fail == 'raise': raise ValueError,m
  154. class AddrIdx(int,InitErrors):
  155. max_digits = 7
  156. def __new__(cls,num,on_fail='die'):
  157. cls.arg_chk(cls,on_fail)
  158. try:
  159. assert type(num) is not float
  160. me = int.__new__(cls,num)
  161. except:
  162. m = "'%s': value cannot be converted to address index" % num
  163. else:
  164. if len(str(me)) > cls.max_digits:
  165. m = "'%s': too many digits in addr idx" % num
  166. elif me < 1:
  167. m = "'%s': addr idx cannot be less than one" % num
  168. else:
  169. return me
  170. return cls.init_fail(m,on_fail)
  171. class AddrIdxList(list,InitErrors):
  172. max_len = 1000000
  173. def __init__(self,fmt_str=None,idx_list=None,on_fail='die',sep=','):
  174. self.arg_chk(type(self),on_fail)
  175. assert fmt_str or idx_list
  176. if idx_list:
  177. # dies on failure
  178. return list.__init__(self,sorted(set([AddrIdx(i) for i in idx_list])))
  179. elif fmt_str:
  180. desc = fmt_str
  181. ret,fs = [],"'%s': value cannot be converted to address index"
  182. from mmgen.util import msg
  183. for i in (fmt_str.split(sep)):
  184. j = i.split('-')
  185. if len(j) == 1:
  186. idx = AddrIdx(i,on_fail='return')
  187. if not idx: break
  188. ret.append(idx)
  189. elif len(j) == 2:
  190. beg = AddrIdx(j[0],on_fail='return')
  191. if not beg: break
  192. end = AddrIdx(j[1],on_fail='return')
  193. if not beg: break
  194. if end < beg:
  195. msg(fs % "%s-%s (invalid range)" % (beg,end)); break
  196. ret.extend([AddrIdx(x) for x in range(beg,end+1)])
  197. else:
  198. msg((fs % i) + ' list'); break
  199. else:
  200. return list.__init__(self,sorted(set(ret))) # fell off end of loop - success
  201. return self.init_fail((fs + ' list') % desc,on_fail)
  202. class Hilite(object):
  203. color = 'red'
  204. color_always = False
  205. width = 0
  206. trunc_ok = True
  207. @classmethod
  208. def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None,center=False,nullrepl=''):
  209. if width == None: width = cls.width
  210. if trunc_ok == None: trunc_ok = cls.trunc_ok
  211. assert width > 0
  212. if s == '' and nullrepl:
  213. s,center = nullrepl,True
  214. if center: s = s.center(width)
  215. assert type(encl) is str and len(encl) in (0,2)
  216. a,b = list(encl) if encl else ('','')
  217. if trunc_ok and len(s) > width: s = s[:width]
  218. return cls.colorize((a+s+b).ljust(width),color=color)
  219. def fmt(self,*args,**kwargs):
  220. assert args == () # forbid invocation w/o keywords
  221. return self.fmtc(self,*args,**kwargs)
  222. @classmethod
  223. def hlc(cls,s,color=True):
  224. return cls.colorize(s,color=color)
  225. def hl(self,color=True):
  226. return self.colorize(self,color=color)
  227. def __str__(self):
  228. return self.colorize(self,color=False)
  229. @classmethod
  230. def colorize(cls,s,color=True):
  231. import mmgen.globalvars as g
  232. from mmgen.util import red,blue,green,yellow,pink,cyan,gray,orange,magenta
  233. k = color if type(color) is str else cls.color # hack: override color with str value
  234. return locals()[k](s) if (color or cls.color_always) and g.color else s
  235. class BTCAmt(Decimal,Hilite,InitErrors):
  236. color = 'yellow'
  237. max_prec = 8
  238. max_amt = 21000000
  239. def __new__(cls,num,on_fail='die'):
  240. cls.arg_chk(cls,on_fail)
  241. try:
  242. me = Decimal.__new__(cls,str(num))
  243. except:
  244. m = "'%s': value cannot be converted to decimal" % num
  245. else:
  246. if me.normalize().as_tuple()[-1] < -cls.max_prec:
  247. m = "'%s': too many decimal places in BTC amount" % num
  248. elif me > cls.max_amt:
  249. m = "'%s': BTC amount too large (>%s)" % (num,cls.max_amt)
  250. # elif me.as_tuple()[0]:
  251. # m = "'%s': BTC amount cannot be negative" % num
  252. else:
  253. return me
  254. return cls.init_fail(m,on_fail)
  255. @classmethod
  256. def fmtc(cls):
  257. raise NotImplemented
  258. def fmt(self,fs='3.8',color=False,suf=''):
  259. s = self.__str__(color=False)
  260. if '.' in fs:
  261. p1,p2 = [int(i) for i in fs.split('.',1)]
  262. ss = s.split('.',1)
  263. if len(ss) == 2:
  264. a,b = ss
  265. ret = a.rjust(p1) + '.' + (b+suf).ljust(p2+len(suf))
  266. else:
  267. ret = s.rjust(p1) + suf + ' ' * (p2+1)
  268. else:
  269. ret = s.ljust(int(fs))
  270. return self.colorize(ret,color=color)
  271. def hl(self,color=True):
  272. return self.__str__(color=color)
  273. def __str__(self,color=False): # format simply, no exponential notation
  274. if int(self) == self:
  275. ret = str(int(self))
  276. else:
  277. ret = self.normalize().__format__('f')
  278. return self.colorize(ret,color=color)
  279. def __repr__(self):
  280. return "{}('{}')".format(type(self).__name__,self.__str__())
  281. def __add__(self,other,context=None):
  282. return type(self)(Decimal.__add__(self,other,context))
  283. __radd__ = __add__
  284. def __sub__(self,other,context=None):
  285. return type(self)(Decimal.__sub__(self,other,context))
  286. def __mul__(self,other,context=None):
  287. return type(self)('{:0.8f}'.format(Decimal.__mul__(self,Decimal(other),context)))
  288. def __div__(self,other,context=None):
  289. return type(self)('{:0.8f}'.format(Decimal.__div__(self,Decimal(other),context)))
  290. def __neg__(self,other,context=None):
  291. return type(self)(Decimal.__neg__(self,other,context))
  292. class BTCAddr(str,Hilite,InitErrors):
  293. color = 'cyan'
  294. width = 34
  295. def __new__(cls,s,on_fail='die'):
  296. cls.arg_chk(cls,on_fail)
  297. me = str.__new__(cls,s)
  298. from mmgen.bitcoin import verify_addr
  299. if type(s) in (str,unicode,BTCAddr) and verify_addr(s):
  300. return me
  301. else:
  302. m = "'%s': value is not a Bitcoin address" % s
  303. return cls.init_fail(m,on_fail)
  304. @classmethod
  305. def fmtc(cls,s,**kwargs):
  306. # True -> 'cyan': use the str value override hack
  307. if 'color' in kwargs and kwargs['color'] == True:
  308. kwargs['color'] = cls.color
  309. if not 'width' in kwargs: kwargs['width'] = cls.width
  310. if kwargs['width'] < len(s):
  311. s = s[:kwargs['width']-2] + '..'
  312. return Hilite.fmtc(s,**kwargs)
  313. class SeedID(str,Hilite,InitErrors):
  314. color = 'blue'
  315. width = 8
  316. trunc_ok = False
  317. def __new__(cls,seed=None,sid=None,on_fail='die'):
  318. cls.arg_chk(cls,on_fail)
  319. assert seed or sid
  320. if seed:
  321. from mmgen.seed import Seed
  322. from mmgen.util import make_chksum_8
  323. if type(seed) == Seed:
  324. return str.__new__(cls,make_chksum_8(seed.get_data()))
  325. elif sid:
  326. from string import hexdigits
  327. if len(sid) == cls.width and set(sid) <= set(hexdigits.upper()):
  328. return str.__new__(cls,sid)
  329. m = "'%s': value cannot be converted to SeedID" % str(seed or sid)
  330. return cls.init_fail(m,on_fail)
  331. class MMGenID(str,Hilite,InitErrors):
  332. color = 'orange'
  333. width = 0
  334. trunc_ok = False
  335. def __new__(cls,s,on_fail='die'):
  336. cls.arg_chk(cls,on_fail)
  337. s = str(s)
  338. if ':' in s:
  339. a,b = s.split(':',1)
  340. sid = SeedID(sid=a,on_fail='silent')
  341. if sid:
  342. idx = AddrIdx(b,on_fail='silent')
  343. if idx:
  344. return str.__new__(cls,'%s:%s' % (sid,idx))
  345. m = "'%s': value cannot be converted to MMGenID" % s
  346. return cls.init_fail(m,on_fail)
  347. class MMGenLabel(unicode,Hilite,InitErrors):
  348. color = 'pink'
  349. allowed = u''
  350. max_len = 0
  351. desc = 'label'
  352. def __new__(cls,s,on_fail='die',msg=None):
  353. cls.arg_chk(cls,on_fail)
  354. try:
  355. s = s.decode('utf8').strip()
  356. except:
  357. m = "'%s: value is not a valid UTF-8 string" % s
  358. else:
  359. if len(s) > cls.max_len:
  360. m = '%s too long (>%s symbols)' % (cls.desc.capitalize(),cls.max_len)
  361. elif cls.allowed and not set(list(s)).issubset(set(list(cls.allowed))):
  362. m = '%s contains non-permitted symbols: %s' % (cls.desc.capitalize(),
  363. ' '.join(set(list(s)) - set(list(cls.allowed))))
  364. else:
  365. return unicode.__new__(cls,s)
  366. return cls.init_fail((msg+'\n' if msg else '') + m,on_fail)
  367. class MMGenWalletLabel(MMGenLabel):
  368. max_len = 48
  369. allowed = [chr(i+32) for i in range(95)]
  370. desc = 'wallet label'
  371. class MMGenAddrLabel(MMGenLabel):
  372. max_len = 32
  373. allowed = [chr(i+32) for i in range(95)]
  374. desc = 'address label'
  375. class MMGenTXLabel(MMGenLabel):
  376. max_len = 72
  377. desc = 'transaction label'