Skip to content

Commit 5ec10f6

Browse files
authored
feat: add node implementation (#81)
* feat: add class endpoint Signed-off-by: dosi <dosi.kolev@limechain.tech> * feat: add class NodeAddress Signed-off-by: dosi <dosi.kolev@limechain.tech> * feat: add ManagedNodeAddress Signed-off-by: dosi <dosi.kolev@limechain.tech> * feat: add classManagedNode Signed-off-by: dosi <dosi.kolev@limechain.tech> * feat: add class Node Signed-off-by: dosi <dosi.kolev@limechain.tech> * test: add unit tests Signed-off-by: dosi <dosi.kolev@limechain.tech> * feat: add from_dict func in Endpoint Signed-off-by: dosi <dosi.kolev@limechain.tech> * chore: improve __str__ func in NodeAddress and remove unnecessary funcs in _ManagedNodeAddress Signed-off-by: dosi <dosi.kolev@limechain.tech> * refactor: merge ManagedNode functionality into Node class and remove the unnessary things Signed-off-by: dosi <dosi.kolev@limechain.tech> * feat: add from_dict() function in NodeAddress Signed-off-by: dosi <dosi.kolev@limechain.tech> * refactor: remove unnecessary node logic in Client Signed-off-by: dosi <dosi.kolev@limechain.tech> * refactor: update Network class to use _Node Signed-off-by: dosi <dosi.kolev@limechain.tech> * refactor: update the logic in Transaction and _Executable Signed-off-by: dosi <dosi.kolev@limechain.tech> * fix: added something necessary Signed-off-by: dosi <dosi.kolev@limechain.tech> * refactor: improve _select_node functionality Signed-off-by: dosi <dosi.kolev@limechain.tech> * test: fix unit tests Signed-off-by: dosi <dosi.kolev@limechain.tech> * docs: update docstring for _select_node in Network Signed-off-by: dosi <dosi.kolev@limechain.tech> * fix: replace random with secrets module for more security Signed-off-by: dosi <dosi.kolev@limechain.tech> * fix: update Network to work with DEFAULT_NODES Signed-off-by: dosi <dosi.kolev@limechain.tech> --------- Signed-off-by: dosi <dosi.kolev@limechain.tech>
1 parent cdd5145 commit 5ec10f6

File tree

14 files changed

+619
-91
lines changed

14 files changed

+619
-91
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
from hiero_sdk_python.hapi.services.basic_types_pb2 import ServiceEndpoint
2+
3+
class Endpoint:
4+
"""
5+
Represents an endpoint with address, port, and domain name information.
6+
This class is used to handle service endpoints in the Hedera network.
7+
"""
8+
9+
def __init__(self, address=None, port=None, domain_name=None):
10+
"""
11+
Initialize a new Endpoint instance.
12+
13+
Args:
14+
address (bytes, optional): The IP address in bytes format.
15+
port (int, optional): The port number.
16+
domain_name (str, optional): The domain name.
17+
"""
18+
self._address : bytes = address
19+
self._port : int = port
20+
self._domain_name : str = domain_name
21+
22+
def set_address(self, address):
23+
"""
24+
Set the IP address of the endpoint.
25+
26+
Args:
27+
address (bytes): The IP address in bytes format.
28+
29+
Returns:
30+
Endpoint: This instance for method chaining.
31+
"""
32+
self._address = address
33+
return self
34+
35+
def get_address(self):
36+
"""
37+
Get the IP address of the endpoint.
38+
39+
Returns:
40+
bytes: The IP address in bytes format.
41+
"""
42+
return self._address
43+
44+
def set_port(self, port):
45+
"""
46+
Set the port of the endpoint.
47+
48+
Args:
49+
port (int): The port number.
50+
51+
Returns:
52+
Endpoint: This instance for method chaining.
53+
"""
54+
self._port = port
55+
return self
56+
57+
def get_port(self):
58+
"""
59+
Get the port of the endpoint.
60+
61+
Returns:
62+
int: The port number.
63+
"""
64+
return self._port
65+
66+
def set_domain_name(self, domain_name):
67+
"""
68+
Set the domain name of the endpoint.
69+
70+
Args:
71+
domain_name (str): The domain name.
72+
73+
Returns:
74+
Endpoint: This instance for method chaining.
75+
"""
76+
self._domain_name = domain_name
77+
return self
78+
79+
def get_domain_name(self):
80+
"""
81+
Get the domain name of the endpoint.
82+
83+
Returns:
84+
str: The domain name.
85+
"""
86+
return self._domain_name
87+
88+
@classmethod
89+
def from_proto(cls, service_endpoint : 'ServiceEndpoint'):
90+
"""
91+
Create an Endpoint from a protobuf ServiceEndpoint.
92+
93+
Args:
94+
service_endpoint: The protobuf ServiceEndpoint object.
95+
96+
Returns:
97+
Endpoint: A new Endpoint instance.
98+
"""
99+
port = service_endpoint.port
100+
101+
if port == 0 or port == 50111:
102+
port = 50211
103+
104+
return cls(
105+
address=service_endpoint.ipAddressV4,
106+
port=port,
107+
domain_name=service_endpoint.domain_name
108+
)
109+
110+
def _to_proto(self):
111+
"""
112+
Convert this Endpoint to a protobuf ServiceEndpoint.
113+
114+
Returns:
115+
ServiceEndpoint: A protobuf ServiceEndpoint object.
116+
"""
117+
118+
return ServiceEndpoint(
119+
ipAddressV4=self._address,
120+
port=self._port,
121+
domain_name=self._domain_name
122+
)
123+
124+
def __str__(self):
125+
"""
126+
Get a string representation of the Endpoint.
127+
128+
Returns:
129+
str: The string representation in the format 'domain:port' or 'ip:port'.
130+
"""
131+
132+
return f"{self._address.decode('utf-8')}:{self._port}"
133+
134+
@classmethod
135+
def from_dict(cls, json_data):
136+
"""
137+
Create an Endpoint from a JSON object.
138+
139+
Args:
140+
json_data: The JSON object.
141+
"""
142+
return cls(
143+
address=bytes(json_data.get('ip_address_v4', ''), 'utf-8'),
144+
port=json_data.get('port'),
145+
domain_name=json_data.get('domain_name')
146+
)
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from hiero_sdk_python.account.account_id import AccountId
2+
from hiero_sdk_python.address_book.endpoint import Endpoint
3+
from hiero_sdk_python.hapi.services.basic_types_pb2 import NodeAddress as NodeAddressProto
4+
5+
6+
class NodeAddress:
7+
"""
8+
Represents the address of a node on the Hedera network.
9+
"""
10+
11+
def __init__(
12+
self,
13+
public_key=None,
14+
account_id=None,
15+
node_id=None,
16+
cert_hash=None,
17+
addresses=None,
18+
description=None
19+
):
20+
"""
21+
Initialize a new NodeAddress instance.
22+
23+
Args:
24+
public_key (str, optional): The RSA public key of the node.
25+
account_id (AccountId, optional): The account ID of the node.
26+
node_id (int, optional): The node ID.
27+
cert_hash (bytes, optional): The node certificate hash.
28+
addresses (list[Endpoint], optional): List of endpoints for the node.
29+
description (str, optional): Description of the node.
30+
"""
31+
self._public_key : str = public_key
32+
self._account_id : AccountId = account_id
33+
self._node_id : int = node_id
34+
self._cert_hash : bytes = cert_hash
35+
self._addresses : list[Endpoint] = addresses
36+
self._description : str = description
37+
38+
@classmethod
39+
def _from_proto(cls, node_address_proto : 'NodeAddressProto'):
40+
"""
41+
Create a NodeAddress from a protobuf NodeAddress.
42+
43+
Args:
44+
node_address_proto: The protobuf NodeAddress object.
45+
46+
Returns:
47+
NodeAddress: A new NodeAddress instance.
48+
"""
49+
addresses = []
50+
51+
for endpoint_proto in node_address_proto.serviceEndpoint:
52+
addresses.append(Endpoint.from_proto(endpoint_proto))
53+
54+
account_id = None
55+
if node_address_proto.nodeAccountId:
56+
account_id = AccountId.from_proto(node_address_proto.nodeAccountId)
57+
58+
return cls(
59+
public_key=node_address_proto.RSA_PubKey,
60+
account_id=account_id,
61+
node_id=node_address_proto.nodeId,
62+
cert_hash=node_address_proto.nodeCertHash,
63+
addresses=addresses,
64+
description=node_address_proto.description
65+
)
66+
67+
def _to_proto(self):
68+
"""
69+
Convert this NodeAddress to a protobuf NodeAddress.
70+
71+
Returns:
72+
NodeAddressProto: A protobuf NodeAddress object.
73+
"""
74+
node_address_proto = NodeAddressProto(
75+
RSA_PubKey=self._public_key,
76+
nodeId=self._node_id,
77+
nodeCertHash=self._cert_hash,
78+
description=self._description
79+
)
80+
81+
if self._account_id:
82+
node_address_proto.nodeAccountId.CopyFrom(self._account_id.to_proto())
83+
84+
service_endpoints = []
85+
for endpoint in self._addresses:
86+
service_endpoints.append(endpoint._to_proto())
87+
88+
node_address_proto.serviceEndpoint = service_endpoints
89+
90+
return node_address_proto
91+
92+
def __str__(self):
93+
"""
94+
Get a string representation of the NodeAddress.
95+
96+
Returns:
97+
str: The string representation of the NodeAddress.
98+
"""
99+
addresses_str = ""
100+
for address in self._addresses:
101+
addresses_str += str(address)
102+
cert_hash_str = self._cert_hash.hex()
103+
node_id_str = str(self._node_id)
104+
account_id_str = str(self._account_id)
105+
106+
return (
107+
f"NodeAccountId: {account_id_str} {addresses_str}\n"
108+
f"CertHash: {cert_hash_str}\n"
109+
f"NodeId: {node_id_str}\n"
110+
f"PubKey: {self._public_key or ''}"
111+
)
112+
113+
@classmethod
114+
def _from_dict(cls, node) -> 'NodeAddress':
115+
"""
116+
Create a NodeAddress from a dictionary.
117+
"""
118+
119+
service_endpoints = node.get('service_endpoints', [])
120+
public_key = node.get('public_key')
121+
account_id = AccountId.from_string(node.get('node_account_id'))
122+
node_id = node.get('node_id')
123+
# Get the hash from the node, remove the 0x prefix and convert to bytes
124+
cert_hash = bytes.fromhex(node.get('node_cert_hash').removeprefix('0x'))
125+
description = node.get('description')
126+
127+
endpoints = []
128+
for endpoint in service_endpoints:
129+
endpoints.append(Endpoint.from_dict(endpoint))
130+
131+
return cls(
132+
public_key=public_key,
133+
account_id=account_id,
134+
node_id=node_id,
135+
cert_hash=cert_hash,
136+
description=description,
137+
addresses=endpoints
138+
)

src/hiero_sdk_python/client/client.py

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,11 @@ def __init__(self, network=None):
2424
network = Network()
2525
self.network = network
2626

27-
self.channel = None
28-
2927
self.mirror_channel = None
3028
self.mirror_stub = None
3129

3230
self.max_attempts = 10
33-
34-
node_account_ids = self.get_node_account_ids()
35-
if not node_account_ids:
36-
raise ValueError("No nodes available in the network configuration.")
37-
38-
initial_node_id = node_account_ids[0]
39-
self._switch_node(initial_node_id)
40-
31+
4132
self._init_mirror_stub()
4233

4334
self.logger = Logger(LogLevel.from_env(), "hiero_sdk_python")
@@ -81,30 +72,16 @@ def get_node_account_ids(self):
8172
Returns a list of node AccountIds that the client can use to send queries and transactions.
8273
"""
8374
if self.network and self.network.nodes:
84-
return [account_id for (address, account_id) in self.network.nodes]
75+
return [node._account_id for node in self.network.nodes]
8576
else:
8677
raise ValueError("No nodes available in the network configuration.")
8778

88-
def _switch_node(self, node_account_id):
89-
"""
90-
Switches to the specified node in the network and updates the gRPC stubs.
91-
"""
92-
node_address = self.network.get_node_address(node_account_id)
93-
if node_address is None:
94-
raise ValueError(f"No node address found for account ID {node_account_id}")
95-
96-
self.channel = grpc.insecure_channel(node_address)
97-
self.node_account_id = node_account_id
98-
9979
def close(self):
10080
"""
10181
Closes any open gRPC channels and frees resources.
10282
Call this when you are done using the Client to ensure a clean shutdown.
10383
"""
104-
if self.channel is not None:
105-
self.channel.close()
106-
self.channel = None
107-
84+
10885
if self.mirror_channel is not None:
10986
self.mirror_channel.close()
11087
self.mirror_channel = None

0 commit comments

Comments
 (0)