diff --git a/src/hiero_sdk_python/consensus/topic_message_submit_transaction.py b/src/hiero_sdk_python/consensus/topic_message_submit_transaction.py index d79c2415f..0cc9529d5 100644 --- a/src/hiero_sdk_python/consensus/topic_message_submit_transaction.py +++ b/src/hiero_sdk_python/consensus/topic_message_submit_transaction.py @@ -253,8 +253,7 @@ def freeze_with(self, client: Client) -> TopicMessageSubmitTransaction: if self._transaction_body_bytes: return self - if self.transaction_id is None: - self.transaction_id = client.generate_transaction_id() + self._resolve_transaction_id(client) if not self._transaction_ids: base_timestamp = self.transaction_id.valid_start @@ -266,8 +265,10 @@ def freeze_with(self, client: Client) -> TopicMessageSubmitTransaction: chunk_transaction_id = self.transaction_id else: + next_nanos = base_timestamp.nanos + i + chunk_valid_start = timestamp_pb2.Timestamp( - seconds=base_timestamp.seconds, nanos=base_timestamp.nanos + i + seconds=base_timestamp.seconds + next_nanos // 1_000_000_000, nanos=next_nanos % 1_000_000_000 ) chunk_transaction_id = TransactionId( account_id=self.transaction_id.account_id, valid_start=chunk_valid_start @@ -404,3 +405,24 @@ def sign(self, private_key: PrivateKey): super().sign(private_key) return self + + @property + def body_size_all_chunks(self) -> list[int]: + """Returns an array of body sizes for transactions with multiple chunks.""" + self._require_frozen() + sizes = [] + + original_index = self._current_chunk_index + original_transaction_id = self.transaction_id + + try: + for i, transaction_id in enumerate(self._transaction_ids): + self._current_chunk_index = i + self.transaction_id = transaction_id + + sizes.append(self.body_size) + finally: + self._current_chunk_index = original_index + self.transaction_id = original_transaction_id + + return sizes diff --git a/src/hiero_sdk_python/file/file_append_transaction.py b/src/hiero_sdk_python/file/file_append_transaction.py index aebb4ad9b..3019296a5 100644 --- a/src/hiero_sdk_python/file/file_append_transaction.py +++ b/src/hiero_sdk_python/file/file_append_transaction.py @@ -29,7 +29,7 @@ # Use TYPE_CHECKING to avoid circular import errors if TYPE_CHECKING: from hiero_sdk_python.channels import _Channel - from hiero_sdk_python.client import Client + from hiero_sdk_python.client.client import Client from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.executable import _Method from hiero_sdk_python.transaction.transaction import TransactionReceipt @@ -286,40 +286,31 @@ def freeze_with(self, client: Client) -> FileAppendTransaction: if self._transaction_body_bytes: return self - if self.transaction_id is None: - self.transaction_id = client.generate_transaction_id() + self._resolve_transaction_id(client) # Generate transaction IDs for all chunks - self._transaction_ids = [] - base_timestamp = self.transaction_id.valid_start - - for i in range(self.get_required_chunks()): - if i == 0: - # First chunk uses the original transaction ID - chunk_transaction_id = self.transaction_id - else: - # Subsequent chunks get incremented timestamps - # Add i nanoseconds to space out chunks - chunk_valid_start = timestamp_pb2.Timestamp( - seconds=base_timestamp.seconds, nanos=base_timestamp.nanos + i - ) - chunk_transaction_id = TransactionId( - account_id=self.transaction_id.account_id, valid_start=chunk_valid_start - ) - self._transaction_ids.append(chunk_transaction_id) - - # We iterate through every node in the client's network - # For each node, set the node_account_id and build the transaction body - # This allows the transaction to be submitted to any node in the network - for node in client.network.nodes: - self.node_account_id = node._account_id - transaction_body = self.build_transaction_body() - self._transaction_body_bytes[node._account_id] = transaction_body.SerializeToString() - - # Set the node account id to the current node in the network - self.node_account_id = client.network.current_node._account_id + if not self._transaction_ids: + base_timestamp = self.transaction_id.valid_start - return self + for i in range(self.get_required_chunks()): + if i == 0: + # First chunk uses the original transaction ID + chunk_transaction_id = self.transaction_id + else: + # Subsequent chunks get incremented timestamps + # Add i nanoseconds to space out chunks + next_nanos = base_timestamp.nanos + i + + chunk_valid_start = timestamp_pb2.Timestamp( + seconds=base_timestamp.seconds + next_nanos // 1_000_000_000, nanos=next_nanos % 1_000_000_000 + ) + chunk_transaction_id = TransactionId( + account_id=self.transaction_id.account_id, valid_start=chunk_valid_start + ) + + self._transaction_ids.append(chunk_transaction_id) + + return super().freeze_with(client) @overload def execute( @@ -458,3 +449,24 @@ def sign(self, private_key: PrivateKey) -> FileAppendTransaction: # Call the parent sign method for the current transaction super().sign(private_key) return self + + @property + def body_size_all_chunks(self) -> list[int]: + """Returns an array of body sizes for transactions with multiple chunks.""" + self._require_frozen() + sizes = [] + + original_index = self._current_chunk_index + original_transaction_id = self.transaction_id + + try: + for i, transaction_id in enumerate(self._transaction_ids): + self._current_chunk_index = i + self.transaction_id = transaction_id + + sizes.append(self.body_size) + finally: + self._current_chunk_index = original_index + self.transaction_id = original_transaction_id + + return sizes diff --git a/src/hiero_sdk_python/node.py b/src/hiero_sdk_python/node.py index 9fba5cf6c..2a0ecde91 100644 --- a/src/hiero_sdk_python/node.py +++ b/src/hiero_sdk_python/node.py @@ -126,6 +126,7 @@ def _get_channel(self): if self._root_certificates: # Use the certificate that is provided self._node_pem_cert = self._root_certificates + else: # Fetch pem_cert for the node self._node_pem_cert = self._fetch_server_certificate_pem() diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index 14691e09a..9d2f1ebff 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -228,6 +228,27 @@ def _to_proto(self): return transaction_pb2.Transaction(signedTransactionBytes=signed_transaction.SerializeToString()) + def _resolve_transaction_id(self, client: Client): + if self.transaction_id is not None: + return + + if client is not None: + operator_account_id = client.operator_account_id + if operator_account_id is not None: + self.transaction_id = client.generate_transaction_id() + else: + raise ValueError("Client must have an operator_account or transactionId must be set.") + else: + raise ValueError( + "Transaction ID must be set before freezing. Use freeze_with(client) or set_transaction_id()." + ) + + def _resolve_node_ids(self, client: Client): + if self.node_account_id is None and len(self.node_account_ids) == 0 and client is None: + raise ValueError( + "Node account ID must be set before freezing. Use freeze_with(client) or manually set node_account_ids." + ) + def freeze(self): """ Freezes the transaction by building the transaction body and setting necessary IDs. @@ -246,33 +267,9 @@ def freeze(self): Raises: ValueError: If transaction_id or node_account_id are not set. """ - if self._transaction_body_bytes: - return self + return self.freeze_with(None) - if self.transaction_id is None: - raise ValueError( - "Transaction ID must be set before freezing. Use freeze_with(client) or set_transaction_id()." - ) - - if self.node_account_id is None and len(self.node_account_ids) == 0: - raise ValueError( - "Node account ID must be set before freezing. Use freeze_with(client) or manually set node_account_ids." - ) - - # Populate node_account_ids for backward compatibility - if self.node_account_id: - self.set_node_account_id(self.node_account_id) - self._transaction_body_bytes[self.node_account_id] = self.build_transaction_body().SerializeToString() - return self - - # Build the transaction body for the single node - for node_account_id in self.node_account_ids: - self.node_account_id = node_account_id - self._transaction_body_bytes[node_account_id] = self.build_transaction_body().SerializeToString() - - return self - - def freeze_with(self, client): + def freeze_with(self, client: Client): """ Freezes the transaction by building the transaction body and setting necessary IDs. @@ -288,8 +285,10 @@ def freeze_with(self, client): if self._transaction_body_bytes: return self - if self.transaction_id is None: - self.transaction_id = client.generate_transaction_id() + # Check transaction_id and node id to be set when using freeze() + self._resolve_transaction_id(client) + + self._resolve_node_ids(client) # We iterate through every node in the client's network # For each node, set the node_account_id and build the transaction body @@ -412,7 +411,7 @@ def is_signed_by(self, public_key): return any(sig_pair.pubKeyPrefix == public_key_bytes for sig_pair in sig_map.sigPair) - def build_transaction_body(self): + def build_transaction_body(self) -> transaction_pb2.TransactionBody: """ Abstract method to build the transaction body. @@ -921,3 +920,15 @@ def get_required_chunks(self): int: Number of chunks required. """ return 1 + + @property + def size(self) -> int: + """Returns the total transaction size in bytes after protobuf encoding""" + self._require_frozen() + return self._make_request().ByteSize() + + @property + def body_size(self) -> int: + """Returns just the transaction body size in bytes after encoding""" + self._require_frozen() + return self.build_transaction_body().ByteSize() diff --git a/tests/integration/file_append_transaction_e2e_test.py b/tests/integration/file_append_transaction_e2e_test.py index e44d61e28..ace6d0acb 100644 --- a/tests/integration/file_append_transaction_e2e_test.py +++ b/tests/integration/file_append_transaction_e2e_test.py @@ -3,10 +3,12 @@ import pytest from pytest import mark +from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.file.file_append_transaction import FileAppendTransaction from hiero_sdk_python.file.file_contents_query import FileContentsQuery from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.transaction.transaction_id import TransactionId # Generate big contents for chunking tests - similar to JavaScript bigContents @@ -283,3 +285,42 @@ def test_integration_file_append_transaction_method_chaining(env): append_receipt = append_tx.execute(env.client) assert append_receipt.status == ResponseCode.SUCCESS + assert append_receipt.status == ResponseCode.SUCCESS + + +@pytest.mark.integration +def test_file_append_chunk_transaction_can_execute_with_manual_freeze(env): + """Test file append transaction can execute with manual freeze.""" + create_receipt = ( + FileCreateTransaction() + .set_keys(env.client.operator_private_key.public_key()) + .set_contents(b"") + .execute(env.client) + ) + + assert create_receipt.status == ResponseCode.SUCCESS + file_id = create_receipt.file_id + + file_contents = FileContentsQuery().set_file_id(file_id).execute(env.client) + assert file_contents == b"" + + content = "A" * (4000) # content with (4000/1024) bytes ie approx 4 chunks + + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_chunk_size(1024) + .set_contents(content) + .set_transaction_id(TransactionId.generate(env.client.operator_account_id)) + .set_node_account_id(AccountId(0, 0, 3)) + .freeze() + ) + + tx.sign(env.client.operator_private_key) + + receipt = tx.execute(env.client) + + assert receipt.status == ResponseCode.SUCCESS + + file_contents = FileContentsQuery().set_file_id(file_id).execute(env.client) + assert file_contents == bytes(content, "utf-8") diff --git a/tests/integration/topic_message_submit_transaction_e2e_test.py b/tests/integration/topic_message_submit_transaction_e2e_test.py index 94feceb6a..23393e150 100644 --- a/tests/integration/topic_message_submit_transaction_e2e_test.py +++ b/tests/integration/topic_message_submit_transaction_e2e_test.py @@ -6,6 +6,7 @@ import pytest +from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction from hiero_sdk_python.consensus.topic_delete_transaction import TopicDeleteTransaction from hiero_sdk_python.consensus.topic_message_submit_transaction import ( @@ -18,6 +19,7 @@ from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee from hiero_sdk_python.transaction.custom_fee_limit import CustomFeeLimit +from hiero_sdk_python.transaction.transaction_id import TransactionId 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 message_transaction.execute(env.client) delete_topic(env.client, topic_id) + + +@pytest.mark.integration +def test_topic_message_submit_transaction_can_submit_a_large_message_manual_freeze(env): + """Test topic message submit transaction can submit large message with manual freeze.""" + topic_id = create_topic(client=env.client, admin_key=env.operator_key) + + info = TopicInfoQuery().set_topic_id(topic_id).execute(env.client) + assert info.sequence_number == 0 + + message = "A" * (1024 * 14) # message with (1024 * 14) bytes ie 14 chunks + + message_tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_message(message) + .set_transaction_id(TransactionId.generate(env.client.operator_account_id)) + .set_node_account_id(AccountId(0, 0, 3)) + .freeze() + ) + + message_tx.sign(env.client.operator_private_key) + message_receipt = message_tx.execute(env.client) + + assert message_receipt.status == ResponseCode.SUCCESS + + info = TopicInfoQuery().set_topic_id(topic_id).execute(env.client) + assert info.sequence_number == 14 + + delete_topic(env.client, topic_id) diff --git a/tests/unit/file_append_transaction_test.py b/tests/unit/file_append_transaction_test.py index 479acefd5..4b832c43e 100644 --- a/tests/unit/file_append_transaction_test.py +++ b/tests/unit/file_append_transaction_test.py @@ -4,12 +4,14 @@ import pytest +from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError from hiero_sdk_python.file.file_append_transaction import FileAppendTransaction from hiero_sdk_python.file.file_id import FileId from hiero_sdk_python.hapi.services import ( response_header_pb2, response_pb2, + timestamp_pb2, transaction_get_receipt_pb2, transaction_receipt_pb2, transaction_response_pb2, @@ -372,3 +374,30 @@ def test_file_append_execute_all_returns_receipt_without_validation(file_id): receipts = tx.execute_all(client) assert receipts[0].status == ResponseCode.INVALID_FILE_ID + + +def test_chunk_transaction_id_nanosecond_overflow(file_id): + """Test that multi-chunk transaction IDs handle nanosecond overflow correctly.""" + base_seconds = 1770911831 + base_nanos = 999_999_999 + + start_timestamp = timestamp_pb2.Timestamp(seconds=base_seconds, nanos=base_nanos) + tx_id = TransactionId(account_id=AccountId(0, 0, 2), valid_start=start_timestamp) + + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents("a" * 20) + .set_chunk_size(10) + .set_transaction_id(tx_id) + .set_node_account_id(AccountId(0, 0, 3)) + .freeze() + ) + + # First chunk is exactly equal initial ID + assert tx._transaction_ids[0].valid_start.seconds == base_seconds + assert tx._transaction_ids[0].valid_start.nanos == base_nanos + + # Second chunk seconds=base_seconds + 1, nanos=0 + assert tx._transaction_ids[1].valid_start.seconds == base_seconds + 1 + assert tx._transaction_ids[1].valid_start.nanos == 0 diff --git a/tests/unit/mock_server.py b/tests/unit/mock_server.py index 3ee264b4f..cdb5f848f 100644 --- a/tests/unit/mock_server.py +++ b/tests/unit/mock_server.py @@ -180,6 +180,7 @@ def mock_hedera_servers(response_sequences): node._close() client = Client(network) + client.logger.set_level(LogLevel.DISABLED) # Set the operator key = PrivateKey.generate() diff --git a/tests/unit/topic_message_submit_transaction_test.py b/tests/unit/topic_message_submit_transaction_test.py index b0adf7a69..b4efcaf38 100644 --- a/tests/unit/topic_message_submit_transaction_test.py +++ b/tests/unit/topic_message_submit_transaction_test.py @@ -6,11 +6,13 @@ import pytest +from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.consensus.topic_message_submit_transaction import TopicMessageSubmitTransaction from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError from hiero_sdk_python.hapi.services import ( response_header_pb2, response_pb2, + timestamp_pb2, transaction_get_receipt_pb2, transaction_receipt_pb2, transaction_response_pb2, @@ -20,6 +22,7 @@ ) from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.transaction.custom_fee_limit import CustomFeeLimit +from hiero_sdk_python.transaction.transaction_id import TransactionId from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt from hiero_sdk_python.transaction.transaction_response import TransactionResponse from tests.unit.mock_server import mock_hedera_servers @@ -503,3 +506,38 @@ def test_topic_submit_execute_returns_failed_receipt_by_default(topic_id): receipt = tx.execute(client) assert receipt.status == ResponseCode.INVALID_SIGNATURE + + +def test_topic_submit_message_raises_error_on_freeze(topic_id): + """Test transaction raises error on freeze when the transaction_id and node_id not set""" + tx = TopicMessageSubmitTransaction().set_topic_id(topic_id).set_message("Hello Hiero") + + with pytest.raises(ValueError): + tx.freeze() + + +def test_chunk_transaction_id_nanosecond_overflow(topic_id): + """Test that multi-chunk transaction IDs handle nanosecond overflow correctly.""" + base_seconds = 1770911831 + base_nanos = 999_999_999 + + start_timestamp = timestamp_pb2.Timestamp(seconds=base_seconds, nanos=base_nanos) + tx_id = TransactionId(account_id=AccountId(0, 0, 2), valid_start=start_timestamp) + + tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_message("a" * 20) + .set_chunk_size(10) + .set_transaction_id(tx_id) + .set_node_account_id(AccountId(0, 0, 3)) + .freeze() + ) + + # First chunk is exactly equal initial ID + assert tx._transaction_ids[0].valid_start.seconds == base_seconds + assert tx._transaction_ids[0].valid_start.nanos == base_nanos + + # Second chunk seconds=base_seconds + 1, nanos=0 + assert tx._transaction_ids[1].valid_start.seconds == base_seconds + 1 + assert tx._transaction_ids[1].valid_start.nanos == 0 diff --git a/tests/unit/transaction_test.py b/tests/unit/transaction_test.py index 5821d9e7a..6ffa36549 100644 --- a/tests/unit/transaction_test.py +++ b/tests/unit/transaction_test.py @@ -4,8 +4,12 @@ from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.consensus.topic_message_submit_transaction import TopicMessageSubmitTransaction from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.exceptions import ReceiptStatusError +from hiero_sdk_python.file.file_append_transaction import FileAppendTransaction +from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction +from hiero_sdk_python.file.file_id import FileId from hiero_sdk_python.hapi.services import ( basic_types_pb2, response_header_pb2, @@ -14,6 +18,7 @@ transaction_receipt_pb2, transaction_response_pb2, ) +from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction @@ -26,6 +31,24 @@ pytestmark = pytest.mark.unit +@pytest.fixture +def file_id(): + """Returns a file_id for test.""" + return FileId.from_string("0.0.1") + + +@pytest.fixture +def account_id(): + """Returns an account_id for test.""" + return AccountId.from_string("0.0.9") + + +@pytest.fixture +def transaction_id(): + """Returns a transaction_id for test.""" + return TransactionId.from_string("0.0.9@1770911831.331000137") + + def test_execute_waits_for_receipt_receipt(): """Test execute return TransactionReceipt when wait_for_receipt is True (default).""" ok_response = transaction_response_pb2.TransactionResponse(nodeTransactionPrecheckCode=ResponseCode.OK) @@ -160,3 +183,248 @@ def test_multiple_keys_still_work(): key2.public_key().to_bytes_raw(), } assert pubkey_prefixes == expected_prefixes, "Signatures should match key1 and key2 exactly" + + +def test_same_size_for_identical_transactions(transaction_id, account_id): + """Test two identical transactions should have the same size.""" + key = PrivateKey.generate() + + tx1 = ( + AccountCreateTransaction() + .set_key_without_alias(key) + .set_initial_balance(Hbar(2)) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + tx2 = ( + AccountCreateTransaction() + .set_key_without_alias(key) + .set_initial_balance(Hbar(2)) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + assert tx1.size == tx2.size + + +def test_signed_tx_have_larger_size(transaction_id, account_id): + """Test signed Transaction should have larger size.""" + key = PrivateKey.generate() + + tx1 = ( + AccountCreateTransaction() + .set_key_without_alias(key) + .set_initial_balance(Hbar(2)) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + .sign(PrivateKey.generate()) + ) + + tx2 = ( + AccountCreateTransaction() + .set_key_without_alias(key) + .set_initial_balance(Hbar(2)) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + assert tx1.size > tx2.size + + +def test_tx_with_larger_content_should_have_larger_tx_body(transaction_id, account_id): + """Test transaction with larger content should have larger transaction body.""" + tx1 = ( + FileCreateTransaction() + .set_contents("smallBody") + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + tx2 = ( + FileCreateTransaction() + .set_contents("veryLargeBody") + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + assert tx1.body_size < tx2.body_size + + +def test_tx_without_optional_fields_should_have_smaller_tx_body(transaction_id, account_id): + """Test transaction with without optional fields should have smaller transaction body.""" + key = PrivateKey.generate() + tx1 = ( + AccountCreateTransaction() + .set_key_without_alias(key) + .set_initial_balance(Hbar(2)) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + tx2 = ( + AccountCreateTransaction() + .set_key_without_alias(key) + .set_initial_balance(Hbar(2)) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .set_alias(PrivateKey.generate_ecdsa().public_key().to_evm_address()) + .set_transaction_valid_duration(10) + .freeze() + ) + + assert tx1.body_size < tx2.body_size + + +def test_file_append_chunk_tx_should_return_list_of_body_sizes(file_id, account_id, transaction_id): + """Test file append tx should return array of body sizes for multi-chunk transaction.""" + chunk_size = 1024 + content = "a" * (chunk_size * 3) + + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_chunk_size(chunk_size) + .set_contents(content) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + sizes = tx.body_size_all_chunks + assert isinstance(sizes, list) + assert len(sizes) == 3 + + +def test_file_append_single_chunk_tx_return_list_of_len_one(file_id, account_id, transaction_id): + """Test file append tx should return array of one size for single-chunk transaction.""" + content = "small_content" + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents(content) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + sizes = tx.body_size_all_chunks + assert isinstance(sizes, list) + assert len(sizes) == 1 + + +def test_message_submit_chunk_tx_should_return_list_of_body_sizes(topic_id, account_id, transaction_id): + """Test topic message submit tx should return array of body sizes for multi-chunk transaction.""" + chunk_size = 1024 + message = "a" * (chunk_size * 3) + + tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_chunk_size(chunk_size) + .set_message(message) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + sizes = tx.body_size_all_chunks + assert isinstance(sizes, list) + assert len(sizes) == 3 + assert tx._current_chunk_index == 0 + + +def test_message_submit_single_chunk_tx_return_list_of_len_one(topic_id, account_id, transaction_id): + """Test topic message submit tx should return array of one size for single-chunk transaction.""" + message = "small_content" + tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_message(message) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + sizes = tx.body_size_all_chunks + assert isinstance(sizes, list) + assert len(sizes) == 1 + + +def test_tx_with_no_content_should_return_single_body_chunk(file_id, account_id, transaction_id): + """Test should return single body chunk for transaction with no content.""" + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents(" ") + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + sizes = tx.body_size_all_chunks + assert isinstance(sizes, list) + assert len(sizes) == 1 + + +def test_chunked_tx_return_proper_sizes(file_id, account_id, transaction_id): + """Test should return proper sizes for FileAppend transactions when chunking occurs.""" + large_content = "a" * 2048 + + large_tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents(large_content) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + large_size = large_tx.size + + small_content = "a" * 512 + + small_tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents(small_content) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + small_size = small_tx.size + + # Size should be greater than single chunk size + assert large_size > 1024 + # The larger chunked transaction should be bigger than the single-chunk transaction + assert large_size > small_size + assert large_tx._current_chunk_index == 0 + + +def test_chunked_tx_differ_size_if_chunk_are_not_equal(topic_id, account_id, transaction_id): + """Test that the last chunk's size is different from the full chunks if the chunk size is not even.""" + chunk_size = 1024 + message = "a" * (chunk_size + 512) + + tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_chunk_size(chunk_size) + .set_message(message) + .set_transaction_id(transaction_id) + .set_node_account_id(account_id) + .freeze() + ) + + sizes = tx.body_size_all_chunks + assert len(sizes) == 2 + # The first chunk should be larger than the second because it carries more message data + assert sizes[0] > sizes[1]