diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 857d104..4fe5cf1 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pytest pytest-asyncio + python -m pip install flake8 pytest==7.4.2 pytest-asyncio if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | diff --git a/pytoniq/contract/wallets/__init__.py b/pytoniq/contract/wallets/__init__.py index a7ab4a6..a2cbcea 100644 --- a/pytoniq/contract/wallets/__init__.py +++ b/pytoniq/contract/wallets/__init__.py @@ -1 +1,2 @@ from .wallet import WalletError, Wallet, BaseWallet, WalletV3, WalletV4, WalletV3R1, WalletV3R2, WalletV4R2, WalletV3Data, WalletV4Data, WalletMessage +from .highload import HighloadWallet diff --git a/pytoniq/contract/wallets/wallet.py b/pytoniq/contract/wallets/wallet.py index 4ca4a34..60bc79d 100644 --- a/pytoniq/contract/wallets/wallet.py +++ b/pytoniq/contract/wallets/wallet.py @@ -41,7 +41,7 @@ def create_wallet_internal_message(destination: Address, send_mode: int = 3, val if isinstance(body, str): body = Builder()\ .store_uint(0, 32)\ - .store_string(body)\ + .store_snake_string(body)\ .end_cell() message = Contract.create_internal_msg(dest=destination, value=value, body=body, state_init=state_init, **kwargs) diff --git a/pytoniq/liteclient/__init__.py b/pytoniq/liteclient/__init__.py index 6bfa4d9..a2c349a 100644 --- a/pytoniq/liteclient/__init__.py +++ b/pytoniq/liteclient/__init__.py @@ -1,6 +1,6 @@ import typing -from .client import LiteClient, LiteClientError, RunGetMethodError, BlockId, BlockIdExt +from .client import LiteClient, LiteClientError, RunGetMethodError, BlockId, BlockIdExt, LiteServerError from .balancer import LiteBalancer, BalancerError LiteClientLike = typing.Union[LiteClient, LiteBalancer] diff --git a/pytoniq/liteclient/balancer.py b/pytoniq/liteclient/balancer.py index 5669dae..f7d07f4 100644 --- a/pytoniq/liteclient/balancer.py +++ b/pytoniq/liteclient/balancer.py @@ -127,7 +127,7 @@ def _check_errors(self, client: LiteClient): if not task.cancelled(): self._logger.debug(f'client task {task} failed with exception: {task.exception()}') return True - return False + return False async def _check_peers(self): while True: @@ -213,14 +213,14 @@ def _delete_unsync_peers(self): self._alive_peers.discard(i) async def execute_method(self, method_name_: str, *args, **kwargs) -> typing.Union[dict, typing.Any]: + only_archive = kwargs.pop('only_archive', False) + choose_random = kwargs.pop('choose_random', False) + for _ in range(self.max_retries): if not len(self._alive_peers): raise BalancerError(f'have no alive peers') - only_archive = kwargs.pop('only_archive', False) - choose_random = kwargs.pop('choose_random', False) - if only_archive and choose_random: raise BalancerError('Currently you cant execute method for both random and archive peer') @@ -267,97 +267,98 @@ def _get_args(locals_: dict): """CODE BELOW IS AUTOGENERATED. DO NOT EDIT MANUALLY""" async def get_masterchain_info(self, **kwargs): - return await self.execute_method('get_masterchain_info', **self._get_args(locals())) + return await self.execute_method('get_masterchain_info', **self._get_args(locals())) async def raw_wait_masterchain_seqno(self, seqno: int, timeout_ms: int, suffix: bytes = b'', **kwargs): - return await self.execute_method('raw_wait_masterchain_seqno', **self._get_args(locals())) + return await self.execute_method('raw_wait_masterchain_seqno', **self._get_args(locals())) async def wait_masterchain_seqno(self, seqno: int, timeout_ms: int, schema_name: str, data: dict = None, **kwargs): - return await self.execute_method('wait_masterchain_seqno', **self._get_args(locals())) + return await self.execute_method('wait_masterchain_seqno', **self._get_args(locals())) async def get_masterchain_info_ext(self, **kwargs): - return await self.execute_method('get_masterchain_info_ext', **self._get_args(locals())) + return await self.execute_method('get_masterchain_info_ext', **self._get_args(locals())) async def get_time(self, **kwargs): - return await self.execute_method('get_time', **self._get_args(locals())) + return await self.execute_method('get_time', **self._get_args(locals())) async def get_version(self, **kwargs): - return await self.execute_method('get_version', **self._get_args(locals())) + return await self.execute_method('get_version', **self._get_args(locals())) async def get_state(self, wc: int, shard: typing.Optional[int], seqno: int, root_hash: typing.Union[str, bytes], file_hash: typing.Union[str, bytes] , **kwargs) -> dict: - return await self.execute_method('get_state', **self._get_args(locals())) + return await self.execute_method('get_state', **self._get_args(locals())) async def raw_get_block_header(self, block: BlockIdExt, **kwargs) -> Block: - return await self.execute_method('raw_get_block_header', **self._get_args(locals())) + return await self.execute_method('raw_get_block_header', **self._get_args(locals())) async def get_block_header(self, wc: int, shard: typing.Optional[int], seqno: int, root_hash: typing.Union[str, bytes], file_hash: typing.Union[str, bytes] , **kwargs) -> Block: - return await self.execute_method('get_block_header', **self._get_args(locals())) + return await self.execute_method('get_block_header', **self._get_args(locals())) async def lookup_block(self, wc: int, shard: int, seqno: int = -1, lt: typing.Optional[int] = None, utime: typing.Optional[int] = None, **kwargs) -> typing.Tuple[BlockIdExt, Block]: - return await self.execute_method('lookup_block', **self._get_args(locals())) + return await self.execute_method('lookup_block', **self._get_args(locals())) async def raw_get_block(self, block: BlockIdExt, **kwargs) -> Block: - return await self.execute_method('raw_get_block', **self._get_args(locals())) + return await self.execute_method('raw_get_block', **self._get_args(locals())) async def get_block(self, wc: int, shard: typing.Optional[int], seqno: int, root_hash: typing.Union[str, bytes], file_hash: typing.Union[str, bytes], **kwargs) -> Block: - return await self.execute_method('get_block', **self._get_args(locals())) + return await self.execute_method('get_block', **self._get_args(locals())) async def raw_get_account_state(self, address: typing.Union[str, Address], block: typing.Optional[BlockIdExt] = None , **kwargs) -> typing.Tuple[typing.Optional[Account], typing.Optional[ShardAccount]]: - return await self.execute_method('raw_get_account_state', **self._get_args(locals())) + return await self.execute_method('raw_get_account_state', **self._get_args(locals())) async def get_account_state(self, address: typing.Union[str, Address], **kwargs) -> SimpleAccount: - return await self.execute_method('get_account_state', **self._get_args(locals())) + return await self.execute_method('get_account_state', **self._get_args(locals())) async def run_get_method(self, address: typing.Union[Address, str], method: typing.Union[int, str], stack: list, block: BlockIdExt = None , **kwargs) -> list: - return await self.execute_method('run_get_method', **self._get_args(locals())) + return await self.execute_method('run_get_method', **self._get_args(locals())) async def raw_get_shard_info(self, block: typing.Optional[BlockIdExt] = None, wc: int = 0, shard: int = -9223372036854775808, exact: bool = True , **kwargs) -> ShardDescr: - return await self.execute_method('raw_get_shard_info', **self._get_args(locals())) + return await self.execute_method('raw_get_shard_info', **self._get_args(locals())) async def raw_get_all_shards_info(self, block: typing.Optional[BlockIdExt] = None, **kwargs) -> typing.Dict[int, BinTree]: - return await self.execute_method('raw_get_all_shards_info', **self._get_args(locals())) + return await self.execute_method('raw_get_all_shards_info', **self._get_args(locals())) async def get_all_shards_info(self, block: typing.Optional[BlockIdExt] = None, **kwargs) -> typing.List[BlockIdExt]: - return await self.execute_method('get_all_shards_info', **self._get_args(locals())) + return await self.execute_method('get_all_shards_info', **self._get_args(locals())) async def get_one_transaction(self, address: typing.Union[Address, str], lt: int, block: BlockIdExt , **kwargs) -> typing.Optional[Transaction]: - return await self.execute_method('get_one_transaction', **self._get_args(locals())) + return await self.execute_method('get_one_transaction', **self._get_args(locals())) async def raw_get_transactions(self, address: typing.Union[Address, str], count: int, from_lt: int = None, from_hash: typing.Optional[bytes] = None , **kwargs) -> typing.Tuple[typing.List[Transaction], typing.List[BlockIdExt]]: - return await self.execute_method('raw_get_transactions', **self._get_args(locals())) + return await self.execute_method('raw_get_transactions', **self._get_args(locals())) async def get_transactions(self, address: typing.Union[Address, str], count: int, - from_lt: int = None, from_hash: typing.Optional[bytes] = None + from_lt: int = None, from_hash: typing.Optional[bytes] = None, + to_lt: int = 0 , **kwargs) -> typing.List[Transaction]: - return await self.execute_method('get_transactions', **self._get_args(locals())) + return await self.execute_method('get_transactions', **self._get_args(locals())) async def raw_get_block_transactions(self, block: BlockIdExt, count: int = 1024, **kwargs) -> typing.List[dict]: - return await self.execute_method('raw_get_block_transactions', **self._get_args(locals())) + return await self.execute_method('raw_get_block_transactions', **self._get_args(locals())) async def raw_get_block_transactions_ext(self, block: BlockIdExt, count: int = 1024, **kwargs) -> typing.List[Transaction]: - return await self.execute_method('raw_get_block_transactions_ext', **self._get_args(locals())) + return await self.execute_method('raw_get_block_transactions_ext', **self._get_args(locals())) async def raw_get_mc_block_proof(self, known_block: BlockIdExt, target_block: typing.Optional[BlockIdExt] = None, return_best_key_block=False @@ -367,28 +368,28 @@ async def raw_get_mc_block_proof(self, known_block: BlockIdExt, target_block: ty typing.Optional[BlockIdExt], typing.Optional[int] ]: - return await self.execute_method('raw_get_mc_block_proof', **self._get_args(locals())) + return await self.execute_method('raw_get_mc_block_proof', **self._get_args(locals())) async def get_mc_block_proof(self, known_block: BlockIdExt, target_block: BlockIdExt, return_best_key_block=False , **kwargs) -> typing.Tuple[typing.Optional[BlockIdExt], int]: - return await self.execute_method('get_mc_block_proof', **self._get_args(locals())) + return await self.execute_method('get_mc_block_proof', **self._get_args(locals())) async def prove_block(self, target_block: BlockIdExt, **kwargs) -> None: - return await self.execute_method('prove_block', **self._get_args(locals())) + return await self.execute_method('prove_block', **self._get_args(locals())) async def get_config_all(self, blk: typing.Optional[BlockIdExt] = None, **kwargs) -> dict: - return await self.execute_method('get_config_all', **self._get_args(locals())) + return await self.execute_method('get_config_all', **self._get_args(locals())) async def get_config_params(self, params: typing.List[int], blk: typing.Optional[BlockIdExt] = None, **kwargs) -> dict: - return await self.execute_method('get_config_params', **self._get_args(locals())) + return await self.execute_method('get_config_params', **self._get_args(locals())) - async def get_libraries(self, library_list: typing.List[bytes], **kwargs): - return await self.execute_method('get_libraries', **self._get_args(locals())) + async def get_libraries(self, library_list: typing.List[typing.Union[bytes, str]], **kwargs): + return await self.execute_method('get_libraries', **self._get_args(locals())) async def get_shard_block_proof(self, blk: BlockIdExt, prove_mc: bool = False, **kwargs): - return await self.execute_method('get_shard_block_proof', **self._get_args(locals())) + return await self.execute_method('get_shard_block_proof', **self._get_args(locals())) """CODE ABOVE IS AUTOGENERATED. DO NOT EDIT MANUALLY""" diff --git a/pytoniq/liteclient/client.py b/pytoniq/liteclient/client.py index c648a0f..e0848f3 100644 --- a/pytoniq/liteclient/client.py +++ b/pytoniq/liteclient/client.py @@ -33,12 +33,19 @@ class LiteClientError(Exception): pass +class LiteServerError(LiteClientError): + def __init__(self, code, message): + self.code = code + self.message = message + super().__init__(f'Liteserver crashed with {code} code. Message: {message}') + + class RunGetMethodError(LiteClientError): def __init__(self, address: typing.Any, method: typing.Any, exit_code: int): self.address = address self.method = method self.exit_code = exit_code - super().__init__(f'get method "{method}" for account {address} returned exit code {exit_code}') + super().__init__(f'Get method "{method}" for account {address} returned exit code {exit_code}') class LiteClient: @@ -246,7 +253,7 @@ async def liteserver_query(self, query: bytes, qid: str) -> dict: result = resp.result() if 'code' in result and 'message' in result: - raise LiteClientError(f'LiteClient crashed with {result["code"]} code. Message: {result["message"]}') + raise LiteServerError(result["code"], result["message"]) return resp.result() @@ -639,7 +646,8 @@ async def raw_get_transactions(self, address: typing.Union[Address, str], count: return tr_result, block_ids async def get_transactions(self, address: typing.Union[Address, str], count: int, - from_lt: int = None, from_hash: typing.Optional[bytes] = None + from_lt: int = None, from_hash: typing.Optional[bytes] = None, + to_lt: int = 0 ) -> typing.List[Transaction]: """ Returns account transactions @@ -647,13 +655,23 @@ async def get_transactions(self, address: typing.Union[Address, str], count: int :param count: :param from_lt: :param from_hash: + :param to_lt: :return: """ result: typing.List[Transaction] = [] + reach_lt = False for i in range(0, count, 16): amount = min(16, count - i) - tr_result, block_ids = await self.raw_get_transactions(address, amount, from_lt, from_hash) + tr_result, _ = await self.raw_get_transactions(address, amount, from_lt, from_hash) + if to_lt > 0 and tr_result[-1].lt <= to_lt: + for j, t in enumerate(tr_result): + if t.lt <= to_lt: + result += tr_result[:j] + reach_lt = True + break + if reach_lt: + break result += tr_result from_lt, from_hash = result[-1].prev_trans_lt, result[-1].prev_trans_hash if from_lt == 0: @@ -905,12 +923,22 @@ async def get_config_params(self, params: typing.List[int], blk: typing.Optional state_proof = Cell.one_from_boc(result['state_proof']) return self.unpack_config(blk, config_proof, state_proof) - async def get_libraries(self, library_list: typing.List[bytes]): + async def get_libraries(self, library_list: typing.List[typing.Union[bytes, str]]): + if len(library_list) > 16: + raise LiteClientError('maximum libraries num could be requested is 16') + library_list = [lib.hex() if isinstance(lib, bytes) else lib for lib in library_list] data = {'library_list': library_list} result = await self.liteserver_request('getLibraries', data) - return result['result'] + libs = result['result'] + + if self.trust_level < 2: + for i, lib in enumerate(libs): + if Cell.one_from_boc(lib['data']).hash.hex() != library_list[i]: + raise LiteClientError('library hash mismatch') + + return libs async def get_shard_block_proof(self, blk: BlockIdExt, prove_mc: bool = False): data = {'id': blk.to_dict()} @@ -928,10 +956,12 @@ def check_shard_in_master(proof: Cell, blk: BlockIdExt): shard = None for sh in shards: sh: ShardDescr - if sh.seq_no == blk.seqno and sh.next_validator_shard_signed == blk.shard: + if sh is not None and sh.seq_no == blk.seqno and sh.next_validator_shard_signed == blk.shard: shard = sh.__dict__ - + if shard is None: + raise LiteClientError('shard not found in masterchain') shardblk = BlockIdExt.from_dict(shard) + shardblk.shard = shard['next_validator_shard_signed'] shardblk.seqno = shard['seq_no'] shardblk.workchain = blk.workchain return shardblk diff --git a/setup.py b/setup.py index 44d70bd..502fcea 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="pytoniq", - version="0.1.24", + version="0.1.30", author="Maksim Kurbatov", author_email="cyrbatoff@gmail.com", description="TON Blockchain SDK", @@ -22,7 +22,7 @@ python_requires='>=3.9', py_modules=["pytoniq"], install_requires=[ - "pytoniq-core>=0.1.15", + "pytoniq-core>=0.1.23", "requests>=2.31.0", "setuptools>=65.5.1", ] diff --git a/tests/test_liteclient.py b/tests/test_liteclient.py index 4933ac4..85409d1 100644 --- a/tests/test_liteclient.py +++ b/tests/test_liteclient.py @@ -1,9 +1,27 @@ import asyncio +import time + import pytest import random + +import pytest_asyncio + from pytoniq import LiteClient +@pytest_asyncio.fixture +async def client(): + while True: + client = LiteClient.from_mainnet_config(random.randint(0, 15), trust_level=1) + try: + await client.connect() + yield client + await client.close() + return + except: + continue + + @pytest.mark.asyncio async def test_init(): client = LiteClient.from_mainnet_config(random.randint(0, 8), trust_level=2) @@ -25,31 +43,16 @@ async def test_init(): @pytest.mark.asyncio -async def test_methods(): - while True: - client = LiteClient.from_mainnet_config(random.randint(0, 8), trust_level=1) - try: - await client.connect() - break - except asyncio.TimeoutError: - await client.close() - continue +async def test_methods(client: LiteClient): await client.get_masterchain_info() await client.get_config_all() await client.raw_get_block(client.last_mc_block) - await client.close() + lib = 'c245262b8c2bce5e9fcd23ca334e1d55fa96d4ce69aa2817ded717cefcba3f73' + await client.get_libraries([lib, lib]) @pytest.mark.asyncio -async def test_get_method(): - while True: - client = LiteClient.from_mainnet_config(random.randint(0, 8), trust_level=1) - try: - await client.connect() - break - except asyncio.TimeoutError: - await client.close() - continue +async def test_get_method(client: LiteClient): result = await client.run_get_method(address='EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG', method='seqno', stack=[])