Skip to content

Asset Manager

← Back to Algorand Client

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
  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example algorand_client/11-asset-manager.ts

View source on GitHub

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