amt.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2023 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: 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. coin = 'Coin'
  32. color = 'yellow'
  33. forbidden_types = (float,int)
  34. max_prec = 0 # number of decimal places for this coin
  35. max_amt = None # coin supply if known, otherwise None
  36. units = () # defined unit names, e.g. ('satoshi',...)
  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(
  68. self,
  69. color = False,
  70. iwidth = 1, # width of the integer part
  71. prec = None ):
  72. s = self.__str__()
  73. prec = prec or self.max_prec
  74. if '.' in s:
  75. a,b = s.split('.',1)
  76. return self.colorize(
  77. a.rjust(iwidth) + '.' + b.ljust(prec)[:prec], # truncation, not rounding!
  78. color = color )
  79. else:
  80. return self.colorize(
  81. s.rjust(iwidth).ljust(iwidth+prec+1),
  82. color = color )
  83. def hl(self,color=True):
  84. return self.colorize(self.__str__(),color=color)
  85. # fancy highlighting with coin unit, enclosure, formatting
  86. def hl2(self,color=True,unit=False,fs='{}',encl=''):
  87. res = fs.format(self)
  88. return (
  89. encl[:-1]
  90. + self.colorize(
  91. (res.rstrip('0').rstrip('.') if '.' in res else res) +
  92. (' ' + self.coin if unit else ''),
  93. color = color )
  94. + encl[1:]
  95. )
  96. def __str__(self): # format simply, with no exponential notation
  97. return str(int(self)) if int(self) == self else self.normalize().__format__('f')
  98. def __repr__(self):
  99. return "{}('{}')".format(type(self).__name__,self.__str__())
  100. def __add__(self,other,*args,**kwargs):
  101. """
  102. we must allow other to be int(0) to use the sum() builtin
  103. """
  104. if type(other) not in ( type(self), DecimalNegateResult ) and other != 0:
  105. raise ValueError(
  106. f'operand {other} of incorrect type ({type(other).__name__} != {type(self).__name__})')
  107. return type(self)(Decimal.__add__(self,other,*args,**kwargs))
  108. __radd__ = __add__
  109. def __sub__(self,other,*args,**kwargs):
  110. if type(other) is not type(self):
  111. raise ValueError(
  112. f'operand {other} of incorrect type ({type(other).__name__} != {type(self).__name__})')
  113. return type(self)(Decimal.__sub__(self,other,*args,**kwargs))
  114. def copy_negate(self,*args,**kwargs):
  115. """
  116. We implement this so that __add__() can check type, because:
  117. class Decimal:
  118. def __sub__(self, other, ...):
  119. ...
  120. return self.__add__(other.copy_negate(), ...)
  121. """
  122. return DecimalNegateResult(Decimal.copy_negate(self,*args,**kwargs))
  123. def __mul__(self,other,*args,**kwargs):
  124. return type(self)('{:0.{p}f}'.format(
  125. Decimal.__mul__(self,Decimal(other),*args,**kwargs),
  126. p = self.max_prec
  127. ))
  128. __rmul__ = __mul__
  129. def __truediv__(self,other,*args,**kwargs):
  130. return type(self)('{:0.{p}f}'.format(
  131. Decimal.__truediv__(self,Decimal(other),*args,**kwargs),
  132. p = self.max_prec
  133. ))
  134. def __neg__(self,*args,**kwargs):
  135. self.method_not_implemented()
  136. def __floordiv__(self,*args,**kwargs):
  137. self.method_not_implemented()
  138. def __mod__(self,*args,**kwargs):
  139. self.method_not_implemented()
  140. class BTCAmt(CoinAmt):
  141. coin = 'BTC'
  142. max_prec = 8
  143. max_amt = 21000000
  144. satoshi = Decimal('0.00000001')
  145. units = ('satoshi',)
  146. class BCHAmt(BTCAmt):
  147. coin = 'BCH'
  148. class LTCAmt(BTCAmt):
  149. coin = 'LTC'
  150. max_amt = 84000000
  151. class XMRAmt(CoinAmt):
  152. coin = 'XMR'
  153. max_prec = 12
  154. atomic = Decimal('0.000000000001')
  155. units = ('atomic',)
  156. # Kwei (babbage) 3, Mwei (lovelace) 6, Gwei (shannon) 9, µETH (szabo) 12, mETH (finney) 15, ETH 18
  157. class ETHAmt(CoinAmt):
  158. coin = 'ETH'
  159. max_prec = 18
  160. wei = Decimal('0.000000000000000001')
  161. Kwei = Decimal('0.000000000000001')
  162. Mwei = Decimal('0.000000000001')
  163. Gwei = Decimal('0.000000001')
  164. szabo = Decimal('0.000001')
  165. finney = Decimal('0.001')
  166. units = ('wei','Kwei','Mwei','Gwei','szabo','finney')
  167. def toWei(self):
  168. return int(Decimal(self) // self.wei)
  169. class ETCAmt(ETHAmt):
  170. coin = 'ETC'