Skip to content

Inner Transaction Subscription

← Back to Examples

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
  • LocalNet running (via algokit localnet start)

From the repository’s examples/subscriber directory:

Terminal window
cd examples/subscriber
npx tsx 09-inner-transactions.ts

View source on GitHub

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