123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- #!/usr/bin/env python3
- #
- # MMGen Wallet, a terminal-based cryptocurrency wallet
- # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- """
- amt: MMGen CoinAmt and related classes
- """
- from decimal import Decimal
- from .objmethods import Hilite, InitErrors
- from .obj import get_obj
- class CoinAmt(Decimal, Hilite, InitErrors): # abstract class
- """
- Instantiating with 'from_decimal' rounds value down to 'max_prec' precision.
- For addition and subtraction, operand types must match.
- For multiplication and division, operand types may differ.
- Negative amounts, floor division and modulus operation are unimplemented.
- Decimal precision is set in init_proto()
- """
- coin = 'Coin'
- color = 'yellow'
- max_prec = 0 # number of decimal places for this coin
- max_amt = None # coin supply if known, otherwise None
- units = () # defined unit names, e.g. ('satoshi',...)
- def __new__(cls, num, *, from_unit=None, from_decimal=False):
- if isinstance(num, CoinAmt):
- raise TypeError(f'CoinAmt: {num} is instance of {cls.__name__}')
- try:
- if from_unit:
- assert from_unit in cls.units, f'{from_unit!r}: unrecognized coin unit for {cls.__name__}'
- assert isinstance(num, int), 'value is not an integer'
- me = Decimal.__new__(cls, num * getattr(cls, from_unit))
- elif from_decimal:
- assert isinstance(num, Decimal), f'number must be of type Decimal, not {type(num).__name__})'
- me = Decimal.__new__(cls, num.quantize(Decimal('10') ** -cls.max_prec))
- else:
- assert isinstance(num, str), f'non-string passed to {cls.__name__} initializer'
- me = Decimal.__new__(cls, num)
- assert me.normalize().as_tuple()[-1] >= -cls.max_prec, 'too many decimal places in coin amount'
- if cls.max_amt:
- assert me <= cls.max_amt, f'{me}: coin amount too large (>{cls.max_amt})'
- assert me >= 0, 'coin amount cannot be negative'
- return me
- except Exception as e:
- return cls.init_fail(e, num)
- def to_unit(self, unit):
- if (u := getattr(self, unit)) == self.atomic:
- return int(Decimal(self) // u)
- else:
- return Decimal('{:0.{w}f}'.format(
- self / u,
- w = (u/self.atomic).as_tuple().exponent))
- @classmethod
- def fmtc(cls, *args, **kwargs):
- cls.method_not_implemented()
- def fmt(self, iwidth=1, /, *, color=False, prec=None): # iwidth: width of the integer part
- prec = prec or self.max_prec
- if '.' in (s := str(self)):
- a, b = s.split('.', 1)
- return self.colorize(
- a.rjust(iwidth) + '.' + b.ljust(prec)[:prec], # truncation, not rounding!
- color = color)
- else:
- return self.colorize(
- s.rjust(iwidth).ljust(iwidth+prec+1),
- color = color)
- def hl(self, *, color=True):
- return self.colorize(str(self), color=color)
- # fancy highlighting with coin unit, enclosure, formatting
- def hl2(self, *, color=True, unit=False, fs='{}', encl=''):
- res = fs.format(self)
- return (
- encl[:-1]
- + self.colorize(
- (res.rstrip('0').rstrip('.') if '.' in res else res) +
- (' ' + self.coin if unit else ''),
- color = color)
- + encl[1:]
- )
- def __str__(self): # format simply, with no exponential notation
- return str(int(self)) if int(self) == self else self.normalize().__format__('f')
- def __repr__(self):
- return "{}('{}')".format(type(self).__name__, self.__str__())
- def __add__(self, other, *args, **kwargs):
- """
- we must allow other to be int(0) to use the sum() builtin
- """
- if type(other) is type(self) or (other == 0 and isinstance(other, int)):
- return type(self)(Decimal.__add__(self, other, *args, **kwargs), from_decimal=True)
- raise TypeError(
- f'operand {other} is of incorrect type ({type(other).__name__} != {type(self).__name__})')
- __radd__ = __add__
- def __sub__(self, other, *args, **kwargs):
- if type(other) is type(self):
- return type(self)(Decimal.__sub__(self, other, *args, **kwargs), from_decimal=True)
- raise TypeError(
- f'operand {other} is of incorrect type ({type(other).__name__} != {type(self).__name__})')
- def __rsub__(self, other, *args, **kwargs):
- if type(other) is type(self):
- return type(self)(Decimal.__rsub__(self, other, *args, **kwargs), from_decimal=True)
- raise TypeError(
- f'operand {other} is of incorrect type ({type(other).__name__} != {type(self).__name__})')
- def __mul__(self, other, *args, **kwargs):
- return type(self)('{:0.{p}f}'.format(
- Decimal.__mul__(self, Decimal(other), *args, **kwargs),
- p = self.max_prec
- ))
- __rmul__ = __mul__
- def __truediv__(self, other, *args, **kwargs):
- return type(self)('{:0.{p}f}'.format(
- Decimal.__truediv__(self, Decimal(other), *args, **kwargs),
- p = self.max_prec
- ))
- def __rtruediv__(self, other, *args, **kwargs):
- return type(self)('{:0.{p}f}'.format(
- Decimal.__rtruediv__(self, Decimal(other), *args, **kwargs),
- p = self.max_prec
- ))
- def __neg__(self, *args, **kwargs):
- self.method_not_implemented()
- def __floordiv__(self, *args, **kwargs):
- self.method_not_implemented()
- def __mod__(self, *args, **kwargs):
- self.method_not_implemented()
- def is_coin_amt(proto, num, *, from_unit=None, from_decimal=False):
- assert proto.coin_amt, 'proto.coin_amt is None! Did you call init_proto() with ‘need_amt’?'
- return get_obj(proto.coin_amt, num=num, from_unit=from_unit, from_decimal=from_decimal, silent=True, return_bool=True)
- class BTCAmt(CoinAmt):
- coin = 'BTC'
- max_prec = 8
- max_amt = 21000000
- satoshi = Decimal('0.00000001')
- atomic = satoshi
- units = ('satoshi',)
- class UniAmt(BTCAmt):
- coin = None
- class BCHAmt(BTCAmt):
- coin = 'BCH'
- class LTCAmt(BTCAmt):
- coin = 'LTC'
- max_amt = 84000000
- class XMRAmt(CoinAmt):
- coin = 'XMR'
- max_prec = 12
- atomic = Decimal('0.000000000001')
- units = ('atomic',)
- # Kwei (babbage) 3, Mwei (lovelace) 6, Gwei (shannon) 9, µETH (szabo) 12, mETH (finney) 15, ETH 18
- class ETHAmt(CoinAmt):
- coin = 'ETH'
- max_prec = 18
- wei = Decimal('0.000000000000000001')
- Kwei = Decimal('0.000000000000001')
- Mwei = Decimal('0.000000000001')
- Gwei = Decimal('0.000000001')
- szabo = Decimal('0.000001')
- finney = Decimal('0.001')
- atomic = wei
- units = ('wei', 'Kwei', 'Mwei', 'Gwei', 'szabo', 'finney')
- def toWei(self):
- return int(Decimal(self) // self.wei)
- class ETCAmt(ETHAmt):
- coin = 'ETC'
- def CoinAmtChk(proto, num):
- assert type(num) is proto.coin_amt, f'CoinAmtChk: {type(num)} != {proto.coin_amt}'
- return num
|