diff --git a/CHANGELOG.md b/CHANGELOG.md index f6b5fd53..60b398a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,10 @@ All notable changes to this project will be documented in this file. ## [1.6.0] - 9999-99-99 ### Added -- Added support for all queries in the chain 'tendermint' module -- Added support for all queries in the IBC Transfer module +- Support for all queries in the chain "tendermint" module +- Support for all queries in the "IBC Transfer" module +- Support for all queries in the "IBC Channel" module +- Support for all queries in the "IBC Client" module ### Changed - Refactored cookies management logic to use all gRPC calls' responses to update the current cookies diff --git a/examples/chain_client/ibc/client/__init__.py b/examples/chain_client/ibc/client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/chain_client/ibc/client/query/1_ClientState.py b/examples/chain_client/ibc/client/query/1_ClientState.py new file mode 100644 index 00000000..47ad1eda --- /dev/null +++ b/examples/chain_client/ibc/client/query/1_ClientState.py @@ -0,0 +1,21 @@ +import asyncio + +from google.protobuf import symbol_database + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + + client_id = "07-tendermint-0" + + state = await client.fetch_ibc_client_state(client_id=client_id) + print(state) + + +if __name__ == "__main__": + symbol_db = symbol_database.Default() + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/ibc/client/query/2_ClientStates.py b/examples/chain_client/ibc/client/query/2_ClientStates.py new file mode 100644 index 00000000..511bc33c --- /dev/null +++ b/examples/chain_client/ibc/client/query/2_ClientStates.py @@ -0,0 +1,22 @@ +import asyncio + +from google.protobuf import symbol_database + +from pyinjective.async_client import AsyncClient +from pyinjective.client.model.pagination import PaginationOption +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + + pagination = PaginationOption(skip=2, limit=4) + + states = await client.fetch_ibc_client_states(pagination=pagination) + print(states) + + +if __name__ == "__main__": + symbol_db = symbol_database.Default() + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/ibc/client/query/3_ConsensusState.py b/examples/chain_client/ibc/client/query/3_ConsensusState.py new file mode 100644 index 00000000..33220967 --- /dev/null +++ b/examples/chain_client/ibc/client/query/3_ConsensusState.py @@ -0,0 +1,25 @@ +import asyncio + +from google.protobuf import symbol_database + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + + client_id = "07-tendermint-0" + revision_number = 0 + revision_height = 7379538 + + state = await client.fetch_ibc_consensus_state( + client_id=client_id, revision_number=revision_number, revision_height=revision_height + ) + print(state) + + +if __name__ == "__main__": + symbol_db = symbol_database.Default() + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/ibc/client/query/4_ConsensusStates.py b/examples/chain_client/ibc/client/query/4_ConsensusStates.py new file mode 100644 index 00000000..66b946dd --- /dev/null +++ b/examples/chain_client/ibc/client/query/4_ConsensusStates.py @@ -0,0 +1,23 @@ +import asyncio + +from google.protobuf import symbol_database + +from pyinjective.async_client import AsyncClient +from pyinjective.client.model.pagination import PaginationOption +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + + client_id = "07-tendermint-0" + pagination = PaginationOption(skip=2, limit=4) + + states = await client.fetch_ibc_consensus_states(client_id=client_id, pagination=pagination) + print(states) + + +if __name__ == "__main__": + symbol_db = symbol_database.Default() + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/ibc/client/query/5_ConsensusStateHeights.py b/examples/chain_client/ibc/client/query/5_ConsensusStateHeights.py new file mode 100644 index 00000000..acd4a440 --- /dev/null +++ b/examples/chain_client/ibc/client/query/5_ConsensusStateHeights.py @@ -0,0 +1,23 @@ +import asyncio + +from google.protobuf import symbol_database + +from pyinjective.async_client import AsyncClient +from pyinjective.client.model.pagination import PaginationOption +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + + client_id = "07-tendermint-0" + pagination = PaginationOption(skip=2, limit=4) + + states = await client.fetch_ibc_consensus_state_heights(client_id=client_id, pagination=pagination) + print(states) + + +if __name__ == "__main__": + symbol_db = symbol_database.Default() + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/ibc/client/query/6_ClientStatus.py b/examples/chain_client/ibc/client/query/6_ClientStatus.py new file mode 100644 index 00000000..9d5954aa --- /dev/null +++ b/examples/chain_client/ibc/client/query/6_ClientStatus.py @@ -0,0 +1,21 @@ +import asyncio + +from google.protobuf import symbol_database + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + + client_id = "07-tendermint-0" + + state = await client.fetch_ibc_client_status(client_id=client_id) + print(state) + + +if __name__ == "__main__": + symbol_db = symbol_database.Default() + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/ibc/client/query/7_ClientParams.py b/examples/chain_client/ibc/client/query/7_ClientParams.py new file mode 100644 index 00000000..1b461b5c --- /dev/null +++ b/examples/chain_client/ibc/client/query/7_ClientParams.py @@ -0,0 +1,19 @@ +import asyncio + +from google.protobuf import symbol_database + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + + params = await client.fetch_ibc_client_params() + print(params) + + +if __name__ == "__main__": + symbol_db = symbol_database.Default() + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/ibc/client/query/8_UpgradedClientState.py b/examples/chain_client/ibc/client/query/8_UpgradedClientState.py new file mode 100644 index 00000000..e200ae80 --- /dev/null +++ b/examples/chain_client/ibc/client/query/8_UpgradedClientState.py @@ -0,0 +1,19 @@ +import asyncio + +from google.protobuf import symbol_database + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + + state = await client.fetch_ibc_upgraded_client_state() + print(state) + + +if __name__ == "__main__": + symbol_db = symbol_database.Default() + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/ibc/client/query/9_UpgradedConsensusState.py b/examples/chain_client/ibc/client/query/9_UpgradedConsensusState.py new file mode 100644 index 00000000..a6e00f7c --- /dev/null +++ b/examples/chain_client/ibc/client/query/9_UpgradedConsensusState.py @@ -0,0 +1,19 @@ +import asyncio + +from google.protobuf import symbol_database + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + + state = await client.fetch_ibc_upgraded_consensus_state() + print(state) + + +if __name__ == "__main__": + symbol_db = symbol_database.Default() + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/ibc/client/query/__init__.py b/examples/chain_client/ibc/client/query/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyinjective/async_client.py b/pyinjective/async_client.py index eeb30b29..20c959a4 100644 --- a/pyinjective/async_client.py +++ b/pyinjective/async_client.py @@ -37,6 +37,7 @@ from pyinjective.client.model.pagination import PaginationOption from pyinjective.composer import Composer from pyinjective.core.ibc.channel.grpc.ibc_channel_grpc_api import IBCChannelGrpcApi +from pyinjective.core.ibc.client.ibc_client_grpc_api import IBCClientGrpcApi from pyinjective.core.ibc.transfer.grpc.ibc_transfer_grpc_api import IBCTransferGrpcApi from pyinjective.core.market import BinaryOptionMarket, DerivativeMarket, SpotMarket from pyinjective.core.network import Network @@ -184,6 +185,10 @@ def __init__( channel=self.chain_channel, cookie_assistant=network.chain_cookie_assistant, ) + self.ibc_client_api = IBCClientGrpcApi( + channel=self.chain_channel, + cookie_assistant=network.chain_cookie_assistant, + ) self.ibc_transfer_api = IBCTransferGrpcApi( channel=self.chain_channel, cookie_assistant=network.chain_cookie_assistant, @@ -3119,6 +3124,55 @@ async def fetch_next_sequence_receive(self, port_id: str, channel_id: str) -> Di # endregion + # region IBC Client module + async def fetch_ibc_client_state(self, client_id: str) -> Dict[str, Any]: + return await self.ibc_client_api.fetch_client_state(client_id=client_id) + + async def fetch_ibc_client_states(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + return await self.ibc_client_api.fetch_client_states(pagination=pagination) + + async def fetch_ibc_consensus_state( + self, + client_id: str, + revision_number: int, + revision_height: int, + latest_height: Optional[bool] = None, + ) -> Dict[str, Any]: + return await self.ibc_client_api.fetch_consensus_state( + client_id=client_id, + revision_number=revision_number, + revision_height=revision_height, + latest_height=latest_height, + ) + + async def fetch_ibc_consensus_states( + self, + client_id: str, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + return await self.ibc_client_api.fetch_consensus_states(client_id=client_id, pagination=pagination) + + async def fetch_ibc_consensus_state_heights( + self, + client_id: str, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + return await self.ibc_client_api.fetch_consensus_state_heights(client_id=client_id, pagination=pagination) + + async def fetch_ibc_client_status(self, client_id: str) -> Dict[str, Any]: + return await self.ibc_client_api.fetch_client_status(client_id=client_id) + + async def fetch_ibc_client_params(self) -> Dict[str, Any]: + return await self.ibc_client_api.fetch_client_params() + + async def fetch_ibc_upgraded_client_state(self) -> Dict[str, Any]: + return await self.ibc_client_api.fetch_upgraded_client_state() + + async def fetch_ibc_upgraded_consensus_state(self) -> Dict[str, Any]: + return await self.ibc_client_api.fetch_upgraded_consensus_state() + + # endregion + async def composer(self): return Composer( network=self.network.string(), diff --git a/pyinjective/core/ibc/channel/grpc/ibc_channel_grpc_api.py b/pyinjective/core/ibc/channel/grpc/ibc_channel_grpc_api.py index b87589e5..362e67e2 100644 --- a/pyinjective/core/ibc/channel/grpc/ibc_channel_grpc_api.py +++ b/pyinjective/core/ibc/channel/grpc/ibc_channel_grpc_api.py @@ -1,6 +1,6 @@ from typing import Any, Callable, Dict, List, Optional -from grpc._cython.cygrpc import Channel +from grpc import Channel from pyinjective.client.model.pagination import PaginationOption from pyinjective.core.network import CookieAssistant diff --git a/pyinjective/core/ibc/client/__init__.py b/pyinjective/core/ibc/client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyinjective/core/ibc/client/ibc_client_grpc_api.py b/pyinjective/core/ibc/client/ibc_client_grpc_api.py new file mode 100644 index 00000000..084dfd89 --- /dev/null +++ b/pyinjective/core/ibc/client/ibc_client_grpc_api.py @@ -0,0 +1,96 @@ +from typing import Any, Callable, Dict, Optional + +from grpc import Channel + +from pyinjective.client.model.pagination import PaginationOption +from pyinjective.core.network import CookieAssistant +from pyinjective.proto.ibc.core.client.v1 import query_pb2 as ibc_client_query, query_pb2_grpc as ibc_client_query_grpc +from pyinjective.utils.grpc_api_request_assistant import GrpcApiRequestAssistant + + +class IBCClientGrpcApi: + def __init__(self, channel: Channel, cookie_assistant: CookieAssistant): + self._stub = ibc_client_query_grpc.QueryStub(channel) + self._assistant = GrpcApiRequestAssistant(cookie_assistant=cookie_assistant) + + async def fetch_client_state(self, client_id: str) -> Dict[str, Any]: + request = ibc_client_query.QueryClientStateRequest(client_id=client_id) + response = await self._execute_call(call=self._stub.ClientState, request=request) + + return response + + async def fetch_client_states(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + if pagination is None: + pagination = PaginationOption() + request = ibc_client_query.QueryClientStatesRequest(pagination=pagination.create_pagination_request()) + response = await self._execute_call(call=self._stub.ClientStates, request=request) + + return response + + async def fetch_consensus_state( + self, + client_id: str, + revision_number: int, + revision_height: int, + latest_height: Optional[bool] = None, + ) -> Dict[str, Any]: + request = ibc_client_query.QueryConsensusStateRequest( + client_id=client_id, + revision_number=revision_number, + revision_height=revision_height, + latest_height=latest_height, + ) + response = await self._execute_call(call=self._stub.ConsensusState, request=request) + + return response + + async def fetch_consensus_states( + self, client_id: str, pagination: Optional[PaginationOption] = None + ) -> Dict[str, Any]: + if pagination is None: + pagination = PaginationOption() + request = ibc_client_query.QueryConsensusStatesRequest( + client_id=client_id, pagination=pagination.create_pagination_request() + ) + response = await self._execute_call(call=self._stub.ConsensusStates, request=request) + + return response + + async def fetch_consensus_state_heights( + self, client_id: str, pagination: Optional[PaginationOption] = None + ) -> Dict[str, Any]: + if pagination is None: + pagination = PaginationOption() + request = ibc_client_query.QueryConsensusStateHeightsRequest( + client_id=client_id, pagination=pagination.create_pagination_request() + ) + response = await self._execute_call(call=self._stub.ConsensusStateHeights, request=request) + + return response + + async def fetch_client_status(self, client_id: str) -> Dict[str, Any]: + request = ibc_client_query.QueryClientStatusRequest(client_id=client_id) + response = await self._execute_call(call=self._stub.ClientStatus, request=request) + + return response + + async def fetch_client_params(self) -> Dict[str, Any]: + request = ibc_client_query.QueryClientParamsRequest() + response = await self._execute_call(call=self._stub.ClientParams, request=request) + + return response + + async def fetch_upgraded_client_state(self) -> Dict[str, Any]: + request = ibc_client_query.QueryUpgradedClientStateRequest() + response = await self._execute_call(call=self._stub.UpgradedClientState, request=request) + + return response + + async def fetch_upgraded_consensus_state(self) -> Dict[str, Any]: + request = ibc_client_query.QueryUpgradedConsensusStateRequest() + response = await self._execute_call(call=self._stub.UpgradedConsensusState, request=request) + + return response + + async def _execute_call(self, call: Callable, request) -> Dict[str, Any]: + return await self._assistant.execute_call(call=call, request=request) diff --git a/tests/core/ibc/client/__init__.py b/tests/core/ibc/client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/core/ibc/client/grpc/__init__.py b/tests/core/ibc/client/grpc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/core/ibc/client/grpc/configurable_ibc_client_query_servicer.py b/tests/core/ibc/client/grpc/configurable_ibc_client_query_servicer.py new file mode 100644 index 00000000..135a0270 --- /dev/null +++ b/tests/core/ibc/client/grpc/configurable_ibc_client_query_servicer.py @@ -0,0 +1,50 @@ +from collections import deque + +from pyinjective.proto.ibc.core.client.v1 import query_pb2 as ibc_client_query, query_pb2_grpc as ibc_client_query_grpc + + +class ConfigurableIBCClientQueryServicer(ibc_client_query_grpc.QueryServicer): + def __init__(self): + super().__init__() + self.client_state_responses = deque() + self.client_states_responses = deque() + self.consensus_state_responses = deque() + self.consensus_states_responses = deque() + self.consensus_state_heights_responses = deque() + self.client_status_responses = deque() + self.client_params_responses = deque() + self.upgraded_client_state_responses = deque() + self.upgraded_consensus_state_responses = deque() + + async def ClientState(self, request: ibc_client_query.QueryClientStateRequest, context=None, metadata=None): + return self.client_state_responses.pop() + + async def ClientStates(self, request: ibc_client_query.QueryClientStatesRequest, context=None, metadata=None): + return self.client_states_responses.pop() + + async def ConsensusState(self, request: ibc_client_query.QueryConsensusStateRequest, context=None, metadata=None): + return self.consensus_state_responses.pop() + + async def ConsensusStates(self, request: ibc_client_query.QueryConsensusStatesRequest, context=None, metadata=None): + return self.consensus_states_responses.pop() + + async def ConsensusStateHeights( + self, request: ibc_client_query.QueryConsensusStateHeightsRequest, context=None, metadata=None + ): + return self.consensus_state_heights_responses.pop() + + async def ClientStatus(self, request: ibc_client_query.QueryClientStatusRequest, context=None, metadata=None): + return self.client_status_responses.pop() + + async def ClientParams(self, request: ibc_client_query.QueryClientParamsRequest, context=None, metadata=None): + return self.client_params_responses.pop() + + async def UpgradedClientState( + self, request: ibc_client_query.QueryUpgradedClientStateRequest, context=None, metadata=None + ): + return self.upgraded_client_state_responses.pop() + + async def UpgradedConsensusState( + self, request: ibc_client_query.QueryUpgradedConsensusStateRequest, context=None, metadata=None + ): + return self.upgraded_consensus_state_responses.pop() diff --git a/tests/core/ibc/client/grpc/test_ibc_client_grpc_api.py b/tests/core/ibc/client/grpc/test_ibc_client_grpc_api.py new file mode 100644 index 00000000..6f3157e6 --- /dev/null +++ b/tests/core/ibc/client/grpc/test_ibc_client_grpc_api.py @@ -0,0 +1,638 @@ +import base64 + +import grpc +import pytest +from google.protobuf import any_pb2 + +from pyinjective.client.model.pagination import PaginationOption +from pyinjective.core.ibc.client.ibc_client_grpc_api import IBCClientGrpcApi +from pyinjective.core.network import DisabledCookieAssistant, Network +from pyinjective.proto.cosmos.base.query.v1beta1 import pagination_pb2 as pagination_pb +from pyinjective.proto.cosmos.ics23.v1 import proofs_pb2 as ics23_proofs +from pyinjective.proto.ibc.core.client.v1 import client_pb2 as ibc_client, query_pb2 as ibc_client_query +from pyinjective.proto.ibc.core.commitment.v1 import commitment_pb2 as ibc_commitment +from pyinjective.proto.ibc.lightclients.tendermint.v1 import tendermint_pb2 as ibc_tendermint +from tests.core.ibc.client.grpc.configurable_ibc_client_query_servicer import ConfigurableIBCClientQueryServicer + + +@pytest.fixture +def ibc_client_servicer(): + return ConfigurableIBCClientQueryServicer() + + +class TestIBCClientGrpcApi: + @pytest.mark.asyncio + async def test_fetch_client_state( + self, + ibc_client_servicer, + ): + trust_level = ibc_tendermint.Fraction( + numerator=1, + denominator=3, + ) + leaf_spec = ics23_proofs.LeafOp( + hash=0, + prehash_key=1, + prehash_value=2, + length=3, + prefix=b"prefix", + ) + inner_spec = ics23_proofs.InnerSpec( + child_order=[0, 1, 2, 3], + child_size=4, + min_prefix_length=5, + max_prefix_length=6, + empty_child=b"empty", + hash=1, + ) + proof_spec = ics23_proofs.ProofSpec( + leaf_spec=leaf_spec, + inner_spec=inner_spec, + max_depth=5, + min_depth=1, + prehash_key_before_comparison=True, + ) + tendermint_client_state = ibc_tendermint.ClientState( + chain_id="injective-1", + trust_level=trust_level, + frozen_height=ibc_client.Height( + revision_number=1, + revision_height=2, + ), + latest_height=ibc_client.Height( + revision_number=3, + revision_height=4, + ), + proof_specs=[proof_spec], + upgrade_path=["upgrade", "upgradedIBCState"], + allow_update_after_expiry=False, + allow_update_after_misbehaviour=False, + ) + any_client_state = any_pb2.Any() + any_client_state.Pack(tendermint_client_state) + proof = b"proof" + proof_height = ibc_client.Height( + revision_number=1, + revision_height=2, + ) + + ibc_client_servicer.client_state_responses.append( + ibc_client_query.QueryClientStateResponse( + client_state=any_client_state, + proof=proof, + proof_height=proof_height, + ) + ) + + api = self._api_instance(servicer=ibc_client_servicer) + + result_client_state = await api.fetch_client_state(client_id="client-id") + expected_client_state = { + "clientState": { + "@type": "type.googleapis.com/ibc.lightclients.tendermint.v1.ClientState", + "chainId": tendermint_client_state.chain_id, + "trustLevel": { + "numerator": str(trust_level.numerator), + "denominator": str(trust_level.denominator), + }, + "frozenHeight": { + "revisionNumber": str(tendermint_client_state.frozen_height.revision_number), + "revisionHeight": str(tendermint_client_state.frozen_height.revision_height), + }, + "latestHeight": { + "revisionNumber": str(tendermint_client_state.latest_height.revision_number), + "revisionHeight": str(tendermint_client_state.latest_height.revision_height), + }, + "proofSpecs": [ + { + "leafSpec": { + "hash": ics23_proofs.HashOp.Name(leaf_spec.hash), + "prehashKey": ics23_proofs.HashOp.Name(leaf_spec.prehash_key), + "prehashValue": ics23_proofs.HashOp.Name(leaf_spec.prehash_value), + "length": ics23_proofs.LengthOp.Name(leaf_spec.length), + "prefix": base64.b64encode(leaf_spec.prefix).decode(), + }, + "innerSpec": { + "childOrder": inner_spec.child_order, + "childSize": inner_spec.child_size, + "minPrefixLength": inner_spec.min_prefix_length, + "maxPrefixLength": inner_spec.max_prefix_length, + "emptyChild": base64.b64encode(inner_spec.empty_child).decode(), + "hash": ics23_proofs.HashOp.Name(inner_spec.hash), + }, + "maxDepth": proof_spec.max_depth, + "minDepth": proof_spec.min_depth, + "prehashKeyBeforeComparison": proof_spec.prehash_key_before_comparison, + }, + ], + "upgradePath": tendermint_client_state.upgrade_path, + "allowUpdateAfterExpiry": tendermint_client_state.allow_update_after_expiry, + "allowUpdateAfterMisbehaviour": tendermint_client_state.allow_update_after_misbehaviour, + }, + "proof": base64.b64encode(proof).decode(), + "proofHeight": { + "revisionNumber": str(proof_height.revision_number), + "revisionHeight": str(proof_height.revision_height), + }, + } + + result_client_state["clientState"] = dict(result_client_state["clientState"]) + + assert result_client_state == expected_client_state + + @pytest.mark.asyncio + async def test_fetch_client_states( + self, + ibc_client_servicer, + ): + trust_level = ibc_tendermint.Fraction( + numerator=1, + denominator=3, + ) + leaf_spec = ics23_proofs.LeafOp( + hash=0, + prehash_key=1, + prehash_value=2, + length=3, + prefix=b"prefix", + ) + inner_spec = ics23_proofs.InnerSpec( + child_order=[0, 1, 2, 3], + child_size=4, + min_prefix_length=5, + max_prefix_length=6, + empty_child=b"empty", + hash=1, + ) + proof_spec = ics23_proofs.ProofSpec( + leaf_spec=leaf_spec, + inner_spec=inner_spec, + max_depth=5, + min_depth=1, + prehash_key_before_comparison=True, + ) + tendermint_client_state = ibc_tendermint.ClientState( + chain_id="injective-1", + trust_level=trust_level, + frozen_height=ibc_client.Height( + revision_number=1, + revision_height=2, + ), + latest_height=ibc_client.Height( + revision_number=3, + revision_height=4, + ), + proof_specs=[proof_spec], + upgrade_path=["upgrade", "upgradedIBCState"], + allow_update_after_expiry=False, + allow_update_after_misbehaviour=False, + ) + any_client_state = any_pb2.Any() + any_client_state.Pack(tendermint_client_state) + client_state = ibc_client.IdentifiedClientState( + client_id="client-1", + client_state=any_client_state, + ) + result_pagination = pagination_pb.PageResponse( + next_key=b"\001\032\264\007Z\224$]\377s8\343\004-\265\267\314?B\341", + total=16036, + ) + + ibc_client_servicer.client_states_responses.append( + ibc_client_query.QueryClientStatesResponse( + client_states=[client_state], + pagination=result_pagination, + ) + ) + + api = self._api_instance(servicer=ibc_client_servicer) + + pagination_option = PaginationOption( + skip=10, + limit=30, + reverse=False, + count_total=True, + ) + + result_client_states = await api.fetch_client_states(pagination=pagination_option) + expected_client_states = { + "clientStates": [ + { + "clientId": client_state.client_id, + "clientState": { + "@type": "type.googleapis.com/ibc.lightclients.tendermint.v1.ClientState", + "chainId": tendermint_client_state.chain_id, + "trustLevel": { + "numerator": str(trust_level.numerator), + "denominator": str(trust_level.denominator), + }, + "frozenHeight": { + "revisionNumber": str(tendermint_client_state.frozen_height.revision_number), + "revisionHeight": str(tendermint_client_state.frozen_height.revision_height), + }, + "latestHeight": { + "revisionNumber": str(tendermint_client_state.latest_height.revision_number), + "revisionHeight": str(tendermint_client_state.latest_height.revision_height), + }, + "proofSpecs": [ + { + "leafSpec": { + "hash": ics23_proofs.HashOp.Name(leaf_spec.hash), + "prehashKey": ics23_proofs.HashOp.Name(leaf_spec.prehash_key), + "prehashValue": ics23_proofs.HashOp.Name(leaf_spec.prehash_value), + "length": ics23_proofs.LengthOp.Name(leaf_spec.length), + "prefix": base64.b64encode(leaf_spec.prefix).decode(), + }, + "innerSpec": { + "childOrder": inner_spec.child_order, + "childSize": inner_spec.child_size, + "minPrefixLength": inner_spec.min_prefix_length, + "maxPrefixLength": inner_spec.max_prefix_length, + "emptyChild": base64.b64encode(inner_spec.empty_child).decode(), + "hash": ics23_proofs.HashOp.Name(inner_spec.hash), + }, + "maxDepth": proof_spec.max_depth, + "minDepth": proof_spec.min_depth, + "prehashKeyBeforeComparison": proof_spec.prehash_key_before_comparison, + }, + ], + "upgradePath": tendermint_client_state.upgrade_path, + "allowUpdateAfterExpiry": tendermint_client_state.allow_update_after_expiry, + "allowUpdateAfterMisbehaviour": tendermint_client_state.allow_update_after_misbehaviour, + }, + }, + ], + "pagination": { + "nextKey": base64.b64encode(result_pagination.next_key).decode(), + "total": str(result_pagination.total), + }, + } + + assert result_client_states == expected_client_states + + @pytest.mark.asyncio + async def test_fetch_consensus_state( + self, + ibc_client_servicer, + ): + root = ibc_commitment.MerkleRoot( + hash=b"root", + ) + consensus_state = ibc_tendermint.ConsensusState( + root=root, + next_validators_hash=b"next_validators_hash", + ) + any_consensus_state = any_pb2.Any() + any_consensus_state.Pack(consensus_state) + proof = b"proof" + proof_height = ibc_client.Height( + revision_number=1, + revision_height=2, + ) + + ibc_client_servicer.consensus_state_responses.append( + ibc_client_query.QueryConsensusStateResponse( + consensus_state=any_consensus_state, + proof=proof, + proof_height=proof_height, + ) + ) + + api = self._api_instance(servicer=ibc_client_servicer) + + result_state = await api.fetch_consensus_state( + client_id="client-id", + revision_number=1, + revision_height=2, + latest_height=False, + ) + expected_state = { + "consensusState": { + "@type": "type.googleapis.com/ibc.lightclients.tendermint.v1.ConsensusState", + "root": { + "hash": base64.b64encode(root.hash).decode(), + }, + "nextValidatorsHash": base64.b64encode(consensus_state.next_validators_hash).decode(), + }, + "proof": base64.b64encode(proof).decode(), + "proofHeight": { + "revisionNumber": str(proof_height.revision_number), + "revisionHeight": str(proof_height.revision_height), + }, + } + + assert result_state == expected_state + + @pytest.mark.asyncio + async def test_fetch_consensus_states( + self, + ibc_client_servicer, + ): + root = ibc_commitment.MerkleRoot( + hash=b"root", + ) + consensus_state = ibc_tendermint.ConsensusState( + root=root, + next_validators_hash=b"next_validators_hash", + ) + any_consensus_state = any_pb2.Any() + any_consensus_state.Pack(consensus_state) + proof_height = ibc_client.Height( + revision_number=1, + revision_height=2, + ) + + state_with_height = ibc_client.ConsensusStateWithHeight( + consensus_state=any_consensus_state, + height=proof_height, + ) + result_pagination = pagination_pb.PageResponse( + next_key=b"\001\032\264\007Z\224$]\377s8\343\004-\265\267\314?B\341", + total=16036, + ) + + ibc_client_servicer.consensus_states_responses.append( + ibc_client_query.QueryConsensusStatesResponse( + consensus_states=[state_with_height], + pagination=result_pagination, + ) + ) + + api = self._api_instance(servicer=ibc_client_servicer) + + pagination_option = PaginationOption( + skip=10, + limit=30, + reverse=False, + count_total=True, + ) + + result_states = await api.fetch_consensus_states( + client_id="client-id", + pagination=pagination_option, + ) + expected_states = { + "consensusStates": [ + { + "consensusState": { + "@type": "type.googleapis.com/ibc.lightclients.tendermint.v1.ConsensusState", + "root": { + "hash": base64.b64encode(root.hash).decode(), + }, + "nextValidatorsHash": base64.b64encode(consensus_state.next_validators_hash).decode(), + }, + "height": { + "revisionNumber": str(proof_height.revision_number), + "revisionHeight": str(proof_height.revision_height), + }, + }, + ], + "pagination": { + "nextKey": base64.b64encode(result_pagination.next_key).decode(), + "total": str(result_pagination.total), + }, + } + + assert result_states == expected_states + + @pytest.mark.asyncio + async def test_fetch_consensus_state_heights( + self, + ibc_client_servicer, + ): + height = ibc_client.Height( + revision_number=1, + revision_height=2, + ) + result_pagination = pagination_pb.PageResponse( + next_key=b"\001\032\264\007Z\224$]\377s8\343\004-\265\267\314?B\341", + total=16036, + ) + + ibc_client_servicer.consensus_state_heights_responses.append( + ibc_client_query.QueryConsensusStateHeightsResponse( + consensus_state_heights=[height], + pagination=result_pagination, + ) + ) + + api = self._api_instance(servicer=ibc_client_servicer) + + pagination_option = PaginationOption( + skip=10, + limit=30, + reverse=False, + count_total=True, + ) + + result_heights = await api.fetch_consensus_state_heights( + client_id="client-id", + pagination=pagination_option, + ) + expected_heights = { + "consensusStateHeights": [ + { + "revisionNumber": str(height.revision_number), + "revisionHeight": str(height.revision_height), + }, + ], + "pagination": { + "nextKey": base64.b64encode(result_pagination.next_key).decode(), + "total": str(result_pagination.total), + }, + } + + assert result_heights == expected_heights + + @pytest.mark.asyncio + async def test_fetch_client_status( + self, + ibc_client_servicer, + ): + status = "Expired" + + ibc_client_servicer.client_status_responses.append(ibc_client_query.QueryClientStatusResponse(status=status)) + + api = self._api_instance(servicer=ibc_client_servicer) + + result_status = await api.fetch_client_status( + client_id="client-id", + ) + expected_status = {"status": status} + + assert result_status == expected_status + + @pytest.mark.asyncio + async def test_fetch_client_params( + self, + ibc_client_servicer, + ): + params = ibc_client.Params( + allowed_clients=["injective-1", "injective-2"], + ) + + ibc_client_servicer.client_params_responses.append(ibc_client_query.QueryClientParamsResponse(params=params)) + + api = self._api_instance(servicer=ibc_client_servicer) + + result_params = await api.fetch_client_params() + expected_params = { + "params": { + "allowedClients": params.allowed_clients, + }, + } + + assert result_params == expected_params + + @pytest.mark.asyncio + async def test_fetch_upgraded_client_state( + self, + ibc_client_servicer, + ): + trust_level = ibc_tendermint.Fraction( + numerator=1, + denominator=3, + ) + leaf_spec = ics23_proofs.LeafOp( + hash=0, + prehash_key=1, + prehash_value=2, + length=3, + prefix=b"prefix", + ) + inner_spec = ics23_proofs.InnerSpec( + child_order=[0, 1, 2, 3], + child_size=4, + min_prefix_length=5, + max_prefix_length=6, + empty_child=b"empty", + hash=1, + ) + proof_spec = ics23_proofs.ProofSpec( + leaf_spec=leaf_spec, + inner_spec=inner_spec, + max_depth=5, + min_depth=1, + prehash_key_before_comparison=True, + ) + tendermint_client_state = ibc_tendermint.ClientState( + chain_id="injective-1", + trust_level=trust_level, + frozen_height=ibc_client.Height( + revision_number=1, + revision_height=2, + ), + latest_height=ibc_client.Height( + revision_number=3, + revision_height=4, + ), + proof_specs=[proof_spec], + upgrade_path=["upgrade", "upgradedIBCState"], + allow_update_after_expiry=False, + allow_update_after_misbehaviour=False, + ) + any_client_state = any_pb2.Any() + any_client_state.Pack(tendermint_client_state) + + ibc_client_servicer.upgraded_client_state_responses.append( + ibc_client_query.QueryUpgradedClientStateResponse( + upgraded_client_state=any_client_state, + ) + ) + + api = self._api_instance(servicer=ibc_client_servicer) + + result_client_state = await api.fetch_upgraded_client_state() + expected_client_state = { + "upgradedClientState": { + "@type": "type.googleapis.com/ibc.lightclients.tendermint.v1.ClientState", + "chainId": tendermint_client_state.chain_id, + "trustLevel": { + "numerator": str(trust_level.numerator), + "denominator": str(trust_level.denominator), + }, + "frozenHeight": { + "revisionNumber": str(tendermint_client_state.frozen_height.revision_number), + "revisionHeight": str(tendermint_client_state.frozen_height.revision_height), + }, + "latestHeight": { + "revisionNumber": str(tendermint_client_state.latest_height.revision_number), + "revisionHeight": str(tendermint_client_state.latest_height.revision_height), + }, + "proofSpecs": [ + { + "leafSpec": { + "hash": ics23_proofs.HashOp.Name(leaf_spec.hash), + "prehashKey": ics23_proofs.HashOp.Name(leaf_spec.prehash_key), + "prehashValue": ics23_proofs.HashOp.Name(leaf_spec.prehash_value), + "length": ics23_proofs.LengthOp.Name(leaf_spec.length), + "prefix": base64.b64encode(leaf_spec.prefix).decode(), + }, + "innerSpec": { + "childOrder": inner_spec.child_order, + "childSize": inner_spec.child_size, + "minPrefixLength": inner_spec.min_prefix_length, + "maxPrefixLength": inner_spec.max_prefix_length, + "emptyChild": base64.b64encode(inner_spec.empty_child).decode(), + "hash": ics23_proofs.HashOp.Name(inner_spec.hash), + }, + "maxDepth": proof_spec.max_depth, + "minDepth": proof_spec.min_depth, + "prehashKeyBeforeComparison": proof_spec.prehash_key_before_comparison, + }, + ], + "upgradePath": tendermint_client_state.upgrade_path, + "allowUpdateAfterExpiry": tendermint_client_state.allow_update_after_expiry, + "allowUpdateAfterMisbehaviour": tendermint_client_state.allow_update_after_misbehaviour, + }, + } + + result_client_state["upgradedClientState"] = dict(result_client_state["upgradedClientState"]) + + assert result_client_state == expected_client_state + + @pytest.mark.asyncio + async def test_fetch_upgraded_consensus_state( + self, + ibc_client_servicer, + ): + root = ibc_commitment.MerkleRoot( + hash=b"root", + ) + consensus_state = ibc_tendermint.ConsensusState( + root=root, + next_validators_hash=b"next_validators_hash", + ) + any_consensus_state = any_pb2.Any() + any_consensus_state.Pack(consensus_state) + + ibc_client_servicer.upgraded_consensus_state_responses.append( + ibc_client_query.QueryUpgradedConsensusStateResponse( + upgraded_consensus_state=any_consensus_state, + ) + ) + + api = self._api_instance(servicer=ibc_client_servicer) + + result_state = await api.fetch_upgraded_consensus_state() + expected_state = { + "upgradedConsensusState": { + "@type": "type.googleapis.com/ibc.lightclients.tendermint.v1.ConsensusState", + "root": { + "hash": base64.b64encode(root.hash).decode(), + }, + "nextValidatorsHash": base64.b64encode(consensus_state.next_validators_hash).decode(), + }, + } + + assert result_state == expected_state + + def _api_instance(self, servicer): + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + cookie_assistant = DisabledCookieAssistant() + + api = IBCClientGrpcApi(channel=channel, cookie_assistant=cookie_assistant) + api._stub = servicer + + return api