Skip to content

Commit bb098cf

Browse files
authored
feat: add nft transfer in TransferTransaction (#99)
* feat: add serial_numbers property to TransactionReceipt Signed-off-by: dosi <[email protected]> * feat: add _TokenNftTransfer class Signed-off-by: dosi <[email protected]> * test: add unit test for _TokenNftTransfer Signed-off-by: dosi <[email protected]> * feat: add nft_trasfers to TransferTransaction class Signed-off-by: dosi <[email protected]> * docs: update docstring in TransferTransaction Signed-off-by: dosi <[email protected]> * test: fix unit tests Signed-off-by: dosi <[email protected]> * test: migrate token transfer integration test to transfer_transaction_e2e_test.py and add more tests Signed-off-by: dosi <[email protected]> * docs: add transfer_nft.py example Signed-off-by: dosi <[email protected]> * docs: add transferring NFTs documentation in README Signed-off-by: dosi <[email protected]> * refactor: reduce cyclomatic complexity in TransferTransaction Signed-off-by: dosi <[email protected]> * test: remove unnecessary freeze_with() and add new tests in transfer_transaction_e2e_test.py Signed-off-by: dosi <[email protected]> * refactor: make _TokenNftTransfer class public Signed-off-by: dosi <[email protected]> * fix: update instances/imports of TokenNftTransfer where necessary Signed-off-by: dosi <[email protected]> * chore: add missing classes in __init__.py exports Signed-off-by: dosi <[email protected]> * revert: remove Duration from __all__ Signed-off-by: dosi <[email protected]> --------- Signed-off-by: dosi <[email protected]>
1 parent cdf89a5 commit bb098cf

File tree

10 files changed

+959
-74
lines changed

10 files changed

+959
-74
lines changed

examples/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ You can choose either syntax or even mix both styles in your projects.
2020
- [Associating a Token](#associating-a-token)
2121
- [Dissociating a Token](#dissociating-a-token)
2222
- [Transferring Tokens](#transferring-tokens)
23+
- [Transferring NFTs](#transferring-nfts)
2324
- [Wiping Tokens](#wiping-tokens)
2425
- [Deleting a Token](#deleting-a-token)
2526
- [Freezing a Token](#freezing-a-token)
@@ -262,6 +263,38 @@ transaction.execute(client)
262263
transaction.execute(client)
263264
```
264265

266+
### Transferring NFTs
267+
268+
#### Pythonic Syntax:
269+
```
270+
transaction = TransferTransaction(
271+
nft_transfers={
272+
token_id: [
273+
(
274+
operator_id,
275+
recipient_id,
276+
serial_number
277+
)
278+
]
279+
}
280+
).freeze_with(client)
281+
282+
transaction.sign(operator_key)
283+
transaction.execute(client)
284+
285+
```
286+
#### Method Chaining:
287+
```
288+
transaction = (
289+
TransferTransaction()
290+
.add_nft_transfer(nft_id, operator_id, recipient_id)
291+
.freeze_with(client)
292+
.sign(operator_key)
293+
)
294+
295+
transaction.execute(client)
296+
```
297+
265298
### Wiping tokens
266299

267300
#### Pythonic Syntax:

examples/transfer_nft.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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+
TransferTransaction,
11+
)
12+
from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction
13+
from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenType
14+
from hiero_sdk_python.hbar import Hbar
15+
from hiero_sdk_python.response_code import ResponseCode
16+
from hiero_sdk_python.tokens.nft_id import NftId
17+
from hiero_sdk_python.tokens.supply_type import SupplyType
18+
from hiero_sdk_python.tokens.token_associate_transaction import TokenAssociateTransaction
19+
from hiero_sdk_python.tokens.token_create_transaction import TokenCreateTransaction
20+
from hiero_sdk_python.tokens.token_mint_transaction import TokenMintTransaction
21+
22+
load_dotenv()
23+
24+
def setup_client():
25+
"""Initialize and set up the client with operator account"""
26+
# Initialize network and client
27+
network = Network(network='testnet')
28+
client = Client(network)
29+
30+
# Set up operator account
31+
operator_id = AccountId.from_string(os.getenv('OPERATOR_ID'))
32+
operator_key = PrivateKey.from_string(os.getenv('OPERATOR_KEY'))
33+
client.set_operator(operator_id, operator_key)
34+
35+
return client, operator_id, operator_key
36+
37+
def create_test_account(client):
38+
"""Create a new account for testing"""
39+
# Generate private key for new account
40+
new_account_private_key = PrivateKey.generate()
41+
new_account_public_key = new_account_private_key.public_key()
42+
43+
# Create new account with initial balance of 1 HBAR
44+
transaction = (
45+
AccountCreateTransaction()
46+
.set_key(new_account_public_key)
47+
.set_initial_balance(Hbar(1))
48+
.freeze_with(client)
49+
)
50+
51+
receipt = transaction.execute(client)
52+
53+
# Check if account creation was successful
54+
if receipt.status != ResponseCode.SUCCESS:
55+
print(f"Account creation failed with status: {ResponseCode.get_name(receipt.status)}")
56+
sys.exit(1)
57+
58+
# Get account ID from receipt
59+
account_id = receipt.accountId
60+
print(f"New account created with ID: {account_id}")
61+
62+
return account_id, new_account_private_key
63+
64+
def create_nft(client, operator_id, operator_key):
65+
"""Create a non-fungible token"""
66+
transaction = (
67+
TokenCreateTransaction()
68+
.set_token_name("MyExampleNFT")
69+
.set_token_symbol("EXNFT")
70+
.set_decimals(0)
71+
.set_initial_supply(0)
72+
.set_treasury_account_id(operator_id)
73+
.set_token_type(TokenType.NON_FUNGIBLE_UNIQUE)
74+
.set_supply_type(SupplyType.FINITE)
75+
.set_max_supply(100)
76+
.set_admin_key(operator_key)
77+
.set_supply_key(operator_key)
78+
.set_freeze_key(operator_key)
79+
.freeze_with(client)
80+
)
81+
82+
receipt = transaction.execute(client)
83+
84+
# Check if nft creation was successful
85+
if receipt.status != ResponseCode.SUCCESS:
86+
print(f"NFT creation failed with status: {ResponseCode.get_name(receipt.status)}")
87+
sys.exit(1)
88+
89+
# Get token ID from receipt
90+
nft_token_id = receipt.tokenId
91+
print(f"NFT created with ID: {nft_token_id}")
92+
93+
return nft_token_id
94+
95+
def mint_nft(client, nft_token_id, operator_key):
96+
"""Mint a non-fungible token"""
97+
transaction = (
98+
TokenMintTransaction()
99+
.set_token_id(nft_token_id)
100+
.set_metadata(b"My NFT Metadata 1")
101+
.freeze_with(client)
102+
)
103+
104+
receipt = transaction.execute(client)
105+
106+
if receipt.status != ResponseCode.SUCCESS:
107+
print(f"NFT minting failed with status: {ResponseCode.get_name(receipt.status)}")
108+
sys.exit(1)
109+
110+
print(f"NFT minted with serial number: {receipt.serial_numbers[0]}")
111+
112+
return NftId(nft_token_id, receipt.serial_numbers[0])
113+
114+
def associate_nft(client, account_id, token_id, account_private_key):
115+
"""Associate a non-fungible token with an account"""
116+
# Associate the token_id with the new account
117+
associate_transaction = (
118+
TokenAssociateTransaction()
119+
.set_account_id(account_id)
120+
.add_token_id(token_id)
121+
.freeze_with(client)
122+
.sign(account_private_key) # Has to be signed by new account's key
123+
)
124+
125+
receipt = associate_transaction.execute(client)
126+
127+
if receipt.status != ResponseCode.SUCCESS:
128+
print(f"NFT association failed with status: {ResponseCode.get_name(receipt.status)}")
129+
sys.exit(1)
130+
131+
print("NFT successfully associated with account")
132+
133+
def transfer_nft():
134+
"""
135+
Demonstrates the nft transfer functionality by:
136+
1. Creating a new account
137+
2. Creating a nft
138+
3. Minting a nft
139+
4. Associating the nft with the new account
140+
5. Transferring the nft to the new account
141+
"""
142+
client, operator_id, operator_key = setup_client()
143+
account_id, new_account_private_key = create_test_account(client)
144+
token_id = create_nft(client, operator_id, operator_key)
145+
nft_id = mint_nft(client, token_id, operator_key)
146+
associate_nft(client, account_id, token_id, new_account_private_key)
147+
148+
# Transfer nft to the new account
149+
transfer_transaction = (
150+
TransferTransaction()
151+
.add_nft_transfer(nft_id, operator_id, account_id)
152+
.freeze_with(client)
153+
)
154+
155+
receipt = transfer_transaction.execute(client)
156+
157+
# Check if nft transfer was successful
158+
if receipt.status != ResponseCode.SUCCESS:
159+
print(f"NFT transfer failed with status: {ResponseCode.get_name(receipt.status)}")
160+
sys.exit(1)
161+
162+
print(f"Successfully transferred NFT to account {account_id}")
163+
164+
if __name__ == "__main__":
165+
transfer_nft()

src/hiero_sdk_python/__init__.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@
1818
from .tokens.token_mint_transaction import TokenMintTransaction
1919
from .tokens.token_freeze_transaction import TokenFreezeTransaction
2020
from .tokens.token_unfreeze_transaction import TokenUnfreezeTransaction
21+
from .tokens.token_wipe_transaction import TokenWipeTransaction
2122
from .tokens.token_id import TokenId
2223
from .tokens.nft_id import NftId
24+
from .tokens.token_nft_transfer import TokenNftTransfer
2325

2426
# Transaction
2527
from .transaction.transfer_transaction import TransferTransaction
2628
from .transaction.transaction_id import TransactionId
2729
from .transaction.transaction_receipt import TransactionReceipt
30+
from .transaction.transaction_response import TransactionResponse
2831

2932
# Response / Codes
3033
from .response_code import ResponseCode
@@ -51,6 +54,15 @@
5154
from .query.transaction_get_receipt_query import TransactionGetReceiptQuery
5255
from .query.account_balance_query import CryptoGetAccountBalanceQuery
5356

57+
# Address book
58+
from .address_book.endpoint import Endpoint
59+
from .address_book.node_address import NodeAddress
60+
61+
# Logger
62+
from .logger.logger import Logger
63+
from .logger.log_level import LogLevel
64+
65+
5466
__all__ = [
5567
# Client
5668
"Client",
@@ -72,13 +84,16 @@
7284
"TokenMintTransaction",
7385
"TokenFreezeTransaction",
7486
"TokenUnfreezeTransaction",
87+
"TokenWipeTransaction",
7588
"TokenId",
7689
"NftId",
90+
"TokenNftTransfer",
7791

7892
# Transaction
7993
"TransferTransaction",
8094
"TransactionId",
8195
"TransactionReceipt",
96+
"TransactionResponse",
8297

8398
# Response
8499
"ResponseCode",
@@ -95,8 +110,18 @@
95110
"TopicMessageQuery",
96111
"TransactionGetReceiptQuery",
97112
"CryptoGetAccountBalanceQuery",
98-
113+
114+
# Address book
115+
"Endpoint",
116+
"NodeAddress",
117+
118+
# Logger
119+
"Logger",
120+
"LogLevel",
121+
122+
# HBAR
99123
"Hbar",
100-
"ResponseCode",
124+
125+
# Timestamp
101126
"Timestamp"
102127
]
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from hiero_sdk_python.account.account_id import AccountId
2+
from hiero_sdk_python.hapi.services import basic_types_pb2
3+
4+
class TokenNftTransfer:
5+
"""
6+
Represents a transfer of a non-fungible token (NFT) from one account to another.
7+
8+
This class encapsulates the details of an NFT transfer, including the sender,
9+
receiver, serial number of the NFT, and whether the transfer is approved.
10+
"""
11+
12+
def __init__(self, sender_id, receiver_id, serial_number, is_approved=False):
13+
"""
14+
Initializes a new TokenNftTransfer instance.
15+
16+
Args:
17+
sender_id (AccountId): The account ID of the sender.
18+
receiver_id (AccountId): The account ID of the receiver.
19+
serial_number (int): The serial number of the NFT being transferred.
20+
is_approved (bool, optional): Whether the transfer is approved. Defaults to False.
21+
"""
22+
self.sender_id : AccountId = sender_id
23+
self.receiver_id : AccountId = receiver_id
24+
self.serial_number : int = serial_number
25+
self.is_approved : bool = is_approved
26+
27+
def to_proto(self):
28+
"""
29+
Converts this TokenNftTransfer instance to its protobuf representation.
30+
31+
Returns:
32+
NftTransfer: The protobuf representation of this NFT transfer.
33+
"""
34+
return basic_types_pb2.NftTransfer(
35+
senderAccountID=self.sender_id.to_proto(),
36+
receiverAccountID=self.receiver_id.to_proto(),
37+
serialNumber=self.serial_number,
38+
is_approval=self.is_approved
39+
)
40+
41+
def __str__(self):
42+
"""
43+
Returns a string representation of this TokenNftTransfer instance.
44+
45+
Returns:
46+
str: A string representation of this NFT transfer.
47+
"""
48+
return f"TokenNftTransfer(sender_id={self.sender_id}, receiver_id={self.receiver_id}, serial_number={self.serial_number}, is_approved={self.is_approved})"

src/hiero_sdk_python/transaction/transaction_receipt.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ def accountId(self):
6464
else:
6565
return None
6666

67+
@property
68+
def serial_numbers(self):
69+
"""
70+
Retrieves the serial numbers associated with the transaction receipt, if available.
71+
72+
Returns:
73+
list of int: The serial numbers if present; otherwise, an empty list.
74+
"""
75+
return self._receipt_proto.serialNumbers
76+
6777
def to_proto(self):
6878
"""
6979
Returns the underlying protobuf transaction receipt.

0 commit comments

Comments
 (0)