Skip to content

Account Applications

← Back to Indexer Client

This example demonstrates how to query account application relationships using the IndexerClient lookupAccountCreatedApplications() and lookupAccountAppLocalStates() methods.

  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example indexer_client/04-account-applications.ts

View source on GitHub

04-account-applications.ts
/**
* Example: Account Applications
*
* This example demonstrates how to query account application relationships using
* the IndexerClient lookupAccountCreatedApplications() and lookupAccountAppLocalStates() methods.
*
* Prerequisites:
* - LocalNet running (via `algokit localnet start`)
*/
import {
assignFee,
OnApplicationComplete,
Transaction,
TransactionType,
type AppCallTransactionFields,
} from '@algorandfoundation/algokit-utils/transact'
import {
createAlgodClient,
createAlgorandClient,
createIndexerClient,
loadTealSource,
printError,
printHeader,
printInfo,
printStep,
printSuccess,
shortenAddress,
} from '../shared/utils.js'
/**
* Wait for a transaction to be confirmed
*/
async function waitForConfirmation(
algod: ReturnType<typeof createAlgodClient>,
txId: string,
maxRounds = 10,
): Promise<Record<string, unknown>> {
let lastRound = (await algod.status()).lastRound
const startRound = lastRound
while (lastRound < startRound + BigInt(maxRounds)) {
const pendingInfo = await algod.pendingTransactionInformation(txId)
if (pendingInfo.confirmedRound && pendingInfo.confirmedRound > 0n) {
return pendingInfo as Record<string, unknown>
}
lastRound = (await algod.statusAfterBlock(lastRound)).lastRound
}
throw new Error(`Transaction ${txId} not confirmed after ${maxRounds} rounds`)
}
async function main() {
printHeader('Account Applications Example')
// Create clients
const indexer = createIndexerClient()
const algorand = createAlgorandClient()
const algod = createAlgodClient()
// =========================================================================
// Step 1: Get a funded account from LocalNet
// =========================================================================
printStep(1, 'Getting a funded account from LocalNet')
let creatorAddress: string
let creatorAccount: Awaited<ReturnType<typeof algorand.account.kmd.getLocalNetDispenserAccount>>
try {
creatorAccount = await algorand.account.kmd.getLocalNetDispenserAccount()
creatorAddress = creatorAccount.addr.toString()
printSuccess(`Using dispenser account: ${shortenAddress(creatorAddress)}`)
} catch (error) {
printError(`Failed to get dispenser account: ${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: Deploy test applications using AlgorandClient
// =========================================================================
printStep(2, 'Deploying test applications for demonstration')
let appId1: bigint
let appId2: bigint
try {
// Load approval program from shared artifacts that stores a counter in global state and supports local state
const approvalSource = loadTealSource('approval-lifecycle-counter.teal')
// Load clear state program from shared artifacts
const clearSource = loadTealSource('clear-state-approve.teal')
// Compile TEAL programs
printInfo('Compiling TEAL programs...')
const approvalResult = await algod.tealCompile(approvalSource)
const approvalProgram = new Uint8Array(Buffer.from(approvalResult.result, 'base64'))
const clearResult = await algod.tealCompile(clearSource)
const clearStateProgram = new Uint8Array(Buffer.from(clearResult.result, 'base64'))
printInfo(`Approval program: ${approvalProgram.length} bytes`)
printInfo(`Clear state program: ${clearStateProgram.length} bytes`)
printInfo('')
// Create first application
printInfo('Creating first test application: DemoApp1...')
const suggestedParams = await algod.suggestedParams()
const appCallFields1: AppCallTransactionFields = {
appId: 0n,
onComplete: OnApplicationComplete.NoOp,
approvalProgram,
clearStateProgram,
globalStateSchema: { numUints: 1, numByteSlices: 0 },
localStateSchema: { numUints: 1, numByteSlices: 0 },
}
const createAppTx1 = new Transaction({
type: TransactionType.AppCall,
sender: creatorAccount.addr,
firstValid: suggestedParams.firstValid,
lastValid: suggestedParams.lastValid,
genesisHash: suggestedParams.genesisHash,
genesisId: suggestedParams.genesisId,
appCall: appCallFields1,
})
const createAppTxWithFee1 = assignFee(createAppTx1, {
feePerByte: suggestedParams.fee,
minFee: suggestedParams.minFee,
})
const signedCreateTx1 = await creatorAccount.signer([createAppTxWithFee1], [0])
await algod.sendRawTransaction(signedCreateTx1)
const createPendingInfo1 = await waitForConfirmation(algod, createAppTxWithFee1.txId())
appId1 = createPendingInfo1.appId as bigint
printSuccess(`Created DemoApp1 with Application ID: ${appId1}`)
// Create second application
printInfo('Creating second test application: DemoApp2...')
const suggestedParams2 = await algod.suggestedParams()
const appCallFields2: AppCallTransactionFields = {
appId: 0n,
onComplete: OnApplicationComplete.NoOp,
approvalProgram,
clearStateProgram,
globalStateSchema: { numUints: 2, numByteSlices: 1 },
localStateSchema: { numUints: 2, numByteSlices: 1 },
}
const createAppTx2 = new Transaction({
type: TransactionType.AppCall,
sender: creatorAccount.addr,
firstValid: suggestedParams2.firstValid,
lastValid: suggestedParams2.lastValid,
genesisHash: suggestedParams2.genesisHash,
genesisId: suggestedParams2.genesisId,
appCall: appCallFields2,
})
const createAppTxWithFee2 = assignFee(createAppTx2, {
feePerByte: suggestedParams2.fee,
minFee: suggestedParams2.minFee,
})
const signedCreateTx2 = await creatorAccount.signer([createAppTxWithFee2], [0])
await algod.sendRawTransaction(signedCreateTx2)
const createPendingInfo2 = await waitForConfirmation(algod, createAppTxWithFee2.txId())
appId2 = createPendingInfo2.appId as bigint
printSuccess(`Created DemoApp2 with Application ID: ${appId2}`)
printInfo('')
} catch (error) {
printError(`Failed to create test applications: ${error instanceof Error ? error.message : String(error)}`)
printInfo('')
printInfo('If LocalNet errors occur, try: algokit localnet reset')
return
}
// =========================================================================
// Step 3: Opt into an application to create local state
// =========================================================================
printStep(3, 'Opting into an application to create local state')
try {
const optInParams = await algod.suggestedParams()
const optInFields: AppCallTransactionFields = {
appId: appId1,
onComplete: OnApplicationComplete.OptIn,
}
const optInTx = new Transaction({
type: TransactionType.AppCall,
sender: creatorAccount.addr,
firstValid: optInParams.firstValid,
lastValid: optInParams.lastValid,
genesisHash: optInParams.genesisHash,
genesisId: optInParams.genesisId,
appCall: optInFields,
})
const optInTxWithFee = assignFee(optInTx, {
feePerByte: optInParams.fee,
minFee: optInParams.minFee,
})
printInfo(`Opting into app ${appId1}...`)
const signedOptInTx = await creatorAccount.signer([optInTxWithFee], [0])
await algod.sendRawTransaction(signedOptInTx)
await waitForConfirmation(algod, optInTxWithFee.txId())
printSuccess(`Successfully opted into app ${appId1}`)
printInfo('')
} catch (error) {
printError(`Failed to opt into application: ${error instanceof Error ? error.message : String(error)}`)
// Continue anyway to demonstrate lookups
}
// =========================================================================
// Step 4: Lookup created applications with lookupAccountCreatedApplications()
// =========================================================================
printStep(4, 'Looking up created applications with lookupAccountCreatedApplications()')
try {
// lookupAccountCreatedApplications() returns applications created by an account
const createdAppsResult = await indexer.lookupAccountCreatedApplications(creatorAddress)
printSuccess(`Found ${createdAppsResult.applications.length} application(s) created by account`)
printInfo('')
if (createdAppsResult.applications.length > 0) {
printInfo('Created applications:')
for (const app of createdAppsResult.applications) {
printInfo(` Application ID: ${app.id}`)
if (app.params.creator) {
printInfo(` - creator: ${shortenAddress(app.params.creator.toString())}`)
}
if (app.params.approvalProgram) {
printInfo(` - approvalProgram: ${app.params.approvalProgram.length} bytes`)
}
if (app.params.clearStateProgram) {
printInfo(` - clearStateProgram: ${app.params.clearStateProgram.length} bytes`)
}
if (app.params.globalStateSchema) {
printInfo(` - globalStateSchema: ${app.params.globalStateSchema.numUints} uints, ${app.params.globalStateSchema.numByteSlices} byte slices`)
}
if (app.params.localStateSchema) {
printInfo(` - localStateSchema: ${app.params.localStateSchema.numUints} uints, ${app.params.localStateSchema.numByteSlices} byte slices`)
}
if (app.createdAtRound !== undefined) {
printInfo(` - createdAtRound: ${app.createdAtRound}`)
}
if (app.params.globalState && app.params.globalState.length > 0) {
printInfo(` - globalState: ${app.params.globalState.length} key-value pair(s)`)
}
printInfo('')
}
}
printInfo(`Query performed at round: ${createdAppsResult.currentRound}`)
} catch (error) {
printError(`lookupAccountCreatedApplications failed: ${error instanceof Error ? error.message : String(error)}`)
}
// =========================================================================
// Step 5: Lookup account app local states with lookupAccountAppLocalStates()
// =========================================================================
printStep(5, 'Looking up app local states with lookupAccountAppLocalStates()')
try {
// lookupAccountAppLocalStates() returns local state for applications the account has opted into
const localStatesResult = await indexer.lookupAccountAppLocalStates(creatorAddress)
printSuccess(`Found ${localStatesResult.appsLocalStates.length} app local state(s) for account`)
printInfo('')
if (localStatesResult.appsLocalStates.length > 0) {
printInfo('App local states:')
for (const localState of localStatesResult.appsLocalStates) {
printInfo(` Application ID: ${localState.id}`)
printInfo(` - schema: ${localState.schema.numUints} uints, ${localState.schema.numByteSlices} byte slices`)
if (localState.optedInAtRound !== undefined) {
printInfo(` - optedInAtRound: ${localState.optedInAtRound}`)
}
if (localState.keyValue && localState.keyValue.length > 0) {
printInfo(` - keyValue pairs: ${localState.keyValue.length}`)
for (const kv of localState.keyValue) {
// Decode the key from bytes to string
const keyStr = new TextDecoder().decode(kv.key)
// Value type: 1 = bytes, 2 = uint
if (kv.value.type === 2) {
printInfo(` - "${keyStr}": ${kv.value.uint} (uint)`)
} else {
const valueStr = new TextDecoder().decode(kv.value.bytes)
printInfo(` - "${keyStr}": "${valueStr}" (bytes)`)
}
}
} else {
printInfo(` - keyValue: (empty)`)
}
printInfo('')
}
}
printInfo(`Query performed at round: ${localStatesResult.currentRound}`)
} catch (error) {
printError(`lookupAccountAppLocalStates failed: ${error instanceof Error ? error.message : String(error)}`)
}
// =========================================================================
// Step 6: Demonstrate pagination with limit parameter
// =========================================================================
printStep(6, 'Demonstrating pagination with limit parameter')
try {
// First query: get only 1 created application
printInfo('Querying created applications with limit=1...')
const page1 = await indexer.lookupAccountCreatedApplications(creatorAddress, { limit: 1 })
printInfo(`Page 1: Retrieved ${page1.applications.length} application(s)`)
if (page1.applications.length > 0) {
printInfo(` - Application ID: ${page1.applications[0].id}`)
}
// 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.lookupAccountCreatedApplications(creatorAddress, {
limit: 1,
next: page1.nextToken,
})
printInfo(`Page 2: Retrieved ${page2.applications.length} application(s)`)
if (page2.applications.length > 0) {
printInfo(` - Application ID: ${page2.applications[0].id}`)
}
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)}`)
}
// =========================================================================
// Step 7: Query specific application with applicationId filter
// =========================================================================
printStep(7, 'Querying specific application with applicationId filter')
try {
// You can filter lookupAccountCreatedApplications by a specific applicationId
printInfo(`Querying created application with ID ${appId1} only...`)
const specificResult = await indexer.lookupAccountCreatedApplications(creatorAddress, {
applicationId: appId1,
})
if (specificResult.applications.length > 0) {
const app = specificResult.applications[0]
printSuccess(`Found created application with ID ${appId1}`)
if (app.params.globalStateSchema) {
printInfo(` - globalStateSchema: ${app.params.globalStateSchema.numUints} uints, ${app.params.globalStateSchema.numByteSlices} byte slices`)
}
if (app.params.localStateSchema) {
printInfo(` - localStateSchema: ${app.params.localStateSchema.numUints} uints, ${app.params.localStateSchema.numByteSlices} byte slices`)
}
} else {
printInfo(`No created application found with ID ${appId1}`)
}
} catch (error) {
printError(`Specific application query failed: ${error instanceof Error ? error.message : String(error)}`)
}
// =========================================================================
// Step 8: Query specific local state with applicationId filter
// =========================================================================
printStep(8, 'Querying specific local state with applicationId filter')
try {
// You can also filter lookupAccountAppLocalStates by a specific applicationId
printInfo(`Querying local state for application ID ${appId1} only...`)
const specificLocalState = await indexer.lookupAccountAppLocalStates(creatorAddress, {
applicationId: appId1,
})
if (specificLocalState.appsLocalStates.length > 0) {
const localState = specificLocalState.appsLocalStates[0]
printSuccess(`Found local state for application ID ${appId1}`)
printInfo(` - schema: ${localState.schema.numUints} uints, ${localState.schema.numByteSlices} byte slices`)
if (localState.keyValue && localState.keyValue.length > 0) {
printInfo(` - keyValue pairs: ${localState.keyValue.length}`)
}
} else {
printInfo(`No local state found for application ID ${appId1}`)
}
} catch (error) {
printError(`Specific local state query failed: ${error instanceof Error ? error.message : String(error)}`)
}
// =========================================================================
// Summary
// =========================================================================
printHeader('Summary')
printInfo('This example demonstrated:')
printInfo(' 1. Deploying test applications using TEAL compilation and TransactionType.AppCall')
printInfo(' 2. Opting into an application to create local state')
printInfo(' 3. lookupAccountCreatedApplications(address) - Get applications created by an account')
printInfo(' 4. lookupAccountAppLocalStates(address) - Get application local states for an account')
printInfo(' 5. Pagination using limit and next parameters')
printInfo(' 6. Filtering by specific applicationId')
printInfo('')
printInfo('Key Application fields (from lookupAccountCreatedApplications):')
printInfo(' - id: The application identifier (bigint)')
printInfo(' - params.creator: Address that created the application')
printInfo(' - params.approvalProgram: TEAL bytecode for approval logic (Uint8Array)')
printInfo(' - params.clearStateProgram: TEAL bytecode for clear state logic (Uint8Array)')
printInfo(' - params.globalStateSchema: {numUints, numByteSlices} for global storage')
printInfo(' - params.localStateSchema: {numUints, numByteSlices} for per-user storage')
printInfo(' - params.globalState: Array of TealKeyValue for current global state')
printInfo(' - createdAtRound: Round when application was created (optional bigint)')
printInfo('')
printInfo('Key ApplicationLocalState fields (from lookupAccountAppLocalStates):')
printInfo(' - id: The application identifier (bigint)')
printInfo(' - schema: {numUints, numByteSlices} for allocated local storage')
printInfo(' - optedInAtRound: Round when account opted in (optional bigint)')
printInfo(' - keyValue: Array of TealKeyValue for current local state')
printInfo('')
printInfo('TealKeyValue structure:')
printInfo(' - key: The key as Uint8Array (decode with TextDecoder)')
printInfo(' - value.type: 1 for bytes, 2 for uint')
printInfo(' - value.bytes: Byte value as Uint8Array')
printInfo(' - value.uint: Integer value as bigint')
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)
})