Transaction Simulation
Description
Section titled “Description”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
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 algod_client/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);});Other examples in Algod Client
Section titled “Other examples in Algod Client”- Node Health and Status
- Version and Genesis Information
- Ledger Supply Information
- Account Information
- Transaction Parameters
- Send and Confirm Transaction
- Pending Transactions
- Block Data
- Asset Information
- Application Information
- Application Boxes
- TEAL Compile and Disassemble
- Transaction Simulation
- Ledger State Deltas
- Transaction Proof
- Light Block Header Proof
- State Proof
- DevMode Timestamp Offset
- Sync Round Management