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.getById() to fetch asset information by asset ID
- algorand.asset.getAccountInformation() to get an account’s asset holding
- algorand.asset.bulkOptIn() to opt into multiple assets at once
- algorand.asset.bulkOptOut() 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 running (via
algokit localnet start)
Run This Example
Section titled “Run This Example”From the repository root:
cd examplesnpm run example algorand_client/11-asset-manager.ts/** * Example: Asset Manager * * This example demonstrates the AssetManager functionality for querying * asset information and performing bulk opt-in/opt-out operations: * - algorand.asset.getById() to fetch asset information by asset ID * - algorand.asset.getAccountInformation() to get an account's asset holding * - algorand.asset.bulkOptIn() to opt into multiple assets at once * - algorand.asset.bulkOptOut() 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: * - LocalNet running (via `algokit localnet start`) */
import { AlgorandClient, algo } from '@algorandfoundation/algokit-utils'import { printError, printHeader, printInfo, printStep, printSuccess, shortenAddress } from '../shared/utils.js'
async function main() { printHeader('Asset Manager Example')
// Initialize client and verify LocalNet is running const algorand = AlgorandClient.defaultLocalNet()
try { await algorand.client.algod.status() printSuccess('Connected to LocalNet') } catch (error) { printError(`Failed to connect to LocalNet: ${error instanceof Error ? error.message : String(error)}`) printInfo('Make sure LocalNet is running (e.g., algokit localnet start)') return }
// Step 1: Create and fund test accounts printStep(1, 'Create and fund test accounts') printInfo('Creating accounts for asset manager demonstrations')
const creator = algorand.account.random() const holder = algorand.account.random()
printInfo(`\nCreated accounts:`) printInfo(` Creator: ${shortenAddress(creator.addr.toString())}`) printInfo(` Holder: ${shortenAddress(holder.addr.toString())}`)
// Fund accounts await algorand.account.ensureFundedFromEnvironment(creator.addr, algo(20)) await algorand.account.ensureFundedFromEnvironment(holder.addr, algo(10))
printSuccess('Created and funded test accounts')
// Step 2: Create test assets printStep(2, 'Create test assets') printInfo('Creating multiple assets to demonstrate bulk operations')
// Create first asset const asset1Result = await algorand.send.assetCreate({ sender: creator.addr, total: 1_000_000n, decimals: 2, assetName: 'Asset Manager Token 1', unitName: 'AMT1', url: 'https://example.com/amt1', manager: creator.addr, }) const asset1Id = asset1Result.assetId
// Create second asset const asset2Result = await algorand.send.assetCreate({ sender: creator.addr, total: 500_000n, decimals: 0, assetName: 'Asset Manager Token 2', unitName: 'AMT2', url: 'https://example.com/amt2', manager: creator.addr, }) const asset2Id = asset2Result.assetId
// Create third asset const asset3Result = await algorand.send.assetCreate({ sender: creator.addr, total: 10_000_000n, decimals: 6, assetName: 'Asset Manager Token 3', unitName: 'AMT3', url: 'https://example.com/amt3', manager: creator.addr, }) const asset3Id = asset3Result.assetId
printInfo(`\nCreated assets:`) printInfo(` Asset 1 (AMT1): ID ${asset1Id}`) printInfo(` Asset 2 (AMT2): ID ${asset2Id}`) printInfo(` Asset 3 (AMT3): ID ${asset3Id}`)
printSuccess('Test assets created')
// Step 3: Demonstrate algorand.asset.getById() printStep(3, 'Demonstrate algorand.asset.getById() to fetch asset information') printInfo('Fetching detailed information about an asset by its ID')
const assetInfo = await algorand.asset.getById(asset1Id)
printInfo(`\nAsset information for ID ${asset1Id}:`) printInfo(` Asset ID (index): ${assetInfo.assetId}`) printInfo(` Name: ${assetInfo.assetName}`) printInfo(` Unit Name: ${assetInfo.unitName}`) printInfo(` Total Supply: ${assetInfo.total} (smallest units)`) printInfo(` Decimals: ${assetInfo.decimals}`) printInfo(` Creator: ${shortenAddress(assetInfo.creator.toString())}`) printInfo(` Manager: ${assetInfo.manager ? shortenAddress(assetInfo.manager.toString()) : 'none'}`) printInfo(` Reserve: ${assetInfo.reserve ? shortenAddress(assetInfo.reserve.toString()) : 'none'}`) printInfo(` Freeze: ${assetInfo.freeze ? shortenAddress(assetInfo.freeze.toString()) : 'none'}`) printInfo(` Clawback: ${assetInfo.clawback ? shortenAddress(assetInfo.clawback.toString()) : 'none'}`) printInfo(` Default Frozen: ${assetInfo.defaultFrozen}`) printInfo(` URL: ${assetInfo.url ?? 'none'}`)
// Show all three assets for comparison printInfo(`\nComparing all created assets:`) const asset2Info = await algorand.asset.getById(asset2Id) const asset3Info = await algorand.asset.getById(asset3Id)
printInfo(`\n Asset 1: ${assetInfo.assetName} (${assetInfo.unitName})`) printInfo(` Total: ${assetInfo.total} | Decimals: ${assetInfo.decimals}`) printInfo(` Asset 2: ${asset2Info.assetName} (${asset2Info.unitName})`) printInfo(` Total: ${asset2Info.total} | Decimals: ${asset2Info.decimals}`) printInfo(` Asset 3: ${asset3Info.assetName} (${asset3Info.unitName})`) printInfo(` Total: ${asset3Info.total} | Decimals: ${asset3Info.decimals}`)
printSuccess('Asset information retrieved')
// Step 4: Handle case where asset doesn't exist printStep(4, 'Handle case where asset does not exist') printInfo('Demonstrating error handling for non-existent asset IDs')
const nonExistentAssetId = 999999999n printInfo(`\nAttempting to fetch asset with ID ${nonExistentAssetId}...`)
try { await algorand.asset.getById(nonExistentAssetId) printError('Expected an error but none was thrown!') } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) printInfo(` Error caught: Asset not found`) printInfo(` Error details: ${errorMessage.slice(0, 80)}...`) }
printSuccess('Non-existent asset handled correctly')
// Step 5: Demonstrate algorand.asset.bulkOptIn() printStep(5, 'Demonstrate algorand.asset.bulkOptIn() to opt into multiple assets at once') printInfo('Bulk opt-in is more efficient than individual opt-ins') printInfo('Transactions are batched in groups of up to 16')
const assetIds = [asset1Id, asset2Id, asset3Id] printInfo(`\nOpting holder into ${assetIds.length} assets in a single batch...`)
const bulkOptInResults = await algorand.asset.bulkOptIn(holder.addr, assetIds, { suppressLog: true, // Suppress SDK logs for cleaner output })
printInfo(`\nBulk opt-in results:`) for (const result of bulkOptInResults) { printInfo(` Asset ${result.assetId}: Transaction ${result.transactionId.slice(0, 20)}...`) }
printInfo(`\nTotal transactions: ${bulkOptInResults.length}`) printInfo(`Efficiency: ${assetIds.length} assets opted in with a single method call`)
printSuccess('Bulk opt-in completed')
// Step 6: Demonstrate algorand.asset.getAccountInformation() printStep(6, 'Demonstrate algorand.asset.getAccountInformation() to get account asset holding') printInfo('Fetching holder\'s asset holding information after opt-in')
const holdingInfo1 = await algorand.asset.getAccountInformation(holder.addr, asset1Id)
printInfo(`\nHolder's holding for Asset ${asset1Id}:`) printInfo(` Asset ID: ${holdingInfo1.assetId}`) printInfo(` Balance: ${holdingInfo1.balance} (smallest units)`) printInfo(` Frozen: ${holdingInfo1.frozen}`)
// Transfer some assets to show non-zero balance printInfo(`\nTransferring assets to holder...`) await algorand.send.assetTransfer({ sender: creator.addr, receiver: holder.addr, assetId: asset1Id, amount: 10_000n, // 100 whole tokens (100 * 10^2) }) await algorand.send.assetTransfer({ sender: creator.addr, receiver: holder.addr, assetId: asset2Id, amount: 500n, })
// Re-fetch holding info const holdingInfo1Updated = await algorand.asset.getAccountInformation(holder.addr, asset1Id) const holdingInfo2 = await algorand.asset.getAccountInformation(holder.addr, asset2Id) const holdingInfo3 = await algorand.asset.getAccountInformation(holder.addr, asset3Id)
printInfo(`\nUpdated holder balances:`) printInfo(` Asset ${asset1Id} (AMT1): ${holdingInfo1Updated.balance} (100 tokens with 2 decimals)`) printInfo(` Asset ${asset2Id} (AMT2): ${holdingInfo2.balance} (500 whole units)`) printInfo(` Asset ${asset3Id} (AMT3): ${holdingInfo3.balance} (no transfers yet)`)
printSuccess('Account asset information retrieved')
// Step 7: Handle case where account not opted in printStep(7, 'Handle case where account is not opted in') printInfo('Demonstrating error handling when querying for assets not opted in')
// Create a new account that hasn't opted in to any assets const nonOptedAccount = algorand.account.random() await algorand.account.ensureFundedFromEnvironment(nonOptedAccount.addr, algo(1))
printInfo(`\nQuerying asset holding for account that hasn't opted in...`)
try { await algorand.asset.getAccountInformation(nonOptedAccount.addr, asset1Id) printError('Expected an error but none was thrown!') } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) printInfo(` Error caught: Account not opted in to asset`) printInfo(` Error details: ${errorMessage.slice(0, 80)}...`) }
printSuccess('Non-opted-in account handled correctly')
// Step 8: Compare bulk operations vs individual opt-ins printStep(8, 'Show how bulk operations are more efficient than individual opt-ins') printInfo('Comparing the approaches for clarity')
printInfo(`\nIndividual opt-in approach (NOT RECOMMENDED for multiple assets):`) printInfo(` // Requires 3 separate transactions and 3 API calls`) printInfo(` await algorand.send.assetOptIn({ sender, assetId: asset1Id })`) printInfo(` await algorand.send.assetOptIn({ sender, assetId: asset2Id })`) printInfo(` await algorand.send.assetOptIn({ sender, assetId: asset3Id })`)
printInfo(`\nBulk opt-in approach (RECOMMENDED):`) printInfo(` // Single method call, transactions batched in groups of 16`) printInfo(` await algorand.asset.bulkOptIn(account, [asset1Id, asset2Id, asset3Id])`)
printInfo(`\nEfficiency benefits:`) printInfo(` - Single method call for any number of assets`) printInfo(` - Automatic batching (up to 16 transactions per group)`) printInfo(` - Reduced code complexity`) printInfo(` - Better error handling with clear result mapping`)
printSuccess('Efficiency comparison demonstrated')
// Step 9: Prepare for bulk opt-out by transferring assets back printStep(9, 'Prepare for bulk opt-out') printInfo('Before opting out, all asset balances must be zero') printInfo('Transferring all held assets back to creator')
// Transfer assets back const currentBalance1 = await algorand.asset.getAccountInformation(holder.addr, asset1Id) const currentBalance2 = await algorand.asset.getAccountInformation(holder.addr, asset2Id)
if (currentBalance1.balance > 0n) { await algorand.send.assetTransfer({ sender: holder.addr, receiver: creator.addr, assetId: asset1Id, amount: currentBalance1.balance, }) printInfo(` Transferred ${currentBalance1.balance} units of Asset ${asset1Id} back to creator`) }
if (currentBalance2.balance > 0n) { await algorand.send.assetTransfer({ sender: holder.addr, receiver: creator.addr, assetId: asset2Id, amount: currentBalance2.balance, }) printInfo(` Transferred ${currentBalance2.balance} units of Asset ${asset2Id} back to creator`) }
// Verify zero balances const finalBalance1 = await algorand.asset.getAccountInformation(holder.addr, asset1Id) const finalBalance2 = await algorand.asset.getAccountInformation(holder.addr, asset2Id) const finalBalance3 = await algorand.asset.getAccountInformation(holder.addr, asset3Id)
printInfo(`\nVerified zero balances:`) printInfo(` Asset ${asset1Id}: ${finalBalance1.balance}`) printInfo(` Asset ${asset2Id}: ${finalBalance2.balance}`) printInfo(` Asset ${asset3Id}: ${finalBalance3.balance}`)
printSuccess('Ready for bulk opt-out')
// Step 10: Demonstrate algorand.asset.bulkOptOut() printStep(10, 'Demonstrate algorand.asset.bulkOptOut() to opt out of multiple assets at once') printInfo('Bulk opt-out validates zero balances by default (ensureZeroBalance: true)')
const optOutAssetIds = [asset1Id, asset2Id, asset3Id] printInfo(`\nOpting holder out of ${optOutAssetIds.length} assets in a single batch...`)
const bulkOptOutResults = await algorand.asset.bulkOptOut(holder.addr, optOutAssetIds, { ensureZeroBalance: true, // Default - validates balances before opting out suppressLog: true, })
printInfo(`\nBulk opt-out results:`) for (const result of bulkOptOutResults) { printInfo(` Asset ${result.assetId}: Transaction ${result.transactionId.slice(0, 20)}...`) }
printInfo(`\nTotal transactions: ${bulkOptOutResults.length}`) printInfo(`Efficiency: ${optOutAssetIds.length} assets opted out with a single method call`)
// Verify opt-out printInfo(`\nVerifying holder is no longer opted in...`) for (const assetId of optOutAssetIds) { try { await algorand.asset.getAccountInformation(holder.addr, assetId) printError(`Holder should not be opted in to asset ${assetId}!`) } catch { printInfo(` Asset ${assetId}: Confirmed not opted in`) } }
printSuccess('Bulk opt-out completed')
// Step 11: Demonstrate error handling for bulk opt-out with non-zero balance printStep(11, 'Demonstrate error handling: bulk opt-out with non-zero balance') printInfo('bulkOptOut throws an error if ensureZeroBalance is true and balance is non-zero')
// First, opt back in to an asset and transfer some tokens await algorand.send.assetOptIn({ sender: holder.addr, assetId: asset1Id, }) await algorand.send.assetTransfer({ sender: creator.addr, receiver: holder.addr, assetId: asset1Id, amount: 100n, })
printInfo(`\nHolder has balance of 100 for Asset ${asset1Id}`) printInfo(`Attempting bulk opt-out with ensureZeroBalance: true...`)
try { await algorand.asset.bulkOptOut(holder.addr, [asset1Id], { ensureZeroBalance: true, }) printError('Expected an error but none was thrown!') } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) printInfo(` Error caught: Non-zero balance prevents opt-out`) printInfo(` Error details: ${errorMessage.slice(0, 100)}...`) }
// Clean up - transfer back and opt out await algorand.send.assetTransfer({ sender: holder.addr, receiver: creator.addr, assetId: asset1Id, amount: 100n, }) await algorand.send.assetOptOut({ sender: holder.addr, assetId: asset1Id, creator: creator.addr, ensureZeroBalance: true, })
printSuccess('Error handling demonstrated')
// Step 12: Summary printStep(12, 'Summary - Asset Manager API') printInfo('The AssetManager provides efficient asset operations:') printInfo('') printInfo('algorand.asset.getById(assetId):') printInfo(' - Fetches complete asset information by ID') printInfo(' - Returns: AssetInformation object with all asset properties') printInfo(' - Properties: assetId, assetName, unitName, total, decimals,') printInfo(' creator, manager, reserve, freeze, clawback, defaultFrozen, url') printInfo('') printInfo('algorand.asset.getAccountInformation(account, assetId):') printInfo(' - Fetches an account\'s holding for a specific asset') printInfo(' - Returns: AccountAssetInformation object') printInfo(' - Properties: assetId, balance, frozen') printInfo(' - Throws if account is not opted in to the asset') printInfo('') printInfo('algorand.asset.bulkOptIn(account, assetIds, options?):') printInfo(' - Opts an account into multiple assets at once') printInfo(' - Batches transactions in groups of 16 (max atomic group size)') printInfo(' - Returns: Array of { assetId, transactionId }') printInfo(' - More efficient than individual assetOptIn calls') printInfo('') printInfo('algorand.asset.bulkOptOut(account, assetIds, options?):') printInfo(' - Opts an account out of multiple assets at once') printInfo(' - ensureZeroBalance: true (default) validates balances first') printInfo(' - Batches transactions in groups of 16') printInfo(' - Returns: Array of { assetId, transactionId }') printInfo(' - Automatically fetches asset creators for opt-out transactions') printInfo('') printInfo('Bulk operation benefits:') printInfo(' - Single method call for any number of assets') printInfo(' - Automatic batching for optimal efficiency') printInfo(' - Consistent result format with asset ID to transaction ID mapping') printInfo(' - Built-in validation for safe opt-out operations')
// Clean up - destroy test assets await algorand.send.assetDestroy({ sender: creator.addr, assetId: asset1Id }) await algorand.send.assetDestroy({ sender: creator.addr, assetId: asset2Id }) await algorand.send.assetDestroy({ sender: creator.addr, assetId: asset3Id })
printSuccess('Asset Manager example completed!')}
main().catch((error) => { printError(`Unhandled error: ${error instanceof Error ? error.message : String(error)}`) process.exit(1)})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
- Transaction Leases