Application Logs Lookup
Description
Section titled “Description”This example demonstrates how to lookup application logs using the IndexerClient lookupApplicationLogsById() 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/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);});