Skip to content

Asset Balances

← Back to Indexer Client

This example demonstrates how to lookup all holders of an asset using the IndexerClient lookupAssetBalances() method.

  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example indexer_client/09-asset-balances.ts

View source on GitHub

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