Pagination
Description
Section titled “Description”This example demonstrates how to properly handle pagination across multiple indexer endpoints using limit and next parameters. It includes a generic pagination helper function and shows iteration through all pages of results.
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/16-pagination.ts/** * Example: Pagination * * This example demonstrates how to properly handle pagination across multiple * indexer endpoints using limit and next parameters. It includes a generic * pagination helper function and shows iteration through all pages of results. * * Prerequisites: * - LocalNet running (via `algokit localnet start`) */
import { algo } from '@algorandfoundation/algokit-utils';import { createAlgorandClient, createIndexerClient, formatMicroAlgo, printError, printHeader, printInfo, printStep, printSuccess, shortenAddress,} from '../shared/utils.js';
// ============================================================================// Generic Pagination Helper Function// ============================================================================
/** * Generic pagination options for fetch function */interface PaginationOptions { /** Maximum items per page */ limit?: number; /** Token for next page */ next?: string;}
/** * Generic response type for paginated endpoints */interface PaginatedResponse<T> { /** The items returned in this page */ items: T[]; /** Token for fetching the next page (undefined if no more pages) */ nextToken?: string; /** Current round when query was performed */ currentRound: bigint;}
/** * Options for the paginateAll helper */interface PaginateAllOptions<T> { /** Page size (default: 100) */ pageSize?: number; /** Maximum total items to fetch (default: unlimited) */ maxItems?: number; /** Callback called for each page, return false to stop pagination */ onPage?: (items: T[], pageNumber: number) => boolean | void; /** Condition to stop early - return true to stop */ stopWhen?: (item: T, index: number) => boolean;}
/** * Generic pagination helper that iterates through all pages of results. * * This function provides a reusable pattern for paginating through any * indexer endpoint that supports limit and next parameters. * * @param fetchPage - Function that fetches a page of results * @param options - Pagination options including pageSize, maxItems, callbacks * @returns All items collected across all pages * * @example * // Fetch all transactions from an account * const allTxns = await paginateAll( * async (opts) => { * const result = await indexer.searchForTransactions({ ...opts }) * return { * items: result.transactions, * nextToken: result.nextToken, * currentRound: result.currentRound, * } * }, * { pageSize: 100, maxItems: 1000 } * ) */async function paginateAll<T>( fetchPage: (options: PaginationOptions) => Promise<PaginatedResponse<T>>, options: PaginateAllOptions<T> = {},): Promise<{ items: T[]; totalPages: number; stoppedEarly: boolean }> { const { pageSize = 100, maxItems, onPage, stopWhen } = options;
const allItems: T[] = []; let nextToken: string | undefined; let pageNumber = 0; let stoppedEarly = false;
do { pageNumber++;
// Fetch the next page const response = await fetchPage({ limit: pageSize, next: nextToken, });
// Process items and check for early termination for (const item of response.items) { // Check stop condition if (stopWhen && stopWhen(item, allItems.length)) { stoppedEarly = true; break; }
allItems.push(item);
// Check max items limit if (maxItems && allItems.length >= maxItems) { stoppedEarly = true; break; } }
// Call page callback if provided if (onPage) { const continueIteration = onPage(response.items, pageNumber); if (continueIteration === false) { stoppedEarly = true; break; } }
// Check if we should stop if (stoppedEarly) { break; }
nextToken = response.nextToken; } while (nextToken);
return { items: allItems, totalPages: pageNumber, stoppedEarly };}
async function main() { printHeader('Pagination Example');
// Create clients const indexer = createIndexerClient(); const algorand = createAlgorandClient();
// ========================================================================= // Step 1: Get a funded account and create some test data // ========================================================================= printStep(1, 'Setting up test data for pagination');
let senderAddress: string; let senderAccount: Awaited<ReturnType<typeof algorand.account.kmd.getLocalNetDispenserAccount>>;
try { senderAccount = await algorand.account.kmd.getLocalNetDispenserAccount(); algorand.setSignerFromAccount(senderAccount); senderAddress = senderAccount.addr.toString(); printSuccess(`Using dispenser account: ${shortenAddress(senderAddress)}`);
// Create several random accounts and send them funds to generate transactions printInfo('Creating test transactions for pagination demo...'); const receiverAccounts: string[] = [];
for (let i = 0; i < 5; i++) { const receiver = algorand.account.random(); receiverAccounts.push(receiver.addr.toString());
await algorand.send.payment({ sender: senderAccount.addr, receiver: receiver.addr, amount: algo(1), }); }
printSuccess(`Created ${receiverAccounts.length} payment transactions`);
// Create a test asset printInfo('Creating test asset...'); const assetResult = await algorand.send.assetCreate({ sender: senderAccount.addr, total: 1_000_000n, decimals: 0, assetName: 'PaginationTestToken', unitName: 'PAGE', }); printSuccess(`Created asset: PaginationTestToken (ID: ${assetResult.assetId})`);
// Wait for indexer to catch up printInfo('Waiting for indexer to index transactions...'); await new Promise(resolve => setTimeout(resolve, 3000)); printInfo(''); } catch (error) { printError( `Failed to set up test data: ${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: Demonstrate pagination with searchForTransactions() // ========================================================================= printStep(2, 'Paginating through searchForTransactions()');
try { printInfo('Using the generic paginateAll helper to iterate through all transactions...'); printInfo('Settings: pageSize=2 (small for demo purposes)'); printInfo('');
const transactionResult = await paginateAll( async opts => { const result = await indexer.searchForTransactions({ limit: opts.limit, next: opts.next, }); return { items: result.transactions, nextToken: result.nextToken, currentRound: result.currentRound, }; }, { pageSize: 2, // Small page size to demonstrate pagination maxItems: 10, // Limit to 10 for demo onPage: (items, pageNumber) => { printInfo(` Page ${pageNumber}: Retrieved ${items.length} transaction(s)`); }, }, );
printSuccess(`Total transactions fetched: ${transactionResult.items.length}`); printInfo(`Total pages fetched: ${transactionResult.totalPages}`); printInfo(`Stopped early (hit maxItems): ${transactionResult.stoppedEarly}`); printInfo('');
// Display first few transactions if (transactionResult.items.length > 0) { printInfo('First 3 transactions:'); for (const tx of transactionResult.items.slice(0, 3)) { printInfo(` - ${tx.id ? shortenAddress(tx.id, 8, 6) : 'N/A'}: ${tx.txType}`); } } } catch (error) { printError( `searchForTransactions pagination failed: ${error instanceof Error ? error.message : String(error)}`, ); }
// ========================================================================= // Step 3: Demonstrate pagination with searchForAccounts() // ========================================================================= printStep(3, 'Paginating through searchForAccounts()');
try { printInfo('Fetching all accounts with balance > 0 using pagination...'); printInfo('Settings: pageSize=3'); printInfo('');
const accountResult = await paginateAll( async opts => { const result = await indexer.searchForAccounts({ currencyGreaterThan: 0n, limit: opts.limit, next: opts.next, }); return { items: result.accounts, nextToken: result.nextToken, currentRound: result.currentRound, }; }, { pageSize: 3, maxItems: 15, onPage: (items, pageNumber) => { printInfo(` Page ${pageNumber}: Retrieved ${items.length} account(s)`); }, }, );
printSuccess(`Total accounts fetched: ${accountResult.items.length}`); printInfo(`Total pages fetched: ${accountResult.totalPages}`); printInfo('');
// Display accounts with their balances if (accountResult.items.length > 0) { printInfo('Accounts found:'); for (const account of accountResult.items.slice(0, 5)) { printInfo(` - ${shortenAddress(account.address)}: ${formatMicroAlgo(account.amount)}`); } if (accountResult.items.length > 5) { printInfo(` ... and ${accountResult.items.length - 5} more`); } } } catch (error) { printError( `searchForAccounts pagination failed: ${error instanceof Error ? error.message : String(error)}`, ); }
// ========================================================================= // Step 4: Demonstrate pagination with searchForAssets() // ========================================================================= printStep(4, 'Paginating through searchForAssets()');
try { printInfo('Fetching all assets using pagination...'); printInfo('Settings: pageSize=2'); printInfo('');
const assetResult = await paginateAll( async opts => { const result = await indexer.searchForAssets({ limit: opts.limit, next: opts.next, }); return { items: result.assets, nextToken: result.nextToken, currentRound: result.currentRound, }; }, { pageSize: 2, maxItems: 10, onPage: (items, pageNumber) => { printInfo(` Page ${pageNumber}: Retrieved ${items.length} asset(s)`); }, }, );
printSuccess(`Total assets fetched: ${assetResult.items.length}`); printInfo(`Total pages fetched: ${assetResult.totalPages}`); printInfo('');
// Display assets if (assetResult.items.length > 0) { printInfo('Assets found:'); for (const asset of assetResult.items.slice(0, 5)) { const name = asset.params.name ?? 'Unnamed'; const unitName = asset.params.unitName ?? 'N/A'; printInfo(` - ID ${asset.id}: ${name} (${unitName})`); } if (assetResult.items.length > 5) { printInfo(` ... and ${assetResult.items.length - 5} more`); } } else { printInfo('No assets found on LocalNet'); } } catch (error) { printError( `searchForAssets pagination failed: ${error instanceof Error ? error.message : String(error)}`, ); }
// ========================================================================= // Step 5: Display total count of items across all pages // ========================================================================= printStep(5, 'Counting total items across all pages');
try { printInfo('Counting all transactions without fetching full data...'); printInfo('');
let totalTransactions = 0; let pageCount = 0; let nextToken: string | undefined;
// Simple counting loop using limit and next do { pageCount++; const result = await indexer.searchForTransactions({ limit: 100, // Use larger page size for counting next: nextToken, });
totalTransactions += result.transactions.length; nextToken = result.nextToken;
// Safety limit for demo if (pageCount >= 10) { printInfo(' (stopping after 10 pages for demo purposes)'); break; } } while (nextToken);
printSuccess(`Total transactions counted: ${totalTransactions}`); printInfo(`Pages scanned: ${pageCount}`); printInfo('');
// Also count accounts printInfo('Counting all accounts...'); let totalAccounts = 0; pageCount = 0; nextToken = undefined;
do { pageCount++; const result = await indexer.searchForAccounts({ currencyGreaterThan: 0n, limit: 100, next: nextToken, });
totalAccounts += result.accounts.length; nextToken = result.nextToken;
if (pageCount >= 10) break; } while (nextToken);
printSuccess(`Total accounts with balance > 0: ${totalAccounts}`); printInfo(`Pages scanned: ${pageCount}`); } catch (error) { printError(`Counting failed: ${error instanceof Error ? error.message : String(error)}`); }
// ========================================================================= // Step 6: Demonstrate early termination when a condition is met // ========================================================================= printStep(6, 'Demonstrating early termination');
try { printInfo('Searching for transactions until we find a payment transaction...'); printInfo('');
const earlyTermResult = await paginateAll( async opts => { const result = await indexer.searchForTransactions({ limit: opts.limit, next: opts.next, }); return { items: result.transactions, nextToken: result.nextToken, currentRound: result.currentRound, }; }, { pageSize: 5, stopWhen: (tx, index) => { // Stop when we find a payment transaction if (tx.txType === 'pay') { printInfo( ` Found payment transaction at index ${index}: ${tx.id ? shortenAddress(tx.id, 8, 6) : 'N/A'}`, ); return true; } return false; }, onPage: (items, pageNumber) => { printInfo(` Page ${pageNumber}: Checking ${items.length} transaction(s)...`); }, }, );
printSuccess(`Stopped early: ${earlyTermResult.stoppedEarly}`); printInfo(`Total transactions before stopping: ${earlyTermResult.items.length}`); printInfo(`Pages checked: ${earlyTermResult.totalPages}`); printInfo('');
// Another example: stop after finding an account with specific balance printInfo('Searching for an account with balance > 1000 ALGO...');
const accountSearchResult = await paginateAll( async opts => { const result = await indexer.searchForAccounts({ currencyGreaterThan: 0n, limit: opts.limit, next: opts.next, }); return { items: result.accounts, nextToken: result.nextToken, currentRound: result.currentRound, }; }, { pageSize: 5, stopWhen: account => { // Stop when we find an account with > 1000 ALGO (1,000,000,000 microAlgos) if (account.amount > 1_000_000_000_000n) { printInfo( ` Found whale account: ${shortenAddress(account.address)} with ${formatMicroAlgo(account.amount)}`, ); return true; } return false; }, }, );
if (accountSearchResult.stoppedEarly) { printSuccess('Found an account with > 1000 ALGO!'); } else { printInfo('No account found with > 1000 ALGO (searched all accounts)'); } } catch (error) { printError( `Early termination demo failed: ${error instanceof Error ? error.message : String(error)}`, ); }
// ========================================================================= // Step 7: Handle the case where there are no results // ========================================================================= printStep(7, 'Handling empty results');
try { printInfo('Searching for assets with a name that does not exist...'); printInfo('');
const emptyResult = await paginateAll( async opts => { const result = await indexer.searchForAssets({ name: 'ThisAssetNameShouldNotExist12345', limit: opts.limit, next: opts.next, }); return { items: result.assets, nextToken: result.nextToken, currentRound: result.currentRound, }; }, { pageSize: 10, onPage: (items, pageNumber) => { printInfo(` Page ${pageNumber}: Retrieved ${items.length} item(s)`); }, }, );
if (emptyResult.items.length === 0) { printSuccess('Correctly handled empty results (no assets found)'); printInfo(`Total pages: ${emptyResult.totalPages}`); printInfo('Note: Empty results return an empty array, not an error'); } else { printInfo(`Unexpectedly found ${emptyResult.items.length} asset(s)`); } printInfo('');
// Also demonstrate with accounts printInfo('Searching for accounts with impossibly high balance...'); const emptyAccountResult = await paginateAll( async opts => { // Search for accounts with balance > max supply (would never exist) const result = await indexer.searchForAccounts({ currencyGreaterThan: 10_000_000_000_000_000n, // > 10 billion ALGO limit: opts.limit, next: opts.next, }); return { items: result.accounts, nextToken: result.nextToken, currentRound: result.currentRound, }; }, { pageSize: 10 }, );
if (emptyAccountResult.items.length === 0) { printSuccess('Correctly handled empty results (no accounts with such high balance)'); } else { printInfo(`Found ${emptyAccountResult.items.length} account(s)`); } } catch (error) { printError( `Empty results handling failed: ${error instanceof Error ? error.message : String(error)}`, ); }
// ========================================================================= // Step 8: Manual pagination without helper // ========================================================================= printStep(8, 'Manual pagination pattern (without helper)');
try { printInfo('Sometimes you may want to control pagination manually...'); printInfo('');
// Manual pagination loop let allTransactions: Awaited<ReturnType<typeof indexer.searchForTransactions>>['transactions'] = []; let nextToken: string | undefined; let pageNum = 0;
do { pageNum++; const page = await indexer.searchForTransactions({ limit: 3, next: nextToken, });
allTransactions = allTransactions.concat(page.transactions); nextToken = page.nextToken;
printInfo( ` Page ${pageNum}: ${page.transactions.length} transactions (total: ${allTransactions.length})`, );
// Limit for demo if (pageNum >= 3) { printInfo(' (stopping after 3 pages for demo)'); break; } } while (nextToken);
printSuccess( `Manual pagination complete: ${allTransactions.length} transactions in ${pageNum} pages`, ); printInfo('');
printInfo('Key pagination fields:'); printInfo(' - limit: Maximum items per page (request parameter)'); printInfo(' - nextToken: Token from response to fetch next page'); printInfo(' - When nextToken is undefined/missing, no more pages exist'); } catch (error) { printError( `Manual pagination failed: ${error instanceof Error ? error.message : String(error)}`, ); }
// ========================================================================= // Summary // ========================================================================= printHeader('Summary'); printInfo('This example demonstrated pagination patterns for indexer endpoints:'); printInfo(''); printInfo('Pagination basics:'); printInfo(' - Use `limit` parameter to control page size'); printInfo(' - Use `next` parameter with `nextToken` from response to get next page'); printInfo(' - When `nextToken` is undefined, there are no more pages'); printInfo(''); printInfo('Generic pagination helper (paginateAll):'); printInfo(' - Reusable across all paginated endpoints'); printInfo(' - Supports pageSize, maxItems limits'); printInfo(' - Supports onPage callback for progress tracking'); printInfo(' - Supports stopWhen condition for early termination'); printInfo(''); printInfo('Endpoints demonstrated:'); printInfo(' - searchForTransactions() - paginate through transactions'); printInfo(' - searchForAccounts() - paginate through accounts'); printInfo(' - searchForAssets() - paginate through assets'); printInfo(''); printInfo('Best practices:'); printInfo(' - Use larger page sizes (50-100) for production to reduce API calls'); printInfo(' - Implement maxItems limit to prevent unbounded queries'); printInfo(' - Use early termination when searching for specific items'); printInfo(' - Handle empty results gracefully (empty array, not error)');}
main().catch(error => { console.error('Fatal error:', error); process.exit(1);});