Skip to content

Commit f8100c0

Browse files
committed
chore: updated the mirrorstub to update on mirronode address change
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
1 parent 7df9a17 commit f8100c0

3 files changed

Lines changed: 117 additions & 25 deletions

File tree

src/hiero_sdk_python/client/client.py

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ def __init__(self, network: Network = None) -> None:
5757

5858
self.network: Network = network
5959

60-
self.mirror_channel: grpc.Channel = None
61-
self.mirror_stub: mirror_consensus_grpc.ConsensusServiceStub = None
62-
6360
self.max_attempts: int = 10
6461
self.default_max_query_payment: Hbar = DEFAULT_MAX_QUERY_PAYMENT
6562

@@ -69,10 +66,17 @@ def __init__(self, network: Network = None) -> None:
6966
self._grpc_deadline: float = DEFAULT_GRPC_DEADLINE
7067
self._request_timeout: float = DEFAULT_REQUEST_TIMEOUT
7168

72-
self._init_mirror_stub()
73-
7469
self.logger: Logger = Logger(LogLevel.from_env(), "hiero_sdk_python")
7570

71+
@property
72+
def mirror_stub(self) -> mirror_consensus_grpc.ConsensusServiceStub | None:
73+
return self.network.get_mirror_stub()
74+
75+
@property
76+
def mirror_channel(self) -> grpc.Channel | None:
77+
self.network.get_mirror_stub()
78+
return self.network._mirror_channel
79+
7680
@classmethod
7781
def from_env(cls, network: NetworkName | None = None) -> Client:
7882
"""
@@ -180,19 +184,6 @@ def for_network(cls, network_map: dict[str, AccountId], network_name: str | None
180184
nodes = [_Node(account_id, address, None) for address, account_id in network_map.items()]
181185
return cls(Network(network=network_name, nodes=nodes))
182186

183-
def _init_mirror_stub(self) -> None:
184-
"""
185-
Connect to a mirror node for topic message subscriptions.
186-
Mirror nodes always use TLS (mandatory). We use self.network.get_mirror_address()
187-
for a configurable mirror address, which should use port 443 for HTTPS connections.
188-
"""
189-
mirror_address = self.network.get_mirror_address()
190-
if mirror_address.endswith(":50212") or mirror_address.endswith(":443"):
191-
self.mirror_channel = grpc.secure_channel(mirror_address, grpc.ssl_channel_credentials())
192-
else:
193-
self.mirror_channel = grpc.insecure_channel(mirror_address)
194-
self.mirror_stub = mirror_consensus_grpc.ConsensusServiceStub(self.mirror_channel)
195-
196187
def set_operator(self, account_id: AccountId, private_key: PrivateKey) -> None:
197188
"""Sets the operator credentials (account ID and private key)."""
198189
self.operator_account_id = account_id
@@ -228,11 +219,7 @@ def close(self) -> None:
228219
Closes any open gRPC channels and frees resources.
229220
Call this when you are done using the Client to ensure a clean shutdown.
230221
"""
231-
if self.mirror_channel is not None:
232-
self.mirror_channel.close()
233-
self.mirror_channel = None
234-
235-
self.mirror_stub = None
222+
self.network.close_mirror_connection()
236223

237224
def set_transport_security(self, enabled: bool) -> Client:
238225
"""

src/hiero_sdk_python/client/network.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import time
77
from typing import Any
88

9+
import grpc
910
import requests
1011

1112
from hiero_sdk_python.account.account_id import AccountId
1213
from hiero_sdk_python.address_book.node_address import NodeAddress
14+
from hiero_sdk_python.hapi.mirror import consensus_service_pb2_grpc as mirror_consensus_grpc
1315
from hiero_sdk_python.node import _Node
1416

1517

@@ -97,7 +99,9 @@ def __init__(
9799
Use Client.set_transport_security() and Client.set_verify_certificates() to customize.
98100
"""
99101
self.network: str = network or "localhost"
100-
self.mirror_address: str = mirror_address or self.MIRROR_ADDRESS_DEFAULT.get(network, "localhost:5600")
102+
self._mirror_address: str = mirror_address or self.MIRROR_ADDRESS_DEFAULT.get(network, "localhost:5600")
103+
self._mirror_channel = None
104+
self._mirror_stub: mirror_consensus_grpc.ConsensusServiceStub = None
101105

102106
self.ledger_id = ledger_id or self.LEDGER_ID.get(network, bytes.fromhex("03"))
103107

@@ -122,6 +126,17 @@ def __init__(
122126
self._node_index: int = secrets.randbelow(len(self._healthy_nodes))
123127
self.current_node: _Node = self._healthy_nodes[self._node_index]
124128

129+
@property
130+
def mirror_address(self) -> str:
131+
return self._mirror_address
132+
133+
@mirror_address.setter
134+
def mirror_address(self, value: str):
135+
"""Reset the connection when the address changes."""
136+
if self._mirror_address != value:
137+
self._mirror_address = value
138+
self.close_mirror_connection()
139+
125140
def _set_network_nodes(self, nodes: list[_Node] | None = None):
126141
"""Configure the consensus nodes used by this network."""
127142
final_nodes = self._resolve_nodes(nodes)
@@ -406,3 +421,25 @@ def _mark_node_healthy(self, node: _Node) -> None:
406421

407422
if node not in self._healthy_nodes:
408423
self._healthy_nodes.append(node)
424+
425+
def close_mirror_connection(self):
426+
"""Safely closes the mirror gRPC channel."""
427+
if self._mirror_channel is not None:
428+
self._mirror_channel.close()
429+
430+
self._mirror_channel = None
431+
self._mirror_stub = None
432+
433+
def get_mirror_stub(self) -> mirror_consensus_grpc.ConsensusServiceStub:
434+
"""Returns the mirror stub."""
435+
if self._mirror_stub is None:
436+
addr = self._mirror_address
437+
438+
if addr.endswith(":50212") or addr.endswith(":443"):
439+
self._mirror_channel = grpc.secure_channel(addr, grpc.ssl_channel_credentials())
440+
else:
441+
self._mirror_channel = grpc.insecure_channel(addr)
442+
443+
self._mirror_stub = mirror_consensus_grpc.ConsensusServiceStub(self._mirror_channel)
444+
445+
return self._mirror_stub

tests/unit/network_test.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import time
44
from unittest.mock import Mock, patch
55

6+
import grpc
67
import pytest
78

89
from hiero_sdk_python.account.account_id import AccountId
@@ -487,7 +488,7 @@ def test_non_hosted_network_respects_port_50211(network):
487488
"""Test that on non-hosted network, port 50211 stays 50211 and remains non-tls."""
488489
node_50211 = _Node(AccountId(0, 0, 3), "127.0.0.1:50211", None)
489490

490-
network = Network(network=None, nodes=[node_50211])
491+
network = Network(network=network, nodes=[node_50211])
491492

492493
assert ":50211" in str(network.nodes[0]._address)
493494
assert network.nodes[0]._address._is_transport_security() is False
@@ -504,3 +505,70 @@ def test_non_hosted_network_respect_port_50212(network):
504505
assert ":50212" in str(network.nodes[0]._address)
505506
assert network.nodes[0]._address._is_transport_security() is True
506507
assert network._transport_security is False
508+
509+
510+
def test_mirror_address_setter_resets_connection(monkeypatch):
511+
"""Test updating the mirror_address automatically closes the existing connection and the stub."""
512+
network = Network("testnet", mirror_address="old.mirror:5600")
513+
514+
mock_channel = Mock(spec=grpc.Channel)
515+
network._mirror_channel = mock_channel
516+
network._mirror_stub = Mock()
517+
518+
network.mirror_address = "new.mirror:5600"
519+
520+
mock_channel.close.assert_called_once()
521+
assert network._mirror_channel is None
522+
assert network._mirror_stub is None
523+
assert network.mirror_address == "new.mirror:5600"
524+
525+
526+
def test_mirror_address_setter_no_op_on_same_value():
527+
"""Test that setting the mirror_address to the current value does not reset the connection."""
528+
network = Network("testnet", mirror_address="same.mirror:5600")
529+
530+
mock_channel = Mock(spec=grpc.Channel)
531+
network._mirror_channel = mock_channel
532+
network._mirror_stub = Mock()
533+
534+
network.mirror_address = "same.mirror:5600"
535+
536+
mock_channel.close.assert_not_called()
537+
assert network._mirror_stub is not None
538+
539+
540+
def test_get_mirror_stub_initializes_secure_channel():
541+
"""Test that get_mirror_stub creates a secure channel for ports 50212 or 443."""
542+
network = Network("testnet", mirror_address="hiero.mirror:50212")
543+
544+
with (
545+
patch("grpc.secure_channel") as mock_secure,
546+
patch("hiero_sdk_python.client.network.mirror_consensus_grpc.ConsensusServiceStub"),
547+
):
548+
network.get_mirror_stub()
549+
550+
mock_secure.assert_called_once()
551+
args, kwargs = mock_secure.call_args
552+
assert any(isinstance(arg, grpc.ChannelCredentials) for arg in args) or "credentials" in kwargs
553+
554+
555+
def test_get_mirror_stub_initializes_insecure_channel():
556+
"""Test get_mirror_stub creates an insecure channel for standard ports."""
557+
network = Network("testnet", mirror_address="localhost:5600")
558+
559+
with (
560+
patch("grpc.insecure_channel") as mock_insecure,
561+
patch("hiero_sdk_python.client.network.mirror_consensus_grpc.ConsensusServiceStub"),
562+
):
563+
network.get_mirror_stub()
564+
565+
mock_insecure.assert_called_once_with("localhost:5600")
566+
567+
568+
def test_close_mirror_connection_is_safe_when_none():
569+
"""Test close_mirror_connection if no connection exists."""
570+
network = Network("testnet")
571+
network._mirror_channel = None
572+
573+
network.close_mirror_connection()
574+
assert network._mirror_stub is None

0 commit comments

Comments
 (0)