amt.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 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. from .obj import get_obj
  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. Decimal precision is set in init_proto()
  31. """
  32. coin = 'Coin'
  33. color = 'yellow'
  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 isinstance(num, CoinAmt):
  39. raise TypeError(f'CoinAmt: {num} is instance of {cls.__name__}')
  40. try:
  41. if from_unit:
  42. assert from_unit in cls.units, f'{from_unit!r}: unrecognized coin unit for {cls.__name__}'
  43. assert isinstance(num, int), 'value is not an integer'
  44. me = Decimal.__new__(cls, num * getattr(cls, from_unit))
  45. elif from_decimal:
  46. assert isinstance(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. assert isinstance(num, str), f'non-string passed to {cls.__name__} initializer'
  50. me = Decimal.__new__(cls, num)
  51. assert me.normalize().as_tuple()[-1] >= -cls.max_prec, 'too many decimal places in coin amount'
  52. if cls.max_amt:
  53. assert me <= cls.max_amt, f'{me}: coin amount too large (>{cls.max_amt})'
  54. assert me >= 0, 'coin amount cannot be negative'
  55. return me
  56. except Exception as e:
  57. return cls.init_fail(e, num)
  58. def to_unit(self, unit):
  59. if (u := getattr(self, unit)) == self.atomic:
  60. return int(Decimal(self) // u)
  61. else:
  62. return Decimal('{:0.{w}f}'.format(
  63. self / u,
  64. w = (u/self.atomic).as_tuple().exponent))
  65. @classmethod
  66. def fmtc(cls, *args, **kwargs):
  67. cls.method_not_implemented()
  68. def fmt(self, iwidth=1, /, *, color=False, prec=None): # iwidth: width of the integer part
  69. prec = prec or self.max_prec
  70. if '.' in (s := str(self)):
  71. a, b = s.split('.', 1)
  72. return self.colorize(
  73. a.rjust(iwidth) + '.' + b.ljust(prec)[:prec], # truncation, not rounding!
  74. color = color)
  75. else:
  76. return self.colorize(
  77. s.rjust(iwidth).ljust(iwidth+prec+1),
  78. color = color)
  79. def hl(self, *, color=True):
  80. return self.colorize(str(self), color=color)
  81. # fancy highlighting with coin unit, enclosure, formatting
  82. def hl2(self, *, color=True, unit=False, fs='{}', encl=''):
  83. res = fs.format(self)
  84. return (
  85. encl[:-1]
  86. + self.colorize(
  87. (res.rstrip('0').rstrip('.') if '.' in res else res) +
  88. (' ' + self.coin if unit else ''),
  89. color = color)
  90. + encl[1:]
  91. )
  92. def __str__(self): # format simply, with no exponential notation
  93. return str(int(self)) if int(self) == self else self.normalize().__format__('f')
  94. def __repr__(self):
  95. return "{}('{}')".format(type(self).__name__, self.__str__())
  96. def __add__(self, other, *args, **kwargs):
  97. """
  98. we must allow other to be int(0) to use the sum() builtin
  99. """
  100. if type(other) is type(self) or (other == 0 and isinstance(other, int)):
  101. return type(self)(Decimal.__add__(self, other, *args, **kwargs), from_decimal=True)
  102. raise TypeError(
  103. f'operand {other} is of incorrect type ({type(other).__name__} != {type(self).__name__})')
  104. __radd__ = __add__
  105. def __sub__(self, other, *args, **kwargs):
  106. if type(other) is type(self):
  107. return type(self)(Decimal.__sub__(self, other, *args, **kwargs), from_decimal=True)
  108. raise TypeError(
  109. f'operand {other} is of incorrect type ({type(other).__name__} != {type(self).__name__})')
  110. def __rsub__(self, other, *args, **kwargs):
  111. if type(other) is type(self):
  112. return type(self)(Decimal.__rsub__(self, other, *args, **kwargs), from_decimal=True)
  113. raise TypeError(
  114. f'operand {other} is of incorrect type ({type(other).__name__} != {type(self).__name__})')
  115. def __mul__(self, other, *args, **kwargs):
  116. return type(self)('{:0.{p}f}'.format(
  117. Decimal.__mul__(self, Decimal(other), *args, **kwargs),
  118. p = self.max_prec
  119. ))
  120. __rmul__ = __mul__
  121. def __truediv__(self, other, *args, **kwargs):
  122. return type(self)('{:0.{p}f}'.format(
  123. Decimal.__truediv__(self, Decimal(other), *args, **kwargs),
  124. p = self.max_prec
  125. ))
  126. def __rtruediv__(self, other, *args, **kwargs):
  127. return type(self)('{:0.{p}f}'.format(
  128. Decimal.__rtruediv__(self, Decimal(other), *args, **kwargs),
  129. p = self.max_prec
  130. ))
  131. def __neg__(self, *args, **kwargs):
  132. self.method_not_implemented()
  133. def __floordiv__(self, *args, **kwargs):
  134. self.method_not_implemented()
  135. def __mod__(self, *args, **kwargs):
  136. self.method_not_implemented()
  137. def is_coin_amt(proto, num, *, from_unit=None, from_decimal=False):
  138. assert proto.coin_amt, 'proto.coin_amt is None! Did you call init_proto() with ‘need_amt’?'
  139. return get_obj(proto.coin_amt, num=num, from_unit=from_unit, from_decimal=from_decimal, silent=True, return_bool=True)
  140. class BTCAmt(CoinAmt):
  141. coin = 'BTC'
  142. max_prec = 8
  143. max_amt = 21000000
  144. satoshi = Decimal('0.00000001')
  145. atomic = satoshi
  146. units = ('satoshi',)
  147. class UniAmt(BTCAmt):
  148. coin = None
  149. class BCHAmt(BTCAmt):
  150. coin = 'BCH'
  151. class LTCAmt(BTCAmt):
  152. coin = 'LTC'
  153. max_amt = 84000000
  154. class XMRAmt(CoinAmt):
  155. coin = 'XMR'
  156. max_prec = 12
  157. atomic = Decimal('0.000000000001')
  158. units = ('atomic',)
  159. # Kwei (babbage) 3, Mwei (lovelace) 6, Gwei (shannon) 9, µETH (szabo) 12, mETH (finney) 15, ETH 18
  160. class ETHAmt(CoinAmt):
  161. coin = 'ETH'
  162. max_prec = 18
  163. wei = Decimal('0.000000000000000001')
  164. Kwei = Decimal('0.000000000000001')
  165. Mwei = Decimal('0.000000000001')
  166. Gwei = Decimal('0.000000001')
  167. szabo = Decimal('0.000001')
  168. finney = Decimal('0.001')
  169. atomic = wei
  170. units = ('wei', 'Kwei', 'Mwei', 'Gwei', 'szabo', 'finney')
  171. def toWei(self):
  172. return int(Decimal(self) // self.wei)
  173. class ETCAmt(ETHAmt):
  174. coin = 'ETC'
  175. def CoinAmtChk(proto, num):
  176. assert type(num) is proto.coin_amt, f'CoinAmtChk: {type(num)} != {proto.coin_amt}'
  177. return num