Skip to content

Commit 481796f

Browse files
committed
feat: added support to serialize chunk transactions
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
1 parent e1543ce commit 481796f

3 files changed

Lines changed: 221 additions & 200 deletions

File tree

src/hiero_sdk_python/consensus/topic_message_submit_transaction.py

Lines changed: 57 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
from hiero_sdk_python.channels import _Channel
77
from hiero_sdk_python.client.client import Client
88
from hiero_sdk_python.consensus.topic_id import TopicId
9-
from hiero_sdk_python.crypto.private_key import PrivateKey
109
from hiero_sdk_python.executable import _Method
11-
from hiero_sdk_python.hapi.services import consensus_submit_message_pb2, timestamp_pb2, transaction_pb2
10+
from hiero_sdk_python.hapi.services import consensus_submit_message_pb2, transaction_pb2
1211
from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import (
1312
SchedulableTransactionBody,
1413
)
@@ -49,11 +48,9 @@ def __init__(
4948
self.chunk_size: int = chunk_size or 1024
5049
self.max_chunks: int = max_chunks or 20
5150

52-
self._current_chunk_index = 0
5351
self._total_chunks = self.get_required_chunks()
5452
self._initial_transaction_id: TransactionId | None = None
55-
self._transaction_ids: list[TransactionId] = []
56-
self._signing_keys: list[PrivateKey] = []
53+
self._chunk_info: consensus_submit_message_pb2.ConsensusMessageChunkInfo | None = None
5754

5855
def get_required_chunks(self) -> int:
5956
"""
@@ -190,25 +187,22 @@ def _build_proto_body(self) -> consensus_submit_message_pb2.ConsensusSubmitMessa
190187

191188
content = self.message.encode("utf-8")
192189

193-
start_index = self._current_chunk_index * self.chunk_size
194-
end_index = min(start_index + self.chunk_size, len(content))
195-
chunk_content = content[start_index:end_index]
190+
if self._chunk_info is not None:
191+
start_index = (self._chunk_info.number - 1) * self.chunk_size
192+
end_index = min(start_index + self.chunk_size, len(content))
196193

197-
body = consensus_submit_message_pb2.ConsensusSubmitMessageTransactionBody(
198-
topicID=self.topic_id._to_proto() if self.topic_id else None, message=chunk_content
199-
)
194+
chunk_content = content[start_index:end_index]
200195

201-
# Multi-chunk metadata
202-
if self._total_chunks > 1:
203-
body.chunkInfo.CopyFrom(
204-
consensus_submit_message_pb2.ConsensusMessageChunkInfo(
205-
initialTransactionID=self._initial_transaction_id._to_proto(),
206-
total=self._total_chunks,
207-
number=self._current_chunk_index + 1,
208-
)
196+
return consensus_submit_message_pb2.ConsensusSubmitMessageTransactionBody(
197+
topicID=self.topic_id._to_proto() if self.topic_id else None,
198+
message=chunk_content,
199+
chunkInfo=self._chunk_info,
209200
)
210201

211-
return body
202+
return consensus_submit_message_pb2.ConsensusSubmitMessageTransactionBody(
203+
topicID=self.topic_id._to_proto() if self.topic_id else None,
204+
message=content,
205+
)
212206

213207
def build_transaction_body(self) -> transaction_pb2.TransactionBody:
214208
"""
@@ -252,27 +246,33 @@ def freeze_with(self, client: Client) -> TopicMessageSubmitTransaction:
252246
return self
253247

254248
self._resolve_transaction_id(client)
249+
self._resolve_node_ids(client)
255250

256-
if not self._transaction_ids:
257-
base_timestamp = self.transaction_id.valid_start
251+
required_chunks = self.get_required_chunks()
252+
self._generate_transaction_ids(self._transaction_ids[0], required_chunks)
258253

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
254+
self._initial_transaction_id = self._transaction_ids[0]
255+
256+
for chunk in range(self.get_required_chunks()):
257+
self._current_transaction_id_index = chunk
258+
self._chunk_info = consensus_submit_message_pb2.ConsensusMessageChunkInfo(
259+
initialTransactionID=self._initial_transaction_id._to_proto(),
260+
total=required_chunks,
261+
number=chunk + 1,
262+
)
263263

264-
chunk_transaction_id = self.transaction_id
265-
else:
266-
next_nanos = base_timestamp.nanos + i
264+
node_bytes = {}
267265

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-
)
266+
for node_account_id in self.node_account_ids:
267+
self._node_account_id = node_account_id
274268

275-
self._transaction_ids.append(chunk_transaction_id)
269+
transaction_body = self.build_transaction_body()
270+
transaction_body.transactionID.CopyFrom(self._transaction_ids[chunk]._to_proto())
271+
transaction_body.nodeAccountID.CopyFrom(node_account_id._to_proto())
272+
273+
node_bytes[node_account_id] = transaction_body.SerializeToString()
274+
275+
self._transaction_body_bytes[self._transaction_ids[chunk]] = node_bytes
276276

277277
return super().freeze_with(client)
278278

@@ -363,47 +363,22 @@ def execute_all(
363363
"""
364364
self._validate_chunking()
365365

366-
if self.get_required_chunks() == 1:
366+
if not self._transaction_body_bytes:
367+
self.freeze_with(client)
368+
369+
if len(self._transaction_ids) == 1:
367370
return [super().execute(client, timeout, wait_for_receipt, validate_status)]
368371

369372
# Multi-chunk transaction - execute all chunks
370373
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
374+
self._current_transaction_id_index = 0
375+
for index in range(len(self._transaction_ids)):
376+
self._current_transaction_id_index = index
387377
response = super().execute(client, timeout, wait_for_receipt, validate_status)
388378
responses.append(response)
389379

390380
return responses
391381

392-
def sign(self, private_key: PrivateKey):
393-
"""
394-
Signs the transaction using the provided private key.
395-
396-
For multi-chunk transactions, this stores the signing key for later use.
397-
398-
Args:
399-
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)
403-
404-
super().sign(private_key)
405-
return self
406-
407382
@property
408383
def body_size_all_chunks(self) -> list[int]:
409384
"""Returns an array of body sizes for transactions with multiple chunks."""
@@ -424,3 +399,17 @@ def body_size_all_chunks(self) -> list[int]:
424399
self.transaction_id = original_transaction_id
425400

426401
return sizes
402+
403+
@classmethod
404+
def _from_protobuf(cls, transaction_body):
405+
transaction = super()._from_protobuf(transaction_body)
406+
407+
if transaction_body.HasField("consensusSubmitMessage"):
408+
body = transaction_body.consensusSubmitMessage
409+
410+
if body.HasField("topicID"):
411+
transaction.topic_id = TopicId._from_proto(body.topicID)
412+
transaction.message = body.message.decode("utf-8") if body.message else None
413+
transaction._total_chunks = transaction.get_required_chunks()
414+
415+
return transaction

src/hiero_sdk_python/file/file_append_transaction.py

Lines changed: 43 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,18 @@
1717
from typing import TYPE_CHECKING, Any, Literal, overload
1818

1919
from hiero_sdk_python.file.file_id import FileId
20-
from hiero_sdk_python.hapi.services import file_append_pb2, timestamp_pb2
20+
from hiero_sdk_python.hapi.services import file_append_pb2
2121
from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import (
2222
SchedulableTransactionBody,
2323
)
2424
from hiero_sdk_python.hbar import Hbar
2525
from hiero_sdk_python.transaction.transaction import Transaction
26-
from hiero_sdk_python.transaction.transaction_id import TransactionId
2726

2827

2928
# Use TYPE_CHECKING to avoid circular import errors
3029
if TYPE_CHECKING:
3130
from hiero_sdk_python.channels import _Channel
3231
from hiero_sdk_python.client.client import Client
33-
from hiero_sdk_python.crypto.private_key import PrivateKey
3432
from hiero_sdk_python.executable import _Method
3533
from hiero_sdk_python.transaction.transaction import TransactionReceipt
3634
from hiero_sdk_python.transaction.transaction_response import TransactionResponse
@@ -76,10 +74,8 @@ def __init__(
7674
self._default_transaction_fee = Hbar(5).to_tinybars()
7775

7876
# Internal tracking for chunking
79-
self._current_chunk_index: int = 0
77+
self._current_chunk_index: int | None = None
8078
self._total_chunks: int = self._calculate_total_chunks()
81-
self._transaction_ids: list[TransactionId] = []
82-
self._signing_keys: list[PrivateKey] = [] # Use string annotation to avoid import issues
8379

8480
def _encode_contents(self, contents: str | bytes | None) -> bytes | None:
8581
"""
@@ -190,15 +186,18 @@ def _build_proto_body(self) -> file_append_pb2.FileAppendTransactionBody:
190186
if self.file_id is None:
191187
raise ValueError("Missing required FileID")
192188

193-
if self.contents is None:
194-
chunk_contents = b""
195-
else:
189+
contents = self.contents if self.contents is not None else b""
190+
191+
if self._current_chunk_index is not None:
196192
start_index = self._current_chunk_index * self.chunk_size
197-
end_index = min(start_index + self.chunk_size, len(self.contents))
198-
chunk_contents = self.contents[start_index:end_index]
193+
end_index = min(start_index + self.chunk_size, len(contents))
199194

195+
chunk_contents = contents[start_index:end_index]
196+
return file_append_pb2.FileAppendTransactionBody(
197+
fileID=self.file_id._to_proto() if self.file_id else None, contents=chunk_contents
198+
)
200199
return file_append_pb2.FileAppendTransactionBody(
201-
fileID=self.file_id._to_proto() if self.file_id else None, contents=chunk_contents
200+
fileID=self.file_id._to_proto() if self.file_id else None, contents=contents
202201
)
203202

204203
def build_transaction_body(self) -> Any:
@@ -287,28 +286,27 @@ def freeze_with(self, client: Client) -> FileAppendTransaction:
287286
return self
288287

289288
self._resolve_transaction_id(client)
289+
self._resolve_node_ids(client)
290+
291+
required_chunks = self.get_required_chunks()
292+
self._generate_transaction_ids(self._transaction_ids[0], required_chunks)
290293

291-
# Generate transaction IDs for all chunks
292-
if not self._transaction_ids:
293-
base_timestamp = self.transaction_id.valid_start
294+
for chunk in range(required_chunks):
295+
self._current_chunk_index = chunk
296+
self._current_transaction_id_index = chunk
294297

295-
for i in range(self.get_required_chunks()):
296-
if i == 0:
297-
# First chunk uses the original transaction ID
298-
chunk_transaction_id = self.transaction_id
299-
else:
300-
# Subsequent chunks get incremented timestamps
301-
# Add i nanoseconds to space out chunks
302-
next_nanos = base_timestamp.nanos + i
298+
node_bytes = {}
303299

304-
chunk_valid_start = timestamp_pb2.Timestamp(
305-
seconds=base_timestamp.seconds + next_nanos // 1_000_000_000, nanos=next_nanos % 1_000_000_000
306-
)
307-
chunk_transaction_id = TransactionId(
308-
account_id=self.transaction_id.account_id, valid_start=chunk_valid_start
309-
)
300+
for node_account_id in self.node_account_ids:
301+
self._node_account_id = node_account_id
310302

311-
self._transaction_ids.append(chunk_transaction_id)
303+
transaction_body = self.build_transaction_body()
304+
transaction_body.transactionID.CopyFrom(self._transaction_ids[chunk]._to_proto())
305+
transaction_body.nodeAccountID.CopyFrom(node_account_id._to_proto())
306+
307+
node_bytes[node_account_id] = transaction_body.SerializeToString()
308+
309+
self._transaction_body_bytes[self._transaction_ids[chunk]] = node_bytes
312310

313311
return super().freeze_with(client)
314312

@@ -399,57 +397,23 @@ def execute_all(
399397
"""
400398
self._validate_chunking()
401399

402-
if self.get_required_chunks() == 1:
403-
# Single chunk transaction
400+
if not self._transaction_body_bytes:
401+
self.freeze_with(client)
402+
403+
# Single chunk transaction
404+
if len(self._transaction_ids) == 1:
404405
return [super().execute(client, timeout, wait_for_receipt, validate_status)]
405406

406407
# Multi-chunk transaction - execute all chunks
407408
responses = []
408-
409-
for chunk_index in range(self.get_required_chunks()):
410-
self._current_chunk_index = chunk_index
411-
412-
# Set the transaction ID for this chunk
413-
if self._transaction_ids and chunk_index < len(self._transaction_ids):
414-
self.transaction_id = self._transaction_ids[chunk_index]
415-
# Clear the frozen state to allow rebuilding with new transaction ID
416-
self._transaction_body_bytes.clear()
417-
self._signature_map.clear()
418-
419-
# Freeze the transaction for this chunk if not already frozen
420-
self.freeze_with(client)
421-
422-
# Sign with all stored signing keys for this chunk
423-
for signing_key in self._signing_keys:
424-
# Call parent sign directly to avoid modifying _signing_keys
425-
super().sign(signing_key)
426-
427-
# Execute the chunk
409+
self._current_transaction_id_index = 0
410+
for index in range(len(self._transaction_ids)):
411+
self._current_transaction_id_index = index
428412
response = super().execute(client, timeout, wait_for_receipt, validate_status)
429413
responses.append(response)
430414

431415
return responses
432416

433-
def sign(self, private_key: PrivateKey) -> FileAppendTransaction:
434-
"""
435-
Signs the transaction using the provided private key.
436-
437-
For multi-chunk transactions, this stores the signing key for later use.
438-
439-
Args:
440-
private_key (PrivateKey): The private key to sign the transaction with.
441-
442-
Returns:
443-
FileAppendTransaction: The current transaction instance for method chaining.
444-
"""
445-
# Store the signing key for multi-chunk transactions (avoid duplicates)
446-
if private_key not in self._signing_keys:
447-
self._signing_keys.append(private_key)
448-
449-
# Call the parent sign method for the current transaction
450-
super().sign(private_key)
451-
return self
452-
453417
@property
454418
def body_size_all_chunks(self) -> list[int]:
455419
"""Returns an array of body sizes for transactions with multiple chunks."""
@@ -470,3 +434,10 @@ def body_size_all_chunks(self) -> list[int]:
470434
self.transaction_id = original_transaction_id
471435

472436
return sizes
437+
438+
@classmethod
439+
def _from_protobuf(cls, transaction_body):
440+
transaction = super()._from_protobuf(transaction_body)
441+
if transaction_body.HasField("fileAppend"):
442+
transaction._from_proto(transaction_body.fileAppend)
443+
return transaction

0 commit comments

Comments
 (0)