Asset Manager
Description
Section titled “Description”This example demonstrates the AssetManager functionality for querying asset information and performing bulk opt-in/opt-out operations:
- algorand.asset.get_by_id() to fetch asset information by asset ID
- algorand.asset.get_account_information() to get an account’s asset holding
- algorand.asset.bulk_opt_in() to opt into multiple assets at once
- algorand.asset.bulk_opt_out() to opt out of multiple assets at once
- Efficiency comparison: bulk operations vs individual opt-ins
- Error handling for non-existent assets and non-opted-in accounts
Prerequisites
Section titled “Prerequisites”LocalNet required for asset operations
Run This Example
Section titled “Run This Example”From the repository’s examples directory:
cd examplesuv run python algorand_client/11_asset_manager.py# ruff: noqa: N999, C901, PLR0912, PLR0915, PLR2004"""Example: Asset Manager
This example demonstrates the AssetManager functionality for queryingasset information and performing bulk opt-in/opt-out operations:- algorand.asset.get_by_id() to fetch asset information by asset ID- algorand.asset.get_account_information() to get an account's asset holding- algorand.asset.bulk_opt_in() to opt into multiple assets at once- algorand.asset.bulk_opt_out() to opt out of multiple assets at once- Efficiency comparison: bulk operations vs individual opt-ins- Error handling for non-existent assets and non-opted-in accounts
LocalNet required for asset operations"""
from shared import ( print_error, print_header, print_info, print_step, print_success, shorten_address,)
from algokit_utils import AlgoAmount, AlgorandClientfrom algokit_utils.transactions.types import ( AssetCreateParams, AssetDestroyParams, AssetOptInParams, AssetOptOutParams, AssetTransferParams,)
def main() -> None: print_header("Asset Manager Example")
# Initialize client and verify LocalNet is running algorand = AlgorandClient.default_localnet()
try: algorand.client.algod.status() print_success("Connected to LocalNet") except Exception as e: print_error(f"Failed to connect to LocalNet: {e}") print_info("Make sure LocalNet is running (e.g., algokit localnet start)") return
# Step 1: Create and fund test accounts print_step(1, "Create and fund test accounts") print_info("Creating accounts for asset manager demonstrations")
creator = algorand.account.random() holder = algorand.account.random()
print_info("") print_info("Created accounts:") print_info(f" Creator: {shorten_address(str(creator.addr))}") print_info(f" Holder: {shorten_address(str(holder.addr))}")
# Fund accounts algorand.account.ensure_funded_from_environment(creator.addr, AlgoAmount.from_algo(20)) algorand.account.ensure_funded_from_environment(holder.addr, AlgoAmount.from_algo(10))
print_success("Created and funded test accounts")
# Step 2: Create test assets print_step(2, "Create test assets") print_info("Creating multiple assets to demonstrate bulk operations")
# Create first asset asset1_result = algorand.send.asset_create( AssetCreateParams( sender=creator.addr, total=1_000_000, decimals=2, asset_name="Asset Manager Token 1", unit_name="AMT1", url="https://example.com/amt1", manager=creator.addr, ) ) asset1_id = asset1_result.asset_id
# Create second asset asset2_result = algorand.send.asset_create( AssetCreateParams( sender=creator.addr, total=500_000, decimals=0, asset_name="Asset Manager Token 2", unit_name="AMT2", url="https://example.com/amt2", manager=creator.addr, ) ) asset2_id = asset2_result.asset_id
# Create third asset asset3_result = algorand.send.asset_create( AssetCreateParams( sender=creator.addr, total=10_000_000, decimals=6, asset_name="Asset Manager Token 3", unit_name="AMT3", url="https://example.com/amt3", manager=creator.addr, ) ) asset3_id = asset3_result.asset_id
print_info("") print_info("Created assets:") print_info(f" Asset 1 (AMT1): ID {asset1_id}") print_info(f" Asset 2 (AMT2): ID {asset2_id}") print_info(f" Asset 3 (AMT3): ID {asset3_id}")
print_success("Test assets created")
# Step 3: Demonstrate algorand.asset.get_by_id() print_step(3, "Demonstrate algorand.asset.get_by_id() to fetch asset information") print_info("Fetching detailed information about an asset by its ID")
asset_info = algorand.asset.get_by_id(asset1_id)
print_info("") print_info(f"Asset information for ID {asset1_id}:") print_info(f" Asset ID (index): {asset_info.asset_id}") print_info(f" Name: {asset_info.asset_name}") print_info(f" Unit Name: {asset_info.unit_name}") print_info(f" Total Supply: {asset_info.total} (smallest units)") print_info(f" Decimals: {asset_info.decimals}") print_info(f" Creator: {shorten_address(str(asset_info.creator))}") manager_str = shorten_address(str(asset_info.manager)) if asset_info.manager else "none" print_info(f" Manager: {manager_str}") reserve_str = shorten_address(str(asset_info.reserve)) if asset_info.reserve else "none" print_info(f" Reserve: {reserve_str}") freeze_str = shorten_address(str(asset_info.freeze)) if asset_info.freeze else "none" print_info(f" Freeze: {freeze_str}") clawback_str = shorten_address(str(asset_info.clawback)) if asset_info.clawback else "none" print_info(f" Clawback: {clawback_str}") print_info(f" Default Frozen: {asset_info.default_frozen}") print_info(f" URL: {asset_info.url or 'none'}")
# Show all three assets for comparison print_info("") print_info("Comparing all created assets:") asset2_info = algorand.asset.get_by_id(asset2_id) asset3_info = algorand.asset.get_by_id(asset3_id)
print_info("") print_info(f" Asset 1: {asset_info.asset_name} ({asset_info.unit_name})") print_info(f" Total: {asset_info.total} | Decimals: {asset_info.decimals}") print_info(f" Asset 2: {asset2_info.asset_name} ({asset2_info.unit_name})") print_info(f" Total: {asset2_info.total} | Decimals: {asset2_info.decimals}") print_info(f" Asset 3: {asset3_info.asset_name} ({asset3_info.unit_name})") print_info(f" Total: {asset3_info.total} | Decimals: {asset3_info.decimals}")
print_success("Asset information retrieved")
# Step 4: Handle case where asset doesn't exist print_step(4, "Handle case where asset does not exist") print_info("Demonstrating error handling for non-existent asset IDs")
non_existent_asset_id = 999999999 print_info("") print_info(f"Attempting to fetch asset with ID {non_existent_asset_id}...")
try: algorand.asset.get_by_id(non_existent_asset_id) print_error("Expected an error but none was thrown!") except Exception as e: error_message = str(e) print_info(" Error caught: Asset not found") print_info(f" Error details: {error_message[:80]}...")
print_success("Non-existent asset handled correctly")
# Step 5: Demonstrate algorand.asset.bulk_opt_in() print_step(5, "Demonstrate algorand.asset.bulk_opt_in() to opt into multiple assets at once") print_info("Bulk opt-in is more efficient than individual opt-ins") print_info("Transactions are batched in groups of up to 16")
asset_ids = [asset1_id, asset2_id, asset3_id] print_info("") print_info(f"Opting holder into {len(asset_ids)} assets in a single batch...")
bulk_opt_in_results = algorand.asset.bulk_opt_in( holder.addr, asset_ids, )
print_info("") print_info("Bulk opt-in results:") for result in bulk_opt_in_results: tx_id_short = result.transaction_id[:20] print_info(f" Asset {result.asset_id}: Transaction {tx_id_short}...")
print_info("") print_info(f"Total transactions: {len(bulk_opt_in_results)}") print_info(f"Efficiency: {len(asset_ids)} assets opted in with a single method call")
print_success("Bulk opt-in completed")
# Step 6: Demonstrate algorand.asset.get_account_information() print_step(6, "Demonstrate algorand.asset.get_account_information() to get account asset holding") print_info("Fetching holder's asset holding information after opt-in")
holding_info1 = algorand.asset.get_account_information(holder.addr, asset1_id)
print_info("") print_info(f"Holder's holding for Asset {asset1_id}:") print_info(f" Asset ID: {holding_info1.asset_id}") print_info(f" Balance: {holding_info1.balance} (smallest units)") print_info(f" Frozen: {holding_info1.frozen}")
# Transfer some assets to show non-zero balance print_info("") print_info("Transferring assets to holder...") algorand.send.asset_transfer( AssetTransferParams( sender=creator.addr, receiver=holder.addr, asset_id=asset1_id, amount=10_000, # 100 whole tokens (100 * 10^2) ) ) algorand.send.asset_transfer( AssetTransferParams( sender=creator.addr, receiver=holder.addr, asset_id=asset2_id, amount=500, ) )
# Re-fetch holding info holding_info1_updated = algorand.asset.get_account_information(holder.addr, asset1_id) holding_info2 = algorand.asset.get_account_information(holder.addr, asset2_id) holding_info3 = algorand.asset.get_account_information(holder.addr, asset3_id)
print_info("") print_info("Updated holder balances:") print_info(f" Asset {asset1_id} (AMT1): {holding_info1_updated.balance} (100 tokens with 2 decimals)") print_info(f" Asset {asset2_id} (AMT2): {holding_info2.balance} (500 whole units)") print_info(f" Asset {asset3_id} (AMT3): {holding_info3.balance} (no transfers yet)")
print_success("Account asset information retrieved")
# Step 7: Handle case where account not opted in print_step(7, "Handle case where account is not opted in") print_info("Demonstrating error handling when querying for assets not opted in")
# Create a new account that hasn't opted in to any assets non_opted_account = algorand.account.random() algorand.account.ensure_funded_from_environment(non_opted_account.addr, AlgoAmount.from_algo(1))
print_info("") print_info("Querying asset holding for account that hasn't opted in...")
try: algorand.asset.get_account_information(non_opted_account.addr, asset1_id) print_error("Expected an error but none was thrown!") except Exception as e: error_message = str(e) print_info(" Error caught: Account not opted in to asset") print_info(f" Error details: {error_message[:80]}...")
print_success("Non-opted-in account handled correctly")
# Step 8: Compare bulk operations vs individual opt-ins print_step(8, "Show how bulk operations are more efficient than individual opt-ins") print_info("Comparing the approaches for clarity")
print_info("") print_info("Individual opt-in approach (NOT RECOMMENDED for multiple assets):") print_info(" # Requires 3 separate transactions and 3 API calls") print_info(" algorand.send.asset_opt_in(AssetOptInParams(sender=addr, asset_id=asset1_id))") print_info(" algorand.send.asset_opt_in(AssetOptInParams(sender=addr, asset_id=asset2_id))") print_info(" algorand.send.asset_opt_in(AssetOptInParams(sender=addr, asset_id=asset3_id))")
print_info("") print_info("Bulk opt-in approach (RECOMMENDED):") print_info(" # Single method call, transactions batched in groups of 16") print_info(" algorand.asset.bulk_opt_in(account, [asset1_id, asset2_id, asset3_id])")
print_info("") print_info("Efficiency benefits:") print_info(" - Single method call for any number of assets") print_info(" - Automatic batching (up to 16 transactions per group)") print_info(" - Reduced code complexity") print_info(" - Better error handling with clear result mapping")
print_success("Efficiency comparison demonstrated")
# Step 9: Prepare for bulk opt-out by transferring assets back print_step(9, "Prepare for bulk opt-out") print_info("Before opting out, all asset balances must be zero") print_info("Transferring all held assets back to creator")
# Transfer assets back current_balance1 = algorand.asset.get_account_information(holder.addr, asset1_id) current_balance2 = algorand.asset.get_account_information(holder.addr, asset2_id)
if current_balance1.balance > 0: algorand.send.asset_transfer( AssetTransferParams( sender=holder.addr, receiver=creator.addr, asset_id=asset1_id, amount=current_balance1.balance, ) ) print_info(f" Transferred {current_balance1.balance} units of Asset {asset1_id} back to creator")
if current_balance2.balance > 0: algorand.send.asset_transfer( AssetTransferParams( sender=holder.addr, receiver=creator.addr, asset_id=asset2_id, amount=current_balance2.balance, ) ) print_info(f" Transferred {current_balance2.balance} units of Asset {asset2_id} back to creator")
# Verify zero balances final_balance1 = algorand.asset.get_account_information(holder.addr, asset1_id) final_balance2 = algorand.asset.get_account_information(holder.addr, asset2_id) final_balance3 = algorand.asset.get_account_information(holder.addr, asset3_id)
print_info("") print_info("Verified zero balances:") print_info(f" Asset {asset1_id}: {final_balance1.balance}") print_info(f" Asset {asset2_id}: {final_balance2.balance}") print_info(f" Asset {asset3_id}: {final_balance3.balance}")
print_success("Ready for bulk opt-out")
# Step 10: Demonstrate algorand.asset.bulk_opt_out() print_step(10, "Demonstrate algorand.asset.bulk_opt_out() to opt out of multiple assets at once") print_info("Bulk opt-out validates zero balances by default (ensure_zero_balance=True)")
opt_out_asset_ids = [asset1_id, asset2_id, asset3_id] print_info("") print_info(f"Opting holder out of {len(opt_out_asset_ids)} assets in a single batch...")
bulk_opt_out_results = algorand.asset.bulk_opt_out( account=holder.addr, asset_ids=opt_out_asset_ids, ensure_zero_balance=True, # Default - validates balances before opting out )
print_info("") print_info("Bulk opt-out results:") for result in bulk_opt_out_results: tx_id_short = result.transaction_id[:20] print_info(f" Asset {result.asset_id}: Transaction {tx_id_short}...")
print_info("") print_info(f"Total transactions: {len(bulk_opt_out_results)}") print_info(f"Efficiency: {len(opt_out_asset_ids)} assets opted out with a single method call")
# Verify opt-out print_info("") print_info("Verifying holder is no longer opted in...") for asset_id in opt_out_asset_ids: try: algorand.asset.get_account_information(holder.addr, asset_id) print_error(f"Holder should not be opted in to asset {asset_id}!") except Exception: print_info(f" Asset {asset_id}: Confirmed not opted in")
print_success("Bulk opt-out completed")
# Step 11: Demonstrate error handling for bulk opt-out with non-zero balance print_step(11, "Demonstrate error handling: bulk opt-out with non-zero balance") print_info("bulk_opt_out throws an error if ensure_zero_balance is True and balance is non-zero")
# First, opt back in to an asset and transfer some tokens algorand.send.asset_opt_in( AssetOptInParams( sender=holder.addr, asset_id=asset1_id, ) ) algorand.send.asset_transfer( AssetTransferParams( sender=creator.addr, receiver=holder.addr, asset_id=asset1_id, amount=100, ) )
print_info("") print_info(f"Holder has balance of 100 for Asset {asset1_id}") print_info("Attempting bulk opt-out with ensure_zero_balance=True...")
try: algorand.asset.bulk_opt_out( account=holder.addr, asset_ids=[asset1_id], ensure_zero_balance=True, ) print_error("Expected an error but none was thrown!") except Exception as e: error_message = str(e) print_info(" Error caught: Non-zero balance prevents opt-out") print_info(f" Error details: {error_message[:100]}...")
# Clean up - transfer back and opt out algorand.send.asset_transfer( AssetTransferParams( sender=holder.addr, receiver=creator.addr, asset_id=asset1_id, amount=100, ) ) algorand.send.asset_opt_out( AssetOptOutParams( sender=holder.addr, asset_id=asset1_id, creator=creator.addr, ), ensure_zero_balance=True, )
print_success("Error handling demonstrated")
# Step 12: Summary print_step(12, "Summary - Asset Manager API") print_info("The AssetManager provides efficient asset operations:") print_info("") print_info("algorand.asset.get_by_id(asset_id):") print_info(" - Fetches complete asset information by ID") print_info(" - Returns: AssetInformation object with all asset properties") print_info(" - Properties: asset_id, asset_name, unit_name, total, decimals,") print_info(" creator, manager, reserve, freeze, clawback, default_frozen, url") print_info("") print_info("algorand.asset.get_account_information(account, asset_id):") print_info(" - Fetches an account's holding for a specific asset") print_info(" - Returns: AccountAssetInformation object") print_info(" - Properties: asset_id, balance, frozen") print_info(" - Throws if account is not opted in to the asset") print_info("") print_info("algorand.asset.bulk_opt_in(account, asset_ids):") print_info(" - Opts an account into multiple assets at once") print_info(" - Batches transactions in groups of 16 (max atomic group size)") print_info(" - Returns: list[BulkAssetOptInOutResult] with asset_id and transaction_id") print_info(" - More efficient than individual asset_opt_in calls") print_info("") print_info("algorand.asset.bulk_opt_out(account=..., asset_ids=..., ensure_zero_balance=True):") print_info(" - Opts an account out of multiple assets at once") print_info(" - ensure_zero_balance=True (default) validates balances first") print_info(" - Batches transactions in groups of 16") print_info(" - Returns: list[BulkAssetOptInOutResult] with asset_id and transaction_id") print_info(" - Automatically fetches asset creators for opt-out transactions") print_info("") print_info("Bulk operation benefits:") print_info(" - Single method call for any number of assets") print_info(" - Automatic batching for optimal efficiency") print_info(" - Consistent result format with asset ID to transaction ID mapping") print_info(" - Built-in validation for safe opt-out operations")
# Clean up - destroy test assets algorand.send.asset_destroy(AssetDestroyParams(sender=creator.addr, asset_id=asset1_id)) algorand.send.asset_destroy(AssetDestroyParams(sender=creator.addr, asset_id=asset2_id)) algorand.send.asset_destroy(AssetDestroyParams(sender=creator.addr, asset_id=asset3_id))
print_success("Asset Manager example completed!")
if __name__ == "__main__": main()Other examples in Algorand Client
Section titled “Other examples in Algorand Client”- Client Instantiation
- AlgoAmount Utility
- Signer Configuration
- Suggested Params Configuration
- Account Manager
- Send Payment
- Send Asset Operations
- Send Application Operations
- Create Transaction (Unsigned Transactions)
- Transaction Composer (Atomic Transaction Groups)
- Asset Manager
- App Manager
- App Deployer
- Client Manager
- Error Transformers