From 3756beff9c30535b0bd2fb3895df540a011c822e Mon Sep 17 00:00:00 2001 From: Manish Dait Date: Sun, 19 Apr 2026 00:17:09 +0530 Subject: [PATCH 1/9] chore: updated default network Signed-off-by: Manish Dait --- src/hiero_sdk_python/client/client.py | 17 ++++++++ src/hiero_sdk_python/client/network.py | 7 ++-- tck/handlers/sdk.py | 13 +++--- tests/unit/client_test.py | 54 ++++++++++++++++++++++++- tests/unit/network_test.py | 55 ++++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 11 deletions(-) diff --git a/src/hiero_sdk_python/client/client.py b/src/hiero_sdk_python/client/client.py index c3f95ff21..30c0427fc 100644 --- a/src/hiero_sdk_python/client/client.py +++ b/src/hiero_sdk_python/client/client.py @@ -19,6 +19,7 @@ ) from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.logger.logger import Logger, LogLevel +from hiero_sdk_python.node import _Node from hiero_sdk_python.transaction.transaction_id import TransactionId from .network import Network @@ -153,6 +154,22 @@ def for_previewnet(cls) -> Client: """ return cls(Network("previewnet")) + @classmethod + def for_network(cls, network_map: dict[str, AccountId], network_name: str | None = "localhost") -> Client: + """ + Create a Client with a custom set of nodes and an optional network label. + + Args: + network_map (dict[str, AccountId]): A map where keys are "host:port" strings + and values are the node's AccountId. + network_name (str): A label for the network. Defaults to "localhost". + + Returns: + Client: A Client instance configured with the custom network. + """ + nodes = [_Node(account_id, address, None) for address, account_id in network_map.items()] + return cls(Network(network=network_name, nodes=nodes)) + def _init_mirror_stub(self) -> None: """ Connect to a mirror node for topic message subscriptions. diff --git a/src/hiero_sdk_python/client/network.py b/src/hiero_sdk_python/client/network.py index 13450b075..31c9ef9df 100644 --- a/src/hiero_sdk_python/client/network.py +++ b/src/hiero_sdk_python/client/network.py @@ -73,7 +73,7 @@ class Network: def __init__( self, - network: str = "testnet", + network: str | None = None, nodes: list[_Node] | None = None, mirror_address: str | None = None, ledger_id: bytes | None = None, @@ -96,7 +96,7 @@ def __init__( Certificate verification is enabled by default for all networks. Use Client.set_transport_security() and Client.set_verify_certificates() to customize. """ - self.network: str = network or "testnet" + self.network: str = network or "localhost" self.mirror_address: str = mirror_address or self.MIRROR_ADDRESS_DEFAULT.get(network, "localhost:5600") self.ledger_id = ledger_id or self.LEDGER_ID.get(network, bytes.fromhex("03")) @@ -128,7 +128,8 @@ def _set_network_nodes(self, nodes: list[_Node] | None = None): # Apply TLS configuration to all nodes for node in final_nodes: - node._apply_transport_security(self._transport_security) # pylint: disable=protected-access + if self._transport_security: + node._apply_transport_security(self._transport_security) # pylint: disable=protected-access node._set_verify_certificates(self._verify_certificates) # pylint: disable=protected-access node._set_root_certificates(self._root_certificates) # pylint: disable=protected-access diff --git a/tck/handlers/sdk.py b/tck/handlers/sdk.py index 4a169dccf..0fac4013b 100644 --- a/tck/handlers/sdk.py +++ b/tck/handlers/sdk.py @@ -1,7 +1,6 @@ from __future__ import annotations -from hiero_sdk_python import AccountId, Client, Network, PrivateKey -from hiero_sdk_python.node import _Node +from hiero_sdk_python import AccountId, Client, PrivateKey from tck.handlers.registry import rpc_method from tck.param.base import BaseParams from tck.param.sdk import SetupParams @@ -15,11 +14,11 @@ def setup_handler(params: SetupParams) -> SetupResponse: operator_private_key = PrivateKey.from_string(params.operatorPrivateKey) if params.nodeIp and params.nodeAccountId and params.mirrorNetworkIp: - client = Client() - client.network = Network( - nodes=[_Node(AccountId.from_string(params.nodeAccountId), params.nodeIp, None)], - mirror_address=params.mirrorNetworkIp, - ) + nodes = {params.nodeIp: AccountId.from_string(params.nodeAccountId)} + + client = Client.for_network(network_map=nodes) + client.network.mirror_address = params.mirrorNetworkIp + client.set_operator(operator_account_id, operator_private_key) client_type = "custom" diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index 6e722c7b9..6bbd21a29 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -10,8 +10,10 @@ import pytest -from hiero_sdk_python import AccountId, Client, PrivateKey +from hiero_sdk_python import AccountId, Client from hiero_sdk_python.client import client as client_module +from hiero_sdk_python.client.network import Network +from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.node import _Node from hiero_sdk_python.transaction.transaction_id import TransactionId @@ -527,3 +529,53 @@ def test_get_node_account_ids_raises_when_no_nodes(): client.get_node_account_ids() client.close() + + +def test_for_network_initializes_with_custom_map(): + """Test for_network correctly maps strings to AccountIds and Nodes.""" + network_map = {"127.0.0.1:50211": AccountId(0, 0, 3), "127.0.0.1:50212": AccountId(0, 0, 4)} + + client = Client.for_network(network_map) + + assert isinstance(client.network, Network) + assert len(client.network.nodes) == 2 + + node_accounts = [node._account_id.__str__() for node in client.network.nodes] + assert "0.0.3" in node_accounts + assert "0.0.4" in node_accounts + + +@pytest.mark.parametrize("network", ["mainnet", "testnet", "previewnet"]) +def test_for_network_with_hosted_network_forces_tls(network): + """Test that if hosted-net the port 50211 is upgraded to 50212.""" + network_map = {"34.94.106.61:50211": AccountId(0, 0, 3)} + client = Client.for_network(network_map, network_name=network) + + node = client.network.nodes[0] + assert str(node._address) == "34.94.106.61:50212" + assert node._address._is_transport_security() is True + assert client.network.is_transport_security() is True + + +@pytest.mark.parametrize("network", ["local", "localhost", "solo", "custom", None]) +def test_for_network_with_non_hosted_network_not_forces_tls(network): + """Test that if non hosted-net the port 50211 does not change.""" + network_map = {"127.0.0.1:50211": AccountId(0, 0, 3)} + client = Client.for_network(network_map, network_name=network) + + node = client.network.nodes[0] + assert str(node._address) == "127.0.0.1:50211" + assert node._address._is_transport_security() is False + assert client.network.is_transport_security() is False + + +@pytest.mark.parametrize("network", ["local", "localhost", "solo", "custom", None]) +def test_for_network_with_non_hosted_network_not_downgrade_tls(network): + """Test that if non hosted-net the port 50212 does not change.""" + network_map = {"127.0.0.1:50212": AccountId(0, 0, 3)} + client = Client.for_network(network_map, network_name=network) + + node = client.network.nodes[0] + assert str(node._address) == "127.0.0.1:50212" + assert node._address._is_transport_security() is True + assert client.network.is_transport_security() is False # Non hosted network. diff --git a/tests/unit/network_test.py b/tests/unit/network_test.py index 9eea1a702..d342f9089 100644 --- a/tests/unit/network_test.py +++ b/tests/unit/network_test.py @@ -449,3 +449,58 @@ def test_resolve_nodes_fallback_to_default(monkeypatch): assert all(isinstance(n, _Node) for n in resolved_nodes) assert len(resolved_nodes) == expected_count assert resolved_nodes[0]._account_id == network.DEFAULT_NODES[network_name][0][1] + + +def test_network_default_is_local(): + """Test that a new Network defaults to localhost and non-tls.""" + network = Network() + assert network.network == "localhost" + assert network._transport_security is False + + +@pytest.mark.parametrize("network", ["mainnet", "previewnet", "testnet"]) +def test_self_hosted_net_auto_converts_port_50211_to_50212(network): + """Test that self hosted port 50211 is upgraded to 50212 and TLS is enabled.""" + node_50211 = _Node(AccountId(0, 0, 3), "34.94.106.61:50211", None) + + network = Network(network=network, nodes=[node_50211]) + + assert ":50212" in str(network.nodes[0]._address) + assert network.nodes[0]._address._is_transport_security() is True + assert network._transport_security is True + + +@pytest.mark.parametrize("network", ["mainnet", "previewnet", "testnet"]) +def test_self_hosted_network_respect_port_50212(network): + """Test that on self hosted network respect port 50212""" + node_50211 = _Node(AccountId(0, 0, 3), "127.0.0.1:50212", None) + + network = Network(network=network, nodes=[node_50211]) + + assert ":50212" in str(network.nodes[0]._address) + assert network.nodes[0]._address._is_transport_security() is True + assert network._transport_security is True + + +@pytest.mark.parametrize("network", ["local", "localhost", "solo", "custom", None]) +def test_non_hosted_network_respects_port_50211(network): + """Test that on non-hosted network, port 50211 stays 50211 and remains non-tls.""" + node_50211 = _Node(AccountId(0, 0, 3), "127.0.0.1:50211", None) + + network = Network(network=None, nodes=[node_50211]) + + assert ":50211" in str(network.nodes[0]._address) + assert network.nodes[0]._address._is_transport_security() is False + assert network._transport_security is False + + +@pytest.mark.parametrize("network", ["local", "localhost", "solo", "custom", None]) +def test_non_hosted_network_respect_port_50212(network): + """Test that on non hosted network respect port 50212""" + node_50211 = _Node(AccountId(0, 0, 3), "127.0.0.1:50212", None) + + network = Network(network=network, nodes=[node_50211]) + + assert ":50212" in str(network.nodes[0]._address) + assert network.nodes[0]._address._is_transport_security() is True + assert network._transport_security is False From de58d6a8198ceca529c8dd8fbeb59e2bca969ea9 Mon Sep 17 00:00:00 2001 From: Manish Dait Date: Mon, 20 Apr 2026 17:03:39 +0530 Subject: [PATCH 2/9] chore: added check fo the nodes Signed-off-by: Manish Dait --- src/hiero_sdk_python/client/client.py | 11 +++++++++++ tests/unit/client_test.py | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/hiero_sdk_python/client/client.py b/src/hiero_sdk_python/client/client.py index 30c0427fc..bfd2997a4 100644 --- a/src/hiero_sdk_python/client/client.py +++ b/src/hiero_sdk_python/client/client.py @@ -167,6 +167,17 @@ def for_network(cls, network_map: dict[str, AccountId], network_name: str | None Returns: Client: A Client instance configured with the custom network. """ + if not network_map: + raise ValueError("network_map cannot be empty") + + first_node = next(iter(network_map.values())) + shard = first_node.shard + realm = first_node.realm + + for account_id in network_map.values(): + if shard != account_id.shard or realm != account_id.realm: + raise ValueError("network is not valid, all nodes must be in the same shard and realm") + nodes = [_Node(account_id, address, None) for address, account_id in network_map.items()] return cls(Network(network=network_name, nodes=nodes)) diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index 6bbd21a29..d1de2349c 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -579,3 +579,28 @@ def test_for_network_with_non_hosted_network_not_downgrade_tls(network): assert str(node._address) == "127.0.0.1:50212" assert node._address._is_transport_security() is True assert client.network.is_transport_security() is False # Non hosted network. + + +def test_for_network_with_empty_map_raises_error(): + """Test that for_network raises ValueError when map is empty.""" + with pytest.raises(ValueError, match="network_map cannot be empty"): + Client.for_network({}) + + +@pytest.mark.parametrize( + "invalid_map, error_msg", + [ + ( + {"127.0.0.1:50211": AccountId(0, 0, 3), "127.0.0.1:50212": AccountId(1, 0, 4)}, + "network is not valid, all nodes must be in the same shard and realm", + ), + ( + {"127.0.0.1:50211": AccountId(0, 0, 3), "127.0.0.1:50212": AccountId(0, 1, 4)}, + "network is not valid, all nodes must be in the same shard and realm", + ), + ], +) +def test_for_network_invalid_shard_realm_raises_error(invalid_map, error_msg): + """Test that for_network catches mismatched shards or realms.""" + with pytest.raises(ValueError, match=error_msg): + Client.for_network(invalid_map) From e7d4b20c97c87ee742b7c7585590a3901cb698e7 Mon Sep 17 00:00:00 2001 From: Manish Dait Date: Mon, 20 Apr 2026 23:21:51 +0530 Subject: [PATCH 3/9] chore: updated the mirrorstub to update on mirronode address change Signed-off-by: Manish Dait --- src/hiero_sdk_python/client/client.py | 36 ++++--------- src/hiero_sdk_python/client/network.py | 39 +++++++++++++- tests/unit/network_test.py | 70 +++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 28 deletions(-) diff --git a/src/hiero_sdk_python/client/client.py b/src/hiero_sdk_python/client/client.py index bfd2997a4..a66670bd5 100644 --- a/src/hiero_sdk_python/client/client.py +++ b/src/hiero_sdk_python/client/client.py @@ -58,9 +58,6 @@ def __init__(self, network: Network = None) -> None: self.network: Network = network - self.mirror_channel: grpc.Channel = None - self.mirror_stub: mirror_consensus_grpc.ConsensusServiceStub = None - self.max_attempts: int = 10 self.default_max_query_payment: Hbar = DEFAULT_MAX_QUERY_PAYMENT @@ -70,10 +67,17 @@ def __init__(self, network: Network = None) -> None: self._grpc_deadline: float = DEFAULT_GRPC_DEADLINE self._request_timeout: float = DEFAULT_REQUEST_TIMEOUT - self._init_mirror_stub() - self.logger: Logger = Logger(LogLevel.from_env(), "hiero_sdk_python") + @property + def mirror_stub(self) -> mirror_consensus_grpc.ConsensusServiceStub | None: + return self.network.get_mirror_stub() + + @property + def mirror_channel(self) -> grpc.Channel | None: + self.network.get_mirror_stub() + return self.network._mirror_channel + @classmethod def from_env(cls, network: NetworkName | None = None) -> Client: """ @@ -181,21 +185,6 @@ def for_network(cls, network_map: dict[str, AccountId], network_name: str | None nodes = [_Node(account_id, address, None) for address, account_id in network_map.items()] return cls(Network(network=network_name, nodes=nodes)) - def _init_mirror_stub(self) -> None: - """ - Connect to a mirror node for topic message subscriptions. - Mirror nodes always use TLS (mandatory). We use self.network.get_mirror_address() - for a configurable mirror address, which should use port 443 for HTTPS connections. - """ - mirror_address = self.network.get_mirror_address() - if mirror_address.endswith(":50212") or mirror_address.endswith(":443"): - self.mirror_channel = grpc.secure_channel(mirror_address, grpc.ssl_channel_credentials()) - else: - self.mirror_channel = grpc.insecure_channel(mirror_address) - - self.mirror_channel = grpc.intercept_channel(self.mirror_channel, _UserAgentInterceptor()) - self.mirror_stub = mirror_consensus_grpc.ConsensusServiceStub(self.mirror_channel) - def set_operator(self, account_id: AccountId, private_key: PrivateKey) -> None: """Sets the operator credentials (account ID and private key).""" self.operator_account_id = account_id @@ -231,12 +220,7 @@ def close(self) -> None: Closes any open gRPC channels and frees resources. Call this when you are done using the Client to ensure a clean shutdown. """ - # Close mirror channel - if self.mirror_channel is not None: - self.mirror_channel.close() - self.mirror_channel = None - - self.mirror_stub = None + self.network.close_mirror_connection() # Fix: Close all consensus node channels if self.network and self.network.nodes: diff --git a/src/hiero_sdk_python/client/network.py b/src/hiero_sdk_python/client/network.py index 31c9ef9df..d573f2a1b 100644 --- a/src/hiero_sdk_python/client/network.py +++ b/src/hiero_sdk_python/client/network.py @@ -6,10 +6,12 @@ import time from typing import Any +import grpc import requests from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.address_book.node_address import NodeAddress +from hiero_sdk_python.hapi.mirror import consensus_service_pb2_grpc as mirror_consensus_grpc from hiero_sdk_python.node import _Node @@ -97,7 +99,9 @@ def __init__( Use Client.set_transport_security() and Client.set_verify_certificates() to customize. """ self.network: str = network or "localhost" - self.mirror_address: str = mirror_address or self.MIRROR_ADDRESS_DEFAULT.get(network, "localhost:5600") + self._mirror_address: str = mirror_address or self.MIRROR_ADDRESS_DEFAULT.get(network, "localhost:5600") + self._mirror_channel = None + self._mirror_stub: mirror_consensus_grpc.ConsensusServiceStub = None self.ledger_id = ledger_id or self.LEDGER_ID.get(network, bytes.fromhex("03")) @@ -122,6 +126,17 @@ def __init__( self._node_index: int = secrets.randbelow(len(self._healthy_nodes)) self.current_node: _Node = self._healthy_nodes[self._node_index] + @property + def mirror_address(self) -> str: + return self._mirror_address + + @mirror_address.setter + def mirror_address(self, value: str): + """Reset the connection when the address changes.""" + if self._mirror_address != value: + self._mirror_address = value + self.close_mirror_connection() + def _set_network_nodes(self, nodes: list[_Node] | None = None): """Configure the consensus nodes used by this network.""" final_nodes = self._resolve_nodes(nodes) @@ -406,3 +421,25 @@ def _mark_node_healthy(self, node: _Node) -> None: if node not in self._healthy_nodes: self._healthy_nodes.append(node) + + def close_mirror_connection(self): + """Safely closes the mirror gRPC channel.""" + if self._mirror_channel is not None: + self._mirror_channel.close() + + self._mirror_channel = None + self._mirror_stub = None + + def get_mirror_stub(self) -> mirror_consensus_grpc.ConsensusServiceStub: + """Returns the mirror stub.""" + if self._mirror_stub is None: + addr = self._mirror_address + + if addr.endswith(":50212") or addr.endswith(":443"): + self._mirror_channel = grpc.secure_channel(addr, grpc.ssl_channel_credentials()) + else: + self._mirror_channel = grpc.insecure_channel(addr) + + self._mirror_stub = mirror_consensus_grpc.ConsensusServiceStub(self._mirror_channel) + + return self._mirror_stub diff --git a/tests/unit/network_test.py b/tests/unit/network_test.py index d342f9089..303725828 100644 --- a/tests/unit/network_test.py +++ b/tests/unit/network_test.py @@ -3,6 +3,7 @@ import time from unittest.mock import Mock, patch +import grpc import pytest from hiero_sdk_python.account.account_id import AccountId @@ -487,7 +488,7 @@ def test_non_hosted_network_respects_port_50211(network): """Test that on non-hosted network, port 50211 stays 50211 and remains non-tls.""" node_50211 = _Node(AccountId(0, 0, 3), "127.0.0.1:50211", None) - network = Network(network=None, nodes=[node_50211]) + network = Network(network=network, nodes=[node_50211]) assert ":50211" in str(network.nodes[0]._address) assert network.nodes[0]._address._is_transport_security() is False @@ -504,3 +505,70 @@ def test_non_hosted_network_respect_port_50212(network): assert ":50212" in str(network.nodes[0]._address) assert network.nodes[0]._address._is_transport_security() is True assert network._transport_security is False + + +def test_mirror_address_setter_resets_connection(monkeypatch): + """Test updating the mirror_address automatically closes the existing connection and the stub.""" + network = Network("testnet", mirror_address="old.mirror:5600") + + mock_channel = Mock(spec=grpc.Channel) + network._mirror_channel = mock_channel + network._mirror_stub = Mock() + + network.mirror_address = "new.mirror:5600" + + mock_channel.close.assert_called_once() + assert network._mirror_channel is None + assert network._mirror_stub is None + assert network.mirror_address == "new.mirror:5600" + + +def test_mirror_address_setter_no_op_on_same_value(): + """Test that setting the mirror_address to the current value does not reset the connection.""" + network = Network("testnet", mirror_address="same.mirror:5600") + + mock_channel = Mock(spec=grpc.Channel) + network._mirror_channel = mock_channel + network._mirror_stub = Mock() + + network.mirror_address = "same.mirror:5600" + + mock_channel.close.assert_not_called() + assert network._mirror_stub is not None + + +def test_get_mirror_stub_initializes_secure_channel(): + """Test that get_mirror_stub creates a secure channel for ports 50212 or 443.""" + network = Network("testnet", mirror_address="hiero.mirror:50212") + + with ( + patch("grpc.secure_channel") as mock_secure, + patch("hiero_sdk_python.client.network.mirror_consensus_grpc.ConsensusServiceStub"), + ): + network.get_mirror_stub() + + mock_secure.assert_called_once() + args, kwargs = mock_secure.call_args + assert any(isinstance(arg, grpc.ChannelCredentials) for arg in args) or "credentials" in kwargs + + +def test_get_mirror_stub_initializes_insecure_channel(): + """Test get_mirror_stub creates an insecure channel for standard ports.""" + network = Network("testnet", mirror_address="localhost:5600") + + with ( + patch("grpc.insecure_channel") as mock_insecure, + patch("hiero_sdk_python.client.network.mirror_consensus_grpc.ConsensusServiceStub"), + ): + network.get_mirror_stub() + + mock_insecure.assert_called_once_with("localhost:5600") + + +def test_close_mirror_connection_is_safe_when_none(): + """Test close_mirror_connection if no connection exists.""" + network = Network("testnet") + network._mirror_channel = None + + network.close_mirror_connection() + assert network._mirror_stub is None From 4c06156a756a80f9f758f83e3488dec274258104 Mon Sep 17 00:00:00 2001 From: Manish Dait Date: Sat, 25 Apr 2026 01:08:28 +0530 Subject: [PATCH 4/9] chore: added review fix Signed-off-by: Manish Dait --- src/hiero_sdk_python/client/client.py | 4 ++-- src/hiero_sdk_python/client/network.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/hiero_sdk_python/client/client.py b/src/hiero_sdk_python/client/client.py index a66670bd5..838478b09 100644 --- a/src/hiero_sdk_python/client/client.py +++ b/src/hiero_sdk_python/client/client.py @@ -70,11 +70,11 @@ def __init__(self, network: Network = None) -> None: self.logger: Logger = Logger(LogLevel.from_env(), "hiero_sdk_python") @property - def mirror_stub(self) -> mirror_consensus_grpc.ConsensusServiceStub | None: + def mirror_stub(self) -> mirror_consensus_grpc.ConsensusServiceStub: return self.network.get_mirror_stub() @property - def mirror_channel(self) -> grpc.Channel | None: + def mirror_channel(self) -> grpc.Channel: self.network.get_mirror_stub() return self.network._mirror_channel diff --git a/src/hiero_sdk_python/client/network.py b/src/hiero_sdk_python/client/network.py index d573f2a1b..d871a05c0 100644 --- a/src/hiero_sdk_python/client/network.py +++ b/src/hiero_sdk_python/client/network.py @@ -99,11 +99,11 @@ def __init__( Use Client.set_transport_security() and Client.set_verify_certificates() to customize. """ self.network: str = network or "localhost" - self._mirror_address: str = mirror_address or self.MIRROR_ADDRESS_DEFAULT.get(network, "localhost:5600") - self._mirror_channel = None - self._mirror_stub: mirror_consensus_grpc.ConsensusServiceStub = None + self._mirror_address: str = mirror_address or self.MIRROR_ADDRESS_DEFAULT.get(self.network, "localhost:5600") + self._mirror_channel: grpc.Channel | None = None + self._mirror_stub: mirror_consensus_grpc.ConsensusServiceStub | None = None - self.ledger_id = ledger_id or self.LEDGER_ID.get(network, bytes.fromhex("03")) + self.ledger_id = ledger_id or self.LEDGER_ID.get(self.network, bytes.fromhex("03")) # Default TLS configuration: enabled for hosted networks, disabled for local/custom hosted_networks = ("mainnet", "testnet", "previewnet") From 33c8e20efdba6a148f754373a3dd50eef53185f3 Mon Sep 17 00:00:00 2001 From: Manish Dait Date: Thu, 30 Apr 2026 21:31:36 +0530 Subject: [PATCH 5/9] chore: added validation for data Signed-off-by: Manish Dait --- src/hiero_sdk_python/client/network.py | 7 +++++++ tests/unit/network_test.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/hiero_sdk_python/client/network.py b/src/hiero_sdk_python/client/network.py index d871a05c0..1540614ef 100644 --- a/src/hiero_sdk_python/client/network.py +++ b/src/hiero_sdk_python/client/network.py @@ -133,6 +133,13 @@ def mirror_address(self) -> str: @mirror_address.setter def mirror_address(self, value: str): """Reset the connection when the address changes.""" + if not isinstance(value, str): + raise TypeError(f"mirror_address must be a string, not {type(value).__name__}") + + value = value.strip() + if not value: + raise ValueError("mirror_address cannot be empty or just whitespace") + if self._mirror_address != value: self._mirror_address = value self.close_mirror_connection() diff --git a/tests/unit/network_test.py b/tests/unit/network_test.py index 303725828..7e8ddf42a 100644 --- a/tests/unit/network_test.py +++ b/tests/unit/network_test.py @@ -572,3 +572,24 @@ def test_close_mirror_connection_is_safe_when_none(): network.close_mirror_connection() assert network._mirror_stub is None + + +@pytest.mark.parametrize("address", [None, 123, True, [], {}]) +def test_mirror_address_setter_validation_type_error(address): + """Test that setting mirror_address to a non-string raises TypeError.""" + network = Network("testnet", mirror_address="valid.mirror:5600") + network._mirror_stub = Mock() + + with pytest.raises(TypeError, match="mirror_address must be a string"): + network.mirror_address = address + + assert network._mirror_stub is not None + + +@pytest.mark.parametrize("address", ["", " ", "\n"]) +def test_mirror_address_setter_validation_value_error(address): + """Test that setting mirror_address to an empty string raises ValueError.""" + network = Network("testnet", mirror_address="valid.mirror:5600") + + with pytest.raises(ValueError, match="mirror_address cannot be empty"): + network.mirror_address = address From d2bd365298968de6d58269b4637ef3230c18908f Mon Sep 17 00:00:00 2001 From: Manish Dait Date: Tue, 5 May 2026 00:40:59 +0530 Subject: [PATCH 6/9] chore: move close node to network Signed-off-by: Manish Dait --- src/hiero_sdk_python/client/client.py | 7 +------ src/hiero_sdk_python/client/network.py | 6 +++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/hiero_sdk_python/client/client.py b/src/hiero_sdk_python/client/client.py index 838478b09..345e102eb 100644 --- a/src/hiero_sdk_python/client/client.py +++ b/src/hiero_sdk_python/client/client.py @@ -220,12 +220,7 @@ def close(self) -> None: Closes any open gRPC channels and frees resources. Call this when you are done using the Client to ensure a clean shutdown. """ - self.network.close_mirror_connection() - - # Fix: Close all consensus node channels - if self.network and self.network.nodes: - for node in self.network.nodes: - node._close() + self.network._close() def set_transport_security(self, enabled: bool) -> Client: """ diff --git a/src/hiero_sdk_python/client/network.py b/src/hiero_sdk_python/client/network.py index 1540614ef..0b155e49f 100644 --- a/src/hiero_sdk_python/client/network.py +++ b/src/hiero_sdk_python/client/network.py @@ -429,7 +429,7 @@ def _mark_node_healthy(self, node: _Node) -> None: if node not in self._healthy_nodes: self._healthy_nodes.append(node) - def close_mirror_connection(self): + def _close(self): """Safely closes the mirror gRPC channel.""" if self._mirror_channel is not None: self._mirror_channel.close() @@ -437,6 +437,10 @@ def close_mirror_connection(self): self._mirror_channel = None self._mirror_stub = None + if self.nodes: + for node in self.nodes: + node._close() + def get_mirror_stub(self) -> mirror_consensus_grpc.ConsensusServiceStub: """Returns the mirror stub.""" if self._mirror_stub is None: From 9da51074ba874f34d2e3ea597304eb3f709e40da Mon Sep 17 00:00:00 2001 From: Manish Dait Date: Tue, 5 May 2026 01:04:11 +0530 Subject: [PATCH 7/9] chore: fix rebase Signed-off-by: Manish Dait --- src/hiero_sdk_python/client/network.py | 8 ++++++-- tests/unit/network_test.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/hiero_sdk_python/client/network.py b/src/hiero_sdk_python/client/network.py index 0b155e49f..a915d7df0 100644 --- a/src/hiero_sdk_python/client/network.py +++ b/src/hiero_sdk_python/client/network.py @@ -142,7 +142,7 @@ def mirror_address(self, value: str): if self._mirror_address != value: self._mirror_address = value - self.close_mirror_connection() + self._close_mirror_node() def _set_network_nodes(self, nodes: list[_Node] | None = None): """Configure the consensus nodes used by this network.""" @@ -429,7 +429,7 @@ def _mark_node_healthy(self, node: _Node) -> None: if node not in self._healthy_nodes: self._healthy_nodes.append(node) - def _close(self): + def _close_mirror_node(self): """Safely closes the mirror gRPC channel.""" if self._mirror_channel is not None: self._mirror_channel.close() @@ -437,6 +437,10 @@ def _close(self): self._mirror_channel = None self._mirror_stub = None + def _close(self): + """Safely closes the mirror gRPC channel and consensus node.""" + self._close_mirror_node() + if self.nodes: for node in self.nodes: node._close() diff --git a/tests/unit/network_test.py b/tests/unit/network_test.py index 7e8ddf42a..af34db1ba 100644 --- a/tests/unit/network_test.py +++ b/tests/unit/network_test.py @@ -570,7 +570,7 @@ def test_close_mirror_connection_is_safe_when_none(): network = Network("testnet") network._mirror_channel = None - network.close_mirror_connection() + network._close_mirror_node() assert network._mirror_stub is None From 7bbb5d714f0d54dce7d94238e37959b94ae49443 Mon Sep 17 00:00:00 2001 From: Manish Dait Date: Mon, 18 May 2026 13:50:38 +0530 Subject: [PATCH 8/9] chore: remove unused imports Signed-off-by: Manish Dait --- src/hiero_sdk_python/client/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hiero_sdk_python/client/client.py b/src/hiero_sdk_python/client/client.py index 345e102eb..7d2af0a0d 100644 --- a/src/hiero_sdk_python/client/client.py +++ b/src/hiero_sdk_python/client/client.py @@ -12,7 +12,6 @@ from dotenv import load_dotenv from hiero_sdk_python.account.account_id import AccountId -from hiero_sdk_python.channels import _UserAgentInterceptor from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.hapi.mirror import ( consensus_service_pb2_grpc as mirror_consensus_grpc, From 04b916556643fff3c7c6c6b072dc00355b46e10f Mon Sep 17 00:00:00 2001 From: Manish Dait Date: Mon, 18 May 2026 14:29:33 +0530 Subject: [PATCH 9/9] chore: make default to tesnet to preseve backward compatiblity Signed-off-by: Manish Dait --- src/hiero_sdk_python/client/network.py | 2 +- tests/unit/client_test.py | 4 ++-- tests/unit/network_test.py | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/hiero_sdk_python/client/network.py b/src/hiero_sdk_python/client/network.py index a915d7df0..2d117280c 100644 --- a/src/hiero_sdk_python/client/network.py +++ b/src/hiero_sdk_python/client/network.py @@ -98,7 +98,7 @@ def __init__( Certificate verification is enabled by default for all networks. Use Client.set_transport_security() and Client.set_verify_certificates() to customize. """ - self.network: str = network or "localhost" + self.network: str = network or "testnet" self._mirror_address: str = mirror_address or self.MIRROR_ADDRESS_DEFAULT.get(self.network, "localhost:5600") self._mirror_channel: grpc.Channel | None = None self._mirror_stub: mirror_consensus_grpc.ConsensusServiceStub | None = None diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index d1de2349c..b590b48b8 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -557,7 +557,7 @@ def test_for_network_with_hosted_network_forces_tls(network): assert client.network.is_transport_security() is True -@pytest.mark.parametrize("network", ["local", "localhost", "solo", "custom", None]) +@pytest.mark.parametrize("network", ["local", "localhost", "solo", "custom"]) def test_for_network_with_non_hosted_network_not_forces_tls(network): """Test that if non hosted-net the port 50211 does not change.""" network_map = {"127.0.0.1:50211": AccountId(0, 0, 3)} @@ -569,7 +569,7 @@ def test_for_network_with_non_hosted_network_not_forces_tls(network): assert client.network.is_transport_security() is False -@pytest.mark.parametrize("network", ["local", "localhost", "solo", "custom", None]) +@pytest.mark.parametrize("network", ["local", "localhost", "solo", "custom"]) def test_for_network_with_non_hosted_network_not_downgrade_tls(network): """Test that if non hosted-net the port 50212 does not change.""" network_map = {"127.0.0.1:50212": AccountId(0, 0, 3)} diff --git a/tests/unit/network_test.py b/tests/unit/network_test.py index af34db1ba..bcba2e535 100644 --- a/tests/unit/network_test.py +++ b/tests/unit/network_test.py @@ -452,11 +452,11 @@ def test_resolve_nodes_fallback_to_default(monkeypatch): assert resolved_nodes[0]._account_id == network.DEFAULT_NODES[network_name][0][1] -def test_network_default_is_local(): - """Test that a new Network defaults to localhost and non-tls.""" +def test_network_default_is_testnet(): + """Test that a new Network defaults to testnet and tls.""" network = Network() - assert network.network == "localhost" - assert network._transport_security is False + assert network.network == "testnet" + assert network._transport_security is True @pytest.mark.parametrize("network", ["mainnet", "previewnet", "testnet"]) @@ -483,7 +483,7 @@ def test_self_hosted_network_respect_port_50212(network): assert network._transport_security is True -@pytest.mark.parametrize("network", ["local", "localhost", "solo", "custom", None]) +@pytest.mark.parametrize("network", ["local", "localhost", "solo", "custom"]) def test_non_hosted_network_respects_port_50211(network): """Test that on non-hosted network, port 50211 stays 50211 and remains non-tls.""" node_50211 = _Node(AccountId(0, 0, 3), "127.0.0.1:50211", None) @@ -495,7 +495,7 @@ def test_non_hosted_network_respects_port_50211(network): assert network._transport_security is False -@pytest.mark.parametrize("network", ["local", "localhost", "solo", "custom", None]) +@pytest.mark.parametrize("network", ["local", "localhost", "solo", "custom"]) def test_non_hosted_network_respect_port_50212(network): """Test that on non hosted network respect port 50212""" node_50211 = _Node(AccountId(0, 0, 3), "127.0.0.1:50212", None)