From 4c8cdc67968a50717469e3159e902fe64c9c09b9 Mon Sep 17 00:00:00 2001 From: iron-prog Date: Fri, 24 Jul 2026 17:58:43 +0530 Subject: [PATCH 1/3] fix(topic): separate topic memo from transaction memo Signed-off-by: iron-prog --- src/hiero_sdk_python/consensus/topic_create_transaction.py | 6 +++--- src/hiero_sdk_python/consensus/topic_update_transaction.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/hiero_sdk_python/consensus/topic_create_transaction.py b/src/hiero_sdk_python/consensus/topic_create_transaction.py index 1eedc813d..fd26e4876 100644 --- a/src/hiero_sdk_python/consensus/topic_create_transaction.py +++ b/src/hiero_sdk_python/consensus/topic_create_transaction.py @@ -63,7 +63,7 @@ def __init__( fee_exempt_keys (list[Key], optional): Optional list of fee exempt keys for the topic (PrivateKey or PublicKey). """ super().__init__() - self.memo: str = memo or "" + self.topic_memo: str = memo or "" self.admin_key: Key | None = admin_key self.submit_key: Key | None = submit_key self.auto_renew_period: Duration = auto_renew_period or Duration(7890000) @@ -84,7 +84,7 @@ def set_memo(self, memo: str) -> TopicCreateTransaction: TopicCreateTransaction: The current instance for method chaining. """ self._require_not_frozen() - self.memo = memo + self.topic_memo = memo return self def set_admin_key(self, key: Key) -> TopicCreateTransaction: @@ -237,7 +237,7 @@ def _build_proto_body(self) -> consensus_create_topic_pb2.ConsensusCreateTopicTr submitKey=key_to_proto(self.submit_key), autoRenewPeriod=(self.auto_renew_period._to_proto() if self.auto_renew_period is not None else None), autoRenewAccount=(self.auto_renew_account._to_proto() if self.auto_renew_account is not None else None), - memo=self.memo, + memo=self.topic_memo, custom_fees=[custom_fee._to_topic_fee_proto() for custom_fee in self.custom_fees], fee_schedule_key=key_to_proto(self.fee_schedule_key), fee_exempt_key_list=[key_to_proto(key) for key in self.fee_exempt_keys], diff --git a/src/hiero_sdk_python/consensus/topic_update_transaction.py b/src/hiero_sdk_python/consensus/topic_update_transaction.py index ef283f9b8..2ba14b60b 100644 --- a/src/hiero_sdk_python/consensus/topic_update_transaction.py +++ b/src/hiero_sdk_python/consensus/topic_update_transaction.py @@ -57,7 +57,7 @@ def __init__( """ super().__init__() self.topic_id: TopicId | None = topic_id - self.memo: str = memo or "" + self.topic_memo: str = memo or "" self.admin_key: Key | None = admin_key self.submit_key: Key | None = submit_key self.auto_renew_period: Duration | None = auto_renew_period @@ -93,7 +93,7 @@ def set_memo(self, memo: str) -> TopicUpdateTransaction: TopicUpdateTransaction: Returns the instance for method chaining. """ self._require_not_frozen() - self.memo = memo + self.topic_memo = memo return self def set_admin_key(self, key: Key) -> TopicUpdateTransaction: @@ -271,7 +271,7 @@ def _build_proto_body(self) -> consensus_update_topic_pb2.ConsensusUpdateTopicTr ), autoRenewAccount=(self.auto_renew_account._to_proto() if self.auto_renew_account else None), expirationTime=self.expiration_time._to_protobuf() if self.expiration_time else None, - memo=_wrappers_pb2.StringValue(value=self.memo) if self.memo is not None else None, + memo=_wrappers_pb2.StringValue(value=self.topic_memo) if self.topic_memo is not None else None, custom_fees=custom_fees, fee_schedule_key=key_to_proto(self.fee_schedule_key) if self.fee_schedule_key else None, fee_exempt_key_list=fee_exempt_key_list, From aeeaa921186baff147231047028f4eb57996f755 Mon Sep 17 00:00:00 2001 From: iron-prog Date: Fri, 24 Jul 2026 17:59:08 +0530 Subject: [PATCH 2/3] test(topic): add memo collision regression tests Signed-off-by: iron-prog --- tests/unit/topic_create_transaction_test.py | 28 +++++++++++++++++++-- tests/unit/topic_update_transaction_test.py | 22 ++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/unit/topic_create_transaction_test.py b/tests/unit/topic_create_transaction_test.py index b6722899d..7711b395e 100644 --- a/tests/unit/topic_create_transaction_test.py +++ b/tests/unit/topic_create_transaction_test.py @@ -328,7 +328,7 @@ def test_constructor(multiple_custom_fees, key_type, use_private): fee_exempt_keys=fee_exempt_keys, ) - assert tx.memo == "Test Topic" + assert tx.topic_memo == "Test Topic" assert tx.admin_key == admin_key assert tx.submit_key == submit_key assert tx.custom_fees == multiple_custom_fees @@ -339,7 +339,7 @@ def test_constructor(multiple_custom_fees, key_type, use_private): def test_constructor_default_values(): """Test constructor with default values.""" tx_default = TopicCreateTransaction() - assert tx_default.memo == "" + assert tx_default.topic_memo == "" assert tx_default.admin_key is None assert tx_default.submit_key is None assert tx_default.custom_fees == [] @@ -627,3 +627,27 @@ def test_freeze_with_leaves_auto_renew_account_unset_without_operator(mock_clien body = frozen_tx.build_transaction_body() assert not body.consensusCreateTopic.HasField("autoRenewAccount") + + +def test_topic_memo_does_not_collide_with_transaction_memo(): + """Regression test for: topic and transaction memos remain independent.""" + tx = TopicCreateTransaction(memo="my topic memo") + assert tx.topic_memo == "my topic memo" + assert tx.memo == "" # base Transaction-level memo defaults independently + + tx.set_transaction_memo("some unrelated audit note") + + assert tx.topic_memo == "my topic memo", "topic_memo was clobbered by set_transaction_memo()" + assert tx.memo == "some unrelated audit note" + + +def test_topic_memo_reflected_in_protobuf_independent_of_transaction_memo(mock_client): + """Verify protobuf preserves separate topic and transaction memos.""" + tx = TopicCreateTransaction(memo="my topic memo") + tx.set_transaction_memo("some unrelated audit note") + tx.transaction_id = TransactionId.generate(AccountId(0, 0, 1234)) + frozen_tx = tx.freeze_with(mock_client) + body = frozen_tx.build_transaction_body() + + assert body.consensusCreateTopic.memo == "my topic memo" + assert body.memo == "some unrelated audit note" diff --git a/tests/unit/topic_update_transaction_test.py b/tests/unit/topic_update_transaction_test.py index 24b1d7dd6..1669bfd70 100644 --- a/tests/unit/topic_update_transaction_test.py +++ b/tests/unit/topic_update_transaction_test.py @@ -359,3 +359,25 @@ def test_topic_update_transaction_with_all_fields(topic_id): # Verify the receipt contains the expected values assert receipt.status == ResponseCode.SUCCESS + + +def test_topic_memo_does_not_collide_with_transaction_memo(): + """Regression test for: topic and transaction memos remain independent.""" + tx = TopicUpdateTransaction(memo="my topic memo") + assert tx.topic_memo == "my topic memo" + assert tx.memo == "" # base Transaction-level memo defaults independently + + tx.set_transaction_memo("some unrelated audit note") + + assert tx.topic_memo == "my topic memo", "topic_memo was clobbered by set_transaction_memo()" + assert tx.memo == "some unrelated audit note" + + +def test_set_memo_updates_topic_memo_only(): + """Verify set_memo() only updates the topic memo.""" + tx = TopicUpdateTransaction() + tx.set_transaction_memo("audit note") + tx.set_memo("new topic memo") + + assert tx.topic_memo == "new topic memo" + assert tx.memo == "audit note" From c547a5825bab7791e0b63006a0ce09c5f0c4ee49 Mon Sep 17 00:00:00 2001 From: iron-prog Date: Sun, 26 Jul 2026 00:48:50 +0530 Subject: [PATCH 3/3] fix(consensus): distinguish unset and empty topic memo - Preserve unset topic memos by storing None instead of collapsing to "" - Add protobuf regression tests for topic and transaction memo serialization - Verify unset and empty topic memos serialize correctly Signed-off-by: iron-prog --- .../consensus/topic_update_transaction.py | 2 +- tests/unit/topic_update_transaction_test.py | 57 ++++++++++++++++--- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/hiero_sdk_python/consensus/topic_update_transaction.py b/src/hiero_sdk_python/consensus/topic_update_transaction.py index 2ba14b60b..2052169ef 100644 --- a/src/hiero_sdk_python/consensus/topic_update_transaction.py +++ b/src/hiero_sdk_python/consensus/topic_update_transaction.py @@ -57,7 +57,7 @@ def __init__( """ super().__init__() self.topic_id: TopicId | None = topic_id - self.topic_memo: str = memo or "" + self.topic_memo: str | None = memo self.admin_key: Key | None = admin_key self.submit_key: Key | None = submit_key self.auto_renew_period: Duration | None = auto_renew_period diff --git a/tests/unit/topic_update_transaction_test.py b/tests/unit/topic_update_transaction_test.py index 1669bfd70..2a0902cf5 100644 --- a/tests/unit/topic_update_transaction_test.py +++ b/tests/unit/topic_update_transaction_test.py @@ -361,16 +361,28 @@ def test_topic_update_transaction_with_all_fields(topic_id): assert receipt.status == ResponseCode.SUCCESS -def test_topic_memo_does_not_collide_with_transaction_memo(): - """Regression test for: topic and transaction memos remain independent.""" - tx = TopicUpdateTransaction(memo="my topic memo") - assert tx.topic_memo == "my topic memo" - assert tx.memo == "" # base Transaction-level memo defaults independently +def test_topic_memo_and_transaction_memo_independent_in_protobuf( + mock_account_ids, + topic_id, +): + """Topic and transaction memos serialize independently.""" + + _, _, node_account_id, _, _ = mock_account_ids + + tx = TopicUpdateTransaction( + topic_id=topic_id, + memo="my topic memo", + ) + + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id tx.set_transaction_memo("some unrelated audit note") - assert tx.topic_memo == "my topic memo", "topic_memo was clobbered by set_transaction_memo()" - assert tx.memo == "some unrelated audit note" + body = tx.build_transaction_body() + + assert body.memo == "some unrelated audit note" + assert body.consensusUpdateTopic.memo.value == "my topic memo" def test_set_memo_updates_topic_memo_only(): @@ -381,3 +393,34 @@ def test_set_memo_updates_topic_memo_only(): assert tx.topic_memo == "new topic memo" assert tx.memo == "audit note" + + +def test_topic_memo_serialization_distinguishes_unset_and_empty( + mock_account_ids, + topic_id, +): + """Unset and explicit empty topic memos serialize differently.""" + _, _, node_account_id, _, _ = mock_account_ids + + tx = TopicUpdateTransaction(topic_id=topic_id) + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id + + body = tx.build_transaction_body().consensusUpdateTopic + assert not body.HasField("memo") + + tx = TopicUpdateTransaction(topic_id=topic_id, memo="") + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id + + body = tx.build_transaction_body().consensusUpdateTopic + assert body.HasField("memo") + assert body.memo.value == "" + + tx = TopicUpdateTransaction(topic_id=topic_id, memo="hello") + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id + + body = tx.build_transaction_body().consensusUpdateTopic + assert body.HasField("memo") + assert body.memo.value == "hello"