11import hashlib
22
3+ from hiero_sdk_python .account .account_id import AccountId
34from hiero_sdk_python .exceptions import PrecheckError
45from hiero_sdk_python .executable import _Executable , _ExecutionState
56from hiero_sdk_python .hapi .services import (basic_types_pb2 , transaction_body_pb2 , transaction_contents_pb2 , transaction_pb2 )
89from hiero_sdk_python .transaction .transaction_id import TransactionId
910from hiero_sdk_python .transaction .transaction_response import TransactionResponse
1011
11-
1212class Transaction (_Executable ):
1313 """
1414 Base class for all Hedera transactions.
@@ -34,8 +34,17 @@ def __init__(self):
3434 self .transaction_valid_duration = 120
3535 self .generate_record = False
3636 self .memo = ""
37- self .transaction_body_bytes = None
38- self .signature_map = basic_types_pb2 .SignatureMap ()
37+ # Maps each node's AccountId to its corresponding transaction body bytes
38+ # This allows us to maintain separate transaction bodies for each node
39+ # which is necessary in case node is unhealthy and we have to switch it with other node.
40+ # Each transaction body has the AccountId of the node it's being submitted to.
41+ # If these do not match `INVALID_NODE_ACCOUNT` error will occur.
42+ self ._transaction_body_bytes : dict [AccountId , bytes ] = {}
43+
44+ # Maps transaction body bytes to their associated signatures
45+ # This allows us to maintain the signatures for each unique transaction
46+ # and ensures that the correct signatures are used when submitting transactions
47+ self ._signature_map : dict [bytes , basic_types_pb2 .SignatureMap ] = {}
3948 self ._default_transaction_fee = 2_000_000
4049 self .operator_account_id = None
4150
@@ -148,18 +157,24 @@ def sign(self, private_key):
148157 """
149158 # We require the transaction to be frozen before signing
150159 self ._require_frozen ()
160+
161+ # We sign the bodies for each node in case we need to switch nodes during execution.
162+ for body_bytes in self ._transaction_body_bytes .values ():
163+ signature = private_key .sign (body_bytes )
151164
152- signature = private_key .sign (self .transaction_body_bytes )
153-
154- public_key_bytes = private_key .public_key ().to_bytes_raw ()
165+ public_key_bytes = private_key .public_key ().to_bytes_raw ()
155166
156- sig_pair = basic_types_pb2 .SignaturePair (
157- pubKeyPrefix = public_key_bytes ,
158- ed25519 = signature
159- )
167+ sig_pair = basic_types_pb2 .SignaturePair (
168+ pubKeyPrefix = public_key_bytes ,
169+ ed25519 = signature
170+ )
160171
161- self .signature_map .sigPair .append (sig_pair )
172+ # We initialize the signature map for this body_bytes if it doesn't exist yet
173+ self ._signature_map .setdefault (body_bytes , basic_types_pb2 .SignatureMap ())
162174
175+ # Append the signature pair to the signature map for this transaction body
176+ self ._signature_map [body_bytes ].sigPair .append (sig_pair )
177+
163178 return self
164179
165180 def to_proto (self ):
@@ -175,9 +190,17 @@ def to_proto(self):
175190 # We require the transaction to be frozen before converting to protobuf
176191 self ._require_frozen ()
177192
193+ body_bytes = self ._transaction_body_bytes .get (self .node_account_id )
194+ if body_bytes is None :
195+ raise ValueError (f"No transaction body found for node { self .node_account_id } " )
196+
197+ sig_map = self ._signature_map .get (body_bytes )
198+ if sig_map is None :
199+ raise ValueError ("No signature map found for the current transaction body" )
200+
178201 signed_transaction = transaction_contents_pb2 .SignedTransaction (
179- bodyBytes = self . transaction_body_bytes ,
180- sigMap = self . signature_map
202+ bodyBytes = body_bytes ,
203+ sigMap = sig_map
181204 )
182205
183206 return transaction_pb2 .Transaction (
@@ -197,18 +220,22 @@ def freeze_with(self, client):
197220 Raises:
198221 Exception: If required IDs are not set.
199222 """
200- if self .transaction_body_bytes is not None :
223+ if self ._transaction_body_bytes :
201224 return self
202-
225+
203226 if self .transaction_id is None :
204227 self .transaction_id = client .generate_transaction_id ()
205-
206- if self .node_account_id is None :
207- self .node_account_id = client .network .current_node ._account_id
208-
209- # print(f"Transaction's node account ID set to: {self.node_account_id}")
210- self .transaction_body_bytes = self .build_transaction_body ().SerializeToString ()
211-
228+
229+ # We iterate through every node in the client's network
230+ # For each node, set the node_account_id and build the transaction body
231+ # This allows the transaction to be submitted to any node in the network
232+ for node in client .network .nodes :
233+ self .node_account_id = node ._account_id
234+ self ._transaction_body_bytes [node ._account_id ] = self .build_transaction_body ().SerializeToString ()
235+
236+ # Set the node account id to the current node in the network
237+ self .node_account_id = client .network .current_node ._account_id
238+
212239 return self
213240
214241 def execute (self , client ):
@@ -228,7 +255,7 @@ def execute(self, client):
228255 MaxAttemptsError: If the transaction/query fails after the maximum number of attempts
229256 ReceiptStatusError: If the query fails with a receipt status error
230257 """
231- if self .transaction_body_bytes is None :
258+ if not self ._transaction_body_bytes :
232259 self .freeze_with (client )
233260
234261 if self .operator_account_id is None :
@@ -257,8 +284,13 @@ def is_signed_by(self, public_key):
257284 bool: True if signed by the given public key, False otherwise.
258285 """
259286 public_key_bytes = public_key .to_bytes_raw ()
260-
261- for sig_pair in self .signature_map .sigPair :
287+
288+ sig_map = self ._signature_map .get (self ._transaction_body_bytes .get (self .node_account_id ))
289+
290+ if sig_map is None :
291+ return False
292+
293+ for sig_pair in sig_map .sigPair :
262294 if sig_pair .pubKeyPrefix == public_key_bytes :
263295 return True
264296 return False
@@ -317,7 +349,7 @@ def _require_not_frozen(self):
317349 Raises:
318350 Exception: If the transaction has already been frozen.
319351 """
320- if self .transaction_body_bytes is not None :
352+ if self ._transaction_body_bytes :
321353 raise Exception ("Transaction is immutable; it has been frozen." )
322354
323355 def _require_frozen (self ):
@@ -330,7 +362,7 @@ def _require_frozen(self):
330362 Raises:
331363 Exception: If the transaction has not been frozen yet.
332364 """
333- if self .transaction_body_bytes is None :
365+ if not self ._transaction_body_bytes :
334366 raise Exception ("Transaction is not frozen" )
335367
336368 def set_transaction_memo (self , memo ):
0 commit comments