From f694268ca3ecb0348f44148d8446f29339319989 Mon Sep 17 00:00:00 2001 From: Angelina Date: Thu, 22 May 2025 10:26:53 -0400 Subject: [PATCH 01/20] add tokenInfoQuery functionality Signed-off-by: Angelina --- .../query/token_info_query.py | 130 ++++++++++++++++++ tests/unit/test_token_info_query.py | 71 ++++++++++ 2 files changed, 201 insertions(+) create mode 100644 src/hiero_sdk_python/query/token_info_query.py create mode 100644 tests/unit/test_token_info_query.py diff --git a/src/hiero_sdk_python/query/token_info_query.py b/src/hiero_sdk_python/query/token_info_query.py new file mode 100644 index 000000000..cbe872aaa --- /dev/null +++ b/src/hiero_sdk_python/query/token_info_query.py @@ -0,0 +1,130 @@ +from hiero_sdk_python.query.query import Query +from hiero_sdk_python.hapi.services import query_pb2, token_get_info_pb2 +from hiero_sdk_python.tokens.token_id import TokenId +from hiero_sdk_python.tokens.token_info import TokenInfo + +class TokenInfoQuery(Query): + """ + A query to retrive information about a specific Token. + """ + + def __init__(self, token_id=None): + """ + Initalizes a new TokenInfoQuery istance with a token_id. + + Args: + token_id (TokenID): The ID of the token to query. + """ + super().__init__() + self.token_id = token_id + self._frozen = False + + def _require_not_frozen(self): + """ + Ensures the query is not frozen before making changes. + """ + + if self._frozen: + raise ValueError("This query is frozen and cannot be modified.") + + def set_token_id(self, token_id: TokenId): + """ + Sets the ID of the token to query. + + Args: + token_id (TokenID): The ID of the token. + + Returns: + TokenInfoQuery: Returns self for method chaining. + """ + self._require_not_frozen() + self.token_id = token_id + return self + + def freeze(self): + """ + Marks the query as frozen, preventing further modificatoin. + + Returns: + TokenInfoQuery: Returns self for chaining. + """ + + self._frozen = True + return self + + def _make_request(self): + """ + Constrcuts the protobuf request for the query. + + Returns: + Query: The protobuf query message. + + Raises: + ValueError: If the token ID is not set. + """ + + if not self.token_id: + raise ValueError("Token ID must be set before making the request. ") + + query_header = self._make_request_header() + + token_info_query = token_get_info_pb2.TokenGetInfoQuery() + token_info_query.header.CopyFrom(query_header) + token_info_query.token.CopyFrom(slef.token_id.to_proto()) + + query = query_pb2.Query() + query.tokenGetInfo.CopyFrom(token_info_query) + return query + + def _get_status_from_response(self, response): + """ + Extracts the status from the query response. + + Args: + response: The response protobuf message. + + Returns: + ResponseCode: The status code from the response. + """ + + return response.tokenGetInfo.header.nodeTransactionPrecheckCode + + def _map_response(self, response): + """ + Maps the protobuf response to a TokenInfo instance. + + Args: + response: The response protobuf message. + + Returns: + TokenInfo: The token info. + + Raises: + Exception: If no tokenInfo is returned in the response. + """ + + if not response.tokenGetInfo.tokenInfo: + raise Exception("No tokenInfo retured in the response.") + + proto_token_info = response.tokenGetInfo.tokenInfo + return TokenInfo.from_proto(proto_token_info) + + def execute(self, client): + """ + Sends the TokenInfoQuery to the network via the given client and returns TokenInfo. + + Args: + client: The client object to execute the query. + + Returns: + TokenInfo: The queried token information. + """ + self.freeze() # prevent further modifications + request = self._make_request() + response = client.send_query(request) # You need this method in your client + status = self._get_status_from_response(response) + + if status != 0: # assuming 0 is OK; adjust based on your ResponseCode enum + raise Exception(f"Query failed with status: {status}") + + return self._map_response(response) \ No newline at end of file diff --git a/tests/unit/test_token_info_query.py b/tests/unit/test_token_info_query.py new file mode 100644 index 000000000..c1678b399 --- /dev/null +++ b/tests/unit/test_token_info_query.py @@ -0,0 +1,71 @@ +import pytest +from unittest.mock import MagicMock +from hiero_sdk_python.query.token_info_query import TokenInfoQuery +from hiero_sdk_python.tokens.token_id import TokenId +from hiero_sdk_python.tokens.token_info import TokenInfo +from hiero_sdk_python.client.client import Client +#from hiero_sdk_python.hapi.services import token_get_info_pb2 +#from hiero_sdk_python.hapi.services import token_get_info_pb2 +#from hiero_sdk_python.hapi.services.token_info_pb2 import TokenInfo as ProtoTokenInfo +#from hiero_sdk_python.hapi.services.token_info_pb2 import TokenInfo as ProtoTokenInfo +from hiero_sdk_python.hapi.services.response_header_pb2 import ResponseHeader +from hiero_sdk_python.tokens.token_type import TokenType +from hiero_sdk_python.hapi.services import ( + token_get_info_pb2, +) + +@pytest.fixture +def mock_token_info_response(): + """Fixture to provide a mock response for the token Info Query""" + + token_info = token_get_info_pb2.TokenInfo( + name = 'TestToken', + symbol = 'TTK', + decimals =8 + ) + + response = token_get_info_pb2.TokenGetInfoResponse( + header = ResponseHeader(nodeTransactionPrecheckCode=0), + tokenInfo=token_info + ) + + return response + +def test_token_info_query_execute(mock_token_info_response): + """ + Test the TokenGetInfoQuery with a mocked response. + """ + + token_id = TokenId(0, 0, 1234) + + query = TokenInfoQuery().set_token_id(token_id) + + query.node_account_ids = [MagicMock()] + query._make_request = MagicMock(return_value="mock_request") + query._get_status_from_response = MagicMock(return_value=0) + + #below is new + mapped_token_info = TokenInfo( + tokenId=token_id, + name="TestToken", + symbol="TTK", + decimals=8, + totalSupply=0, + treasury=None, + isDeleted=False, + memo="", + tokenType=TokenType.FUNGIBLE_COMMON, + maxSupply=0, + ledger_id=b"", + ) + + query._map_response = MagicMock(return_value=mock_token_info_response.tokenInfo) + + mock_client = MagicMock(spec=Client) + mock_client.send_query = MagicMock(return_value=mock_token_info_response) + + token_info = query.execute(mock_client) + + assert token_info.name == "TestToken" + assert token_info.symbol == "TTK" + assert token_info.decimals == 8 From d5310c814cb6fcb528aa198b8b7227b52fc019a8 Mon Sep 17 00:00:00 2001 From: dosi Date: Tue, 27 May 2025 22:27:18 +0300 Subject: [PATCH 02/20] refactor: update TokenInfoQuery implementation Signed-off-by: dosi --- .../query/token_info_query.py | 159 +++++++++--------- 1 file changed, 79 insertions(+), 80 deletions(-) diff --git a/src/hiero_sdk_python/query/token_info_query.py b/src/hiero_sdk_python/query/token_info_query.py index cbe872aaa..7c7693db4 100644 --- a/src/hiero_sdk_python/query/token_info_query.py +++ b/src/hiero_sdk_python/query/token_info_query.py @@ -1,31 +1,29 @@ from hiero_sdk_python.query.query import Query from hiero_sdk_python.hapi.services import query_pb2, token_get_info_pb2 +from hiero_sdk_python.executable import _Method +from hiero_sdk_python.channels import _Channel + from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.tokens.token_info import TokenInfo -class TokenInfoQuery(Query): +class TokenInfoQuery(Query): """ - A query to retrive information about a specific Token. + A query to retrieve information about a specific Token. + + This class constructs and executes a query to retrieve information + about a fungible or non-fungible token on the network, + including the token's properties and settings. + """ - def __init__(self, token_id=None): """ - Initalizes a new TokenInfoQuery istance with a token_id. - - Args: - token_id (TokenID): The ID of the token to query. + Initializes a new TokenInfoQuery instance with an optional token_id. + + Args: + token_id (TokenId, optional): The ID of the token to query. """ super().__init__() - self.token_id = token_id - self._frozen = False - - def _require_not_frozen(self): - """ - Ensures the query is not frozen before making changes. - """ - - if self._frozen: - raise ValueError("This query is frozen and cannot be modified.") + self.token_id : TokenId = token_id def set_token_id(self, token_id: TokenId): """ @@ -37,94 +35,95 @@ def set_token_id(self, token_id: TokenId): Returns: TokenInfoQuery: Returns self for method chaining. """ - self._require_not_frozen() self.token_id = token_id return self - - def freeze(self): - """ - Marks the query as frozen, preventing further modificatoin. - Returns: - TokenInfoQuery: Returns self for chaining. + def _make_request(self): """ + Constructs the protobuf request for the query. + + Builds a TokenGetInfoQuery protobuf message with the + appropriate header and token ID. - self._frozen = True - return self + Returns: + Query: The protobuf query message. - def _make_request(self): + Raises: + ValueError: If the token ID is not set. + Exception: If any other error occurs during request construction. """ - Constrcuts the protobuf request for the query. + try: + if not self.token_id: + raise ValueError("Token ID must be set before making the request.") - Returns: - Query: The protobuf query message. - - Raises: - ValueError: If the token ID is not set. - """ + query_header = self._make_request_header() - if not self.token_id: - raise ValueError("Token ID must be set before making the request. ") - - query_header = self._make_request_header() + token_info_query = token_get_info_pb2.TokenGetInfoQuery() + token_info_query.header.CopyFrom(query_header) + token_info_query.token.CopyFrom(self.token_id.to_proto()) - token_info_query = token_get_info_pb2.TokenGetInfoQuery() - token_info_query.header.CopyFrom(query_header) - token_info_query.token.CopyFrom(slef.token_id.to_proto()) + query = query_pb2.Query() + query.tokenGetInfo.CopyFrom(token_info_query) + + return query + except Exception as e: + print(f"Exception in _make_request: {e}") + raise - query = query_pb2.Query() - query.tokenGetInfo.CopyFrom(token_info_query) - return query - - def _get_status_from_response(self, response): + def _get_method(self, channel: _Channel) -> _Method: """ - Extracts the status from the query response. + Returns the appropriate gRPC method for the token info query. + + Implements the abstract method from Query to provide the specific + gRPC method for getting token information. Args: - response: The response protobuf message. + channel (_Channel): The channel containing service stubs - Returns: - ResponseCode: The status code from the response. + Returns: + _Method: The method wrapper containing the query function """ + return _Method( + transaction_func=None, + query_func=channel.token.getTokenInfo + ) - return response.tokenGetInfo.header.nodeTransactionPrecheckCode - - def _map_response(self, response): + def execute(self, client): """ - Maps the protobuf response to a TokenInfo instance. + Executes the token info query. + + Sends the query to the Hedera network and processes the response + to return a TokenInfo object. - Args: - response: The response protobuf message. + This function delegates the core logic to `_execute()`, and may propagate exceptions raised by it. + + Args: + client (Client): The client instance to use for execution Returns: - TokenInfo: The token info. - + TokenInfo: The token info from the network + Raises: - Exception: If no tokenInfo is returned in the response. + PrecheckError: If the query fails with a non-retryable error + MaxAttemptsError: If the query fails after the maximum number of attempts + ReceiptStatusError: If the query fails with a receipt status error """ + self._before_execute(client) + response = self._execute(client) - if not response.tokenGetInfo.tokenInfo: - raise Exception("No tokenInfo retured in the response.") - - proto_token_info = response.tokenGetInfo.tokenInfo - return TokenInfo.from_proto(proto_token_info) - - def execute(self, client): - """ - Sends the TokenInfoQuery to the network via the given client and returns TokenInfo. + return TokenInfo._from_proto(response.tokenGetInfo.tokenInfo) + def _get_query_response(self, response): + """ + Extracts the token info response from the full response. + + Implements the abstract method from Query to extract the + specific token info response object. + Args: - client: The client object to execute the query. - + response: The full response from the network + Returns: - TokenInfo: The queried token information. + The token get info response object """ - self.freeze() # prevent further modifications - request = self._make_request() - response = client.send_query(request) # You need this method in your client - status = self._get_status_from_response(response) - - if status != 0: # assuming 0 is OK; adjust based on your ResponseCode enum - raise Exception(f"Query failed with status: {status}") - - return self._map_response(response) \ No newline at end of file + return response.tokenGetInfo \ No newline at end of file From 76b0d854c42be1ba88aead0f7dd6eec3476857bf Mon Sep 17 00:00:00 2001 From: dosi Date: Tue, 27 May 2025 22:27:45 +0300 Subject: [PATCH 03/20] test: add unit tests Signed-off-by: dosi --- tests/unit/conftest.py | 5 + tests/unit/test_token_info_query.py | 157 ++++++++++++++++++---------- 2 files changed, 107 insertions(+), 55 deletions(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 9e511f8de..680e3b573 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -52,6 +52,11 @@ def nft_id(): serial_number = 8 return NftId(tokenId=token_id, serialNumber=serial_number) +@pytest.fixture +def token_id(): + """Fixture to provide a mock TokenId instance.""" + return TokenId(shard=0, realm=0, num=3) + @pytest.fixture def mock_client(): """Fixture to provide a mock client with hardcoded nodes for testing purposes.""" diff --git a/tests/unit/test_token_info_query.py b/tests/unit/test_token_info_query.py index c1678b399..aa7d07ff2 100644 --- a/tests/unit/test_token_info_query.py +++ b/tests/unit/test_token_info_query.py @@ -1,71 +1,118 @@ import pytest -from unittest.mock import MagicMock +from unittest.mock import Mock + +from hiero_sdk_python.hapi.services.query_header_pb2 import ResponseType from hiero_sdk_python.query.token_info_query import TokenInfoQuery -from hiero_sdk_python.tokens.token_id import TokenId -from hiero_sdk_python.tokens.token_info import TokenInfo -from hiero_sdk_python.client.client import Client -#from hiero_sdk_python.hapi.services import token_get_info_pb2 -#from hiero_sdk_python.hapi.services import token_get_info_pb2 -#from hiero_sdk_python.hapi.services.token_info_pb2 import TokenInfo as ProtoTokenInfo -#from hiero_sdk_python.hapi.services.token_info_pb2 import TokenInfo as ProtoTokenInfo -from hiero_sdk_python.hapi.services.response_header_pb2 import ResponseHeader -from hiero_sdk_python.tokens.token_type import TokenType +from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.hapi.services import ( + response_pb2, + response_header_pb2, token_get_info_pb2, ) -@pytest.fixture -def mock_token_info_response(): - """Fixture to provide a mock response for the token Info Query""" - - token_info = token_get_info_pb2.TokenInfo( - name = 'TestToken', - symbol = 'TTK', - decimals =8 - ) - - response = token_get_info_pb2.TokenGetInfoResponse( - header = ResponseHeader(nodeTransactionPrecheckCode=0), - tokenInfo=token_info - ) +from tests.unit.mock_server import mock_hedera_servers - return response +pytestmark = pytest.mark.unit -def test_token_info_query_execute(mock_token_info_response): - """ - Test the TokenGetInfoQuery with a mocked response. - """ +# This test uses fixture token_id as parameter +def test_constructor(token_id): + """Test initialization of TokenInfoQuery.""" + query = TokenInfoQuery() + assert query.token_id is None + + query = TokenInfoQuery(token_id) + assert query.token_id == token_id - token_id = TokenId(0, 0, 1234) +# This test uses fixture mock_client as parameter +def test_execute_without_token_id(mock_client): + """Test request creation with missing Token ID.""" + query = TokenInfoQuery() + + with pytest.raises(ValueError, match="Token ID must be set before making the request."): + query.execute(mock_client) - query = TokenInfoQuery().set_token_id(token_id) +def test_get_method(): + """Test retrieving the gRPC method for the query.""" + query = TokenInfoQuery() + + mock_channel = Mock() + mock_token_stub = Mock() + mock_channel.token = mock_token_stub + + method = query._get_method(mock_channel) + + assert method.transaction is None + assert method.query == mock_token_stub.getTokenInfo - query.node_account_ids = [MagicMock()] - query._make_request = MagicMock(return_value="mock_request") - query._get_status_from_response = MagicMock(return_value=0) - - #below is new - mapped_token_info = TokenInfo( - tokenId=token_id, - name="TestToken", - symbol="TTK", +# This test uses fixture mock_account_ids as parameter +def test_token_info_query_execute(mock_account_ids): + """Test basic functionality of TokenInfoQuery with mock server.""" + account_id, renew_account_id, _, token_id, _ = mock_account_ids + token_info_response = token_get_info_pb2.TokenInfo( + tokenId=token_id.to_proto(), + name="Test Token", + symbol="TEST", decimals=8, - totalSupply=0, - treasury=None, - isDeleted=False, + totalSupply=1000000, + treasury=account_id.to_proto(), + adminKey=None, + kycKey=None, + freezeKey=None, + wipeKey=None, + supplyKey=None, + defaultFreezeStatus=0, + defaultKycStatus=0, + deleted=False, + autoRenewAccount=renew_account_id.to_proto(), + autoRenewPeriod=None, + expiry=None, memo="", - tokenType=TokenType.FUNGIBLE_COMMON, + tokenType=0, + supplyType=0, maxSupply=0, - ledger_id=b"", + fee_schedule_key=None, + custom_fees=None, + pause_key=None, + pause_status=0, + metadata_key=None, + ledger_id=None ) - query._map_response = MagicMock(return_value=mock_token_info_response.tokenInfo) - - mock_client = MagicMock(spec=Client) - mock_client.send_query = MagicMock(return_value=mock_token_info_response) - - token_info = query.execute(mock_client) - - assert token_info.name == "TestToken" - assert token_info.symbol == "TTK" - assert token_info.decimals == 8 + response = response_pb2.Response( + tokenGetInfo=token_get_info_pb2.TokenGetInfoResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK, + responseType=ResponseType.ANSWER_ONLY, + cost=2 + ), + tokenInfo=token_info_response + ) + ) + + response_sequences = [[response]] + + with mock_hedera_servers(response_sequences) as client: + query = TokenInfoQuery().set_token_id(token_id) + + try: + result = query.execute(client) + except Exception as e: + pytest.fail(f"Unexpected exception raised: {e}") + + # Verify the result contains the expected values + assert result.tokenId.shard == 1 + assert result.tokenId.realm == 1 + assert result.tokenId.num == 1 + assert result.name == "Test Token" + assert result.symbol == "TEST" + assert result.decimals == 8 + assert result.totalSupply == 1000000 + assert result.treasury.shard == 0 + assert result.treasury.realm == 0 + assert result.treasury.num == 1 + assert result.autoRenewAccount.shard == 0 + assert result.autoRenewAccount.realm == 0 + assert result.autoRenewAccount.num == 2 + assert result.defaultFreezeStatus == 0 + assert result.defaultKycStatus == 0 + assert result.isDeleted == False \ No newline at end of file From 256e807eb6fd374f329d7ca5741b3c67decd5f6d Mon Sep 17 00:00:00 2001 From: dosi Date: Tue, 27 May 2025 22:27:58 +0300 Subject: [PATCH 04/20] test: add integration tests Signed-off-by: dosi --- .../integration/token_info_query_e2e_test.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/integration/token_info_query_e2e_test.py diff --git a/tests/integration/token_info_query_e2e_test.py b/tests/integration/token_info_query_e2e_test.py new file mode 100644 index 000000000..053fc7f9b --- /dev/null +++ b/tests/integration/token_info_query_e2e_test.py @@ -0,0 +1,69 @@ +import pytest + +from hiero_sdk_python.exceptions import PrecheckError +from hiero_sdk_python.hbar import Hbar +from hiero_sdk_python.tokens.supply_type import SupplyType +from hiero_sdk_python.tokens.token_type import TokenType +from hiero_sdk_python.tokens.token_id import TokenId +from hiero_sdk_python.query.token_info_query import TokenInfoQuery +from tests.integration.utils_for_test import IntegrationTestEnv, create_fungible_token + +@pytest.mark.integration +def test_integration_token_info_query_can_execute(): + env = IntegrationTestEnv() + + try: + token_id = create_fungible_token(env, [ + lambda tx: tx.set_decimals(3), + lambda tx: tx.set_wipe_key(None) # Set wipe key to None to verify TokenInfoQuery returns correct key state + ]) + + info = TokenInfoQuery(token_id).execute(env.client) + + assert str(info.tokenId) == str(token_id), "Token ID mismatch" + assert info.name == "PTokenTest34", "Name mismatch" + assert info.symbol == "PTT34", "Symbol mismatch" + assert info.decimals == 3, "Decimals mismatch" + assert str(info.treasury) == str(env.client.operator_account_id), "Treasury mismatch" + assert info.tokenType == TokenType.FUNGIBLE_COMMON, "Token type mismatch" + assert info.supplyType == SupplyType.FINITE, "Supply type mismatch" + assert info.maxSupply == 10000, "Max supply mismatch" + + assert info.adminKey is not None, "Admin key should not be None" + assert info.freezeKey is not None, "Freeze key should not be None" + assert info.wipeKey is None, "Wipe key should be None" + assert info.supplyKey is not None, "Supply key should not be None" + assert info.kycKey is None, "KYC key should be None" + + assert str(info.adminKey) == str(env.client.operator_private_key.public_key()), "Admin key mismatch" + assert str(info.freezeKey) == str(env.client.operator_private_key.public_key()), "Freeze key mismatch" + assert str(info.supplyKey) == str(env.client.operator_private_key.public_key()), "Supply key mismatch" + finally: + env.close() + +@pytest.mark.integration +def test_integration_token_info_query_insufficient_cost(): + """Test that token info query fails with insufficient payment.""" + env = IntegrationTestEnv() + + try: + token_id = create_fungible_token(env) + + query = TokenInfoQuery(token_id) + query.set_query_payment(Hbar.from_tinybars(1)) # Set very low query payment + + with pytest.raises(PrecheckError, match="failed precheck with status: INSUFFICIENT_TX_FEE"): + query.execute(env.client) + finally: + env.close() + +@pytest.mark.integration +def test_integration_token_info_query_can_execute_with_invalid_token_id(): + env = IntegrationTestEnv() + + try: + token_id = TokenId(0,0,123456789) + with pytest.raises(PrecheckError, match="failed precheck with status: INVALID_TOKEN_ID"): + TokenInfoQuery(token_id).execute(env.client) + finally: + env.close() \ No newline at end of file From 32443f19907adc5c9bb9567dcb562689c38376c2 Mon Sep 17 00:00:00 2001 From: dosi Date: Tue, 27 May 2025 22:29:04 +0300 Subject: [PATCH 05/20] docs: add examples for both non-fungible/fungible tokens Signed-off-by: dosi --- examples/query_token_info_fungible.py | 71 ++++++++++++++++++++++++++ examples/query_token_info_nft.py | 73 +++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 examples/query_token_info_fungible.py create mode 100644 examples/query_token_info_nft.py diff --git a/examples/query_token_info_fungible.py b/examples/query_token_info_fungible.py new file mode 100644 index 000000000..60bb5e909 --- /dev/null +++ b/examples/query_token_info_fungible.py @@ -0,0 +1,71 @@ +import os +import sys + +from dotenv import load_dotenv + +from hiero_sdk_python import ( + Client, + AccountId, + PrivateKey, + Network, +) +from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenType +from hiero_sdk_python.query.token_info_query import TokenInfoQuery +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.tokens.supply_type import SupplyType +from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction + +load_dotenv() + +def setup_client(): + """Initialize and set up the client with operator account""" + network = Network(network='solo') + client = Client(network) + + operator_id = AccountId.from_string(os.getenv('OPERATOR_ID')) + operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY')) + client.set_operator(operator_id, operator_key) + + return client, operator_id, operator_key + +def create_fungible_token(client: 'Client', treasury_id, treasury_private_key): + """Create a fungible token""" + receipt = ( + TokenCreateTransaction() + .set_token_name("MyExampleFT") + .set_token_symbol("EXFT") + .set_decimals(2) + .set_initial_supply(100) + .set_treasury_account_id(treasury_id) + .set_token_type(TokenType.FUNGIBLE_COMMON) + .set_supply_type(SupplyType.FINITE) + .set_max_supply(1000) + .set_admin_key(treasury_private_key) + .set_supply_key(treasury_private_key) + .set_freeze_key(treasury_private_key) + .execute(client) + ) + + if receipt.status != ResponseCode.SUCCESS: + print(f"Fungible token creation failed with status: {ResponseCode.get_name(receipt.status)}") + sys.exit(1) + + token_id = receipt.tokenId + print(f"Fungible token created with ID: {token_id}") + + return token_id + +def query_token_info(): + """ + Demonstrates the token info query functionality by: + 1. Creating a fungible token + 2. Querying the token info + """ + client, operator_id, operator_key = setup_client() + token_id = create_fungible_token(client, operator_id, operator_key) + + info = TokenInfoQuery().set_token_id(token_id).execute(client) + print(f"Fungible token info: {info}") + +if __name__ == "__main__": + query_token_info() diff --git a/examples/query_token_info_nft.py b/examples/query_token_info_nft.py new file mode 100644 index 000000000..d1ab5ce7a --- /dev/null +++ b/examples/query_token_info_nft.py @@ -0,0 +1,73 @@ +import os +import sys + +from dotenv import load_dotenv + +from hiero_sdk_python import ( + Client, + AccountId, + PrivateKey, + Network, +) +from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenType +from hiero_sdk_python.query.token_info_query import TokenInfoQuery +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.tokens.supply_type import SupplyType +from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction + +load_dotenv() + +def setup_client(): + """Initialize and set up the client with operator account""" + network = Network(network='solo') + client = Client(network) + + operator_id = AccountId.from_string(os.getenv('OPERATOR_ID')) + operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY')) + client.set_operator(operator_id, operator_key) + + return client, operator_id, operator_key + +def create_nft(client, operator_id, operator_key): + """Create a non-fungible token""" + receipt = ( + TokenCreateTransaction() + .set_token_name("MyExampleNFT") + .set_token_symbol("EXNFT") + .set_decimals(0) + .set_initial_supply(0) + .set_treasury_account_id(operator_id) + .set_token_type(TokenType.NON_FUNGIBLE_UNIQUE) + .set_supply_type(SupplyType.FINITE) + .set_max_supply(100) + .set_admin_key(operator_key) + .set_supply_key(operator_key) + .set_freeze_key(operator_key) + .execute(client) + ) + + # Check if nft creation was successful + if receipt.status != ResponseCode.SUCCESS: + print(f"NFT creation failed with status: {ResponseCode.get_name(receipt.status)}") + sys.exit(1) + + # Get token ID from receipt + nft_token_id = receipt.tokenId + print(f"NFT created with ID: {nft_token_id}") + + return nft_token_id + +def query_token_info(): + """ + Demonstrates the token info query functionality by: + 1. Creating a NFT + 2. Querying the token info + """ + client, operator_id, operator_key = setup_client() + token_id = create_nft(client, operator_id, operator_key) + + info = TokenInfoQuery().set_token_id(token_id).execute(client) + print(f"Non-fungible token info: {info}") + +if __name__ == "__main__": + query_token_info() From 3c3a6ea30910729c254c7f08509a4b0f760d45e9 Mon Sep 17 00:00:00 2001 From: dosi Date: Tue, 27 May 2025 22:29:24 +0300 Subject: [PATCH 06/20] docs: update README in examples Signed-off-by: dosi --- examples/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/examples/README.md b/examples/README.md index b5e53734a..194e82899 100644 --- a/examples/README.md +++ b/examples/README.md @@ -28,6 +28,7 @@ You can choose either syntax or even mix both styles in your projects. - [Rejecting a Token](#rejecting-a-token) - [Rejecting a Non-Fungible Token](#rejecting-a-non-fungible-token) - [Querying NFT Info](#querying-nft-info) + - [Querying Fungible Token Info](#querying-fungible-token-info) - [HBAR Transactions](#hbar-transactions) - [Transferring HBAR](#transferring-hbar) - [Topic Transactions](#topic-transactions) @@ -467,6 +468,25 @@ nft_info = nft_info_query.execute(client) print(nft_info) ``` +### Querying Fungible Token Info + +#### Pythonic Syntax: +``` +info_query = TokenInfoQuery(token_id=token_id) +info = info_query.execute(client) +print(info) +``` +#### Method Chaining: +``` +info_query = ( + TokenInfoQuery() + .set_token_id(token_id) + ) + +info = info_query.execute(client) +print(info) +``` + ## HBAR Transactions ### Transferring HBAR From 1a453eecadfaaa43a41b69a65a48a0387e15cb43 Mon Sep 17 00:00:00 2001 From: dosi Date: Tue, 27 May 2025 22:30:11 +0300 Subject: [PATCH 07/20] chore: add TokenInfoQuery to __init__.py Signed-off-by: dosi --- src/hiero_sdk_python/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hiero_sdk_python/__init__.py b/src/hiero_sdk_python/__init__.py index a023f2660..caec1bcbf 100644 --- a/src/hiero_sdk_python/__init__.py +++ b/src/hiero_sdk_python/__init__.py @@ -57,6 +57,7 @@ from .query.transaction_get_receipt_query import TransactionGetReceiptQuery from .query.account_balance_query import CryptoGetAccountBalanceQuery from .query.token_nft_info_query import TokenNftInfoQuery +from .query.token_info_query import TokenInfoQuery # Address book from .address_book.endpoint import Endpoint @@ -118,6 +119,7 @@ "TransactionGetReceiptQuery", "CryptoGetAccountBalanceQuery", "TokenNftInfoQuery", + "TokenInfoQuery", # Address book "Endpoint", From 1c6ac60c0ef2f2a59e64e65b4a32c939b7d5465d Mon Sep 17 00:00:00 2001 From: dosi Date: Wed, 28 May 2025 10:13:02 +0300 Subject: [PATCH 08/20] test: reduce test_token_info_query_execute function length Signed-off-by: dosi --- tests/unit/test_token_info_query.py | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/tests/unit/test_token_info_query.py b/tests/unit/test_token_info_query.py index aa7d07ff2..074bfd63c 100644 --- a/tests/unit/test_token_info_query.py +++ b/tests/unit/test_token_info_query.py @@ -53,29 +53,12 @@ def test_token_info_query_execute(mock_account_ids): name="Test Token", symbol="TEST", decimals=8, - totalSupply=1000000, + totalSupply=100, treasury=account_id.to_proto(), - adminKey=None, - kycKey=None, - freezeKey=None, - wipeKey=None, - supplyKey=None, defaultFreezeStatus=0, defaultKycStatus=0, - deleted=False, autoRenewAccount=renew_account_id.to_proto(), - autoRenewPeriod=None, - expiry=None, - memo="", - tokenType=0, - supplyType=0, - maxSupply=0, - fee_schedule_key=None, - custom_fees=None, - pause_key=None, - pause_status=0, - metadata_key=None, - ledger_id=None + maxSupply=10000 ) response = response_pb2.Response( @@ -92,21 +75,21 @@ def test_token_info_query_execute(mock_account_ids): response_sequences = [[response]] with mock_hedera_servers(response_sequences) as client: - query = TokenInfoQuery().set_token_id(token_id) + query = TokenInfoQuery(token_id) try: result = query.execute(client) except Exception as e: pytest.fail(f"Unexpected exception raised: {e}") - # Verify the result contains the expected values assert result.tokenId.shard == 1 assert result.tokenId.realm == 1 assert result.tokenId.num == 1 assert result.name == "Test Token" assert result.symbol == "TEST" assert result.decimals == 8 - assert result.totalSupply == 1000000 + assert result.totalSupply == 100 + assert result.maxSupply == 10000 assert result.treasury.shard == 0 assert result.treasury.realm == 0 assert result.treasury.num == 1 From df1069cddc8b495de8806d15da06fb3fed15dbe7 Mon Sep 17 00:00:00 2001 From: dosi Date: Wed, 28 May 2025 10:55:13 +0300 Subject: [PATCH 09/20] test: fix incorrect integration test function names Signed-off-by: dosi --- tests/integration/token_info_query_e2e_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integration/token_info_query_e2e_test.py b/tests/integration/token_info_query_e2e_test.py index 053fc7f9b..70d96dbf6 100644 --- a/tests/integration/token_info_query_e2e_test.py +++ b/tests/integration/token_info_query_e2e_test.py @@ -42,7 +42,7 @@ def test_integration_token_info_query_can_execute(): env.close() @pytest.mark.integration -def test_integration_token_info_query_insufficient_cost(): +def test_integration_token_info_query_fails_with_insufficient_tx_fee(): """Test that token info query fails with insufficient payment.""" env = IntegrationTestEnv() @@ -58,11 +58,12 @@ def test_integration_token_info_query_insufficient_cost(): env.close() @pytest.mark.integration -def test_integration_token_info_query_can_execute_with_invalid_token_id(): +def test_integration_token_info_query_fails_with_invalid_token_id(): env = IntegrationTestEnv() try: token_id = TokenId(0,0,123456789) + with pytest.raises(PrecheckError, match="failed precheck with status: INVALID_TOKEN_ID"): TokenInfoQuery(token_id).execute(env.client) finally: From 67c9eb7ddb4f32cc44199398502d6e46dbf022da Mon Sep 17 00:00:00 2001 From: dosi Date: Wed, 28 May 2025 11:33:48 +0300 Subject: [PATCH 10/20] docs: change network from solo to testnet and some minor changes in examples Signed-off-by: dosi --- examples/query_token_info_fungible.py | 14 ++++++++------ examples/query_token_info_nft.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/query_token_info_fungible.py b/examples/query_token_info_fungible.py index 60bb5e909..3308fc908 100644 --- a/examples/query_token_info_fungible.py +++ b/examples/query_token_info_fungible.py @@ -19,7 +19,7 @@ def setup_client(): """Initialize and set up the client with operator account""" - network = Network(network='solo') + network = Network(network='testnet') client = Client(network) operator_id = AccountId.from_string(os.getenv('OPERATOR_ID')) @@ -28,7 +28,7 @@ def setup_client(): return client, operator_id, operator_key -def create_fungible_token(client: 'Client', treasury_id, treasury_private_key): +def create_fungible_token(client, operator_id, operator_key): """Create a fungible token""" receipt = ( TokenCreateTransaction() @@ -36,20 +36,22 @@ def create_fungible_token(client: 'Client', treasury_id, treasury_private_key): .set_token_symbol("EXFT") .set_decimals(2) .set_initial_supply(100) - .set_treasury_account_id(treasury_id) + .set_treasury_account_id(operator_id) .set_token_type(TokenType.FUNGIBLE_COMMON) .set_supply_type(SupplyType.FINITE) .set_max_supply(1000) - .set_admin_key(treasury_private_key) - .set_supply_key(treasury_private_key) - .set_freeze_key(treasury_private_key) + .set_admin_key(operator_key) + .set_supply_key(operator_key) + .set_freeze_key(operator_key) .execute(client) ) + # Check if token creation was successful if receipt.status != ResponseCode.SUCCESS: print(f"Fungible token creation failed with status: {ResponseCode.get_name(receipt.status)}") sys.exit(1) + # Get token ID from receipt token_id = receipt.tokenId print(f"Fungible token created with ID: {token_id}") diff --git a/examples/query_token_info_nft.py b/examples/query_token_info_nft.py index d1ab5ce7a..f52d209c5 100644 --- a/examples/query_token_info_nft.py +++ b/examples/query_token_info_nft.py @@ -19,7 +19,7 @@ def setup_client(): """Initialize and set up the client with operator account""" - network = Network(network='solo') + network = Network(network='testnet') client = Client(network) operator_id = AccountId.from_string(os.getenv('OPERATOR_ID')) From d7d6c18e6c20c79cbdc8b135142b7877b3a3f8f2 Mon Sep 17 00:00:00 2001 From: dosi Date: Fri, 30 May 2025 13:05:13 +0300 Subject: [PATCH 11/20] test: address PR comments for integration/unit tests Signed-off-by: dosi --- .../integration/token_info_query_e2e_test.py | 8 ++--- tests/unit/test_token_info_query.py | 29 ++++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/tests/integration/token_info_query_e2e_test.py b/tests/integration/token_info_query_e2e_test.py index 70d96dbf6..32c60b53c 100644 --- a/tests/integration/token_info_query_e2e_test.py +++ b/tests/integration/token_info_query_e2e_test.py @@ -24,7 +24,7 @@ def test_integration_token_info_query_can_execute(): assert info.name == "PTokenTest34", "Name mismatch" assert info.symbol == "PTT34", "Symbol mismatch" assert info.decimals == 3, "Decimals mismatch" - assert str(info.treasury) == str(env.client.operator_account_id), "Treasury mismatch" + assert str(info.treasury) == str(env.operator_id), "Treasury mismatch" assert info.tokenType == TokenType.FUNGIBLE_COMMON, "Token type mismatch" assert info.supplyType == SupplyType.FINITE, "Supply type mismatch" assert info.maxSupply == 10000, "Max supply mismatch" @@ -35,9 +35,9 @@ def test_integration_token_info_query_can_execute(): assert info.supplyKey is not None, "Supply key should not be None" assert info.kycKey is None, "KYC key should be None" - assert str(info.adminKey) == str(env.client.operator_private_key.public_key()), "Admin key mismatch" - assert str(info.freezeKey) == str(env.client.operator_private_key.public_key()), "Freeze key mismatch" - assert str(info.supplyKey) == str(env.client.operator_private_key.public_key()), "Supply key mismatch" + assert str(info.adminKey) == str(env.operator_key.public_key()), "Admin key mismatch" + assert str(info.freezeKey) == str(env.operator_key.public_key()), "Freeze key mismatch" + assert str(info.supplyKey) == str(env.operator_key.public_key()), "Supply key mismatch" finally: env.close() diff --git a/tests/unit/test_token_info_query.py b/tests/unit/test_token_info_query.py index 074bfd63c..a6fd457b6 100644 --- a/tests/unit/test_token_info_query.py +++ b/tests/unit/test_token_info_query.py @@ -24,7 +24,7 @@ def test_constructor(token_id): assert query.token_id == token_id # This test uses fixture mock_client as parameter -def test_execute_without_token_id(mock_client): +def test_execute_fails_with_missing_token_id(mock_client): """Test request creation with missing Token ID.""" query = TokenInfoQuery() @@ -44,8 +44,8 @@ def test_get_method(): assert method.transaction is None assert method.query == mock_token_stub.getTokenInfo -# This test uses fixture mock_account_ids as parameter -def test_token_info_query_execute(mock_account_ids): +# This test uses fixture (mock_account_ids, private_key) as parameter +def test_token_info_query_execute(mock_account_ids, private_key): """Test basic functionality of TokenInfoQuery with mock server.""" account_id, renew_account_id, _, token_id, _ = mock_account_ids token_info_response = token_get_info_pb2.TokenInfo( @@ -58,7 +58,10 @@ def test_token_info_query_execute(mock_account_ids): defaultFreezeStatus=0, defaultKycStatus=0, autoRenewAccount=renew_account_id.to_proto(), - maxSupply=10000 + maxSupply=10000, + adminKey=private_key.public_key().to_proto(), + kycKey=private_key.public_key().to_proto(), + wipeKey=private_key.public_key().to_proto(), ) response = response_pb2.Response( @@ -82,20 +85,18 @@ def test_token_info_query_execute(mock_account_ids): except Exception as e: pytest.fail(f"Unexpected exception raised: {e}") - assert result.tokenId.shard == 1 - assert result.tokenId.realm == 1 - assert result.tokenId.num == 1 + assert result.tokenId == token_id assert result.name == "Test Token" assert result.symbol == "TEST" assert result.decimals == 8 assert result.totalSupply == 100 assert result.maxSupply == 10000 - assert result.treasury.shard == 0 - assert result.treasury.realm == 0 - assert result.treasury.num == 1 - assert result.autoRenewAccount.shard == 0 - assert result.autoRenewAccount.realm == 0 - assert result.autoRenewAccount.num == 2 + assert result.treasury == account_id + assert result.autoRenewAccount == renew_account_id assert result.defaultFreezeStatus == 0 assert result.defaultKycStatus == 0 - assert result.isDeleted == False \ No newline at end of file + assert result.adminKey.to_bytes_raw() == private_key.public_key().to_bytes_raw() + assert result.kycKey.to_bytes_raw() == private_key.public_key().to_bytes_raw() + assert result.wipeKey.to_bytes_raw() == private_key.public_key().to_bytes_raw() + assert result.supplyKey == None + assert result.freezeKey == None \ No newline at end of file From 79c1f1033359d73cbbfe386e4d70a1f3a6bc083f Mon Sep 17 00:00:00 2001 From: dosi Date: Fri, 30 May 2025 13:05:33 +0300 Subject: [PATCH 12/20] docs: update examples for TokenInfoQuery to address PR comments Signed-off-by: dosi --- examples/query_token_info_fungible.py | 3 ++- examples/query_token_info_nft.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/query_token_info_fungible.py b/examples/query_token_info_fungible.py index 3308fc908..31a5897fb 100644 --- a/examples/query_token_info_fungible.py +++ b/examples/query_token_info_fungible.py @@ -61,7 +61,8 @@ def query_token_info(): """ Demonstrates the token info query functionality by: 1. Creating a fungible token - 2. Querying the token info + 2. Querying the token's information using TokenInfoQuery + 3. Printing the token details of the TokenInfo object """ client, operator_id, operator_key = setup_client() token_id = create_fungible_token(client, operator_id, operator_key) diff --git a/examples/query_token_info_nft.py b/examples/query_token_info_nft.py index f52d209c5..246b8f387 100644 --- a/examples/query_token_info_nft.py +++ b/examples/query_token_info_nft.py @@ -61,7 +61,8 @@ def query_token_info(): """ Demonstrates the token info query functionality by: 1. Creating a NFT - 2. Querying the token info + 2. Querying the token's information using TokenInfoQuery + 3. Printing the token details of the TokenInfo object """ client, operator_id, operator_key = setup_client() token_id = create_nft(client, operator_id, operator_key) From 50f258eae5351b936a146c478d387ee8173153bf Mon Sep 17 00:00:00 2001 From: Angelina Date: Mon, 8 Sep 2025 12:06:11 -0400 Subject: [PATCH 13/20] Add set_node_account_id method Signed-off-by: Angelina --- src/hiero_sdk_python/query/query.py | 42 +++++++++++++++ .../transaction/transaction.py | 5 +- tests/unit/test_query_nodes.py | 31 +++++++++++ tests/unit/test_transaction_nodes.py | 52 +++++++++++++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_query_nodes.py create mode 100644 tests/unit/test_transaction_nodes.py diff --git a/src/hiero_sdk_python/query/query.py b/src/hiero_sdk_python/query/query.py index fb7d9f858..8e3d8de81 100644 --- a/src/hiero_sdk_python/query/query.py +++ b/src/hiero_sdk_python/query/query.py @@ -59,6 +59,9 @@ def __init__(self) -> None: self.node_index: int = 0 self.payment_amount: Optional[Hbar] = None + self.node_account_ids: Optional[List[AccountId]] = None + self._used_node_account_id: Optional[AccountId] = None + def _get_query_response(self, response: Any) -> query_pb2.Query: """ Extracts the query-specific response object from the full response. @@ -379,3 +382,42 @@ def _is_payment_required(self) -> bool: bool: True if payment is required, False otherwise """ return True + + def set_node_account_ids(self, node_account_ids: List[AccountId]): + """ + Sets the list of node account IDs the query can be sent to. + + Args: + node_account_ids (List[AccountId]): The list of node account IDs. + + Returns: + Self: Returns self for method chaining. + """ + self.node_account_ids = node_account_ids + return self + def set_node_account_id(self, node_account_id: AccountId): + """ + Sets a single node account ID the query will be sent to. + + Args: + node_account_id (AccountId): The node account ID. + + Returns: + Self: Returns self for method chaining. + """ + + return self.set_node_account_ids([node_account_id]) + + def _select_node_account_id(self) -> Optional[AccountId]: + """ + Internal method to select a node account ID to send the query to. + Defaults to the first in the list. + + Returns: + Optional[AccountId]: The selected node account ID. + """ + if self.node_account_ids: + selected = self.node_account_ids[0] + self._used_node_account_id = selected + return selected + return None \ No newline at end of file diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index 097661f6a..6c9142bff 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -59,7 +59,10 @@ def __init__(self) -> None: # and ensures that the correct signatures are used when submitting transactions self._signature_map: dict[bytes, basic_types_pb2.SignatureMap] = {} self._default_transaction_fee = 2_000_000 - self.operator_account_id = None + self.operator_account_id = None + + self.node_account_ids: Optional[List[AccountId]] = None + self._used_node_account_id: Optional[AccountId] = None def _make_request(self): """ diff --git a/tests/unit/test_query_nodes.py b/tests/unit/test_query_nodes.py new file mode 100644 index 000000000..6696be9a7 --- /dev/null +++ b/tests/unit/test_query_nodes.py @@ -0,0 +1,31 @@ +import pytest +from hiero_sdk_python.query.query import Query +from hiero_sdk_python.account.account_id import AccountId + +def test_set_single_node_account_id(): + q = Query() + node = AccountId(0, 0, 3) + + q.set_node_account_id(node) + + assert q.node_account_ids == [node] + assert q._used_node_account_id is None # not selected until execution + +def test_set_multiple_node_account_ids(): + q = Query() + nodes = [AccountId(0, 0, 3), AccountId(0, 0, 4)] + + q.set_node_account_ids(nodes) + + assert q.node_account_ids == nodes + assert q._used_node_account_id is None + +def test_select_node_account_id(): + q = Query() + nodes = [AccountId(0, 0, 3), AccountId(0, 0, 4)] + q.set_node_account_ids(nodes) + + selected = q._select_node_account_id() + + assert selected == nodes[0] + assert q._used_node_account_id == nodes[0] diff --git a/tests/unit/test_transaction_nodes.py b/tests/unit/test_transaction_nodes.py new file mode 100644 index 000000000..7f0d8e718 --- /dev/null +++ b/tests/unit/test_transaction_nodes.py @@ -0,0 +1,52 @@ +import pytest +from hiero_sdk_python.transaction.transaction import Transaction +from hiero_sdk_python.account.account_id import AccountId + + +class DummyTransaction(Transaction): + """ + Minimal subclass of Transaction for testing. + Transaction is abstract (requires build methods), so we stub them out. + """ + def __init__(self): + super().__init__() + + def build_base_transaction_body(self): + return None # stub + + def _make_request(self): + return None # stub + + def _get_method(self): + return None # stub + + +def test_set_single_node_account_id(): + txn = DummyTransaction() + node = AccountId(0, 0, 3) + + txn.set_node_account_id(node) + + assert txn.node_account_ids == [node] + assert txn._used_node_account_id is None + + +def test_set_multiple_node_account_ids(): + txn = DummyTransaction() + nodes = [AccountId(0, 0, 3), AccountId(0, 0, 4)] + + txn.set_node_account_ids(nodes) + + assert txn.node_account_ids == nodes + assert txn._used_node_account_id is None + + +def test_select_node_account_id(): + txn = DummyTransaction() + nodes = [AccountId(0, 0, 3), AccountId(0, 0, 4)] + txn.set_node_account_ids(nodes) + + selected = txn._select_node_account_id() + + assert selected == nodes[0] + assert txn._used_node_account_id == nodes[0] From 94c89d3182f76dcaff06851992ebcb2d802d5234 Mon Sep 17 00:00:00 2001 From: Angelina Date: Tue, 28 Oct 2025 11:35:33 -0400 Subject: [PATCH 14/20] fix: add missing List import and update changelog Signed-off-by: Angelina --- src/hiero_sdk_python/transaction/transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index 6c9142bff..8ac009edf 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -1,5 +1,5 @@ import hashlib -from typing import Optional +from typing import List, Optional from typing import TYPE_CHECKING From e83e701ba8f22c669ce2ecabaecbf7fef687a892 Mon Sep 17 00:00:00 2001 From: Angelina Date: Tue, 28 Oct 2025 17:31:20 -0400 Subject: [PATCH 15/20] style: add spacing in set_node_account_id method Signed-off-by: Angelina --- src/hiero_sdk_python/query/query.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hiero_sdk_python/query/query.py b/src/hiero_sdk_python/query/query.py index 8e3d8de81..63cf13cdb 100644 --- a/src/hiero_sdk_python/query/query.py +++ b/src/hiero_sdk_python/query/query.py @@ -395,6 +395,7 @@ def set_node_account_ids(self, node_account_ids: List[AccountId]): """ self.node_account_ids = node_account_ids return self + def set_node_account_id(self, node_account_id: AccountId): """ Sets a single node account ID the query will be sent to. From cc7d9a82fda66fa3cd6c76c27e904fc345a21179 Mon Sep 17 00:00:00 2001 From: Angelina Date: Wed, 29 Oct 2025 11:30:30 -0400 Subject: [PATCH 16/20] feat: support selecting node account ID for execution flow Signed-off-by: Angelina --- src/hiero_sdk_python/executable.py | 13 +++++++++---- src/hiero_sdk_python/query/query.py | 3 ++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/hiero_sdk_python/executable.py b/src/hiero_sdk_python/executable.py index 72236b730..c6381aca5 100644 --- a/src/hiero_sdk_python/executable.py +++ b/src/hiero_sdk_python/executable.py @@ -176,10 +176,15 @@ def _execute(self, client: "Client"): if attempt > 0 and current_backoff < self._max_backoff: current_backoff *= 2 - # Set the node account id to the client's node account id - node = client.network.current_node - self.node_account_id = node._account_id - + # Select preferred node if provided, fallback to client's default + selected_node_account_id = ( + self._select_node_account_id() + or client.network.current_node._account_id + ) + + self.node_account_id = selected_node_account_id + node = client.network._get_node(self.node_account_id) + # Create a channel wrapper from the client's channel channel = node._get_channel() diff --git a/src/hiero_sdk_python/query/query.py b/src/hiero_sdk_python/query/query.py index 63cf13cdb..36a68d9b7 100644 --- a/src/hiero_sdk_python/query/query.py +++ b/src/hiero_sdk_python/query/query.py @@ -421,4 +421,5 @@ def _select_node_account_id(self) -> Optional[AccountId]: selected = self.node_account_ids[0] self._used_node_account_id = selected return selected - return None \ No newline at end of file + return None + \ No newline at end of file From dac9cd22fe494764f445e36af8be69cdc4e2b48a Mon Sep 17 00:00:00 2001 From: Angelina Date: Thu, 30 Oct 2025 11:47:54 -0400 Subject: [PATCH 17/20] Fix node selection to use _select_node instead of _get_node Signed-off-by: Angelina --- src/hiero_sdk_python/executable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hiero_sdk_python/executable.py b/src/hiero_sdk_python/executable.py index c6381aca5..1d9a3f315 100644 --- a/src/hiero_sdk_python/executable.py +++ b/src/hiero_sdk_python/executable.py @@ -183,7 +183,7 @@ def _execute(self, client: "Client"): ) self.node_account_id = selected_node_account_id - node = client.network._get_node(self.node_account_id) + node = client.network._select_node(self.node_account_id) # Create a channel wrapper from the client's channel channel = node._get_channel() From 13a542ae3a8c74ae3cdb0b6b376590a14d923cf4 Mon Sep 17 00:00:00 2001 From: Angelina Date: Tue, 4 Nov 2025 11:55:01 -0500 Subject: [PATCH 18/20] feat: add Network._get_node() and update executable to support node selection Signed-off-by: Angelina --- CHANGELOG.md | 2 ++ src/hiero_sdk_python/client/network.py | 15 +++++++++++++++ src/hiero_sdk_python/executable.py | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29591fb61..98ea707da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,8 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Added expiration_time, auto_renew_period, auto_renew_account, fee_schedule_key, kyc_key in `TokenCreateTransaction`, `TokenUpdateTransaction` classes - Added comprehensive Google-style docstrings to the `CustomFee` class and its methods in `custom_fee.py`. - docs: Add `docs/sdk_developers/project_structure.md` to explain repository layout and import paths. +- Support selecting specific node account ID(s) for queries and transactions (#362) +- Added `Network._get_node()` method and updated execution flow to use node selection (#362) ### Changed - chore: bumped solo action from 14.0 to 15.0 (#764) diff --git a/src/hiero_sdk_python/client/network.py b/src/hiero_sdk_python/client/network.py index 35dd30ebb..b3ddc4bbf 100644 --- a/src/hiero_sdk_python/client/network.py +++ b/src/hiero_sdk_python/client/network.py @@ -177,6 +177,21 @@ def _select_node(self) -> _Node: self._node_index = (self._node_index + 1) % len(self.nodes) self.current_node = self.nodes[self._node_index] return self.current_node + + def _get_node(self, account_id: AccountId) -> Optional[_Node]: + """ + Get a node matching the given account ID. + + Args: + account_id (AccountId): The account ID of the node to locate. + + Returns: + Optional[_Node]: The matching node, or None if not found. + """ + for node in self.nodes: + if node._account_id == account_id: + return node + return None def get_mirror_address(self) -> str: """ diff --git a/src/hiero_sdk_python/executable.py b/src/hiero_sdk_python/executable.py index 1d9a3f315..c6381aca5 100644 --- a/src/hiero_sdk_python/executable.py +++ b/src/hiero_sdk_python/executable.py @@ -183,7 +183,7 @@ def _execute(self, client: "Client"): ) self.node_account_id = selected_node_account_id - node = client.network._select_node(self.node_account_id) + node = client.network._get_node(self.node_account_id) # Create a channel wrapper from the client's channel channel = node._get_channel() From 7ffc7e4ccec596bf84cbc416ebfb3910043413d9 Mon Sep 17 00:00:00 2001 From: Angelina Date: Mon, 10 Nov 2025 11:26:36 -0500 Subject: [PATCH 19/20] feat: move node account ID selection to _Executable and update execution flow (#362) Signed-off-by: Angelina --- CHANGELOG.md | 3 +- src/hiero_sdk_python/executable.py | 36 +++++++++++++---- src/hiero_sdk_python/query/query.py | 40 ------------------- .../transaction/transaction.py | 3 -- 4 files changed, 30 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98ea707da..d1692cc07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,8 +64,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Added expiration_time, auto_renew_period, auto_renew_account, fee_schedule_key, kyc_key in `TokenCreateTransaction`, `TokenUpdateTransaction` classes - Added comprehensive Google-style docstrings to the `CustomFee` class and its methods in `custom_fee.py`. - docs: Add `docs/sdk_developers/project_structure.md` to explain repository layout and import paths. -- Support selecting specific node account ID(s) for queries and transactions (#362) -- Added `Network._get_node()` method and updated execution flow to use node selection (#362) +- Support selecting specific node account ID(s) for queries and transactions and added `Network._get_node()` with updated execution flow (#362) ### Changed - chore: bumped solo action from 14.0 to 15.0 (#764) diff --git a/src/hiero_sdk_python/executable.py b/src/hiero_sdk_python/executable.py index c6381aca5..8bf9d295f 100644 --- a/src/hiero_sdk_python/executable.py +++ b/src/hiero_sdk_python/executable.py @@ -75,6 +75,26 @@ def __init__(self): self._grpc_deadline = DEFAULT_GRPC_DEADLINE self.node_account_id = None + self.node_account_ids: Optional[List[AccountId]] = None + self._used_node_account_id: Optional[AccountId] = None + + def set_node_account_ids(self, node_account_ids: List[AccountId]): + """Select node account IDs for sending the request.""" + self.node_account_ids = node_account_ids + return self + + def set_node_account_id(self, node_account_id: AccountId): + """Convenience wrapper to set a single node account ID.""" + return self.set_node_account_ids([node_account_id]) + + def _select_node_account_id(self) -> Optional[AccountId]: + """Pick the first preferred node if available, otherwise None.""" + if self.node_account_ids: + selected = self.node_account_ids[0] + self._used_node_account_id = selected + return selected + return None + @abstractmethod def _should_retry(self, response) -> _ExecutionState: """ @@ -177,13 +197,15 @@ def _execute(self, client: "Client"): current_backoff *= 2 # Select preferred node if provided, fallback to client's default - selected_node_account_id = ( - self._select_node_account_id() - or client.network.current_node._account_id - ) - - self.node_account_id = selected_node_account_id - node = client.network._get_node(self.node_account_id) + selected = self._select_node_account_id() + + if selected is not None: + node = client.network._get_node(selected) + else: + node = client.network.current_node + + #Store for logging and receipts + self.node_account_id = node._account_id # Create a channel wrapper from the client's channel channel = node._get_channel() diff --git a/src/hiero_sdk_python/query/query.py b/src/hiero_sdk_python/query/query.py index 36a68d9b7..49862921e 100644 --- a/src/hiero_sdk_python/query/query.py +++ b/src/hiero_sdk_python/query/query.py @@ -382,44 +382,4 @@ def _is_payment_required(self) -> bool: bool: True if payment is required, False otherwise """ return True - - def set_node_account_ids(self, node_account_ids: List[AccountId]): - """ - Sets the list of node account IDs the query can be sent to. - - Args: - node_account_ids (List[AccountId]): The list of node account IDs. - - Returns: - Self: Returns self for method chaining. - """ - self.node_account_ids = node_account_ids - return self - - def set_node_account_id(self, node_account_id: AccountId): - """ - Sets a single node account ID the query will be sent to. - - Args: - node_account_id (AccountId): The node account ID. - - Returns: - Self: Returns self for method chaining. - """ - - return self.set_node_account_ids([node_account_id]) - - def _select_node_account_id(self) -> Optional[AccountId]: - """ - Internal method to select a node account ID to send the query to. - Defaults to the first in the list. - - Returns: - Optional[AccountId]: The selected node account ID. - """ - if self.node_account_ids: - selected = self.node_account_ids[0] - self._used_node_account_id = selected - return selected - return None \ No newline at end of file diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index 8ac009edf..3026b57b6 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -61,9 +61,6 @@ def __init__(self) -> None: self._default_transaction_fee = 2_000_000 self.operator_account_id = None - self.node_account_ids: Optional[List[AccountId]] = None - self._used_node_account_id: Optional[AccountId] = None - def _make_request(self): """ Implements the Executable._make_request method to build the transaction request. From f52124111d761b62b1dca8e21a3444774d7cfaf3 Mon Sep 17 00:00:00 2001 From: Angelina Date: Mon, 17 Nov 2025 18:14:03 -0500 Subject: [PATCH 20/20] Fix imports for List and AccountId; update executable; sync with main Signed-off-by: Angelina --- src/hiero_sdk_python/executable.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hiero_sdk_python/executable.py b/src/hiero_sdk_python/executable.py index 8bf9d295f..f5f4ce8e2 100644 --- a/src/hiero_sdk_python/executable.py +++ b/src/hiero_sdk_python/executable.py @@ -1,12 +1,13 @@ from os import error import time -from typing import Callable, Optional, Any, TYPE_CHECKING +from typing import Callable, Optional, Any, TYPE_CHECKING, List import grpc from abc import ABC, abstractmethod from enum import IntEnum from hiero_sdk_python.channels import _Channel from hiero_sdk_python.exceptions import MaxAttemptsError +from hiero_sdk_python.account.account_id import AccountId if TYPE_CHECKING: from hiero_sdk_python.client.client import Client