From 8da302a67ccea69566212c68889ede62fa09ce65 Mon Sep 17 00:00:00 2001 From: abel Date: Mon, 25 Dec 2023 16:59:01 -0300 Subject: [PATCH 1/6] (feat) Added all endpoint for chain bank module --- examples/chain_client/50_SpendableBalances.py | 16 + .../51_SpendableBalancesByDenom.py | 17 + examples/chain_client/52_TotalSupply.py | 18 + examples/chain_client/53_SupplyOf.py | 15 + examples/chain_client/54_DenomMetadata.py | 16 + examples/chain_client/55_DenomsMetadata.py | 18 + examples/chain_client/56_DenomOwners.py | 20 ++ examples/chain_client/57_SendEnabled.py | 20 ++ pyinjective/async_client.py | 36 ++ .../client/chain/grpc/chain_grpc_bank_api.py | 82 ++++- .../grpc/configurable_bank_query_servicer.py | 32 ++ .../chain/grpc/test_chain_grpc_bank_api.py | 325 ++++++++++++++++++ 12 files changed, 612 insertions(+), 3 deletions(-) create mode 100644 examples/chain_client/50_SpendableBalances.py create mode 100644 examples/chain_client/51_SpendableBalancesByDenom.py create mode 100644 examples/chain_client/52_TotalSupply.py create mode 100644 examples/chain_client/53_SupplyOf.py create mode 100644 examples/chain_client/54_DenomMetadata.py create mode 100644 examples/chain_client/55_DenomsMetadata.py create mode 100644 examples/chain_client/56_DenomOwners.py create mode 100644 examples/chain_client/57_SendEnabled.py diff --git a/examples/chain_client/50_SpendableBalances.py b/examples/chain_client/50_SpendableBalances.py new file mode 100644 index 00000000..d13836e0 --- /dev/null +++ b/examples/chain_client/50_SpendableBalances.py @@ -0,0 +1,16 @@ +import asyncio + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + address = "inj1cml96vmptgw99syqrrz8az79xer2pcgp0a885r" + spendable_balances = await client.fetch_spendable_balances(address=address) + print(spendable_balances) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/51_SpendableBalancesByDenom.py b/examples/chain_client/51_SpendableBalancesByDenom.py new file mode 100644 index 00000000..639d7933 --- /dev/null +++ b/examples/chain_client/51_SpendableBalancesByDenom.py @@ -0,0 +1,17 @@ +import asyncio + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + address = "inj1cml96vmptgw99syqrrz8az79xer2pcgp0a885r" + denom = "inj" + spendable_balances = await client.fetch_spendable_balances_by_denom(address=address, denom=denom) + print(spendable_balances) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/52_TotalSupply.py b/examples/chain_client/52_TotalSupply.py new file mode 100644 index 00000000..8156c54b --- /dev/null +++ b/examples/chain_client/52_TotalSupply.py @@ -0,0 +1,18 @@ +import asyncio + +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) + total_supply = await client.fetch_total_supply( + pagination=PaginationOption(limit=10), + ) + print(total_supply) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/53_SupplyOf.py b/examples/chain_client/53_SupplyOf.py new file mode 100644 index 00000000..225b9db7 --- /dev/null +++ b/examples/chain_client/53_SupplyOf.py @@ -0,0 +1,15 @@ +import asyncio + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + supply_of = await client.fetch_supply_of(denom="inj") + print(supply_of) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/54_DenomMetadata.py b/examples/chain_client/54_DenomMetadata.py new file mode 100644 index 00000000..ff8a9337 --- /dev/null +++ b/examples/chain_client/54_DenomMetadata.py @@ -0,0 +1,16 @@ +import asyncio + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + denom = "factory/inj107aqkjc3t5r3l9j4n9lgrma5tm3jav8qgppz6m/position" + metadata = await client.fetch_denom_metadata(denom=denom) + print(metadata) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/55_DenomsMetadata.py b/examples/chain_client/55_DenomsMetadata.py new file mode 100644 index 00000000..26402f49 --- /dev/null +++ b/examples/chain_client/55_DenomsMetadata.py @@ -0,0 +1,18 @@ +import asyncio + +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) + denoms = await client.fetch_denoms_metadata( + pagination=PaginationOption(limit=10), + ) + print(denoms) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/56_DenomOwners.py b/examples/chain_client/56_DenomOwners.py new file mode 100644 index 00000000..3204cc34 --- /dev/null +++ b/examples/chain_client/56_DenomOwners.py @@ -0,0 +1,20 @@ +import asyncio + +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) + denom = "inj" + owners = await client.fetch_denom_owners( + denom=denom, + pagination=PaginationOption(limit=10), + ) + print(owners) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/57_SendEnabled.py b/examples/chain_client/57_SendEnabled.py new file mode 100644 index 00000000..03ed41d9 --- /dev/null +++ b/examples/chain_client/57_SendEnabled.py @@ -0,0 +1,20 @@ +import asyncio + +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) + denom = "inj" + enabled = await client.fetch_send_enabled( + denoms=[denom], + pagination=PaginationOption(limit=10), + ) + print(enabled) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/pyinjective/async_client.py b/pyinjective/async_client.py index 75c3d159..7480613b 100644 --- a/pyinjective/async_client.py +++ b/pyinjective/async_client.py @@ -522,6 +522,42 @@ async def get_bank_balance(self, address: str, denom: str): async def fetch_bank_balance(self, address: str, denom: str) -> Dict[str, Any]: return await self.bank_api.fetch_balance(account_address=address, denom=denom) + async def fetch_spendable_balances( + self, + address: str, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + return await self.bank_api.fetch_spendable_balances(account_address=address, pagination=pagination) + + async def fetch_spendable_balances_by_denom( + self, + address: str, + denom: str, + ) -> Dict[str, Any]: + return await self.bank_api.fetch_spendable_balances_by_denom(account_address=address, denom=denom) + + async def fetch_total_supply(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + return await self.bank_api.fetch_total_supply(pagination=pagination) + + async def fetch_supply_of(self, denom: str) -> Dict[str, Any]: + return await self.bank_api.fetch_supply_of(denom=denom) + + async def fetch_denom_metadata(self, denom: str) -> Dict[str, Any]: + return await self.bank_api.fetch_denom_metadata(denom=denom) + + async def fetch_denoms_metadata(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + return await self.bank_api.fetch_denoms_metadata(pagination=pagination) + + async def fetch_denom_owners(self, denom: str, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + return await self.bank_api.fetch_denom_owners(denom=denom, pagination=pagination) + + async def fetch_send_enabled( + self, + denoms: Optional[List[str]] = None, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + return await self.bank_api.fetch_send_enabled(denoms=denoms, pagination=pagination) + # Injective Exchange client methods # Auction RPC diff --git a/pyinjective/client/chain/grpc/chain_grpc_bank_api.py b/pyinjective/client/chain/grpc/chain_grpc_bank_api.py index b774707f..69d0cd0e 100644 --- a/pyinjective/client/chain/grpc/chain_grpc_bank_api.py +++ b/pyinjective/client/chain/grpc/chain_grpc_bank_api.py @@ -1,7 +1,8 @@ -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List, Optional from grpc.aio import Channel +from pyinjective.client.model.pagination import PaginationOption from pyinjective.proto.cosmos.bank.v1beta1 import query_pb2 as bank_query_pb, query_pb2_grpc as bank_query_grpc from pyinjective.utils.grpc_api_request_assistant import GrpcApiRequestAssistant @@ -29,11 +30,86 @@ async def fetch_balances(self, account_address: str) -> Dict[str, Any]: return response - async def fetch_total_supply(self) -> Dict[str, Any]: - request = bank_query_pb.QueryTotalSupplyRequest() + async def fetch_spendable_balances( + self, + account_address: str, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QuerySpendableBalancesRequest( + address=account_address, + pagination=pagination_request, + ) + response = await self._execute_call(call=self._stub.SpendableBalances, request=request) + + return response + + async def fetch_spendable_balances_by_denom( + self, + account_address: str, + denom: str, + ) -> Dict[str, Any]: + request = bank_query_pb.QuerySpendableBalanceByDenomRequest( + address=account_address, + denom=denom, + ) + response = await self._execute_call(call=self._stub.SpendableBalanceByDenom, request=request) + + return response + + async def fetch_total_supply(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QueryTotalSupplyRequest(pagination=pagination_request) response = await self._execute_call(call=self._stub.TotalSupply, request=request) return response + async def fetch_supply_of(self, denom: str) -> Dict[str, Any]: + request = bank_query_pb.QuerySupplyOfRequest(denom=denom) + response = await self._execute_call(call=self._stub.SupplyOf, request=request) + + return response + + async def fetch_denom_metadata(self, denom: str) -> Dict[str, Any]: + request = bank_query_pb.QueryDenomMetadataRequest(denom=denom) + response = await self._execute_call(call=self._stub.DenomMetadata, request=request) + + return response + + async def fetch_denoms_metadata(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QueryDenomsMetadataRequest(pagination=pagination_request) + response = await self._execute_call(call=self._stub.DenomsMetadata, request=request) + + return response + + async def fetch_denom_owners(self, denom: str, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QueryDenomOwnersRequest(denom=denom, pagination=pagination_request) + response = await self._execute_call(call=self._stub.DenomOwners, request=request) + + return response + + async def fetch_send_enabled( + self, + denoms: Optional[List[str]] = None, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QuerySendEnabledRequest(denoms=denoms, pagination=pagination_request) + response = await self._execute_call(call=self._stub.SendEnabled, 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/client/chain/grpc/configurable_bank_query_servicer.py b/tests/client/chain/grpc/configurable_bank_query_servicer.py index 015ce2c8..314a6622 100644 --- a/tests/client/chain/grpc/configurable_bank_query_servicer.py +++ b/tests/client/chain/grpc/configurable_bank_query_servicer.py @@ -9,7 +9,14 @@ def __init__(self): self.bank_params = deque() self.balance_responses = deque() self.balances_responses = deque() + self.spendable_balances_responses = deque() + self.spendable_balances_by_denom_responses = deque() self.total_supply_responses = deque() + self.supply_of_responses = deque() + self.denom_metadata_responses = deque() + self.denoms_metadata_responses = deque() + self.denom_owners_responses = deque() + self.send_enabled_responses = deque() async def Params(self, request: bank_query_pb.QueryParamsRequest, context=None, metadata=None): return self.bank_params.pop() @@ -20,5 +27,30 @@ async def Balance(self, request: bank_query_pb.QueryBalanceRequest, context=None async def AllBalances(self, request: bank_query_pb.QueryAllBalancesRequest, context=None, metadata=None): return self.balances_responses.pop() + async def SpendableBalances( + self, request: bank_query_pb.QuerySpendableBalancesRequest, context=None, metadata=None + ): + return self.spendable_balances_responses.pop() + + async def SpendableBalanceByDenom( + self, request: bank_query_pb.QuerySpendableBalanceByDenomRequest, context=None, metadata=None + ): + return self.spendable_balances_by_denom_responses.pop() + async def TotalSupply(self, request: bank_query_pb.QueryTotalSupplyRequest, context=None, metadata=None): return self.total_supply_responses.pop() + + async def SupplyOf(self, request: bank_query_pb.QuerySupplyOfRequest, context=None, metadata=None): + return self.supply_of_responses.pop() + + async def DenomMetadata(self, request: bank_query_pb.QueryDenomMetadataRequest, context=None, metadata=None): + return self.denom_metadata_responses.pop() + + async def DenomsMetadata(self, request: bank_query_pb.QueryDenomsMetadataRequest, context=None, metadata=None): + return self.denoms_metadata_responses.pop() + + async def DenomOwners(self, request: bank_query_pb.QueryDenomOwnersRequest, context=None, metadata=None): + return self.denom_owners_responses.pop() + + async def SendEnabled(self, request: bank_query_pb.QuerySendEnabledRequest, context=None, metadata=None): + return self.send_enabled_responses.pop() diff --git a/tests/client/chain/grpc/test_chain_grpc_bank_api.py b/tests/client/chain/grpc/test_chain_grpc_bank_api.py index 1c8c0ffa..f2c6cbdd 100644 --- a/tests/client/chain/grpc/test_chain_grpc_bank_api.py +++ b/tests/client/chain/grpc/test_chain_grpc_bank_api.py @@ -4,6 +4,7 @@ import pytest from pyinjective.client.chain.grpc.chain_grpc_bank_api import ChainGrpcBankApi +from pyinjective.client.model.pagination import PaginationOption from pyinjective.core.network import Network from pyinjective.proto.cosmos.bank.v1beta1 import bank_pb2 as bank_pb, query_pb2 as bank_query_pb from pyinjective.proto.cosmos.base.query.v1beta1 import pagination_pb2 as pagination_pb @@ -120,6 +121,69 @@ async def test_fetch_balances( assert expected_balances == bank_balances + @pytest.mark.asyncio + async def test_fetch_spendable_balances( + self, + bank_servicer, + ): + first_balance = coin_pb.Coin(denom="inj", amount="988987297011197594664") + second_balance = coin_pb.Coin(denom="peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5", amount="54497408") + pagination = pagination_pb.PageResponse(total=2) + + bank_servicer.spendable_balances_responses.append( + bank_query_pb.QuerySpendableBalancesResponse( + balances=[first_balance, second_balance], + pagination=pagination, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + balances = await api.fetch_spendable_balances( + account_address="inj1cml96vmptgw99syqrrz8az79xer2pcgp0a885r", + pagination=PaginationOption( + skip=0, + limit=100, + ), + ) + expected_balances = { + "balances": [{"denom": coin.denom, "amount": coin.amount} for coin in [first_balance, second_balance]], + "pagination": {"nextKey": "", "total": "2"}, + } + + assert expected_balances == balances + + @pytest.mark.asyncio + async def test_fetch_spendable_balances_by_denom( + self, + bank_servicer, + ): + first_balance = coin_pb.Coin(denom="inj", amount="988987297011197594664") + + bank_servicer.spendable_balances_by_denom_responses.append( + bank_query_pb.QuerySpendableBalanceByDenomResponse(balance=first_balance) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + balances = await api.fetch_spendable_balances_by_denom( + account_address="inj1cml96vmptgw99syqrrz8az79xer2pcgp0a885r", + denom=first_balance.denom, + ) + expected_balances = { + "balance": {"denom": first_balance.denom, "amount": first_balance.amount}, + } + + assert expected_balances == balances + @pytest.mark.asyncio async def test_fetch_total_supply( self, @@ -164,5 +228,266 @@ async def test_fetch_total_supply( assert expected_supply == total_supply + @pytest.mark.asyncio + async def test_fetch_supply_of( + self, + bank_servicer, + ): + first_supply = coin_pb.Coin( + denom="factory/inj108t3mlej0dph8er6ca2lq5cs9pdgzva5mqsn5p/position", amount="5556700000000000000" + ) + + bank_servicer.supply_of_responses.append( + bank_query_pb.QuerySupplyOfResponse( + amount=first_supply, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + total_supply = await api.fetch_supply_of(denom=first_supply.denom) + expected_supply = {"amount": {"denom": first_supply.denom, "amount": first_supply.amount}} + + assert expected_supply == total_supply + + @pytest.mark.asyncio + async def test_fetch_denom_metadata( + self, + bank_servicer, + ): + first_denom_unit = bank_pb.DenomUnit( + denom="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", exponent=0, aliases=["microSMART"] + ) + second_denom_unit = bank_pb.DenomUnit(denom="SMART", exponent=6, aliases=["SMART"]) + metadata = bank_pb.Metadata( + description="SMART", + denom_units=[first_denom_unit, second_denom_unit], + base="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", + display="SMART", + name="SMART", + symbol="SMART", + uri=( + "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/" + "Flag_of_the_People%27s_Republic_of_China.svg/" + "2560px-Flag_of_the_People%27s_Republic_of_China.svg.png" + ), + uri_hash="", + ) + + bank_servicer.denom_metadata_responses.append( + bank_query_pb.QueryDenomMetadataResponse( + metadata=metadata, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + denom_metadata = await api.fetch_denom_metadata(denom=metadata.base) + + expected_denom_metadata = { + "metadata": { + "description": metadata.description, + "denomUnits": [ + {"denom": denom.denom, "exponent": denom.exponent, "aliases": denom.aliases} + for denom in [first_denom_unit, second_denom_unit] + ], + "base": metadata.base, + "display": metadata.display, + "name": metadata.name, + "symbol": metadata.symbol, + "uri": metadata.uri, + "uriHash": metadata.uri_hash, + } + } + + assert expected_denom_metadata == denom_metadata + + @pytest.mark.asyncio + async def test_fetch_denoms_metadata( + self, + bank_servicer, + ): + first_denom_unit = bank_pb.DenomUnit( + denom="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", exponent=0, aliases=["microSMART"] + ) + second_denom_unit = bank_pb.DenomUnit(denom="SMART", exponent=6, aliases=["SMART"]) + metadata = bank_pb.Metadata( + description="SMART", + denom_units=[first_denom_unit, second_denom_unit], + base="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", + display="SMART", + name="SMART", + symbol="SMART", + uri=( + "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/" + "Flag_of_the_People%27s_Republic_of_China.svg/" + "2560px-Flag_of_the_People%27s_Republic_of_China.svg.png" + ), + uri_hash="", + ) + pagination = pagination_pb.PageResponse( + next_key=( + "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/" "inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + ).encode(), + total=179, + ) + + bank_servicer.denoms_metadata_responses.append( + bank_query_pb.QueryDenomsMetadataResponse( + metadatas=[metadata], + pagination=pagination, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + denoms_metadata = await api.fetch_denoms_metadata( + pagination=PaginationOption( + skip=0, + limit=100, + ), + ) + next_key = "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + expected_denoms_metadata = { + "metadatas": [ + { + "description": metadata.description, + "denomUnits": [ + {"denom": denom.denom, "exponent": denom.exponent, "aliases": denom.aliases} + for denom in [first_denom_unit, second_denom_unit] + ], + "base": metadata.base, + "display": metadata.display, + "name": metadata.name, + "symbol": metadata.symbol, + "uri": metadata.uri, + "uriHash": metadata.uri_hash, + }, + ], + "pagination": { + "nextKey": base64.b64encode(next_key.encode()).decode(), + "total": "179", + }, + } + + assert expected_denoms_metadata == denoms_metadata + + @pytest.mark.asyncio + async def test_fetch_denom_owners( + self, + bank_servicer, + ): + balance = coin_pb.Coin(denom="inj", amount="988987297011197594664") + denom_owner = bank_query_pb.DenomOwner(address="inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r", balance=balance) + pagination = pagination_pb.PageResponse( + next_key=( + "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/" "inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + ).encode(), + total=179, + ) + + bank_servicer.denom_owners_responses.append( + bank_query_pb.QueryDenomOwnersResponse( + denom_owners=[denom_owner], + pagination=pagination, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + denoms_metadata = await api.fetch_denom_owners( + denom=balance.denom, + pagination=PaginationOption( + skip=0, + limit=100, + ), + ) + next_key = "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + expected_denoms_metadata = { + "denomOwners": [ + { + "address": denom_owner.address, + "balance": { + "denom": balance.denom, + "amount": balance.amount, + }, + }, + ], + "pagination": { + "nextKey": base64.b64encode(next_key.encode()).decode(), + "total": "179", + }, + } + + assert expected_denoms_metadata == denoms_metadata + + @pytest.mark.asyncio + async def test_fetch_send_enabled( + self, + bank_servicer, + ): + send_enabled = bank_pb.SendEnabled( + denom="inj", + enabled=True, + ) + pagination = pagination_pb.PageResponse( + next_key=( + "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/" "inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + ).encode(), + total=179, + ) + + bank_servicer.send_enabled_responses.append( + bank_query_pb.QuerySendEnabledResponse( + send_enabled=[send_enabled], + pagination=pagination, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + denoms_metadata = await api.fetch_send_enabled( + denoms=[send_enabled.denom], + pagination=PaginationOption( + skip=0, + limit=100, + ), + ) + next_key = "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + expected_denoms_metadata = { + "sendEnabled": [ + { + "denom": send_enabled.denom, + "enabled": send_enabled.enabled, + }, + ], + "pagination": { + "nextKey": base64.b64encode(next_key.encode()).decode(), + "total": "179", + }, + } + + assert expected_denoms_metadata == denoms_metadata + async def _dummy_metadata_provider(self): return None From 50d93a43af8899fc340fb7ccde77aa74afee664a Mon Sep 17 00:00:00 2001 From: abel Date: Mon, 25 Dec 2023 23:26:52 -0300 Subject: [PATCH 2/6] (feat) Added logic to initialize tokens in AsyncClient using the denoms metadata from chain bank module --- pyinjective/async_client.py | 46 ++++++++++++++++++ pyinjective/client/model/pagination.py | 2 +- tests/rpc_fixtures/markets_fixtures.py | 26 ++++++++++ tests/test_async_client.py | 66 ++++++++++++++++++++++---- 4 files changed, 131 insertions(+), 9 deletions(-) diff --git a/pyinjective/async_client.py b/pyinjective/async_client.py index 7480613b..ebd140fd 100644 --- a/pyinjective/async_client.py +++ b/pyinjective/async_client.py @@ -1,4 +1,5 @@ import asyncio +import base64 import time from copy import deepcopy from decimal import Decimal @@ -2495,6 +2496,51 @@ async def composer(self): tokens=await self.all_tokens(), ) + async def initialize_tokens_from_chain_denoms(self): + # force initialization of markets and tokens + await self.all_tokens() + + all_denoms_metadata = [] + + query_result = await self.fetch_denoms_metadata() + + all_denoms_metadata.extend(query_result.get("metadatas", [])) + next_key = query_result.get("pagination", {}).get("nextKey", "") + + while next_key != "": + query_result = await self.fetch_denoms_metadata(pagination=PaginationOption(key=next_key)) + + all_denoms_metadata.extend(query_result.get("metadatas", [])) + result_next_key = query_result.get("pagination", {}).get("nextKey", "") + next_key = base64.b64decode(result_next_key).decode() + + for token_metadata in all_denoms_metadata: + symbol = token_metadata["symbol"] + denom = token_metadata["base"] + + if denom != "" and symbol != "" and denom not in self._tokens_by_denom: + name = token_metadata["name"] or symbol + decimals = max({denom_unit["exponent"] for denom_unit in token_metadata["denomUnits"]}) + + unique_symbol = denom + for symbol_candidate in [symbol, name]: + if symbol_candidate not in self._tokens_by_symbol: + unique_symbol = symbol_candidate + break + + token = Token( + name=name, + symbol=symbol, + denom=denom, + address="", + decimals=decimals, + logo=token_metadata["uri"], + updated=-1, + ) + + self._tokens_by_denom[denom] = token + self._tokens_by_symbol[unique_symbol] = token + async def _initialize_tokens_and_markets(self): spot_markets = dict() derivative_markets = dict() diff --git a/pyinjective/client/model/pagination.py b/pyinjective/client/model/pagination.py index f3607c17..906a6060 100644 --- a/pyinjective/client/model/pagination.py +++ b/pyinjective/client/model/pagination.py @@ -31,7 +31,7 @@ def create_pagination_request(self) -> pagination_pb.PageRequest: page_request = pagination_pb.PageRequest() if self.key is not None: - page_request.key = bytes.fromhex(self.key) + page_request.key = self.key.encode() if self.skip is not None: page_request.offset = self.skip if self.limit is not None: diff --git a/tests/rpc_fixtures/markets_fixtures.py b/tests/rpc_fixtures/markets_fixtures.py index 69f835df..9c3a56ad 100644 --- a/tests/rpc_fixtures/markets_fixtures.py +++ b/tests/rpc_fixtures/markets_fixtures.py @@ -1,6 +1,32 @@ import pytest +@pytest.fixture +def smart_denom_metadata(): + from pyinjective.proto.cosmos.bank.v1beta1 import bank_pb2 as bank_pb + + first_denom_unit = bank_pb.DenomUnit( + denom="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", exponent=0, aliases=["microSMART"] + ) + second_denom_unit = bank_pb.DenomUnit(denom="SMART", exponent=6, aliases=["SMART"]) + metadata = bank_pb.Metadata( + description="SMART", + denom_units=[first_denom_unit, second_denom_unit], + base="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", + display="SMART", + name="SMART", + symbol="SMART", + uri=( + "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/" + "Flag_of_the_People%27s_Republic_of_China.svg/" + "2560px-Flag_of_the_People%27s_Republic_of_China.svg.png" + ), + uri_hash="", + ) + + return metadata + + @pytest.fixture def inj_token_meta(): from pyinjective.proto.exchange.injective_spot_exchange_rpc_pb2 import TokenMeta diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 7780e088..b7b597ed 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -4,22 +4,31 @@ from pyinjective.async_client import AsyncClient from pyinjective.core.network import Network +from pyinjective.proto.cosmos.bank.v1beta1 import query_pb2 as bank_query_pb +from pyinjective.proto.cosmos.base.query.v1beta1 import pagination_pb2 as pagination_pb from pyinjective.proto.exchange import injective_derivative_exchange_rpc_pb2, injective_spot_exchange_rpc_pb2 +from tests.client.chain.grpc.configurable_bank_query_servicer import ConfigurableBankQueryServicer from tests.client.indexer.configurable_derivative_query_servicer import ConfigurableDerivativeQueryServicer from tests.client.indexer.configurable_spot_query_servicer import ConfigurableSpotQueryServicer -from tests.rpc_fixtures.markets_fixtures import ape_token_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import ape_usdt_spot_market_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import btc_usdt_perp_market_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import inj_token_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import inj_usdt_spot_market_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import usdt_perp_token_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import usdt_token_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import ( # noqa: F401; noqa: F401; noqa: F401 +from tests.rpc_fixtures.markets_fixtures import ( # noqa: F401 + ape_token_meta, + ape_usdt_spot_market_meta, + btc_usdt_perp_market_meta, first_match_bet_market_meta, + inj_token_meta, + inj_usdt_spot_market_meta, + smart_denom_metadata, + usdt_perp_token_meta, + usdt_token_meta, usdt_token_meta_second_denom, ) +@pytest.fixture +def bank_servicer(): + return ConfigurableBankQueryServicer() + + @pytest.fixture def spot_servicer(): return ConfigurableSpotQueryServicer() @@ -127,3 +136,44 @@ async def test_initialize_tokens_and_markets( assert any( (first_match_bet_market_meta.market_id == market.id for market in all_binary_option_markets.values()) ) + + @pytest.mark.asyncio + async def test_initialize_tokens_from_chain_denoms( + self, + bank_servicer, + spot_servicer, + derivative_servicer, + smart_denom_metadata, + ): + pagination = pagination_pb.PageResponse( + total=1, + ) + + bank_servicer.denoms_metadata_responses.append( + bank_query_pb.QueryDenomsMetadataResponse( + metadatas=[smart_denom_metadata], + pagination=pagination, + ) + ) + + spot_servicer.markets_responses.append(injective_spot_exchange_rpc_pb2.MarketsResponse(markets=[])) + derivative_servicer.markets_responses.append(injective_derivative_exchange_rpc_pb2.MarketsResponse(markets=[])) + derivative_servicer.binary_options_markets_responses.append( + injective_derivative_exchange_rpc_pb2.BinaryOptionsMarketsResponse(markets=[]) + ) + + client = AsyncClient( + network=Network.local(), + insecure=False, + ) + + client.bank_api._stub = bank_servicer + client.exchange_spot_api._stub = spot_servicer + client.exchange_derivative_api._stub = derivative_servicer + + await client._initialize_tokens_and_markets() + await client.initialize_tokens_from_chain_denoms() + + all_tokens = await client.all_tokens() + assert 1 == len(all_tokens) + assert smart_denom_metadata.symbol in all_tokens From ff3aa9df2afde5b66ba7977b89df89560710102e Mon Sep 17 00:00:00 2001 From: abel Date: Mon, 25 Dec 2023 16:59:01 -0300 Subject: [PATCH 3/6] (feat) Added all endpoint for chain bank module --- examples/chain_client/50_SpendableBalances.py | 16 + .../51_SpendableBalancesByDenom.py | 17 + examples/chain_client/52_TotalSupply.py | 18 + examples/chain_client/53_SupplyOf.py | 15 + examples/chain_client/54_DenomMetadata.py | 16 + examples/chain_client/55_DenomsMetadata.py | 18 + examples/chain_client/56_DenomOwners.py | 20 ++ examples/chain_client/57_SendEnabled.py | 20 ++ pyinjective/async_client.py | 36 ++ .../client/chain/grpc/chain_grpc_bank_api.py | 82 ++++- .../grpc/configurable_bank_query_servicer.py | 32 ++ .../chain/grpc/test_chain_grpc_bank_api.py | 325 ++++++++++++++++++ 12 files changed, 612 insertions(+), 3 deletions(-) create mode 100644 examples/chain_client/50_SpendableBalances.py create mode 100644 examples/chain_client/51_SpendableBalancesByDenom.py create mode 100644 examples/chain_client/52_TotalSupply.py create mode 100644 examples/chain_client/53_SupplyOf.py create mode 100644 examples/chain_client/54_DenomMetadata.py create mode 100644 examples/chain_client/55_DenomsMetadata.py create mode 100644 examples/chain_client/56_DenomOwners.py create mode 100644 examples/chain_client/57_SendEnabled.py diff --git a/examples/chain_client/50_SpendableBalances.py b/examples/chain_client/50_SpendableBalances.py new file mode 100644 index 00000000..d13836e0 --- /dev/null +++ b/examples/chain_client/50_SpendableBalances.py @@ -0,0 +1,16 @@ +import asyncio + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + address = "inj1cml96vmptgw99syqrrz8az79xer2pcgp0a885r" + spendable_balances = await client.fetch_spendable_balances(address=address) + print(spendable_balances) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/51_SpendableBalancesByDenom.py b/examples/chain_client/51_SpendableBalancesByDenom.py new file mode 100644 index 00000000..639d7933 --- /dev/null +++ b/examples/chain_client/51_SpendableBalancesByDenom.py @@ -0,0 +1,17 @@ +import asyncio + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + address = "inj1cml96vmptgw99syqrrz8az79xer2pcgp0a885r" + denom = "inj" + spendable_balances = await client.fetch_spendable_balances_by_denom(address=address, denom=denom) + print(spendable_balances) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/52_TotalSupply.py b/examples/chain_client/52_TotalSupply.py new file mode 100644 index 00000000..8156c54b --- /dev/null +++ b/examples/chain_client/52_TotalSupply.py @@ -0,0 +1,18 @@ +import asyncio + +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) + total_supply = await client.fetch_total_supply( + pagination=PaginationOption(limit=10), + ) + print(total_supply) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/53_SupplyOf.py b/examples/chain_client/53_SupplyOf.py new file mode 100644 index 00000000..225b9db7 --- /dev/null +++ b/examples/chain_client/53_SupplyOf.py @@ -0,0 +1,15 @@ +import asyncio + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + supply_of = await client.fetch_supply_of(denom="inj") + print(supply_of) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/54_DenomMetadata.py b/examples/chain_client/54_DenomMetadata.py new file mode 100644 index 00000000..ff8a9337 --- /dev/null +++ b/examples/chain_client/54_DenomMetadata.py @@ -0,0 +1,16 @@ +import asyncio + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + denom = "factory/inj107aqkjc3t5r3l9j4n9lgrma5tm3jav8qgppz6m/position" + metadata = await client.fetch_denom_metadata(denom=denom) + print(metadata) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/55_DenomsMetadata.py b/examples/chain_client/55_DenomsMetadata.py new file mode 100644 index 00000000..26402f49 --- /dev/null +++ b/examples/chain_client/55_DenomsMetadata.py @@ -0,0 +1,18 @@ +import asyncio + +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) + denoms = await client.fetch_denoms_metadata( + pagination=PaginationOption(limit=10), + ) + print(denoms) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/56_DenomOwners.py b/examples/chain_client/56_DenomOwners.py new file mode 100644 index 00000000..3204cc34 --- /dev/null +++ b/examples/chain_client/56_DenomOwners.py @@ -0,0 +1,20 @@ +import asyncio + +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) + denom = "inj" + owners = await client.fetch_denom_owners( + denom=denom, + pagination=PaginationOption(limit=10), + ) + print(owners) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/57_SendEnabled.py b/examples/chain_client/57_SendEnabled.py new file mode 100644 index 00000000..03ed41d9 --- /dev/null +++ b/examples/chain_client/57_SendEnabled.py @@ -0,0 +1,20 @@ +import asyncio + +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) + denom = "inj" + enabled = await client.fetch_send_enabled( + denoms=[denom], + pagination=PaginationOption(limit=10), + ) + print(enabled) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/pyinjective/async_client.py b/pyinjective/async_client.py index 3bfd30d1..f437db7f 100644 --- a/pyinjective/async_client.py +++ b/pyinjective/async_client.py @@ -522,6 +522,42 @@ async def get_bank_balance(self, address: str, denom: str): async def fetch_bank_balance(self, address: str, denom: str) -> Dict[str, Any]: return await self.bank_api.fetch_balance(account_address=address, denom=denom) + async def fetch_spendable_balances( + self, + address: str, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + return await self.bank_api.fetch_spendable_balances(account_address=address, pagination=pagination) + + async def fetch_spendable_balances_by_denom( + self, + address: str, + denom: str, + ) -> Dict[str, Any]: + return await self.bank_api.fetch_spendable_balances_by_denom(account_address=address, denom=denom) + + async def fetch_total_supply(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + return await self.bank_api.fetch_total_supply(pagination=pagination) + + async def fetch_supply_of(self, denom: str) -> Dict[str, Any]: + return await self.bank_api.fetch_supply_of(denom=denom) + + async def fetch_denom_metadata(self, denom: str) -> Dict[str, Any]: + return await self.bank_api.fetch_denom_metadata(denom=denom) + + async def fetch_denoms_metadata(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + return await self.bank_api.fetch_denoms_metadata(pagination=pagination) + + async def fetch_denom_owners(self, denom: str, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + return await self.bank_api.fetch_denom_owners(denom=denom, pagination=pagination) + + async def fetch_send_enabled( + self, + denoms: Optional[List[str]] = None, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + return await self.bank_api.fetch_send_enabled(denoms=denoms, pagination=pagination) + # Injective Exchange client methods # Auction RPC diff --git a/pyinjective/client/chain/grpc/chain_grpc_bank_api.py b/pyinjective/client/chain/grpc/chain_grpc_bank_api.py index b774707f..69d0cd0e 100644 --- a/pyinjective/client/chain/grpc/chain_grpc_bank_api.py +++ b/pyinjective/client/chain/grpc/chain_grpc_bank_api.py @@ -1,7 +1,8 @@ -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List, Optional from grpc.aio import Channel +from pyinjective.client.model.pagination import PaginationOption from pyinjective.proto.cosmos.bank.v1beta1 import query_pb2 as bank_query_pb, query_pb2_grpc as bank_query_grpc from pyinjective.utils.grpc_api_request_assistant import GrpcApiRequestAssistant @@ -29,11 +30,86 @@ async def fetch_balances(self, account_address: str) -> Dict[str, Any]: return response - async def fetch_total_supply(self) -> Dict[str, Any]: - request = bank_query_pb.QueryTotalSupplyRequest() + async def fetch_spendable_balances( + self, + account_address: str, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QuerySpendableBalancesRequest( + address=account_address, + pagination=pagination_request, + ) + response = await self._execute_call(call=self._stub.SpendableBalances, request=request) + + return response + + async def fetch_spendable_balances_by_denom( + self, + account_address: str, + denom: str, + ) -> Dict[str, Any]: + request = bank_query_pb.QuerySpendableBalanceByDenomRequest( + address=account_address, + denom=denom, + ) + response = await self._execute_call(call=self._stub.SpendableBalanceByDenom, request=request) + + return response + + async def fetch_total_supply(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QueryTotalSupplyRequest(pagination=pagination_request) response = await self._execute_call(call=self._stub.TotalSupply, request=request) return response + async def fetch_supply_of(self, denom: str) -> Dict[str, Any]: + request = bank_query_pb.QuerySupplyOfRequest(denom=denom) + response = await self._execute_call(call=self._stub.SupplyOf, request=request) + + return response + + async def fetch_denom_metadata(self, denom: str) -> Dict[str, Any]: + request = bank_query_pb.QueryDenomMetadataRequest(denom=denom) + response = await self._execute_call(call=self._stub.DenomMetadata, request=request) + + return response + + async def fetch_denoms_metadata(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QueryDenomsMetadataRequest(pagination=pagination_request) + response = await self._execute_call(call=self._stub.DenomsMetadata, request=request) + + return response + + async def fetch_denom_owners(self, denom: str, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QueryDenomOwnersRequest(denom=denom, pagination=pagination_request) + response = await self._execute_call(call=self._stub.DenomOwners, request=request) + + return response + + async def fetch_send_enabled( + self, + denoms: Optional[List[str]] = None, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QuerySendEnabledRequest(denoms=denoms, pagination=pagination_request) + response = await self._execute_call(call=self._stub.SendEnabled, 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/client/chain/grpc/configurable_bank_query_servicer.py b/tests/client/chain/grpc/configurable_bank_query_servicer.py index 015ce2c8..314a6622 100644 --- a/tests/client/chain/grpc/configurable_bank_query_servicer.py +++ b/tests/client/chain/grpc/configurable_bank_query_servicer.py @@ -9,7 +9,14 @@ def __init__(self): self.bank_params = deque() self.balance_responses = deque() self.balances_responses = deque() + self.spendable_balances_responses = deque() + self.spendable_balances_by_denom_responses = deque() self.total_supply_responses = deque() + self.supply_of_responses = deque() + self.denom_metadata_responses = deque() + self.denoms_metadata_responses = deque() + self.denom_owners_responses = deque() + self.send_enabled_responses = deque() async def Params(self, request: bank_query_pb.QueryParamsRequest, context=None, metadata=None): return self.bank_params.pop() @@ -20,5 +27,30 @@ async def Balance(self, request: bank_query_pb.QueryBalanceRequest, context=None async def AllBalances(self, request: bank_query_pb.QueryAllBalancesRequest, context=None, metadata=None): return self.balances_responses.pop() + async def SpendableBalances( + self, request: bank_query_pb.QuerySpendableBalancesRequest, context=None, metadata=None + ): + return self.spendable_balances_responses.pop() + + async def SpendableBalanceByDenom( + self, request: bank_query_pb.QuerySpendableBalanceByDenomRequest, context=None, metadata=None + ): + return self.spendable_balances_by_denom_responses.pop() + async def TotalSupply(self, request: bank_query_pb.QueryTotalSupplyRequest, context=None, metadata=None): return self.total_supply_responses.pop() + + async def SupplyOf(self, request: bank_query_pb.QuerySupplyOfRequest, context=None, metadata=None): + return self.supply_of_responses.pop() + + async def DenomMetadata(self, request: bank_query_pb.QueryDenomMetadataRequest, context=None, metadata=None): + return self.denom_metadata_responses.pop() + + async def DenomsMetadata(self, request: bank_query_pb.QueryDenomsMetadataRequest, context=None, metadata=None): + return self.denoms_metadata_responses.pop() + + async def DenomOwners(self, request: bank_query_pb.QueryDenomOwnersRequest, context=None, metadata=None): + return self.denom_owners_responses.pop() + + async def SendEnabled(self, request: bank_query_pb.QuerySendEnabledRequest, context=None, metadata=None): + return self.send_enabled_responses.pop() diff --git a/tests/client/chain/grpc/test_chain_grpc_bank_api.py b/tests/client/chain/grpc/test_chain_grpc_bank_api.py index 1c8c0ffa..f2c6cbdd 100644 --- a/tests/client/chain/grpc/test_chain_grpc_bank_api.py +++ b/tests/client/chain/grpc/test_chain_grpc_bank_api.py @@ -4,6 +4,7 @@ import pytest from pyinjective.client.chain.grpc.chain_grpc_bank_api import ChainGrpcBankApi +from pyinjective.client.model.pagination import PaginationOption from pyinjective.core.network import Network from pyinjective.proto.cosmos.bank.v1beta1 import bank_pb2 as bank_pb, query_pb2 as bank_query_pb from pyinjective.proto.cosmos.base.query.v1beta1 import pagination_pb2 as pagination_pb @@ -120,6 +121,69 @@ async def test_fetch_balances( assert expected_balances == bank_balances + @pytest.mark.asyncio + async def test_fetch_spendable_balances( + self, + bank_servicer, + ): + first_balance = coin_pb.Coin(denom="inj", amount="988987297011197594664") + second_balance = coin_pb.Coin(denom="peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5", amount="54497408") + pagination = pagination_pb.PageResponse(total=2) + + bank_servicer.spendable_balances_responses.append( + bank_query_pb.QuerySpendableBalancesResponse( + balances=[first_balance, second_balance], + pagination=pagination, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + balances = await api.fetch_spendable_balances( + account_address="inj1cml96vmptgw99syqrrz8az79xer2pcgp0a885r", + pagination=PaginationOption( + skip=0, + limit=100, + ), + ) + expected_balances = { + "balances": [{"denom": coin.denom, "amount": coin.amount} for coin in [first_balance, second_balance]], + "pagination": {"nextKey": "", "total": "2"}, + } + + assert expected_balances == balances + + @pytest.mark.asyncio + async def test_fetch_spendable_balances_by_denom( + self, + bank_servicer, + ): + first_balance = coin_pb.Coin(denom="inj", amount="988987297011197594664") + + bank_servicer.spendable_balances_by_denom_responses.append( + bank_query_pb.QuerySpendableBalanceByDenomResponse(balance=first_balance) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + balances = await api.fetch_spendable_balances_by_denom( + account_address="inj1cml96vmptgw99syqrrz8az79xer2pcgp0a885r", + denom=first_balance.denom, + ) + expected_balances = { + "balance": {"denom": first_balance.denom, "amount": first_balance.amount}, + } + + assert expected_balances == balances + @pytest.mark.asyncio async def test_fetch_total_supply( self, @@ -164,5 +228,266 @@ async def test_fetch_total_supply( assert expected_supply == total_supply + @pytest.mark.asyncio + async def test_fetch_supply_of( + self, + bank_servicer, + ): + first_supply = coin_pb.Coin( + denom="factory/inj108t3mlej0dph8er6ca2lq5cs9pdgzva5mqsn5p/position", amount="5556700000000000000" + ) + + bank_servicer.supply_of_responses.append( + bank_query_pb.QuerySupplyOfResponse( + amount=first_supply, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + total_supply = await api.fetch_supply_of(denom=first_supply.denom) + expected_supply = {"amount": {"denom": first_supply.denom, "amount": first_supply.amount}} + + assert expected_supply == total_supply + + @pytest.mark.asyncio + async def test_fetch_denom_metadata( + self, + bank_servicer, + ): + first_denom_unit = bank_pb.DenomUnit( + denom="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", exponent=0, aliases=["microSMART"] + ) + second_denom_unit = bank_pb.DenomUnit(denom="SMART", exponent=6, aliases=["SMART"]) + metadata = bank_pb.Metadata( + description="SMART", + denom_units=[first_denom_unit, second_denom_unit], + base="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", + display="SMART", + name="SMART", + symbol="SMART", + uri=( + "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/" + "Flag_of_the_People%27s_Republic_of_China.svg/" + "2560px-Flag_of_the_People%27s_Republic_of_China.svg.png" + ), + uri_hash="", + ) + + bank_servicer.denom_metadata_responses.append( + bank_query_pb.QueryDenomMetadataResponse( + metadata=metadata, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + denom_metadata = await api.fetch_denom_metadata(denom=metadata.base) + + expected_denom_metadata = { + "metadata": { + "description": metadata.description, + "denomUnits": [ + {"denom": denom.denom, "exponent": denom.exponent, "aliases": denom.aliases} + for denom in [first_denom_unit, second_denom_unit] + ], + "base": metadata.base, + "display": metadata.display, + "name": metadata.name, + "symbol": metadata.symbol, + "uri": metadata.uri, + "uriHash": metadata.uri_hash, + } + } + + assert expected_denom_metadata == denom_metadata + + @pytest.mark.asyncio + async def test_fetch_denoms_metadata( + self, + bank_servicer, + ): + first_denom_unit = bank_pb.DenomUnit( + denom="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", exponent=0, aliases=["microSMART"] + ) + second_denom_unit = bank_pb.DenomUnit(denom="SMART", exponent=6, aliases=["SMART"]) + metadata = bank_pb.Metadata( + description="SMART", + denom_units=[first_denom_unit, second_denom_unit], + base="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", + display="SMART", + name="SMART", + symbol="SMART", + uri=( + "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/" + "Flag_of_the_People%27s_Republic_of_China.svg/" + "2560px-Flag_of_the_People%27s_Republic_of_China.svg.png" + ), + uri_hash="", + ) + pagination = pagination_pb.PageResponse( + next_key=( + "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/" "inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + ).encode(), + total=179, + ) + + bank_servicer.denoms_metadata_responses.append( + bank_query_pb.QueryDenomsMetadataResponse( + metadatas=[metadata], + pagination=pagination, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + denoms_metadata = await api.fetch_denoms_metadata( + pagination=PaginationOption( + skip=0, + limit=100, + ), + ) + next_key = "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + expected_denoms_metadata = { + "metadatas": [ + { + "description": metadata.description, + "denomUnits": [ + {"denom": denom.denom, "exponent": denom.exponent, "aliases": denom.aliases} + for denom in [first_denom_unit, second_denom_unit] + ], + "base": metadata.base, + "display": metadata.display, + "name": metadata.name, + "symbol": metadata.symbol, + "uri": metadata.uri, + "uriHash": metadata.uri_hash, + }, + ], + "pagination": { + "nextKey": base64.b64encode(next_key.encode()).decode(), + "total": "179", + }, + } + + assert expected_denoms_metadata == denoms_metadata + + @pytest.mark.asyncio + async def test_fetch_denom_owners( + self, + bank_servicer, + ): + balance = coin_pb.Coin(denom="inj", amount="988987297011197594664") + denom_owner = bank_query_pb.DenomOwner(address="inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r", balance=balance) + pagination = pagination_pb.PageResponse( + next_key=( + "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/" "inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + ).encode(), + total=179, + ) + + bank_servicer.denom_owners_responses.append( + bank_query_pb.QueryDenomOwnersResponse( + denom_owners=[denom_owner], + pagination=pagination, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + denoms_metadata = await api.fetch_denom_owners( + denom=balance.denom, + pagination=PaginationOption( + skip=0, + limit=100, + ), + ) + next_key = "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + expected_denoms_metadata = { + "denomOwners": [ + { + "address": denom_owner.address, + "balance": { + "denom": balance.denom, + "amount": balance.amount, + }, + }, + ], + "pagination": { + "nextKey": base64.b64encode(next_key.encode()).decode(), + "total": "179", + }, + } + + assert expected_denoms_metadata == denoms_metadata + + @pytest.mark.asyncio + async def test_fetch_send_enabled( + self, + bank_servicer, + ): + send_enabled = bank_pb.SendEnabled( + denom="inj", + enabled=True, + ) + pagination = pagination_pb.PageResponse( + next_key=( + "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/" "inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + ).encode(), + total=179, + ) + + bank_servicer.send_enabled_responses.append( + bank_query_pb.QuerySendEnabledResponse( + send_enabled=[send_enabled], + pagination=pagination, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + denoms_metadata = await api.fetch_send_enabled( + denoms=[send_enabled.denom], + pagination=PaginationOption( + skip=0, + limit=100, + ), + ) + next_key = "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + expected_denoms_metadata = { + "sendEnabled": [ + { + "denom": send_enabled.denom, + "enabled": send_enabled.enabled, + }, + ], + "pagination": { + "nextKey": base64.b64encode(next_key.encode()).decode(), + "total": "179", + }, + } + + assert expected_denoms_metadata == denoms_metadata + async def _dummy_metadata_provider(self): return None From 65281889a10ea906b74ae12bca714313c645cf3f Mon Sep 17 00:00:00 2001 From: abel Date: Mon, 25 Dec 2023 23:26:52 -0300 Subject: [PATCH 4/6] (feat) Added logic to initialize tokens in AsyncClient using the denoms metadata from chain bank module --- pyinjective/async_client.py | 46 ++++++++++++++++++ pyinjective/client/model/pagination.py | 2 +- tests/rpc_fixtures/markets_fixtures.py | 26 ++++++++++ tests/test_async_client.py | 66 ++++++++++++++++++++++---- 4 files changed, 131 insertions(+), 9 deletions(-) diff --git a/pyinjective/async_client.py b/pyinjective/async_client.py index f437db7f..50eddecf 100644 --- a/pyinjective/async_client.py +++ b/pyinjective/async_client.py @@ -1,4 +1,5 @@ import asyncio +import base64 import time from copy import deepcopy from decimal import Decimal @@ -2497,6 +2498,51 @@ async def composer(self): tokens=await self.all_tokens(), ) + async def initialize_tokens_from_chain_denoms(self): + # force initialization of markets and tokens + await self.all_tokens() + + all_denoms_metadata = [] + + query_result = await self.fetch_denoms_metadata() + + all_denoms_metadata.extend(query_result.get("metadatas", [])) + next_key = query_result.get("pagination", {}).get("nextKey", "") + + while next_key != "": + query_result = await self.fetch_denoms_metadata(pagination=PaginationOption(key=next_key)) + + all_denoms_metadata.extend(query_result.get("metadatas", [])) + result_next_key = query_result.get("pagination", {}).get("nextKey", "") + next_key = base64.b64decode(result_next_key).decode() + + for token_metadata in all_denoms_metadata: + symbol = token_metadata["symbol"] + denom = token_metadata["base"] + + if denom != "" and symbol != "" and denom not in self._tokens_by_denom: + name = token_metadata["name"] or symbol + decimals = max({denom_unit["exponent"] for denom_unit in token_metadata["denomUnits"]}) + + unique_symbol = denom + for symbol_candidate in [symbol, name]: + if symbol_candidate not in self._tokens_by_symbol: + unique_symbol = symbol_candidate + break + + token = Token( + name=name, + symbol=symbol, + denom=denom, + address="", + decimals=decimals, + logo=token_metadata["uri"], + updated=-1, + ) + + self._tokens_by_denom[denom] = token + self._tokens_by_symbol[unique_symbol] = token + async def _initialize_tokens_and_markets(self): spot_markets = dict() derivative_markets = dict() diff --git a/pyinjective/client/model/pagination.py b/pyinjective/client/model/pagination.py index f3607c17..906a6060 100644 --- a/pyinjective/client/model/pagination.py +++ b/pyinjective/client/model/pagination.py @@ -31,7 +31,7 @@ def create_pagination_request(self) -> pagination_pb.PageRequest: page_request = pagination_pb.PageRequest() if self.key is not None: - page_request.key = bytes.fromhex(self.key) + page_request.key = self.key.encode() if self.skip is not None: page_request.offset = self.skip if self.limit is not None: diff --git a/tests/rpc_fixtures/markets_fixtures.py b/tests/rpc_fixtures/markets_fixtures.py index 69f835df..9c3a56ad 100644 --- a/tests/rpc_fixtures/markets_fixtures.py +++ b/tests/rpc_fixtures/markets_fixtures.py @@ -1,6 +1,32 @@ import pytest +@pytest.fixture +def smart_denom_metadata(): + from pyinjective.proto.cosmos.bank.v1beta1 import bank_pb2 as bank_pb + + first_denom_unit = bank_pb.DenomUnit( + denom="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", exponent=0, aliases=["microSMART"] + ) + second_denom_unit = bank_pb.DenomUnit(denom="SMART", exponent=6, aliases=["SMART"]) + metadata = bank_pb.Metadata( + description="SMART", + denom_units=[first_denom_unit, second_denom_unit], + base="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", + display="SMART", + name="SMART", + symbol="SMART", + uri=( + "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/" + "Flag_of_the_People%27s_Republic_of_China.svg/" + "2560px-Flag_of_the_People%27s_Republic_of_China.svg.png" + ), + uri_hash="", + ) + + return metadata + + @pytest.fixture def inj_token_meta(): from pyinjective.proto.exchange.injective_spot_exchange_rpc_pb2 import TokenMeta diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 7780e088..b7b597ed 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -4,22 +4,31 @@ from pyinjective.async_client import AsyncClient from pyinjective.core.network import Network +from pyinjective.proto.cosmos.bank.v1beta1 import query_pb2 as bank_query_pb +from pyinjective.proto.cosmos.base.query.v1beta1 import pagination_pb2 as pagination_pb from pyinjective.proto.exchange import injective_derivative_exchange_rpc_pb2, injective_spot_exchange_rpc_pb2 +from tests.client.chain.grpc.configurable_bank_query_servicer import ConfigurableBankQueryServicer from tests.client.indexer.configurable_derivative_query_servicer import ConfigurableDerivativeQueryServicer from tests.client.indexer.configurable_spot_query_servicer import ConfigurableSpotQueryServicer -from tests.rpc_fixtures.markets_fixtures import ape_token_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import ape_usdt_spot_market_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import btc_usdt_perp_market_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import inj_token_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import inj_usdt_spot_market_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import usdt_perp_token_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import usdt_token_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import ( # noqa: F401; noqa: F401; noqa: F401 +from tests.rpc_fixtures.markets_fixtures import ( # noqa: F401 + ape_token_meta, + ape_usdt_spot_market_meta, + btc_usdt_perp_market_meta, first_match_bet_market_meta, + inj_token_meta, + inj_usdt_spot_market_meta, + smart_denom_metadata, + usdt_perp_token_meta, + usdt_token_meta, usdt_token_meta_second_denom, ) +@pytest.fixture +def bank_servicer(): + return ConfigurableBankQueryServicer() + + @pytest.fixture def spot_servicer(): return ConfigurableSpotQueryServicer() @@ -127,3 +136,44 @@ async def test_initialize_tokens_and_markets( assert any( (first_match_bet_market_meta.market_id == market.id for market in all_binary_option_markets.values()) ) + + @pytest.mark.asyncio + async def test_initialize_tokens_from_chain_denoms( + self, + bank_servicer, + spot_servicer, + derivative_servicer, + smart_denom_metadata, + ): + pagination = pagination_pb.PageResponse( + total=1, + ) + + bank_servicer.denoms_metadata_responses.append( + bank_query_pb.QueryDenomsMetadataResponse( + metadatas=[smart_denom_metadata], + pagination=pagination, + ) + ) + + spot_servicer.markets_responses.append(injective_spot_exchange_rpc_pb2.MarketsResponse(markets=[])) + derivative_servicer.markets_responses.append(injective_derivative_exchange_rpc_pb2.MarketsResponse(markets=[])) + derivative_servicer.binary_options_markets_responses.append( + injective_derivative_exchange_rpc_pb2.BinaryOptionsMarketsResponse(markets=[]) + ) + + client = AsyncClient( + network=Network.local(), + insecure=False, + ) + + client.bank_api._stub = bank_servicer + client.exchange_spot_api._stub = spot_servicer + client.exchange_derivative_api._stub = derivative_servicer + + await client._initialize_tokens_and_markets() + await client.initialize_tokens_from_chain_denoms() + + all_tokens = await client.all_tokens() + assert 1 == len(all_tokens) + assert smart_denom_metadata.symbol in all_tokens From e6a54ddb1e1eb20e2312045374047380cf9e7a68 Mon Sep 17 00:00:00 2001 From: abel Date: Mon, 25 Dec 2023 16:59:01 -0300 Subject: [PATCH 5/6] (feat) Added all endpoint for chain bank module --- examples/chain_client/50_SpendableBalances.py | 16 + .../51_SpendableBalancesByDenom.py | 17 + examples/chain_client/52_TotalSupply.py | 18 + examples/chain_client/53_SupplyOf.py | 15 + examples/chain_client/54_DenomMetadata.py | 16 + examples/chain_client/55_DenomsMetadata.py | 18 + examples/chain_client/56_DenomOwners.py | 20 ++ examples/chain_client/57_SendEnabled.py | 20 ++ pyinjective/async_client.py | 36 ++ .../client/chain/grpc/chain_grpc_bank_api.py | 82 ++++- .../grpc/configurable_bank_query_servicer.py | 32 ++ .../chain/grpc/test_chain_grpc_bank_api.py | 325 ++++++++++++++++++ 12 files changed, 612 insertions(+), 3 deletions(-) create mode 100644 examples/chain_client/50_SpendableBalances.py create mode 100644 examples/chain_client/51_SpendableBalancesByDenom.py create mode 100644 examples/chain_client/52_TotalSupply.py create mode 100644 examples/chain_client/53_SupplyOf.py create mode 100644 examples/chain_client/54_DenomMetadata.py create mode 100644 examples/chain_client/55_DenomsMetadata.py create mode 100644 examples/chain_client/56_DenomOwners.py create mode 100644 examples/chain_client/57_SendEnabled.py diff --git a/examples/chain_client/50_SpendableBalances.py b/examples/chain_client/50_SpendableBalances.py new file mode 100644 index 00000000..d13836e0 --- /dev/null +++ b/examples/chain_client/50_SpendableBalances.py @@ -0,0 +1,16 @@ +import asyncio + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + address = "inj1cml96vmptgw99syqrrz8az79xer2pcgp0a885r" + spendable_balances = await client.fetch_spendable_balances(address=address) + print(spendable_balances) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/51_SpendableBalancesByDenom.py b/examples/chain_client/51_SpendableBalancesByDenom.py new file mode 100644 index 00000000..639d7933 --- /dev/null +++ b/examples/chain_client/51_SpendableBalancesByDenom.py @@ -0,0 +1,17 @@ +import asyncio + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + address = "inj1cml96vmptgw99syqrrz8az79xer2pcgp0a885r" + denom = "inj" + spendable_balances = await client.fetch_spendable_balances_by_denom(address=address, denom=denom) + print(spendable_balances) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/52_TotalSupply.py b/examples/chain_client/52_TotalSupply.py new file mode 100644 index 00000000..8156c54b --- /dev/null +++ b/examples/chain_client/52_TotalSupply.py @@ -0,0 +1,18 @@ +import asyncio + +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) + total_supply = await client.fetch_total_supply( + pagination=PaginationOption(limit=10), + ) + print(total_supply) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/53_SupplyOf.py b/examples/chain_client/53_SupplyOf.py new file mode 100644 index 00000000..225b9db7 --- /dev/null +++ b/examples/chain_client/53_SupplyOf.py @@ -0,0 +1,15 @@ +import asyncio + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + supply_of = await client.fetch_supply_of(denom="inj") + print(supply_of) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/54_DenomMetadata.py b/examples/chain_client/54_DenomMetadata.py new file mode 100644 index 00000000..ff8a9337 --- /dev/null +++ b/examples/chain_client/54_DenomMetadata.py @@ -0,0 +1,16 @@ +import asyncio + +from pyinjective.async_client import AsyncClient +from pyinjective.core.network import Network + + +async def main() -> None: + network = Network.testnet() + client = AsyncClient(network) + denom = "factory/inj107aqkjc3t5r3l9j4n9lgrma5tm3jav8qgppz6m/position" + metadata = await client.fetch_denom_metadata(denom=denom) + print(metadata) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/55_DenomsMetadata.py b/examples/chain_client/55_DenomsMetadata.py new file mode 100644 index 00000000..26402f49 --- /dev/null +++ b/examples/chain_client/55_DenomsMetadata.py @@ -0,0 +1,18 @@ +import asyncio + +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) + denoms = await client.fetch_denoms_metadata( + pagination=PaginationOption(limit=10), + ) + print(denoms) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/56_DenomOwners.py b/examples/chain_client/56_DenomOwners.py new file mode 100644 index 00000000..3204cc34 --- /dev/null +++ b/examples/chain_client/56_DenomOwners.py @@ -0,0 +1,20 @@ +import asyncio + +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) + denom = "inj" + owners = await client.fetch_denom_owners( + denom=denom, + pagination=PaginationOption(limit=10), + ) + print(owners) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/chain_client/57_SendEnabled.py b/examples/chain_client/57_SendEnabled.py new file mode 100644 index 00000000..03ed41d9 --- /dev/null +++ b/examples/chain_client/57_SendEnabled.py @@ -0,0 +1,20 @@ +import asyncio + +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) + denom = "inj" + enabled = await client.fetch_send_enabled( + denoms=[denom], + pagination=PaginationOption(limit=10), + ) + print(enabled) + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) diff --git a/pyinjective/async_client.py b/pyinjective/async_client.py index 3bfd30d1..f437db7f 100644 --- a/pyinjective/async_client.py +++ b/pyinjective/async_client.py @@ -522,6 +522,42 @@ async def get_bank_balance(self, address: str, denom: str): async def fetch_bank_balance(self, address: str, denom: str) -> Dict[str, Any]: return await self.bank_api.fetch_balance(account_address=address, denom=denom) + async def fetch_spendable_balances( + self, + address: str, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + return await self.bank_api.fetch_spendable_balances(account_address=address, pagination=pagination) + + async def fetch_spendable_balances_by_denom( + self, + address: str, + denom: str, + ) -> Dict[str, Any]: + return await self.bank_api.fetch_spendable_balances_by_denom(account_address=address, denom=denom) + + async def fetch_total_supply(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + return await self.bank_api.fetch_total_supply(pagination=pagination) + + async def fetch_supply_of(self, denom: str) -> Dict[str, Any]: + return await self.bank_api.fetch_supply_of(denom=denom) + + async def fetch_denom_metadata(self, denom: str) -> Dict[str, Any]: + return await self.bank_api.fetch_denom_metadata(denom=denom) + + async def fetch_denoms_metadata(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + return await self.bank_api.fetch_denoms_metadata(pagination=pagination) + + async def fetch_denom_owners(self, denom: str, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + return await self.bank_api.fetch_denom_owners(denom=denom, pagination=pagination) + + async def fetch_send_enabled( + self, + denoms: Optional[List[str]] = None, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + return await self.bank_api.fetch_send_enabled(denoms=denoms, pagination=pagination) + # Injective Exchange client methods # Auction RPC diff --git a/pyinjective/client/chain/grpc/chain_grpc_bank_api.py b/pyinjective/client/chain/grpc/chain_grpc_bank_api.py index b774707f..69d0cd0e 100644 --- a/pyinjective/client/chain/grpc/chain_grpc_bank_api.py +++ b/pyinjective/client/chain/grpc/chain_grpc_bank_api.py @@ -1,7 +1,8 @@ -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List, Optional from grpc.aio import Channel +from pyinjective.client.model.pagination import PaginationOption from pyinjective.proto.cosmos.bank.v1beta1 import query_pb2 as bank_query_pb, query_pb2_grpc as bank_query_grpc from pyinjective.utils.grpc_api_request_assistant import GrpcApiRequestAssistant @@ -29,11 +30,86 @@ async def fetch_balances(self, account_address: str) -> Dict[str, Any]: return response - async def fetch_total_supply(self) -> Dict[str, Any]: - request = bank_query_pb.QueryTotalSupplyRequest() + async def fetch_spendable_balances( + self, + account_address: str, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QuerySpendableBalancesRequest( + address=account_address, + pagination=pagination_request, + ) + response = await self._execute_call(call=self._stub.SpendableBalances, request=request) + + return response + + async def fetch_spendable_balances_by_denom( + self, + account_address: str, + denom: str, + ) -> Dict[str, Any]: + request = bank_query_pb.QuerySpendableBalanceByDenomRequest( + address=account_address, + denom=denom, + ) + response = await self._execute_call(call=self._stub.SpendableBalanceByDenom, request=request) + + return response + + async def fetch_total_supply(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QueryTotalSupplyRequest(pagination=pagination_request) response = await self._execute_call(call=self._stub.TotalSupply, request=request) return response + async def fetch_supply_of(self, denom: str) -> Dict[str, Any]: + request = bank_query_pb.QuerySupplyOfRequest(denom=denom) + response = await self._execute_call(call=self._stub.SupplyOf, request=request) + + return response + + async def fetch_denom_metadata(self, denom: str) -> Dict[str, Any]: + request = bank_query_pb.QueryDenomMetadataRequest(denom=denom) + response = await self._execute_call(call=self._stub.DenomMetadata, request=request) + + return response + + async def fetch_denoms_metadata(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QueryDenomsMetadataRequest(pagination=pagination_request) + response = await self._execute_call(call=self._stub.DenomsMetadata, request=request) + + return response + + async def fetch_denom_owners(self, denom: str, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QueryDenomOwnersRequest(denom=denom, pagination=pagination_request) + response = await self._execute_call(call=self._stub.DenomOwners, request=request) + + return response + + async def fetch_send_enabled( + self, + denoms: Optional[List[str]] = None, + pagination: Optional[PaginationOption] = None, + ) -> Dict[str, Any]: + pagination_request = None + if pagination is not None: + pagination_request = pagination.create_pagination_request() + request = bank_query_pb.QuerySendEnabledRequest(denoms=denoms, pagination=pagination_request) + response = await self._execute_call(call=self._stub.SendEnabled, 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/client/chain/grpc/configurable_bank_query_servicer.py b/tests/client/chain/grpc/configurable_bank_query_servicer.py index 015ce2c8..314a6622 100644 --- a/tests/client/chain/grpc/configurable_bank_query_servicer.py +++ b/tests/client/chain/grpc/configurable_bank_query_servicer.py @@ -9,7 +9,14 @@ def __init__(self): self.bank_params = deque() self.balance_responses = deque() self.balances_responses = deque() + self.spendable_balances_responses = deque() + self.spendable_balances_by_denom_responses = deque() self.total_supply_responses = deque() + self.supply_of_responses = deque() + self.denom_metadata_responses = deque() + self.denoms_metadata_responses = deque() + self.denom_owners_responses = deque() + self.send_enabled_responses = deque() async def Params(self, request: bank_query_pb.QueryParamsRequest, context=None, metadata=None): return self.bank_params.pop() @@ -20,5 +27,30 @@ async def Balance(self, request: bank_query_pb.QueryBalanceRequest, context=None async def AllBalances(self, request: bank_query_pb.QueryAllBalancesRequest, context=None, metadata=None): return self.balances_responses.pop() + async def SpendableBalances( + self, request: bank_query_pb.QuerySpendableBalancesRequest, context=None, metadata=None + ): + return self.spendable_balances_responses.pop() + + async def SpendableBalanceByDenom( + self, request: bank_query_pb.QuerySpendableBalanceByDenomRequest, context=None, metadata=None + ): + return self.spendable_balances_by_denom_responses.pop() + async def TotalSupply(self, request: bank_query_pb.QueryTotalSupplyRequest, context=None, metadata=None): return self.total_supply_responses.pop() + + async def SupplyOf(self, request: bank_query_pb.QuerySupplyOfRequest, context=None, metadata=None): + return self.supply_of_responses.pop() + + async def DenomMetadata(self, request: bank_query_pb.QueryDenomMetadataRequest, context=None, metadata=None): + return self.denom_metadata_responses.pop() + + async def DenomsMetadata(self, request: bank_query_pb.QueryDenomsMetadataRequest, context=None, metadata=None): + return self.denoms_metadata_responses.pop() + + async def DenomOwners(self, request: bank_query_pb.QueryDenomOwnersRequest, context=None, metadata=None): + return self.denom_owners_responses.pop() + + async def SendEnabled(self, request: bank_query_pb.QuerySendEnabledRequest, context=None, metadata=None): + return self.send_enabled_responses.pop() diff --git a/tests/client/chain/grpc/test_chain_grpc_bank_api.py b/tests/client/chain/grpc/test_chain_grpc_bank_api.py index 1c8c0ffa..f2c6cbdd 100644 --- a/tests/client/chain/grpc/test_chain_grpc_bank_api.py +++ b/tests/client/chain/grpc/test_chain_grpc_bank_api.py @@ -4,6 +4,7 @@ import pytest from pyinjective.client.chain.grpc.chain_grpc_bank_api import ChainGrpcBankApi +from pyinjective.client.model.pagination import PaginationOption from pyinjective.core.network import Network from pyinjective.proto.cosmos.bank.v1beta1 import bank_pb2 as bank_pb, query_pb2 as bank_query_pb from pyinjective.proto.cosmos.base.query.v1beta1 import pagination_pb2 as pagination_pb @@ -120,6 +121,69 @@ async def test_fetch_balances( assert expected_balances == bank_balances + @pytest.mark.asyncio + async def test_fetch_spendable_balances( + self, + bank_servicer, + ): + first_balance = coin_pb.Coin(denom="inj", amount="988987297011197594664") + second_balance = coin_pb.Coin(denom="peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5", amount="54497408") + pagination = pagination_pb.PageResponse(total=2) + + bank_servicer.spendable_balances_responses.append( + bank_query_pb.QuerySpendableBalancesResponse( + balances=[first_balance, second_balance], + pagination=pagination, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + balances = await api.fetch_spendable_balances( + account_address="inj1cml96vmptgw99syqrrz8az79xer2pcgp0a885r", + pagination=PaginationOption( + skip=0, + limit=100, + ), + ) + expected_balances = { + "balances": [{"denom": coin.denom, "amount": coin.amount} for coin in [first_balance, second_balance]], + "pagination": {"nextKey": "", "total": "2"}, + } + + assert expected_balances == balances + + @pytest.mark.asyncio + async def test_fetch_spendable_balances_by_denom( + self, + bank_servicer, + ): + first_balance = coin_pb.Coin(denom="inj", amount="988987297011197594664") + + bank_servicer.spendable_balances_by_denom_responses.append( + bank_query_pb.QuerySpendableBalanceByDenomResponse(balance=first_balance) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + balances = await api.fetch_spendable_balances_by_denom( + account_address="inj1cml96vmptgw99syqrrz8az79xer2pcgp0a885r", + denom=first_balance.denom, + ) + expected_balances = { + "balance": {"denom": first_balance.denom, "amount": first_balance.amount}, + } + + assert expected_balances == balances + @pytest.mark.asyncio async def test_fetch_total_supply( self, @@ -164,5 +228,266 @@ async def test_fetch_total_supply( assert expected_supply == total_supply + @pytest.mark.asyncio + async def test_fetch_supply_of( + self, + bank_servicer, + ): + first_supply = coin_pb.Coin( + denom="factory/inj108t3mlej0dph8er6ca2lq5cs9pdgzva5mqsn5p/position", amount="5556700000000000000" + ) + + bank_servicer.supply_of_responses.append( + bank_query_pb.QuerySupplyOfResponse( + amount=first_supply, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + total_supply = await api.fetch_supply_of(denom=first_supply.denom) + expected_supply = {"amount": {"denom": first_supply.denom, "amount": first_supply.amount}} + + assert expected_supply == total_supply + + @pytest.mark.asyncio + async def test_fetch_denom_metadata( + self, + bank_servicer, + ): + first_denom_unit = bank_pb.DenomUnit( + denom="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", exponent=0, aliases=["microSMART"] + ) + second_denom_unit = bank_pb.DenomUnit(denom="SMART", exponent=6, aliases=["SMART"]) + metadata = bank_pb.Metadata( + description="SMART", + denom_units=[first_denom_unit, second_denom_unit], + base="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", + display="SMART", + name="SMART", + symbol="SMART", + uri=( + "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/" + "Flag_of_the_People%27s_Republic_of_China.svg/" + "2560px-Flag_of_the_People%27s_Republic_of_China.svg.png" + ), + uri_hash="", + ) + + bank_servicer.denom_metadata_responses.append( + bank_query_pb.QueryDenomMetadataResponse( + metadata=metadata, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + denom_metadata = await api.fetch_denom_metadata(denom=metadata.base) + + expected_denom_metadata = { + "metadata": { + "description": metadata.description, + "denomUnits": [ + {"denom": denom.denom, "exponent": denom.exponent, "aliases": denom.aliases} + for denom in [first_denom_unit, second_denom_unit] + ], + "base": metadata.base, + "display": metadata.display, + "name": metadata.name, + "symbol": metadata.symbol, + "uri": metadata.uri, + "uriHash": metadata.uri_hash, + } + } + + assert expected_denom_metadata == denom_metadata + + @pytest.mark.asyncio + async def test_fetch_denoms_metadata( + self, + bank_servicer, + ): + first_denom_unit = bank_pb.DenomUnit( + denom="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", exponent=0, aliases=["microSMART"] + ) + second_denom_unit = bank_pb.DenomUnit(denom="SMART", exponent=6, aliases=["SMART"]) + metadata = bank_pb.Metadata( + description="SMART", + denom_units=[first_denom_unit, second_denom_unit], + base="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", + display="SMART", + name="SMART", + symbol="SMART", + uri=( + "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/" + "Flag_of_the_People%27s_Republic_of_China.svg/" + "2560px-Flag_of_the_People%27s_Republic_of_China.svg.png" + ), + uri_hash="", + ) + pagination = pagination_pb.PageResponse( + next_key=( + "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/" "inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + ).encode(), + total=179, + ) + + bank_servicer.denoms_metadata_responses.append( + bank_query_pb.QueryDenomsMetadataResponse( + metadatas=[metadata], + pagination=pagination, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + denoms_metadata = await api.fetch_denoms_metadata( + pagination=PaginationOption( + skip=0, + limit=100, + ), + ) + next_key = "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + expected_denoms_metadata = { + "metadatas": [ + { + "description": metadata.description, + "denomUnits": [ + {"denom": denom.denom, "exponent": denom.exponent, "aliases": denom.aliases} + for denom in [first_denom_unit, second_denom_unit] + ], + "base": metadata.base, + "display": metadata.display, + "name": metadata.name, + "symbol": metadata.symbol, + "uri": metadata.uri, + "uriHash": metadata.uri_hash, + }, + ], + "pagination": { + "nextKey": base64.b64encode(next_key.encode()).decode(), + "total": "179", + }, + } + + assert expected_denoms_metadata == denoms_metadata + + @pytest.mark.asyncio + async def test_fetch_denom_owners( + self, + bank_servicer, + ): + balance = coin_pb.Coin(denom="inj", amount="988987297011197594664") + denom_owner = bank_query_pb.DenomOwner(address="inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r", balance=balance) + pagination = pagination_pb.PageResponse( + next_key=( + "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/" "inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + ).encode(), + total=179, + ) + + bank_servicer.denom_owners_responses.append( + bank_query_pb.QueryDenomOwnersResponse( + denom_owners=[denom_owner], + pagination=pagination, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + denoms_metadata = await api.fetch_denom_owners( + denom=balance.denom, + pagination=PaginationOption( + skip=0, + limit=100, + ), + ) + next_key = "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + expected_denoms_metadata = { + "denomOwners": [ + { + "address": denom_owner.address, + "balance": { + "denom": balance.denom, + "amount": balance.amount, + }, + }, + ], + "pagination": { + "nextKey": base64.b64encode(next_key.encode()).decode(), + "total": "179", + }, + } + + assert expected_denoms_metadata == denoms_metadata + + @pytest.mark.asyncio + async def test_fetch_send_enabled( + self, + bank_servicer, + ): + send_enabled = bank_pb.SendEnabled( + denom="inj", + enabled=True, + ) + pagination = pagination_pb.PageResponse( + next_key=( + "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/" "inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + ).encode(), + total=179, + ) + + bank_servicer.send_enabled_responses.append( + bank_query_pb.QuerySendEnabledResponse( + send_enabled=[send_enabled], + pagination=pagination, + ) + ) + + network = Network.devnet() + channel = grpc.aio.insecure_channel(network.grpc_endpoint) + + api = ChainGrpcBankApi(channel=channel, metadata_provider=lambda: self._dummy_metadata_provider()) + api._stub = bank_servicer + + denoms_metadata = await api.fetch_send_enabled( + denoms=[send_enabled.denom], + pagination=PaginationOption( + skip=0, + limit=100, + ), + ) + next_key = "factory/inj1vkrp72xd67plcggcfjtjelqa4t5a093xljf2vj/inj1spw6nd0pj3kd3fgjljhuqpc8tv72a9v89myuja" + expected_denoms_metadata = { + "sendEnabled": [ + { + "denom": send_enabled.denom, + "enabled": send_enabled.enabled, + }, + ], + "pagination": { + "nextKey": base64.b64encode(next_key.encode()).decode(), + "total": "179", + }, + } + + assert expected_denoms_metadata == denoms_metadata + async def _dummy_metadata_provider(self): return None From 35281456126a393349178137dbcf15b7c7bd7780 Mon Sep 17 00:00:00 2001 From: abel Date: Mon, 25 Dec 2023 23:26:52 -0300 Subject: [PATCH 6/6] (feat) Added logic to initialize tokens in AsyncClient using the denoms metadata from chain bank module --- pyinjective/async_client.py | 46 ++++++++++++++++++ pyinjective/client/model/pagination.py | 2 +- tests/rpc_fixtures/markets_fixtures.py | 26 ++++++++++ tests/test_async_client.py | 66 ++++++++++++++++++++++---- 4 files changed, 131 insertions(+), 9 deletions(-) diff --git a/pyinjective/async_client.py b/pyinjective/async_client.py index f437db7f..50eddecf 100644 --- a/pyinjective/async_client.py +++ b/pyinjective/async_client.py @@ -1,4 +1,5 @@ import asyncio +import base64 import time from copy import deepcopy from decimal import Decimal @@ -2497,6 +2498,51 @@ async def composer(self): tokens=await self.all_tokens(), ) + async def initialize_tokens_from_chain_denoms(self): + # force initialization of markets and tokens + await self.all_tokens() + + all_denoms_metadata = [] + + query_result = await self.fetch_denoms_metadata() + + all_denoms_metadata.extend(query_result.get("metadatas", [])) + next_key = query_result.get("pagination", {}).get("nextKey", "") + + while next_key != "": + query_result = await self.fetch_denoms_metadata(pagination=PaginationOption(key=next_key)) + + all_denoms_metadata.extend(query_result.get("metadatas", [])) + result_next_key = query_result.get("pagination", {}).get("nextKey", "") + next_key = base64.b64decode(result_next_key).decode() + + for token_metadata in all_denoms_metadata: + symbol = token_metadata["symbol"] + denom = token_metadata["base"] + + if denom != "" and symbol != "" and denom not in self._tokens_by_denom: + name = token_metadata["name"] or symbol + decimals = max({denom_unit["exponent"] for denom_unit in token_metadata["denomUnits"]}) + + unique_symbol = denom + for symbol_candidate in [symbol, name]: + if symbol_candidate not in self._tokens_by_symbol: + unique_symbol = symbol_candidate + break + + token = Token( + name=name, + symbol=symbol, + denom=denom, + address="", + decimals=decimals, + logo=token_metadata["uri"], + updated=-1, + ) + + self._tokens_by_denom[denom] = token + self._tokens_by_symbol[unique_symbol] = token + async def _initialize_tokens_and_markets(self): spot_markets = dict() derivative_markets = dict() diff --git a/pyinjective/client/model/pagination.py b/pyinjective/client/model/pagination.py index f3607c17..906a6060 100644 --- a/pyinjective/client/model/pagination.py +++ b/pyinjective/client/model/pagination.py @@ -31,7 +31,7 @@ def create_pagination_request(self) -> pagination_pb.PageRequest: page_request = pagination_pb.PageRequest() if self.key is not None: - page_request.key = bytes.fromhex(self.key) + page_request.key = self.key.encode() if self.skip is not None: page_request.offset = self.skip if self.limit is not None: diff --git a/tests/rpc_fixtures/markets_fixtures.py b/tests/rpc_fixtures/markets_fixtures.py index 69f835df..9c3a56ad 100644 --- a/tests/rpc_fixtures/markets_fixtures.py +++ b/tests/rpc_fixtures/markets_fixtures.py @@ -1,6 +1,32 @@ import pytest +@pytest.fixture +def smart_denom_metadata(): + from pyinjective.proto.cosmos.bank.v1beta1 import bank_pb2 as bank_pb + + first_denom_unit = bank_pb.DenomUnit( + denom="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", exponent=0, aliases=["microSMART"] + ) + second_denom_unit = bank_pb.DenomUnit(denom="SMART", exponent=6, aliases=["SMART"]) + metadata = bank_pb.Metadata( + description="SMART", + denom_units=[first_denom_unit, second_denom_unit], + base="factory/inj105ujajd95znwjvcy3hwcz80pgy8tc6v77spur0/SMART", + display="SMART", + name="SMART", + symbol="SMART", + uri=( + "https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/" + "Flag_of_the_People%27s_Republic_of_China.svg/" + "2560px-Flag_of_the_People%27s_Republic_of_China.svg.png" + ), + uri_hash="", + ) + + return metadata + + @pytest.fixture def inj_token_meta(): from pyinjective.proto.exchange.injective_spot_exchange_rpc_pb2 import TokenMeta diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 7780e088..b7b597ed 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -4,22 +4,31 @@ from pyinjective.async_client import AsyncClient from pyinjective.core.network import Network +from pyinjective.proto.cosmos.bank.v1beta1 import query_pb2 as bank_query_pb +from pyinjective.proto.cosmos.base.query.v1beta1 import pagination_pb2 as pagination_pb from pyinjective.proto.exchange import injective_derivative_exchange_rpc_pb2, injective_spot_exchange_rpc_pb2 +from tests.client.chain.grpc.configurable_bank_query_servicer import ConfigurableBankQueryServicer from tests.client.indexer.configurable_derivative_query_servicer import ConfigurableDerivativeQueryServicer from tests.client.indexer.configurable_spot_query_servicer import ConfigurableSpotQueryServicer -from tests.rpc_fixtures.markets_fixtures import ape_token_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import ape_usdt_spot_market_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import btc_usdt_perp_market_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import inj_token_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import inj_usdt_spot_market_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import usdt_perp_token_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import usdt_token_meta # noqa: F401 -from tests.rpc_fixtures.markets_fixtures import ( # noqa: F401; noqa: F401; noqa: F401 +from tests.rpc_fixtures.markets_fixtures import ( # noqa: F401 + ape_token_meta, + ape_usdt_spot_market_meta, + btc_usdt_perp_market_meta, first_match_bet_market_meta, + inj_token_meta, + inj_usdt_spot_market_meta, + smart_denom_metadata, + usdt_perp_token_meta, + usdt_token_meta, usdt_token_meta_second_denom, ) +@pytest.fixture +def bank_servicer(): + return ConfigurableBankQueryServicer() + + @pytest.fixture def spot_servicer(): return ConfigurableSpotQueryServicer() @@ -127,3 +136,44 @@ async def test_initialize_tokens_and_markets( assert any( (first_match_bet_market_meta.market_id == market.id for market in all_binary_option_markets.values()) ) + + @pytest.mark.asyncio + async def test_initialize_tokens_from_chain_denoms( + self, + bank_servicer, + spot_servicer, + derivative_servicer, + smart_denom_metadata, + ): + pagination = pagination_pb.PageResponse( + total=1, + ) + + bank_servicer.denoms_metadata_responses.append( + bank_query_pb.QueryDenomsMetadataResponse( + metadatas=[smart_denom_metadata], + pagination=pagination, + ) + ) + + spot_servicer.markets_responses.append(injective_spot_exchange_rpc_pb2.MarketsResponse(markets=[])) + derivative_servicer.markets_responses.append(injective_derivative_exchange_rpc_pb2.MarketsResponse(markets=[])) + derivative_servicer.binary_options_markets_responses.append( + injective_derivative_exchange_rpc_pb2.BinaryOptionsMarketsResponse(markets=[]) + ) + + client = AsyncClient( + network=Network.local(), + insecure=False, + ) + + client.bank_api._stub = bank_servicer + client.exchange_spot_api._stub = spot_servicer + client.exchange_derivative_api._stub = derivative_servicer + + await client._initialize_tokens_and_markets() + await client.initialize_tokens_from_chain_denoms() + + all_tokens = await client.all_tokens() + assert 1 == len(all_tokens) + assert smart_denom_metadata.symbol in all_tokens