ARC-28 Event Subscription
Description
Section titled “Description”This example demonstrates ARC-28 event parsing, filtering, and inspection.
- Define event definitions matching ARC-56 spec
- Configure config-level arc28Events for event parsing
- Filter transactions by emitted event names
- Inspect parsed event data (args, argsByName)
Prerequisites
Section titled “Prerequisites”- LocalNet running (via
algokit localnet start)
Run This Example
Section titled “Run This Example”From the repository’s examples/subscriber directory:
cd examples/subscribernpx tsx 08-arc28-events.ts/** * Example: ARC-28 Event Subscription * * This example demonstrates ARC-28 event parsing, filtering, and inspection. * - Define event definitions matching ARC-56 spec * - Configure config-level arc28Events for event parsing * - Filter transactions by emitted event names * - Inspect parsed event data (args, argsByName) * * Prerequisites: * - LocalNet running (via `algokit localnet start`) */import { readFileSync } from 'node:fs';import { fileURLToPath } from 'node:url';import { dirname, join } from 'node:path';import { algo, AlgorandClient, AppFactory } from '@algorandfoundation/algokit-utils';import { AlgorandSubscriber } from '@algorandfoundation/algokit-subscriber';import type { Arc28Event, Arc28EventGroup,} from '@algorandfoundation/algokit-subscriber/types/arc-28';import { printHeader, printStep, printInfo, printSuccess, printError, shortenAddress,} from './shared/utils.js';
const __filename = fileURLToPath(import.meta.url);const __dirname = dirname(__filename);
async function main() { printHeader('08 — ARC-28 Event Subscription');
// Step 1: Connect to LocalNet printStep(1, 'Connect to LocalNet'); const algorand = AlgorandClient.defaultLocalNet(); const status = await algorand.client.algod.status(); printInfo(`Current round: ${status.lastRound.toString()}`); printSuccess('Connected to LocalNet');
// Step 2: Create and fund an account printStep(2, 'Create and fund account'); const creator = await algorand.account.fromEnvironment('ARC28_CREATOR', algo(100)); const creatorAddr = creator.addr.toString(); printInfo(`Creator: ${shortenAddress(creatorAddr)}`); printSuccess('Account created and funded');
// Step 3: Deploy TestingApp using AppFactory with ARC-56 spec printStep(3, 'Deploy TestingApp via AppFactory'); const appSpec = JSON.parse( readFileSync(join(__dirname, 'shared/artifacts/testing-app.arc56.json'), 'utf-8'), ); const factory = new AppFactory({ appSpec, algorand, defaultSender: creator.addr, }); const { result: createResult, appClient } = await factory.send.bare.create({ sender: creator.addr, }); const appId = createResult.appId; const createRound = createResult.confirmation.confirmedRound!; printInfo(`App ID: ${appId.toString()}`); printInfo(`Create round: ${createRound.toString()}`); printSuccess('TestingApp deployed');
// Step 4: Call emitSwapped(a, b) and emitComplex(a, b, array) to emit events printStep(4, 'Emit ARC-28 events via app calls');
const swapResult = await appClient.send.call({ method: 'emitSwapped', args: [42n, 99n], sender: creator.addr, }); printInfo(`emitSwapped(42, 99) txn: ${swapResult.txIds.at(-1)}`);
const complexResult = await appClient.send.call({ method: 'emitComplex', args: [10n, 20n, [1, 2, 3]], sender: creator.addr, }); printInfo(`emitComplex(10, 20, [1,2,3]) txn: ${complexResult.txIds.at(-1)}`); printSuccess('2 event-emitting app calls sent');
// Watermark: just before the app creation round const watermarkBefore = createRound - 1n;
// Step 5: Define ARC-28 event definitions // These mirror the events in the ARC-56 spec's "events" section printStep(5, 'Define ARC-28 event definitions');
const swappedEvent: Arc28Event = { name: 'Swapped', args: [ { type: 'uint64', name: 'field1' }, { type: 'uint64', name: 'field2' }, ], };
const complexEvent: Arc28Event = { name: 'Complex', args: [ { type: 'uint32[]', name: 'field1' }, { type: 'uint64', name: 'field2' }, ], };
printInfo(`Swapped signature: Swapped(uint64,uint64)`); printInfo(`Complex signature: Complex(uint32[],uint64)`); printSuccess('Event definitions ready');
// Step 6: Subscribe with arc28Events config (parsing) and arc28Events filter (matching) // KEY DISTINCTION: // - Config-level arc28Events (Arc28EventGroup[]): defines HOW to parse events from logs // - Filter-level arc28Events: defines WHICH transactions to match based on emitted events printStep(6, 'Subscribe with arc28Events — event parsing + filtering');
const arc28EventGroup: Arc28EventGroup = { groupName: 'testing-app-events', events: [swappedEvent, complexEvent], processForAppIds: [appId], // Only parse events from our deployed app continueOnError: true, // Silently skip unparseable events };
let watermark = watermarkBefore; const subscriber = new AlgorandSubscriber( { filters: [ { name: 'arc28-events', filter: { appId: appId, arc28Events: [ { groupName: 'testing-app-events', eventName: 'Swapped' }, { groupName: 'testing-app-events', eventName: 'Complex' }, ], }, }, ], arc28Events: [arc28EventGroup], // Config-level: event parsing definitions syncBehaviour: 'sync-oldest', maxRoundsToSync: 100, watermarkPersistence: { get: async () => watermark, set: async (w: bigint) => { watermark = w; }, }, }, algorand.client.algod, );
const result = await subscriber.pollOnce(); const matchedTxns = result.subscribedTransactions;
printInfo(`Matched count: ${matchedTxns.length.toString()}`);
// Step 7: Inspect parsed ARC-28 events on each matched transaction printStep(7, 'Inspect parsed ARC-28 event data');
for (const txn of matchedTxns) { printInfo(`Transaction: ${txn.id}`); printInfo(` Filters matched: [${txn.filtersMatched?.join(', ')}]`);
if (!txn.arc28Events || txn.arc28Events.length === 0) { printInfo(` Events: none`); continue; }
for (const event of txn.arc28Events) { printInfo(` Event name: ${event.eventName}`); printInfo(` Event signature: ${event.eventSignature}`); printInfo(` Event prefix: ${event.eventPrefix}`); printInfo(` Group name: ${event.groupName}`); printInfo( ` Args (ordered): ${JSON.stringify(event.args, (_, v) => (typeof v === 'bigint' ? v.toString() + 'n' : v))}`, ); printInfo( ` Args (by name): ${JSON.stringify(event.argsByName, (_, v) => (typeof v === 'bigint' ? v.toString() + 'n' : v))}`, ); } }
// Verify: emitSwapped produces 1 Swapped event, emitComplex produces 1 Swapped + 1 Complex if (matchedTxns.length !== 2) { throw new Error(`Expected 2 matched transactions, got ${matchedTxns.length}`); }
// First transaction: emitSwapped — 1 Swapped event const swapTxn = matchedTxns[0]; if (!swapTxn.arc28Events || swapTxn.arc28Events.length !== 1) { throw new Error(`emitSwapped: expected 1 event, got ${swapTxn.arc28Events?.length ?? 0}`); } if (swapTxn.arc28Events[0].eventName !== 'Swapped') { throw new Error(`emitSwapped: expected Swapped event, got ${swapTxn.arc28Events[0].eventName}`); } if (swapTxn.arc28Events[0].args[0] !== 42n || swapTxn.arc28Events[0].args[1] !== 99n) { throw new Error('emitSwapped: unexpected args'); } if ( swapTxn.arc28Events[0].argsByName['field1'] !== 42n || swapTxn.arc28Events[0].argsByName['field2'] !== 99n ) { throw new Error('emitSwapped: unexpected argsByName'); } printSuccess('emitSwapped: Swapped event parsed correctly (field1=42, field2=99)');
// Second transaction: emitComplex — 1 Swapped + 1 Complex event const complexTxn = matchedTxns[1]; if (!complexTxn.arc28Events || complexTxn.arc28Events.length !== 2) { throw new Error(`emitComplex: expected 2 events, got ${complexTxn.arc28Events?.length ?? 0}`); } if (complexTxn.arc28Events[0].eventName !== 'Swapped') { throw new Error( `emitComplex: first event should be Swapped, got ${complexTxn.arc28Events[0].eventName}`, ); } if (complexTxn.arc28Events[1].eventName !== 'Complex') { throw new Error( `emitComplex: second event should be Complex, got ${complexTxn.arc28Events[1].eventName}`, ); } printSuccess('emitComplex: Swapped + Complex events parsed correctly');
// Step 8: Demonstrate continueOnError behavior printStep(8, 'Demonstrate continueOnError: true'); printInfo( `continueOnError: true — if an event log cannot be decoded, a warning is logged and the event is skipped`, ); printInfo( `Behavior: Without continueOnError, a parse failure would throw an error and halt processing`, ); printSuccess( 'continueOnError: true is set on the event group — unparseable events are silently skipped', );
// Step 9: Summary printStep(9, 'Summary'); printInfo(`App ID: ${appId.toString()}`); printInfo( `Event group: "${arc28EventGroup.groupName}" with ${arc28EventGroup.events.length} event definitions`, ); printInfo(`processForAppIds: [${appId}] — only parse events from this app`); printInfo(`continueOnError: true — skip unparseable events`); printInfo(`Config-level arc28Events: Defines HOW events are parsed from app call logs`); printInfo( `Filter-level arc28Events: Defines WHICH transactions to match (by group + event name)`, ); printInfo(`emitSwapped result: 1 Swapped event with args [42n, 99n]`); printInfo(`emitComplex result: 2 events: Swapped [10n, 20n] + Complex [[1,2,3], 20n]`);
printHeader('Example complete');}
main().catch(err => { printError(err.message); process.exit(1);});Other examples
Section titled “Other examples”- Basic Poll Once
- Continuous Subscriber
- Payment Filters
- Asset Transfer Subscription
- App Call Subscription
- Multiple Named Filters
- Balance Change Tracking
- ARC-28 Event Subscription
- Inner Transaction Subscription
- Batch Handling & Data Mappers
- Watermark Persistence
- Sync Behaviours
- Custom Filters
- Stateless Subscriptions
- Lifecycle Hooks & Error Handling