Skip to content

Commit 8d592d7

Browse files
authored
feat: add TokenFeeScheduleUpdateTransaction class and tests (#722)
Signed-off-by: Akshat Kumar <[email protected]> Signed-off-by: Akshat8510 <[email protected]>
1 parent cb1e57c commit 8d592d7

File tree

7 files changed

+827
-5
lines changed

7 files changed

+827
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
77
## [Unreleased]
88

99
### Added
10+
- Add `TokenFeeScheduleUpdateTransaction` class to support updating custom fee schedules on tokens (#471).
11+
- Add `examples/token_update_fee_schedule_fungible.py` and `examples/token_update_fee_schedule_nft.py` demonstrating the use of `TokenFeeScheduleUpdateTransaction`.
12+
- Update `docs/sdk_users/running_examples.md` to include `TokenFeeScheduleUpdateTransaction`.
1013
- added FreezeTransaction class
1114
- added FreezeType class
1215
- Added `docs/sdk_developers/pylance.md`, a new guide explaining how to set up and use **Pylance** in VS Code for validating imports, file references, and methods before review. (#713)

docs/sdk_users/running_examples.md

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,22 +1034,61 @@ print(nft_info)
10341034
### Querying Fungible Token Info
10351035

10361036
#### Pythonic Syntax:
1037-
```
1037+
```python
10381038
info_query = TokenInfoQuery(token_id=token_id)
10391039
info = info_query.execute(client)
10401040
print(info)
10411041
```
1042+
10421043
#### Method Chaining:
1043-
```
1044+
```python
10441045
info_query = (
1045-
TokenInfoQuery()
1046-
.set_token_id(token_id)
1047-
)
1046+
TokenInfoQuery()
1047+
.set_token_id(token_id)
1048+
)
10481049

10491050
info = info_query.execute(client)
10501051
print(info)
10511052
```
10521053

1054+
### Updating a Token Fee Schedule
1055+
1056+
#### Pythonic Syntax:
1057+
1058+
```python
1059+
# Note: Royalty fees are only for NON_FUNGIBLE_UNIQUE tokens.
1060+
new_fees = [
1061+
CustomFixedFee(amount=100, fee_collector_account_id=collector_account_id),
1062+
CustomRoyaltyFee(numerator=5, denominator=10, fee_collector_account_id=collector_account_id)
1063+
]
1064+
1065+
transaction = TokenFeeScheduleUpdateTransaction(
1066+
token_id=token_id, # assumed NFT in this example
1067+
custom_fees=new_fees
1068+
).freeze_with(client)
1069+
1070+
transaction.sign(fee_schedule_key) # The fee_schedule_key MUST sign
1071+
transaction.execute(client)
1072+
```
1073+
1074+
#### Method Chaining:
1075+
1076+
```python
1077+
# Note: Fractional fees are only for FUNGIBLE_COMMON tokens.
1078+
new_fees = [
1079+
CustomFixedFee(amount=100, fee_collector_account_id=collector_account_id)
1080+
]
1081+
1082+
transaction = (
1083+
TokenFeeScheduleUpdateTransaction()
1084+
.set_token_id(token_id) # assumed FUNGIBLE in this example
1085+
.set_custom_fees(new_fees)
1086+
.freeze_with(client)
1087+
)
1088+
1089+
transaction.sign(fee_schedule_key) # The fee_schedule_key MUST sign
1090+
transaction.execute(client)
1091+
```
10531092
## HBAR Transactions
10541093

10551094
### Transferring HBAR
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""Example: Update Custom Fees for a Fungible Token"""
2+
import os
3+
import sys
4+
from dotenv import load_dotenv
5+
6+
from hiero_sdk_python import Client, AccountId, PrivateKey, Network
7+
from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction, TokenParams, TokenKeys
8+
from hiero_sdk_python.tokens.token_type import TokenType
9+
from hiero_sdk_python.tokens.supply_type import SupplyType
10+
from hiero_sdk_python.tokens.token_fee_schedule_update_transaction import TokenFeeScheduleUpdateTransaction
11+
from hiero_sdk_python.tokens.custom_fixed_fee import CustomFixedFee
12+
from hiero_sdk_python.response_code import ResponseCode
13+
from hiero_sdk_python.query.token_info_query import TokenInfoQuery
14+
15+
16+
def setup_client():
17+
"""Initialize client and operator credentials from .env."""
18+
load_dotenv()
19+
try:
20+
client = Client(Network(os.getenv("NETWORK", "testnet")))
21+
operator_id = AccountId.from_string(os.getenv("OPERATOR_ID"))
22+
operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY"))
23+
client.set_operator(operator_id, operator_key)
24+
print(f" Operator set: {operator_id}\n")
25+
return client, operator_id, operator_key
26+
except Exception as e:
27+
print(f" Error setting up client: {e}")
28+
sys.exit(1)
29+
30+
31+
def create_fungible_token(client, operator_id, fee_schedule_key):
32+
"""Create a fungible token with only a fee schedule key."""
33+
print(" Creating fungible token...")
34+
token_params = TokenParams(
35+
token_name="Fungible Fee Example",
36+
token_symbol="FFE",
37+
treasury_account_id=operator_id,
38+
initial_supply=1000,
39+
decimals=2,
40+
token_type=TokenType.FUNGIBLE_COMMON,
41+
supply_type=SupplyType.FINITE,
42+
max_supply=2000,
43+
custom_fees=[], # No custom fees at creation
44+
)
45+
46+
keys = TokenKeys(
47+
fee_schedule_key=fee_schedule_key
48+
)
49+
50+
tx = TokenCreateTransaction(token_params=token_params, keys=keys)
51+
52+
tx.freeze_with(client)
53+
receipt = tx.execute(client)
54+
55+
if receipt.status != ResponseCode.SUCCESS:
56+
print(f" Token creation failed: {ResponseCode(receipt.status).name}\n")
57+
client.close()
58+
sys.exit(1)
59+
60+
token_id = receipt.token_id
61+
print(f" Token created successfully: {token_id}\n")
62+
return token_id
63+
64+
65+
def update_custom_fixed_fee(client, token_id, fee_schedule_key, treasury_account_id):
66+
"""Updates the token's fee schedule with a new fixed fee."""
67+
print(f" Updating custom fixed fee for token {token_id}...")
68+
new_fees = [
69+
# Send the custom fee to the token's treasury account
70+
CustomFixedFee(amount=150, fee_collector_account_id=treasury_account_id)
71+
]
72+
print(f" Defined {len(new_fees)} new custom fees.\n")
73+
tx = (
74+
TokenFeeScheduleUpdateTransaction()
75+
.set_token_id(token_id)
76+
.set_custom_fees(new_fees)
77+
)
78+
79+
# The transaction MUST be signed by the fee_schedule_key
80+
tx.freeze_with(client).sign(fee_schedule_key)
81+
82+
try:
83+
receipt = tx.execute(client)
84+
if receipt.status != ResponseCode.SUCCESS:
85+
print(f" Fee schedule update failed: {ResponseCode(receipt.status).name}\n")
86+
sys.exit(1)
87+
else:
88+
print(" Fee schedule updated successfully.\n")
89+
except Exception as e:
90+
print(f" Error during fee schedule update execution: {e}\n")
91+
sys.exit(1)
92+
93+
94+
def query_token_info(client, token_id):
95+
"""Query token info to show the custom fees."""
96+
print(f"\nQuerying token info for {token_id}...\n")
97+
try:
98+
token_info = TokenInfoQuery(token_id=token_id).execute(client)
99+
print("Token Info Retrieved Successfully!\n")
100+
101+
print(f"Name: {getattr(token_info, 'name', 'N/A')}")
102+
print(f"Symbol: {getattr(token_info, 'symbol', 'N/A')}")
103+
print(f"Total Supply: {getattr(token_info, 'total_supply', 'N/A')}")
104+
print(f"Treasury: {getattr(token_info, 'treasury_account_id', 'N/A')}")
105+
print(f"Decimals: {getattr(token_info, 'decimals', 'N/A')}")
106+
print(f"Max Supply: {getattr(token_info, 'max_supply', 'N/A')}")
107+
print()
108+
109+
custom_fees = getattr(token_info, "custom_fees", [])
110+
if custom_fees:
111+
print(f"Found {len(custom_fees)} custom fee(s):")
112+
for i, fee in enumerate(custom_fees, 1):
113+
print(f" Fee #{i}: {type(fee).__name__}")
114+
print(f" Collector: {getattr(fee, 'fee_collector_account_id', 'N/A')}")
115+
if isinstance(fee, CustomFixedFee):
116+
print(f" Amount: {getattr(fee, 'amount', 'N/A')}")
117+
else:
118+
print("No custom fees defined for this token.\n")
119+
120+
except Exception as e:
121+
print(f"Error querying token info: {e}")
122+
sys.exit(1)
123+
124+
125+
def main():
126+
client, operator_id, operator_key = setup_client()
127+
token_id = None
128+
try:
129+
fee_key = operator_key
130+
131+
token_id = create_fungible_token(client, operator_id, fee_key)
132+
133+
if token_id:
134+
query_token_info(client, token_id)
135+
# Pass the operator_id as the fee collector (which is also the treasury)
136+
update_custom_fixed_fee(client, token_id, fee_key, operator_id)
137+
query_token_info(client, token_id)
138+
139+
except Exception as e:
140+
print(f" Error during token operations: {e}")
141+
finally:
142+
client.close()
143+
print("\n Client closed. Example complete.")
144+
145+
146+
if __name__ == "__main__":
147+
main()
148+
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""Example: Update Custom Fees for an NFT"""
2+
import os
3+
import sys
4+
from dotenv import load_dotenv
5+
6+
from hiero_sdk_python import Client, AccountId, PrivateKey, Network
7+
from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction, TokenParams, TokenKeys
8+
from hiero_sdk_python.tokens.token_type import TokenType
9+
from hiero_sdk_python.tokens.supply_type import SupplyType
10+
from hiero_sdk_python.tokens.token_fee_schedule_update_transaction import TokenFeeScheduleUpdateTransaction
11+
from hiero_sdk_python.tokens.custom_royalty_fee import CustomRoyaltyFee
12+
from hiero_sdk_python.response_code import ResponseCode
13+
from hiero_sdk_python.query.token_info_query import TokenInfoQuery
14+
15+
16+
def setup_client():
17+
"""Initialize client and operator credentials from .env."""
18+
load_dotenv()
19+
try:
20+
client = Client(Network(os.getenv("NETWORK", "testnet")))
21+
operator_id = AccountId.from_string(os.getenv("OPERATOR_ID"))
22+
operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY"))
23+
client.set_operator(operator_id, operator_key)
24+
print(f" Operator set: {operator_id}\n")
25+
return client, operator_id, operator_key
26+
except Exception as e:
27+
print(f" Error setting up client: {e}")
28+
sys.exit(1)
29+
30+
31+
def create_nft(client, operator_id, supply_key, fee_schedule_key):
32+
"""Create an NFT with supply and fee schedule keys."""
33+
print(" Creating NFT...")
34+
token_params = TokenParams(
35+
token_name="NFT Fee Example",
36+
token_symbol="NFE",
37+
treasury_account_id=operator_id,
38+
initial_supply=0,
39+
decimals=0,
40+
token_type=TokenType.NON_FUNGIBLE_UNIQUE,
41+
supply_type=SupplyType.FINITE,
42+
max_supply=1000,
43+
custom_fees=[],
44+
)
45+
46+
# A supply_key is REQUIRED for NFTs (to mint)
47+
# A fee_schedule_key is required to update fees
48+
keys = TokenKeys(
49+
supply_key=supply_key,
50+
fee_schedule_key=fee_schedule_key
51+
)
52+
53+
tx = TokenCreateTransaction(token_params=token_params, keys=keys)
54+
# tx.set_fee_schedule_key(fee_schedule_key)
55+
56+
# Sign with the supply key as well
57+
tx.freeze_with(client).sign(supply_key)
58+
receipt = tx.execute(client)
59+
60+
if receipt.status != ResponseCode.SUCCESS:
61+
print(f" Token creation failed: {ResponseCode(receipt.status).name}\n")
62+
client.close()
63+
sys.exit(1)
64+
65+
token_id = receipt.token_id
66+
print(f" Token created successfully: {token_id}\n")
67+
return token_id
68+
69+
70+
def update_custom_royalty_fee(client, token_id, fee_schedule_key, collector_account_id):
71+
"""Updates the token's fee schedule with a new royalty fee."""
72+
print(f" Updating custom royalty fee for token {token_id}...")
73+
new_fees = [
74+
CustomRoyaltyFee(
75+
numerator=5,
76+
denominator=100, # 5% royalty
77+
fee_collector_account_id=collector_account_id
78+
)
79+
]
80+
print(f" Defined {len(new_fees)} new custom fees.\n")
81+
tx = (
82+
TokenFeeScheduleUpdateTransaction()
83+
.set_token_id(token_id)
84+
.set_custom_fees(new_fees)
85+
)
86+
87+
tx.freeze_with(client).sign(fee_schedule_key)
88+
89+
try:
90+
receipt = tx.execute(client)
91+
if receipt.status != ResponseCode.SUCCESS:
92+
print(f" Fee schedule update failed: {ResponseCode(receipt.status).name}\n")
93+
sys.exit(1)
94+
else:
95+
print(" Fee schedule updated successfully.\n")
96+
except Exception as e:
97+
print(f" Error during fee schedule update execution: {e}\n")
98+
sys.exit(1)
99+
100+
101+
def query_token_info(client, token_id):
102+
"""Query token info and verify updated custom fees."""
103+
print(f"\nQuerying token info for {token_id}...\n")
104+
try:
105+
token_info = TokenInfoQuery(token_id=token_id).execute(client)
106+
print("Token Info Retrieved Successfully!\n")
107+
108+
print(f"Name: {getattr(token_info, 'name', 'N/A')}")
109+
print(f"Symbol: {getattr(token_info, 'symbol', 'N/A')}")
110+
print(f"Total Supply: {getattr(token_info, 'total_supply', 'N/A')}")
111+
print(f"Treasury: {getattr(token_info, 'treasury_account_id', 'N/A')}")
112+
print(f"Decimals: {getattr(token_info, 'decimals', 'N/A')}")
113+
print(f"Max Supply: {getattr(token_info, 'max_supply', 'N/A')}")
114+
print()
115+
116+
custom_fees = getattr(token_info, "custom_fees", [])
117+
if custom_fees:
118+
print(f"Found {len(custom_fees)} custom fee(s):")
119+
for i, fee in enumerate(custom_fees, 1):
120+
print(f" Fee #{i}: {type(fee).__name__}")
121+
print(f" Collector: {getattr(fee, 'fee_collector_account_id', 'N/A')}")
122+
if isinstance(fee, CustomRoyaltyFee):
123+
print(f" Royalty: {fee.numerator}/{fee.denominator}")
124+
else:
125+
print(f" Amount: {getattr(fee, 'amount', 'N/A')}")
126+
else:
127+
print("No custom fees defined for this token.\n")
128+
129+
except Exception as e:
130+
print(f"Error querying token info: {e}")
131+
sys.exit(1)
132+
133+
def main():
134+
client, operator_id, operator_key = setup_client()
135+
token_id = None
136+
try:
137+
# Use operator key as both supply and fee key
138+
supply_key = operator_key
139+
fee_key = operator_key
140+
141+
token_id = create_nft(client, operator_id, supply_key, fee_key)
142+
143+
if token_id:
144+
query_token_info(client, token_id)
145+
update_custom_royalty_fee(client, token_id, fee_key, operator_id)
146+
query_token_info(client, token_id)
147+
148+
except Exception as e:
149+
print(f" Error during token operations: {e}")
150+
finally:
151+
client.close()
152+
print("\n Client closed. Example complete.")
153+
154+
155+
if __name__ == "__main__":
156+
main()
157+

0 commit comments

Comments
 (0)