Inner Transaction Subscription
Description
Section titled “Description”This example demonstrates inner transaction subscription and parent-child relationships.
- Subscribe to inner payment transactions by sender/receiver
- Verify parentTransactionId links to the parent app call
- Inspect inner transaction ID format
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 09-inner-transactions.ts/** * Example: Inner Transaction Subscription * * This example demonstrates inner transaction subscription and parent-child relationships. * - Subscribe to inner payment transactions by sender/receiver * - Verify parentTransactionId links to the parent app call * - Inspect inner transaction ID format * * 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 { printHeader, printStep, printInfo, printSuccess, printError, shortenAddress, formatAlgo } from './shared/utils.js'
const __filename = fileURLToPath(import.meta.url)const __dirname = dirname(__filename)
async function main() { printHeader('09 — Inner Transaction 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 caller = await algorand.account.fromEnvironment('INNER_TXN_CALLER', algo(100)) const callerAddr = caller.addr.toString() printInfo(`Caller: ${shortenAddress(callerAddr)}`) 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: caller.addr, }) const { result: createResult, appClient } = await factory.send.bare.create({ sender: caller.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: Fund the app account so it can issue inner payment transactions printStep(4, 'Fund app account for inner transactions') const appAddress = appClient.appAddress.toString() printInfo(`App address: ${shortenAddress(appAddress)}`)
await algorand.send.payment({ sender: caller.addr, receiver: appClient.appAddress, amount: algo(10), }) printSuccess('App account funded with 10 ALGO')
// Step 5: Call issue_transfer_to_sender to create an inner payment transaction printStep(5, 'Call issue_transfer_to_sender (creates inner payment)') const transferAmount = 1_000_000n // 1 ALGO in microAlgos const issueResult = await appClient.send.call({ method: 'issue_transfer_to_sender', args: [transferAmount], sender: caller.addr, extraFee: algo(0.001), // Cover the inner transaction fee }) const appCallTxnId = issueResult.txIds.at(-1) const appCallRound = issueResult.confirmation.confirmedRound! printInfo(`App call txn: ${appCallTxnId}`) printInfo(`Confirmed round: ${appCallRound.toString()}`) printInfo(`Transfer amount: ${formatAlgo(transferAmount)}`) printSuccess('issue_transfer_to_sender called — inner payment created')
// Watermark: just before the app call round to capture only the inner transaction const watermarkBefore = appCallRound - 1n
// Step 6: Subscribe with a payment filter matching the inner transaction by receiver printStep(6, 'Subscribe with payment filter matching inner transaction') let watermark = watermarkBefore const subscriber = new AlgorandSubscriber( { filters: [ { name: 'inner-payments', filter: { receiver: callerAddr, sender: appAddress, }, }, ], 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()}`)
if (matchedTxns.length !== 1) { throw new Error(`Expected 1 matched inner transaction, got ${matchedTxns.length}`) } printSuccess('Payment filter matched 1 inner transaction')
const innerTxn = matchedTxns[0]
// Step 7: Show inner transaction has parentTransactionId set printStep(7, 'Inspect inner transaction — parentTransactionId') printInfo(`Inner txn ID: ${innerTxn.id}`) printInfo(`parentTransactionId: ${innerTxn.parentTransactionId ?? 'undefined'}`)
if (!innerTxn.parentTransactionId) { throw new Error('Expected inner transaction to have parentTransactionId set') } printSuccess('parentTransactionId is set on the inner transaction')
// Step 8: Show inner transaction ID follows <rootTxId>/inner/<N> format printStep(8, 'Verify inner transaction ID format') const innerIdPattern = /^[A-Z0-9]+\/inner\/\d+$/ printInfo(`Expected format: <rootTxId>/inner/<N>`) printInfo(`Actual ID: ${innerTxn.id}`)
if (!innerIdPattern.test(innerTxn.id)) { throw new Error(`Inner transaction ID does not match expected format: ${innerTxn.id}`) } printSuccess(`Inner transaction ID matches <rootTxId>/inner/<N> format`)
// Step 9: Show parent transaction's innerTxns array contains the inner transaction printStep(9, 'Verify parent has innerTxns containing this transaction')
// Poll again to get the parent app call transaction let watermark2 = watermarkBefore const parentSubscriber = new AlgorandSubscriber( { filters: [ { name: 'parent-app-call', filter: { appId: appId, sender: callerAddr, }, }, ], syncBehaviour: 'sync-oldest', maxRoundsToSync: 100, watermarkPersistence: { get: async () => watermark2, set: async (w: bigint) => { watermark2 = w }, }, }, algorand.client.algod, )
const parentResult = await parentSubscriber.pollOnce() const parentTxn = parentResult.subscribedTransactions.find((t) => t.id === innerTxn.parentTransactionId)
if (!parentTxn) { throw new Error(`Could not find parent transaction with ID: ${innerTxn.parentTransactionId}`) }
printInfo(`Parent txn ID: ${parentTxn.id}`) printInfo(`Parent innerTxns count: ${(parentTxn.innerTxns?.length ?? 0).toString()}`)
if (!parentTxn.innerTxns || parentTxn.innerTxns.length === 0) { throw new Error('Parent transaction has no innerTxns') }
const matchedInner = parentTxn.innerTxns.find((t) => t.id === innerTxn.id) if (!matchedInner) { // Inner txns on the parent may use a different ID format; check by payment details const paymentInner = parentTxn.innerTxns.find( (t) => t.paymentTransaction?.receiver === callerAddr, ) if (paymentInner) { printInfo(`Inner txn in parent: ${paymentInner.id ?? '(present)'}`) } else { throw new Error('Inner transaction not found in parent innerTxns array') } } else { printInfo(`Inner txn in parent: ${matchedInner.id}`) } printSuccess('Parent transaction innerTxns array contains the inner transaction')
// Step 10: Print parent-child relationship clearly printStep(10, 'Parent-child relationship') console.log() console.log(' Parent (app call):') printInfo(` ID: ${parentTxn.id}`) printInfo(` Type: appl (application call)`) printInfo(` App ID: ${parentTxn.applicationTransaction?.applicationId?.toString() ?? appId.toString()}`) printInfo(` Method: issue_transfer_to_sender(uint64)void`) printInfo(` Sender: ${shortenAddress(callerAddr)}`) printInfo(` innerTxns count: ${(parentTxn.innerTxns?.length ?? 0).toString()}`) console.log() console.log(' └── Inner (payment):') printInfo(` ID: ${innerTxn.id}`) printInfo(` Type: pay (payment)`) printInfo(` Sender: ${shortenAddress(appAddress)}`) printInfo(` Receiver: ${shortenAddress(callerAddr)}`) printInfo(` Amount: ${formatAlgo(innerTxn.paymentTransaction?.amount ?? 0n)}`) printInfo(` parentTransactionId: ${innerTxn.parentTransactionId!}`) console.log() printSuccess('Parent-child relationship displayed')
// Summary printStep(11, 'Summary') printInfo(`App ID: ${appId.toString()}`) printInfo(`App address: ${shortenAddress(appAddress)}`) printInfo(`Method called: issue_transfer_to_sender(1_000_000) — 1 ALGO inner payment`) printInfo(`Inner txn matched: by payment filter (sender: app, receiver: caller)`) printInfo(`parentTransactionId: set on inner txn — links to parent app call`) printInfo(`Inner txn ID format: <rootTxId>/inner/<N>`) printInfo(`Parent innerTxns: contains the inner transaction`)
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