Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.

## [Unreleased]

- Added comprehensive Token Airdrop example script demonstrating token/NFT creation, airdrops, verification, and detailed logging

### Added

- add revenue generating topic tests/example
Expand All @@ -24,6 +26,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- Allowance examples (hbar_allowance.py, token_allowance.py, nft_allowance.py)

### Changed

- TransferTransaction refactored to use TokenTransfer and HbarTransfer classes instead of dictionaries
- Added checksum validation for TokenId
- Refactor examples/token_cancel_airdrop
Expand All @@ -37,7 +40,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
### Fixed

- Incompatible Types assignment in token_transfer_list.py
- Corrected references to __require_not_frozen() to _require_not_frozen() and removed the surplus _is_frozen
- Corrected references to \_\_require_not_frozen() to \_require_not_frozen() and removed the surplus \_is_frozen

## [0.1.5] - 2025-09-25

Expand Down Expand Up @@ -67,8 +70,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
- AccountId support for ECDSA alias accounts
- ContractId.to_evm_address() method for EVM compatibility
- consumeLargeData() function in StatefulContract
- example script for Token Airdrop
- added variables directly in the example script to reduce the need for users to supply extra environment variables.
- Added variables directly in the example script to reduce the need for users to supply extra environment variables.
- Added new `merge_conflicts.md` with detailed guidance on handling conflicts during rebase.
- Type hinting to /tokens, /transaction, /query, /consensus
- Linting to /tokens, /transaction, /query, /consensus
Expand Down Expand Up @@ -135,7 +137,7 @@ contract_call_local_pb2.ContractLoginfo -> contract_types_pb2.ContractLoginfo

- src/hiero_sdk_python/consensus/topic_message.py: from hiero_sdk_python import Timestamp → from hiero_sdk_python.timestamp import Timestamp
- src/hiero_sdk_python/query/topic_message_query.py: from hiero_sdk_python import Client → from hiero_sdk_python.client.client import Client
- src/hiero_sdk_python/tokens/**init**.py: content removed.
- src/hiero_sdk_python/tokens/\_\_init.py\_\_: content removed.
- src/hiero_sdk_python/tokens/token_info.py: from hiero_sdk_python.hapi.services.token_get_info_pb2 import TokenInfo as proto_TokenInfo → from hiero_sdk_python.hapi.services import token_get_info_pb2
- src/hiero_sdk_python/tokens/token_key_validation.py: from hiero_sdk_python.hapi.services → import basic_types_pb2
- src/hiero_sdk_python/tokens/token_kyc_status.py: from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenKycStatus as proto_TokenKycStatus → from hiero_sdk_python.hapi.services import basic_types_pb2
Expand Down
171 changes: 146 additions & 25 deletions examples/token_airdrop.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
TokenAirdropTransaction,
TokenAssociateTransaction,
TokenMintTransaction,
TokenNftInfoQuery,
CryptoGetAccountBalanceQuery,
TokenType,
ResponseCode,
NftId
NftId,
TransactionRecordQuery
)
# Check the transaction record to verify the contents

load_dotenv()

Expand Down Expand Up @@ -57,7 +60,7 @@ def create_account(client, operator_key):

def create_token(client, operator_id, operator_key):
"""Create a fungible token"""
print("\nCreating a token...")
print("\nStep 1: Creating a fungible token (TKA)...")
try:
token_tx = (
TokenCreateTransaction()
Expand All @@ -79,7 +82,7 @@ def create_token(client, operator_id, operator_key):

def create_nft(client, operator_key, operator_id):
"""Create a NFT"""
print("\nCreating a nft...")
print("\nStep 2: Creating a non-fungible token (NFTA)...")
try:
nft_tx = (
TokenCreateTransaction()
Expand All @@ -101,7 +104,7 @@ def create_nft(client, operator_key, operator_id):

def mint_nft(client, operator_key, nft_id):
"""Mint the NFT with metadata"""
print("\nMinting a nft...")
print("\nStep 3: Minting an NFT for NFTA...")
try:
mint_tx = TokenMintTransaction(token_id=nft_id, metadata=[b"NFT data"])
mint_tx.freeze_with(client)
Expand All @@ -117,25 +120,29 @@ def mint_nft(client, operator_key, nft_id):

def associate_tokens(client, recipient_id, recipient_key, tokens):
"""Associate the token and nft with the recipient"""
print("\nAssociating tokens to recipient...")
print("\nStep 4: Associating tokens to recipient...")
try:
assocciate_tx = TokenAssociateTransaction(
associate_tx = TokenAssociateTransaction(
account_id=recipient_id,
token_ids=tokens
)
assocciate_tx.freeze_with(client)
assocciate_tx.sign(recipient_key)
assocciate_tx.execute(client)
associate_tx.freeze_with(client)
associate_tx.sign(recipient_key)
associate_receipt = associate_tx.execute(client)

# Verify association was successful
if associate_receipt.status != ResponseCode.SUCCESS:
print(f"❌ Failed to associate tokens: Status: {associate_receipt.status}")
sys.exit(1)

balance_before = (
CryptoGetAccountBalanceQuery(account_id=recipient_id)
.execute(client)
.token_balances
)
print("Tokens associated with recipient:")
print(f"{tokens[0]}: {balance_before.get(tokens[0])}")
print(f"{tokens[1]}: {balance_before.get(tokens[1])}")

print("Tokens associated with recipient (should be 0 for both):")
print(f" {tokens[0]}: {balance_before.get(tokens[0], 0)}")
print(f" {tokens[1]}: {balance_before.get(tokens[1], 0)}")
print("\n✅ Success! Token association complete.")

except Exception as e:
Expand All @@ -160,14 +167,28 @@ def token_airdrop():
# Create a nft
nft_id = create_nft(client, operator_key, operator_id)

#Mint nft
# Mint nft
serial_number = mint_nft(client, operator_key, nft_id)
print(f"Using NFT with serial #{serial_number} for the airdrop")

# Associate tokens
associate_tokens(client, recipient_id, recipient_key, [token_id, nft_id])

# Airdrop Tthe tokens
print("\nAirdropping tokens...")
# Log balances before airdrop
print("\nStep 5: Checking balances before airdrop...")
sender_balances_before = CryptoGetAccountBalanceQuery(account_id=operator_id).execute(client).token_balances
recipient_balances_before = CryptoGetAccountBalanceQuery(account_id=recipient_id).execute(client).token_balances
print(f"Sender ({operator_id}) balances before airdrop:")
print(f" {token_id}: {sender_balances_before.get(token_id, 0)}")
print(f" {nft_id}: {sender_balances_before.get(nft_id, 0)}")
print(f"Recipient ({recipient_id}) balances before airdrop:")
print(f" {token_id}: {recipient_balances_before.get(token_id, 0)}")
print(f" {nft_id}: {recipient_balances_before.get(nft_id, 0)}")

# Airdrop the tokens
print(f"\nStep 6: Airdropping tokens to recipient {recipient_id}:")
print(f" - 1 fungible token TKA ({token_id})")
print(f" - NFT from NFTA collection ({nft_id}) with serial number #{serial_number}")
try:
airdrop_receipt = (
TokenAirdropTransaction()
Expand All @@ -183,24 +204,124 @@ def token_airdrop():
)

if airdrop_receipt.status != ResponseCode.SUCCESS:
print(f"Fail to cancel airdrop: Status: {airdrop_receipt.status}")
print(f"Fail to airdrop: Status: {airdrop_receipt.status}")
sys.exit(1)

print(f"Token airdrop ID: {airdrop_receipt.transaction_id}")
print("\nVerifying airdrop contents:")
print(f" - Transaction status: {airdrop_receipt.status}")

# Fetch the transaction record and combine three verification sources into one step:
# 1) record.nft_transfers -> exact NFT serial transfers
# 2) balance deltas -> confirm ownership moved
# 3) TokenNftInfoQuery -> confirm current owner for the serial
record = TransactionRecordQuery(transaction_id=airdrop_receipt.transaction_id).execute(client)

after_balance = (
CryptoGetAccountBalanceQuery(account_id=recipient_id)
.execute(client)
.token_balances
)
print("Recipient balance after token airdrop:")
print(f"{token_id}: {after_balance.get(token_id)}")
print(f"{nft_id}: {after_balance.get(nft_id)}")
print(" - Token transfers in this transaction:")
expected_token_transfer = False
for token_id_key, transfers in record.token_transfers.items():
is_expected_token = token_id_key == token_id
token_indicator = "✓ EXPECTED" if is_expected_token else ""
print(f" Token {token_id_key}: {token_indicator}")
# Check for the expected transfer pattern (operator -> recipient)
sender_sent_one = False
recipient_received_one = False
for account, amount in transfers.items():
if amount > 0:
print(f" → {account} received {amount} token(s)")
if account == recipient_id and amount == 1 and is_expected_token:
recipient_received_one = True
else:
print(f" → {account} sent {abs(amount)} token(s)")
if account == operator_id and amount == -1 and is_expected_token:
sender_sent_one = True
if sender_sent_one and recipient_received_one:
expected_token_transfer = True

print("\n - NFT transfers recorded in this transaction:")
nft_transfer_confirmed = False
nft_serials_transferred = []
# record.nft_transfers is expected to be a mapping token_id -> list of nft transfer entries
try:
# record.nft_transfers holds TokenId -> list[TokenNftTransfer]
for token_key, nft_transfers in record.nft_transfers.items():
print(f" NFT Token: {token_key}")
for nft_transfer in nft_transfers:
# nft_transfer is a TokenNftTransfer instance
sender = getattr(nft_transfer, 'sender_id', None)
receiver = getattr(nft_transfer, 'receiver_id', None)
serial = getattr(nft_transfer, 'serial_number', None)
print(f" → serial #{serial}: {sender} -> {receiver}")
nft_serials_transferred.append((token_key, serial, sender, receiver))
# Confirm the exact serial was moved from operator -> recipient
if token_key == nft_id and sender == operator_id and receiver == recipient_id and serial == serial_number:
nft_transfer_confirmed = True
except Exception as ex:
# If unexpected structure, print for debugging
print(f" (Error parsing record.nft_transfers: {ex})")

# Now validate balances and TokenNftInfoQuery for the specific serial
try:
sender_current = CryptoGetAccountBalanceQuery(account_id=operator_id).execute(client).token_balances
recipient_current = CryptoGetAccountBalanceQuery(account_id=recipient_id).execute(client).token_balances

sender_nft_before = sender_balances_before.get(nft_id, 0)
sender_nft_after = sender_current.get(nft_id, 0)
recipient_nft_before = recipient_balances_before.get(nft_id, 0)
recipient_nft_after = recipient_current.get(nft_id, 0)

print(f"\n - NFT balance changes: sender {sender_nft_before} -> {sender_nft_after}, recipient {recipient_nft_before} -> {recipient_nft_after}")

# Query NFT info for the serial to confirm ownership
nft_serial_id = NftId(token_id=nft_id, serial_number=serial_number)
nft_info = TokenNftInfoQuery(nft_id=nft_serial_id).execute(client)
nft_owner = getattr(nft_info, 'account_id', None)

owner_matches = nft_owner == recipient_id

# Combine all checks
fully_verified = nft_transfer_confirmed and (sender_nft_after < sender_nft_before) and (recipient_nft_after > recipient_nft_before) and owner_matches

print(f" - Token transfer verification (fungible): {'OK' if expected_token_transfer else 'MISSING'}")
print(f" - NFT transfer seen in record: {'OK' if nft_transfer_confirmed else 'MISSING'}")
print(f" - NFT owner according to TokenNftInfoQuery: {nft_owner}")
print(f" - NFT owner matches recipient: {'YES' if owner_matches else 'NO'}")

if fully_verified:
print(f"\n ✅ Success! NFT {nft_id} serial #{serial_number} was transferred from {operator_id} to {recipient_id} and verified by record + balances + TokenNftInfoQuery")
else:
print(f"\n ⚠️ Warning: Could not fully verify NFT {nft_id} serial #{serial_number}. Combined checks result: {fully_verified}")
except Exception as e:
print(f" Error during combined NFT verification: {e}")

# Save balances after airdrop for later display
sender_balances_after = CryptoGetAccountBalanceQuery(account_id=operator_id).execute(client).token_balances
recipient_balances_after = CryptoGetAccountBalanceQuery(account_id=recipient_id).execute(client).token_balances

# Log balances after airdrop
sender_balances_after = CryptoGetAccountBalanceQuery(account_id=operator_id).execute(client).token_balances
recipient_balances_after = CryptoGetAccountBalanceQuery(account_id=recipient_id).execute(client).token_balances
print("\nBalances after airdrop:")
print(f"Sender ({operator_id}):")
print(f" {token_id}: {sender_balances_after.get(token_id, 0)}")
print(f" {nft_id}: {sender_balances_after.get(nft_id, 0)}")
print(f"Recipient ({recipient_id}):")
print(f" {token_id}: {recipient_balances_after.get(token_id, 0)}")
print(f" {nft_id}: {recipient_balances_after.get(nft_id, 0)}")

# Summary table
print("\nSummary Table:")
print("+----------------+----------------------+----------------------+----------------------+----------------------+")
print("| Token Type | Token ID | NFT Serial | Sender Balance | Recipient Balance |")
print("+----------------+----------------------+----------------------+----------------------+----------------------+")
print(f"| Fungible (TKA) | {str(token_id):20} | {'N/A':20} | {str(sender_balances_after.get(token_id, 0)):20} | {str(recipient_balances_after.get(token_id, 0)):20} |")
print(f"| NFT (NFTA) | {str(nft_id):20} | #{str(serial_number):19} | {str(sender_balances_after.get(nft_id, 0)):20} | {str(recipient_balances_after.get(nft_id, 0)):20} |")
print("+----------------+----------------------+----------------------+----------------------+----------------------+")
print("\n✅ Success! Token Airdrop transaction successful")
except Exception as e:
print(f"❌ Error airdropping tokens: {e}")
sys.exit(1)

if __name__ == "__main__":
token_airdrop()

67 changes: 66 additions & 1 deletion examples/token_cancel_airdrop.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,43 @@ def create_token(client, operator_id, operator_key, token_name, token_symbol, in
token_id = receipt.token_id
print(f"Created token {token_name} with ID: {token_id}")
return token_id
# Create two new tokens.
print("\nStep 1: Creating two new fungible tokens...")
try:
tx1 = TokenCreateTransaction().set_token_name("First Token").set_token_symbol("TKA").set_initial_supply(1).set_treasury_account_id(operator_id)
receipt1 = tx1.freeze_with(client).sign(operator_key).execute(client)
token_id_1 = receipt1.token_id

tx2 = TokenCreateTransaction().set_token_name("Second Token").set_token_symbol("TKB").set_initial_supply(1).set_treasury_account_id(operator_id)
receipt2 = tx2.freeze_with(client).sign(operator_key).execute(client)
token_id_2 = receipt2.token_id

print(f"✅ Created tokens: {token_id_1} (TKA) and {token_id_2} (TKB)")
except Exception as e:
print(f"Error creating token {token_name}: {e}")
sys.exit(1)

# Log balances before airdrop
print("\nStep 2: Checking balances before airdrop...")
from hiero_sdk_python import CryptoGetAccountBalanceQuery
sender_balances_before = CryptoGetAccountBalanceQuery(account_id=operator_id).execute(client).token_balances

recipient_balances_before = CryptoGetAccountBalanceQuery(account_id=recipient_id).execute(client).token_balances
print(f"Sender ({operator_id}) balances before airdrop:")
print(f" {str(token_id_1)}: {sender_balances_before.get(str(token_id_1), 0)}")

print(f" {str(token_id_2)}: {sender_balances_before.get(str(token_id_2), 0)}")

print(f"Recipient ({recipient_id}) balances before airdrop:")

print(f" {str(token_id_1)}: {recipient_balances_before.get(str(token_id_1), 0)}")

print(f" {str(token_id_2)}: {recipient_balances_before.get(str(token_id_2), 0)}")

def airdrop_tokens(client, operator_id, operator_key, recipient_id, token_ids):
"""Airdrop the provided tokens to a recipient account."""
print("\nAirdropping tokens...")
print(f"\nStep 3: Airdroppingping tokens TKA ({token_id_1}) and TKB ({token_id_2}) to recipient {recipient_id}...")

try:
tx = TokenAirdropTransaction()
for token_id in token_ids:
Expand All @@ -93,6 +122,42 @@ def airdrop_tokens(client, operator_id, operator_key, recipient_id, token_ids):

airdrop_record = TransactionRecordQuery(receipt.transaction_id).execute(client)
return airdrop_record.new_pending_airdrops
# Log balances after airdrop
sender_balances_after = CryptoGetAccountBalanceQuery(account_id=operator_id).execute(client).token_balances

recipient_balances_after = CryptoGetAccountBalanceQuery(account_id=recipient_id).execute(client).token_balances
print("\nBalances after airdrop:")

print(f"Sender ({operator_id}):")

print(f" {str(token_id_1)}: {sender_balances_after.get(str(token_id_1), 0)}")

print(f" {str(token_id_2)}: {sender_balances_after.get(str(token_id_2), 0)}")

print(f"Recipient ({recipient_id}):")

print(f" {str(token_id_1)}: {recipient_balances_after.get(str(token_id_1), 0)}")

print(f" {str(token_id_2)}: {recipient_balances_after.get(str(token_id_2), 0)}")



# Summary table

print("\nSummary Table:")

print("+----------------+----------------------+----------------------+----------------------+\n"

"| Token Symbol | Token ID | Sender Balance | Recipient Balance |\n"

"+----------------+----------------------+----------------------+----------------------+")

print(f"| TKA | {str(token_id_1):<20} | {str(sender_balances_after.get(str(token_id_1), 0)):<20} | {str(recipient_balances_after.get(str(token_id_1), 0)):<20} |")

print(f"| TKB | {str(token_id_2):<20} | {str(sender_balances_after.get(str(token_id_2), 0)):<20} | {str(recipient_balances_after.get(str(token_id_2), 0)):<20} |")

print("+----------------+----------------------+----------------------+----------------------+")

except Exception as e:
print(f"Error airdropping tokens: {e}")
sys.exit(1)
Expand Down