Skip to content

Balance Change Tracking

← Back to Examples

This example demonstrates balance change filtering for ALGO and ASA transfers.

  • Filter by assetId, role, minAbsoluteAmount, and address
  • Inspect balanceChanges array on matched transactions
  • Explore BalanceChangeRole enum values
  • LocalNet running (via algokit localnet start)

From the repository’s examples/subscriber directory:

Terminal window
cd examples/subscriber
npx tsx 07-balance-changes.ts

View source on GitHub

07-balance-changes.ts
/**
* Example: Balance Change Tracking
*
* This example demonstrates balance change filtering for ALGO and ASA transfers.
* - Filter by assetId, role, minAbsoluteAmount, and address
* - Inspect balanceChanges array on matched transactions
* - Explore BalanceChangeRole enum values
*
* Prerequisites:
* - LocalNet running (via `algokit localnet start`)
*/
import { algo, AlgorandClient } from '@algorandfoundation/algokit-utils'
import { AlgorandSubscriber } from '@algorandfoundation/algokit-subscriber'
import { BalanceChangeRole } from '@algorandfoundation/algokit-subscriber/types/subscription'
import { printHeader, printStep, printInfo, printSuccess, printError, shortenAddress, formatMicroAlgo } from './shared/utils.js'
async function main() {
printHeader('07 — Balance Change Tracking')
// 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 accounts
printStep(2, 'Create and fund accounts')
const sender = await algorand.account.fromEnvironment('BAL_SENDER', algo(100))
const receiver = await algorand.account.fromEnvironment('BAL_RECEIVER', algo(10))
const senderAddr = sender.addr.toString()
const receiverAddr = receiver.addr.toString()
printInfo(`Sender: ${shortenAddress(senderAddr)}`)
printInfo(`Receiver: ${shortenAddress(receiverAddr)}`)
printSuccess('Accounts created and funded')
// Step 3: Create an ASA for asset transfer testing
printStep(3, 'Create ASA and opt in receiver')
const asaResult = await algorand.send.assetCreate({
sender: sender.addr,
total: 1_000_000n,
decimals: 0,
assetName: 'BalTestToken',
unitName: 'BTT',
})
const assetId = asaResult.assetId
printInfo(`Asset ID: ${assetId.toString()}`)
await algorand.send.assetOptIn({ sender: receiver.addr, assetId })
printSuccess('Receiver opted in to ASA')
// Step 4: Send Algo payments and ASA transfers
printStep(4, 'Send Algo payments and ASA transfers')
// Payment 1: Sender -> Receiver, 5 ALGO
const pay1 = await algorand.send.payment({
sender: sender.addr,
receiver: receiver.addr,
amount: algo(5),
note: 'bal-pay-1',
})
printInfo(`Txn 1: Sender -> Receiver, 5 ALGO`)
// Payment 2: Sender -> Receiver, 2 ALGO
const pay2 = await algorand.send.payment({
sender: sender.addr,
receiver: receiver.addr,
amount: algo(2),
note: 'bal-pay-2',
})
printInfo(`Txn 2: Sender -> Receiver, 2 ALGO`)
// ASA Transfer: Sender -> Receiver, 500 tokens
const axfer1 = await algorand.send.assetTransfer({
sender: sender.addr,
receiver: receiver.addr,
assetId,
amount: 500n,
note: 'bal-axfer-1',
})
printInfo(`Txn 3: Sender -> Receiver, 500 BTT (ASA)`)
printSuccess('All transactions sent')
// Step 5: Subscribe with balanceChanges filter — Algo Sender with minAbsoluteAmount
printStep(5, 'Filter: Algo balance changes for Sender role with minAbsoluteAmount')
const watermarkBefore = pay1.confirmation!.confirmedRound! - 1n
let watermark1 = watermarkBefore
const algoSenderSub = new AlgorandSubscriber(
{
filters: [
{
name: 'algo-sender-changes',
filter: {
balanceChanges: [
{
assetId: 0n,
role: BalanceChangeRole.Sender,
minAbsoluteAmount: 2_000_000,
},
],
},
},
],
syncBehaviour: 'sync-oldest',
maxRoundsToSync: 100,
watermarkPersistence: {
get: async () => watermark1,
set: async (w: bigint) => { watermark1 = w },
},
},
algorand.client.algod,
)
const algoSenderResult = await algoSenderSub.pollOnce()
const algoSenderTxns = algoSenderResult.subscribedTransactions
printInfo(`Matched transactions: ${algoSenderTxns.length.toString()}`)
// Both pay1 (5 ALGO) and pay2 (2 ALGO) should match — sender loses >= 2 ALGO (amount + fee)
// The ASA transfer only moves tokens, but the sender also pays a fee in Algo
for (const txn of algoSenderTxns) {
const note = txn.note ? Buffer.from(txn.note).toString('utf-8') : ''
printInfo(` ${note}: id=${txn.id.slice(0, 12)}...`)
}
if (algoSenderTxns.length < 2) {
throw new Error(`Expected at least 2 Algo Sender transactions (>= 2M microAlgo), got ${algoSenderTxns.length}`)
}
printSuccess('Algo Sender filter matched expected transactions')
// Step 6: Subscribe with balanceChanges filter — ASA Receiver
printStep(6, 'Filter: ASA balance changes for Receiver role')
let watermark2 = watermarkBefore
const asaReceiverSub = new AlgorandSubscriber(
{
filters: [
{
name: 'asa-receiver-changes',
filter: {
balanceChanges: [
{
assetId: assetId,
role: BalanceChangeRole.Receiver,
},
],
},
},
],
syncBehaviour: 'sync-oldest',
maxRoundsToSync: 100,
watermarkPersistence: {
get: async () => watermark2,
set: async (w: bigint) => { watermark2 = w },
},
},
algorand.client.algod,
)
const asaReceiverResult = await asaReceiverSub.pollOnce()
const asaReceiverTxns = asaReceiverResult.subscribedTransactions
printInfo(`Matched transactions: ${asaReceiverTxns.length.toString()}`)
for (const txn of asaReceiverTxns) {
const note = txn.note ? Buffer.from(txn.note).toString('utf-8') : ''
printInfo(` ${note}: id=${txn.id.slice(0, 12)}...`)
}
if (asaReceiverTxns.length < 1) {
throw new Error(`Expected at least 1 ASA Receiver transaction, got ${asaReceiverTxns.length}`)
}
printSuccess('ASA Receiver filter matched expected transactions')
// Step 7: Subscribe with balanceChanges filter — Address (any role)
printStep(7, 'Filter: All balance changes for a specific address')
let watermark3 = watermarkBefore
const addressSub = new AlgorandSubscriber(
{
filters: [
{
name: 'address-changes',
filter: {
balanceChanges: [
{
address: senderAddr,
},
],
},
},
],
syncBehaviour: 'sync-oldest',
maxRoundsToSync: 100,
watermarkPersistence: {
get: async () => watermark3,
set: async (w: bigint) => { watermark3 = w },
},
},
algorand.client.algod,
)
const addressResult = await addressSub.pollOnce()
const addressTxns = addressResult.subscribedTransactions
printInfo(`Matched transactions: ${addressTxns.length.toString()}`)
for (const txn of addressTxns) {
const note = txn.note ? Buffer.from(txn.note).toString('utf-8') : ''
printInfo(` ${note}: id=${txn.id.slice(0, 12)}...`)
}
if (addressTxns.length < 3) {
throw new Error(`Expected at least 3 address-filtered transactions, got ${addressTxns.length}`)
}
printSuccess('Address filter matched expected transactions')
// Step 8: Inspect balanceChanges array on matched transactions
printStep(8, 'Inspect balanceChanges array on matched transactions')
// Collect all unique transactions from both polls
const allTxns = [...algoSenderTxns, ...asaReceiverTxns]
const seen = new Set<string>()
for (const txn of allTxns) {
if (seen.has(txn.id)) continue
seen.add(txn.id)
const note = txn.note ? Buffer.from(txn.note).toString('utf-8') : ''
console.log()
printInfo(`Transaction: ${note} (${txn.id.slice(0, 12)}...)`)
if (txn.balanceChanges && txn.balanceChanges.length > 0) {
for (const bc of txn.balanceChanges) {
const assetLabel = bc.assetId === 0n ? 'ALGO' : `ASA #${bc.assetId}`
const roles = bc.roles.join(', ')
printInfo(
` ${shortenAddress(bc.address)}: asset=${assetLabel}, amount=${bc.amount.toString()}, roles=[${roles}]`,
)
}
} else {
printInfo(' (no balance changes)')
}
}
// Step 9: Demonstrate BalanceChangeRole enum values
printStep(9, 'BalanceChangeRole enum values')
printInfo(`Sender: ${BalanceChangeRole.Sender}`)
printInfo(`Receiver: ${BalanceChangeRole.Receiver}`)
printInfo(`CloseTo: ${BalanceChangeRole.CloseTo}`)
printInfo(`AssetCreator: ${BalanceChangeRole.AssetCreator}`)
printInfo(`AssetDestroyer: ${BalanceChangeRole.AssetDestroyer}`)
printSuccess('All BalanceChangeRole values demonstrated')
// Step 10: Summary
printStep(10, 'Summary')
console.log()
console.log(' ┌──────────────────┬──────────────────────────────────────────────────┐')
console.log(' │ Filter │ Description │')
console.log(' ├──────────────────┼──────────────────────────────────────────────────┤')
console.log(' │ algo-sender │ assetId=0, role=Sender, minAbsoluteAmount=2M │')
console.log(' │ asa-receiver │ assetId=ASA, role=Receiver │')
console.log(' │ address │ address=Sender (any role, any asset) │')
console.log(' └──────────────────┴──────────────────────────────────────────────────┘')
console.log()
printHeader('Example complete')
}
main().catch((err) => {
printError(err.message)
process.exit(1)
})