Asset Balances
Description
Section titled “Description”This example demonstrates how to lookup all holders of an asset using the IndexerClient lookupAssetBalances() method.
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 indexer_client/09-asset-balances.ts/** * Example: Asset Balances * * This example demonstrates how to lookup all holders of an asset using * the IndexerClient lookupAssetBalances() method. * * Prerequisites: * - LocalNet running (via `algokit localnet start`) */
import { createAlgorandClient, createIndexerClient, createRandomAccount, printError, printHeader, printInfo, printStep, printSuccess, shortenAddress,} from '../shared/utils.js'
async function main() { printHeader('Asset Balances Example')
// Create clients const indexer = createIndexerClient() const algorand = createAlgorandClient()
// ========================================================================= // Step 1: Get a funded account and create additional accounts // ========================================================================= printStep(1, 'Setting up accounts for demonstration')
let creatorAddress: string let holder1Address: string let holder2Address: string let holder3Address: string
try { // Get the dispenser account as the creator const dispenser = await algorand.account.dispenserFromEnvironment() creatorAddress = dispenser.addr.toString() printSuccess(`Creator account (dispenser): ${shortenAddress(creatorAddress)}`)
// Create additional accounts to hold the asset const holder1 = await createRandomAccount(algorand) holder1Address = holder1.addr.toString() printSuccess(`Holder 1: ${shortenAddress(holder1Address)}`)
const holder2 = await createRandomAccount(algorand) holder2Address = holder2.addr.toString() printSuccess(`Holder 2: ${shortenAddress(holder2Address)}`)
const holder3 = await createRandomAccount(algorand) holder3Address = holder3.addr.toString() printSuccess(`Holder 3: ${shortenAddress(holder3Address)}`) } catch (error) { printError(`Failed to set up accounts: ${error instanceof Error ? error.message : String(error)}`) printInfo('') printInfo('Make sure LocalNet is running: algokit localnet start') printInfo('If issues persist, try: algokit localnet reset') return }
// ========================================================================= // Step 2: Create a test asset // ========================================================================= printStep(2, 'Creating a test asset')
let assetId: bigint
try { printInfo('Creating test asset: BalanceToken (BAL)...') const result = await algorand.send.assetCreate({ sender: creatorAddress, total: 10_000_000n, // 10,000 units with 3 decimals decimals: 3, assetName: 'BalanceToken', unitName: 'BAL', url: 'https://example.com/balancetoken', defaultFrozen: false, }) assetId = result.assetId printSuccess(`Created BalanceToken with Asset ID: ${assetId}`) } catch (error) { printError(`Failed to create test asset: ${error instanceof Error ? error.message : String(error)}`) printInfo('') printInfo('If LocalNet errors occur, try: algokit localnet reset') return }
// ========================================================================= // Step 3: Distribute asset to multiple accounts // ========================================================================= printStep(3, 'Distributing asset to multiple accounts')
try { // Holder 1: Opt-in and receive 1000 BAL printInfo(`Opting in Holder 1 and sending 1000 BAL...`) await algorand.send.assetOptIn({ sender: holder1Address, assetId, }) await algorand.send.assetTransfer({ sender: creatorAddress, receiver: holder1Address, assetId, amount: 1_000_000n, // 1000 BAL (with 3 decimals) }) printSuccess(`Holder 1 received 1000 BAL`)
// Holder 2: Opt-in and receive 500 BAL printInfo(`Opting in Holder 2 and sending 500 BAL...`) await algorand.send.assetOptIn({ sender: holder2Address, assetId, }) await algorand.send.assetTransfer({ sender: creatorAddress, receiver: holder2Address, assetId, amount: 500_000n, // 500 BAL (with 3 decimals) }) printSuccess(`Holder 2 received 500 BAL`)
// Holder 3: Opt-in only (0 balance but still a holder) printInfo(`Opting in Holder 3 (no transfer, will have 0 balance)...`) await algorand.send.assetOptIn({ sender: holder3Address, assetId, }) printSuccess(`Holder 3 opted in with 0 balance`)
printInfo('') printInfo('Distribution summary:') printInfo(` - Creator: ~8500 BAL (remainder)`) printInfo(` - Holder 1: 1000 BAL`) printInfo(` - Holder 2: 500 BAL`) printInfo(` - Holder 3: 0 BAL (opted-in only)`) } catch (error) { printError(`Failed to distribute asset: ${error instanceof Error ? error.message : String(error)}`) printInfo('') printInfo('If LocalNet errors occur, try: algokit localnet reset') return }
// ========================================================================= // Step 4: Basic lookupAssetBalances() - Get all holders // ========================================================================= printStep(4, 'Looking up all asset holders with lookupAssetBalances()')
try { // lookupAssetBalances() returns all accounts that hold (or have opted into) an asset const balancesResult = await indexer.lookupAssetBalances(assetId)
printSuccess(`Found ${balancesResult.balances.length} holder(s) for Asset ID ${assetId}`) printInfo('')
if (balancesResult.balances.length > 0) { printInfo('Asset balances:') for (const balance of balancesResult.balances) { printInfo(` Address: ${shortenAddress(balance.address)}`) printInfo(` - amount: ${balance.amount.toLocaleString('en-US')}`) printInfo(` - isFrozen: ${balance.isFrozen}`) if (balance.optedInAtRound !== undefined) { printInfo(` - optedInAtRound: ${balance.optedInAtRound}`) } printInfo('') } }
printInfo(`Query performed at round: ${balancesResult.currentRound}`) } catch (error) { printError(`lookupAssetBalances failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Step 5: Filter by currencyGreaterThan // ========================================================================= printStep(5, 'Filtering holders by currencyGreaterThan')
try { // Filter to only show accounts with more than 500 BAL (500,000 base units) printInfo('Querying holders with amount > 500,000 base units (> 500 BAL)...') const highBalanceResult = await indexer.lookupAssetBalances(assetId, { currencyGreaterThan: 500_000n, })
printSuccess(`Found ${highBalanceResult.balances.length} holder(s) with balance > 500 BAL`) for (const balance of highBalanceResult.balances) { printInfo(` ${shortenAddress(balance.address)}: ${balance.amount.toLocaleString('en-US')} base units`) } } catch (error) { printError(`currencyGreaterThan query failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Step 6: Filter by currencyLessThan // ========================================================================= printStep(6, 'Filtering holders by currencyLessThan')
try { // Filter to only show accounts with less than 1,000,000 base units (< 1000 BAL) printInfo('Querying holders with amount < 1,000,000 base units (< 1000 BAL)...') const lowBalanceResult = await indexer.lookupAssetBalances(assetId, { currencyLessThan: 1_000_000n, })
printSuccess(`Found ${lowBalanceResult.balances.length} holder(s) with balance < 1000 BAL`) for (const balance of lowBalanceResult.balances) { printInfo(` ${shortenAddress(balance.address)}: ${balance.amount.toLocaleString('en-US')} base units`) } } catch (error) { printError(`currencyLessThan query failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Step 7: Combine currencyGreaterThan and currencyLessThan (range filter) // ========================================================================= printStep(7, 'Filtering holders by balance range (combining currency filters)')
try { // Filter to show accounts with balance between 100 BAL and 2000 BAL printInfo('Querying holders with 100,000 < amount < 2,000,000 base units (100-2000 BAL)...') const rangeResult = await indexer.lookupAssetBalances(assetId, { currencyGreaterThan: 100_000n, currencyLessThan: 2_000_000n, })
printSuccess(`Found ${rangeResult.balances.length} holder(s) with balance between 100 and 2000 BAL`) for (const balance of rangeResult.balances) { printInfo(` ${shortenAddress(balance.address)}: ${balance.amount.toLocaleString('en-US')} base units`) } } catch (error) { printError(`Range filter query failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Step 8: Using includeAll to include 0 balance accounts // ========================================================================= printStep(8, 'Using includeAll to include accounts with 0 balance')
try { // By default, lookupAssetBalances may exclude accounts with 0 balance // Use includeAll=true to include opted-in accounts with no holdings printInfo('Querying with includeAll=true to include all opted-in accounts...') const allHoldersResult = await indexer.lookupAssetBalances(assetId, { includeAll: true, })
printSuccess(`Found ${allHoldersResult.balances.length} holder(s) (including 0 balance)`) printInfo('')
const zeroBalanceCount = allHoldersResult.balances.filter((b) => b.amount === 0n).length const nonZeroCount = allHoldersResult.balances.filter((b) => b.amount > 0n).length
printInfo(` - Accounts with balance > 0: ${nonZeroCount}`) printInfo(` - Accounts with balance = 0: ${zeroBalanceCount}`) printInfo('')
printInfo('All holders:') for (const balance of allHoldersResult.balances) { const balanceStr = balance.amount === 0n ? '0 (opted-in only)' : balance.amount.toLocaleString('en-US') printInfo(` ${shortenAddress(balance.address)}: ${balanceStr}`) } } catch (error) { printError(`includeAll query failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Step 9: Demonstrate pagination // ========================================================================= printStep(9, 'Demonstrating pagination for assets with many holders')
try { // First query: get only 2 holders printInfo('Querying with limit=2...') const page1 = await indexer.lookupAssetBalances(assetId, { limit: 2, includeAll: true, })
printInfo(`Page 1: Retrieved ${page1.balances.length} holder(s)`) for (const balance of page1.balances) { printInfo(` - ${shortenAddress(balance.address)}: ${balance.amount.toLocaleString('en-US')}`) }
// Check if there are more results if (page1.nextToken) { printInfo(` Next token available: ${page1.nextToken.substring(0, 20)}...`) printInfo('')
// Second query: use the next token to get more results printInfo('Querying next page with next parameter...') const page2 = await indexer.lookupAssetBalances(assetId, { limit: 2, includeAll: true, next: page1.nextToken, })
printInfo(`Page 2: Retrieved ${page2.balances.length} holder(s)`) for (const balance of page2.balances) { printInfo(` - ${shortenAddress(balance.address)}: ${balance.amount.toLocaleString('en-US')}`) }
if (page2.nextToken) { printInfo(` More results available (nextToken present)`) } else { printInfo(` No more results (no nextToken)`) } } else { printInfo(' No pagination needed (all results fit in one page)') } } catch (error) { printError(`Pagination demo failed: ${error instanceof Error ? error.message : String(error)}`) }
// ========================================================================= // Summary // ========================================================================= printHeader('Summary') printInfo('This example demonstrated:') printInfo(' 1. Creating a test asset and distributing to multiple accounts') printInfo(' 2. lookupAssetBalances(assetId) - Get all holders of an asset') printInfo(' 3. Balance fields: address, amount, isFrozen, optedInAtRound') printInfo(' 4. Filtering with currencyGreaterThan (minimum balance)') printInfo(' 5. Filtering with currencyLessThan (maximum balance)') printInfo(' 6. Combining currency filters for range queries') printInfo(' 7. Using includeAll=true to include accounts with 0 balance') printInfo(' 8. Pagination using limit and next parameters') printInfo('') printInfo('Key MiniAssetHolding fields (from lookupAssetBalances):') printInfo(' - address: The account address holding the asset (string)') printInfo(' - amount: Number of base units held (bigint)') printInfo(' - isFrozen: Whether the holding is frozen (boolean)') printInfo(' - optedInAtRound: Round when account opted into asset (optional bigint)') printInfo('') printInfo('Filter parameters:') printInfo(' - currencyGreaterThan: Only return balances > this value (bigint)') printInfo(' - currencyLessThan: Only return balances < this value (bigint)') printInfo(' - includeAll: Include accounts with 0 balance (boolean)') printInfo('') printInfo('Pagination parameters:') printInfo(' - limit: Maximum number of results per page') printInfo(' - next: Token from previous response to get next page')}
main().catch((error) => { console.error('Fatal error:', error) process.exit(1)})