Skip to content

Commit 34d9e8a

Browse files
authored
fix: update TopicUpdateTransaction to use Key instead of PublicKey (hiero-ledger#2241)
Signed-off-by: oGranny <ogranny.github.io@gmail.com>
1 parent 79b34a7 commit 34d9e8a

2 files changed

Lines changed: 206 additions & 26 deletions

File tree

src/hiero_sdk_python/consensus/topic_update_transaction.py

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from hiero_sdk_python.account.account_id import AccountId
1111
from hiero_sdk_python.channels import _Channel
1212
from hiero_sdk_python.consensus.topic_id import TopicId
13-
from hiero_sdk_python.crypto.public_key import PublicKey
13+
from hiero_sdk_python.crypto.key import Key
1414
from hiero_sdk_python.Duration import Duration
1515
from hiero_sdk_python.executable import _Method
1616
from hiero_sdk_python.hapi.services import consensus_update_topic_pb2, duration_pb2, timestamp_pb2, transaction_pb2
@@ -21,6 +21,7 @@
2121
from hiero_sdk_python.timestamp import Timestamp
2222
from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee
2323
from hiero_sdk_python.transaction.transaction import Transaction
24+
from hiero_sdk_python.utils.key_utils import key_to_proto
2425

2526

2627
class TopicUpdateTransaction(Transaction):
@@ -30,39 +31,42 @@ def __init__(
3031
self,
3132
topic_id: TopicId | None = None,
3233
memo: str | None = None,
33-
admin_key: PublicKey | None = None,
34-
submit_key: PublicKey | None = None,
34+
admin_key: Key | None = None,
35+
submit_key: Key | None = None,
3536
auto_renew_period: Duration | None = Duration(7890000),
3637
auto_renew_account: AccountId | None = None,
3738
expiration_time: Timestamp | None = None,
3839
custom_fees: list[CustomFixedFee] | None = None,
39-
fee_schedule_key: PublicKey | None = None,
40-
fee_exempt_keys: list[PublicKey] | None = None,
40+
fee_schedule_key: Key | None = None,
41+
fee_exempt_keys: list[Key] | None = None,
4142
) -> None:
4243
"""
4344
Initializes a new instance of the TopicUpdateTransaction class.
4445
4546
Args:
4647
topic_id (TopicId): The ID of the topic to update.
4748
memo (str): The memo associated with the topic.
48-
admin_key (PublicKey): The admin key for the topic.
49-
submit_key (PublicKey): The submit key for the topic.
49+
admin_key (Key): The admin key for the topic.
50+
submit_key (Key): The submit key for the topic.
5051
auto_renew_period (Duration): The auto-renew period for the topic.
5152
auto_renew_account (AccountId): The account ID for auto-renewal.
5253
expiration_time (Timestamp): The expiration time of the topic.
54+
custom_fees (list[CustomFixedFee]): A list of custom fees to set for the topic.
55+
fee_schedule_key (Key): The fee schedule key for the topic.
56+
fee_exempt_keys (list[Key]): A list of fee exempt keys for the topic
5357
"""
5458
super().__init__()
5559
self.topic_id: TopicId | None = topic_id
5660
self.memo: str = memo or ""
57-
self.admin_key: PublicKey | None = admin_key
58-
self.submit_key: PublicKey | None = submit_key
61+
self.admin_key: Key | None = admin_key
62+
self.submit_key: Key | None = submit_key
5963
self.auto_renew_period: Duration | None = auto_renew_period
6064
self.auto_renew_account: AccountId | None = auto_renew_account
6165
self.expiration_time: Timestamp | None = expiration_time
6266
self.transaction_fee: int = 10_000_000
6367
self.custom_fees: list[CustomFixedFee] | None = custom_fees
64-
self.fee_schedule_key: PublicKey | None = fee_schedule_key
65-
self.fee_exempt_keys: list[PublicKey] | None = fee_exempt_keys
68+
self.fee_schedule_key: Key | None = fee_schedule_key
69+
self.fee_exempt_keys: list[Key] | None = fee_exempt_keys
6670

6771
def set_topic_id(self, topic_id: TopicId) -> TopicUpdateTransaction:
6872
"""
@@ -92,12 +96,12 @@ def set_memo(self, memo: str) -> TopicUpdateTransaction:
9296
self.memo = memo
9397
return self
9498

95-
def set_admin_key(self, key: PublicKey) -> TopicUpdateTransaction:
99+
def set_admin_key(self, key: Key) -> TopicUpdateTransaction:
96100
"""
97-
Sets the public admin key for the topic.
101+
Sets the admin key for the topic.
98102
99103
Args:
100-
Publickey: The admin key to set.
104+
key: The admin key to set.
101105
102106
Returns:
103107
TopicUpdateTransaction: Returns the instance for method chaining.
@@ -106,12 +110,12 @@ def set_admin_key(self, key: PublicKey) -> TopicUpdateTransaction:
106110
self.admin_key = key
107111
return self
108112

109-
def set_submit_key(self, key: PublicKey) -> TopicUpdateTransaction:
113+
def set_submit_key(self, key: Key) -> TopicUpdateTransaction:
110114
"""
111-
Sets the public submit key for the topic.
115+
Sets the submit key for the topic.
112116
113117
Args:
114-
Publickey: The submit key to set.
118+
key: The submit key to set.
115119
116120
Returns:
117121
TopicUpdateTransaction: Returns the instance for method chaining.
@@ -181,12 +185,12 @@ def set_custom_fees(self, custom_fees: list[CustomFixedFee]) -> TopicUpdateTrans
181185
self.custom_fees = custom_fees
182186
return self
183187

184-
def set_fee_schedule_key(self, key: PublicKey) -> TopicUpdateTransaction:
188+
def set_fee_schedule_key(self, key: Key) -> TopicUpdateTransaction:
185189
"""
186190
Sets the fee schedule key for the topic update transaction.
187191
188192
Args:
189-
key (PublicKey): The fee schedule key to set for the topic.
193+
key (Key): The fee schedule key to set for the topic.
190194
191195
Returns:
192196
TopicUpdateTransaction: The current instance for method chaining.
@@ -195,12 +199,12 @@ def set_fee_schedule_key(self, key: PublicKey) -> TopicUpdateTransaction:
195199
self.fee_schedule_key = key
196200
return self
197201

198-
def set_fee_exempt_keys(self, keys: list[PublicKey]) -> TopicUpdateTransaction:
202+
def set_fee_exempt_keys(self, keys: list[Key]) -> TopicUpdateTransaction:
199203
"""
200204
Sets the fee exempt keys for the topic update transaction.
201205
202206
Args:
203-
keys (list[PublicKey]): The fee exempt keys to set for the topic.
207+
keys (list[Key]): The fee exempt keys to set for the topic.
204208
205209
Returns:
206210
TopicUpdateTransaction: The current instance for method chaining.
@@ -253,23 +257,23 @@ def _build_proto_body(self) -> consensus_update_topic_pb2.ConsensusUpdateTopicTr
253257
)
254258

255259
fee_exempt_key_list = (
256-
FeeExemptKeyList(keys=[key._to_proto() for key in self.fee_exempt_keys])
260+
FeeExemptKeyList(keys=[key_to_proto(key) for key in self.fee_exempt_keys])
257261
if self.fee_exempt_keys is not None
258262
else None
259263
)
260264

261265
return consensus_update_topic_pb2.ConsensusUpdateTopicTransactionBody(
262266
topicID=self.topic_id._to_proto(),
263-
adminKey=self.admin_key._to_proto() if self.admin_key else None,
264-
submitKey=self.submit_key._to_proto() if self.submit_key else None,
267+
adminKey=key_to_proto(self.admin_key) if self.admin_key else None,
268+
submitKey=key_to_proto(self.submit_key) if self.submit_key else None,
265269
autoRenewPeriod=(
266270
duration_pb2.Duration(seconds=self.auto_renew_period.seconds) if self.auto_renew_period else None
267271
),
268272
autoRenewAccount=(self.auto_renew_account._to_proto() if self.auto_renew_account else None),
269273
expirationTime=self.expiration_time._to_protobuf() if self.expiration_time else None,
270274
memo=_wrappers_pb2.StringValue(value=self.memo) if self.memo is not None else None,
271275
custom_fees=custom_fees,
272-
fee_schedule_key=self.fee_schedule_key._to_proto() if self.fee_schedule_key else None,
276+
fee_schedule_key=key_to_proto(self.fee_schedule_key) if self.fee_schedule_key else None,
273277
fee_exempt_key_list=fee_exempt_key_list,
274278
)
275279

tests/unit/topic_update_transaction_test.py

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
from hiero_sdk_python.account.account_id import AccountId
88
from hiero_sdk_python.consensus.topic_update_transaction import TopicUpdateTransaction
9+
from hiero_sdk_python.crypto.key import Key
910
from hiero_sdk_python.crypto.private_key import PrivateKey
11+
from hiero_sdk_python.crypto.public_key import PublicKey
1012
from hiero_sdk_python.Duration import Duration
1113
from hiero_sdk_python.hapi.services import (
1214
response_header_pb2,
@@ -26,7 +28,181 @@
2628
pytestmark = pytest.mark.unit
2729

2830

29-
# This test uses fixtures (mock_account_ids, topic_id) as parameters
31+
def create_key(key_type: str, use_private: bool) -> PrivateKey | PublicKey:
32+
"""
33+
Create a key based on type and whether to use private or public.
34+
35+
Args:
36+
key_type: "ed25519" or "ecdsa"
37+
use_private: True for PrivateKey, False for PublicKey
38+
39+
Returns:
40+
The created key (PrivateKey or PublicKey)
41+
"""
42+
if key_type == "ed25519":
43+
private_key = PrivateKey.generate_ed25519()
44+
elif key_type == "ecdsa":
45+
private_key = PrivateKey.generate("ecdsa")
46+
else:
47+
raise ValueError(f"Unsupported key_type: {key_type!r}")
48+
return private_key if use_private else private_key.public_key()
49+
50+
51+
def get_expected_public_key(key: Key) -> PublicKey:
52+
"""
53+
Get the public key from either PrivateKey or PublicKey.
54+
55+
Args:
56+
key: PrivateKey or PublicKey
57+
58+
Returns:
59+
PublicKey
60+
"""
61+
return key if isinstance(key, PublicKey) else key.public_key()
62+
63+
64+
def verify_key_in_proto(proto_key, expected_public_key: PublicKey, key_type: str) -> None:
65+
"""Verify the proto key matches expected public key."""
66+
if key_type == "ed25519":
67+
assert proto_key.ed25519 == expected_public_key.to_bytes_raw()
68+
elif key_type == "ecdsa": # ecdsa
69+
assert proto_key.HasField("ECDSA_secp256k1")
70+
assert proto_key.ECDSA_secp256k1 == expected_public_key.to_bytes_raw()
71+
else:
72+
raise ValueError(f"Unsupported key_type: {key_type!r}")
73+
74+
75+
@pytest.mark.parametrize(
76+
"key_type,use_private",
77+
[
78+
("ed25519", True),
79+
("ed25519", False),
80+
("ecdsa", True),
81+
("ecdsa", False),
82+
],
83+
)
84+
def test_topic_update_setters_return_self(key_type, use_private, topic_id):
85+
"""Fluent setters must return self for the Key-typed API."""
86+
key = create_key(key_type, use_private)
87+
tx = TopicUpdateTransaction()
88+
89+
assert tx.set_topic_id(topic_id) is tx
90+
assert tx.set_admin_key(key) is tx
91+
assert tx.set_submit_key(key) is tx
92+
assert tx.set_fee_schedule_key(key) is tx
93+
assert tx.set_fee_exempt_keys([key]) is tx
94+
95+
96+
@pytest.mark.parametrize(
97+
"key_type,use_private",
98+
[
99+
("ed25519", True),
100+
("ed25519", False),
101+
("ecdsa", True),
102+
("ecdsa", False),
103+
],
104+
)
105+
def test_build_topic_update_transaction_body_with_all_key_types(mock_account_ids, topic_id, key_type, use_private):
106+
"""Test building a TopicUpdateTransaction body with different key types."""
107+
_, _, node_account_id, _, _ = mock_account_ids
108+
109+
admin_key = create_key(key_type, use_private)
110+
submit_key = create_key(key_type, use_private)
111+
fee_schedule_key = create_key(key_type, use_private)
112+
fee_exempt_keys = [
113+
create_key(key_type, use_private),
114+
create_key(key_type, use_private),
115+
]
116+
117+
expected_admin_public = get_expected_public_key(admin_key)
118+
expected_submit_public = get_expected_public_key(submit_key)
119+
expected_fee_schedule_public = get_expected_public_key(fee_schedule_key)
120+
expected_fee_exempt_publics = [get_expected_public_key(key) for key in fee_exempt_keys]
121+
122+
tx = TopicUpdateTransaction(
123+
topic_id=topic_id,
124+
memo="Updated Memo",
125+
admin_key=admin_key,
126+
submit_key=submit_key,
127+
custom_fees=[CustomFixedFee(1000, fee_collector_account_id=AccountId(0, 0, 9876))],
128+
fee_schedule_key=fee_schedule_key,
129+
fee_exempt_keys=fee_exempt_keys,
130+
)
131+
132+
tx.operator_account_id = AccountId(0, 0, 2)
133+
tx.node_account_id = node_account_id
134+
135+
transaction_body = tx.build_transaction_body()
136+
137+
assert transaction_body.consensusUpdateTopic.topicID.topicNum == 1234
138+
assert transaction_body.consensusUpdateTopic.memo.value == "Updated Memo"
139+
verify_key_in_proto(transaction_body.consensusUpdateTopic.adminKey, expected_admin_public, key_type)
140+
verify_key_in_proto(transaction_body.consensusUpdateTopic.submitKey, expected_submit_public, key_type)
141+
verify_key_in_proto(transaction_body.consensusUpdateTopic.fee_schedule_key, expected_fee_schedule_public, key_type)
142+
assert len(transaction_body.consensusUpdateTopic.fee_exempt_key_list.keys) == 2
143+
verify_key_in_proto(
144+
transaction_body.consensusUpdateTopic.fee_exempt_key_list.keys[0], expected_fee_exempt_publics[0], key_type
145+
)
146+
verify_key_in_proto(
147+
transaction_body.consensusUpdateTopic.fee_exempt_key_list.keys[1], expected_fee_exempt_publics[1], key_type
148+
)
149+
assert len(transaction_body.consensusUpdateTopic.custom_fees.fees) == 1
150+
151+
152+
@pytest.mark.parametrize(
153+
"key_type,use_private",
154+
[
155+
("ed25519", True),
156+
("ed25519", False),
157+
("ecdsa", True),
158+
("ecdsa", False),
159+
],
160+
)
161+
def test_build_scheduled_topic_update_body_with_all_key_types(topic_id, key_type, use_private):
162+
"""Test building scheduled body for TopicUpdateTransaction with different key types."""
163+
admin_key = create_key(key_type, use_private)
164+
submit_key = create_key(key_type, use_private)
165+
fee_schedule_key = create_key(key_type, use_private)
166+
fee_exempt_keys = [
167+
create_key(key_type, use_private),
168+
create_key(key_type, use_private),
169+
]
170+
171+
expected_admin_public = get_expected_public_key(admin_key)
172+
expected_submit_public = get_expected_public_key(submit_key)
173+
expected_fee_schedule_public = get_expected_public_key(fee_schedule_key)
174+
expected_fee_exempt_publics = [get_expected_public_key(key) for key in fee_exempt_keys]
175+
176+
tx = TopicUpdateTransaction()
177+
tx.set_topic_id(topic_id)
178+
tx.set_memo("Scheduled Topic Update")
179+
tx.set_admin_key(admin_key)
180+
tx.set_submit_key(submit_key)
181+
tx.set_auto_renew_period(Duration(8000000))
182+
tx.set_auto_renew_account(AccountId(0, 0, 9876))
183+
tx.set_custom_fees([CustomFixedFee(1000, fee_collector_account_id=AccountId(0, 0, 9876))])
184+
tx.set_fee_schedule_key(fee_schedule_key)
185+
tx.set_fee_exempt_keys(fee_exempt_keys)
186+
187+
schedulable_body = tx.build_scheduled_body()
188+
189+
assert isinstance(schedulable_body, SchedulableTransactionBody)
190+
assert schedulable_body.HasField("consensusUpdateTopic")
191+
assert schedulable_body.consensusUpdateTopic.topicID.topicNum == 1234
192+
assert schedulable_body.consensusUpdateTopic.memo.value == "Scheduled Topic Update"
193+
verify_key_in_proto(schedulable_body.consensusUpdateTopic.adminKey, expected_admin_public, key_type)
194+
verify_key_in_proto(schedulable_body.consensusUpdateTopic.submitKey, expected_submit_public, key_type)
195+
verify_key_in_proto(schedulable_body.consensusUpdateTopic.fee_schedule_key, expected_fee_schedule_public, key_type)
196+
assert len(schedulable_body.consensusUpdateTopic.fee_exempt_key_list.keys) == 2
197+
verify_key_in_proto(
198+
schedulable_body.consensusUpdateTopic.fee_exempt_key_list.keys[0], expected_fee_exempt_publics[0], key_type
199+
)
200+
verify_key_in_proto(
201+
schedulable_body.consensusUpdateTopic.fee_exempt_key_list.keys[1], expected_fee_exempt_publics[1], key_type
202+
)
203+
assert len(schedulable_body.consensusUpdateTopic.custom_fees.fees) == 1
204+
205+
30206
def test_build_topic_update_transaction_body(mock_account_ids, topic_id):
31207
"""Test building a TopicUpdateTransaction body with valid topic ID and memo."""
32208
_, _, node_account_id, _, _ = mock_account_ids

0 commit comments

Comments
 (0)