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