Skip to content

Commit

Permalink
Merge pull request crypto-org-chain#35 from macong-cdc/coin-class
Browse files Browse the repository at this point in the history
Problem: Coin protobuf message is exposed to user directly(fix crypto-org-chain#28)
  • Loading branch information
linfeng-crypto authored Jan 27, 2022
2 parents 06b3988 + 7a6c320 commit 379fa24
Show file tree
Hide file tree
Showing 12 changed files with 479 additions and 17 deletions.
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

## Installing<a name="installing"></a>

Require Python >= 3.7, installing from PyPI repository (https://pypi.org/project/chainlibpy):
Require Python >= 3.8, installing from [PyPI repository](https://pypi.org/project/chainlibpy):

```bash
pip install chainlibpy
Expand Down Expand Up @@ -84,32 +84,36 @@ Please refer to `example/secure_channel_example.py` on how to use secure gRPC ch

## Acknowledgement<a name="acknowledgement"></a>

Thanks [cosmospy](https://github.com/hukkinj1/cosmospy) for the following:
Thanks to [cosmospy](https://github.com/hukkinj1/cosmospy) for the following:

- referenced the packages to sign transaction and create hd wallet
- python lint config file
- use same sign method

Thanks to [eth-utils](https://github.com/ethereum/eth-utils) for the following:

- Conversion of different units without facing precision issues in Python

## Development<a name="development"></a>

### Set up development environment<a name="set-up-development-environment"></a>

More about [poetry](https://python-poetry.org/docs/).

```
```bash
poetry install
```

### Generate gRPC code<a name="generate-grpc-code"></a>

```
```bash
poetry shell
./generated_protos.sh
```

**NOTE:** By default, `master` branch of `cosmos-sdk` is used. Use command below to download a different version:

```
```bash
./generated_protos.sh -COSMOS_REF=v0.44.5
```

Expand All @@ -124,7 +128,7 @@ $COSMOS_SDK_DIR/proto/cosmos/auth/v1beta1/auth.proto
### Tox<a name="tox"></a>
```
```bash
pyenv local 3.8.a 3.9.b
```
Expand All @@ -133,7 +137,7 @@ pyenv local 3.8.a 3.9.b
After this command, a `.python-version` file will be generated at project root directory, which means python versions inside `.python-version` are presented for this project. So running `tox` command with `py{38,39}` configuration should succeed.\
Then run to verify. Command below is recommended to run before pushing a commit.
```sh
```bash
poetry run tox
# or
poetry shell
Expand Down
12 changes: 3 additions & 9 deletions chainlibpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
from .grpc_client import CRO_NETWORK, GrpcClient, NetworkConfig
from .wallet import Wallet

__all__ = [
"Wallet",
"GrpcClient",
"CRO_NETWORK",
"NetworkConfig",
]
from .cro_coin import MAX_CRO_SUPPLY, CROCoin # noqa: F401
from .grpc_client import CRO_NETWORK, GrpcClient, NetworkConfig # noqa: F401
from .wallet import Wallet # noqa: F401
174 changes: 174 additions & 0 deletions chainlibpy/cro_coin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import decimal
from typing import Union

from chainlibpy.generated.cosmos.base.v1beta1.coin_pb2 import Coin
from chainlibpy.grpc_client import NetworkConfig

from .utils import is_integer

MAX_CRO_SUPPLY = 30_000_000_000 # max CRO supply: 30 billion


class CROCoin:
def __init__(
self,
amount: "Union[int, float, str, decimal.Decimal]",
unit: str,
network_config: "NetworkConfig",
) -> None:
"""
Parameters
----------
amount : int, float, str, decimal.Decimal
The amount of coin
To avoid precision issues in Python,
"str" and "decimal.Decimal" types are highly recommended
unit : str
One of coin_denom and coin_base_denom from network_config
network_config : NetworkConfig
Network configuration to interact with the chain
"""
assert unit in [network_config.coin_denom, network_config.coin_base_denom], (
f"unit should be {network_config.coin_denom} or "
f"{network_config.coin_base_denom}, got {unit}"
)

self._denom = network_config.coin_denom
self._base_denom = network_config.coin_base_denom
self._exponent = network_config.exponent
self._unit = unit
self.amount_base = amount

@property
def amount_base(self) -> str:
"""Returns Coin amount only, in base denom unit.
("1cro" is returned as "100000000")
"""
return self._amount_base

@amount_base.setter
def amount_base(self, amount):
temp_base_amount = self._to_number_in_base(amount, self._unit)

if "." in temp_base_amount:
raise ValueError(f"Amount is less than 1{self._base_denom}")
if int(temp_base_amount) > MAX_CRO_SUPPLY * 10 ** self._exponent:
raise ValueError(
"Input is more than maximum cro supply"
f" {MAX_CRO_SUPPLY * 10 ** self._exponent}{self._base_denom}"
f" got {temp_base_amount}{self._base_denom}"
)
if int(temp_base_amount) < 0:
raise ValueError("Amount cannot be negative")

self._amount_base = temp_base_amount

@property
def amount(self) -> str:
"""Returns Coin amount only, in denom unit.
("1cro" is returned as "1")
"""
return self._from_number_in_base(self.amount_base, self._denom)

@property
def amount_with_unit(self) -> str:
"""Returns converted Coin amount with denom unit.
(e.g. "10cro").
"""
return f"{self.amount}{self._denom}"

@property
def amount_base_with_unit(self) -> str:
"""Returns converted Coin amount with base denom unit.
(e.g. "100000basecro").
"""
return f"{self.amount_base}{self._base_denom}"

def __eq__(self, __o: "CROCoin") -> bool:
return self.amount_base == __o.amount_base

def _cast_to_str(self, number: Union[int, float, decimal.Decimal]) -> str:
"""Cast number to string format.
Remove trailing "0" and "." if necessary
"""
s_number = "{:f}".format(number)

if "." in s_number:
s_number = s_number.rstrip("0").rstrip(".")
return s_number

def _cast_to_Decimal_obj(
self, number: Union[int, float, str, "decimal.Decimal"]
) -> "decimal.Decimal":
if is_integer(number) or isinstance(number, str):
d_number = decimal.Decimal(value=number)
elif isinstance(number, float):
d_number = decimal.Decimal(value=str(number))
elif isinstance(number, decimal.Decimal):
d_number = number
else:
raise TypeError("Unsupported type. Must be one of integer, float, or string")

return d_number

def _get_conversion_rate_to_base_unit(self, unit: str) -> decimal.Decimal:
"""Takes a unit and gets its conversion rate to the base unit."""
if unit == self._denom:
return decimal.Decimal(value=10 ** self._exponent)
elif unit == self._base_denom:
return decimal.Decimal(1)
else:
raise ValueError(
f"Expect denom to be {self._denom} or {self._base_denom}, got ${unit}"
)

def _from_number_in_base(self, number: int, unit: str) -> str:
"""Takes an amount of base denom and converts it to an amount of other
denom unit."""
if number == 0:
return "0"

unit_conversion = self._get_conversion_rate_to_base_unit(unit)

with decimal.localcontext() as ctx:
ctx.prec = 999
d_number = decimal.Decimal(value=number, context=ctx)
result_value = d_number / unit_conversion

return self._cast_to_str(result_value)

def _to_number_in_base(
self, number: Union[int, float, str, "decimal.Decimal"], unit: str
) -> str:
"""Takes a number of a unit and converts it to a number of the base
denom."""
d_number = self._cast_to_Decimal_obj(number)

s_number = str(number)
unit_conversion = self._get_conversion_rate_to_base_unit(unit)

if d_number == decimal.Decimal(0):
return "0"

if d_number < 1 and "." in s_number:
with decimal.localcontext() as ctx:
multiplier = len(s_number) - s_number.index(".") - 1
ctx.prec = multiplier
d_number = decimal.Decimal(value=number, context=ctx) * 10 ** multiplier
unit_conversion /= 10 ** multiplier

with decimal.localcontext() as ctx:
ctx.prec = 999
result_value = decimal.Decimal(value=d_number, context=ctx) * unit_conversion

return self._cast_to_str(result_value)

def to_coin_message(self) -> "Coin":
"""Returns protobuf compatiable Coin message."""
return Coin(amount=self.base_amount, denom=self._base_denom)
3 changes: 3 additions & 0 deletions chainlibpy/grpc_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class NetworkConfig:
address_prefix: str
coin_denom: str
coin_base_denom: str
exponent: int
derivation_path: str


Expand All @@ -64,6 +65,7 @@ class NetworkConfig:
address_prefix="cro",
coin_denom="cro",
coin_base_denom="basecro",
exponent=8,
derivation_path="m/44'/394'/0'/0/0",
),
"testnet_croeseid": NetworkConfig(
Expand All @@ -72,6 +74,7 @@ class NetworkConfig:
address_prefix="tcro",
coin_denom="tcro",
coin_base_denom="basetcro",
exponent=8,
derivation_path="m/44'/1'/0'/0/0",
),
}
Expand Down
1 change: 1 addition & 0 deletions chainlibpy/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .types import is_integer # noqa: F401
5 changes: 5 additions & 0 deletions chainlibpy/utils/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import Any


def is_integer(value: Any) -> bool:
return isinstance(value, int) and not isinstance(value, bool)
46 changes: 45 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pytest-env = "^0.6.2"
pystarport = "^0.2.3"
requests = "^2.27.1"
toml = "^0.10.2"
hypothesis = "^6.35.1"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,6 @@ def local_test_network_config(data_folder: "Path", chain_id):
address_prefix="cro",
coin_denom="cro",
coin_base_denom="basecro",
exponent=8,
derivation_path="m/44'/394'/0'/0/0",
)
Loading

0 comments on commit 379fa24

Please sign in to comment.