Skip to content

Application Logs Lookup

← Back to Indexer Client

This example demonstrates how to lookup application logs using the IndexerClient lookupApplicationLogsById() method.

  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example indexer_client/12-application-logs.ts

View source on GitHub

12-application-logs.ts
/**
* Example: Application Logs Lookup
*
* This example demonstrates how to lookup application logs using
* the IndexerClient lookupApplicationLogsById() method.
*
* Prerequisites:
* - LocalNet running (via `algokit localnet start`)
*/
import { algo } from '@algorandfoundation/algokit-utils'
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`)
}
/**
* Decode log bytes to string if possible, otherwise show hex
*/
function decodeLogEntry(logBytes: Uint8Array): string {
try {
// Try to decode as UTF-8 string
const decoded = new TextDecoder().decode(logBytes)
// Check if it's printable ASCII/UTF-8
if (/^[\x20-\x7E\s]+$/.test(decoded)) {
return `"${decoded}"`
}
} catch {
// Fall through to hex display
}
// Display as hex for binary data
const hex = Array.from(logBytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
return `0x${hex} (${logBytes.length} bytes)`
}
async function main() {
printHeader('Application Logs Lookup Example')
// Create clients
const indexer = createIndexerClient()
const algorand = createAlgorandClient()
const algod = createAlgodClient()
// =========================================================================
// Step 1: Get funded accounts from LocalNet
// =========================================================================
printStep(1, 'Getting funded accounts from LocalNet')
let creatorAddress: string
let creatorAccount: Awaited<ReturnType<typeof algorand.account.kmd.getLocalNetDispenserAccount>>
let callerAccount: Awaited<ReturnType<typeof algorand.account.kmd.getOrCreateWalletAccount>>
try {
creatorAccount = await algorand.account.kmd.getLocalNetDispenserAccount()
algorand.setSignerFromAccount(creatorAccount)
creatorAddress = creatorAccount.addr.toString()
printSuccess(`Using dispenser account as creator: ${shortenAddress(creatorAddress)}`)
// Create a separate caller account for sender filtering demo
callerAccount = await algorand.account.kmd.getOrCreateWalletAccount('caller-account')
algorand.setSignerFromAccount(callerAccount)
const callerAddress = callerAccount.addr.toString()
printSuccess(`Using caller account: ${shortenAddress(callerAddress)}`)
// Fund the caller account
await algorand.send.payment({
sender: creatorAccount.addr,
receiver: callerAccount.addr,
amount: algo(1),
})
printInfo('Funded caller account with 1 ALGO')
} catch (error) {
printError(`Failed to get 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: Deploy an application that emits logs
// =========================================================================
printStep(2, 'Deploying an application that emits logs')
let appId: bigint
try {
// Load TEAL programs from shared artifacts
// The approval program emits logs on each call using the `log` opcode
const approvalSource = loadTealSource('approval-logging.teal')
// Load clear state program that logs when called
const clearSource = loadTealSource('clear-state-logging.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 application
printInfo('Creating application...')
const suggestedParams = await algod.suggestedParams()
const appCallFields: AppCallTransactionFields = {
appId: 0n,
onComplete: OnApplicationComplete.NoOp,
approvalProgram,
clearStateProgram,
globalStateSchema: { numUints: 0, numByteSlices: 0 },
localStateSchema: { numUints: 0, numByteSlices: 0 },
}
const createAppTx = new Transaction({
type: TransactionType.AppCall,
sender: creatorAccount.addr,
firstValid: suggestedParams.firstValid,
lastValid: suggestedParams.lastValid,
genesisHash: suggestedParams.genesisHash,
genesisId: suggestedParams.genesisId,
appCall: appCallFields,
})
const createAppTxWithFee = assignFee(createAppTx, {
feePerByte: suggestedParams.fee,
minFee: suggestedParams.minFee,
})
const signedCreateTx = await creatorAccount.signer([createAppTxWithFee], [0])
await algod.sendRawTransaction(signedCreateTx)
const createPendingInfo = await waitForConfirmation(algod, createAppTxWithFee.txId())
appId = createPendingInfo.appId as bigint
const createTxId = createAppTxWithFee.txId()
printSuccess(`Created application with ID: ${appId}`)
printInfo(`Creation transaction ID: ${createTxId}`)
printInfo('')
} catch (error) {
printError(`Failed to create application: ${error instanceof Error ? error.message : String(error)}`)
printInfo('')
printInfo('If LocalNet errors occur, try: algokit localnet reset')
return
}
// =========================================================================
// Step 3: Call the application multiple times to generate logs
// =========================================================================
printStep(3, 'Calling the application multiple times to generate logs')
const callTxIds: string[] = []
let firstCallRound: bigint = 0n
let lastCallRound: bigint = 0n
try {
// Make several calls from the creator account
for (let i = 0; i < 3; i++) {
const callParams = await algod.suggestedParams()
const callFields: AppCallTransactionFields = {
appId,
onComplete: OnApplicationComplete.NoOp,
}
const callTx = new Transaction({
type: TransactionType.AppCall,
sender: creatorAccount.addr,
firstValid: callParams.firstValid,
lastValid: callParams.lastValid,
genesisHash: callParams.genesisHash,
genesisId: callParams.genesisId,
appCall: callFields,
})
const callTxWithFee = assignFee(callTx, {
feePerByte: callParams.fee,
minFee: callParams.minFee,
})
const signedCallTx = await creatorAccount.signer([callTxWithFee], [0])
await algod.sendRawTransaction(signedCallTx)
const callPendingInfo = await waitForConfirmation(algod, callTxWithFee.txId())
callTxIds.push(callTxWithFee.txId())
const confirmedRound = callPendingInfo.confirmedRound as bigint
if (firstCallRound === 0n) {
firstCallRound = confirmedRound
}
lastCallRound = confirmedRound
printInfo(`Call ${i + 1}: txId=${callTxWithFee.txId().substring(0, 12)}..., round=${confirmedRound}`)
}
// Make a call from the caller account (different sender)
const callerCallParams = await algod.suggestedParams()
const callerCallFields: AppCallTransactionFields = {
appId,
onComplete: OnApplicationComplete.NoOp,
}
const callerCallTx = new Transaction({
type: TransactionType.AppCall,
sender: callerAccount.addr,
firstValid: callerCallParams.firstValid,
lastValid: callerCallParams.lastValid,
genesisHash: callerCallParams.genesisHash,
genesisId: callerCallParams.genesisId,
appCall: callerCallFields,
})
const callerCallTxWithFee = assignFee(callerCallTx, {
feePerByte: callerCallParams.fee,
minFee: callerCallParams.minFee,
})
const signedCallerCallTx = await callerAccount.signer([callerCallTxWithFee], [0])
await algod.sendRawTransaction(signedCallerCallTx)
const callerCallPendingInfo = await waitForConfirmation(algod, callerCallTxWithFee.txId())
callTxIds.push(callerCallTxWithFee.txId())
lastCallRound = callerCallPendingInfo.confirmedRound as bigint
printInfo(
`Call 4 (from caller): txId=${callerCallTxWithFee.txId().substring(0, 12)}..., round=${lastCallRound}`,
)
printSuccess('Made 4 application calls (3 from creator, 1 from caller)')
printInfo('')
} catch (error) {
printError(`Failed to call application: ${error instanceof Error ? error.message : String(error)}`)
return
}
// =========================================================================
// Step 4: Lookup application logs with lookupApplicationLogsById()
// =========================================================================
printStep(4, 'Looking up application logs with lookupApplicationLogsById()')
// Wait for indexer to catch up with algod
printInfo('Waiting for indexer to sync...')
await new Promise((resolve) => setTimeout(resolve, 3000))
try {
// lookupApplicationLogsById() returns all logs for an application
const logsResult = await indexer.lookupApplicationLogsById(appId)
printSuccess(`Retrieved logs for application ${logsResult.applicationId}`)
printInfo(`Query performed at round: ${logsResult.currentRound}`)
printInfo('')
if (logsResult.logData && logsResult.logData.length > 0) {
printInfo(`Found ${logsResult.logData.length} transaction(s) with logs:`)
printInfo('')
for (const logEntry of logsResult.logData) {
printInfo(`Transaction: ${logEntry.txId}`)
printInfo(` Logs (${logEntry.logs.length} entries):`)
for (let i = 0; i < logEntry.logs.length; i++) {
const decoded = decodeLogEntry(logEntry.logs[i])
printInfo(` [${i}] ${decoded}`)
}
printInfo('')
}
} else {
printInfo('No logs found for this application')
}
if (logsResult.nextToken) {
printInfo(`More results available (nextToken: ${logsResult.nextToken.substring(0, 20)}...)`)
}
} catch (error) {
printError(`lookupApplicationLogsById failed: ${error instanceof Error ? error.message : String(error)}`)
}
// =========================================================================
// Step 5: Filter logs by txId to get logs from a specific transaction
// =========================================================================
printStep(5, 'Filtering logs by txId')
try {
const specificTxId = callTxIds[0]
printInfo(`Filtering logs for specific transaction: ${specificTxId.substring(0, 20)}...`)
const filteredResult = await indexer.lookupApplicationLogsById(appId, {
txId: specificTxId,
})
if (filteredResult.logData && filteredResult.logData.length > 0) {
printSuccess(`Found ${filteredResult.logData.length} log entry for transaction`)
for (const logEntry of filteredResult.logData) {
printInfo(`Transaction: ${logEntry.txId}`)
printInfo(` Logs (${logEntry.logs.length} entries):`)
for (let i = 0; i < logEntry.logs.length; i++) {
const decoded = decodeLogEntry(logEntry.logs[i])
printInfo(` [${i}] ${decoded}`)
}
}
} else {
printInfo('No logs found for this transaction')
}
} catch (error) {
printError(`txId filtering failed: ${error instanceof Error ? error.message : String(error)}`)
}
// =========================================================================
// Step 6: Filter logs by minRound and maxRound
// =========================================================================
printStep(6, 'Filtering logs by minRound and maxRound')
try {
printInfo(`Filtering logs between rounds ${firstCallRound} and ${lastCallRound}`)
// Filter by minRound
printInfo('')
printInfo(`Logs with minRound=${firstCallRound}:`)
const minRoundResult = await indexer.lookupApplicationLogsById(appId, {
minRound: firstCallRound,
})
if (minRoundResult.logData) {
printInfo(` Found ${minRoundResult.logData.length} transaction(s) with logs`)
}
// Filter by maxRound
printInfo('')
printInfo(`Logs with maxRound=${firstCallRound}:`)
const maxRoundResult = await indexer.lookupApplicationLogsById(appId, {
maxRound: firstCallRound,
})
if (maxRoundResult.logData) {
printInfo(` Found ${maxRoundResult.logData.length} transaction(s) with logs`)
}
// Filter by range (minRound and maxRound combined)
printInfo('')
printInfo(`Logs with minRound=${firstCallRound} and maxRound=${lastCallRound}:`)
const rangeResult = await indexer.lookupApplicationLogsById(appId, {
minRound: firstCallRound,
maxRound: lastCallRound,
})
if (rangeResult.logData) {
printInfo(` Found ${rangeResult.logData.length} transaction(s) with logs`)
for (const logEntry of rangeResult.logData) {
printInfo(` - ${logEntry.txId.substring(0, 20)}... (${logEntry.logs.length} log entries)`)
}
}
} catch (error) {
printError(`Round filtering failed: ${error instanceof Error ? error.message : String(error)}`)
}
// =========================================================================
// Step 7: Filter logs by senderAddress
// =========================================================================
printStep(7, 'Filtering logs by senderAddress')
try {
const callerAddress = callerAccount.addr.toString()
// Filter logs by creator address
printInfo(`Filtering logs by sender: ${shortenAddress(creatorAddress)}`)
const creatorLogsResult = await indexer.lookupApplicationLogsById(appId, {
senderAddress: creatorAddress,
})
if (creatorLogsResult.logData) {
printSuccess(`Found ${creatorLogsResult.logData.length} transaction(s) from creator`)
}
// Filter logs by caller address
printInfo('')
printInfo(`Filtering logs by sender: ${shortenAddress(callerAddress)}`)
const callerLogsResult = await indexer.lookupApplicationLogsById(appId, {
senderAddress: callerAddress,
})
if (callerLogsResult.logData) {
printSuccess(`Found ${callerLogsResult.logData.length} transaction(s) from caller`)
for (const logEntry of callerLogsResult.logData) {
printInfo(` Transaction: ${logEntry.txId.substring(0, 20)}...`)
for (let i = 0; i < logEntry.logs.length; i++) {
const decoded = decodeLogEntry(logEntry.logs[i])
printInfo(` [${i}] ${decoded}`)
}
}
}
} catch (error) {
printError(`senderAddress filtering failed: ${error instanceof Error ? error.message : String(error)}`)
}
// =========================================================================
// Step 8: Demonstrate pagination for applications with many log entries
// =========================================================================
printStep(8, 'Demonstrating pagination with limit and next parameters')
try {
// First page with limit of 2
printInfo('Fetching first page of logs (limit: 2)...')
const page1 = await indexer.lookupApplicationLogsById(appId, { limit: 2 })
if (page1.logData) {
printInfo(`Page 1: Retrieved ${page1.logData.length} transaction(s) with logs`)
for (const logEntry of page1.logData) {
printInfo(` - ${logEntry.txId.substring(0, 20)}...`)
}
}
// Check if there are more results
if (page1.nextToken) {
printInfo(` Next token available: ${page1.nextToken.substring(0, 20)}...`)
printInfo('')
// Fetch second page using next token
printInfo('Fetching second page using next token...')
const page2 = await indexer.lookupApplicationLogsById(appId, {
limit: 2,
next: page1.nextToken,
})
if (page2.logData) {
printInfo(`Page 2: Retrieved ${page2.logData.length} transaction(s) with logs`)
for (const logEntry of page2.logData) {
printInfo(` - ${logEntry.txId.substring(0, 20)}...`)
}
}
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. Deploying an application that emits logs using the `log` opcode')
printInfo(' 2. Calling the application to generate log entries')
printInfo(' 3. lookupApplicationLogsById(appId) - Get all logs for an application')
printInfo(' 4. Displaying log entry fields: txId and logs (decoded from Uint8Array)')
printInfo(' 5. Filtering by txId to get logs from a specific transaction')
printInfo(' 6. Filtering by minRound and maxRound for round range queries')
printInfo(' 7. Filtering by senderAddress to get logs from a specific caller')
printInfo(' 8. Pagination with limit and next parameters')
printInfo('')
printInfo('Key lookupApplicationLogsById response fields:')
printInfo(' - applicationId: The application identifier (bigint)')
printInfo(' - currentRound: Round at which results were computed (bigint)')
printInfo(' - logData: Array of ApplicationLogData objects')
printInfo(' - nextToken: Pagination token for next page (optional)')
printInfo('')
printInfo('Key ApplicationLogData fields:')
printInfo(' - txId: Transaction ID that generated the logs')
printInfo(' - logs: Array of Uint8Array, each containing a log entry')
printInfo('')
printInfo('Filter parameters:')
printInfo(' - txId: Filter by specific transaction ID')
printInfo(' - minRound: Only include logs from this round onwards')
printInfo(' - maxRound: Only include logs up to this round')
printInfo(' - senderAddress: Filter by the address that called the application')
printInfo(' - limit: Maximum results per page')
printInfo(' - next: Pagination token from previous response')
printInfo('')
printInfo('Note: The `log` opcode in TEAL emits log entries that are stored')
printInfo('in the transaction result and indexed by the indexer.')
}
main().catch((error) => {
console.error('Fatal error:', error)
process.exit(1)
})