From 306cd7a0c1b8800558da581fe37df8fc7787e4d9 Mon Sep 17 00:00:00 2001 From: Manish Dait Date: Thu, 23 Jul 2026 23:43:38 +0530 Subject: [PATCH 1/2] feat: deprecate node_account_id Signed-off-by: Manish Dait --- src/hiero_sdk_python/executable.py | 40 ++++++++++++++----- src/hiero_sdk_python/query/query.py | 6 +++ .../transaction/transaction.py | 39 ++++++++---------- tests/unit/executable_test.py | 16 ++++---- tests/unit/query_test.py | 12 +++--- .../unit/transaction_freeze_and_bytes_test.py | 6 ++- 6 files changed, 71 insertions(+), 48 deletions(-) diff --git a/src/hiero_sdk_python/executable.py b/src/hiero_sdk_python/executable.py index 145311d46..c08f22996 100644 --- a/src/hiero_sdk_python/executable.py +++ b/src/hiero_sdk_python/executable.py @@ -86,12 +86,27 @@ def __init__(self): self._grpc_deadline: float | None = None self._request_timeout: float | None = None - self.node_account_id: AccountId | None = None + self._node_account_id: AccountId | None = None self.node_account_ids: list[AccountId] = [] self._used_node_account_id: AccountId | None = None self._node_account_ids_index: int = 0 + @property + def node_account_id(self) -> AccountId | None: + warnings.warn( + "'node_account_id' is deprecated; use 'node_account_ids' instead.", DeprecationWarning, stacklevel=2 + ) + return self.node_account_ids[0] if len(self.node_account_ids) > 0 else None + + @node_account_id.setter + def node_account_id(self, account_id: AccountId) -> None: + warnings.warn( + "'node_account_id' is deprecated; use 'node_account_ids' instead.", DeprecationWarning, stacklevel=2 + ) + + self.node_account_ids = [account_id] if account_id is not None else [] + def set_node_account_ids(self, node_account_ids: list[AccountId]): """ Explicitly set the node account IDs to execute against. @@ -115,6 +130,11 @@ def set_node_account_id(self, node_account_id: AccountId): Returns: The current instance of the class for chaining. """ + warnings.warn( + "Method 'set_node_account_id()' is deprecated; use 'set_node_account_ids()' instead.", + DeprecationWarning, + stacklevel=2, + ) return self.set_node_account_ids([node_account_id]) def set_max_attempts(self, max_attempts: int): @@ -346,12 +366,12 @@ def _resolve_execution_config(self, client: Client, timeout: int | float | None) if getattr(self, attr) is None: setattr(self, attr, default) - # nodes to which the executaion must be run against, if not provided used nodes from client + # Temp workaround till the chunk-tx fix if not self.node_account_ids: - self.node_account_ids = [node._account_id for node in client.network._healthy_nodes] + self.node_account_ids = [node._account_id for node in client.network.nodes] if not self.node_account_ids: - raise RuntimeError("No healthy nodes available for execution") + raise RuntimeError("No nodes available for execution") def _should_retry_exponentially(self, err: Exception) -> bool: """ @@ -429,10 +449,10 @@ def _execute(self, client: Client, timeout: int | float | None = None): node = client.network._get_node(node_id) if node is None: - raise RuntimeError(f"No node found for node_account_id: {self.node_account_id}") + raise RuntimeError(f"No node found for node_account_id: {self._node_account_id}") # Store for logging and receipts - self.node_account_id = node._account_id + self._node_account_id = node._account_id # Create a channel wrapper from the client's channel channel = node._get_channel() @@ -442,7 +462,7 @@ def _execute(self, client: Client, timeout: int | float | None = None): "requestId", self._get_request_id(), "nodeAccountID", - self.node_account_id, + self._node_account_id, "attempt", attempt + 1, "maxAttempts", @@ -483,7 +503,7 @@ def _execute(self, client: Client, timeout: int | float | None = None): logger.trace( f"{self.__class__.__name__} status received", "nodeAccountID", - self.node_account_id, + self._node_account_id, "network", client.network.network, "state", @@ -518,7 +538,7 @@ def _execute(self, client: Client, timeout: int | float | None = None): case _ExecutionState.FINISHED: # If the transaction completed successfully, map the response and return it logger.trace(f"{self.__class__.__name__} finished execution") - return self._map_response(response, self.node_account_id, proto_request) + return self._map_response(response, self._node_account_id, proto_request) logger.error( "Exceeded maximum attempts for request", @@ -529,7 +549,7 @@ def _execute(self, client: Client, timeout: int | float | None = None): ) raise MaxAttemptsError( "Exceeded maximum attempts or request timeout", - self.node_account_id, + self._node_account_id, err_persistant, ) diff --git a/src/hiero_sdk_python/query/query.py b/src/hiero_sdk_python/query/query.py index 6a9f093bc..525693326 100644 --- a/src/hiero_sdk_python/query/query.py +++ b/src/hiero_sdk_python/query/query.py @@ -136,6 +136,9 @@ def _before_execute(self, client: Client) -> None: """ self.operator = self.operator or client.operator + if not self.node_account_ids: + self.node_account_ids = [node._account_id for node in client.network.nodes] + # If no payment amount was specified and payment is required for this query, # get the cost from the network and set it as the payment amount if self.payment_amount is None and self._is_payment_required(): @@ -295,6 +298,9 @@ def get_cost(self, client: Client) -> Hbar: if client is None or client.operator is None: raise ValueError("Client and operator must be set to get the cost") + if not self.node_account_ids: + self.node_account_ids = [node._account_id for node in client.network.nodes] + # Here we execute the query to get the cost of it resp = self._execute(client) query_response = self._get_query_response(resp) diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index 00e7b8ff3..5e57bba56 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -216,9 +216,9 @@ def _to_proto(self): # We require the transaction to be frozen before converting to protobuf self._require_frozen() - body_bytes = self._transaction_body_bytes.get(self.node_account_id) + body_bytes = self._transaction_body_bytes.get(self._node_account_id) if body_bytes is None: - raise ValueError(f"No transaction body found for node {self.node_account_id}") + raise ValueError(f"No transaction body found for node {self._node_account_id}") # Get signature map, or create empty one if transaction is not signed sig_map = self._signature_map.get(body_bytes) @@ -245,7 +245,7 @@ def _resolve_transaction_id(self, client: Client): ) def _resolve_node_ids(self, client: Client): - if self.node_account_id is None and len(self.node_account_ids) == 0 and client is None: + if len(self.node_account_ids) == 0 and client is None: raise ValueError( "Node account ID must be set before freezing. Use freeze_with(client) or manually set node_account_ids." ) @@ -297,27 +297,18 @@ def freeze_with(self, client: Client): if self.batch_key: # For Inner Transaction of batch transaction node_account_id=0.0.0 - self.node_account_id = AccountId(0, 0, 0) + self.node_account_ids = [AccountId(0, 0, 0)] + self._node_account_id = AccountId(0, 0, 0) self._transaction_body_bytes[AccountId(0, 0, 0)] = self.build_transaction_body().SerializeToString() return self - # Single node - if self.node_account_id: - self.set_node_account_id(self.node_account_id) - self._transaction_body_bytes[self.node_account_id] = self.build_transaction_body().SerializeToString() - return self - - # Multiple node - if len(self.node_account_ids) > 0: - for node_account_id in self.node_account_ids: - self.node_account_id = node_account_id - self._transaction_body_bytes[node_account_id] = self.build_transaction_body().SerializeToString() + # If not nodes set manually use the all network nodes available in client + if len(self.node_account_ids) == 0: + self.node_account_ids = [node._account_id for node in client.network.nodes] - else: - # Use all nodes from client network - for node in client.network.nodes: - self.node_account_id = node._account_id - self._transaction_body_bytes[node._account_id] = self.build_transaction_body().SerializeToString() + for node_account_id in self.node_account_ids: + self._node_account_id = node_account_id + self._transaction_body_bytes[node_account_id] = self.build_transaction_body().SerializeToString() return self @@ -405,7 +396,7 @@ def is_signed_by(self, public_key): """ public_key_bytes = public_key.to_bytes_raw() - sig_map = self._signature_map.get(self._transaction_body_bytes.get(self.node_account_id)) + sig_map = self._signature_map.get(self._transaction_body_bytes.get(self._node_account_id)) if sig_map is None: return False @@ -459,7 +450,8 @@ def build_base_transaction_body(self) -> transaction_pb2.TransactionBody: transaction_id_proto = self.transaction_id._to_proto() - selected_node = self.node_account_id or (self.node_account_ids[0] if self.node_account_ids else None) + # This will change with the fix for the chunk tx to create bytes on freeze + selected_node = self._node_account_id or (self.node_account_ids[0] if self.node_account_ids else None) if selected_node is None: raise ValueError("Node account ID is not set.") @@ -877,7 +869,8 @@ def _from_protobuf(cls, transaction_body, body_bytes: bytes, sig_map): transaction.transaction_id = TransactionId._from_proto(transaction_body.transactionID) if transaction_body.HasField("nodeAccountID"): - transaction.node_account_id = AccountId._from_proto(transaction_body.nodeAccountID) + transaction._node_account_id = AccountId._from_proto(transaction_body.nodeAccountID) + transaction.node_account_ids = [AccountId._from_proto(transaction_body.nodeAccountID)] transaction.transaction_fee = transaction_body.transactionFee transaction.transaction_valid_duration = transaction_body.transactionValidDuration.seconds diff --git a/tests/unit/executable_test.py b/tests/unit/executable_test.py index bbe7f76a2..32de25338 100644 --- a/tests/unit/executable_test.py +++ b/tests/unit/executable_test.py @@ -816,7 +816,7 @@ def test_execution_config_inherits_from_client(mock_client): mock_client._request_timeout = 20 tx = AccountCreateTransaction() - + tx.set_node_account_ids([AccountId(0, 0, 3)]) tx._resolve_execution_config(mock_client, None) assert tx._max_attempts == 7 @@ -831,19 +831,19 @@ def test_executable_overrides_client_config(mock_client): mock_client.max_attempts = 10 tx = AccountCreateTransaction().set_max_attempts(3) + tx.set_node_account_ids([AccountId(0, 0, 3)]) tx._resolve_execution_config(mock_client, None) assert tx._max_attempts == 3 -def test_no_healthy_nodes_raises(mock_client): - """Test that execution fails if no healthy nodes are available.""" - mock_client.network._healthy_nodes = [] - +def test_no_nodes_raises_exception(mock_client): + """Test that execution fails if no nodes are available.""" + mock_client.network.nodes = [] tx = AccountCreateTransaction().set_key_without_alias(PrivateKey.generate().public_key()).set_initial_balance(1) - with pytest.raises(RuntimeError, match="No healthy nodes available"): - tx.execute(mock_client) + with pytest.raises(RuntimeError, match="No nodes available for execution"): + tx._resolve_execution_config(mock_client, None) def test_set_node_account_ids_overrides_client_nodes(mock_client): @@ -859,6 +859,7 @@ def test_set_node_account_ids_overrides_client_nodes(mock_client): def test_parameter_timeout_overrides_client_default(mock_client): """Explicit timeout pass on the executable should override the client default timeout.""" tx = AccountCreateTransaction() + tx.set_node_account_ids([AccountId(0, 0, 3)]) tx._resolve_execution_config(mock_client, 2) assert tx._request_timeout == 2 @@ -868,6 +869,7 @@ def test_set_timeout_overrides_parameter_timeout(mock_client): """Explicit timeout set on the tx should override the pass timeout.""" tx = AccountCreateTransaction() tx.set_request_timeout(5) + tx.set_node_account_ids([AccountId(0, 0, 3)]) tx._resolve_execution_config(mock_client, 2) assert tx._request_timeout == 5 diff --git a/tests/unit/query_test.py b/tests/unit/query_test.py index 91cfe5f62..6667d2752 100644 --- a/tests/unit/query_test.py +++ b/tests/unit/query_test.py @@ -61,9 +61,9 @@ def test_before_execute_payment_not_required(query, mock_client): # payment_amount is None, should not set payment_amount query._before_execute(mock_client) - # since node_account_ids is not set it will be empty - # query internally use node form client - assert query.node_account_ids == [] + # since node_account_ids is not set it will use node form client + assert len(query.node_account_ids) == len(mock_client.network.nodes) + assert query.node_account_ids == [node._account_id for node in mock_client.network.nodes] assert query.operator == mock_client.operator assert query.payment_amount is None @@ -79,9 +79,9 @@ def test_before_execute_payment_required(query_requires_payment, mock_client): # payment_amount is None, should set payment_amount to 2 Hbars query_requires_payment._before_execute(mock_client) - # since node_account_ids is not set it will be empty - # query internally use node form client - assert query_requires_payment.node_account_ids == [] + # since node_account_ids is not set it will use node form client + assert len(query_requires_payment.node_account_ids) == len(mock_client.network.nodes) + assert query_requires_payment.node_account_ids == [node._account_id for node in mock_client.network.nodes] assert query_requires_payment.operator == mock_client.operator assert query_requires_payment.payment_amount.to_tinybars() == Hbar(2).to_tinybars() diff --git a/tests/unit/transaction_freeze_and_bytes_test.py b/tests/unit/transaction_freeze_and_bytes_test.py index c61302b39..cf7cef517 100644 --- a/tests/unit/transaction_freeze_and_bytes_test.py +++ b/tests/unit/transaction_freeze_and_bytes_test.py @@ -558,7 +558,7 @@ def test_changing_node_after_freeze_fails_for_to_bytes(): assert isinstance(bytes_node_1, bytes) # Change to a different node that wasn't frozen - transaction.node_account_id = node_id_2 + transaction._node_account_id = node_id_2 # This should fail - no transaction body for node_id_2 with pytest.raises(ValueError, match="No transaction body found for node"): @@ -663,7 +663,9 @@ def test_transaction_freeze_without_node_ids(mock_client): tx = TransferTransaction() tx.freeze_with(mock_client) - assert tx.node_account_ids == [] + assert len(tx.node_account_ids) == len(mock_client.network.nodes) + assert tx.node_account_ids == [node._account_id for node in mock_client.network.nodes] + # Verify creates transaction_bytes for client network nodes assert len(tx._transaction_body_bytes) == len(mock_client.network.nodes) assert set(tx._transaction_body_bytes.keys()) == set(node._account_id for node in mock_client.network.nodes) From 6c743bd383556093f2676644bc8224329fb17c3a Mon Sep 17 00:00:00 2001 From: Manish Dait Date: Thu, 23 Jul 2026 23:44:02 +0530 Subject: [PATCH 2/2] chore: added aditional test Signed-off-by: Manish Dait --- tests/unit/transaction_test.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/unit/transaction_test.py b/tests/unit/transaction_test.py index 97698a16a..86f32e68c 100644 --- a/tests/unit/transaction_test.py +++ b/tests/unit/transaction_test.py @@ -553,3 +553,37 @@ def test_transaction_default_max_fee(account_id): assert tx_body is not None assert tx_body.transactionFee == Hbar(2).to_tinybars() + + +def test_transaction_body_bytes_for_each_node_id_on_freeze(mock_client): + """Test create transaction body bytes for each network node when frozen.""" + tx = AccountCreateTransaction().set_key_without_alias(PrivateKey.generate_ecdsa()) + tx.freeze_with(mock_client) + + body_bytes = tx._transaction_body_bytes + + assert body_bytes + assert body_bytes.keys() == {node._account_id for node in mock_client.network.nodes} + + for node_id, body_bytes_value in body_bytes.items(): + body = transaction_pb2.TransactionBody() + body.ParseFromString(body_bytes_value) + assert body.nodeAccountID == AccountId._to_proto(node_id) + + +def test_transaction_body_bytes_for_each_node_id_on_freeze_manual(mock_client): + """Test create transaction body bytes for manually configured node account IDs.""" + node_ids = [AccountId(0, 0, 3), AccountId(0, 0, 4)] + + tx = AccountCreateTransaction().set_key_without_alias(PrivateKey.generate_ecdsa()).set_node_account_ids(node_ids) + tx.freeze_with(mock_client) + + body_bytes = tx._transaction_body_bytes + + assert body_bytes + assert set(body_bytes) == set(node_ids) + + for node_id, body_bytes_value in body_bytes.items(): + body = transaction_pb2.TransactionBody() + body.ParseFromString(body_bytes_value) + assert body.nodeAccountID == AccountId._to_proto(node_id)