Skip to content

Transaction Simulation

← Back to Algod Client

This example demonstrates how to simulate transactions before submitting them to the Algorand network. Simulation allows you to:

  • Preview transaction outcomes without committing to the blockchain
  • Estimate transaction fees
  • Detect potential errors before spending fees
  • Debug smart contract execution
  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example algod_client/13-simulation.ts

View source on GitHub

13-simulation.ts
/**
* Example: Transaction Simulation
*
* This example demonstrates how to simulate transactions before submitting them
* to the Algorand network. Simulation allows you to:
* - Preview transaction outcomes without committing to the blockchain
* - Estimate transaction fees
* - Detect potential errors before spending fees
* - Debug smart contract execution
*
* Prerequisites:
* - LocalNet running (via `algokit localnet start`)
*/
import { algo } from '@algorandfoundation/algokit-utils'
import type {
SimulateRequest,
} from '@algorandfoundation/algokit-utils/algod-client'
import {
createAlgodClient,
createAlgorandClient,
formatMicroAlgo,
printHeader,
printInfo,
printStep,
printSuccess,
shortenAddress,
} from '../shared/utils.js'
async function main() {
printHeader('Transaction Simulation Example')
// Create clients
const algod = createAlgodClient()
const algorand = createAlgorandClient()
// =========================================================================
// Step 1: Set up accounts for simulation
// =========================================================================
printStep(1, 'Setting up accounts for simulation')
// Get a funded account from LocalNet (the dispenser)
const sender = await algorand.account.dispenserFromEnvironment()
printInfo(`Sender address: ${shortenAddress(sender.addr.toString())}`)
// Get sender balance
const senderInfo = await algod.accountInformation(sender.addr.toString())
printInfo(`Sender balance: ${formatMicroAlgo(senderInfo.amount)}`)
// Create a new random account as receiver
const receiver = algorand.account.random()
printInfo(`Receiver address: ${shortenAddress(receiver.addr.toString())}`)
printInfo('Receiver is a new unfunded account')
printInfo('')
// =========================================================================
// Step 2: Create a payment transaction for simulation
// =========================================================================
printStep(2, 'Creating a payment transaction for simulation')
const paymentAmount = algo(1) // 1 ALGO
printInfo(`Payment amount: ${paymentAmount.algo} ALGO`)
// Build the transaction using AlgorandClient.createTransaction
const paymentTxn = await algorand.createTransaction.payment({
sender: sender.addr,
receiver: receiver.addr,
amount: paymentAmount,
})
// Get the transaction ID
const txId = paymentTxn.txId()
printInfo(`Transaction ID: ${txId}`)
// Sign the transaction
const signedTxn = await sender.signer([paymentTxn], [0])
printSuccess('Transaction signed successfully!')
printInfo('')
// =========================================================================
// Step 3: Simulate using simulateRawTransactions()
// =========================================================================
printStep(3, 'Simulating with simulateRawTransactions(signedTxns)')
printInfo('simulateRawTransactions() is a convenience method that:')
printInfo(' - Accepts encoded signed transactions (Uint8Array or Uint8Array[])')
printInfo(' - Decodes and wraps them in a SimulateRequest')
printInfo(' - Returns SimulateResponse with detailed results')
printInfo('')
const simResult = await algod.simulateRawTransactions(signedTxn)
printSuccess('Simulation completed!')
printInfo('')
// =========================================================================
// Step 4: Display simulation results
// =========================================================================
printStep(4, 'Displaying simulation results')
printInfo('SimulateResponse structure:')
printInfo(` version: ${simResult.version}`)
printInfo(` lastRound: ${simResult.lastRound.toLocaleString('en-US')}`)
printInfo(` txnGroups: ${simResult.txnGroups.length} group(s)`)
printInfo('')
// Check if the transaction would succeed
const txnGroup = simResult.txnGroups[0]
const wouldSucceed = !txnGroup.failureMessage
printInfo('Transaction Group Result:')
printInfo(` Would succeed: ${wouldSucceed ? 'Yes' : 'No'}`)
if (txnGroup.failureMessage) {
printInfo(` Failure message: ${txnGroup.failureMessage}`)
if (txnGroup.failedAt) {
printInfo(` Failed at: [${txnGroup.failedAt.join(', ')}]`)
}
}
printInfo('')
// Display budget information (for app calls)
if (txnGroup.appBudgetAdded !== undefined || txnGroup.appBudgetConsumed !== undefined) {
printInfo('App Budget:')
printInfo(` Budget added: ${txnGroup.appBudgetAdded ?? 'N/A'}`)
printInfo(` Budget consumed: ${txnGroup.appBudgetConsumed ?? 'N/A'}`)
printInfo('')
}
// =========================================================================
// Step 5: Display individual transaction results and fixedSigner
// =========================================================================
printStep(5, 'Displaying individual transaction results')
for (let i = 0; i < txnGroup.txnResults.length; i++) {
const txnResult = txnGroup.txnResults[i]
printInfo(`Transaction ${i + 1}:`)
// The txnResult contains a PendingTransactionResponse
const pendingResponse = txnResult.txnResult
printInfo(` Transaction type: ${pendingResponse.txn.txn.type}`)
// Display fixedSigner if present (indicates missing/wrong signature)
if (txnResult.fixedSigner) {
printInfo(` Fixed signer: ${shortenAddress(txnResult.fixedSigner.toString())}`)
printInfo(' ^ This indicates the correct signer when simulation used allowEmptySignatures or fixSigners')
} else {
printInfo(' Fixed signer: None (signature was correct)')
}
// Display budget consumed (for app calls)
if (txnResult.appBudgetConsumed !== undefined) {
printInfo(` App budget consumed: ${txnResult.appBudgetConsumed}`)
}
if (txnResult.logicSigBudgetConsumed !== undefined) {
printInfo(` LogicSig budget consumed: ${txnResult.logicSigBudgetConsumed}`)
}
printInfo('')
}
// =========================================================================
// Step 6: Show transaction group details
// =========================================================================
printStep(6, 'Show transaction group details from simulation')
printInfo('Each SimulateTransactionResult contains txnResult (PendingTransactionResponse)')
printInfo('which includes the transaction details as if it were confirmed.')
printInfo('')
const txnDetails = txnGroup.txnResults[0].txnResult
printInfo('Simulated Transaction Details:')
printInfo(` Type: ${txnDetails.txn.txn.type}`)
printInfo(` Sender: ${shortenAddress(txnDetails.txn.txn.sender.toString())}`)
printInfo(` Fee: ${formatMicroAlgo(txnDetails.txn.txn.fee ?? 0n)}`)
printInfo(` First valid: ${txnDetails.txn.txn.firstValid.toLocaleString('en-US')}`)
printInfo(` Last valid: ${txnDetails.txn.txn.lastValid.toLocaleString('en-US')}`)
if (txnDetails.txn.txn.payment) {
printInfo('')
printInfo(' Payment fields:')
printInfo(` Receiver: ${shortenAddress(txnDetails.txn.txn.payment.receiver.toString())}`)
printInfo(` Amount: ${formatMicroAlgo(txnDetails.txn.txn.payment.amount)}`)
}
printInfo('')
// =========================================================================
// Step 7: Demonstrate simulation with extra budget options
// =========================================================================
printStep(7, 'Demonstrating simulation with extra budget options')
printInfo('simulateTransactions() accepts a SimulateRequest with various options:')
printInfo(' - allowEmptySignatures: Simulate unsigned transactions')
printInfo(' - allowMoreLogging: Lift limits on log opcode usage')
printInfo(' - allowUnnamedResources: Access resources not in txn references')
printInfo(' - extraOpcodeBudget: Add extra opcode budget for app calls')
printInfo(' - fixSigners: Auto-fix incorrect signers in simulation')
printInfo('')
// Demonstrate with allowEmptySignatures (useful for fee estimation without signing)
printInfo('Simulating an UNSIGNED transaction with allowEmptySignatures=true:')
printInfo('')
// Create an unsigned transaction
const unsignedTxn = await algorand.createTransaction.payment({
sender: sender.addr,
receiver: receiver.addr,
amount: algo(0.5),
})
// Import SignedTransaction type to create unsigned wrapper
const { decodeSignedTransaction, encodeSignedTransaction } = await import('@algorandfoundation/algokit-transact')
// Create a "signed" transaction with no actual signature for simulation
// We encode the transaction without a signature - using the signer but only getting the transaction portion
const unsignedForSim: SimulateRequest = {
txnGroups: [{
txns: [{
txn: unsignedTxn,
// No sig field - transaction is unsigned
}]
}],
allowEmptySignatures: true, // Key option: allow unsigned transactions
fixSigners: true, // Fix any signer issues during simulation
}
const unsignedSimResult = await algod.simulateTransactions(unsignedForSim)
printInfo('Unsigned transaction simulation result:')
printInfo(` Would succeed: ${!unsignedSimResult.txnGroups[0].failureMessage ? 'Yes' : 'No'}`)
// Check if fixedSigner shows the required signer
const fixedSignerResult = unsignedSimResult.txnGroups[0].txnResults[0]
if (fixedSignerResult.fixedSigner) {
printInfo(` Required signer: ${shortenAddress(fixedSignerResult.fixedSigner.toString())}`)
}
printInfo('')
// Demonstrate with extraOpcodeBudget
printInfo('extraOpcodeBudget is useful for complex app calls that need more compute:')
printInfo('')
const extraBudgetRequest: SimulateRequest = {
txnGroups: [{
txns: [{
txn: unsignedTxn,
}]
}],
allowEmptySignatures: true,
extraOpcodeBudget: 10000, // Add 10,000 extra opcodes
}
const extraBudgetResult = await algod.simulateTransactions(extraBudgetRequest)
printInfo('Simulation with extra budget:')
printInfo(` Would succeed: ${!extraBudgetResult.txnGroups[0].failureMessage ? 'Yes' : 'No'}`)
printInfo(` (extraOpcodeBudget is mainly useful for app calls, not simple payments)`)
printInfo('')
// =========================================================================
// Step 8: Show how simulation can estimate fees and detect errors
// =========================================================================
printStep(8, 'Using simulation to estimate fees and detect errors')
printInfo('Simulation helps with fee estimation by:')
printInfo(' 1. Determining minimum fee for transaction to succeed')
printInfo(' 2. Checking if your fee is sufficient before sending')
printInfo(' 3. Avoiding wasted fees on transactions that would fail')
printInfo('')
// Get current suggested params to show fee structure
const params = await algod.suggestedParams()
printInfo('Current fee structure:')
printInfo(` Min fee: ${formatMicroAlgo(params.minFee)}`)
printInfo(` Suggested fee: ${formatMicroAlgo(params.fee)}`)
printInfo('')
printInfo('Simulation can detect errors BEFORE spending fees:')
printInfo('')
// Error detection example - insufficient balance
printInfo('Example: Detecting an overspend error')
// Create a transaction that would overspend
const poorAccount = algorand.account.random()
const overspendTxn = await algorand.createTransaction.payment({
sender: poorAccount.addr,
receiver: receiver.addr,
amount: algo(1000000), // 1 million ALGO - way more than account has
})
const overspendRequest: SimulateRequest = {
txnGroups: [{
txns: [{
txn: overspendTxn,
}]
}],
allowEmptySignatures: true,
}
const overspendResult = await algod.simulateTransactions(overspendRequest)
const overspendGroup = overspendResult.txnGroups[0]
printInfo(' Overspend simulation result:')
printInfo(` Would succeed: ${!overspendGroup.failureMessage ? 'Yes' : 'No'}`)
if (overspendGroup.failureMessage) {
printInfo(` Failure: ${overspendGroup.failureMessage}`)
}
printInfo('')
// =========================================================================
// Step 9: Simulate a failing transaction and display the failure reason
// =========================================================================
printStep(9, 'Simulating a failing transaction')
printInfo('Demonstrating various failure scenarios:')
printInfo('')
// Failure scenario 1: Insufficient funds (already shown above)
printInfo('1. Insufficient funds:')
printInfo(` Error: ${overspendGroup.failureMessage}`)
printInfo('')
// Failure scenario 2: Invalid receiver address - let's try sending to the sender (self-send is OK, but we can show transaction with close-remainder issue)
printInfo('2. Sending to zero balance account and closing:')
printInfo(' Simulating a close-out transaction to demonstrate simulation details')
// Create a transaction that closes out to a specific address
const closeOutTxn = await algorand.createTransaction.payment({
sender: sender.addr,
receiver: receiver.addr,
amount: algo(0),
closeRemainderTo: receiver.addr, // Close account to receiver
})
const closeOutRequest: SimulateRequest = {
txnGroups: [{
txns: [{
txn: closeOutTxn,
}]
}],
allowEmptySignatures: true,
}
const closeOutResult = await algod.simulateTransactions(closeOutRequest)
const closeOutGroup = closeOutResult.txnGroups[0]
printInfo(` Would succeed: ${!closeOutGroup.failureMessage ? 'Yes' : 'No'}`)
if (closeOutGroup.failureMessage) {
printInfo(` Failure: ${closeOutGroup.failureMessage}`)
} else {
printInfo(' This would succeed (sender can close to receiver)')
}
printInfo('')
// Failure scenario 3: Insufficient fee (below minimum)
printInfo('3. Transaction with fee below minimum:')
printInfo(' Note: Very low fees are rejected before simulation even runs')
// Create a transaction with a fee set to 0 (below min fee of 1000)
const lowFeeTxn = await algorand.createTransaction.payment({
sender: sender.addr,
receiver: receiver.addr,
amount: algo(0.1),
staticFee: algo(0), // 0 fee - below minimum
})
const lowFeeRequest: SimulateRequest = {
txnGroups: [{
txns: [{
txn: lowFeeTxn,
}]
}],
allowEmptySignatures: true,
}
try {
const lowFeeResult = await algod.simulateTransactions(lowFeeRequest)
const lowFeeGroup = lowFeeResult.txnGroups[0]
printInfo(` Would succeed: ${!lowFeeGroup.failureMessage ? 'Yes' : 'No'}`)
if (lowFeeGroup.failureMessage) {
printInfo(` Failure: ${lowFeeGroup.failureMessage}`)
}
} catch (error) {
// Fee too low errors are caught at the API level, not in simulation results
const errorMessage = error instanceof Error ? error.message : String(error)
if (errorMessage.includes('less than the minimum')) {
printInfo(` Rejected before simulation: Fee too low`)
printInfo(' Algod validates minimum fees before running simulation')
} else {
printInfo(` Error: ${errorMessage}`)
}
}
printInfo('')
// =========================================================================
// Summary
// =========================================================================
printHeader('Summary')
printInfo('This example demonstrated:')
printInfo('')
printInfo('1. simulateRawTransactions(signedTxns):')
printInfo(' - Convenience method for simulating encoded signed transactions')
printInfo(' - Accepts Uint8Array or Uint8Array[] (signed transaction bytes)')
printInfo(' - Returns SimulateResponse with detailed results')
printInfo('')
printInfo('2. SimulateResponse structure:')
printInfo(' - version: API version number')
printInfo(' - lastRound: Round at which simulation was performed')
printInfo(' - txnGroups: Array of SimulateTransactionGroupResult')
printInfo('')
printInfo('3. SimulateTransactionGroupResult:')
printInfo(' - txnResults: Array of individual transaction results')
printInfo(' - failureMessage?: Error message if group would fail')
printInfo(' - failedAt?: Index path to failing transaction')
printInfo(' - appBudgetAdded/Consumed: App call budget tracking')
printInfo('')
printInfo('4. SimulateTransactionResult:')
printInfo(' - txnResult: PendingTransactionResponse with full details')
printInfo(' - fixedSigner?: Address that should have signed')
printInfo(' - appBudgetConsumed: Budget used by this transaction')
printInfo(' - logicSigBudgetConsumed: Budget used by logic signature')
printInfo('')
printInfo('5. SimulateRequest options:')
printInfo(' - allowEmptySignatures: Simulate without signatures')
printInfo(' - allowMoreLogging: Lift log opcode limits')
printInfo(' - allowUnnamedResources: Access unref\'d resources')
printInfo(' - extraOpcodeBudget: Add extra compute budget')
printInfo(' - fixSigners: Auto-fix incorrect signers')
printInfo('')
printInfo('6. Use cases for simulation:')
printInfo(' - Fee estimation without signing')
printInfo(' - Error detection before spending fees')
printInfo(' - Debugging smart contract execution')
printInfo(' - Validating transaction groups')
printInfo(' - Testing complex app interactions')
}
main().catch((error) => {
console.error('Fatal error:', error)
process.exit(1)
})