Skip to content

Commit d8ff38b

Browse files
authored
feat: Added Transaction size calculation methods (hiero-ledger#1795)
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
1 parent ec557e8 commit d8ff38b

10 files changed

Lines changed: 519 additions & 64 deletions

src/hiero_sdk_python/consensus/topic_message_submit_transaction.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,7 @@ def freeze_with(self, client: Client) -> TopicMessageSubmitTransaction:
253253
if self._transaction_body_bytes:
254254
return self
255255

256-
if self.transaction_id is None:
257-
self.transaction_id = client.generate_transaction_id()
256+
self._resolve_transaction_id(client)
258257

259258
if not self._transaction_ids:
260259
base_timestamp = self.transaction_id.valid_start
@@ -266,8 +265,10 @@ def freeze_with(self, client: Client) -> TopicMessageSubmitTransaction:
266265

267266
chunk_transaction_id = self.transaction_id
268267
else:
268+
next_nanos = base_timestamp.nanos + i
269+
269270
chunk_valid_start = timestamp_pb2.Timestamp(
270-
seconds=base_timestamp.seconds, nanos=base_timestamp.nanos + i
271+
seconds=base_timestamp.seconds + next_nanos // 1_000_000_000, nanos=next_nanos % 1_000_000_000
271272
)
272273
chunk_transaction_id = TransactionId(
273274
account_id=self.transaction_id.account_id, valid_start=chunk_valid_start
@@ -404,3 +405,24 @@ def sign(self, private_key: PrivateKey):
404405

405406
super().sign(private_key)
406407
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

src/hiero_sdk_python/file/file_append_transaction.py

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
# Use TYPE_CHECKING to avoid circular import errors
3030
if TYPE_CHECKING:
3131
from hiero_sdk_python.channels import _Channel
32-
from hiero_sdk_python.client import Client
32+
from hiero_sdk_python.client.client import Client
3333
from hiero_sdk_python.crypto.private_key import PrivateKey
3434
from hiero_sdk_python.executable import _Method
3535
from hiero_sdk_python.transaction.transaction import TransactionReceipt
@@ -286,40 +286,31 @@ def freeze_with(self, client: Client) -> FileAppendTransaction:
286286
if self._transaction_body_bytes:
287287
return self
288288

289-
if self.transaction_id is None:
290-
self.transaction_id = client.generate_transaction_id()
289+
self._resolve_transaction_id(client)
291290

292291
# Generate transaction IDs for all chunks
293-
self._transaction_ids = []
294-
base_timestamp = self.transaction_id.valid_start
295-
296-
for i in range(self.get_required_chunks()):
297-
if i == 0:
298-
# First chunk uses the original transaction ID
299-
chunk_transaction_id = self.transaction_id
300-
else:
301-
# Subsequent chunks get incremented timestamps
302-
# Add i nanoseconds to space out chunks
303-
chunk_valid_start = timestamp_pb2.Timestamp(
304-
seconds=base_timestamp.seconds, nanos=base_timestamp.nanos + i
305-
)
306-
chunk_transaction_id = TransactionId(
307-
account_id=self.transaction_id.account_id, valid_start=chunk_valid_start
308-
)
309-
self._transaction_ids.append(chunk_transaction_id)
310-
311-
# We iterate through every node in the client's network
312-
# For each node, set the node_account_id and build the transaction body
313-
# This allows the transaction to be submitted to any node in the network
314-
for node in client.network.nodes:
315-
self.node_account_id = node._account_id
316-
transaction_body = self.build_transaction_body()
317-
self._transaction_body_bytes[node._account_id] = transaction_body.SerializeToString()
318-
319-
# Set the node account id to the current node in the network
320-
self.node_account_id = client.network.current_node._account_id
292+
if not self._transaction_ids:
293+
base_timestamp = self.transaction_id.valid_start
321294

322-
return self
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
303+
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+
)
310+
311+
self._transaction_ids.append(chunk_transaction_id)
312+
313+
return super().freeze_with(client)
323314

324315
@overload
325316
def execute(
@@ -458,3 +449,24 @@ def sign(self, private_key: PrivateKey) -> FileAppendTransaction:
458449
# Call the parent sign method for the current transaction
459450
super().sign(private_key)
460451
return self
452+
453+
@property
454+
def body_size_all_chunks(self) -> list[int]:
455+
"""Returns an array of body sizes for transactions with multiple chunks."""
456+
self._require_frozen()
457+
sizes = []
458+
459+
original_index = self._current_chunk_index
460+
original_transaction_id = self.transaction_id
461+
462+
try:
463+
for i, transaction_id in enumerate(self._transaction_ids):
464+
self._current_chunk_index = i
465+
self.transaction_id = transaction_id
466+
467+
sizes.append(self.body_size)
468+
finally:
469+
self._current_chunk_index = original_index
470+
self.transaction_id = original_transaction_id
471+
472+
return sizes

src/hiero_sdk_python/node.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def _get_channel(self):
126126
if self._root_certificates:
127127
# Use the certificate that is provided
128128
self._node_pem_cert = self._root_certificates
129+
129130
else:
130131
# Fetch pem_cert for the node
131132
self._node_pem_cert = self._fetch_server_certificate_pem()

src/hiero_sdk_python/transaction/transaction.py

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,27 @@ def _to_proto(self):
228228

229229
return transaction_pb2.Transaction(signedTransactionBytes=signed_transaction.SerializeToString())
230230

231+
def _resolve_transaction_id(self, client: Client):
232+
if self.transaction_id is not None:
233+
return
234+
235+
if client is not None:
236+
operator_account_id = client.operator_account_id
237+
if operator_account_id is not None:
238+
self.transaction_id = client.generate_transaction_id()
239+
else:
240+
raise ValueError("Client must have an operator_account or transactionId must be set.")
241+
else:
242+
raise ValueError(
243+
"Transaction ID must be set before freezing. Use freeze_with(client) or set_transaction_id()."
244+
)
245+
246+
def _resolve_node_ids(self, client: Client):
247+
if self.node_account_id is None and len(self.node_account_ids) == 0 and client is None:
248+
raise ValueError(
249+
"Node account ID must be set before freezing. Use freeze_with(client) or manually set node_account_ids."
250+
)
251+
231252
def freeze(self):
232253
"""
233254
Freezes the transaction by building the transaction body and setting necessary IDs.
@@ -246,33 +267,9 @@ def freeze(self):
246267
Raises:
247268
ValueError: If transaction_id or node_account_id are not set.
248269
"""
249-
if self._transaction_body_bytes:
250-
return self
270+
return self.freeze_with(None)
251271

252-
if self.transaction_id is None:
253-
raise ValueError(
254-
"Transaction ID must be set before freezing. Use freeze_with(client) or set_transaction_id()."
255-
)
256-
257-
if self.node_account_id is None and len(self.node_account_ids) == 0:
258-
raise ValueError(
259-
"Node account ID must be set before freezing. Use freeze_with(client) or manually set node_account_ids."
260-
)
261-
262-
# Populate node_account_ids for backward compatibility
263-
if self.node_account_id:
264-
self.set_node_account_id(self.node_account_id)
265-
self._transaction_body_bytes[self.node_account_id] = self.build_transaction_body().SerializeToString()
266-
return self
267-
268-
# Build the transaction body for the single node
269-
for node_account_id in self.node_account_ids:
270-
self.node_account_id = node_account_id
271-
self._transaction_body_bytes[node_account_id] = self.build_transaction_body().SerializeToString()
272-
273-
return self
274-
275-
def freeze_with(self, client):
272+
def freeze_with(self, client: Client):
276273
"""
277274
Freezes the transaction by building the transaction body and setting necessary IDs.
278275
@@ -288,8 +285,10 @@ def freeze_with(self, client):
288285
if self._transaction_body_bytes:
289286
return self
290287

291-
if self.transaction_id is None:
292-
self.transaction_id = client.generate_transaction_id()
288+
# Check transaction_id and node id to be set when using freeze()
289+
self._resolve_transaction_id(client)
290+
291+
self._resolve_node_ids(client)
293292

294293
# We iterate through every node in the client's network
295294
# For each node, set the node_account_id and build the transaction body
@@ -412,7 +411,7 @@ def is_signed_by(self, public_key):
412411

413412
return any(sig_pair.pubKeyPrefix == public_key_bytes for sig_pair in sig_map.sigPair)
414413

415-
def build_transaction_body(self):
414+
def build_transaction_body(self) -> transaction_pb2.TransactionBody:
416415
"""
417416
Abstract method to build the transaction body.
418417
@@ -921,3 +920,15 @@ def get_required_chunks(self):
921920
int: Number of chunks required.
922921
"""
923922
return 1
923+
924+
@property
925+
def size(self) -> int:
926+
"""Returns the total transaction size in bytes after protobuf encoding"""
927+
self._require_frozen()
928+
return self._make_request().ByteSize()
929+
930+
@property
931+
def body_size(self) -> int:
932+
"""Returns just the transaction body size in bytes after encoding"""
933+
self._require_frozen()
934+
return self.build_transaction_body().ByteSize()

tests/integration/file_append_transaction_e2e_test.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import pytest
44
from pytest import mark
55

6+
from hiero_sdk_python.account.account_id import AccountId
67
from hiero_sdk_python.file.file_append_transaction import FileAppendTransaction
78
from hiero_sdk_python.file.file_contents_query import FileContentsQuery
89
from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction
910
from hiero_sdk_python.response_code import ResponseCode
11+
from hiero_sdk_python.transaction.transaction_id import TransactionId
1012

1113

1214
# Generate big contents for chunking tests - similar to JavaScript bigContents
@@ -283,3 +285,42 @@ def test_integration_file_append_transaction_method_chaining(env):
283285

284286
append_receipt = append_tx.execute(env.client)
285287
assert append_receipt.status == ResponseCode.SUCCESS
288+
assert append_receipt.status == ResponseCode.SUCCESS
289+
290+
291+
@pytest.mark.integration
292+
def test_file_append_chunk_transaction_can_execute_with_manual_freeze(env):
293+
"""Test file append transaction can execute with manual freeze."""
294+
create_receipt = (
295+
FileCreateTransaction()
296+
.set_keys(env.client.operator_private_key.public_key())
297+
.set_contents(b"")
298+
.execute(env.client)
299+
)
300+
301+
assert create_receipt.status == ResponseCode.SUCCESS
302+
file_id = create_receipt.file_id
303+
304+
file_contents = FileContentsQuery().set_file_id(file_id).execute(env.client)
305+
assert file_contents == b""
306+
307+
content = "A" * (4000) # content with (4000/1024) bytes ie approx 4 chunks
308+
309+
tx = (
310+
FileAppendTransaction()
311+
.set_file_id(file_id)
312+
.set_chunk_size(1024)
313+
.set_contents(content)
314+
.set_transaction_id(TransactionId.generate(env.client.operator_account_id))
315+
.set_node_account_id(AccountId(0, 0, 3))
316+
.freeze()
317+
)
318+
319+
tx.sign(env.client.operator_private_key)
320+
321+
receipt = tx.execute(env.client)
322+
323+
assert receipt.status == ResponseCode.SUCCESS
324+
325+
file_contents = FileContentsQuery().set_file_id(file_id).execute(env.client)
326+
assert file_contents == bytes(content, "utf-8")

tests/integration/topic_message_submit_transaction_e2e_test.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import pytest
88

9+
from hiero_sdk_python.account.account_id import AccountId
910
from hiero_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction
1011
from hiero_sdk_python.consensus.topic_delete_transaction import TopicDeleteTransaction
1112
from hiero_sdk_python.consensus.topic_message_submit_transaction import (
@@ -18,6 +19,7 @@
1819
from hiero_sdk_python.response_code import ResponseCode
1920
from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee
2021
from hiero_sdk_python.transaction.custom_fee_limit import CustomFeeLimit
22+
from hiero_sdk_python.transaction.transaction_id import TransactionId
2123

2224

2325
def create_topic(client, admin_key=None, submit_key=None, custom_fees=None):
@@ -296,3 +298,33 @@ def test_integration_topic_message_submit_transaction_fails_if_required_chunk_gr
296298
message_transaction.execute(env.client)
297299

298300
delete_topic(env.client, topic_id)
301+
302+
303+
@pytest.mark.integration
304+
def test_topic_message_submit_transaction_can_submit_a_large_message_manual_freeze(env):
305+
"""Test topic message submit transaction can submit large message with manual freeze."""
306+
topic_id = create_topic(client=env.client, admin_key=env.operator_key)
307+
308+
info = TopicInfoQuery().set_topic_id(topic_id).execute(env.client)
309+
assert info.sequence_number == 0
310+
311+
message = "A" * (1024 * 14) # message with (1024 * 14) bytes ie 14 chunks
312+
313+
message_tx = (
314+
TopicMessageSubmitTransaction()
315+
.set_topic_id(topic_id)
316+
.set_message(message)
317+
.set_transaction_id(TransactionId.generate(env.client.operator_account_id))
318+
.set_node_account_id(AccountId(0, 0, 3))
319+
.freeze()
320+
)
321+
322+
message_tx.sign(env.client.operator_private_key)
323+
message_receipt = message_tx.execute(env.client)
324+
325+
assert message_receipt.status == ResponseCode.SUCCESS
326+
327+
info = TopicInfoQuery().set_topic_id(topic_id).execute(env.client)
328+
assert info.sequence_number == 14
329+
330+
delete_topic(env.client, topic_id)

0 commit comments

Comments
 (0)