Skip to content

Commit 89b1391

Browse files
authored
feat: tokenclaimairdrop transaction (#730)
Signed-off-by: exploreriii <133720349+exploreriii@users.noreply.github.com>
1 parent 94d6fac commit 89b1391

File tree

9 files changed

+1580
-10
lines changed

9 files changed

+1580
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
1010
- added FreezeTransaction class
1111
- added FreezeType class
1212
- 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)
13+
- feat: TokenAirdropClaim Transaction, examples (with signing required and not), unit and integration tests (#201)
1314
- docs: Add Google-style docstrings to `TokenId` class and its methods in `token_id.py`.
1415
- added Google-style docstrings to the `TransactionRecord` class including all dataclass fields, `__repr__`, `_from_proto()` & `_to_proto()` methods.
1516
- Standardized docstrings, improved error handling, and updated type hinting (`str | None` to `Optional[str]`) for the `FileId` class (#652).
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
"""
2+
Hedera Token Airdrop Example Script
3+
4+
This script demonstrates and end-to-end example for an account to automatically (no user action required) claim a set of airdrops.
5+
6+
Unique configurations of this account:
7+
- 10 auto-association slots.
8+
- Does not require a signature to claim the airdrop.
9+
The Hedera network will auto-associate the token and claim it on airdrop.
10+
11+
This script demonstrates:
12+
- Setting up a Hedera client
13+
- Creating fungible and NFT tokens
14+
- Creating a receiver account with unique configurations
15+
- Performing token airdrops to the receiver
16+
- Checking balances for verification purposes.
17+
18+
Run this script using:
19+
uv run examples/token_airdrop_pending_claim_signature_not_required_auto_airdrop.py
20+
python examples/token_airdrop_pending_claim_signature_not_required_auto_airdrop.py
21+
"""
22+
import os
23+
import sys
24+
from typing import Iterable
25+
from dotenv import load_dotenv
26+
from hiero_sdk_python import (
27+
Client,
28+
Network,
29+
AccountId,
30+
PrivateKey,
31+
AccountCreateTransaction,
32+
TokenCreateTransaction,
33+
TokenMintTransaction,
34+
TokenAirdropTransaction,
35+
TokenType,
36+
SupplyType,
37+
NftId,
38+
CryptoGetAccountBalanceQuery,
39+
ResponseCode,
40+
Hbar,
41+
TokenId,
42+
TokenNftInfoQuery
43+
)
44+
45+
load_dotenv()
46+
47+
def setup_client():
48+
network_name = os.getenv("NETWORK", "testnet")
49+
50+
# Validate environment variables
51+
if not os.getenv("OPERATOR_ID") or not os.getenv("OPERATOR_KEY"):
52+
print("❌ Missing OPERATOR_ID or OPERATOR_KEY in .env file.")
53+
sys.exit(1)
54+
55+
try:
56+
network = Network(network_name)
57+
client = Client(network)
58+
59+
operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", ''))
60+
operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", ''))
61+
client.set_operator(operator_id, operator_key)
62+
63+
except Exception as e:
64+
raise ConnectionError(f"Error initializing client: {e}")
65+
66+
print(f"✅ Connected to Hedera {network_name} network as operator: {operator_id}")
67+
return client, operator_id, operator_key
68+
69+
def create_receiver(
70+
client: Client,
71+
signature_required: bool =False,
72+
max_auto_assoc: int =10
73+
):
74+
receiver_key = PrivateKey.generate()
75+
receiver_public_key = receiver_key.public_key()
76+
77+
try:
78+
receipt = (
79+
AccountCreateTransaction()
80+
.set_key(receiver_public_key)
81+
.set_initial_balance(Hbar(1))
82+
.set_receiver_signature_required(signature_required)
83+
.set_max_automatic_token_associations(max_auto_assoc)
84+
.freeze_with(client)
85+
.sign(receiver_key)
86+
.execute(client)
87+
)
88+
if receipt.status != ResponseCode.SUCCESS:
89+
status_message = ResponseCode(receipt.status).name
90+
raise RuntimeError(f"❌ Receiver account creation failed: {status_message}")
91+
92+
receiver_id = receipt.account_id
93+
print(
94+
f"✅ Receiver account {receiver_id} created "
95+
f"(auto-assoc={max_auto_assoc}, sig_required={signature_required})"
96+
)
97+
return receiver_id, receiver_key
98+
except Exception as e:
99+
raise RuntimeError(f"❌ Error creating receiver account: {e}") from e
100+
101+
102+
def create_fungible_token(
103+
client: Client,
104+
operator_id: AccountId,
105+
operator_key: PrivateKey,
106+
name: str ="My Fungible Token",
107+
symbol: str ="MFT",
108+
initial_supply: int =50,
109+
max_supply: int = 1000,
110+
):
111+
try:
112+
receipt = (
113+
TokenCreateTransaction()
114+
.set_token_name(name)
115+
.set_token_symbol(symbol)
116+
.set_initial_supply(initial_supply)
117+
.set_token_type(TokenType.FUNGIBLE_COMMON)
118+
.set_supply_type(SupplyType.FINITE)
119+
.set_max_supply(max_supply)
120+
.set_treasury_account_id(operator_id)
121+
.freeze_with(client)
122+
.sign(operator_key)
123+
.execute(client)
124+
)
125+
token_id = receipt.token_id
126+
if receipt.status != ResponseCode.SUCCESS:
127+
status_message = ResponseCode(receipt.status).name
128+
raise RuntimeError(f"❌ Fungible token creation failed: {status_message}")
129+
130+
print(f"✅ Fungible token created: {token_id}")
131+
return token_id
132+
except Exception as e:
133+
raise RuntimeError(f"❌ Error creating fungible token: {e}") from e
134+
135+
136+
def create_nft_token(
137+
client: Client,
138+
operator_id: AccountId,
139+
operator_key: PrivateKey,
140+
name: str ="My NFT Token",
141+
symbol: str ="MNT",
142+
max_supply: int = 100
143+
):
144+
try:
145+
receipt = (
146+
TokenCreateTransaction()
147+
.set_token_name(name)
148+
.set_token_symbol(symbol)
149+
.set_initial_supply(0)
150+
.set_token_type(TokenType.NON_FUNGIBLE_UNIQUE)
151+
.set_supply_type(SupplyType.FINITE)
152+
.set_max_supply(max_supply)
153+
.set_treasury_account_id(operator_id)
154+
.set_supply_key(operator_key)
155+
.freeze_with(client)
156+
.sign(operator_key)
157+
.execute(client)
158+
)
159+
token_id = receipt.token_id
160+
if receipt.status != ResponseCode.SUCCESS:
161+
status_message = ResponseCode(receipt.status).name
162+
raise RuntimeError(f"❌ NFT token creation failed: {status_message}")
163+
164+
print(f"✅ NFT token created: {token_id}")
165+
return token_id
166+
except Exception as e:
167+
raise RuntimeError(f"❌ Error creating NFT token: {e}") from e
168+
169+
170+
def mint_nft_token(
171+
client: Client,
172+
operator_key: PrivateKey,
173+
nft_token_id: TokenId,
174+
):
175+
try:
176+
receipt = (
177+
TokenMintTransaction()
178+
.set_token_id(nft_token_id)
179+
.set_metadata([b"NFT Metadata Example"])
180+
.freeze_with(client)
181+
.sign(operator_key)
182+
.execute(client)
183+
)
184+
total_supply = receipt._receipt_proto.newTotalSupply
185+
serial = receipt.serial_numbers[0]
186+
nft_id = NftId(nft_token_id, serial)
187+
if receipt.status != ResponseCode.SUCCESS:
188+
status_message = ResponseCode(receipt.status).name
189+
raise RuntimeError(f"❌ NFT token mint failed: {status_message}")
190+
191+
print(f"✅ NFT {nft_token_id} serial {serial} minted with NFT id of {nft_id}. Total NFT supply is {total_supply} ")
192+
return nft_id
193+
except Exception as e:
194+
raise RuntimeError(f"❌ Error minting NFT token: {e}") from e
195+
def log_balances(
196+
client: Client,
197+
operator_id: AccountId,
198+
receiver_id: AccountId,
199+
fungible_ids: Iterable[TokenId],
200+
nft_ids: Iterable[NftId],
201+
prefix: str = ""
202+
):
203+
print(f"\n===== {prefix} Balances =====")
204+
205+
try:
206+
operator_balance = CryptoGetAccountBalanceQuery().set_account_id(operator_id).execute(client)
207+
receiver_balance = CryptoGetAccountBalanceQuery().set_account_id(receiver_id).execute(client)
208+
except Exception as e:
209+
print(f"❌ Failed to fetch balances: {e}")
210+
return
211+
212+
def log_fungible(account_id: AccountId, balances: dict, token_ids: Iterable[TokenId]):
213+
print(" Fungible tokens:")
214+
for token_id in token_ids:
215+
print(f" {token_id}: {balances.get(token_id, 0)}")
216+
217+
def log_nfts(account_id: AccountId, nft_ids: Iterable[NftId]):
218+
print(" NFTs:")
219+
owned = []
220+
for nft_id in nft_ids:
221+
try:
222+
info = TokenNftInfoQuery().set_nft_id(nft_id).execute(client)
223+
if info.account_id == account_id:
224+
owned.append(str(nft_id))
225+
except Exception as e:
226+
print(f" ⚠️ Error fetching NFT {nft_id}: {e}")
227+
if owned:
228+
for nft in owned:
229+
print(f" {nft}")
230+
else:
231+
print(" (none)")
232+
233+
print(f"\nSender ({operator_id}):")
234+
log_fungible(operator_id, dict(operator_balance.token_balances), fungible_ids)
235+
log_nfts(operator_id, nft_ids)
236+
237+
print(f"\nReceiver ({receiver_id}):")
238+
log_fungible(receiver_id, dict(receiver_balance.token_balances), fungible_ids)
239+
log_nfts(receiver_id, nft_ids)
240+
241+
print("=============================================\n")
242+
243+
def perform_airdrop(
244+
client: Client,
245+
operator_id: AccountId,
246+
operator_key: PrivateKey,
247+
receiver_id: AccountId,
248+
fungible_ids: Iterable[TokenId],
249+
nft_ids: Iterable[NftId],
250+
ft_amount: int = 100
251+
):
252+
253+
try:
254+
tx = TokenAirdropTransaction()
255+
256+
for fungible_id in fungible_ids:
257+
tx.add_token_transfer(fungible_id, operator_id, -ft_amount)
258+
tx.add_token_transfer(fungible_id, receiver_id, ft_amount)
259+
print(f"📤 Transferring {ft_amount} of fungible token {fungible_id} from {operator_id}{receiver_id}")
260+
261+
for nft_id in nft_ids:
262+
tx.add_nft_transfer(nft_id, operator_id, receiver_id)
263+
print(f"🎨 Transferring NFT {nft_id} from {operator_id}{receiver_id}")
264+
265+
print("\n⏳ Submitting airdrop transaction...")
266+
receipt = tx.freeze_with(client).sign(operator_key).execute(client)
267+
268+
if receipt.status != ResponseCode.SUCCESS:
269+
status_message = ResponseCode(receipt.status).name
270+
raise RuntimeError(f"Airdrop transaction failed with status: {status_message}")
271+
272+
print(f"✅ Airdrop executed successfully! Transaction ID: {receipt.transaction_id}")
273+
274+
except Exception as e:
275+
print(f"❌ Airdrop failed: {e}")
276+
raise RuntimeError("Airdrop execution failed") from e
277+
278+
def main():
279+
# Set up client and return client, operator_id, operator_key
280+
client, operator_id, operator_key = setup_client()
281+
282+
# Create and return a fungible token to airdrop
283+
print("Create 50 fungible tokens and 1 NFT to airdrop")
284+
fungible_id = create_fungible_token(client, operator_id, operator_key, name="My Fungible Token", symbol="123", initial_supply=50, max_supply = 2000)
285+
286+
# Create and return an nft token to airdrop
287+
nft_token_id = create_nft_token(client, operator_id, operator_key, name="My NFT Token", symbol = "MNFT", max_supply=1000)
288+
289+
# Mint and return an nft to airdrop
290+
nft_serial = mint_nft_token(client, operator_key, nft_token_id)
291+
292+
# Create a receiver that will test no signature is required to claim the auto-airdrop
293+
# Ensure false for signature required
294+
# Assume 10 max association slots
295+
# Return the receiver id and receiver private key
296+
print("Creating the account that will automatically receive the airdropped tokens")
297+
receiver_id, receiver_key = create_receiver(client, False, 10)
298+
299+
# Check pre-airdrop balances
300+
print("\n🔍 Verifying sender has tokens to airdrop and receiver neither:")
301+
log_balances(client, operator_id, receiver_id, [fungible_id], [nft_serial], prefix="Before airdrop")
302+
303+
# Initiate airdrop of 20 fungible tokens and 1 nft token id
304+
perform_airdrop(client, operator_id, operator_key, receiver_id, [fungible_id], [nft_serial], 20)
305+
306+
print("\n🔍 Verifying receiver has received airdrop contents automatically and sender has sent:")
307+
log_balances(client, operator_id, receiver_id, [fungible_id], [nft_serial], prefix="After airdrop")
308+
309+
print("✅ Auto-association successful: Receiver accepted airdropped tokens without pre-association.")
310+
print("✅ Airdrop successful: Receiver accepted new fungible tokens without pre-association.")
311+
312+
if __name__ == "__main__":
313+
main()

0 commit comments

Comments
 (0)