amt.py 6.1 KB

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