Skip to content
Algorand Developer Portal

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