Atomic Transaction Group
Description
Section titled “Description”This example demonstrates how to group multiple transactions atomically. All transactions in a group either succeed together or fail together. It shows:
- Creating multiple payment transactions
- Using groupTransactions() to assign a group ID
- Signing all transactions with the same signer
- Submitting as a single atomic 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 transact/07-atomic-group.ts/** * Example: Atomic Transaction Group * * This example demonstrates how to group multiple transactions atomically. * All transactions in a group either succeed together or fail together. * It shows: * - Creating multiple payment transactions * - Using groupTransactions() to assign a group ID * - Signing all transactions with the same signer * - Submitting as a single atomic group * * Prerequisites: * - LocalNet running (via `algokit localnet start`) */
import { AlgorandClient } from '@algorandfoundation/algokit-utils'import { Transaction, TransactionType, assignFee, groupTransactions, type PaymentTransactionFields,} from '@algorandfoundation/algokit-utils/transact'import { createAlgodClient, formatAlgo, getAccountBalance, printHeader, printInfo, printStep, printSuccess, shortenAddress, waitForConfirmation,} from '../shared/utils.js'
/** * Gets a funded account from LocalNet's KMD wallet */async function getLocalNetFundedAccount(algorand: AlgorandClient) { return await algorand.account.kmd.getLocalNetDispenserAccount()}
async function main() { printHeader('Atomic Transaction Group Example')
// Step 1: Initialize clients printStep(1, 'Initialize Algod Client') const algod = createAlgodClient() const algorand = AlgorandClient.defaultLocalNet() printInfo('Connected to LocalNet Algod')
// Step 2: Get a funded account from KMD (sender for all transactions) printStep(2, 'Get Funded Account from KMD') const sender = await getLocalNetFundedAccount(algorand) const senderBalance = await getAccountBalance(algorand, sender.addr.toString()) printInfo(`Sender address: ${shortenAddress(sender.addr.toString())}`) printInfo(`Sender balance: ${formatAlgo(senderBalance.microAlgo)}`)
// Step 3: Generate 3 receiver accounts using AlgorandClient helper printStep(3, 'Generate 3 Receiver Accounts') const receiver1 = algorand.account.random() const receiver2 = algorand.account.random() const receiver3 = algorand.account.random()
printInfo(`Receiver 1: ${shortenAddress(receiver1.addr.toString())}`) printInfo(`Receiver 2: ${shortenAddress(receiver2.addr.toString())}`) printInfo(`Receiver 3: ${shortenAddress(receiver3.addr.toString())}`)
// Step 4: Get suggested transaction parameters printStep(4, 'Get Suggested Transaction Parameters') const suggestedParams = await algod.suggestedParams() printInfo(`First valid round: ${suggestedParams.firstValid}`) printInfo(`Last valid round: ${suggestedParams.lastValid}`) printInfo(`Min fee: ${suggestedParams.minFee} microALGO`)
// Step 5: Create 3 payment transactions with different amounts printStep(5, 'Create 3 Payment Transactions') const amounts = [1_000_000n, 2_000_000n, 3_000_000n] // 1, 2, 3 ALGO
// Create payment fields for each receiver const paymentFields1: PaymentTransactionFields = { receiver: receiver1.addr, amount: amounts[0], } const paymentFields2: PaymentTransactionFields = { receiver: receiver2.addr, amount: amounts[1], } const paymentFields3: PaymentTransactionFields = { receiver: receiver3.addr, amount: amounts[2], }
// Create base transactions const tx1 = new Transaction({ type: TransactionType.Payment, sender: sender.addr, firstValid: suggestedParams.firstValid, lastValid: suggestedParams.lastValid, genesisHash: suggestedParams.genesisHash, genesisId: suggestedParams.genesisId, payment: paymentFields1, })
const tx2 = new Transaction({ type: TransactionType.Payment, sender: sender.addr, firstValid: suggestedParams.firstValid, lastValid: suggestedParams.lastValid, genesisHash: suggestedParams.genesisHash, genesisId: suggestedParams.genesisId, payment: paymentFields2, })
const tx3 = new Transaction({ type: TransactionType.Payment, sender: sender.addr, firstValid: suggestedParams.firstValid, lastValid: suggestedParams.lastValid, genesisHash: suggestedParams.genesisHash, genesisId: suggestedParams.genesisId, payment: paymentFields3, })
printInfo(`Transaction 1: ${formatAlgo(amounts[0])} to Receiver 1`) printInfo(`Transaction 2: ${formatAlgo(amounts[1])} to Receiver 2`) printInfo(`Transaction 3: ${formatAlgo(amounts[2])} to Receiver 3`)
// Step 6: Assign fees to all transactions printStep(6, 'Assign Transaction Fees') const tx1WithFee = assignFee(tx1, { feePerByte: suggestedParams.fee, minFee: suggestedParams.minFee, }) const tx2WithFee = assignFee(tx2, { feePerByte: suggestedParams.fee, minFee: suggestedParams.minFee, }) const tx3WithFee = assignFee(tx3, { feePerByte: suggestedParams.fee, minFee: suggestedParams.minFee, })
printInfo(`Fee per transaction: ${tx1WithFee.fee} microALGO`) printInfo(`Total fees: ${(tx1WithFee.fee ?? 0n) + (tx2WithFee.fee ?? 0n) + (tx3WithFee.fee ?? 0n)} microALGO`)
// Step 7: Group the transactions using groupTransactions() printStep(7, 'Group Transactions with groupTransactions()') const transactionsWithFees = [tx1WithFee, tx2WithFee, tx3WithFee] const groupedTransactions = groupTransactions(transactionsWithFees)
// All transactions now have the same group ID const groupId = groupedTransactions[0].group printInfo(`Group ID assigned to all transactions`) printInfo(`Group ID (base64): ${groupId ? Buffer.from(groupId).toString('base64') : 'undefined'}`) printInfo(`All 3 transactions now share the same group ID`)
// Step 8: Sign all transactions with the same signer printStep(8, 'Sign All Transactions')
// Sign each transaction (all from same sender) const signedTx1 = await sender.signer([groupedTransactions[0]], [0]) const signedTx2 = await sender.signer([groupedTransactions[1]], [0]) const signedTx3 = await sender.signer([groupedTransactions[2]], [0])
printInfo('All 3 transactions signed successfully')
// Get transaction IDs for confirmation tracking const txId1 = groupedTransactions[0].txId() const txId2 = groupedTransactions[1].txId() const txId3 = groupedTransactions[2].txId()
printInfo(`Transaction 1 ID: ${txId1}`) printInfo(`Transaction 2 ID: ${txId2}`) printInfo(`Transaction 3 ID: ${txId3}`)
// Step 9: Submit as a single group using concatenated bytes printStep(9, 'Submit Atomic Group')
// Concatenate all signed transaction bytes const concatenatedBytes = new Uint8Array([...signedTx1[0], ...signedTx2[0], ...signedTx3[0]])
printInfo(`Submitting ${groupedTransactions.length} grouped transactions as a single atomic unit`)
await algod.sendRawTransaction(concatenatedBytes) printInfo('Atomic group submitted to network')
// Wait for confirmation of the first transaction (all will be confirmed together) const pendingInfo = await waitForConfirmation(algod, txId1) printInfo(`Atomic group confirmed in round: ${pendingInfo.confirmedRound}`)
// Step 10: Verify all receivers received their amounts printStep(10, 'Verify All Receivers Received Amounts')
const receiver1Balance = await getAccountBalance(algorand, receiver1.addr.toString()) const receiver2Balance = await getAccountBalance(algorand, receiver2.addr.toString()) const receiver3Balance = await getAccountBalance(algorand, receiver3.addr.toString())
printInfo(`Receiver 1 balance: ${formatAlgo(receiver1Balance.microAlgo)} (expected: ${formatAlgo(amounts[0])})`) printInfo(`Receiver 2 balance: ${formatAlgo(receiver2Balance.microAlgo)} (expected: ${formatAlgo(amounts[1])})`) printInfo(`Receiver 3 balance: ${formatAlgo(receiver3Balance.microAlgo)} (expected: ${formatAlgo(amounts[2])})`)
// Verify all balances match expected amounts const allCorrect = receiver1Balance.microAlgo === amounts[0] && receiver2Balance.microAlgo === amounts[1] && receiver3Balance.microAlgo === amounts[2]
if (allCorrect) { printSuccess('All receivers received their expected amounts!') } else { throw new Error('One or more receivers did not receive the expected amount') }
// Step 11: Demonstrate atomicity concept printStep(11, 'Atomicity Explanation') printInfo('Group transactions succeed or fail together:') printInfo('- If any transaction in the group fails validation, ALL fail') printInfo('- If all transactions pass validation, ALL succeed') printInfo('- This is crucial for atomic swaps, multi-party payments, etc.') printInfo('') printInfo('Example failure scenarios that would cause ALL transactions to fail:') printInfo('- Insufficient funds for any payment') printInfo('- Invalid signature on any transaction') printInfo('- Mismatched group IDs between transactions')
// Get final sender balance const senderFinalBalance = await getAccountBalance(algorand, sender.addr.toString()) const totalSent = amounts[0] + amounts[1] + amounts[2] const totalFees = (tx1WithFee.fee ?? 0n) + (tx2WithFee.fee ?? 0n) + (tx3WithFee.fee ?? 0n)
printInfo('') printInfo(`Total ALGO sent: ${formatAlgo(totalSent)}`) printInfo(`Total fees paid: ${formatAlgo(totalFees)}`) printInfo(`Sender balance change: ${formatAlgo(senderBalance.microAlgo - senderFinalBalance.microAlgo)}`)
printSuccess('Atomic transaction group example completed!')}
main().catch((error) => { console.error('Error:', error) process.exit(1)})