diff --git a/src/hiero_sdk_python/consensus/topic_update_transaction.py b/src/hiero_sdk_python/consensus/topic_update_transaction.py index a886073bf..ef283f9b8 100644 --- a/src/hiero_sdk_python/consensus/topic_update_transaction.py +++ b/src/hiero_sdk_python/consensus/topic_update_transaction.py @@ -10,7 +10,7 @@ from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.channels import _Channel from hiero_sdk_python.consensus.topic_id import TopicId -from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.Duration import Duration from hiero_sdk_python.executable import _Method from hiero_sdk_python.hapi.services import consensus_update_topic_pb2, duration_pb2, timestamp_pb2, transaction_pb2 @@ -21,6 +21,7 @@ from hiero_sdk_python.timestamp import Timestamp from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee from hiero_sdk_python.transaction.transaction import Transaction +from hiero_sdk_python.utils.key_utils import key_to_proto class TopicUpdateTransaction(Transaction): @@ -30,14 +31,14 @@ def __init__( self, topic_id: TopicId | None = None, memo: str | None = None, - admin_key: PublicKey | None = None, - submit_key: PublicKey | None = None, + admin_key: Key | None = None, + submit_key: Key | None = None, auto_renew_period: Duration | None = Duration(7890000), auto_renew_account: AccountId | None = None, expiration_time: Timestamp | None = None, custom_fees: list[CustomFixedFee] | None = None, - fee_schedule_key: PublicKey | None = None, - fee_exempt_keys: list[PublicKey] | None = None, + fee_schedule_key: Key | None = None, + fee_exempt_keys: list[Key] | None = None, ) -> None: """ Initializes a new instance of the TopicUpdateTransaction class. @@ -45,24 +46,27 @@ def __init__( Args: topic_id (TopicId): The ID of the topic to update. memo (str): The memo associated with the topic. - admin_key (PublicKey): The admin key for the topic. - submit_key (PublicKey): The submit key for the topic. + admin_key (Key): The admin key for the topic. + submit_key (Key): The submit key for the topic. auto_renew_period (Duration): The auto-renew period for the topic. auto_renew_account (AccountId): The account ID for auto-renewal. expiration_time (Timestamp): The expiration time of the topic. + custom_fees (list[CustomFixedFee]): A list of custom fees to set for the topic. + fee_schedule_key (Key): The fee schedule key for the topic. + fee_exempt_keys (list[Key]): A list of fee exempt keys for the topic """ super().__init__() self.topic_id: TopicId | None = topic_id self.memo: str = memo or "" - self.admin_key: PublicKey | None = admin_key - self.submit_key: PublicKey | None = submit_key + self.admin_key: Key | None = admin_key + self.submit_key: Key | None = submit_key self.auto_renew_period: Duration | None = auto_renew_period self.auto_renew_account: AccountId | None = auto_renew_account self.expiration_time: Timestamp | None = expiration_time self.transaction_fee: int = 10_000_000 self.custom_fees: list[CustomFixedFee] | None = custom_fees - self.fee_schedule_key: PublicKey | None = fee_schedule_key - self.fee_exempt_keys: list[PublicKey] | None = fee_exempt_keys + self.fee_schedule_key: Key | None = fee_schedule_key + self.fee_exempt_keys: list[Key] | None = fee_exempt_keys def set_topic_id(self, topic_id: TopicId) -> TopicUpdateTransaction: """ @@ -92,12 +96,12 @@ def set_memo(self, memo: str) -> TopicUpdateTransaction: self.memo = memo return self - def set_admin_key(self, key: PublicKey) -> TopicUpdateTransaction: + def set_admin_key(self, key: Key) -> TopicUpdateTransaction: """ - Sets the public admin key for the topic. + Sets the admin key for the topic. Args: - Publickey: The admin key to set. + key: The admin key to set. Returns: TopicUpdateTransaction: Returns the instance for method chaining. @@ -106,12 +110,12 @@ def set_admin_key(self, key: PublicKey) -> TopicUpdateTransaction: self.admin_key = key return self - def set_submit_key(self, key: PublicKey) -> TopicUpdateTransaction: + def set_submit_key(self, key: Key) -> TopicUpdateTransaction: """ - Sets the public submit key for the topic. + Sets the submit key for the topic. Args: - Publickey: The submit key to set. + key: The submit key to set. Returns: TopicUpdateTransaction: Returns the instance for method chaining. @@ -181,12 +185,12 @@ def set_custom_fees(self, custom_fees: list[CustomFixedFee]) -> TopicUpdateTrans self.custom_fees = custom_fees return self - def set_fee_schedule_key(self, key: PublicKey) -> TopicUpdateTransaction: + def set_fee_schedule_key(self, key: Key) -> TopicUpdateTransaction: """ Sets the fee schedule key for the topic update transaction. Args: - key (PublicKey): The fee schedule key to set for the topic. + key (Key): The fee schedule key to set for the topic. Returns: TopicUpdateTransaction: The current instance for method chaining. @@ -195,12 +199,12 @@ def set_fee_schedule_key(self, key: PublicKey) -> TopicUpdateTransaction: self.fee_schedule_key = key return self - def set_fee_exempt_keys(self, keys: list[PublicKey]) -> TopicUpdateTransaction: + def set_fee_exempt_keys(self, keys: list[Key]) -> TopicUpdateTransaction: """ Sets the fee exempt keys for the topic update transaction. Args: - keys (list[PublicKey]): The fee exempt keys to set for the topic. + keys (list[Key]): The fee exempt keys to set for the topic. Returns: TopicUpdateTransaction: The current instance for method chaining. @@ -253,15 +257,15 @@ def _build_proto_body(self) -> consensus_update_topic_pb2.ConsensusUpdateTopicTr ) fee_exempt_key_list = ( - FeeExemptKeyList(keys=[key._to_proto() for key in self.fee_exempt_keys]) + FeeExemptKeyList(keys=[key_to_proto(key) for key in self.fee_exempt_keys]) if self.fee_exempt_keys is not None else None ) return consensus_update_topic_pb2.ConsensusUpdateTopicTransactionBody( topicID=self.topic_id._to_proto(), - adminKey=self.admin_key._to_proto() if self.admin_key else None, - submitKey=self.submit_key._to_proto() if self.submit_key else None, + adminKey=key_to_proto(self.admin_key) if self.admin_key else None, + submitKey=key_to_proto(self.submit_key) if self.submit_key else None, autoRenewPeriod=( duration_pb2.Duration(seconds=self.auto_renew_period.seconds) if self.auto_renew_period else None ), @@ -269,7 +273,7 @@ def _build_proto_body(self) -> consensus_update_topic_pb2.ConsensusUpdateTopicTr 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, custom_fees=custom_fees, - fee_schedule_key=self.fee_schedule_key._to_proto() if self.fee_schedule_key else None, + 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, ) diff --git a/tests/unit/topic_update_transaction_test.py b/tests/unit/topic_update_transaction_test.py index 12124de98..24b1d7dd6 100644 --- a/tests/unit/topic_update_transaction_test.py +++ b/tests/unit/topic_update_transaction_test.py @@ -6,7 +6,9 @@ from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.consensus.topic_update_transaction import TopicUpdateTransaction +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.crypto.public_key import PublicKey from hiero_sdk_python.Duration import Duration from hiero_sdk_python.hapi.services import ( response_header_pb2, @@ -26,7 +28,181 @@ pytestmark = pytest.mark.unit -# This test uses fixtures (mock_account_ids, topic_id) as parameters +def create_key(key_type: str, use_private: bool) -> PrivateKey | PublicKey: + """ + Create a key based on type and whether to use private or public. + + Args: + key_type: "ed25519" or "ecdsa" + use_private: True for PrivateKey, False for PublicKey + + Returns: + The created key (PrivateKey or PublicKey) + """ + if key_type == "ed25519": + private_key = PrivateKey.generate_ed25519() + elif key_type == "ecdsa": + private_key = PrivateKey.generate("ecdsa") + else: + raise ValueError(f"Unsupported key_type: {key_type!r}") + return private_key if use_private else private_key.public_key() + + +def get_expected_public_key(key: Key) -> PublicKey: + """ + Get the public key from either PrivateKey or PublicKey. + + Args: + key: PrivateKey or PublicKey + + Returns: + PublicKey + """ + return key if isinstance(key, PublicKey) else key.public_key() + + +def verify_key_in_proto(proto_key, expected_public_key: PublicKey, key_type: str) -> None: + """Verify the proto key matches expected public key.""" + if key_type == "ed25519": + assert proto_key.ed25519 == expected_public_key.to_bytes_raw() + elif key_type == "ecdsa": # ecdsa + assert proto_key.HasField("ECDSA_secp256k1") + assert proto_key.ECDSA_secp256k1 == expected_public_key.to_bytes_raw() + else: + raise ValueError(f"Unsupported key_type: {key_type!r}") + + +@pytest.mark.parametrize( + "key_type,use_private", + [ + ("ed25519", True), + ("ed25519", False), + ("ecdsa", True), + ("ecdsa", False), + ], +) +def test_topic_update_setters_return_self(key_type, use_private, topic_id): + """Fluent setters must return self for the Key-typed API.""" + key = create_key(key_type, use_private) + tx = TopicUpdateTransaction() + + assert tx.set_topic_id(topic_id) is tx + assert tx.set_admin_key(key) is tx + assert tx.set_submit_key(key) is tx + assert tx.set_fee_schedule_key(key) is tx + assert tx.set_fee_exempt_keys([key]) is tx + + +@pytest.mark.parametrize( + "key_type,use_private", + [ + ("ed25519", True), + ("ed25519", False), + ("ecdsa", True), + ("ecdsa", False), + ], +) +def test_build_topic_update_transaction_body_with_all_key_types(mock_account_ids, topic_id, key_type, use_private): + """Test building a TopicUpdateTransaction body with different key types.""" + _, _, node_account_id, _, _ = mock_account_ids + + admin_key = create_key(key_type, use_private) + submit_key = create_key(key_type, use_private) + fee_schedule_key = create_key(key_type, use_private) + fee_exempt_keys = [ + create_key(key_type, use_private), + create_key(key_type, use_private), + ] + + expected_admin_public = get_expected_public_key(admin_key) + expected_submit_public = get_expected_public_key(submit_key) + expected_fee_schedule_public = get_expected_public_key(fee_schedule_key) + expected_fee_exempt_publics = [get_expected_public_key(key) for key in fee_exempt_keys] + + tx = TopicUpdateTransaction( + topic_id=topic_id, + memo="Updated Memo", + admin_key=admin_key, + submit_key=submit_key, + custom_fees=[CustomFixedFee(1000, fee_collector_account_id=AccountId(0, 0, 9876))], + fee_schedule_key=fee_schedule_key, + fee_exempt_keys=fee_exempt_keys, + ) + + tx.operator_account_id = AccountId(0, 0, 2) + tx.node_account_id = node_account_id + + transaction_body = tx.build_transaction_body() + + assert transaction_body.consensusUpdateTopic.topicID.topicNum == 1234 + assert transaction_body.consensusUpdateTopic.memo.value == "Updated Memo" + verify_key_in_proto(transaction_body.consensusUpdateTopic.adminKey, expected_admin_public, key_type) + verify_key_in_proto(transaction_body.consensusUpdateTopic.submitKey, expected_submit_public, key_type) + verify_key_in_proto(transaction_body.consensusUpdateTopic.fee_schedule_key, expected_fee_schedule_public, key_type) + assert len(transaction_body.consensusUpdateTopic.fee_exempt_key_list.keys) == 2 + verify_key_in_proto( + transaction_body.consensusUpdateTopic.fee_exempt_key_list.keys[0], expected_fee_exempt_publics[0], key_type + ) + verify_key_in_proto( + transaction_body.consensusUpdateTopic.fee_exempt_key_list.keys[1], expected_fee_exempt_publics[1], key_type + ) + assert len(transaction_body.consensusUpdateTopic.custom_fees.fees) == 1 + + +@pytest.mark.parametrize( + "key_type,use_private", + [ + ("ed25519", True), + ("ed25519", False), + ("ecdsa", True), + ("ecdsa", False), + ], +) +def test_build_scheduled_topic_update_body_with_all_key_types(topic_id, key_type, use_private): + """Test building scheduled body for TopicUpdateTransaction with different key types.""" + admin_key = create_key(key_type, use_private) + submit_key = create_key(key_type, use_private) + fee_schedule_key = create_key(key_type, use_private) + fee_exempt_keys = [ + create_key(key_type, use_private), + create_key(key_type, use_private), + ] + + expected_admin_public = get_expected_public_key(admin_key) + expected_submit_public = get_expected_public_key(submit_key) + expected_fee_schedule_public = get_expected_public_key(fee_schedule_key) + expected_fee_exempt_publics = [get_expected_public_key(key) for key in fee_exempt_keys] + + tx = TopicUpdateTransaction() + tx.set_topic_id(topic_id) + tx.set_memo("Scheduled Topic Update") + tx.set_admin_key(admin_key) + tx.set_submit_key(submit_key) + tx.set_auto_renew_period(Duration(8000000)) + tx.set_auto_renew_account(AccountId(0, 0, 9876)) + tx.set_custom_fees([CustomFixedFee(1000, fee_collector_account_id=AccountId(0, 0, 9876))]) + tx.set_fee_schedule_key(fee_schedule_key) + tx.set_fee_exempt_keys(fee_exempt_keys) + + schedulable_body = tx.build_scheduled_body() + + assert isinstance(schedulable_body, SchedulableTransactionBody) + assert schedulable_body.HasField("consensusUpdateTopic") + assert schedulable_body.consensusUpdateTopic.topicID.topicNum == 1234 + assert schedulable_body.consensusUpdateTopic.memo.value == "Scheduled Topic Update" + verify_key_in_proto(schedulable_body.consensusUpdateTopic.adminKey, expected_admin_public, key_type) + verify_key_in_proto(schedulable_body.consensusUpdateTopic.submitKey, expected_submit_public, key_type) + verify_key_in_proto(schedulable_body.consensusUpdateTopic.fee_schedule_key, expected_fee_schedule_public, key_type) + assert len(schedulable_body.consensusUpdateTopic.fee_exempt_key_list.keys) == 2 + verify_key_in_proto( + schedulable_body.consensusUpdateTopic.fee_exempt_key_list.keys[0], expected_fee_exempt_publics[0], key_type + ) + verify_key_in_proto( + schedulable_body.consensusUpdateTopic.fee_exempt_key_list.keys[1], expected_fee_exempt_publics[1], key_type + ) + assert len(schedulable_body.consensusUpdateTopic.custom_fees.fees) == 1 + + def test_build_topic_update_transaction_body(mock_account_ids, topic_id): """Test building a TopicUpdateTransaction body with valid topic ID and memo.""" _, _, node_account_id, _, _ = mock_account_ids