from __future__ import annotations
import math
import re
from typing import Union
import attr
from terra_proto.cosmos.base.v1beta1 import Coin as Coin_pb
from terra_sdk.util.json import JSONSerializable
from .numeric import Dec, Numeric
[docs]@attr.s(frozen=True)
class Coin(JSONSerializable):
"""Represents a (denom, amount) pairing, analagous to ``sdk.Coin`` and ``sdk.DecCoin``
in Cosmos SDK. Used for representing Terra native assets.
"""
denom: str = attr.ib()
"""Coin's denomination, ex ``uusd``, ``uluna``, etc."""
amount: Numeric.Output = attr.ib(converter=Numeric.parse) # type: ignore
"""Coin's amount -- can be a ``int`` or :class:`Dec`"""
[docs] @staticmethod
def parse(arg: Union[Coin, str, dict]) -> Coin:
"""Converts the argument into a coin.
Args:
arg (Union[Coin, str, dict]): value to be converted to coin
"""
if isinstance(arg, Coin):
return arg
elif isinstance(arg, str):
return Coin.from_str(arg)
else:
return Coin.from_data(arg)
[docs] def is_int_coin(self) -> bool:
"""Checks whether the coin's amount is of type ``int``."""
return isinstance(self.amount, int)
[docs] def is_dec_coin(self) -> bool:
"""Checks whether the coin's amount is of type :class:`Dec`."""
return isinstance(self.amount, Dec)
[docs] def to_int_coin(self) -> Coin:
"""Creates a new :class:`Coin` with an ``int`` amount."""
return Coin(self.denom, int(self.amount))
[docs] def to_int_ceil_coin(self) -> Coin:
"""Turns the :class:`coin` into an ``int`` coin with ceiling the amount."""
return Coin(self.denom, int(math.ceil(self.amount)))
[docs] def to_dec_coin(self) -> Coin:
"""Creates a new :class:`Coin` with a :class:`Dec` amount."""
return Coin(self.denom, Dec(self.amount))
def __str__(self) -> str:
if self.is_dec_coin():
amount_str = str(self.amount).rstrip("0")
if amount_str.endswith("."):
amount_str += "0"
return f"{amount_str}{self.denom}"
return f"{self.amount}{self.denom}"
def to_amino(self) -> dict:
return {"denom": self.denom, "amount": str(self.amount)}
[docs] def to_data(self) -> dict:
return {"denom": self.denom, "amount": str(self.amount)}
@classmethod
def from_proto(cls, proto: Coin_pb) -> Coin:
return cls(proto.denom, proto.amount)
def to_proto(self) -> Coin_pb:
coin = Coin_pb()
coin.denom = self.denom
coin.amount = str(self.amount)
return coin
[docs] @classmethod
def from_str(cls, string: str) -> Coin:
"""Creates a new :class:`Coin` from a coin-format string. Must match the format:
``283923uusd`` (``int``-Coin) or ``23920.23020uusd`` (:class:`Dec`-Coin).
>>> int_coin = Coin.from_str("230920uusd")
>>> int_coin.denom
'uusd'
>>> int_coin.amount
230920
>>> dec_coin = Coin.from_str("203922.223uluna")
>>> dec_coin.denom
'uluna'
>>> dec_coin.amount
Dec('203922.223')
Args:
string (str): string to convert
Raises:
ValueError: if string is in wrong format
Returns:
Coin: converted string
"""
pattern = r"^(\-?[0-9]+(\.[0-9]+)?)([0-9a-zA-Z/]+)$"
match = re.match(pattern, string)
if match is None:
raise ValueError(f"failed to parse Coin: {string}")
else:
return cls(match.group(3), match.group(1))
[docs] def add(self, addend: Union[Numeric.Input, Coin]) -> Coin:
"""Creates a new :class:`Coin` with the sum as amount. If the ``addend`` is a
:class:`Coin`, its ``denom`` must match.
Args:
addend (Union[Numeric.Input, Coin]): addend
Raises:
ArithmeticError: if addedend has different ``denom``
Returns:
Coin: sum
"""
if isinstance(addend, Coin):
if addend.denom != self.denom:
raise ArithmeticError(
f"cannot add/subtract two Coin objects of different denoms: {self.denom} and {addend.denom}"
)
return Coin(self.denom, self.amount + addend.amount)
else:
return Coin(self.denom, self.amount + Numeric.parse(addend))
def __add__(self, addend: Union[Numeric.Input, Coin]) -> Coin:
return self.add(addend)
[docs] def sub(self, subtrahend: Union[Numeric.Input, Coin]) -> Coin:
"""Creates a new :class:`Coin` with the difference as amount. If the ``subtrahend`` is a
:class:`Coin`, its ``denom`` must match.
Args:
subtrahend (Union[Numeric.Input, Coin]): subtrahend
Returns:
Coin: difference
"""
if isinstance(subtrahend, Coin):
return self.add(subtrahend.mul(-1))
else:
return self.add(Numeric.parse(subtrahend) * -1)
def __sub__(self, subtrahend: Union[Numeric.Input, Coin]) -> Coin:
return self.sub(subtrahend)
[docs] def mul(self, multiplier: Numeric.Input) -> Coin:
"""Creates a new :class:`Coin` with the product as amount. The ``multiplier``
argument is first coerced to either an ``int`` or :class:`Dec`.
Args:
multiplier (Numeric.Input): multiplier
Returns:
Coin: product
"""
other_amount = Numeric.parse(multiplier)
return Coin(self.denom, self.amount * other_amount)
def __mul__(self, multiplier: Numeric.Input) -> Coin:
return self.mul(multiplier)
[docs] def div(self, divisor: Numeric.Input) -> Coin:
"""Creates a new :class:`Coin` with the quotient as amount. The ``divisor``
argument is first coerced to either an ``int`` or :class:`Dec`.
Args:
divisor (Numeric.Input): divisor
Returns:
Coin: quotient
"""
other_amount = Numeric.parse(divisor)
if isinstance(other_amount, int):
return Coin(self.denom, (self.amount // other_amount))
else:
return Coin(self.denom, (self.amount / other_amount))
def __truediv__(self, divisor: Numeric.Input) -> Coin:
return self.div(divisor)
def __floordiv__(self, divisor: Numeric.Input) -> Coin:
return self.div(int(Numeric.parse(divisor)))
[docs] def mod(self, modulo: Numeric.Input) -> Coin:
"""Creates a new :class:`Coin` with the modulus as amount.
Args:
modulo (Numeric.Input): modulo
Returns:
Coin: modulo
"""
other_amount = Numeric.parse(modulo)
if isinstance(other_amount, Dec):
return Coin(self.denom, Dec(self.amount).mod(other_amount))
else:
return Coin(self.denom, self.amount % other_amount)
def __mod__(self, modulo: Numeric.Input) -> Coin:
return self.mod(modulo)
def __neg__(self) -> Coin:
return Coin(denom=self.denom, amount=(-self.amount))
def __abs__(self) -> Coin:
return Coin(denom=self.denom, amount=abs(self.amount))
def __pos__(self) -> Coin:
return abs(self)
[docs] @classmethod
def from_data(cls, data: dict) -> Coin:
"""Deserializes a :class:`Coin` object from its JSON data representation.
Args:
data (dict): data object
"""
return cls(data["denom"], data["amount"])
[docs] @classmethod
def from_amino(cls, data: dict) -> Coin:
"""Deserializes a :class:`Coin` object from its amino-codec representation.
Args:
data (dict): data object
"""
return cls(data["denom"], data["amount"])