Skip to content
Algorand Developer Portal

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