|
| 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