amt.py 6.1 KB

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