amt.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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. amt.py: MMGen CoinAmt and related classes
  20. """
  21. from decimal import Decimal
  22. from .objmethods import Hilite,InitErrors
  23. class DecimalNegateResult(Decimal): pass
  24. class CoinAmt(Decimal,Hilite,InitErrors): # abstract class
  25. """
  26. Instantiating with 'from_decimal' rounds value down to 'max_prec' precision.
  27. For addition and subtraction, operand types must match.
  28. For multiplication and division, operand types may differ.
  29. Negative amounts, floor division and modulus operation are unimplemented.
  30. """
  31. color = 'yellow'
  32. forbidden_types = (float,int)
  33. max_prec = 0 # number of decimal places for this coin
  34. max_amt = None # coin supply if known, otherwise None
  35. units = () # defined unit names, e.g. ('satoshi',...)
  36. amt_fs = '0.0' # format string for the fmt() method
  37. def __new__(cls,num,from_unit=None,from_decimal=False):
  38. if type(num) == cls:
  39. return num
  40. try:
  41. if from_unit:
  42. assert from_unit in cls.units, f'{from_unit!r}: unrecognized denomination for {cls.__name__}'
  43. assert type(num) == int,'value is not an integer'
  44. me = Decimal.__new__(cls,num * getattr(cls,from_unit))
  45. elif from_decimal:
  46. assert type(num) == Decimal, f'number must be of type Decimal, not {type(num).__name__})'
  47. me = Decimal.__new__(cls,num.quantize(Decimal('10') ** -cls.max_prec))
  48. else:
  49. for t in cls.forbidden_types:
  50. assert type(num) is not t, f'number is of forbidden type {t.__name__}'
  51. me = Decimal.__new__(cls,str(num))
  52. assert me.normalize().as_tuple()[-1] >= -cls.max_prec,'too many decimal places in coin amount'
  53. if cls.max_amt:
  54. assert me <= cls.max_amt, f'{me}: coin amount too large (>{cls.max_amt})'
  55. assert me >= 0,'coin amount cannot be negative'
  56. return me
  57. except Exception as e:
  58. return cls.init_fail(e,num)
  59. def to_unit(self,unit,show_decimal=False):
  60. ret = Decimal(self) // getattr(self,unit)
  61. if show_decimal and ret < 1:
  62. return f'{ret:.8f}'.rstrip('0')
  63. return int(ret)
  64. @classmethod
  65. def fmtc(cls):
  66. cls.method_not_implemented()
  67. def fmt(self,fs=None,color=False,suf='',prec=1000):
  68. if fs == None:
  69. fs = self.amt_fs
  70. s = self.__str__()
  71. if '.' in fs:
  72. p1,p2 = list(map(int,fs.split('.',1)))
  73. ss = s.split('.',1)
  74. if len(ss) == 2:
  75. a,b = ss
  76. ret = a.rjust(p1) + '.' + ((b+suf).ljust(p2+len(suf)))[:prec]
  77. else:
  78. ret = s.rjust(p1) + suf + (' ' * (p2+1))[:prec+1-len(suf)]
  79. else:
  80. ret = s.ljust(int(fs))
  81. return self.colorize(ret,color=color)
  82. def hl(self,color=True):
  83. return self.colorize(self.__str__(),color=color)
  84. def hl2(self,color=True,encl=''): # display with coin symbol
  85. return (
  86. encl[:-1]
  87. + self.colorize(self.__str__(),color=color)
  88. + ' ' + type(self).__name__[:-3]
  89. + encl[1:]
  90. )
  91. def __str__(self): # format simply, with no exponential notation
  92. return str(int(self)) if int(self) == self else self.normalize().__format__('f')
  93. def __repr__(self):
  94. return "{}('{}')".format(type(self).__name__,self.__str__())
  95. def __add__(self,other,*args,**kwargs):
  96. """
  97. we must allow other to be int(0) to use the sum() builtin
  98. """
  99. if type(other) not in ( type(self), DecimalNegateResult ) and other != 0:
  100. raise ValueError(
  101. f'operand {other} of incorrect type ({type(other).__name__} != {type(self).__name__})')
  102. return type(self)(Decimal.__add__(self,other,*args,**kwargs))
  103. __radd__ = __add__
  104. def __sub__(self,other,*args,**kwargs):
  105. if type(other) is not type(self):
  106. raise ValueError(
  107. f'operand {other} of incorrect type ({type(other).__name__} != {type(self).__name__})')
  108. return type(self)(Decimal.__sub__(self,other,*args,**kwargs))
  109. def copy_negate(self,*args,**kwargs):
  110. """
  111. We implement this so that __add__() can check type, because:
  112. class Decimal:
  113. def __sub__(self, other, ...):
  114. ...
  115. return self.__add__(other.copy_negate(), ...)
  116. """
  117. return DecimalNegateResult(Decimal.copy_negate(self,*args,**kwargs))
  118. def __mul__(self,other,*args,**kwargs):
  119. return type(self)('{:0.{p}f}'.format(
  120. Decimal.__mul__(self,Decimal(other),*args,**kwargs),
  121. p = self.max_prec
  122. ))
  123. __rmul__ = __mul__
  124. def __truediv__(self,other,*args,**kwargs):
  125. return type(self)('{:0.{p}f}'.format(
  126. Decimal.__truediv__(self,Decimal(other),*args,**kwargs),
  127. p = self.max_prec
  128. ))
  129. def __neg__(self,*args,**kwargs):
  130. self.method_not_implemented()
  131. def __floordiv__(self,*args,**kwargs):
  132. self.method_not_implemented()
  133. def __mod__(self,*args,**kwargs):
  134. self.method_not_implemented()
  135. class BTCAmt(CoinAmt):
  136. max_prec = 8
  137. max_amt = 21000000
  138. satoshi = Decimal('0.00000001')
  139. units = ('satoshi',)
  140. amt_fs = '4.8'
  141. class BCHAmt(BTCAmt):
  142. pass
  143. class LTCAmt(BTCAmt):
  144. max_amt = 84000000
  145. class XMRAmt(CoinAmt):
  146. max_prec = 12
  147. atomic = Decimal('0.000000000001')
  148. units = ('atomic',)
  149. amt_fs = '4.12'
  150. # Kwei (babbage) 3, Mwei (lovelace) 6, Gwei (shannon) 9, µETH (szabo) 12, mETH (finney) 15, ETH 18
  151. class ETHAmt(CoinAmt):
  152. max_prec = 18
  153. wei = Decimal('0.000000000000000001')
  154. Kwei = Decimal('0.000000000000001')
  155. Mwei = Decimal('0.000000000001')
  156. Gwei = Decimal('0.000000001')
  157. szabo = Decimal('0.000001')
  158. finney = Decimal('0.001')
  159. units = ('wei','Kwei','Mwei','Gwei','szabo','finney')
  160. amt_fs = '4.18'
  161. def toWei(self):
  162. return int(Decimal(self) // self.wei)