Transaction Composer (Atomic Transaction Groups)
Description
Section titled “Description”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
Section titled “Prerequisites”- LocalNet running (via
algokit localnet start)
Run This Example
Section titled “Run This Example”From the repository root:
cd examplesnpm run example algorand_client/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)})Other examples in Algorand Client
Section titled “Other examples in Algorand Client”- Client Instantiation
- AlgoAmount Utility
- Signer Configuration
- Suggested Params Configuration
- Account Manager
- Send Payment
- Send Asset Operations
- Send Application Operations
- Create Transaction (Unsigned Transactions)
- Transaction Composer (Atomic Transaction Groups)
- Asset Manager
- App Manager
- App Deployer
- Client Manager
- Error Transformers
- Transaction Leases