Skip to content

Commit fd4f8ac

Browse files
authored
feat: Allow PublicKey for TokenUpdateKeys in TokenUpdateTransaction (#946)
Signed-off-by: emiliyank <e.kadiyski@gmail.com>
1 parent 0eeae95 commit fd4f8ac

File tree

9 files changed

+609
-165
lines changed

9 files changed

+609
-165
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
1212
- Add examples/tokens/token_create_transaction_pause_key.py example demonstrating token pause/unpause behavior and pause key usage (#833)
1313
- Added `docs/sdk_developers/training/transaction_lifecycle.md` to explain the typical lifecycle of executing a transaction using the Hedera Python SDK.
1414
- Add inactivity bot workflow to unassign stale issue assignees (#952)
15+
1516
### Changed
17+
- Allow `PublicKey` for `TokenUpdateKeys` in `TokenUpdateTransaction`, enabling non-custodial workflows where operators can build transactions using only public keys (#934).
1618

1719
### Fixed
1820
- Fixed inactivity bot workflow not checking out repository before running (#964)

src/hiero_sdk_python/consensus/topic_create_transaction.py

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@
2121
from hiero_sdk_python.channels import _Channel
2222
from hiero_sdk_python.executable import _Method
2323
from hiero_sdk_python.account.account_id import AccountId
24-
from hiero_sdk_python.crypto.private_key import PrivateKey
25-
from hiero_sdk_python.crypto.public_key import PublicKey
26-
27-
Key = Union[PrivateKey, PublicKey]
24+
from hiero_sdk_python.utils.key_utils import Key, key_to_proto
2825

2926

3027
class TopicCreateTransaction(Transaction):
@@ -174,25 +171,16 @@ def set_fee_exempt_keys(self, keys: List[Key]) -> "TopicCreateTransaction":
174171
self.fee_exempt_keys = keys
175172
return self
176173

177-
def _to_proto_key(self, key: Optional[Key]) -> Optional[basic_types_pb2.Key]:
174+
def _to_proto_key(self, key: Optional[Key]):
178175
"""
179-
Helper method to convert a key (PrivateKey or PublicKey) to protobuf Key format.
176+
Backwards-compatible wrapper around `key_to_proto` for converting SDK keys
177+
(PrivateKey or PublicKey) to protobuf `Key` messages.
180178
181-
Args:
182-
key (Optional[Key]): The key to convert (PrivateKey or PublicKey), or None
183-
184-
Returns:
185-
basic_types_pb2.Key (Optional): The protobuf key or None if key is None
179+
This exists so that existing unit tests and any external callers that rely
180+
on `_to_proto_key` continue to work after centralizing the logic in
181+
`hiero_sdk_python.utils.key_utils.key_to_proto`.
186182
"""
187-
if not key:
188-
return None
189-
190-
# If it's a PrivateKey, get the public key first, then convert to proto
191-
if isinstance(key, PrivateKey):
192-
return key.public_key()._to_proto()
193-
194-
# If it's a PublicKey, convert directly to proto
195-
return key._to_proto()
183+
return key_to_proto(key)
196184

197185
def _build_proto_body(self) -> consensus_create_topic_pb2.ConsensusCreateTopicTransactionBody:
198186
"""
@@ -202,8 +190,8 @@ def _build_proto_body(self) -> consensus_create_topic_pb2.ConsensusCreateTopicTr
202190
ConsensusCreateTopicTransactionBody: The protobuf body for this transaction.
203191
"""
204192
return consensus_create_topic_pb2.ConsensusCreateTopicTransactionBody(
205-
adminKey=self._to_proto_key(self.admin_key),
206-
submitKey=self._to_proto_key(self.submit_key),
193+
adminKey=key_to_proto(self.admin_key),
194+
submitKey=key_to_proto(self.submit_key),
207195
autoRenewPeriod=(
208196
self.auto_renew_period._to_proto()
209197
if self.auto_renew_period is not None
@@ -214,8 +202,8 @@ def _build_proto_body(self) -> consensus_create_topic_pb2.ConsensusCreateTopicTr
214202
else None),
215203
memo=self.memo,
216204
custom_fees=[custom_fee._to_topic_fee_proto() for custom_fee in self.custom_fees],
217-
fee_schedule_key=self._to_proto_key(self.fee_schedule_key),
218-
fee_exempt_key_list=[self._to_proto_key(key) for key in self.fee_exempt_keys],
205+
fee_schedule_key=key_to_proto(self.fee_schedule_key),
206+
fee_exempt_key_list=[key_to_proto(key) for key in self.fee_exempt_keys],
219207
)
220208

221209
def build_transaction_body(self) -> transaction_pb2.TransactionBody:

src/hiero_sdk_python/tokens/token_create_transaction.py

Lines changed: 20 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,12 @@
2626
from hiero_sdk_python.tokens.token_type import TokenType
2727
from hiero_sdk_python.tokens.supply_type import SupplyType
2828
from hiero_sdk_python.account.account_id import AccountId
29-
from hiero_sdk_python.crypto.private_key import PrivateKey
30-
from hiero_sdk_python.crypto.public_key import PublicKey
3129
from hiero_sdk_python.tokens.custom_fee import CustomFee
30+
from hiero_sdk_python.utils.key_utils import Key, key_to_proto
3231

3332
AUTO_RENEW_PERIOD = Duration(7890000) # around 90 days in seconds
3433
DEFAULT_TRANSACTION_FEE = 3_000_000_000
3534

36-
Key = Union[PrivateKey, PublicKey]
37-
3835
@dataclass
3936
class TokenParams:
4037
"""
@@ -446,38 +443,6 @@ def set_metadata(self, metadata: bytes | str) -> "TokenCreateTransaction":
446443
self._token_params.metadata = metadata
447444
return self
448445

449-
def _to_proto_key(self, key: Optional[Key]) -> Optional[basic_types_pb2.Key]:
450-
"""
451-
Helper method to convert a PrivateKey or PublicKey to the protobuf Key format.
452-
453-
This ensures only public keys are serialized:
454-
- If a PublicKey is provided, it is used directly.
455-
- If a PrivateKey is provided, its corresponding public key is extracted and used.
456-
457-
Args:
458-
key (Key, Optional): The PrivateKey or PublicKey to convert.
459-
460-
Returns:
461-
basic_types_pb2.Key (Optional): The protobuf key, or None.
462-
463-
Raises:
464-
TypeError: If the provided key is not a PrivateKey, PublicKey, or None.
465-
"""
466-
if not key:
467-
return None
468-
469-
# If it's a PrivateKey, get its public key first
470-
if isinstance(key, PrivateKey):
471-
return key.public_key()._to_proto()
472-
473-
# If it's already a PublicKey, just convert it
474-
if isinstance(key, PublicKey):
475-
return key._to_proto()
476-
477-
# Safety net: This will fail if a non-key is passed
478-
raise TypeError("Key must be of type PrivateKey or PublicKey")
479-
480-
481446
def freeze_with(self, client) -> "TokenCreateTransaction":
482447
"""
483448
Freeze the transaction with the given client.
@@ -501,6 +466,17 @@ def freeze_with(self, client) -> "TokenCreateTransaction":
501466
return super().freeze_with(client)
502467

503468

469+
def _to_proto_key(self, key: Optional[Key]):
470+
"""
471+
Backwards-compatible wrapper around `key_to_proto` for converting SDK keys
472+
(PrivateKey or PublicKey) to protobuf `Key` messages.
473+
474+
This exists so that existing unit tests and any external callers that rely
475+
on `_to_proto_key` continue to work after centralizing the logic in
476+
`hiero_sdk_python.utils.key_utils.key_to_proto`.
477+
"""
478+
return key_to_proto(key)
479+
504480
def _build_proto_body(self) -> token_create_pb2.TokenCreateTransactionBody:
505481
"""
506482
Returns the protobuf body for the token create transaction.
@@ -518,14 +494,14 @@ def _build_proto_body(self) -> token_create_pb2.TokenCreateTransactionBody:
518494
TokenCreateValidator._validate_token_freeze_status(self._keys, self._token_params)
519495

520496
# Convert keys
521-
admin_key_proto = self._to_proto_key(self._keys.admin_key)
522-
supply_key_proto = self._to_proto_key(self._keys.supply_key)
523-
freeze_key_proto = self._to_proto_key(self._keys.freeze_key)
524-
wipe_key_proto = self._to_proto_key(self._keys.wipe_key)
525-
metadata_key_proto = self._to_proto_key(self._keys.metadata_key)
526-
pause_key_proto = self._to_proto_key(self._keys.pause_key)
527-
kyc_key_proto = self._to_proto_key(self._keys.kyc_key)
528-
fee_schedules_key_proto = self._to_proto_key(self._keys.fee_schedule_key);
497+
admin_key_proto = key_to_proto(self._keys.admin_key)
498+
supply_key_proto = key_to_proto(self._keys.supply_key)
499+
freeze_key_proto = key_to_proto(self._keys.freeze_key)
500+
wipe_key_proto = key_to_proto(self._keys.wipe_key)
501+
metadata_key_proto = key_to_proto(self._keys.metadata_key)
502+
pause_key_proto = key_to_proto(self._keys.pause_key)
503+
kyc_key_proto = key_to_proto(self._keys.kyc_key)
504+
fee_schedules_key_proto = key_to_proto(self._keys.fee_schedule_key)
529505

530506
# Resolve enum values with defaults
531507
token_type_value = (

src/hiero_sdk_python/tokens/token_update_transaction.py

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from google.protobuf.wrappers_pb2 import (BytesValue, StringValue)
1111

1212
from hiero_sdk_python.Duration import Duration
13-
from hiero_sdk_python.crypto.private_key import PrivateKey
1413
from hiero_sdk_python.hbar import Hbar
1514
from hiero_sdk_python.timestamp import Timestamp
1615
from hiero_sdk_python.tokens.token_id import TokenId
@@ -23,6 +22,7 @@
2322
SchedulableTransactionBody,
2423
)
2524
from hiero_sdk_python.hapi.services import token_update_pb2, transaction_pb2
25+
from hiero_sdk_python.utils.key_utils import Key, key_to_proto
2626

2727
@dataclass
2828
class TokenUpdateParams:
@@ -59,14 +59,14 @@ class TokenUpdateKeys:
5959
metadata_key: The new metadata key for the token.
6060
pause_key: The new pause key for the token.
6161
"""
62-
admin_key: Optional[PrivateKey] = None
63-
supply_key: Optional[PrivateKey] = None
64-
freeze_key: Optional[PrivateKey] = None
65-
wipe_key: Optional[PrivateKey] = None
66-
metadata_key: Optional[PrivateKey] = None
67-
pause_key: Optional[PrivateKey] = None
68-
kyc_key: Optional[PrivateKey] = None
69-
fee_schedule_key: Optional[PrivateKey] = None
62+
admin_key: Optional[Key] = None
63+
supply_key: Optional[Key] = None
64+
freeze_key: Optional[Key] = None
65+
wipe_key: Optional[Key] = None
66+
metadata_key: Optional[Key] = None
67+
pause_key: Optional[Key] = None
68+
kyc_key: Optional[Key] = None
69+
fee_schedule_key: Optional[Key] = None
7070

7171

7272
class TokenUpdateTransaction(Transaction):
@@ -124,14 +124,14 @@ def __init__(
124124

125125
# Initialize keys attributes
126126
keys: TokenUpdateKeys = token_keys or TokenUpdateKeys()
127-
self.admin_key: Optional[PrivateKey] = keys.admin_key
128-
self.freeze_key: Optional[PrivateKey] = keys.freeze_key
129-
self.wipe_key: Optional[PrivateKey] = keys.wipe_key
130-
self.supply_key: Optional[PrivateKey] = keys.supply_key
131-
self.pause_key: Optional[PrivateKey] = keys.pause_key
132-
self.metadata_key: Optional[PrivateKey] = keys.metadata_key
133-
self.kyc_key: Optional[PrivateKey] = keys.kyc_key
134-
self.fee_schedule_key: Optional[PrivateKey] = keys.fee_schedule_key
127+
self.admin_key: Optional[Key] = keys.admin_key
128+
self.freeze_key: Optional[Key] = keys.freeze_key
129+
self.wipe_key: Optional[Key] = keys.wipe_key
130+
self.supply_key: Optional[Key] = keys.supply_key
131+
self.pause_key: Optional[Key] = keys.pause_key
132+
self.metadata_key: Optional[Key] = keys.metadata_key
133+
self.kyc_key: Optional[Key] = keys.kyc_key
134+
self.fee_schedule_key: Optional[Key] = keys.fee_schedule_key
135135

136136
self.token_key_verification_mode: TokenKeyValidation = token_key_verification_mode
137137

@@ -284,13 +284,13 @@ def set_expiration_time(self, expiration_time: Timestamp) -> "TokenUpdateTransac
284284

285285
def set_admin_key(
286286
self,
287-
admin_key: PrivateKey
287+
admin_key: Key
288288
) -> "TokenUpdateTransaction":
289289
"""
290290
Sets the new admin key for the token.
291291
292292
Args:
293-
admin_key (PrivateKey): The new admin key to set.
293+
admin_key (Key): The new admin key to set (PrivateKey or PublicKey).
294294
295295
Returns:
296296
TokenUpdateTransaction: This transaction instance.
@@ -301,13 +301,13 @@ def set_admin_key(
301301

302302
def set_freeze_key(
303303
self,
304-
freeze_key: PrivateKey
304+
freeze_key: Key
305305
) -> "TokenUpdateTransaction":
306306
"""
307307
Sets the new freeze key for the token.
308308
309309
Args:
310-
freeze_key (PrivateKey): The new freeze key to set.
310+
freeze_key (Key): The new freeze key to set (PrivateKey or PublicKey).
311311
312312
Returns:
313313
TokenUpdateTransaction: This transaction instance.
@@ -318,13 +318,13 @@ def set_freeze_key(
318318

319319
def set_wipe_key(
320320
self,
321-
wipe_key: PrivateKey
321+
wipe_key: Key
322322
) -> "TokenUpdateTransaction":
323323
"""
324324
Sets the new wipe key for the token.
325325
326326
Args:
327-
wipe_key (PrivateKey): The new wipe key to set.
327+
wipe_key (Key): The new wipe key to set (PrivateKey or PublicKey).
328328
329329
Returns:
330330
TokenUpdateTransaction: This transaction instance.
@@ -335,13 +335,13 @@ def set_wipe_key(
335335

336336
def set_supply_key(
337337
self,
338-
supply_key: PrivateKey
338+
supply_key: Key
339339
) -> "TokenUpdateTransaction":
340340
"""
341341
Sets the new supply key for the token.
342342
343343
Args:
344-
supply_key (PrivateKey): The new supply key to set.
344+
supply_key (Key): The new supply key to set (PrivateKey or PublicKey).
345345
346346
Returns:
347347
TokenUpdateTransaction: This transaction instance.
@@ -352,13 +352,13 @@ def set_supply_key(
352352

353353
def set_pause_key(
354354
self,
355-
pause_key: PrivateKey
355+
pause_key: Key
356356
) -> "TokenUpdateTransaction":
357357
"""
358358
Sets the new pause key for the token.
359359
360360
Args:
361-
pause_key (PrivateKey): The new pause key to set.
361+
pause_key (Key): The new pause key to set (PrivateKey or PublicKey).
362362
363363
Returns:
364364
TokenUpdateTransaction: This transaction instance.
@@ -369,13 +369,13 @@ def set_pause_key(
369369

370370
def set_metadata_key(
371371
self,
372-
metadata_key: PrivateKey
372+
metadata_key: Key
373373
) -> "TokenUpdateTransaction":
374374
"""
375375
Sets the new metadata key for the token.
376376
377377
Args:
378-
metadata_key (PrivateKey): The new metadata key to set.
378+
metadata_key (Key): The new metadata key to set (PrivateKey or PublicKey).
379379
380380
Returns:
381381
TokenUpdateTransaction: This transaction instance.
@@ -384,12 +384,12 @@ def set_metadata_key(
384384
self.metadata_key = metadata_key
385385
return self
386386

387-
def set_kyc_key(self, kyc_key: PrivateKey) -> "TokenUpdateTransaction":
387+
def set_kyc_key(self, kyc_key: Key) -> "TokenUpdateTransaction":
388388
"""
389389
Sets the kyc key for the token
390390
391391
Args:
392-
kyc_key (Private Key): The new kyc_key to set.
392+
kyc_key (Key): The new kyc_key to set (PrivateKey or PublicKey).
393393
394394
Returns:
395395
TokenUpdateTransaction: This transaction instance.
@@ -398,12 +398,12 @@ def set_kyc_key(self, kyc_key: PrivateKey) -> "TokenUpdateTransaction":
398398
self.kyc_key = kyc_key
399399
return self
400400

401-
def set_fee_schedule_key(self, fee_schedule_key: PrivateKey) -> "TokenUpdateTransaction":
401+
def set_fee_schedule_key(self, fee_schedule_key: Key) -> "TokenUpdateTransaction":
402402
"""
403403
Sets the fee schedule key for the token
404404
405405
Args:
406-
fee_schedule_key (Private Key): The new fee_schedule_key to set.
406+
fee_schedule_key (Key): The new fee_schedule_key to set (PrivateKey or PublicKey).
407407
408408
Returns:
409409
TokenUpdateTransaction: This transaction instance.
@@ -507,18 +507,18 @@ def _set_keys_to_proto(
507507
Sets the keys to the protobuf transaction body.
508508
"""
509509
if self.admin_key:
510-
token_update_body.adminKey.CopyFrom(self.admin_key.public_key()._to_proto())
510+
token_update_body.adminKey.CopyFrom(key_to_proto(self.admin_key))
511511
if self.freeze_key:
512-
token_update_body.freezeKey.CopyFrom(self.freeze_key.public_key()._to_proto())
512+
token_update_body.freezeKey.CopyFrom(key_to_proto(self.freeze_key))
513513
if self.wipe_key:
514-
token_update_body.wipeKey.CopyFrom(self.wipe_key.public_key()._to_proto())
514+
token_update_body.wipeKey.CopyFrom(key_to_proto(self.wipe_key))
515515
if self.supply_key:
516-
token_update_body.supplyKey.CopyFrom(self.supply_key.public_key()._to_proto())
516+
token_update_body.supplyKey.CopyFrom(key_to_proto(self.supply_key))
517517
if self.metadata_key:
518-
token_update_body.metadata_key.CopyFrom(self.metadata_key.public_key()._to_proto())
518+
token_update_body.metadata_key.CopyFrom(key_to_proto(self.metadata_key))
519519
if self.pause_key:
520-
token_update_body.pause_key.CopyFrom(self.pause_key.public_key()._to_proto())
520+
token_update_body.pause_key.CopyFrom(key_to_proto(self.pause_key))
521521
if self.kyc_key:
522-
token_update_body.kycKey.CopyFrom(self.kyc_key.public_key()._to_proto())
522+
token_update_body.kycKey.CopyFrom(key_to_proto(self.kyc_key))
523523
if self.fee_schedule_key:
524-
token_update_body.fee_schedule_key.CopyFrom(self.fee_schedule_key.public_key()._to_proto())
524+
token_update_body.fee_schedule_key.CopyFrom(key_to_proto(self.fee_schedule_key))

0 commit comments

Comments
 (0)