11from __future__ import annotations
22
33import math
4- from typing import Literal , overload
54
65from hiero_sdk_python .channels import _Channel
7- from hiero_sdk_python .client .client import Client
86from hiero_sdk_python .consensus .topic_id import TopicId
97from hiero_sdk_python .crypto .private_key import PrivateKey
108from hiero_sdk_python .executable import _Method
11- from hiero_sdk_python .hapi .services import consensus_submit_message_pb2 , timestamp_pb2 , transaction_pb2
9+ from hiero_sdk_python .hapi .services import consensus_submit_message_pb2 , transaction_pb2
1210from hiero_sdk_python .hapi .services .schedulable_transaction_body_pb2 import (
1311 SchedulableTransactionBody ,
1412)
13+ from hiero_sdk_python .transaction .chunked_transaction import ChunkedTransaction
1514from hiero_sdk_python .transaction .custom_fee_limit import CustomFeeLimit
16- from hiero_sdk_python .transaction .transaction import Transaction
17- from hiero_sdk_python .transaction .transaction_id import TransactionId
18- from hiero_sdk_python .transaction .transaction_receipt import TransactionReceipt
19- from hiero_sdk_python .transaction .transaction_response import TransactionResponse
2015
2116
22- class TopicMessageSubmitTransaction (Transaction ):
17+ class TopicMessageSubmitTransaction (ChunkedTransaction ):
2318 """
2419 Represents a transaction that submits a message to a Hedera Consensus Service topic.
2520
2621 Allows setting the target topic ID and message, building the transaction body,
2722 and executing the submission through a network channel.
23+
24+ Supports automatic chunking for large messages.
2825 """
2926
3027 def __init__ (
3128 self ,
3229 topic_id : TopicId | None = None ,
33- message : str | None = None ,
30+ message : bytes | str | None = None ,
3431 chunk_size : int | None = None ,
3532 max_chunks : int | None = None ,
3633 ) -> None :
@@ -39,21 +36,34 @@ def __init__(
3936
4037 Args:
4138 topic_id (TopicId, optional): The ID of the topic.
42- message (str, optional): The message to submit.
43- chunk_size (int, optional): The maximum chunk size in bytes, Default: 1024.
44- max_chunks (int, optional): The maximum number of chunks allowed, Default: 20.
39+ message (str or bytes , optional): The message to submit to the topic .
40+ chunk_size (int, optional): The maximum chunk size in bytes. Default: 1024.
41+ max_chunks (int, optional): The maximum number of chunks allowed. Default: 20.
4542 """
4643 super ().__init__ ()
4744 self .topic_id : TopicId | None = topic_id
48- self .message : str | None = message
49- self .chunk_size : int = chunk_size or 1024
50- self .max_chunks : int = max_chunks or 20
45+ self .message : bytes | str | None = message
46+ self .chunk_size : int = 1024
47+ self .max_chunks : int = 20
48+ if chunk_size is not None :
49+ self .set_chunk_size (chunk_size )
50+
51+ if max_chunks is not None :
52+ self .set_max_chunks (max_chunks )
5153
52- self ._current_chunk_index = 0
5354 self ._total_chunks = self .get_required_chunks ()
54- self ._initial_transaction_id : TransactionId | None = None
55- self ._transaction_ids : list [TransactionId ] = []
56- self ._signing_keys : list [PrivateKey ] = []
55+
56+ def _message_as_bytes (self ) -> bytes :
57+ """
58+ Returns the message encoded as bytes for chunking and protobuf serialization.
59+
60+ Returns:
61+ bytes: The message as bytes.
62+ """
63+ if self .message is None :
64+ return b""
65+
66+ return self .message .encode ("utf-8" ) if isinstance (self .message , str ) else self .message
5767
5868 def get_required_chunks (self ) -> int :
5969 """
@@ -65,7 +75,7 @@ def get_required_chunks(self) -> int:
6575 if not self .message :
6676 return 1
6777
68- content = self .message . encode ( "utf-8" )
78+ content = self ._message_as_bytes ( )
6979 return math .ceil (len (content ) / self .chunk_size )
7080
7181 def set_topic_id (self , topic_id : TopicId ) -> TopicMessageSubmitTransaction :
@@ -82,12 +92,12 @@ def set_topic_id(self, topic_id: TopicId) -> TopicMessageSubmitTransaction:
8292 self .topic_id = topic_id
8393 return self
8494
85- def set_message (self , message : str ) -> TopicMessageSubmitTransaction :
95+ def set_message (self , message : bytes | str ) -> TopicMessageSubmitTransaction :
8696 """
8797 Sets the message to submit to the topic.
8898
8999 Args:
90- message (str): The message to submit to the topic.
100+ message (str or bytes ): The message to submit to the topic.
91101
92102 Returns:
93103 TopicMessageSubmitTransaction: This transaction instance (for chaining).
@@ -102,16 +112,12 @@ def set_chunk_size(self, chunk_size: int) -> TopicMessageSubmitTransaction:
102112 Set maximum chunk size in bytes.
103113
104114 Args:
105- chunk_size (int): The size of each chunk in bytes.
115+ chunk_size (int): The size of each chunk in bytes.
106116
107117 Returns:
108118 TopicMessageSubmitTransaction: This transaction instance (for chaining).
109119 """
110- self ._require_not_frozen ()
111- if chunk_size <= 0 :
112- raise ValueError ("chunk_size must be positive" )
113-
114- self .chunk_size = chunk_size
120+ super ().set_chunk_size (chunk_size )
115121 self ._total_chunks = self .get_required_chunks ()
116122 return self
117123
@@ -125,11 +131,7 @@ def set_max_chunks(self, max_chunks: int) -> TopicMessageSubmitTransaction:
125131 Returns:
126132 TopicMessageSubmitTransaction: This transaction instance (for chaining).
127133 """
128- self ._require_not_frozen ()
129- if max_chunks <= 0 :
130- raise ValueError ("max_chunks must be positive" )
131-
132- self .max_chunks = max_chunks
134+ super ().set_max_chunks (max_chunks )
133135 return self
134136
135137 def set_custom_fee_limits (self , custom_fee_limits : list [CustomFeeLimit ]) -> TopicMessageSubmitTransaction :
@@ -160,21 +162,6 @@ def add_custom_fee_limit(self, custom_fee_limit: CustomFeeLimit) -> TopicMessage
160162 self .custom_fee_limits .append (custom_fee_limit )
161163 return self
162164
163- def _validate_chunking (self ) -> None :
164- """
165- Validates that chunk count does not exceed max_chunks.
166-
167- Raises:
168- ValueError: If chunk count exceeds `max_chunks`.
169- """
170- required = self .get_required_chunks ()
171-
172- if self .max_chunks and required > self .max_chunks :
173- raise ValueError (
174- f"Message requires { required } chunks but max_chunks={ self .max_chunks } . "
175- f"Increase limit with set_max_chunks()."
176- )
177-
178165 def _build_proto_body (self ) -> consensus_submit_message_pb2 .ConsensusSubmitMessageTransactionBody :
179166 """
180167 Returns the protobuf body for the topic message submit transaction.
@@ -185,10 +172,10 @@ def _build_proto_body(self) -> consensus_submit_message_pb2.ConsensusSubmitMessa
185172 Raises:
186173 ValueError: If required fields (message) are missing.
187174 """
188- if self . message is None or self .message == "" :
175+ if not self .message :
189176 raise ValueError ("Missing required fields: message." )
190177
191- content = self .message . encode ( "utf-8" )
178+ content = self ._message_as_bytes ( )
192179
193180 start_index = self ._current_chunk_index * self .chunk_size
194181 end_index = min (start_index + self .chunk_size , len (content ))
@@ -215,8 +202,7 @@ def build_transaction_body(self) -> transaction_pb2.TransactionBody:
215202 Builds and returns the protobuf transaction body for message submission.
216203
217204 Returns:
218- TransactionBody: The protobuf transaction body containing
219- the message submission details.
205+ TransactionBody: The protobuf transaction body containing the message submission details.
220206 """
221207 consensus_submit_message_body = self ._build_proto_body ()
222208 transaction_body = self .build_base_transaction_body ()
@@ -247,180 +233,15 @@ def _get_method(self, channel: _Channel) -> _Method:
247233 """
248234 return _Method (transaction_func = channel .topic .submitMessage , query_func = None )
249235
250- def freeze_with (self , client : Client ) -> TopicMessageSubmitTransaction :
251- if self ._transaction_body_bytes :
252- return self
253-
254- self ._resolve_transaction_id (client )
255-
256- if not self ._transaction_ids :
257- base_timestamp = self .transaction_id .valid_start
258-
259- for i in range (self .get_required_chunks ()):
260- if i == 0 :
261- if self ._initial_transaction_id is None :
262- self ._initial_transaction_id = self .transaction_id
263-
264- chunk_transaction_id = self .transaction_id
265- else :
266- next_nanos = base_timestamp .nanos + i
267-
268- chunk_valid_start = timestamp_pb2 .Timestamp (
269- seconds = base_timestamp .seconds + next_nanos // 1_000_000_000 , nanos = next_nanos % 1_000_000_000
270- )
271- chunk_transaction_id = TransactionId (
272- account_id = self .transaction_id .account_id , valid_start = chunk_valid_start
273- )
274-
275- self ._transaction_ids .append (chunk_transaction_id )
276-
277- return super ().freeze_with (client )
278-
279- @overload
280- def execute (
281- self ,
282- client : Client ,
283- timeout : int | float | None = None ,
284- wait_for_receipt : Literal [True ] = True ,
285- validate_status : bool = False ,
286- ) -> TransactionReceipt : ...
287-
288- @overload
289- def execute (
290- self ,
291- client : Client ,
292- timeout : int | float | None = None ,
293- wait_for_receipt : Literal [False ] = False ,
294- validate_status : bool = False ,
295- ) -> TransactionResponse : ...
296-
297- def execute (
298- self ,
299- client : Client ,
300- timeout : int | float | None = None ,
301- wait_for_receipt : bool = True ,
302- validate_status : bool = False ,
303- ) -> TransactionReceipt | TransactionResponse :
304- """
305- Executes the topic message submit transaction.
306-
307- For multi-chunk transactions, this method will execute all chunks sequentially and return first response.
308-
309- Args:
310- client: The client to execute the transaction with.
311- timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution.
312- wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt.
313- If False, the method returns a TransactionResponse immediately after submission.
314- validate_status: (bool): Whether the query should automatically validate the transaction status (default False).
315-
316- Returns:
317- TransactionReceipt: If wait_for_receipt is True (default)
318- TransactionResponse: If wait_for_receipt is False
319- """
320- # Return the first response as the JS SDK does
321- return self .execute_all (client , timeout , wait_for_receipt , validate_status )[0 ]
322-
323- @overload
324- def execute_all (
325- self ,
326- client : Client ,
327- timeout : int | float | None = None ,
328- wait_for_receipt : Literal [True ] = True ,
329- validate_status : bool = False ,
330- ) -> list [TransactionReceipt ]: ...
331-
332- @overload
333- def execute_all (
334- self ,
335- client : Client ,
336- timeout : int | float | None = None ,
337- wait_for_receipt : Literal [False ] = False ,
338- validate_status : bool = False ,
339- ) -> list [TransactionResponse ]: ...
340-
341- def execute_all (
342- self ,
343- client : Client ,
344- timeout : int | float | None = None ,
345- wait_for_receipt : bool = True ,
346- validate_status : bool = False ,
347- ) -> list [TransactionReceipt ] | list [TransactionResponse ]:
348- """
349- Executes the topic message submit transaction.
350-
351- This method will execute all chunks sequentially and return list of all responses.
352-
353- Args:
354- client: The client to execute the transaction with.
355- timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution.
356- wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt.
357- If False, the method returns a TransactionResponse immediately after submission.
358- validate_status: (bool): Whether the query should automatically validate the transaction status (default False).
359-
360- Returns:
361- List[TransactionReceipt]: If wait_for_receipt is True (default)
362- List[TransactionResponse]: If wait_for_receipt is False
363- """
364- self ._validate_chunking ()
365-
366- if self .get_required_chunks () == 1 :
367- return [super ().execute (client , timeout , wait_for_receipt , validate_status )]
368-
369- # Multi-chunk transaction - execute all chunks
370- responses = []
371-
372- for chunk_index in range (self .get_required_chunks ()):
373- self ._current_chunk_index = chunk_index
374-
375- if self ._transaction_ids and chunk_index < len (self ._transaction_ids ):
376- self .transaction_id = self ._transaction_ids [chunk_index ]
377-
378- self ._transaction_body_bytes .clear ()
379- self ._signature_map .clear ()
380-
381- self .freeze_with (client )
382-
383- for signing_key in self ._signing_keys :
384- super ().sign (signing_key )
385-
386- # Execute the chunk
387- response = super ().execute (client , timeout , wait_for_receipt , validate_status )
388- responses .append (response )
389-
390- return responses
391-
392- def sign (self , private_key : PrivateKey ):
236+ def sign (self , private_key : PrivateKey ) -> TopicMessageSubmitTransaction :
393237 """
394238 Signs the transaction using the provided private key.
395239
396- For multi-chunk transactions, this stores the signing key for later use.
397-
398240 Args:
399241 private_key (PrivateKey): The private key to sign the transaction with.
400- """
401- if private_key not in self ._signing_keys :
402- self ._signing_keys .append (private_key )
403242
243+ Returns:
244+ TopicMessageSubmitTransaction: This transaction instance (for chaining).
245+ """
404246 super ().sign (private_key )
405247 return self
406-
407- @property
408- def body_size_all_chunks (self ) -> list [int ]:
409- """Returns an array of body sizes for transactions with multiple chunks."""
410- self ._require_frozen ()
411- sizes = []
412-
413- original_index = self ._current_chunk_index
414- original_transaction_id = self .transaction_id
415-
416- try :
417- for i , transaction_id in enumerate (self ._transaction_ids ):
418- self ._current_chunk_index = i
419- self .transaction_id = transaction_id
420-
421- sizes .append (self .body_size )
422- finally :
423- self ._current_chunk_index = original_index
424- self .transaction_id = original_transaction_id
425-
426- return sizes
0 commit comments