Skip to content

Transaction Composer (Atomic Transaction Groups)

← Back to Algorand Client

This example demonstrates how to build atomic transaction groups using the transaction composer:

  • algorand.newGroup() creates a new transaction composer
  • Adding multiple transactions using .addPayment(), .addAssetOptIn(), etc.
  • Method chaining: algorand.newGroup().addPayment(…).addPayment(…)
  • .simulate() to simulate the transaction group before sending
  • .send() to execute the atomic transaction group
  • Atomicity: all transactions succeed or fail together
  • Adding transactions with different signers
  • Group ID assigned to all transactions in the group
  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example algorand_client/10-transaction-composer.ts

View source on GitHub

10-transaction-composer.ts
/**
* Example: Transaction Composer (Atomic Transaction Groups)
*
* This example demonstrates how to build atomic transaction groups using
* the transaction composer:
* - algorand.newGroup() creates a new transaction composer
* - Adding multiple transactions using .addPayment(), .addAssetOptIn(), etc.
* - Method chaining: algorand.newGroup().addPayment(...).addPayment(...)
* - .simulate() to simulate the transaction group before sending
* - .send() to execute the atomic transaction group
* - Atomicity: all transactions succeed or fail together
* - Adding transactions with different signers
* - Group ID assigned to all transactions in the group
*
* Prerequisites:
* - LocalNet running (via `algokit localnet start`)
*/
import { AlgorandClient, algo } from '@algorandfoundation/algokit-utils'
import { formatAlgo, printError, printHeader, printInfo, printStep, printSuccess, shortenAddress } from '../shared/utils.js'
async function main() {
printHeader('Transaction Composer Example')
// Initialize client and verify LocalNet is running
const algorand = AlgorandClient.defaultLocalNet()
try {
await algorand.client.algod.status()
printSuccess('Connected to LocalNet')
} catch (error) {
printError(`Failed to connect to LocalNet: ${error instanceof Error ? error.message : String(error)}`)
printInfo('Make sure LocalNet is running (e.g., algokit localnet start)')
return
}
// Step 1: Create and fund test accounts
printStep(1, 'Create and fund test accounts')
printInfo('Creating multiple accounts to demonstrate atomic transactions with different signers')
const alice = algorand.account.random()
const bob = algorand.account.random()
const charlie = algorand.account.random()
printInfo(`\nCreated accounts:`)
printInfo(` Alice: ${shortenAddress(alice.addr.toString())}`)
printInfo(` Bob: ${shortenAddress(bob.addr.toString())}`)
printInfo(` Charlie: ${shortenAddress(charlie.addr.toString())}`)
// Fund accounts
await algorand.account.ensureFundedFromEnvironment(alice.addr, algo(20))
await algorand.account.ensureFundedFromEnvironment(bob.addr, algo(10))
await algorand.account.ensureFundedFromEnvironment(charlie.addr, algo(5))
printSuccess('Created and funded test accounts')
// Step 2: Demonstrate algorand.newGroup() to create a new transaction composer
printStep(2, 'Demonstrate algorand.newGroup() to create a new transaction composer')
printInfo('algorand.newGroup() returns a TransactionComposer for building atomic groups')
printInfo('')
printInfo('The TransactionComposer provides:')
printInfo(' - .addPayment() - Add payment transactions')
printInfo(' - .addAssetCreate() - Add asset creation transactions')
printInfo(' - .addAssetOptIn() - Add asset opt-in transactions')
printInfo(' - .addAssetTransfer() - Add asset transfer transactions')
printInfo(' - .addAppCall() - Add application call transactions')
printInfo(' - .simulate() - Simulate the group before sending')
printInfo(' - .send() - Execute the atomic transaction group')
const composer = algorand.newGroup()
printInfo(`\nCreated new transaction composer`)
printInfo(` Initial transaction count: ${composer.count()}`)
printSuccess('Transaction composer created')
// Step 3: Add multiple transactions to the group
printStep(3, 'Add multiple transactions using .addPayment(), etc.')
printInfo('Each add method returns the composer for chaining')
// Add first payment
composer.addPayment({
sender: alice.addr,
receiver: bob.addr,
amount: algo(1),
note: 'Payment 1: Alice to Bob',
})
printInfo(`\nAdded payment: Alice -> Bob (1 ALGO)`)
printInfo(` Transaction count: ${composer.count()}`)
// Add second payment
composer.addPayment({
sender: alice.addr,
receiver: charlie.addr,
amount: algo(0.5),
note: 'Payment 2: Alice to Charlie',
})
printInfo(`Added payment: Alice -> Charlie (0.5 ALGO)`)
printInfo(` Transaction count: ${composer.count()}`)
printSuccess('Added multiple transactions to the group')
// Step 4: Demonstrate method chaining
printStep(4, 'Demonstrate chaining: algorand.newGroup().addPayment(...).addPayment(...)')
printInfo('Methods can be chained for fluent, readable code')
const chainedComposer = algorand
.newGroup()
.addPayment({
sender: alice.addr,
receiver: bob.addr,
amount: algo(0.25),
note: 'Chained payment 1',
})
.addPayment({
sender: alice.addr,
receiver: charlie.addr,
amount: algo(0.25),
note: 'Chained payment 2',
})
.addPayment({
sender: alice.addr,
receiver: bob.addr,
amount: algo(0.25),
note: 'Chained payment 3',
})
printInfo(`\nChained 3 payments in a single fluent expression`)
printInfo(` Transaction count: ${chainedComposer.count()}`)
printSuccess('Demonstrated method chaining')
// Step 5: Demonstrate .simulate() to simulate before sending
printStep(5, 'Demonstrate .simulate() to simulate the transaction group before sending')
printInfo('Simulation allows you to preview results and check for failures without sending')
const simulateResult = await chainedComposer.simulate({ skipSignatures: true })
printInfo(`\nSimulation results:`)
printInfo(` Transactions simulated: ${simulateResult.transactions.length}`)
printInfo(` Transaction IDs:`)
for (let i = 0; i < simulateResult.txIds.length; i++) {
printInfo(` [${i}]: ${simulateResult.txIds[i]}`)
}
printInfo(` Group ID: ${simulateResult.groupId}`)
// Check simulation response for failures
const simulateResponse = simulateResult.simulateResponse
const groupResult = simulateResponse.txnGroups[0]
printInfo(`\nSimulation response:`)
printInfo(` Would succeed: ${!groupResult.failureMessage}`)
if (groupResult.failureMessage) {
printInfo(` Failure message: ${groupResult.failureMessage}`)
printInfo(` Failed at index: ${groupResult.failedAt?.join(', ') ?? 'N/A'}`)
}
// Show transaction results from simulation
printInfo(`\nSimulated transaction results:`)
for (let i = 0; i < groupResult.txnResults.length; i++) {
const txnResult = groupResult.txnResults[i]
printInfo(` [${i}]: Confirmed round: ${txnResult.txnResult.confirmedRound ?? 'N/A (simulated)'}`)
}
printSuccess('Simulation completed successfully')
// Step 6: Demonstrate .send() to execute the atomic transaction group
printStep(6, 'Demonstrate .send() to execute the atomic transaction group')
printInfo('Calling .send() signs and submits all transactions atomically')
// Use the original composer (not the chained one which was already used for simulation)
const sendResult = await composer.send()
printInfo(`\nSend results:`)
printInfo(` Transactions sent: ${sendResult.transactions.length}`)
printInfo(` Group ID: ${sendResult.groupId}`)
printInfo(`\nTransaction IDs and confirmations:`)
for (let i = 0; i < sendResult.txIds.length; i++) {
const txId = sendResult.txIds[i]
const confirmation = sendResult.confirmations[i]
printInfo(` [${i}]: ${txId}`)
printInfo(` Confirmed in round: ${confirmation.confirmedRound}`)
}
printSuccess('Atomic transaction group executed')
// Step 7: Show that all transactions succeed or fail together (atomicity)
printStep(7, 'Show that all transactions succeed or fail together (atomicity)')
printInfo('Atomic groups ensure all-or-nothing execution')
printInfo('')
printInfo('Key atomicity properties:')
printInfo(' - All transactions share the same group ID')
printInfo(' - If any transaction fails, none are committed')
printInfo(' - Transactions are executed in order within the group')
printInfo('')
// Verify all transactions have the same group ID
const transactions = sendResult.transactions
const firstGroupId = transactions[0].group
const allSameGroup = transactions.every(
(txn) => txn.group && firstGroupId && Buffer.from(txn.group).toString('base64') === Buffer.from(firstGroupId).toString('base64'),
)
printInfo(`All transactions have same group ID: ${allSameGroup}`)
printInfo(`Group ID (base64): ${sendResult.groupId}`)
// Verify all confirmations are in the same round
const firstRound = sendResult.confirmations[0].confirmedRound
const allSameRound = sendResult.confirmations.every((conf) => conf.confirmedRound === firstRound)
printInfo(`All transactions confirmed in same round: ${allSameRound}`)
printInfo(`Confirmed round: ${firstRound}`)
printSuccess('Atomicity verified')
// Step 8: Demonstrate adding transactions with different signers
printStep(8, 'Demonstrate adding transactions with different signers')
printInfo('Atomic groups can include transactions from multiple signers')
printInfo('Each transaction uses the signer registered for its sender')
// Create an asset first (Alice creates it with manager role for cleanup)
const assetCreateResult = await algorand.send.assetCreate({
sender: alice.addr,
total: 1_000_000n,
decimals: 0,
assetName: 'Multi-Signer Token',
unitName: 'MST',
manager: alice.addr, // Manager role needed to destroy the asset later
})
const assetId = assetCreateResult.assetId
printInfo(`\nCreated asset for multi-signer demo:`)
printInfo(` Asset ID: ${assetId}`)
printInfo(` Asset name: Multi-Signer Token`)
// Build a multi-signer atomic group:
// 1. Bob opts in to the asset (signed by Bob)
// 2. Alice transfers asset to Bob (signed by Alice)
// 3. Charlie pays Alice (signed by Charlie)
const multiSignerResult = await algorand
.newGroup()
.addAssetOptIn({
sender: bob.addr, // Signed by Bob
assetId: assetId,
})
.addAssetTransfer({
sender: alice.addr, // Signed by Alice
receiver: bob.addr,
assetId: assetId,
amount: 100n,
})
.addPayment({
sender: charlie.addr, // Signed by Charlie
receiver: alice.addr,
amount: algo(0.1),
note: 'Payment for asset',
})
.send()
printInfo(`\nMulti-signer atomic group executed:`)
printInfo(` Transactions: ${multiSignerResult.transactions.length}`)
printInfo(` Group ID: ${multiSignerResult.groupId}`)
printInfo(`\nSigner breakdown:`)
printInfo(` [0] Asset Opt-In: Sender ${shortenAddress(multiSignerResult.transactions[0].sender.toString())} (Bob)`)
printInfo(` [1] Asset Transfer: Sender ${shortenAddress(multiSignerResult.transactions[1].sender.toString())} (Alice)`)
printInfo(` [2] Payment: Sender ${shortenAddress(multiSignerResult.transactions[2].sender.toString())} (Charlie)`)
printSuccess('Multi-signer atomic group completed')
// Step 9: Show the group ID assigned to transactions
printStep(9, 'Show the group ID assigned to transactions')
printInfo('All transactions in a group share a unique group ID')
printInfo('The group ID is a hash of all transactions in the group')
printInfo(`\nGroup ID details:`)
printInfo(` Group ID (base64): ${multiSignerResult.groupId}`)
printInfo(` Group ID length: ${multiSignerResult.groupId?.length ?? 0} characters (base64)`)
printInfo(`\nTransaction group membership:`)
for (let i = 0; i < multiSignerResult.transactions.length; i++) {
const txn = multiSignerResult.transactions[i]
const groupBase64 = txn.group ? Buffer.from(txn.group).toString('base64') : 'N/A'
printInfo(` Transaction [${i}]:`)
printInfo(` Type: ${txn.type}`)
printInfo(` Sender: ${shortenAddress(txn.sender.toString())}`)
printInfo(` Group ID matches: ${groupBase64 === multiSignerResult.groupId}`)
}
printSuccess('Group ID demonstrated')
// Step 10: Display all transaction IDs and confirmations
printStep(10, 'Display all transaction IDs and confirmations')
printInfo('Complete summary of the multi-signer atomic group')
printInfo(`\n${'─'.repeat(70)}`)
printInfo(`Transaction Group Summary`)
printInfo(`${'─'.repeat(70)}`)
printInfo(`Group ID: ${multiSignerResult.groupId}`)
printInfo(`Total Transactions: ${multiSignerResult.transactions.length}`)
printInfo(`${'─'.repeat(70)}`)
for (let i = 0; i < multiSignerResult.transactions.length; i++) {
const txn = multiSignerResult.transactions[i]
const confirmation = multiSignerResult.confirmations[i]
const txId = multiSignerResult.txIds[i]
printInfo(`\nTransaction [${i}]:`)
printInfo(` Transaction ID: ${txId}`)
printInfo(` Type: ${txn.type}`)
printInfo(` Sender: ${shortenAddress(txn.sender.toString())}`)
printInfo(` Fee: ${txn.fee ?? 0n} µALGO`)
printInfo(` First Valid: ${txn.firstValid}`)
printInfo(` Last Valid: ${txn.lastValid}`)
printInfo(` Confirmed Round: ${confirmation.confirmedRound}`)
}
printInfo(`\n${'─'.repeat(70)}`)
// Verify final balances
const aliceInfo = await algorand.account.getInformation(alice.addr)
const bobInfo = await algorand.account.getInformation(bob.addr)
const charlieInfo = await algorand.account.getInformation(charlie.addr)
printInfo(`\nFinal account balances:`)
printInfo(` Alice: ${formatAlgo(aliceInfo.balance)}`)
printInfo(` Bob: ${formatAlgo(bobInfo.balance)}`)
printInfo(` Charlie: ${formatAlgo(charlieInfo.balance)}`)
// Check Bob's asset balance
const bobAssets = bobInfo.assets
const bobAssetHolding = bobAssets?.find((a) => a.assetId === assetId)
printInfo(`\nBob's asset holdings:`)
printInfo(` Asset ID ${assetId}: ${bobAssetHolding?.amount ?? 0} units`)
printSuccess('Transaction Composer example completed!')
// Step 11: Summary of TransactionComposer API
printStep(11, 'Summary - TransactionComposer API')
printInfo('The TransactionComposer provides a fluent API for atomic transaction groups:')
printInfo('')
printInfo('Creating a composer:')
printInfo(' const composer = algorand.newGroup()')
printInfo('')
printInfo('Adding transactions:')
printInfo(' .addPayment({ sender, receiver, amount, ... })')
printInfo(' .addAssetCreate({ sender, total, decimals, ... })')
printInfo(' .addAssetOptIn({ sender, assetId })')
printInfo(' .addAssetTransfer({ sender, receiver, assetId, amount })')
printInfo(' .addAssetOptOut({ sender, assetId, creator })')
printInfo(' .addAssetConfig({ sender, assetId, ... })')
printInfo(' .addAssetFreeze({ sender, assetId, account, frozen })')
printInfo(' .addAssetDestroy({ sender, assetId })')
printInfo(' .addAppCreate({ sender, approvalProgram, clearStateProgram })')
printInfo(' .addAppUpdate({ sender, appId, approvalProgram, clearStateProgram })')
printInfo(' .addAppCall({ sender, appId, ... })')
printInfo(' .addAppDelete({ sender, appId })')
printInfo(' .addTransaction(txn, signer?) - Add a pre-built transaction')
printInfo('')
printInfo('Executing:')
printInfo(' .simulate({ skipSignatures: true }) - Preview without signing')
printInfo(' .send() - Sign and submit atomically')
printInfo('')
printInfo('Utility methods:')
printInfo(' .count() - Get number of transactions in the group')
printInfo(' .build() - Build transactions without sending')
printInfo(' .buildTransactions() - Get raw unsigned transactions')
printInfo('')
printInfo('Key concepts:')
printInfo(' - All transactions in a group share a unique group ID')
printInfo(' - Atomic execution: all succeed or all fail')
printInfo(' - Multiple signers supported (each tx uses its sender\'s signer)')
printInfo(' - Maximum 16 transactions per group')
// Clean up - Bob opts out (returns assets to Alice), then Alice destroys the asset
await algorand.send.assetOptOut({
sender: bob.addr,
assetId: assetId,
creator: alice.addr,
ensureZeroBalance: false,
})
await algorand.send.assetDestroy({
sender: alice.addr,
assetId: assetId,
})
}
main().catch((error) => {
printError(`Unhandled error: ${error instanceof Error ? error.message : String(error)}`)
process.exit(1)
})