Skip to content

Commit 67973bf

Browse files
chore: Add admin key token example (#798) (#802)
Signed-off-by: jaideepkathiresan <[email protected]> Signed-off-by: Jaideep Vishnu Kathiresan <[email protected]>
1 parent 4a453db commit 67973bf

File tree

3 files changed

+348
-0
lines changed

3 files changed

+348
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
1212
- Add `examples/topic_message.py` to demonstrate `TopicMessage` and `TopicMessageChunk` with local mock data.
1313
- Added missing validation logic `fee_schedule_key` in integration `token_create_transaction_e2e_test.py` and ``token_update_transaction_e2e_test.py`.
1414
- Add `account_balance_query.py` example to demonstrate how to use the CryptoGetAccountBalanceQuery class.
15+
- Add `examples/token_create_transaction_admin_key.py` demonstrating admin key privileges for token management including token updates, key changes, and deletion (#798)
1516

1617

1718
### Changed
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
"""
2+
This example demonstrates the admin key privileges for token management using Hiero SDK Python.
3+
4+
It shows:
5+
1. Creating a token with an admin key
6+
2. Demonstrating admin-only operations like updating token memo and deleting the token
7+
3. Attempting to add a supply key (which fails because admin key cannot add new keys)
8+
4. Updating existing keys using admin key authorization
9+
5. Verifying operations using TokenInfoQuery
10+
11+
Required environment variables:
12+
- OPERATOR_ID, OPERATOR_KEY
13+
14+
Usage:
15+
uv run examples/token_create_transaction_admin_key.py
16+
python examples/token_create_transaction_admin_key.py
17+
"""
18+
19+
import os
20+
import sys
21+
from dotenv import load_dotenv
22+
23+
from hiero_sdk_python import (
24+
Client,
25+
AccountId,
26+
PrivateKey,
27+
Network,
28+
TokenCreateTransaction,
29+
TokenUpdateTransaction,
30+
TokenDeleteTransaction,
31+
TokenInfoQuery,
32+
)
33+
34+
from hiero_sdk_python.response_code import ResponseCode
35+
from hiero_sdk_python.tokens.token_type import TokenType
36+
from hiero_sdk_python.tokens.supply_type import SupplyType
37+
38+
load_dotenv()
39+
network_name = os.getenv('NETWORK', 'testnet').lower()
40+
41+
42+
def setup_client():
43+
"""Initialize and set up the client with operator account"""
44+
network = Network(network_name)
45+
print(f"Connecting to Hedera {network_name} network!")
46+
client = Client(network)
47+
48+
try:
49+
operator_id = AccountId.from_string(os.getenv('OPERATOR_ID', ''))
50+
operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY', ''))
51+
client.set_operator(operator_id, operator_key)
52+
print(f"Client set up with operator id {client.operator_account_id}")
53+
return client, operator_id, operator_key
54+
55+
except (TypeError, ValueError):
56+
print("Error: Please check OPERATOR_ID and OPERATOR_KEY in your .env file.")
57+
sys.exit(1)
58+
59+
60+
def generate_admin_key():
61+
"""Generate a new admin key for the token."""
62+
print("\nGenerating a new admin key for the token...")
63+
admin_key = PrivateKey.generate_ed25519()
64+
print("Admin key generated successfully.")
65+
return admin_key
66+
67+
68+
def create_token_with_admin_key(client, operator_id, operator_key, admin_key):
69+
"""
70+
Create a fungible token with only an admin key.
71+
The admin key grants privileges to update token properties and delete the token.
72+
"""
73+
print("\nCreating a fungible token with admin key...")
74+
75+
transaction = (
76+
TokenCreateTransaction()
77+
.set_token_name("Admin Key Demo Token")
78+
.set_token_symbol("AKDT")
79+
.set_decimals(2)
80+
.set_initial_supply(1000)
81+
.set_treasury_account_id(operator_id)
82+
.set_token_type(TokenType.FUNGIBLE_COMMON)
83+
.set_supply_type(SupplyType.INFINITE)
84+
.set_admin_key(admin_key) # Only admin key is set
85+
.freeze_with(client)
86+
)
87+
88+
# Sign with operator (treasury) and admin key
89+
transaction.sign(operator_key)
90+
transaction.sign(admin_key)
91+
92+
receipt = transaction.execute(client)
93+
if receipt.status != ResponseCode.SUCCESS:
94+
print(f"Token creation failed with status: {ResponseCode(receipt.status).name}")
95+
sys.exit(1)
96+
97+
token_id = receipt.token_id
98+
print(f"✅ Token created successfully with ID: {token_id}")
99+
return token_id
100+
101+
102+
def demonstrate_admin_update_memo(client, token_id, admin_key):
103+
"""Demonstrate updating token memo using admin key."""
104+
print(f"\nUpdating token memo for {token_id} using admin key...")
105+
106+
transaction = (
107+
TokenUpdateTransaction()
108+
.set_token_id(token_id)
109+
.set_token_memo("Updated by admin key")
110+
.freeze_with(client)
111+
.sign(admin_key) # Only admin key signature needed for updates
112+
)
113+
114+
receipt = transaction.execute(client)
115+
if receipt.status != ResponseCode.SUCCESS:
116+
print(f"Token update failed with status: {ResponseCode(receipt.status).name}")
117+
return False
118+
119+
print("✅ Token memo updated successfully using admin key")
120+
return True
121+
122+
123+
def demonstrate_failed_supply_key_addition(client, token_id, admin_key):
124+
"""
125+
Demonstrate that admin key cannot add a new supply key if none was present during creation.
126+
This shows the limitation of admin key privileges.
127+
"""
128+
print(f"\nAttempting to add supply key to {token_id} (this should fail)...")
129+
130+
new_supply_key = PrivateKey.generate_ed25519()
131+
132+
transaction = (
133+
TokenUpdateTransaction()
134+
.set_token_id(token_id)
135+
.set_supply_key(new_supply_key) # Trying to add supply key that wasn't present
136+
.freeze_with(client)
137+
.sign(admin_key) # Admin key cannot authorize adding new keys
138+
)
139+
140+
try:
141+
receipt = transaction.execute(client)
142+
if receipt.status != ResponseCode.SUCCESS:
143+
print(f"❌ As expected, adding supply key failed: {ResponseCode(receipt.status).name}")
144+
print(" Admin key cannot authorize adding keys that were not present during token creation.")
145+
return True # Expected failure
146+
else:
147+
print("⚠️ Unexpectedly succeeded - this shouldn't happen")
148+
return False
149+
except Exception as e:
150+
print(f"❌ As expected, adding supply key failed with exception: {e}")
151+
return True
152+
153+
154+
def demonstrate_admin_key_update(client, token_id, admin_key, operator_key):
155+
"""
156+
Demonstrate updating the admin key itself using current admin key authorization.
157+
This shows admin key can change itself.
158+
"""
159+
print(f"\nUpdating admin key for {token_id} to operator key...")
160+
161+
transaction = (
162+
TokenUpdateTransaction()
163+
.set_token_id(token_id)
164+
.set_admin_key(operator_key) # Change admin key to operator key
165+
.freeze_with(client)
166+
.sign(admin_key) # Current admin key authorizes the change
167+
.sign(operator_key) # New admin key must also sign
168+
)
169+
170+
receipt = transaction.execute(client)
171+
if receipt.status != ResponseCode.SUCCESS:
172+
print(f"Admin key update failed with status: {ResponseCode(receipt.status).name}")
173+
return False
174+
175+
print("✅ Admin key updated successfully")
176+
return True
177+
178+
179+
def demonstrate_token_deletion(client, token_id, operator_key):
180+
"""
181+
Demonstrate deleting the token using admin key (now operator key).
182+
Note: Since we updated admin key to operator_key, we use that.
183+
"""
184+
print(f"\nDeleting token {token_id} using admin key...")
185+
186+
transaction = (
187+
TokenDeleteTransaction()
188+
.set_token_id(token_id)
189+
.freeze_with(client)
190+
.sign(operator_key) # Admin key (now operator_key) signs the deletion
191+
)
192+
193+
receipt = transaction.execute(client)
194+
if receipt.status != ResponseCode.SUCCESS:
195+
print(f"Token deletion failed with status: {ResponseCode(receipt.status).name}")
196+
return False
197+
198+
print("✅ Token deleted successfully using admin key")
199+
return True
200+
201+
202+
def get_token_info(client, token_id):
203+
"""Query and display token information."""
204+
try:
205+
info = (
206+
TokenInfoQuery()
207+
.set_token_id(token_id)
208+
.execute(client)
209+
)
210+
print(f"\nToken Info for {token_id}:")
211+
print(f" Name: {info.name}")
212+
print(f" Symbol: {info.symbol}")
213+
print(f" Memo: {info.memo}")
214+
print(f" Admin Key: {info.admin_key}")
215+
print(f" Supply Key: {info.supply_key}")
216+
return info
217+
except Exception as e:
218+
print(f"Failed to get token info: {e}")
219+
return None
220+
221+
222+
def main():
223+
"""
224+
Main function demonstrating admin key capabilities:
225+
1. Create token with admin key
226+
2. Update token memo (admin privilege)
227+
3. Attempt to add supply key (should fail)
228+
4. Update admin key itself
229+
5. Delete token (admin privilege)
230+
"""
231+
client, operator_id, operator_key = setup_client()
232+
admin_key = generate_admin_key()
233+
234+
# Step 1: Create token with admin key
235+
token_id = create_token_with_admin_key(client, operator_id, operator_key, admin_key)
236+
237+
# Get initial token info
238+
get_token_info(client, token_id)
239+
240+
# Step 2: Demonstrate admin-only update
241+
if demonstrate_admin_update_memo(client, token_id, admin_key):
242+
get_token_info(client, token_id) # Verify the update
243+
244+
# Step 3: Show limitation - cannot add new keys
245+
demonstrate_failed_supply_key_addition(client, token_id, admin_key)
246+
247+
# Step 4: Update admin key itself
248+
if demonstrate_admin_key_update(client, token_id, admin_key, operator_key):
249+
get_token_info(client, token_id) # Verify admin key changed
250+
251+
# Step 5: Delete token using admin privilege
252+
demonstrate_token_deletion(client, token_id, operator_key)
253+
254+
print("\n🎉 Admin key demonstration completed!")
255+
256+
257+
if __name__ == "__main__":
258+
main()

tests/unit/test_token_create_transaction.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import (
4242
SchedulableTransactionBody,
4343
)
44+
from hiero_sdk_python.tokens.token_update_transaction import TokenUpdateTransaction
45+
from hiero_sdk_python.tokens.token_delete_transaction import TokenDeleteTransaction
46+
from hiero_sdk_python.query.token_info_query import TokenInfoQuery
47+
from hiero_sdk_python.tokens.token_id import TokenId
4448

4549
pytestmark = pytest.mark.unit
4650

@@ -1066,3 +1070,88 @@ def test_auto_renew_account_assignment_during_freeze_with_client(mock_account_id
10661070

10671071
assert body3.tokenCreation.autoRenewPeriod == Duration(7890000)._to_proto() # Default around 90 days
10681072
assert body3.tokenCreation.autoRenewAccount == treasury_account._to_proto()
1073+
1074+
1075+
def test_admin_key_token_operations_logic(mock_client):
1076+
"""
1077+
Test the logic of admin key token operations from the example.
1078+
This test verifies that the transactions are built correctly with proper signing requirements.
1079+
"""
1080+
client = mock_client
1081+
operator_id = client.operator_account_id
1082+
operator_key = PrivateKey.generate() # Generate a key for testing
1083+
1084+
# Find a node account ID from the client's network
1085+
node_account_id = client.network.nodes[0]._account_id if client.network.nodes else AccountId(0, 0, 3)
1086+
1087+
# Generate transaction ID helper
1088+
import time
1089+
current_time = time.time()
1090+
timestamp_seconds = int(current_time)
1091+
timestamp_nanos = int((current_time - timestamp_seconds) * 1e9)
1092+
from hiero_sdk_python.hapi.services import timestamp_pb2
1093+
tx_timestamp = timestamp_pb2.Timestamp(seconds=timestamp_seconds, nanos=timestamp_nanos)
1094+
from hiero_sdk_python.transaction.transaction_id import TransactionId
1095+
tx_id = TransactionId(valid_start=tx_timestamp, account_id=operator_id)
1096+
1097+
# Step 1: Generate admin key
1098+
admin_key = PrivateKey.generate_ed25519()
1099+
1100+
# Step 2: Create token with admin key
1101+
create_tx = TokenCreateTransaction()
1102+
create_tx.set_token_name("Admin Key Demo Token")
1103+
create_tx.set_token_symbol("AKDT")
1104+
create_tx.set_decimals(2)
1105+
create_tx.set_initial_supply(1000)
1106+
create_tx.set_treasury_account_id(operator_id)
1107+
create_tx.set_token_type(TokenType.FUNGIBLE_COMMON)
1108+
create_tx.set_supply_type(SupplyType.INFINITE)
1109+
create_tx.set_admin_key(admin_key)
1110+
create_tx.transaction_id = tx_id
1111+
create_tx.node_account_id = node_account_id
1112+
1113+
transaction_body = create_tx.build_transaction_body()
1114+
1115+
# Verify admin key is set in protobuf
1116+
assert transaction_body.tokenCreation.HasField("adminKey")
1117+
1118+
# Verify no other keys are set
1119+
assert not transaction_body.tokenCreation.HasField("supplyKey")
1120+
assert not transaction_body.tokenCreation.HasField("freezeKey")
1121+
assert not transaction_body.tokenCreation.HasField("wipeKey")
1122+
assert not transaction_body.tokenCreation.HasField("pause_key")
1123+
1124+
# Step 3: Test token update with admin key
1125+
update_tx = TokenUpdateTransaction()
1126+
update_tx.set_token_id("0.0.12345")
1127+
update_tx.set_token_memo("Updated by admin key")
1128+
1129+
# Step 4: Test failed supply key addition (this would fail in integration)
1130+
failed_update_tx = TokenUpdateTransaction()
1131+
failed_update_tx.set_token_id("0.0.12345")
1132+
failed_update_tx.set_supply_key(PrivateKey.generate_ed25519())
1133+
1134+
# This transaction would fail because admin key cannot add new keys
1135+
# In a real scenario, this would return ResponseCode.TOKEN_HAS_NO_SUPPLY_KEY
1136+
1137+
# Step 5: Test admin key update
1138+
new_admin_key = PrivateKey.generate_ed25519()
1139+
admin_update_tx = TokenUpdateTransaction()
1140+
admin_update_tx.set_token_id("0.0.12345")
1141+
admin_update_tx.set_admin_key(new_admin_key)
1142+
1143+
# Step 6: Test token deletion
1144+
delete_tx = TokenDeleteTransaction()
1145+
delete_tx.set_token_id("0.0.12345")
1146+
1147+
print("✅ All admin key token operation logic tests passed")
1148+
1149+
1150+
def test_token_info_query_structure():
1151+
"""Test that TokenInfoQuery is properly structured."""
1152+
query = TokenInfoQuery(TokenId.from_string("0.0.12345"))
1153+
1154+
# Verify query has the token ID set
1155+
assert str(query.token_id) == "0.0.12345"
1156+
1157+
print("✅ TokenInfoQuery structure test passed")

0 commit comments

Comments
 (0)