obj.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  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(str(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 addr idx" % 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. return list.__init__(self,sorted(set(idx_list)))
  178. elif fmt_str:
  179. ret,fs = [],"'%s': value cannot be converted to addr idx"
  180. from mmgen.util import msg
  181. for i in (fmt_str.split(sep)):
  182. j = i.split('-')
  183. if len(j) == 1:
  184. idx = AddrIdx(i,on_fail='return')
  185. if not idx: break
  186. ret.append(idx)
  187. elif len(j) == 2:
  188. beg = AddrIdx(j[0],on_fail='return')
  189. if not beg: break
  190. end = AddrIdx(j[1],on_fail='return')
  191. if not beg: break
  192. if end < beg:
  193. msg(fs % "%s-%s (invalid range)" % (beg,end)); break
  194. ret.extend([AddrIdx(x) for x in range(beg,end+1)])
  195. else:
  196. msg((fs % i) + ' list'); break
  197. else:
  198. return list.__init__(self,sorted(set(ret))) # fell off end of loop - success
  199. return self.init_fail(fs % err,on_fail,silent=True)
  200. class Hilite(object):
  201. color = 'red'
  202. color_always = False
  203. width = 0
  204. trunc_ok = True
  205. @classmethod
  206. def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None):
  207. if width == None: width = cls.width
  208. if trunc_ok == None: trunc_ok = cls.trunc_ok
  209. assert width > 0
  210. assert type(encl) is str and len(encl) in (0,2)
  211. a,b = list(encl) if encl else ('','')
  212. if trunc_ok and len(s) > width: s = s[:width]
  213. return cls.colorize((a+s+b).ljust(width),color=color)
  214. def fmt(self,width=None,color=False,encl='',trunc_ok=None):
  215. if width == None: width = self.width
  216. if trunc_ok == None: trunc_ok = self.trunc_ok
  217. return self.fmtc(self,width=width,color=color,encl=encl,trunc_ok=trunc_ok)
  218. @classmethod
  219. def hlc(cls,s,color=True):
  220. return cls.colorize(s,color=color)
  221. def hl(self,color=True):
  222. return self.colorize(self,color=color)
  223. def __str__(self):
  224. return self.colorize(self,color=False)
  225. @classmethod
  226. def colorize(cls,s,color=True):
  227. import mmgen.globalvars as g
  228. from mmgen.util import red,blue,green,yellow,pink,cyan,gray,orange,magenta
  229. return locals()[cls.color](s) if (color or cls.color_always) and g.color else s
  230. class BTCAmt(Decimal,Hilite,InitErrors):
  231. color = 'yellow'
  232. max_prec = 8
  233. max_amt = 21000000
  234. def __new__(cls,num,on_fail='die'):
  235. cls.arg_chk(cls,on_fail)
  236. try:
  237. me = Decimal.__new__(cls,str(num))
  238. except:
  239. m = "'%s': value cannot be converted to decimal" % num
  240. else:
  241. if me.normalize().as_tuple()[-1] < -cls.max_prec:
  242. m = "'%s': too many decimal places in BTC amount" % num
  243. elif me > cls.max_amt:
  244. m = "'%s': BTC amount too large (>%s)" % (num,cls.max_amt)
  245. # elif me.as_tuple()[0]:
  246. # m = "'%s': BTC amount cannot be negative" % num
  247. else:
  248. return me
  249. return cls.init_fail(m,on_fail)
  250. @classmethod
  251. def fmtc(cls):
  252. raise NotImplemented
  253. def fmt(self,fs='3.8',color=False,suf=''):
  254. s = self.__str__(color=False)
  255. if '.' in fs:
  256. p1,p2 = [int(i) for i in fs.split('.',1)]
  257. ss = s.split('.',1)
  258. if len(ss) == 2:
  259. a,b = ss
  260. ret = a.rjust(p1) + '.' + (b+suf).ljust(p2+len(suf))
  261. else:
  262. ret = s.rjust(p1) + suf + ' ' * (p2+1)
  263. else:
  264. ret = s.ljust(int(fs))
  265. return self.colorize(ret,color=color)
  266. def hl(self,color=True):
  267. return self.__str__(color=color)
  268. def __str__(self,color=False): # format simply, no exponential notation
  269. if int(self) == self:
  270. ret = str(int(self))
  271. else:
  272. ret = self.normalize().__format__('f')
  273. return self.colorize(ret,color=color)
  274. def __repr__(self):
  275. return "{}('{}')".format(type(self).__name__,self.__str__())
  276. def __add__(self,other,context=None):
  277. return type(self)(Decimal.__add__(self,other,context))
  278. __radd__ = __add__
  279. def __sub__(self,other,context=None):
  280. return type(self)(Decimal.__sub__(self,other,context))
  281. def __mul__(self,other,context=None):
  282. return type(self)('{:0.8f}'.format(Decimal.__mul__(self,Decimal(other),context)))
  283. def __div__(self,other,context=None):
  284. return type(self)('{:0.8f}'.format(Decimal.__div__(self,Decimal(other),context)))
  285. def __neg__(self,other,context=None):
  286. return type(self)(Decimal.__neg__(self,other,context))
  287. class BTCAddr(str,Hilite,InitErrors):
  288. color = 'cyan'
  289. width = 34
  290. def __new__(cls,s,on_fail='die'):
  291. cls.arg_chk(cls,on_fail)
  292. me = str.__new__(cls,s)
  293. from mmgen.bitcoin import verify_addr
  294. if verify_addr(s):
  295. return me
  296. else:
  297. m = "'%s': value is not a Bitcoin address" % s
  298. return cls.init_fail(m,on_fail)
  299. def fmt(self,width=width,color=False):
  300. return self.fmtc(self,width=width,color=color)
  301. @classmethod
  302. def fmtc(cls,s,width=width,color=False):
  303. if width >= len(s):
  304. s = s.ljust(width)
  305. else:
  306. s = s[:width-2] + '..'
  307. return cls.colorize(s,color=color)
  308. class SeedID(str,Hilite,InitErrors):
  309. color = 'blue'
  310. width = 8
  311. trunc_ok = False
  312. def __new__(cls,seed=None,sid=None,on_fail='die'):
  313. cls.arg_chk(cls,on_fail)
  314. assert seed or sid
  315. if seed:
  316. from mmgen.seed import Seed
  317. from mmgen.util import make_chksum_8
  318. assert type(seed) == Seed
  319. return str.__new__(cls,make_chksum_8(seed.get_data()))
  320. elif sid:
  321. from string import hexdigits
  322. assert len(sid) == cls.width and set(sid) <= set(hexdigits.upper())
  323. return str.__new__(cls,sid)
  324. m = "'%s': value cannot be converted to SeedID" % s
  325. return cls.init_fail(m,on_fail)
  326. class MMGenID(str,Hilite,InitErrors):
  327. color = 'orange'
  328. width = 0
  329. trunc_ok = False
  330. def __new__(cls,s,on_fail='die'):
  331. cls.arg_chk(cls,on_fail)
  332. s = str(s)
  333. if ':' in s:
  334. a,b = s.split(':',1)
  335. sid = SeedID(sid=a,on_fail='return')
  336. if sid:
  337. idx = AddrIdx(b,on_fail='return')
  338. if idx:
  339. return str.__new__(cls,'%s:%s' % (sid,idx))
  340. m = "'%s': value cannot be converted to MMGenID" % s
  341. return cls.init_fail(m,on_fail)
  342. class MMGenLabel(unicode,Hilite,InitErrors):
  343. color = 'pink'
  344. allowed = u''
  345. max_len = 0
  346. desc = 'label'
  347. def __new__(cls,s,on_fail='die',msg=None):
  348. cls.arg_chk(cls,on_fail)
  349. try:
  350. s = s.decode('utf8').strip()
  351. except:
  352. m = "'%s: value is not a valid UTF-8 string" % s
  353. else:
  354. if len(s) > cls.max_len:
  355. m = '%s too long (>%s symbols)' % (cls.desc.capitalize(),cls.max_len)
  356. elif cls.allowed and not set(list(s)).issubset(set(list(cls.allowed))):
  357. m = '%s contains non-permitted symbols: %s' % (cls.desc.capitalize(),
  358. ' '.join(set(list(s)) - set(list(cls.allowed))))
  359. else:
  360. return unicode.__new__(cls,s)
  361. return cls.init_fail((msg+'\n' if msg else '') + m,on_fail)
  362. class MMGenWalletLabel(MMGenLabel):
  363. max_len = 48
  364. allowed = [chr(i+32) for i in range(95)]
  365. desc = 'wallet label'
  366. class MMGenAddrLabel(MMGenLabel):
  367. max_len = 32
  368. desc = 'address label'
  369. class MMGenTXLabel(MMGenLabel):
  370. max_len = 72
  371. desc = 'transaction label'