Skip to content

Commit e6eb047

Browse files
authored
feat: add TokenUpdateNftsTransaction (#108)
* feat: add metadata_key attribute to TokenCreateTransaction Signed-off-by: dosi <[email protected]> * feat: implement TokenUpdateNftsTransaction class Signed-off-by: dosi <[email protected]> * test: add integration tests for TokenUpdateNftsTransaction Signed-off-by: dosi <[email protected]> * test: add unit tests for TokenUpdateNftsTransaction Signed-off-by: dosi <[email protected]> * docs: add token update nfts example Signed-off-by: dosi <[email protected]> * docs: add token update nfts transaction documentation to README Signed-off-by: dosi <[email protected]> * chore: add TokenUpdateNftsTransaction to __init__.py Signed-off-by: dosi <[email protected]> * test: add wipe_key and metadata_key to TokenCreateTransaction unit tests Signed-off-by: dosi <[email protected]> * test: add integration test for NFT metadata update failure on unminted serials Signed-off-by: dosi <[email protected]> * test: address PR comments for TokenUpdateNftsTransaction integration tests Signed-off-by: dosi <[email protected]> * refactor: simplify TokenUpdateNftsTransactionBody construction with direct initialization Signed-off-by: dosi <[email protected]> * docs: simplify token update nfts example Signed-off-by: dosi <[email protected]> --------- Signed-off-by: dosi <[email protected]>
1 parent 574e1f8 commit e6eb047

File tree

8 files changed

+853
-3
lines changed

8 files changed

+853
-3
lines changed

examples/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ You can choose either syntax or even mix both styles in your projects.
2727
- [Unfreezing a Token](#unfreezing-a-token)
2828
- [Rejecting a Token](#rejecting-a-token)
2929
- [Rejecting a Non-Fungible Token](#rejecting-a-non-fungible-token)
30+
- [Token Update NFTs](#token-update-nfts)
3031
- [Querying NFT Info](#querying-nft-info)
3132
- [HBAR Transactions](#hbar-transactions)
3233
- [Transferring HBAR](#transferring-hbar)
@@ -448,6 +449,35 @@ transaction.execute(client)
448449
transaction.execute(client)
449450
```
450451

452+
### Token Update NFTs
453+
454+
#### Pythonic Syntax:
455+
```
456+
transaction = TokenUpdateNftsTransaction(
457+
token_id=nft_token_id,
458+
serial_numbers=serial_numbers,
459+
metadata=new_metadata
460+
).freeze_with(client)
461+
462+
transaction.sign(metadata_key)
463+
transaction.execute(client)
464+
465+
```
466+
#### Method Chaining:
467+
```
468+
transaction = (
469+
TokenUpdateNftsTransaction()
470+
.set_token_id(nft_token_id)
471+
.set_serial_numbers(serial_numbers)
472+
.set_metadata(new_metadata)
473+
.freeze_with(client)
474+
.sign(metadata_key)
475+
)
476+
477+
transaction.execute(client)
478+
479+
```
480+
451481
### Querying NFT Info
452482

453483
#### Pythonic Syntax:

examples/token_update_nfts.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import os
2+
import sys
3+
from dotenv import load_dotenv
4+
5+
from hiero_sdk_python import (
6+
Client,
7+
AccountId,
8+
PrivateKey,
9+
Network,
10+
)
11+
from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenType
12+
from hiero_sdk_python.response_code import ResponseCode
13+
from hiero_sdk_python.tokens.nft_id import NftId
14+
from hiero_sdk_python.tokens.supply_type import SupplyType
15+
from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction
16+
from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction
17+
from hiero_sdk_python.tokens.token_update_nfts_transaction import TokenUpdateNftsTransaction
18+
from hiero_sdk_python.query.token_nft_info_query import TokenNftInfoQuery
19+
20+
load_dotenv()
21+
22+
def setup_client():
23+
"""Initialize and set up the client with operator account"""
24+
network = Network(network='testnet')
25+
client = Client(network)
26+
27+
operator_id = AccountId.from_string(os.getenv('OPERATOR_ID'))
28+
operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY'))
29+
client.set_operator(operator_id, operator_key)
30+
31+
return client, operator_id, operator_key
32+
33+
def create_nft(client, operator_id, operator_key, metadata_key):
34+
"""Create a non-fungible token"""
35+
receipt = (
36+
TokenCreateTransaction()
37+
.set_token_name("MyExampleNFT")
38+
.set_token_symbol("EXNFT")
39+
.set_decimals(0)
40+
.set_initial_supply(0)
41+
.set_treasury_account_id(operator_id)
42+
.set_token_type(TokenType.NON_FUNGIBLE_UNIQUE)
43+
.set_supply_type(SupplyType.FINITE)
44+
.set_max_supply(100)
45+
.set_admin_key(operator_key)
46+
.set_freeze_key(operator_key)
47+
.set_supply_key(operator_key)
48+
.set_metadata_key(metadata_key) # Needed to update NFTs
49+
.execute(client)
50+
)
51+
52+
# Check if nft creation was successful
53+
if receipt.status != ResponseCode.SUCCESS:
54+
print(f"NFT creation failed with status: {ResponseCode.get_name(receipt.status)}")
55+
sys.exit(1)
56+
57+
# Get token ID from receipt
58+
nft_token_id = receipt.tokenId
59+
print(f"NFT created with ID: {nft_token_id}")
60+
61+
return nft_token_id
62+
63+
def mint_nfts(client, nft_token_id, metadata_list):
64+
"""Mint a non-fungible token"""
65+
receipt = (
66+
TokenMintTransaction()
67+
.set_token_id(nft_token_id)
68+
.set_metadata(metadata_list)
69+
.execute(client)
70+
)
71+
72+
if receipt.status != ResponseCode.SUCCESS:
73+
print(f"NFT minting failed with status: {ResponseCode.get_name(receipt.status)}")
74+
sys.exit(1)
75+
76+
print(f"NFT minted with serial numbers: {receipt.serial_numbers}")
77+
78+
return [NftId(nft_token_id, serial_number) for serial_number in receipt.serial_numbers], receipt.serial_numbers
79+
80+
def get_nft_info(client, nft_id):
81+
"""Get information about an NFT"""
82+
info = (
83+
TokenNftInfoQuery()
84+
.set_nft_id(nft_id)
85+
.execute(client)
86+
)
87+
88+
return info
89+
90+
def update_nft_metadata(client, nft_token_id, serial_numbers, new_metadata, metadata_private_key):
91+
"""Update metadata for NFTs in a collection"""
92+
receipt = (
93+
TokenUpdateNftsTransaction()
94+
.set_token_id(nft_token_id)
95+
.set_serial_numbers(serial_numbers)
96+
.set_metadata(new_metadata)
97+
.freeze_with(client)
98+
.sign(metadata_private_key) # Has to be signed here by metadata_key
99+
.execute(client)
100+
)
101+
102+
if receipt.status != ResponseCode.SUCCESS:
103+
print(f"NFT metadata update failed with status: {ResponseCode.get_name(receipt.status)}")
104+
sys.exit(1)
105+
106+
print(f"Successfully updated metadata for NFTs with serial numbers: {serial_numbers}")
107+
108+
def token_update_nfts():
109+
"""
110+
Demonstrates the NFT token update functionality by:
111+
1. Setting up client with operator account
112+
2. Creating a non-fungible token with metadata key
113+
3. Minting two NFTs with initial metadata
114+
4. Checking the current NFT info
115+
5. Updating metadata for the first NFT
116+
6. Verifying the updated NFT metadata
117+
"""
118+
client, operator_id, operator_key = setup_client()
119+
120+
# Create metadata key
121+
metadata_private_key = PrivateKey.generate_ed25519()
122+
123+
# Create a new NFT collection with the treasury account as owner
124+
nft_token_id = create_nft(client, operator_id, operator_key, metadata_private_key)
125+
126+
# Initial metadata for our NFTs
127+
initial_metadata = [b"Initial metadata 1", b"Initial metadata 2"]
128+
129+
# New metadata to update the first NFT
130+
new_metadata = b"Updated metadata1"
131+
132+
# Mint 2 NFTs in the collection with initial metadata
133+
nft_ids, serial_numbers = mint_nfts(client, nft_token_id, initial_metadata)
134+
135+
# Get and print information about the NFTs
136+
print("\nCheck that the NFTs have the initial metadata")
137+
for nft_id in nft_ids:
138+
nft_info = get_nft_info(client, nft_id)
139+
print(f"NFT ID: {nft_info.nft_id}, Metadata: {nft_info.metadata}")
140+
141+
# Update metadata for specific NFTs by providing their id and serial numbers
142+
# Only the NFTs with the provided serial numbers will have their metadata updated
143+
serial_numbers_to_update = [serial_numbers[0]]
144+
update_nft_metadata(client, nft_token_id, serial_numbers_to_update, new_metadata, metadata_private_key)
145+
146+
# Get and print information about the NFTs
147+
print("\nCheck that only the first NFT has the updated metadata")
148+
for nft_id in nft_ids:
149+
nft_info = get_nft_info(client, nft_id)
150+
print(f"NFT ID: {nft_info.nft_id}, Metadata: {nft_info.metadata}")
151+
152+
if __name__ == "__main__":
153+
token_update_nfts()

src/hiero_sdk_python/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .tokens.token_unfreeze_transaction import TokenUnfreezeTransaction
2222
from .tokens.token_wipe_transaction import TokenWipeTransaction
2323
from .tokens.token_reject_transaction import TokenRejectTransaction
24+
from .tokens.token_update_nfts_transaction import TokenUpdateNftsTransaction
2425
from .tokens.token_id import TokenId
2526
from .tokens.nft_id import NftId
2627
from .tokens.token_nft_transfer import TokenNftTransfer
@@ -95,6 +96,7 @@
9596
"TokenNftTransfer",
9697
"TokenNftInfo",
9798
"TokenRejectTransaction",
99+
"TokenUpdateNftsTransaction",
98100

99101
# Transaction
100102
"TransferTransaction",

src/hiero_sdk_python/tokens/token_create_transaction.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,14 @@ class TokenKeys:
178178
supply_key: The supply key for the token to mint and burn.
179179
freeze_key: The freeze key for the token to freeze and unfreeze.
180180
wipe_key: The wipe key for the token to wipe tokens from an account.
181+
metadata_key: The metadata key for the token to update NFT metadata.
181182
"""
182183

183184
admin_key: Optional[PrivateKey] = None
184185
supply_key: Optional[PrivateKey] = None
185186
freeze_key: Optional[PrivateKey] = None
186187
wipe_key: Optional[PrivateKey] = None
188+
metadata_key: Optional[PrivateKey] = None
187189

188190
class TokenCreateTransaction(Transaction):
189191
"""
@@ -319,6 +321,11 @@ def set_wipe_key(self, key):
319321
self._require_not_frozen()
320322
self._keys.wipe_key = key
321323
return self
324+
325+
def set_metadata_key(self, key):
326+
self._require_not_frozen()
327+
self._keys.metadata_key = key
328+
return self
322329

323330
def build_transaction_body(self):
324331
"""
@@ -357,6 +364,11 @@ def build_transaction_body(self):
357364
wipe_public_key_bytes = self._keys.wipe_key.public_key().to_bytes_raw()
358365
wipe_key_proto = basic_types_pb2.Key(ed25519=wipe_public_key_bytes)
359366

367+
metadata_key_proto = None
368+
if self._keys.metadata_key:
369+
metadata_public_key_bytes = self._keys.metadata_key.public_key().to_bytes_raw()
370+
metadata_key_proto = basic_types_pb2.Key(ed25519=metadata_public_key_bytes)
371+
360372
# Ensure token type is correctly set with default to fungible
361373
if self._token_params.token_type is None:
362374
token_type_value = 0 # default FUNGIBLE_COMMON
@@ -387,7 +399,8 @@ def build_transaction_body(self):
387399
adminKey=admin_key_proto,
388400
supplyKey=supply_key_proto,
389401
freezeKey=freeze_key_proto,
390-
wipeKey=wipe_key_proto
402+
wipeKey=wipe_key_proto,
403+
metadata_key=metadata_key_proto
391404
)
392405
# Build the base transaction body and attach the token creation details
393406
transaction_body = self.build_base_transaction_body()
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from operator import le
2+
from hiero_sdk_python.tokens.token_id import TokenId
3+
from hiero_sdk_python.transaction.transaction import Transaction
4+
from hiero_sdk_python.channels import _Channel
5+
from hiero_sdk_python.executable import _Method
6+
from hiero_sdk_python.hapi.services.token_update_nfts_pb2 import TokenUpdateNftsTransactionBody
7+
from google.protobuf.wrappers_pb2 import BytesValue
8+
9+
class TokenUpdateNftsTransaction(Transaction):
10+
"""
11+
Represents a token update NFTs transaction on the Hedera network.
12+
13+
This transaction updates the metadata of NFTs.
14+
15+
Inherits from the base Transaction class and implements the required methods
16+
to build and execute a token update NFTs transaction.
17+
"""
18+
def __init__(self, token_id=None, serial_numbers=None, metadata=None):
19+
"""
20+
Initializes a new TokenUpdateNftsTransaction instance with optional token_id, serial_numbers, and metadata.
21+
22+
Args:
23+
token_id (TokenId, optional): The ID of the token whose NFTs will be updated.
24+
serial_numbers (list[int], optional): The serial numbers of the NFTs to update.
25+
metadata (bytes, optional): The new metadata for the NFTs.
26+
"""
27+
super().__init__()
28+
self.token_id : TokenId = token_id
29+
self.serial_numbers : list[int] = serial_numbers if serial_numbers else []
30+
self.metadata : bytes = metadata
31+
32+
def set_token_id(self, token_id):
33+
self._require_not_frozen()
34+
self.token_id = token_id
35+
return self
36+
37+
def set_serial_numbers(self, serial_numbers):
38+
self._require_not_frozen()
39+
self.serial_numbers = serial_numbers
40+
return self
41+
42+
def set_metadata(self, metadata):
43+
self._require_not_frozen()
44+
self.metadata = metadata
45+
return self
46+
47+
def build_transaction_body(self):
48+
"""
49+
Builds and returns the protobuf transaction body for token update NFTs.
50+
51+
Returns:
52+
TransactionBody: The protobuf transaction body containing the token update NFTs details.
53+
54+
Raises:
55+
ValueError: If the token ID and serial numbers are not set or metadata is greater than 100 bytes.
56+
"""
57+
if not self.token_id:
58+
raise ValueError("Missing token ID")
59+
60+
if not self.serial_numbers:
61+
raise ValueError("Missing serial numbers")
62+
63+
if self.metadata and len(self.metadata) > 100:
64+
raise ValueError("Metadata must be less than 100 bytes")
65+
66+
token_update_body = TokenUpdateNftsTransactionBody(
67+
token=self.token_id.to_proto(),
68+
serial_numbers=self.serial_numbers,
69+
metadata=BytesValue(value=self.metadata)
70+
)
71+
72+
transaction_body = self.build_base_transaction_body()
73+
transaction_body.token_update_nfts.CopyFrom(token_update_body)
74+
return transaction_body
75+
76+
def _get_method(self, channel: _Channel) -> _Method:
77+
"""
78+
Gets the method to execute the token update NFTs transaction.
79+
80+
This internal method returns a _Method object containing the appropriate gRPC
81+
function to call when executing this transaction on the Hedera network.
82+
83+
Args:
84+
channel (_Channel): The channel containing service stubs
85+
86+
Returns:
87+
_Method: An object containing the transaction function to update NFTs.
88+
"""
89+
return _Method(
90+
transaction_func=channel.token.updateNfts,
91+
query_func=None
92+
)
93+
94+
def _from_proto(self, proto: TokenUpdateNftsTransactionBody):
95+
"""
96+
Deserializes a TokenUpdateNftsTransactionBody from a protobuf object.
97+
98+
Args:
99+
proto (TokenUpdateNftsTransactionBody): The protobuf object to deserialize.
100+
101+
Returns:
102+
TokenUpdateNftsTransaction: Returns self for method chaining.
103+
"""
104+
self.token_id = TokenId.from_proto(proto.token)
105+
self.serial_numbers = proto.serial_numbers
106+
self.metadata = proto.metadata.value
107+
return self

0 commit comments

Comments
 (0)