Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
90c4560
chore: added some helper method
manishdait Feb 11, 2026
2119103
chore: refactor chunck method for file append tx
manishdait Feb 12, 2026
3643524
chore: refactor freeze() to work with chunck tx
manishdait Feb 12, 2026
bbcbe3a
chore: added test fro chunk tx manual freeze
manishdait Feb 13, 2026
80b83a3
fix: fix the intendation in file_append tx execute func
manishdait Feb 13, 2026
8f4b861
chore: fix unused imports and prints
manishdait Feb 13, 2026
eab6e62
chore: converted method to property
manishdait Feb 13, 2026
436fb88
chore: formated unit test
manishdait Feb 13, 2026
384a5f3
chore: updated changelog
manishdait Feb 13, 2026
af8b999
chore: fix the unit test
manishdait Feb 16, 2026
a223cbe
chore: fix rebase
manishdait Mar 20, 2026
7f3a4f3
chore: added test for topic message submit tx
manishdait Mar 22, 2026
01c8fee
chore: fix the transaction_id to return original one
manishdait Mar 24, 2026
147aa97
chore: imporve freeze_with to work with the client without operator
manishdait Mar 24, 2026
52379e6
chore: added some suggesttions
manishdait Mar 24, 2026
5d64698
chore: neat pick
manishdait Mar 25, 2026
381fae3
chore: cyvlometic codacy fix
manishdait Mar 25, 2026
db49e98
chore: fix pyptojectoml
manishdait Mar 25, 2026
4bc8097
chore: make some neetpick
manishdait Apr 7, 2026
f2efa73
chore: fix rebase
manishdait Apr 13, 2026
11192dc
chore: fix rebase
manishdait Apr 16, 2026
1c0d56b
chore: added suggestion
manishdait Apr 21, 2026
765b8ba
chore: add test for validating changes
manishdait Apr 21, 2026
e759b2e
chore: remove test.py
manishdait Apr 21, 2026
26d08b4
chore: rebase fix
manishdait May 9, 2026
f6367a1
Merge branch 'main' into feat/tx-size
manishdait May 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Comment thread
manishdait marked this conversation as resolved.
76 changes: 44 additions & 32 deletions src/hiero_sdk_python/file/file_append_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Comment thread
manishdait marked this conversation as resolved.

self._transaction_ids.append(chunk_transaction_id)

return super().freeze_with(client)

@overload
def execute(
Expand Down Expand Up @@ -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
Comment thread
manishdait marked this conversation as resolved.
1 change: 1 addition & 0 deletions src/hiero_sdk_python/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
69 changes: 40 additions & 29 deletions src/hiero_sdk_python/transaction/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment thread
manishdait marked this conversation as resolved.
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.
Expand All @@ -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)
Comment thread
manishdait marked this conversation as resolved.

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.

Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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()
Comment thread
manishdait marked this conversation as resolved.

@property
def body_size(self) -> int:
Comment thread
exploreriii marked this conversation as resolved.
"""Returns just the transaction body size in bytes after encoding"""
self._require_frozen()
return self.build_transaction_body().ByteSize()
Comment thread
manishdait marked this conversation as resolved.
41 changes: 41 additions & 0 deletions tests/integration/file_append_transaction_e2e_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Comment thread
exploreriii marked this conversation as resolved.
"""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()
Comment thread
aceppaluni marked this conversation as resolved.
)

tx.sign(env.client.operator_private_key)

receipt = tx.execute(env.client)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

assert receipt.status == ResponseCode.SUCCESS

file_contents = FileContentsQuery().set_file_id(file_id).execute(env.client)
assert file_contents == bytes(content, "utf-8")
32 changes: 32 additions & 0 deletions tests/integration/topic_message_submit_transaction_e2e_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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):
Expand Down Expand Up @@ -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()
Comment thread
manishdait marked this conversation as resolved.
)

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)
Loading
Loading