Skip to content

Commit 7d14c4a

Browse files
authored
feat: Allow PublicKey for TokenCreateTransaction keys (hiero-ledger#754)
Signed-off-by: Adityarya11 <arya050411@gmail.com>
1 parent 7a590fa commit 7d14c4a

File tree

4 files changed

+201
-40
lines changed

4 files changed

+201
-40
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
88

99

1010
### Added
11+
1112
- Add `examples/topic_id.py` to demonstrate `TopicId` opeartions
1213
- Add `examples/topic_message.py` to demonstrate `TopicMessage` and `TopicMessageChunk` with local mock data.
1314
- Added missing validation logic `fee_schedule_key` in integration `token_create_transaction_e2e_test.py` and ``token_update_transaction_e2e_test.py`.
1415
- Add `account_balance_query.py` example to demonstrate how to use the CryptoGetAccountBalanceQuery class.
1516
- Add `examples/token_create_transaction_admin_key.py` demonstrating admin key privileges for token management including token updates, key changes, and deletion (#798)
1617
- Add `examples/account_info.py` to demonstrate `AccountInfo` opeartions
1718
- Added `HbarUnit` class and Extend `Hbar` class to handle floating-point numbers
19+
- feat: Allow `PrivateKey` to be used for keys in `TopicCreateTransaction` for consistency.
1820

1921

2022
### Changed

src/hiero_sdk_python/tokens/token_create_transaction.py

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"""
1313

1414
from dataclasses import dataclass, field
15-
from typing import Optional, Any, List
15+
from typing import Optional, Any, List, Union
1616

1717
from hiero_sdk_python.Duration import Duration
1818
from hiero_sdk_python.channels import _Channel
@@ -27,11 +27,14 @@
2727
from hiero_sdk_python.tokens.supply_type import SupplyType
2828
from hiero_sdk_python.account.account_id import AccountId
2929
from hiero_sdk_python.crypto.private_key import PrivateKey
30+
from hiero_sdk_python.crypto.public_key import PublicKey
3031
from hiero_sdk_python.tokens.custom_fee import CustomFee
3132

3233
AUTO_RENEW_PERIOD = Duration(7890000) # around 90 days in seconds
3334
DEFAULT_TRANSACTION_FEE = 3_000_000_000
3435

36+
Key = Union[PrivateKey, PublicKey]
37+
3538
@dataclass
3639
class TokenParams:
3740
"""
@@ -81,14 +84,14 @@ class TokenKeys:
8184
kyc_key: The KYC key for the token to grant KYC to an account.
8285
"""
8386

84-
admin_key: Optional[PrivateKey] = None
85-
supply_key: Optional[PrivateKey] = None
86-
freeze_key: Optional[PrivateKey] = None
87-
wipe_key: Optional[PrivateKey] = None
88-
metadata_key: Optional[PrivateKey] = None
89-
pause_key: Optional[PrivateKey] = None
90-
kyc_key: Optional[PrivateKey] = None
91-
fee_schedule_key: Optional[PrivateKey] = None
87+
admin_key: Optional[Key] = None
88+
supply_key: Optional[Key] = None
89+
freeze_key: Optional[Key] = None
90+
wipe_key: Optional[Key] = None
91+
metadata_key: Optional[Key] = None
92+
pause_key: Optional[Key] = None
93+
kyc_key: Optional[Key] = None
94+
fee_schedule_key: Optional[Key] = None
9295

9396
class TokenCreateValidator:
9497
"""Token, key and freeze checks for creating a token as per the proto"""
@@ -368,43 +371,43 @@ def set_memo(self, memo: str) -> "TokenCreateTransaction":
368371
self._token_params.memo = memo
369372
return self
370373

371-
def set_admin_key(self, key: PrivateKey) -> "TokenCreateTransaction":
374+
def set_admin_key(self, key: Key) -> "TokenCreateTransaction":
372375
""" Sets the admin key for the token, which allows updating and deleting the token."""
373376
self._require_not_frozen()
374377
self._keys.admin_key = key
375378
return self
376379

377-
def set_supply_key(self, key: PrivateKey) -> "TokenCreateTransaction":
380+
def set_supply_key(self, key: Key) -> "TokenCreateTransaction":
378381
""" Sets the supply key for the token, which allows minting and burning tokens."""
379382
self._require_not_frozen()
380383
self._keys.supply_key = key
381384
return self
382385

383-
def set_freeze_key(self, key: PrivateKey) -> "TokenCreateTransaction":
386+
def set_freeze_key(self, key: Key) -> "TokenCreateTransaction":
384387
""" Sets the freeze key for the token, which allows freezing and unfreezing accounts."""
385388
self._require_not_frozen()
386389
self._keys.freeze_key = key
387390
return self
388391

389-
def set_wipe_key(self, key: PrivateKey) -> "TokenCreateTransaction":
392+
def set_wipe_key(self, key: Key) -> "TokenCreateTransaction":
390393
""" Sets the wipe key for the token, which allows wiping tokens from an account."""
391394
self._require_not_frozen()
392395
self._keys.wipe_key = key
393396
return self
394397

395-
def set_metadata_key(self, key: PrivateKey) -> "TokenCreateTransaction":
398+
def set_metadata_key(self, key: Key) -> "TokenCreateTransaction":
396399
""" Sets the metadata key for the token, which allows updating NFT metadata."""
397400
self._require_not_frozen()
398401
self._keys.metadata_key = key
399402
return self
400403

401-
def set_pause_key(self, key: PrivateKey) -> "TokenCreateTransaction":
404+
def set_pause_key(self, key: Key) -> "TokenCreateTransaction":
402405
""" Sets the pause key for the token, which allows pausing and unpausing the token."""
403406
self._require_not_frozen()
404407
self._keys.pause_key = key
405408
return self
406409

407-
def set_kyc_key(self, key: PrivateKey) -> "TokenCreateTransaction":
410+
def set_kyc_key(self, key: Key) -> "TokenCreateTransaction":
408411
""" Sets the KYC key for the token, which allows granting KYC to an account."""
409412
self._require_not_frozen()
410413
self._keys.kyc_key = key
@@ -416,26 +419,43 @@ def set_custom_fees(self, custom_fees: List[CustomFee]) -> "TokenCreateTransacti
416419
self._token_params.custom_fees = custom_fees
417420
return self
418421

419-
def set_fee_schedule_key(self, key: PrivateKey) -> "TokenCreateTransaction":
422+
def set_fee_schedule_key(self, key: Key) -> "TokenCreateTransaction":
420423
"""Sets the fee schedule key for the token."""
421424
self._require_not_frozen()
422425
self._keys.fee_schedule_key = key
423426
return self
424427

425-
def _to_proto_key(self, private_key: Optional[PrivateKey]) -> Optional[basic_types_pb2.Key]:
428+
def _to_proto_key(self, key: Optional[Key]) -> Optional[basic_types_pb2.Key]:
426429
"""
427-
Helper method to convert a private key to protobuf Key format.
430+
Helper method to convert a PrivateKey or PublicKey to the protobuf Key format.
431+
432+
This ensures only public keys are serialized:
433+
- If a PublicKey is provided, it is used directly.
434+
- If a PrivateKey is provided, its corresponding public key is extracted and used.
428435
429436
Args:
430-
private_key (PrivateKey, Optional): The private key to convert, or None
437+
key (Key, Optional): The PrivateKey or PublicKey to convert.
431438
432439
Returns:
433-
basic_types_pb2.Key (Optional): The protobuf key or None if private_key is None
440+
basic_types_pb2.Key (Optional): The protobuf key, or None.
441+
442+
Raises:
443+
TypeError: If the provided key is not a PrivateKey, PublicKey, or None.
434444
"""
435-
if not private_key:
445+
if not key:
436446
return None
437447

438-
return private_key.public_key()._to_proto()
448+
# If it's a PrivateKey, get its public key first
449+
if isinstance(key, PrivateKey):
450+
return key.public_key()._to_proto()
451+
452+
# If it's already a PublicKey, just convert it
453+
if isinstance(key, PublicKey):
454+
return key._to_proto()
455+
456+
# Safety net: This will fail if a non-key is passed
457+
raise TypeError("Key must be of type PrivateKey or PublicKey")
458+
439459

440460
def freeze_with(self, client) -> "TokenCreateTransaction":
441461
"""

tests/integration/token_create_transaction_e2e_test.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from hiero_sdk_python.response_code import ResponseCode
77
from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee
88
from hiero_sdk_python.tokens.token_fee_schedule_update_transaction import TokenFeeScheduleUpdateTransaction
9+
from hiero_sdk_python.crypto.public_key import PublicKey
10+
from hiero_sdk_python.transaction.transaction import Transaction
911
from hiero_sdk_python.tokens.token_type import TokenType
1012
from hiero_sdk_python.query.token_info_query import TokenInfoQuery
1113
from hiero_sdk_python.timestamp import Timestamp
@@ -165,3 +167,72 @@ def test_fungible_token_create_with_fee_schedule_key():
165167

166168
finally:
167169
env.close()
170+
171+
@pytest.mark.integration
172+
def test_token_create_non_custodial_flow():
173+
"""
174+
Tests the full non-custodial flow:
175+
1. Operator builds a TX using only a PublicKey.
176+
2. Operator gets the transaction bytes.
177+
3. User (with the PrivateKey) signs the bytes.
178+
4. Operator executes the signed transaction.
179+
"""
180+
181+
env = IntegrationTestEnv()
182+
client = env.client
183+
184+
try:
185+
# 1. SETUP: Create a new key pair for the "user"
186+
user_private_key = PrivateKey.generate_ed25519()
187+
user_public_key = user_private_key.public_key()
188+
189+
# =================================================================
190+
# STEP 1 & 2: OPERATOR (CLIENT) BUILDS THE TRANSACTION
191+
# =================================================================
192+
193+
tx = (
194+
TokenCreateTransaction()
195+
.set_token_name("NonCustodialToken")
196+
.set_token_symbol("NCT")
197+
.set_token_type(TokenType.FUNGIBLE_COMMON)
198+
.set_treasury_account_id(client.operator_account_id)
199+
.set_initial_supply(100)
200+
.set_admin_key(user_public_key) # <-- The new feature!
201+
.freeze_with(client)
202+
)
203+
204+
tx_bytes = tx.to_bytes()
205+
206+
# =================================================================
207+
# STEP 3: USER (SIGNER) SIGNS THE TRANSACTION
208+
# =================================================================
209+
210+
tx_from_bytes = Transaction.from_bytes(tx_bytes)
211+
tx_from_bytes.sign(user_private_key)
212+
213+
# =================================================================
214+
# STEP 4: OPERATOR (CLIENT) EXECUTES THE SIGNED TX
215+
# =================================================================
216+
217+
receipt = tx_from_bytes.execute(client)
218+
219+
assert receipt is not None
220+
token_id = receipt.token_id
221+
assert token_id is not None
222+
223+
# PROOF: Query the new token and check if the admin key matches
224+
token_info = TokenInfoQuery(token_id=token_id).execute(client)
225+
226+
assert token_info.admin_key is not None
227+
228+
# This is the STRONG assertion:
229+
# Compare the bytes of the key from the network
230+
# with the bytes of the key we originally used.
231+
admin_key_bytes = token_info.admin_key.to_bytes_raw()
232+
public_key_bytes = user_public_key.to_bytes_raw()
233+
234+
assert admin_key_bytes == public_key_bytes
235+
236+
finally:
237+
# Clean up the environment
238+
env.close()

0 commit comments

Comments
 (0)