Skip to content

Commit 16e3540

Browse files
committed
Create ChunkedTransaction class
Signed-off-by: MonaaEid <monaa_eid@hotmail.com>
1 parent ce16902 commit 16e3540

5 files changed

Lines changed: 579 additions & 478 deletions

File tree

Lines changed: 47 additions & 225 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,68 @@
11
from __future__ import annotations
22

3-
import math
4-
from typing import Literal, overload
5-
63
from hiero_sdk_python.channels import _Channel
7-
from hiero_sdk_python.client.client import Client
84
from hiero_sdk_python.consensus.topic_id import TopicId
95
from hiero_sdk_python.crypto.private_key import PrivateKey
106
from hiero_sdk_python.executable import _Method
11-
from hiero_sdk_python.hapi.services import consensus_submit_message_pb2, timestamp_pb2, transaction_pb2
7+
from hiero_sdk_python.hapi.services import consensus_submit_message_pb2, transaction_pb2
128
from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import (
139
SchedulableTransactionBody,
1410
)
11+
from hiero_sdk_python.transaction.chunked_transaction import ChunkedTransaction
1512
from 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
2013

2114

22-
class TopicMessageSubmitTransaction(Transaction):
15+
class TopicMessageSubmitTransaction(ChunkedTransaction):
2316
"""
2417
Represents a transaction that submits a message to a Hedera Consensus Service topic.
2518
2619
Allows setting the target topic ID and message, building the transaction body,
2720
and executing the submission through a network channel.
21+
22+
Supports automatic chunking for large messages.
2823
"""
2924

3025
def __init__(
31-
self,
32-
topic_id: TopicId | None = None,
33-
message: str | None = None,
34-
chunk_size: int | None = None,
35-
max_chunks: int | None = None,
26+
self,
27+
topic_id: TopicId | None = None,
28+
message: bytes | str | None = None,
29+
chunk_size: int | None = None,
30+
max_chunks: int | None = None,
3631
) -> None:
3732
"""
3833
Initializes a new TopicMessageSubmitTransaction instance.
3934
4035
Args:
4136
topic_id (TopicId, optional): The ID of the topic.
4237
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.
38+
chunk_size (int, optional): The maximum chunk size in bytes. Default: 1024.
39+
max_chunks (int, optional): The maximum number of chunks allowed. Default: 20.
4540
"""
4641
super().__init__()
4742
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
43+
self.message: bytes | str | None = message
44+
self.chunk_size: int = 1024
45+
self.max_chunks: int = 20
46+
if chunk_size is not None:
47+
super().set_chunk_size(chunk_size)
48+
49+
if max_chunks is not None:
50+
super().set_max_chunks(max_chunks)
5151

52-
self._current_chunk_index = 0
5352
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] = []
53+
54+
def _message_as_bytes(self) -> bytes:
55+
"""
56+
Returns the message encoded as bytes for chunking and protobuf serialization.
57+
58+
Returns:
59+
bytes: The message as bytes.
60+
"""
61+
if self.message is None:
62+
return b""
63+
64+
return self.message.encode("utf-8") if isinstance(self.message, str) else self.message
65+
5766

5867
def get_required_chunks(self) -> int:
5968
"""
@@ -65,7 +74,8 @@ def get_required_chunks(self) -> int:
6574
if not self.message:
6675
return 1
6776

68-
content = self.message.encode("utf-8")
77+
content = self._message_as_bytes()
78+
import math
6979
return math.ceil(len(content) / self.chunk_size)
7080

7181
def set_topic_id(self, topic_id: TopicId) -> TopicMessageSubmitTransaction:
@@ -82,7 +92,7 @@ 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
@@ -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.
@@ -190,14 +177,15 @@ def _build_proto_body(self) -> consensus_submit_message_pb2.ConsensusSubmitMessa
190177
if self.message is None:
191178
raise ValueError("Missing required fields: message.")
192179

193-
content = self.message.encode("utf-8")
180+
content = self._message_as_bytes()
194181

195182
start_index = self._current_chunk_index * self.chunk_size
196183
end_index = min(start_index + self.chunk_size, len(content))
197184
chunk_content = content[start_index:end_index]
198185

199186
body = consensus_submit_message_pb2.ConsensusSubmitMessageTransactionBody(
200-
topicID=self.topic_id._to_proto(), message=chunk_content
187+
topicID=self.topic_id._to_proto(),
188+
message=chunk_content
201189
)
202190

203191
# Multi-chunk metadata
@@ -217,8 +205,7 @@ def build_transaction_body(self) -> transaction_pb2.TransactionBody:
217205
Builds and returns the protobuf transaction body for message submission.
218206
219207
Returns:
220-
TransactionBody: The protobuf transaction body containing
221-
the message submission details.
208+
TransactionBody: The protobuf transaction body containing the message submission details.
222209
"""
223210
consensus_submit_message_body = self._build_proto_body()
224211
transaction_body = self.build_base_transaction_body()
@@ -249,180 +236,15 @@ def _get_method(self, channel: _Channel) -> _Method:
249236
"""
250237
return _Method(transaction_func=channel.topic.submitMessage, query_func=None)
251238

252-
def freeze_with(self, client: Client) -> TopicMessageSubmitTransaction:
253-
if self._transaction_body_bytes:
254-
return self
255-
256-
self._resolve_transaction_id(client)
257-
258-
if not self._transaction_ids:
259-
base_timestamp = self.transaction_id.valid_start
260-
261-
for i in range(self.get_required_chunks()):
262-
if i == 0:
263-
if self._initial_transaction_id is None:
264-
self._initial_transaction_id = self.transaction_id
265-
266-
chunk_transaction_id = self.transaction_id
267-
else:
268-
next_nanos = base_timestamp.nanos + i
269-
270-
chunk_valid_start = timestamp_pb2.Timestamp(
271-
seconds=base_timestamp.seconds + next_nanos // 1_000_000_000, nanos=next_nanos % 1_000_000_000
272-
)
273-
chunk_transaction_id = TransactionId(
274-
account_id=self.transaction_id.account_id, valid_start=chunk_valid_start
275-
)
276-
277-
self._transaction_ids.append(chunk_transaction_id)
278-
279-
return super().freeze_with(client)
280-
281-
@overload
282-
def execute(
283-
self,
284-
client: Client,
285-
timeout: int | float | None = None,
286-
wait_for_receipt: Literal[True] = True,
287-
validate_status: bool = False,
288-
) -> TransactionReceipt: ...
289-
290-
@overload
291-
def execute(
292-
self,
293-
client: Client,
294-
timeout: int | float | None = None,
295-
wait_for_receipt: Literal[False] = False,
296-
validate_status: bool = False,
297-
) -> TransactionResponse: ...
298-
299-
def execute(
300-
self,
301-
client: Client,
302-
timeout: int | float | None = None,
303-
wait_for_receipt: bool = True,
304-
validate_status: bool = False,
305-
) -> TransactionReceipt | TransactionResponse:
306-
"""
307-
Executes the topic message submit transaction.
308-
309-
For multi-chunk transactions, this method will execute all chunks sequentially and return first response.
310-
311-
Args:
312-
client: The client to execute the transaction with.
313-
timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution.
314-
wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt.
315-
If False, the method returns a TransactionResponse immediately after submission.
316-
validate_status: (bool): Whether the query should automatically validate the transaction status (default False).
317-
318-
Returns:
319-
TransactionReceipt: If wait_for_receipt is True (default)
320-
TransactionResponse: If wait_for_receipt is False
321-
"""
322-
# Return the first response as the JS SDK does
323-
return self.execute_all(client, timeout, wait_for_receipt, validate_status)[0]
324-
325-
@overload
326-
def execute_all(
327-
self,
328-
client: Client,
329-
timeout: int | float | None = None,
330-
wait_for_receipt: Literal[True] = True,
331-
validate_status: bool = False,
332-
) -> list[TransactionReceipt]: ...
333-
334-
@overload
335-
def execute_all(
336-
self,
337-
client: Client,
338-
timeout: int | float | None = None,
339-
wait_for_receipt: Literal[False] = False,
340-
validate_status: bool = False,
341-
) -> list[TransactionResponse]: ...
342-
343-
def execute_all(
344-
self,
345-
client: Client,
346-
timeout: int | float | None = None,
347-
wait_for_receipt: bool = True,
348-
validate_status: bool = False,
349-
) -> list[TransactionReceipt] | list[TransactionResponse]:
350-
"""
351-
Executes the topic message submit transaction.
352-
353-
This method will execute all chunks sequentially and return list of all responses.
354-
355-
Args:
356-
client: The client to execute the transaction with.
357-
timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution.
358-
wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt.
359-
If False, the method returns a TransactionResponse immediately after submission.
360-
validate_status: (bool): Whether the query should automatically validate the transaction status (default False).
361-
362-
Returns:
363-
List[TransactionReceipt]: If wait_for_receipt is True (default)
364-
List[TransactionResponse]: If wait_for_receipt is False
365-
"""
366-
self._validate_chunking()
367-
368-
if self.get_required_chunks() == 1:
369-
return [super().execute(client, timeout, wait_for_receipt, validate_status)]
370-
371-
# Multi-chunk transaction - execute all chunks
372-
responses = []
373-
374-
for chunk_index in range(self.get_required_chunks()):
375-
self._current_chunk_index = chunk_index
376-
377-
if self._transaction_ids and chunk_index < len(self._transaction_ids):
378-
self.transaction_id = self._transaction_ids[chunk_index]
379-
380-
self._transaction_body_bytes.clear()
381-
self._signature_map.clear()
382-
383-
self.freeze_with(client)
384-
385-
for signing_key in self._signing_keys:
386-
super().sign(signing_key)
387-
388-
# Execute the chunk
389-
response = super().execute(client, timeout, wait_for_receipt, validate_status)
390-
responses.append(response)
391-
392-
return responses
393-
394-
def sign(self, private_key: PrivateKey):
239+
def sign(self, private_key: PrivateKey) -> TopicMessageSubmitTransaction:
395240
"""
396241
Signs the transaction using the provided private key.
397242
398-
For multi-chunk transactions, this stores the signing key for later use.
399-
400243
Args:
401244
private_key (PrivateKey): The private key to sign the transaction with.
402-
"""
403-
if private_key not in self._signing_keys:
404-
self._signing_keys.append(private_key)
405245
246+
Returns:
247+
TopicMessageSubmitTransaction: This transaction instance (for chaining).
248+
"""
406249
super().sign(private_key)
407250
return self
408-
409-
@property
410-
def body_size_all_chunks(self) -> list[int]:
411-
"""Returns an array of body sizes for transactions with multiple chunks."""
412-
self._require_frozen()
413-
sizes = []
414-
415-
original_index = self._current_chunk_index
416-
original_transaction_id = self.transaction_id
417-
418-
try:
419-
for i, transaction_id in enumerate(self._transaction_ids):
420-
self._current_chunk_index = i
421-
self.transaction_id = transaction_id
422-
423-
sizes.append(self.body_size)
424-
finally:
425-
self._current_chunk_index = original_index
426-
self.transaction_id = original_transaction_id
427-
428-
return sizes

0 commit comments

Comments
 (0)