diff --git a/.bumpversion.cfg b/.bumpversion.cfg
deleted file mode 100644
index f60d086..0000000
--- a/.bumpversion.cfg
+++ /dev/null
@@ -1,17 +0,0 @@
-[bumpversion]
-commit = True
-tag = True
-tag_name = {new_version}
-current_version = 1.0.0
-
-[bumpversion:file:pyproject.toml]
-search = version = "{current_version}" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT
-replace = version = "{new_version}" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT
-
-[bumpversion:file:chainlibpy/__init__.py]
-search = __version__ = "{current_version}"
-replace = __version__ = "{new_version}"
-
-[bumpversion:file:README.md]
-search = > Version {current_version}
-replace = > Version {new_version}
\ No newline at end of file
diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 78d4090..0000000
--- a/.coveragerc
+++ /dev/null
@@ -1,4 +0,0 @@
-[run]
-omit =
- tests/*
- */site-packages/*
diff --git a/.mypy.ini b/.mypy.ini
index d692dae..985d214 100644
--- a/.mypy.ini
+++ b/.mypy.ini
@@ -1,4 +1,11 @@
[mypy]
+# NOTE!
+# exclude, files config does not work for https://github.com/pre-commit/mirrors-mypy configuration
+exclude = (?x)(
+ example/
+ | chainlibpy/generated/
+ | chainlibpy/amino/ # TODO to fix type errors in this directory
+ ) # leave two spaces before ")" to prevent parsing error
warn_unreachable = True
warn_unused_ignores = True
warn_redundant_casts = True
@@ -11,6 +18,12 @@ strict_equality = True
implicit_reexport = False
no_implicit_optional = True
+[mypy-chainlibpy.generated.*]
+ignore_missing_imports = True
+
+[mypy-pystarport.*]
+ignore_missing_imports = True
+
[mypy-tests.*]
disallow_untyped_defs = False
@@ -19,3 +32,6 @@ ignore_missing_imports = True
[mypy-mnemonic.*]
ignore_missing_imports = True
+
+[mypy-bech32.*]
+ignore_missing_imports = True
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index cbf17c4..c54cd01 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,48 +1,72 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: 9136088a246768144165fcc3ecc3d31bb686920a # frozen: v3.3.0
+ rev: v4.1.0
hooks:
- id: check-yaml
- id: check-toml
+
- repo: https://github.com/pre-commit/pygrep-hooks
- rev: 4f4c0a4cda27980be153cca2cb7710c9fec57ba3 # frozen: v1.7.0
+ rev: v1.9.0
hooks:
- id: python-use-type-annotations
- id: python-check-blanket-noqa
-- repo: https://github.com/timothycrosley/isort
- rev: 6bb47b7acc1554ecb59d2855e9110c447162f674 # frozen: 5.6.4
+
+- repo: https://github.com/pycqa/isort
+ rev: 5.10.1
hooks:
- id: isort
+
- repo: https://github.com/psf/black
- rev: e66be67b9b6811913470f70c28b4d50f94d05b22 # frozen: 20.8b1
+ rev: 21.12b0
hooks:
- id: black
+ exclude: ^chainlibpy/generated/
+
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: f3bfcb5479b4fa73b3fbb95a6390420575f20b51 # frozen: v0.790
+ rev: v0.931
hooks:
- id: mypy
- args:
- # Suppress errors resulting from no access to dependencies
- - --ignore-missing-imports
- - --no-warn-unused-ignores
- # Allow multiple scripts (no .py postfix in name) to be checked in a single mypy invocation
- - --scripts-are-modules
+ # NOTE: this hook does NOT read "files" and "exclude" configs from mypy configuration files
+ files: ^chainlibpy/
+ exclude: ^chainlibpy/(generated/|amino/) # TODO to fix type errors in amino directory
+ # NOTE: need to add additional_dependencies explicitly
+ additional_dependencies:
+ - grpc-stubs==1.24.7
+ - types-PyYAML==6.0.4
+ - types-protobuf==3.19.8
+ - types-requests==2.27.8
+ - types-toml==0.10.3
+
- repo: https://gitlab.com/pycqa/flake8
- rev: bb6a530e28acab8d3551043b3e8709db8bcbac6b # frozen: 3.8.4
+ rev: 4.0.1
hooks:
- id: flake8
additional_dependencies:
- flake8-bugbear
- - flake8-builtins
+ # TODO FIX chainlibpy/amino/message.py:284:5: A003 class attribute "id" is shadowing a python builtin
+ # when enable flake8-builtins option
+ # - flake8-builtins
- flake8-comprehensions
+
- repo: https://github.com/myint/docformatter
- rev: de0bf8fa254d25a01383fecdb6335bea01daeae3 # frozen: v1.3.1
+ rev: v1.4
hooks:
- id: docformatter
+ args:
+ - ./chainlibpy
+ - --recursive
+ - --in-place
+ - --exclude
+ - chainlibpy/generated
+
- repo: https://github.com/executablebooks/mdformat
- rev: 492440cdb4f3ca87eff24dea09c85881b0e5d597 # frozen: 0.4.0
+ rev: 0.7.13
hooks:
- id: mdformat
+ args:
+ - CHANGELOG.md
+ - CONTRIBUTING.md
+ - README.md
additional_dependencies:
- mdformat-black
- mdformat-toc
diff --git a/CHANGELOG.md b/CHANGELOG.md
index efa66d5..fb3712c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,14 +4,17 @@ This log documents all public API breaking backwards incompatible changes.
## 2.2.0 - to be released
-[#32](https://github.com/crypto-org-chain/chainlibpy/issues/32) Add test environment\
-Require Python >= 3.8\
-[#25](https://github.com/crypto-org-chain/chainlibpy/issues/25) Add mainnet and testnet-croeseid-4 network configurations\
-[#24](https://github.com/crypto-org-chain/chainlibpy/pull/24) Fix unable to use secure gRPC channel to interact with chain
+[#26](https://github.com/crypto-org-chain/chainlibpy/issues/26) [#27](https://github.com/crypto-org-chain/chainlibpy/issues/27) [#28](https://github.com/crypto-org-chain/chainlibpy/issues/28) Refactor protobuf message to class to add more functionalities and hide protobuf complexity
+
+[#32](https://github.com/crypto-org-chain/chainlibpy/issues/32) Add test environment
+
+Require Python >= 3.8
-*Dec 7, 2021*
+[#25](https://github.com/crypto-org-chain/chainlibpy/issues/25) Add mainnet and testnet-croeseid-4 network configurations
+
+[#24](https://github.com/crypto-org-chain/chainlibpy/pull/24) Fix unable to use secure gRPC channel to interact with chain
-## 2.1.0
+## 2.1.0 - 7/Dec/2021
[#28](https://github.com/crypto-org-chain/chainlibpy/pull/21) Migrate to gRPC which supports `chain-main` using `Cosmos SDK` version v0.43/0.44
diff --git a/README.md b/README.md
index d2d13f4..8415705 100644
--- a/README.md
+++ b/README.md
@@ -13,10 +13,11 @@
- [Usage](#usage)
- [Generating a wallet](#generating-a-wallet)
- [Signing and broadcasting a transaction](#signing-and-broadcasting-a-transaction)
- - [Using secure gRPC channel](#using-secure-grpc-channel)
+ - [Interact with mainnet or testnet](#interact-with-mainnet-or-testnet)
- [Acknowledgement](#acknowledgement)
- [Development](#development)
- [Set up development environment](#set-up-development-environment)
+ - [Add pre-commit git hook](#add-pre-commit-git-hook)
- [Generate gRPC code](#generate-grpc-code)
- [Tox](#tox)
@@ -50,37 +51,21 @@ print(wallet.address)
### Signing and broadcasting a transaction
-```python
-from chainlibpy.generated.cosmos.base.v1beta1.coin_pb2 import Coin
-from chainlibpy.grpc_client import GrpcClient
-from chainlibpy.transaction import sign_transaction
-from chainlibpy.wallet import Wallet
-
-# Refer to example/transaction.py for how to obtain CONSTANT values below
-DENOM = "basecro"
-MNEMONIC_PHRASE = "first ... last"
-TO_ADDRESS = "cro...add"
-AMOUNT = [Coin(amount="10000", denom=DENOM)]
-CHAIN_ID = "chainmaind"
-GRPC_ENDPOINT = "0.0.0.0:26653"
-
-wallet = Wallet(MNEMONIC_PHRASE)
-client = GrpcClient(wallet, CHAIN_ID, GRPC_ENDPOINT)
-
-from_address = wallet.address
-account_number = client.query_account_data(wallet.address).account_number
-
-msg = client.get_packed_send_msg(wallet.address, TO_ADDRESS, AMOUNT)
-tx = client.generate_tx([msg], [wallet.address], [wallet.public_key])
-sign_transaction(tx, wallet.private_key, CHAIN_ID, account_number)
-client.broadcast_tx(tx)
-```
+Please refer to `example/transaction.py` for how to start a local testnet with `pystarport` and change information below to run the examples successfully.
-You may also refer to `example/transaction.py` on how to use a high level function `bank_send()` to sign and broadcast a transaction
+```diff
+# Obtained from {directory_started_pystarport}/data/chainmaind/accounts.json
+# To recover one of the genesis account
+- MNEMONIC_PHRASE = "first ... last"
++ MNEMONIC_PHRASE = "REMEMBER TO CHANGE"
+# Obtained from {directory_started_pystarport}/data/chainmaind/accounts.json
+- TO_ADDRESS = "cro...add"
++ TO_ADDRESS = "REMEMBER TO CHANGE"
+```
-### Using secure gRPC channel
+### Interact with mainnet or testnet
-Please refer to `example/secure_channel_example.py` on how to use secure gRPC channel with server certificate
+Please refer to `example/secure_channel_example.py` on how to use secure gRPC channel with server certificate to interact with mainnet or testnet.
## Acknowledgement
@@ -98,12 +83,20 @@ Thanks to [eth-utils](https://github.com/ethereum/eth-utils) for the following:
### Set up development environment
-More about [poetry](https://python-poetry.org/docs/).
+Run command below to install dependencies (more about [poetry](https://python-poetry.org/docs/)):
```bash
poetry install
```
+### Add pre-commit git hook
+
+To set up the git hook scripts, so that [`pre-commit`](https://pre-commit.com/) will run automatically on `git commit`:
+
+```bash
+pre-commit install
+```
+
### Generate gRPC code
```bash
@@ -117,25 +110,31 @@ poetry shell
./generated_protos.sh -COSMOS_REF=v0.44.5
```
-If more generated gRPC code is needed in the future, please add the `.proto` files needed here in `generated_protos.sh`:
+If more generated gRPC code is needed in the future, please add the path to `.proto` file needed here in `generated_protos.sh`:
-```bash
+```diff
# Add .proto files here to generate respective gRPC code
PROTO_FILES="
$COSMOS_SDK_DIR/proto/cosmos/auth/v1beta1/auth.proto
++$COSMOS_SDK_DIR/proto/other.proto
...
```
### Tox
+[Tox](https://tox.wiki/en/latest/) is a tool to automate and standardize testing processes in Python.
+
+For this project, the list of environment that will be run when invoking `tox` command is `py{38,39}`. Hence we need to set up Python 3.8 and 3.9 for this project. Run command below to set a local application-specific Python version (in this case 3.8 and 3.9) with [pyenv](https://github.com/pyenv/pyenv):
+
```bash
pyenv local 3.8.a 3.9.b
```
-`a` and `b` are python versions installed on your computer by `pyenv`. More about [pyenv](https://github.com/pyenv/pyenv).
+**Note:** `a` and `b` are python versions installed on your computer by `pyenv`.
+
+After running command above, a `.python-version` file will be generated, which means python versions inside `.python-version` are presented for this project. Now, running command `tox` should succeed without prompting environment missing error.
-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.
+Run command below to verify:
```bash
poetry run tox
@@ -143,3 +142,5 @@ poetry run tox
poetry shell
tox
```
+
+It is also recommended to run `tox` command before pushing a commit.
diff --git a/chainlibpy/__init__.py b/chainlibpy/__init__.py
index 95dcf2d..c65fd04 100644
--- a/chainlibpy/__init__.py
+++ b/chainlibpy/__init__.py
@@ -1,3 +1,14 @@
-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
+from .cro_coin import MAX_CRO_SUPPLY, CROCoin
+from .grpc_client import CRO_NETWORK, GrpcClient, NetworkConfig
+from .transaction import Transaction
+from .wallet import Wallet
+
+__all__ = [
+ "CROCoin",
+ "MAX_CRO_SUPPLY",
+ "CRO_NETWORK",
+ "GrpcClient",
+ "NetworkConfig",
+ "Transaction",
+ "Wallet",
+]
diff --git a/chainlibpy/cro_coin.py b/chainlibpy/cro_coin.py
index 47345d8..44881e1 100644
--- a/chainlibpy/cro_coin.py
+++ b/chainlibpy/cro_coin.py
@@ -1,5 +1,5 @@
import decimal
-from typing import Union
+from typing import Dict, Union
from chainlibpy.generated.cosmos.base.v1beta1.coin_pb2 import Coin
from chainlibpy.grpc_client import NetworkConfig
@@ -38,7 +38,9 @@ def __init__(
self._base_denom = network_config.coin_base_denom
self._exponent = network_config.exponent
self._unit = unit
- self.amount_base = amount
+ self._network_config = network_config
+ self.amount_base = amount # type:ignore
+ # pending https://github.com/python/mypy/issues/3004 to remove above type:ignore
@property
def amount_base(self) -> str:
@@ -49,7 +51,7 @@ def amount_base(self) -> str:
return self._amount_base
@amount_base.setter
- def amount_base(self, amount):
+ def amount_base(self, amount: Union[int, float, str, "decimal.Decimal"]) -> None:
temp_base_amount = self._to_number_in_base(amount, self._unit)
if "." in temp_base_amount:
@@ -89,7 +91,9 @@ def amount_base_with_unit(self) -> str:
"""
return f"{self.amount_base}{self._base_denom}"
- def __eq__(self, __o: "CROCoin") -> bool:
+ def __eq__(self, __o: object) -> bool:
+ if not isinstance(__o, CROCoin):
+ return NotImplemented
return self.amount_base == __o.amount_base
def _cast_to_str(self, number: Union[int, float, decimal.Decimal]) -> str:
@@ -128,10 +132,10 @@ def _get_conversion_rate_to_base_unit(self, unit: str) -> decimal.Decimal:
f"Expect denom to be {self._denom} or {self._base_denom}, got ${unit}"
)
- def _from_number_in_base(self, number: int, unit: str) -> str:
+ def _from_number_in_base(self, number: str, unit: str) -> str:
"""Takes an amount of base denom and converts it to an amount of other
denom unit."""
- if number == 0:
+ if number == "0":
return "0"
unit_conversion = self._get_conversion_rate_to_base_unit(unit)
@@ -169,6 +173,32 @@ def _to_number_in_base(
return self._cast_to_str(result_value)
- def to_coin_message(self) -> "Coin":
+ @property
+ def protobuf_coin_message(self) -> "Coin":
"""Returns protobuf compatiable Coin message."""
- return Coin(amount=self.base_amount, denom=self._base_denom)
+ return Coin(amount=self.amount_base, denom=self._base_denom)
+
+ @property
+ def amino_coin_message(self) -> Dict[str, str]:
+ """Returns json amino compatiable Coin message."""
+ return {"amount": self.amount_base, "denom": self._base_denom}
+
+ def __add__(self, __o: object) -> "CROCoin":
+ if not isinstance(__o, CROCoin):
+ return NotImplemented
+
+ with decimal.localcontext() as ctx:
+ ctx.prec = 999
+ result_value = decimal.Decimal(self.amount_base) + decimal.Decimal(__o.amount_base)
+
+ return type(self)(result_value, self._base_denom, self._network_config)
+
+ def __sub__(self, __o: object) -> "CROCoin":
+ if not isinstance(__o, CROCoin):
+ return NotImplemented
+
+ with decimal.localcontext() as ctx:
+ ctx.prec = 999
+ result_value = decimal.Decimal(self.amount_base) - decimal.Decimal(__o.amount_base)
+
+ return type(self)(result_value, self._base_denom, self._network_config)
diff --git a/chainlibpy/grpc_client.py b/chainlibpy/grpc_client.py
index 8508b1b..b10eb28 100644
--- a/chainlibpy/grpc_client.py
+++ b/chainlibpy/grpc_client.py
@@ -1,12 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-import time
from dataclasses import dataclass
-from typing import List, Optional
+from typing import Literal, Optional
-from google.protobuf.any_pb2 import Any as ProtoAny
-from grpc import ChannelCredentials, RpcError, insecure_channel, secure_channel
+from grpc import ChannelCredentials, insecure_channel, secure_channel
from chainlibpy.generated.cosmos.auth.v1beta1.auth_pb2 import BaseAccount
from chainlibpy.generated.cosmos.auth.v1beta1.query_pb2 import QueryAccountRequest
@@ -22,29 +20,13 @@
from chainlibpy.generated.cosmos.bank.v1beta1.query_pb2_grpc import (
QueryStub as BankGrpcClient,
)
-from chainlibpy.generated.cosmos.bank.v1beta1.tx_pb2 import MsgSend
-from chainlibpy.generated.cosmos.base.v1beta1.coin_pb2 import Coin
-from chainlibpy.generated.cosmos.crypto.secp256k1.keys_pb2 import PubKey as ProtoPubKey
-from chainlibpy.generated.cosmos.tx.signing.v1beta1.signing_pb2 import SignMode
from chainlibpy.generated.cosmos.tx.v1beta1.service_pb2 import (
BroadcastMode,
BroadcastTxRequest,
- GetTxRequest,
- GetTxResponse,
)
from chainlibpy.generated.cosmos.tx.v1beta1.service_pb2_grpc import (
ServiceStub as TxGrpcClient,
)
-from chainlibpy.generated.cosmos.tx.v1beta1.tx_pb2 import (
- AuthInfo,
- Fee,
- ModeInfo,
- SignerInfo,
- Tx,
- TxBody,
-)
-from chainlibpy.transaction import sign_transaction
-from chainlibpy.wallet import Wallet
@dataclass
@@ -81,13 +63,10 @@ class NetworkConfig:
class GrpcClient:
- DEFAULT_GAS_LIMIT = 200000
-
def __init__(
self,
- wallet: Wallet,
network: NetworkConfig,
- credentials: ChannelCredentials = None,
+ credentials: Optional[ChannelCredentials] = None,
) -> None:
if credentials is None:
channel = insecure_channel(network.grpc_endpoint)
@@ -97,26 +76,55 @@ def __init__(
self.bank_client = BankGrpcClient(channel)
self.tx_client = TxGrpcClient(channel)
self.auth_client = AuthGrpcClient(channel)
- self.wallet = wallet
self.chain_id = network.chain_id
-
- # TODO to remove when removing wallet parameter from this class
- try:
- account = self.query_account_data(self.wallet.address)
- self.account_number = account.account_number
- except RpcError:
- # TODO dummy code, to remove when removing wallet parameter from this class
- self.account_number = 0
+ self.network = network
def query_bank_denom_metadata(self, denom: str) -> QueryDenomMetadataResponse:
+ """Queries metadata of a given coin denomination.
+
+ Args:
+ denom (str): raw transaction
+
+ Returns:
+ QueryDenomMetadataResponse: cosmos.bank.v1beta1.QueryDenomMetadataResponse message
+ """
res = self.bank_client.DenomMetadata(QueryDenomMetadataRequest(denom=denom))
return res
- def get_balance(self, address: str, denom: str) -> QueryBalanceResponse:
- res = self.bank_client.Balance(QueryBalanceRequest(address=address, denom=denom))
+ def query_account_balance(self, address: str) -> QueryBalanceResponse:
+ """Queries the balance of an address in base denomination.
+
+ Args:
+ address (str): address to query balance
+
+ Returns:
+ QueryBalanceResponse: cosmos.bank.v1beta1.QueryBalanceResponse message
+
+ Access `amount` by `.balance.amount`
+
+ Access `denom` by `.balance.denom`
+ """
+ res = self.bank_client.Balance(
+ QueryBalanceRequest(address=address, denom=self.network.coin_base_denom)
+ )
return res
- def query_account_data(self, address: str) -> BaseAccount:
+ def query_account(self, address: str) -> BaseAccount:
+ """Queries the account information.
+
+ Args:
+ address (str): address to query account
+
+ Raises:
+ TypeError: account associated with address is not `BaseAccount`
+
+ Returns:
+ BaseAccount: cosmos.auth.v1beta1.BaseAccount message
+
+ Access `account_number` by `.account_number`
+
+ Access `sequence` by `.sequence`
+ """
account_response = self.auth_client.Account(QueryAccountRequest(address=address))
account = BaseAccount()
if account_response.account.Is(BaseAccount.DESCRIPTOR):
@@ -125,82 +133,31 @@ def query_account_data(self, address: str) -> BaseAccount:
raise TypeError("Unexpected account type")
return account
- def generate_tx(
- self,
- packed_msgs: List[ProtoAny],
- from_addresses: List[str],
- pub_keys: List[bytes],
- fee: Optional[List[Coin]] = None,
- memo: str = "",
- gas_limit: int = DEFAULT_GAS_LIMIT,
- ) -> Tx:
- accounts: List[BaseAccount] = []
- signer_infos: List[SignerInfo] = []
- for from_address, pub_key in zip(from_addresses, pub_keys):
- account = self.query_account_data(from_address)
- accounts.append(account)
- signer_infos.append(self._get_signer_info(account, pub_key))
-
- auth_info = AuthInfo(
- signer_infos=signer_infos,
- fee=Fee(amount=fee, gas_limit=gas_limit),
- )
-
- tx_body = TxBody()
- tx_body.memo = memo
- tx_body.messages.extend(packed_msgs)
-
- tx = Tx(body=tx_body, auth_info=auth_info)
- return tx
-
- def sign_tx(self, tx: Tx):
- sign_transaction(tx, self.wallet.private_key, self.chain_id, self.account_number)
-
- def get_packed_send_msg(
- self, from_address: str, to_address: str, amount: List[Coin]
- ) -> ProtoAny:
- msg_send = MsgSend(from_address=from_address, to_address=to_address, amount=amount)
- send_msg_packed = ProtoAny()
- send_msg_packed.Pack(msg_send, type_url_prefix="/")
-
- return send_msg_packed
-
- def broadcast_tx(self, tx: Tx, wait_time: int = 10) -> GetTxResponse:
- tx_data = tx.SerializeToString()
- broad_tx_req = BroadcastTxRequest(tx_bytes=tx_data, mode=BroadcastMode.BROADCAST_MODE_SYNC)
- broad_tx_resp = self.tx_client.BroadcastTx(broad_tx_req)
+ def broadcast_transaction(
+ self, tx_byte: bytes, mode: Literal["sync", "async", "block"] = "block"
+ ) -> None:
+ """Broadcasts raw transaction in a mode.
- if broad_tx_resp.tx_response.code != 0:
- raw_log = broad_tx_resp.tx_response.raw_log
- raise RuntimeError(f"Transaction failed: {raw_log}")
+ sync mode: client waits for a CheckTx execution response only
- time.sleep(wait_time)
+ async mode: client returns immediately
- tx_request = GetTxRequest(hash=broad_tx_resp.tx_response.txhash)
- tx_response = self.tx_client.GetTx(tx_request)
+ block mode(default): client waits for the tx to be committed in a block
- return tx_response
+ Args:
+ tx_byte (bytes): raw transaction
+ mode (Literal[, optional): broadcast mode. Defaults to "block".
- def bank_send(self, to_address: str, amount: List[Coin]) -> GetTxResponse:
- msg = self.get_packed_send_msg(
- from_address=self.wallet.address, to_address=to_address, amount=amount
- )
+ Raises:
+ TypeError: mode is not one of "sync", "async" or "block"
+ """
+ if mode == "sync":
+ _mode = BroadcastMode.BROADCAST_MODE_SYNC
+ elif mode == "async":
+ _mode = BroadcastMode.BROADCAST_MODE_ASYNC
+ elif mode == "block":
+ _mode = BroadcastMode.BROADCAST_MODE_BLOCK
+ else:
+ raise TypeError("Unexcepted mode, should be [sync, async, block]")
- tx = self.generate_tx([msg], [self.wallet.address], [self.wallet.public_key])
- self.sign_tx(tx)
- return self.broadcast_tx(tx)
-
- def _get_signer_info(self, from_acc: BaseAccount, pub_key: bytes) -> SignerInfo:
- from_pub_key_packed = ProtoAny()
- from_pub_key_pb = ProtoPubKey(key=pub_key)
- from_pub_key_packed.Pack(from_pub_key_pb, type_url_prefix="/")
-
- # Prepare auth info
- single = ModeInfo.Single(mode=SignMode.SIGN_MODE_DIRECT)
- mode_info = ModeInfo(single=single)
- signer_info = SignerInfo(
- public_key=from_pub_key_packed,
- mode_info=mode_info,
- sequence=from_acc.sequence,
- )
- return signer_info
+ self.tx_client.BroadcastTx(BroadcastTxRequest(tx_bytes=tx_byte, mode=_mode))
diff --git a/chainlibpy/transaction.py b/chainlibpy/transaction.py
index d41e5a7..b10754f 100644
--- a/chainlibpy/transaction.py
+++ b/chainlibpy/transaction.py
@@ -2,31 +2,143 @@
# -*- coding: utf-8 -*-
-import hashlib
-
-import ecdsa
-
-from chainlibpy.generated.cosmos.tx.v1beta1.tx_pb2 import SignDoc, Tx
-
-
-def sign_transaction(
- tx: Tx,
- private_key: bytes,
- chain_id: str,
- account_number: int,
-):
- sd = SignDoc()
- sd.body_bytes = tx.body.SerializeToString()
- sd.auth_info_bytes = tx.auth_info.SerializeToString()
- sd.chain_id = chain_id
- sd.account_number = account_number
-
- data_for_signing = sd.SerializeToString()
-
- signing_key = ecdsa.SigningKey.from_string(
- private_key, curve=ecdsa.SECP256k1, hashfunc=hashlib.sha256
- )
- signature = signing_key.sign_deterministic(
- data_for_signing, sigencode=ecdsa.util.sigencode_string_canonize
- )
- tx.signatures.extend([signature])
+from typing import List, Optional
+
+from google.protobuf import any_pb2, message
+
+from chainlibpy.generated.cosmos.base.v1beta1.coin_pb2 import Coin
+from chainlibpy.generated.cosmos.crypto.secp256k1.keys_pb2 import PubKey
+from chainlibpy.generated.cosmos.tx.signing.v1beta1.signing_pb2 import SignMode
+from chainlibpy.generated.cosmos.tx.v1beta1.tx_pb2 import (
+ AuthInfo,
+ Fee,
+ ModeInfo,
+ SignDoc,
+ SignerInfo,
+ Tx,
+ TxBody,
+)
+from chainlibpy.grpc_client import GrpcClient
+from chainlibpy.utils import pack_to_any_message
+from chainlibpy.wallet import Wallet
+
+DEFAULT_GAS_LIMIT = 200_000
+
+
+class Transaction:
+ def __init__(
+ self,
+ chain_id: str,
+ from_wallets: List[Wallet],
+ msgs: List[message.Message],
+ account_number: int,
+ client: "GrpcClient",
+ gas_limit: int = DEFAULT_GAS_LIMIT,
+ fee: Optional[List[Coin]] = None,
+ memo: str = "",
+ timeout_height: Optional[int] = None,
+ ) -> None:
+ """Transaction class to prepare unsigned transaction and generate
+ signed transaction with signatures.
+
+ Args:
+ chain_id (str): chain id this transaction targets
+
+ from_wallets (List[Wallet]): wallets for the authorization
+ related content of the transaction
+
+ msgs (List[message.Message]): messages to be included in this transaction
+
+ account_number (int): account number of the account in state
+
+ client (GrpcClient): GrpcClient object to connect to chain
+
+ gas_limit (int, optional): maximum gas can be used in transaction processing.
+ Defaults to DEFAULT_GAS_LIMIT.
+
+ fee (Optional[List[Coin]], optional): amount of coins to be paid as a fee.
+ Defaults to None.
+
+ memo (str, optional): note to be added to the transaction. Defaults to "".
+
+ timeout_height (int, optional): this transaction will not be processed
+ after timeout height. Defaults to None.
+ """
+ self._chain_id = chain_id
+ self._packed_msgs = self._pack_msgs_to_any_msgs(msgs)
+ self._fee = fee
+ self._from_wallets = from_wallets
+ self._memo = memo
+ self._timeout_height = timeout_height
+ self._account_number = account_number
+ self._gas_limit = gas_limit
+ self._client = client
+
+ def _pack_msgs_to_any_msgs(self, msgs: List[message.Message]) -> List[any_pb2.Any]:
+ return [pack_to_any_message(msg) for msg in msgs]
+
+ def append_message(self, *msgs: message.Message) -> "Transaction":
+ """Append more messages in this transaction.
+
+ Args:
+ *msgs (message.Message): messages to be included in this transaction
+
+ Returns:
+ Transaction: transaction object with newly added messages
+ """
+ self._packed_msgs.extend(self._pack_msgs_to_any_msgs(list(msgs)))
+
+ return self
+
+ def set_signatures(self, *signatures: bytes) -> "Transaction":
+ """Set signatures for this transaction.
+
+ Args:
+ *signatures (bytes): signatures to be included in this transaction
+
+ Returns:
+ Transaction: transaction object with newly added signatures
+ """
+
+ self._signatures = list(signatures)
+
+ return self
+
+ @property
+ def tx_body(self) -> TxBody:
+ return TxBody(messages=self._packed_msgs, memo=self._memo)
+
+ @property
+ def auth_info(self) -> AuthInfo:
+ signer_infos = []
+ for wallet in self._from_wallets:
+ # query account to get the latest account.sequence
+ account = self._client.query_account(wallet.address)
+
+ signer_info = SignerInfo(
+ public_key=pack_to_any_message(PubKey(key=wallet.public_key)),
+ mode_info=ModeInfo(single=ModeInfo.Single(mode=SignMode.SIGN_MODE_DIRECT)),
+ sequence=account.sequence,
+ )
+
+ signer_infos.append(signer_info)
+
+ return AuthInfo(
+ signer_infos=signer_infos, fee=Fee(amount=self._fee, gas_limit=self._gas_limit)
+ )
+
+ @property
+ def sign_doc(self) -> SignDoc:
+ return SignDoc(
+ body_bytes=self.tx_body.SerializeToString(),
+ auth_info_bytes=self.auth_info.SerializeToString(),
+ chain_id=self._chain_id,
+ account_number=self._account_number,
+ )
+
+ @property
+ def signed_tx(self) -> Tx:
+ if self._signatures is None:
+ raise TypeError("Set signatures first before getting signed_tx")
+
+ return Tx(body=self.tx_body, auth_info=self.auth_info, signatures=self._signatures)
diff --git a/chainlibpy/utils/__init__.py b/chainlibpy/utils/__init__.py
index 48eedb2..b228de0 100644
--- a/chainlibpy/utils/__init__.py
+++ b/chainlibpy/utils/__init__.py
@@ -1 +1,7 @@
-from .types import is_integer # noqa: F401
+from .protobuf_utils import pack_to_any_message
+from .types import is_integer
+
+__all__ = [
+ "is_integer",
+ "pack_to_any_message",
+]
diff --git a/chainlibpy/utils/protobuf_utils.py b/chainlibpy/utils/protobuf_utils.py
new file mode 100644
index 0000000..f286399
--- /dev/null
+++ b/chainlibpy/utils/protobuf_utils.py
@@ -0,0 +1,19 @@
+from google.protobuf import any_pb2, message
+
+
+def pack_to_any_message(msg: message.Message) -> any_pb2.Any:
+ """Packs a protobuf Message type to protobuf Any type.
+
+ Args:
+ msg (message.Message): protobuf Message to be packed
+
+ Returns:
+ any_pb2.Any: to be used for `google.protobuf.Any` type
+ """
+
+ assert isinstance(msg, message.Message), "Wrong type"
+
+ packed_any = any_pb2.Any()
+ packed_any.Pack(msg, type_url_prefix="/")
+
+ return packed_any
diff --git a/chainlibpy/wallet.py b/chainlibpy/wallet.py
index 79f2073..5f09ec5 100644
--- a/chainlibpy/wallet.py
+++ b/chainlibpy/wallet.py
@@ -13,13 +13,15 @@
class Wallet:
- def __init__(self, seed: str, path=DEFAULT_DERIVATION_PATH, hrp=DEFAULT_BECH32_HRP):
+ def __init__(
+ self, seed: str, path: str = DEFAULT_DERIVATION_PATH, hrp: str = DEFAULT_BECH32_HRP
+ ):
self.seed = seed
self.path = path
self.hrp = hrp
@classmethod
- def new(cls, path=DEFAULT_DERIVATION_PATH, hrp=DEFAULT_BECH32_HRP):
+ def new(cls, path: str = DEFAULT_DERIVATION_PATH, hrp: str = DEFAULT_BECH32_HRP) -> "Wallet":
seed = Mnemonic(language="english").generate(strength=256)
return Wallet(seed, path, hrp)
@@ -52,3 +54,26 @@ def address(self) -> str:
five_bit_r = bech32.convertbits(r, 8, 5)
assert five_bit_r is not None, "Unsuccessful bech32.convertbits call"
return bech32.bech32_encode(self.hrp, five_bit_r)
+
+ def sign(self, msg: bytes) -> bytes:
+ """Sign the input msg with wallet's private key.
+
+ Args:
+ msg (bytes): msg to be signed,
+ use `.SerializeToString()` method to the original protobuf message type
+
+ Returns:
+ bytes: signature of the input msg
+
+ Raises:
+ AssertionError: Incorrect msg type
+ """
+ assert isinstance(msg, bytes), "Wrong Type"
+
+ signing_key = ecdsa.SigningKey.from_string(
+ self.private_key, curve=ecdsa.SECP256k1, hashfunc=hashlib.sha256
+ )
+
+ return signing_key.sign_deterministic(
+ msg, sigencode=ecdsa.util.sigencode_string_canonize, hashfunc=hashlib.sha256
+ )
diff --git a/example/local_testnet_configs/default.yaml b/example/local_testnet_configs/default.yaml
new file mode 100644
index 0000000..a9709eb
--- /dev/null
+++ b/example/local_testnet_configs/default.yaml
@@ -0,0 +1,23 @@
+chaintest:
+ validators:
+ - coins: 10cro
+ staked: 10cro
+ - coins: 10cro
+ staked: 10cro
+ accounts:
+ - name: community
+ coins: 100cro
+ - name: ecosystem
+ coins: 200cro
+ - name: reserve
+ coins: 200cro
+ vesting: "60s"
+ - name: alice
+ coins: 5000cro
+ - name: bob
+ coins: 5000cro
+ genesis:
+ app_state:
+ staking:
+ params:
+ unbonding_time: "10s"
diff --git a/example/secure_channel_example.py b/example/secure_channel_example.py
index 323923f..1cc1bdf 100644
--- a/example/secure_channel_example.py
+++ b/example/secure_channel_example.py
@@ -1,58 +1,46 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+import socket
import ssl
import grpc
-from chainlibpy.grpc_client import GrpcClient
-from chainlibpy.wallet import Wallet
-
-DENOM = "basetcro"
-MNEMONIC_PHRASE = "first ... last"
-CHAIN_ID = "testnet-croeseid-4"
-
-SERVER_HOST = "testnet-croeseid-4.crypto.org"
-SERVER_PORT = "9090"
-GRPC_ENDPOINT = f"{SERVER_HOST}:{SERVER_PORT}"
-
-DEFAULT_DERIVATION_PATH = "m/44'/1'/0'/0/0"
-DEFAULT_BECH32_HRP = "tcro"
+from chainlibpy import CRO_NETWORK, GrpcClient
def example_with_certificate_file():
- wallet = Wallet(MNEMONIC_PHRASE, DEFAULT_DERIVATION_PATH, DEFAULT_BECH32_HRP)
-
# 1. .cer certificate file could be obtained from the browser
- # more details could be found here https://stackoverflow.com/questions/25940396/how-to-export-certificate-from-chrome-on-a-mac/59466184#59466184 # noqa501
+ # more details could be found here https://stackoverflow.com/questions/25940396/how-to-export-certificate-from-chrome-on-a-mac/59466184#59466184 # noqa: 501
# 2. convert .cer file to .crt file
- # `openssl x509 -inform DER -in cert.cer -out cert.crt``
+ # `openssl x509 -inform DER -in cert.cer -out cert.crt`
with open("./cert.crt", "rb") as f:
creds = grpc.ssl_channel_credentials(f.read())
- client = GrpcClient(wallet, CHAIN_ID, GRPC_ENDPOINT, creds)
+ client = GrpcClient(CRO_NETWORK["testnet_croeseid"], creds)
- from_address = wallet.address
- res = client.get_balance(from_address, DENOM)
- print(f"address {from_address} initial balance: {res.balance.amount}")
+ print(client.query_bank_denom_metadata(CRO_NETWORK["testnet_croeseid"].coin_base_denom))
def example_with_certificate_request():
- wallet = Wallet(MNEMONIC_PHRASE, DEFAULT_DERIVATION_PATH, DEFAULT_BECH32_HRP)
+ (server_host, server_port) = CRO_NETWORK["testnet_croeseid"].grpc_endpoint.split(":")
# if server does not use Server Name Indication (SNI), commented code below is enough:
# creds = ssl.get_server_certificate((SERVER_HOST, SERVER_PORT))
- conn = ssl.create_connection((SERVER_HOST, SERVER_PORT))
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
- sock = context.wrap_socket(conn, server_hostname=SERVER_HOST)
- certificate = ssl.DER_cert_to_PEM_cert(sock.getpeercert(True))
- creds = grpc.ssl_channel_credentials(str.encode(certificate))
+ with socket.create_connection((server_host, int(server_port))) as sock:
+ with context.wrap_socket(sock, server_hostname=server_host) as ssock:
+ certificate_DER = ssock.getpeercert(True)
+
+ if certificate_DER is None:
+ raise RuntimeError("no certificate returned from server")
+
+ certificate_PEM = ssl.DER_cert_to_PEM_cert(certificate_DER)
+ creds = grpc.ssl_channel_credentials(str.encode(certificate_PEM))
- client = GrpcClient(wallet, CHAIN_ID, GRPC_ENDPOINT, creds)
+ client = GrpcClient(CRO_NETWORK["testnet_croeseid"], creds)
- from_address = wallet.address
- res = client.get_balance(from_address, DENOM)
- print(f"address {from_address} initial balance: {res.balance.amount}")
+ print(client.query_bank_denom_metadata(CRO_NETWORK["testnet_croeseid"].coin_base_denom))
if __name__ == "__main__":
diff --git a/example/transaction.py b/example/transaction.py
index 12c9e54..8916109 100644
--- a/example/transaction.py
+++ b/example/transaction.py
@@ -1,46 +1,163 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-from chainlibpy.generated.cosmos.base.v1beta1.coin_pb2 import Coin
-from chainlibpy.grpc_client import GrpcClient
-from chainlibpy.wallet import Wallet
-# NOTE:
-# Recommend to use pystarport(https://pypi.org/project/pystarport/) to setup a testnet locally
+from chainlibpy import CROCoin, GrpcClient, Transaction, Wallet
+from chainlibpy.generated.cosmos.bank.v1beta1.tx_pb2 import MsgSend
+from chainlibpy.grpc_client import NetworkConfig
+
+# Recommend to use [pystarport](https://pypi.org/project/pystarport/) to setup a testnet locally
+# To use testnet configs in local_testnet_configs with pystarport:
+# 1. Download corresponding chain-maind binary from https://github.com/crypto-org-chain/chain-main/releases # noqa: 501
+# 2. Copy chain-maind binary to `./example` directory
+# 3. Enter `poetry shell`
+# 4. Go to `./example` directory
+# 5. Run Command `pystarport serve --data=./data --config=./local_testnet_configs/default.yaml --cmd=./chain-maind` # noqa: 501
+# 6. Obtain `MNEMONIC_PHRASE` and `TO_ADDRESS` accordingly
+# 7. Open another terminal window and run examples in this file
-DENOM = "basecro"
# Obtained from {directory_started_pystarport}/data/chainmaind/accounts.json
# To recover one of the genesis account
MNEMONIC_PHRASE = "first ... last"
# Obtained from {directory_started_pystarport}/data/chainmaind/accounts.json
-# Another address to receive coins sent
TO_ADDRESS = "cro...add"
-AMOUNT = [Coin(amount="10000", denom=DENOM)]
-# Obtained from {directory_started_pystarport}/data/chainmaind/genesis.json
-CHAIN_ID = "chainmaind"
-# Obtained from {directory_started_pystarport}/data/chainmaind/nodex/config/app.toml
-# Look for "gRPC Configuration" section
-GRPC_ENDPOINT = "0.0.0.0:26653"
+LOCAL_NETWORK = NetworkConfig(
+ # grpc_endpoint from {directory_started_pystarport}/data/chaintest/nodex/config/app.toml
+ # Look for "gRPC Configuration" section
+ grpc_endpoint="0.0.0.0:26653",
+ # chain_id from from {directory_started_pystarport}/data/
+ # the directory name under data is the chain_id
+ chain_id="chaintest",
+ address_prefix="cro",
+ coin_denom="cro",
+ coin_base_denom="basecro",
+ exponent=8,
+ derivation_path="m/44'/394'/0'/0/0",
+)
+
+
+def simple_transaction():
+ client = GrpcClient(LOCAL_NETWORK)
+
+ sending_wallet = Wallet(
+ MNEMONIC_PHRASE, LOCAL_NETWORK.derivation_path, LOCAL_NETWORK.address_prefix
+ )
+ sending_account = client.query_account(sending_wallet.address)
+ sending_account_init_bal = client.query_account_balance(sending_wallet.address)
+ receiving_account_init_bal = client.query_account_balance(TO_ADDRESS)
+
+ print(
+ f"sending account initial balance: {sending_account_init_bal.balance.amount}"
+ f"{sending_account_init_bal.balance.denom}"
+ )
+ print(
+ f"receiving account initial balance: {receiving_account_init_bal.balance.amount}"
+ f"{receiving_account_init_bal.balance.denom}"
+ )
+
+ ten_cro = CROCoin("10", "cro", LOCAL_NETWORK)
+ one_cro_fee = CROCoin("1", "cro", LOCAL_NETWORK)
+
+ msg_send = MsgSend(
+ from_address=sending_wallet.address,
+ to_address=TO_ADDRESS,
+ amount=[ten_cro.protobuf_coin_message],
+ )
+ tx = Transaction(
+ chain_id=LOCAL_NETWORK.chain_id,
+ from_wallets=[sending_wallet],
+ msgs=[msg_send],
+ account_number=sending_account.account_number,
+ fee=[one_cro_fee.protobuf_coin_message],
+ client=client,
+ )
+
+ signature_alice = sending_wallet.sign(tx.sign_doc.SerializeToString())
+ signed_tx = tx.set_signatures(signature_alice).signed_tx
+
+ client.broadcast_transaction(signed_tx.SerializeToString())
+
+ sending_account_aft_bal = client.query_account_balance(sending_wallet.address)
+ receiving_account_aft_bal = client.query_account_balance(TO_ADDRESS)
+
+ print("After transaction of sending 10cro with a 1cro fee:")
+ print(
+ f"sending account after balance: {sending_account_aft_bal.balance.amount}"
+ f"{sending_account_aft_bal.balance.denom}"
+ )
+ print(
+ f"receiving account after balance: {receiving_account_aft_bal.balance.amount}"
+ f"{receiving_account_aft_bal.balance.denom}"
+ )
+
+
+def transaction_with_two_messages():
+ client = GrpcClient(LOCAL_NETWORK)
+
+ sending_wallet = Wallet(
+ MNEMONIC_PHRASE, LOCAL_NETWORK.derivation_path, LOCAL_NETWORK.address_prefix
+ )
+ sending_account = client.query_account(sending_wallet.address)
+ sending_account_init_bal = client.query_account_balance(sending_wallet.address)
+ receiving_account_init_bal = client.query_account_balance(TO_ADDRESS)
+
+ print(
+ f"sending account initial balance : {sending_account_init_bal.balance.amount}"
+ f"{sending_account_init_bal.balance.denom}"
+ )
+ print(
+ f"receiving account initial balance: {receiving_account_init_bal.balance.amount}"
+ f"{receiving_account_init_bal.balance.denom}"
+ )
+
+ one_hundred_cro = CROCoin("100", "cro", LOCAL_NETWORK)
+ two_hundred_cro = CROCoin("200", "cro", LOCAL_NETWORK)
+ one_cro_fee = CROCoin("1", "cro", LOCAL_NETWORK)
+
+ msg_send_100_cro = MsgSend(
+ from_address=sending_wallet.address,
+ to_address=TO_ADDRESS,
+ amount=[one_hundred_cro.protobuf_coin_message],
+ )
+ msg_send_200_cro = MsgSend(
+ from_address=sending_wallet.address,
+ to_address=TO_ADDRESS,
+ amount=[two_hundred_cro.protobuf_coin_message],
+ )
+ tx = Transaction(
+ chain_id=LOCAL_NETWORK.chain_id,
+ from_wallets=[sending_wallet],
+ msgs=[msg_send_100_cro],
+ account_number=sending_account.account_number,
+ fee=[one_cro_fee.protobuf_coin_message],
+ client=client,
+ )
+ tx.append_message(msg_send_200_cro)
-def main():
- wallet = Wallet(MNEMONIC_PHRASE)
- client = GrpcClient(wallet, CHAIN_ID, GRPC_ENDPOINT)
+ signature_alice = sending_wallet.sign(tx.sign_doc.SerializeToString())
+ signed_tx = tx.set_signatures(signature_alice).signed_tx
- from_address = wallet.address
- res = client.get_balance(from_address, DENOM)
- print(f"from_address initial balance: {res.balance.amount}")
- res = client.get_balance(TO_ADDRESS, DENOM)
- print(f"to_address initial balance: {res.balance.amount}")
+ client.broadcast_transaction(signed_tx.SerializeToString())
- client.bank_send(TO_ADDRESS, AMOUNT)
+ sending_account_aft_bal = client.query_account_balance(sending_wallet.address)
+ receiving_account_aft_bal = client.query_account_balance(TO_ADDRESS)
+ sending_account_cro = CROCoin(
+ sending_account_aft_bal.balance.amount,
+ sending_account_aft_bal.balance.denom,
+ LOCAL_NETWORK,
+ )
+ receiving_account_cro = CROCoin(
+ receiving_account_aft_bal.balance.amount,
+ receiving_account_aft_bal.balance.denom,
+ LOCAL_NETWORK,
+ )
- print("after successful transaction")
- res = client.get_balance(from_address, DENOM)
- print(f"from_address updated balance: {res.balance.amount}")
- res = client.get_balance(TO_ADDRESS, DENOM)
- print(f"to_address updated balance: {res.balance.amount}")
+ print("After transaction of sending 300cro in total with a 1cro fee:")
+ print(f"sending account after balance : {sending_account_cro.amount_with_unit}")
+ print(f"receiving account after balance: {receiving_account_cro.amount_with_unit}")
if __name__ == "__main__":
- main()
+ simple_transaction()
+ transaction_with_two_messages()
diff --git a/poetry.lock b/poetry.lock
index 0b973c0..0d3b91d 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -62,6 +62,14 @@ category = "dev"
optional = false
python-versions = "*"
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
[[package]]
name = "charset-normalizer"
version = "2.0.10"
@@ -189,6 +197,18 @@ mccabe = ">=0.6.0,<0.7.0"
pycodestyle = ">=2.8.0,<2.9.0"
pyflakes = ">=2.4.0,<2.5.0"
+[[package]]
+name = "grpc-stubs"
+version = "1.24.7"
+description = "Mypy stubs for gRPC"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+grpcio = "*"
+mypy = ">=0.902"
+
[[package]]
name = "grpcio"
version = "1.43.0"
@@ -254,6 +274,17 @@ pytz = ["pytz (>=2014.1)"]
redis = ["redis (>=3.0.0)"]
zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2021.5)"]
+[[package]]
+name = "identify"
+version = "2.4.7"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+license = ["ukkonen"]
+
[[package]]
name = "idna"
version = "3.3"
@@ -435,6 +466,23 @@ python-versions = "*"
[package.dependencies]
six = "*"
+[[package]]
+name = "mypy"
+version = "0.931"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+mypy-extensions = ">=0.4.3"
+tomli = ">=1.1.0"
+typing-extensions = ">=3.10"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+
[[package]]
name = "mypy-extensions"
version = "0.4.3"
@@ -443,6 +491,14 @@ category = "dev"
optional = false
python-versions = "*"
+[[package]]
+name = "nodeenv"
+version = "1.6.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = "*"
+
[[package]]
name = "packaging"
version = "21.3"
@@ -486,6 +542,22 @@ python-versions = ">=3.6"
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
+[[package]]
+name = "pre-commit"
+version = "2.17.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+toml = "*"
+virtualenv = ">=20.0.8"
+
[[package]]
name = "protobuf"
version = "3.19.3"
@@ -727,6 +799,60 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,
docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"]
testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"]
+[[package]]
+name = "types-futures"
+version = "3.3.8"
+description = "Typing stubs for futures"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "types-protobuf"
+version = "3.19.8"
+description = "Typing stubs for protobuf"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+types-futures = "*"
+
+[[package]]
+name = "types-pyyaml"
+version = "6.0.4"
+description = "Typing stubs for PyYAML"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "types-requests"
+version = "2.27.8"
+description = "Typing stubs for requests"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+types-urllib3 = "<1.27"
+
+[[package]]
+name = "types-toml"
+version = "0.10.3"
+description = "Typing stubs for toml"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "types-urllib3"
+version = "1.26.9"
+description = "Typing stubs for urllib3"
+category = "dev"
+optional = false
+python-versions = "*"
+
[[package]]
name = "typing-extensions"
version = "4.0.1"
@@ -802,7 +928,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata]
lock-version = "1.1"
python-versions = ">=3.8, <4.0"
-content-hash = "1c3af2f51ff4960ee46720410290aa3ec9d66cae1866e2c79181d9e4adb9a913"
+content-hash = "8665735f8e28f1ecc67576a1955a8cf7910f08339ee5162ec47313028eb752ff"
[metadata.files]
atomicwrites = [
@@ -825,6 +951,10 @@ certifi = [
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
]
+cfgv = [
+ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
charset-normalizer = [
{file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"},
{file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"},
@@ -866,6 +996,10 @@ flake8 = [
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
{file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
]
+grpc-stubs = [
+ {file = "grpc-stubs-1.24.7.tar.gz", hash = "sha256:33e804c21d9839857c3e913c8c4d1ef3b57631a2c69c6a476cd809c9387e24ca"},
+ {file = "grpc_stubs-1.24.7-py3-none-any.whl", hash = "sha256:7a018c9249aba0fa0a017ddd916b9b6b67abffb0e69b2cfaa0892af893d5d573"},
+]
grpcio = [
{file = "grpcio-1.43.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:a4e786a8ee8b30b25d70ee52cda6d1dbba2a8ca2f1208d8e20ed8280774f15c8"},
{file = "grpcio-1.43.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:af9c3742f6c13575c0d4147a8454da0ff5308c4d9469462ff18402c6416942fe"},
@@ -966,6 +1100,10 @@ hypothesis = [
{file = "hypothesis-6.35.1-py3-none-any.whl", hash = "sha256:536b928d14934809d0da676579436aaa379b06df84408b4c154412e8fd4e1b91"},
{file = "hypothesis-6.35.1.tar.gz", hash = "sha256:8533812bd277925b0c594ef2681dc8f4289a7b6be0169cc2df295d096c7cd783"},
]
+identify = [
+ {file = "identify-2.4.7-py2.py3-none-any.whl", hash = "sha256:e64210654dfbca6ced33230eb1b137591a0981425e1a60b4c6c36309f787bbd5"},
+ {file = "identify-2.4.7.tar.gz", hash = "sha256:8408f01e0be25492017346d7dffe7e7711b762b23375c775d24d3bc38618fabc"},
+]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
@@ -1024,10 +1162,36 @@ mnemonic = [
multitail2 = [
{file = "multitail2-1.5.2.tar.gz", hash = "sha256:7086598c1cd1901ec79ce3c1eda9420299e3778f6c18464958c1f74ffd1950c9"},
]
+mypy = [
+ {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"},
+ {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"},
+ {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"},
+ {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"},
+ {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"},
+ {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"},
+ {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"},
+ {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"},
+ {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"},
+ {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"},
+ {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"},
+ {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"},
+ {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"},
+ {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"},
+ {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"},
+ {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"},
+ {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"},
+ {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"},
+ {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"},
+ {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"},
+]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
+nodeenv = [
+ {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
+ {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
+]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
@@ -1044,6 +1208,10 @@ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
+pre-commit = [
+ {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"},
+ {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"},
+]
protobuf = [
{file = "protobuf-3.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1cb2ed66aac593adbf6dca4f07cd7ee7e2958b17bbc85b2cc8bc564ebeb258ec"},
{file = "protobuf-3.19.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:898bda9cd37ec0c781b598891e86435de80c3bfa53eb483a9dac5a11ec93e942"},
@@ -1209,6 +1377,30 @@ tox = [
{file = "tox-3.24.5-py2.py3-none-any.whl", hash = "sha256:be3362472a33094bce26727f5f771ca0facf6dafa217f65875314e9a6600c95c"},
{file = "tox-3.24.5.tar.gz", hash = "sha256:67e0e32c90e278251fea45b696d0fef3879089ccbe979b0c556d35d5a70e2993"},
]
+types-futures = [
+ {file = "types-futures-3.3.8.tar.gz", hash = "sha256:6fe8ccc2c2af7ef2fdd9bf73eab6d617074f09f30ad7d373510b4043d39c42de"},
+ {file = "types_futures-3.3.8-py3-none-any.whl", hash = "sha256:d6e97ec51d56b96debfbf1dea32ebec22c1687f16d2547ea0a34b48db45df205"},
+]
+types-protobuf = [
+ {file = "types-protobuf-3.19.8.tar.gz", hash = "sha256:5ff1a5b7d0f36e3600ad1a3d4b55ba6c446cef2ef82d25f06a0aa43912345fb4"},
+ {file = "types_protobuf-3.19.8-py3-none-any.whl", hash = "sha256:1364327ebfb4360b36bd62b55fb32f704a516c8c26d82bad566938a23e644eca"},
+]
+types-pyyaml = [
+ {file = "types-PyYAML-6.0.4.tar.gz", hash = "sha256:6252f62d785e730e454dfa0c9f0fb99d8dae254c5c3c686903cf878ea27c04b7"},
+ {file = "types_PyYAML-6.0.4-py3-none-any.whl", hash = "sha256:693b01c713464a6851f36ff41077f8adbc6e355eda929addfb4a97208aea9b4b"},
+]
+types-requests = [
+ {file = "types-requests-2.27.8.tar.gz", hash = "sha256:c2f4e4754d07ca0a88fd8a89bbc6c8a9f90fb441f9c9b572fd5c484f04817486"},
+ {file = "types_requests-2.27.8-py3-none-any.whl", hash = "sha256:8ec9f5f84adc6f579f53943312c28a84e87dc70201b54f7c4fbc7d22ecfa8a3e"},
+]
+types-toml = [
+ {file = "types-toml-0.10.3.tar.gz", hash = "sha256:215a7a79198651ec5bdfd66193c1e71eb681a42f3ef7226c9af3123ced62564a"},
+ {file = "types_toml-0.10.3-py3-none-any.whl", hash = "sha256:988457744d9774d194e3539388772e3a685d8057b7c4a89407afeb0a6cbd1b14"},
+]
+types-urllib3 = [
+ {file = "types-urllib3-1.26.9.tar.gz", hash = "sha256:abd2d4857837482b1834b4817f0587678dcc531dbc9abe4cde4da28cef3f522c"},
+ {file = "types_urllib3-1.26.9-py3-none-any.whl", hash = "sha256:4a54f6274ab1c80968115634a55fb9341a699492b95e32104a7c513db9fe02e9"},
+]
typing-extensions = [
{file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
{file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
diff --git a/pyproject.toml b/pyproject.toml
index 2a3ff1f..6663352 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,52 +1,56 @@
[tool.poetry]
-name = "chainlibpy"
-version = "2.2.0"
-description = "Tools for Crypto.org Chain wallet management and offline transaction signing"
authors = ["chain-dev-team "]
-license = "Apache-2.0"
-keywords = ["CRO", "blockchain", "signature", "crypto.com"]
-readme = "README.md"
-repository = "https://github.com/crypto-org-chain/chainlibpy"
classifiers = [
"Intended Audience :: Developers",
- "Topic :: Software Development :: Libraries :: Python Modules"
+ "Topic :: Software Development :: Libraries :: Python Modules",
]
-include = ["LICENSE"]
+description = "Tools for Crypto.org Chain wallet management and offline transaction signing"
exclude = ["generate_protos.sh"]
+include = ["LICENSE"]
+keywords = ["CRO", "blockchain", "signature", "crypto.com"]
+license = "Apache-2.0"
+name = "chainlibpy"
+readme = "README.md"
+repository = "https://github.com/crypto-org-chain/chainlibpy"
+version = "2.2.0"
[tool.poetry.dependencies]
-python = ">=3.8, <4.0"
-ecdsa = ">=0.14.0, <0.17.0"
bech32 = "~=1.1.0"
-mnemonic = ">=0.19, <0.20"
-hdwallets = "~=0.1.0"
-grpcio-tools = "^1.42.0"
+ecdsa = ">=0.14.0, <0.17.0"
grpcio = "^1.42.0"
+grpcio-tools = "^1.42.0"
+hdwallets = "~=0.1.0"
+mnemonic = ">=0.19, <0.20"
+python = ">=3.8, <4.0"
[tool.poetry.dev-dependencies]
-pytest = "^6.2.5"
black = "^21.11b1"
-isort = "^5.10.1"
-mdformat = "^0.7.10"
docformatter = "^1.4"
-tox = "^3.24.4"
flake8 = "^4.0.1"
+grpc-stubs = "^1.24.7"
+hypothesis = "^6.35.1"
+isort = "^5.10.1"
+mdformat = "^0.7.10"
mdformat-black = "^0.1.1"
mdformat-toc = "^0.3.0"
-pytest-env = "^0.6.2"
+mypy = "^0.931"
+pre-commit = "^2.17.0"
pystarport = "^0.2.3"
+pytest = "^6.2.5"
+pytest-env = "^0.6.2"
requests = "^2.27.1"
toml = "^0.10.2"
-hypothesis = "^6.35.1"
+tox = "^3.24.4"
+types-PyYAML = "^6.0.4"
+types-protobuf = "^3.19.8"
+types-requests = "^2.27.8"
+types-toml = "^0.10.3"
[build-system]
-requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
+requires = ["poetry-core>=1.0.0"]
[tool.black]
-line-length = 99
-target-version = ['py39']
-include = '\.pyi?$'
exclude = '''
(
@@ -57,16 +61,26 @@ exclude = '''
| \.mypy_cache
| \.tox
| \.venv
+ | \.hypothesis
| _build
| buck-out
| build
| dist
)/
- | foo.py # also separately exclude a file named foo.py in
- # the root of the project
+ | ^chainlibpy/generated/
)
'''
+include = '\.pyi?$'
+line-length = 99
+target-version = ['py39']
[tool.isort]
-profile = "black"
extend_skip_glob = ["*/generated/*"]
+profile = "black"
+
+[tool.pytest.ini_options]
+env = [
+ # To avoid "Error: Cannot open an HTTP server: socket.error reported AF_UNIX path too long"
+ # PYTEST_DEBUG_TEMPROOT is used by pytest default tmp_path_factory fixture
+ "PYTEST_DEBUG_TEMPROOT = /tmp",
+]
diff --git a/tests/pytest.ini b/tests/pytest.ini
deleted file mode 100644
index 797ab50..0000000
--- a/tests/pytest.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[pytest]
-env =
- ; To avoid "Error: Cannot open an HTTP server: socket.error reported AF_UNIX path too long"
- ; PYTEST_DEBUG_TEMPROOT is used by pytest default tmp_path_factory fixture
- PYTEST_DEBUG_TEMPROOT=/tmp
\ No newline at end of file
diff --git a/tests/test_coin.py b/tests/test_coin.py
index 5db0652..e80755c 100644
--- a/tests/test_coin.py
+++ b/tests/test_coin.py
@@ -160,7 +160,7 @@ def test_crocoin_with_wrong_unit_should_raise_exception(
):
with pytest.raises(
AssertionError,
- match=f"unit should be {local_test_network_config.coin_denom} or {local_test_network_config.coin_base_denom}, got {wrong_unit}", # noqa 501
+ match=f"unit should be {local_test_network_config.coin_denom} or {local_test_network_config.coin_base_denom}, got {wrong_unit}", # noqa: 501
):
CROCoin(amount, wrong_unit, local_test_network_config)
@@ -188,7 +188,7 @@ def test_crocoin_beyond_max_supply_should_raise_exception(
with pytest.raises(
ValueError,
- match=rf"^Input is more than maximum cro supply .* got {invalid_amount_base}basecro$", # noqa 501
+ match=rf"^Input is more than maximum cro supply .* got {invalid_amount_base}basecro$", # noqa: 501
):
CROCoin(invalid_amount, unit, local_test_network_config)
@@ -226,6 +226,27 @@ def test_crocoin_below_zero_should_raise_exception(
("50000000.00000000000001", CRO_DENOM),
],
)
-def test_crocoin_temp(invalid_amount, unit, local_test_network_config: "NetworkConfig"):
+def test_crocoin_less_than_1basecro_should_raise_exception(
+ invalid_amount, unit, local_test_network_config: "NetworkConfig"
+):
with pytest.raises(ValueError, match="Amount is less than 1basecro"):
CROCoin(invalid_amount, unit, local_test_network_config)
+
+
+def test_addition_result_more_than_max_should_raise_exception(local_test_network_config):
+ max_supply_cro = CROCoin(MAX_CRO_SUPPLY, CRO_DENOM, local_test_network_config)
+ one_cro = CROCoin(1, CRO_DENOM, local_test_network_config)
+
+ with pytest.raises(
+ ValueError,
+ match=rf"^Input is more than maximum cro supply .* got 3000000000100000000basecro$", # noqa: 501
+ ):
+ max_supply_cro + one_cro
+
+
+def test_subtraction_result_below_zero_should_raise_exception(local_test_network_config):
+ one_cro = CROCoin(1, CRO_DENOM, local_test_network_config)
+ two_cro = CROCoin(2, CRO_DENOM, local_test_network_config)
+
+ with pytest.raises(ValueError, match="Amount cannot be negative"):
+ one_cro - two_cro
diff --git a/tests/test_grpc_client.py b/tests/test_grpc_client.py
index c56d65a..fd8bcd1 100644
--- a/tests/test_grpc_client.py
+++ b/tests/test_grpc_client.py
@@ -1,26 +1,37 @@
+import socket
import ssl
import grpc
import pytest
-from chainlibpy import CRO_NETWORK, GrpcClient, NetworkConfig, Wallet
+from chainlibpy import (
+ CRO_NETWORK,
+ CROCoin,
+ GrpcClient,
+ NetworkConfig,
+ Transaction,
+ Wallet,
+)
+from chainlibpy.generated.cosmos.bank.v1beta1.tx_pb2 import MsgSend
-from .utils import ALICE, get_blockchain_account_info, get_predefined_account_coins
+from .utils import ALICE, BOB, CRO_DENOM, get_blockchain_account_info
@pytest.mark.parametrize("network_config", CRO_NETWORK.values())
def test_network_config(network_config: "NetworkConfig"):
- wallet = Wallet.new(path=network_config.derivation_path, hrp=network_config.address_prefix)
-
(server_host, server_port) = network_config.grpc_endpoint.split(":")
- conn = ssl.create_connection((server_host, server_port))
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
- sock = context.wrap_socket(conn, server_hostname=server_host)
- certificate = ssl.DER_cert_to_PEM_cert(sock.getpeercert(True))
- creds = grpc.ssl_channel_credentials(str.encode(certificate))
+ with socket.create_connection((server_host, int(server_port))) as sock:
+ with context.wrap_socket(sock, server_hostname=server_host) as ssock:
+ certificate_DER = ssock.getpeercert(True)
+
+ if certificate_DER is None:
+ pytest.fail("no certificate returned from server")
- client = GrpcClient(wallet, network_config, creds)
+ certificate_PEM = ssl.DER_cert_to_PEM_cert(certificate_DER)
+ creds = grpc.ssl_channel_credentials(str.encode(certificate_PEM))
+ client = GrpcClient(network_config, creds)
assert (
client.query_bank_denom_metadata(network_config.coin_base_denom).metadata.base
@@ -28,17 +39,109 @@ def test_network_config(network_config: "NetworkConfig"):
)
-# TODO
-# Note: temporary test case to test newly added fixtures and local test environment
-def test_test_environment(blockchain_config_dict, blockchain_accounts, local_test_network_config):
- alice_coin = get_predefined_account_coins(blockchain_config_dict, ALICE)
- print(alice_coin)
+def test_send_cro(blockchain_accounts, local_test_network_config: "NetworkConfig"):
+ client = GrpcClient(local_test_network_config)
+ alice_info = get_blockchain_account_info(blockchain_accounts, ALICE)
+ alice_wallet = Wallet(alice_info["mnemonic"])
+ alice_account = client.query_account(alice_info["address"])
+ bob_info = get_blockchain_account_info(blockchain_accounts, BOB)
+ bob_wallet = Wallet(bob_info["mnemonic"])
+ alice_bal_init = client.query_account_balance(alice_wallet.address)
+ bob_bal_init = client.query_account_balance(bob_wallet.address)
+ alice_coin_init = CROCoin(
+ alice_bal_init.balance.amount, alice_bal_init.balance.denom, local_test_network_config
+ )
+ bob_coin_init = CROCoin(
+ bob_bal_init.balance.amount, bob_bal_init.balance.denom, local_test_network_config
+ )
+
+ ten_cro = CROCoin("10", CRO_DENOM, local_test_network_config)
+ one_cro_fee = CROCoin("1", CRO_DENOM, local_test_network_config)
+ msg_send = MsgSend(
+ from_address=alice_info["address"],
+ to_address=bob_info["address"],
+ amount=[ten_cro.protobuf_coin_message],
+ )
+
+ tx = Transaction(
+ chain_id=local_test_network_config.chain_id,
+ from_wallets=[alice_wallet],
+ msgs=[msg_send],
+ account_number=alice_account.account_number,
+ fee=[one_cro_fee.protobuf_coin_message],
+ client=client,
+ )
+
+ signature_alice = alice_wallet.sign(tx.sign_doc.SerializeToString())
+ signed_tx = tx.set_signatures(signature_alice).signed_tx
+
+ client.broadcast_transaction(signed_tx.SerializeToString())
+
+ alice_bal_aft = client.query_account_balance(alice_wallet.address)
+ bob_bal_aft = client.query_account_balance(bob_wallet.address)
+ alice_coin_aft = CROCoin(
+ alice_bal_aft.balance.amount, alice_bal_aft.balance.denom, local_test_network_config
+ )
+ bob_coin_aft = CROCoin(
+ bob_bal_aft.balance.amount, bob_bal_aft.balance.denom, local_test_network_config
+ )
- alice_account = get_blockchain_account_info(blockchain_accounts, ALICE)
- print(alice_account)
+ assert alice_coin_aft == alice_coin_init - ten_cro - one_cro_fee
+ assert bob_coin_aft == bob_coin_init + ten_cro
- wallet_default_derivation = Wallet(alice_account["mnemonic"])
- assert wallet_default_derivation.address == alice_account["address"]
- client = GrpcClient(wallet_default_derivation, local_test_network_config)
- print(client.get_balance(wallet_default_derivation.address, "basecro"))
+def test_2_msgs_in_1_tx(blockchain_accounts, local_test_network_config: "NetworkConfig"):
+ client = GrpcClient(local_test_network_config)
+ alice_info = get_blockchain_account_info(blockchain_accounts, ALICE)
+ alice_wallet = Wallet(alice_info["mnemonic"])
+ alice_account = client.query_account(alice_info["address"])
+ bob_info = get_blockchain_account_info(blockchain_accounts, BOB)
+ bob_wallet = Wallet(bob_info["mnemonic"])
+ alice_bal_init = client.query_account_balance(alice_wallet.address)
+ bob_bal_init = client.query_account_balance(bob_wallet.address)
+ alice_coin_init = CROCoin(
+ alice_bal_init.balance.amount, alice_bal_init.balance.denom, local_test_network_config
+ )
+ bob_coin_init = CROCoin(
+ bob_bal_init.balance.amount, bob_bal_init.balance.denom, local_test_network_config
+ )
+
+ ten_cro = CROCoin("10", CRO_DENOM, local_test_network_config)
+ twnenty_cro = CROCoin("20", CRO_DENOM, local_test_network_config)
+ one_cro_fee = CROCoin("1", CRO_DENOM, local_test_network_config)
+ msg_send_10_cro = MsgSend(
+ from_address=alice_info["address"],
+ to_address=bob_info["address"],
+ amount=[ten_cro.protobuf_coin_message],
+ )
+ msg_send_20_cro = MsgSend(
+ from_address=alice_info["address"],
+ to_address=bob_info["address"],
+ amount=[twnenty_cro.protobuf_coin_message],
+ )
+
+ tx = Transaction(
+ chain_id=local_test_network_config.chain_id,
+ from_wallets=[alice_wallet],
+ msgs=[msg_send_10_cro],
+ account_number=alice_account.account_number,
+ fee=[one_cro_fee.protobuf_coin_message],
+ client=client,
+ ).append_message(msg_send_20_cro)
+
+ signature_alice = alice_wallet.sign(tx.sign_doc.SerializeToString())
+ signed_tx = tx.set_signatures(signature_alice).signed_tx
+
+ client.broadcast_transaction(signed_tx.SerializeToString())
+
+ alice_bal_aft = client.query_account_balance(alice_wallet.address)
+ bob_bal_aft = client.query_account_balance(bob_wallet.address)
+ alice_coin_aft = CROCoin(
+ alice_bal_aft.balance.amount, alice_bal_aft.balance.denom, local_test_network_config
+ )
+ bob_coin_aft = CROCoin(
+ bob_bal_aft.balance.amount, bob_bal_aft.balance.denom, local_test_network_config
+ )
+
+ assert alice_coin_aft == alice_coin_init - ten_cro - twnenty_cro - one_cro_fee
+ assert bob_coin_aft == bob_coin_init + ten_cro + twnenty_cro
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index 7fe4115..ef8abc2 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -5,7 +5,7 @@
def test_generate_wallet():
- seed = "burst negative solar evoke traffic yard lizard next series foster seminar enter wrist captain bulb trap giggle country sword season shoot boy bargain deal" # noqa 501
+ seed = "burst negative solar evoke traffic yard lizard next series foster seminar enter wrist captain bulb trap giggle country sword season shoot boy bargain deal" # noqa: 501
wallet = Wallet(seed)
assert wallet.private_key == bytes.fromhex(
"dc81c553efffdce74035a194ea7a58f1d67bdfd1329e33f684460d9ed6223faf"
diff --git a/tests/utils.py b/tests/utils.py
index 7c50d96..1f7e0de 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -142,7 +142,7 @@ def download_latest_binary(latest_release_info):
local_file.unlink()
else:
raise ValueError(
- f"{compatible_asset_name} could not be found on crypto-org-chain/chain-main GitHub release" # noqa 501
+ f"{compatible_asset_name} could not be found on crypto-org-chain/chain-main GitHub release" # noqa: 501
)
@@ -160,7 +160,7 @@ def check_local_chain_binary():
if latest_release_version != local_version:
print(
- f"download binary due to outdated version local: {local_version}, latest release: {latest_release_version}" # noqa 501
+ f"download binary due to outdated version local: {local_version}, latest release: {latest_release_version}" # noqa: 501
)
download_latest_binary(latest_release_info)
else:
diff --git a/tox.ini b/tox.ini
index a5fc217..6e8e68e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -17,23 +17,13 @@ isolated_build = true
whitelist_externals = poetry
deps =
poetry >=1.1.12
- flake8
- isort
- docformatter
- mdformat
- mdformat-black
- mdformat-toc
+ pre-commit
pytest
- grpcio-tools
- grpcio
pytest-env
pystarport
requests
toml
hypothesis
commands =
- poetry run flake8 .
- poetry run isort --check-only .
- poetry run docformatter --recursive --check chainlibpy/ --exclude chainlibpy/generated tests/
- poetry run mdformat --check CHANGELOG.md CONTRIBUTING.md README.md
+ poetry run pre-commit run --all-files
poetry run pytest tests