Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 29 additions & 25 deletions src/hiero_sdk_python/consensus/topic_update_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -30,39 +31,42 @@ 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,
Comment thread
oGranny marked this conversation as resolved.
) -> None:
"""
Initializes a new instance of the TopicUpdateTransaction class.

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:
"""
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -253,23 +257,23 @@ 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
),
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,
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,
)

Expand Down
178 changes: 177 additions & 1 deletion tests/unit/topic_update_transaction_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
Comment thread
coderabbitai[bot] marked this conversation as resolved.


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

Comment thread
oGranny marked this conversation as resolved.

@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

Comment thread
oGranny marked this conversation as resolved.

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
Expand Down
Loading