Skip to content
Algorand Developer Portal

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