Skip to content

Commit df3bd1e

Browse files
authored
Create ChunkedTransaction class (#2389)
Signed-off-by: MonaaEid <monaa_eid@hotmail.com> Signed-off-by: MontyPokemon <59332150+MonaaEid@users.noreply.github.com>
1 parent 8f240d9 commit df3bd1e

7 files changed

Lines changed: 586 additions & 483 deletions
Lines changed: 42 additions & 221 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,33 @@
11
from __future__ import annotations
22

33
import math
4-
from typing import Literal, overload
54

65
from hiero_sdk_python.channels import _Channel
7-
from hiero_sdk_python.client.client import Client
86
from hiero_sdk_python.consensus.topic_id import TopicId
97
from hiero_sdk_python.crypto.private_key import PrivateKey
108
from 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
1210
from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import (
1311
SchedulableTransactionBody,
1412
)
13+
from hiero_sdk_python.transaction.chunked_transaction import ChunkedTransaction
1514
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
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

Comments
 (0)