Skip to content
Algorand Developer Portal

Send and Confirm Transaction

← Back to Algod Client

This example demonstrates how to send transactions and wait for confirmation using sendRawTransaction() and pendingTransactionInformation(). It shows the complete lifecycle of submitting a transaction to the Algorand network.

  • LocalNet running (via algokit localnet start)

From the repository root:

Terminal window
cd examples
npm run example algod_client/06-send-transaction.ts

View source on GitHub

06-send-transaction.ts
/**
* Example: Send and Confirm Transaction
*
* This example demonstrates how to send transactions and wait for confirmation
* using sendRawTransaction() and pendingTransactionInformation(). It shows
* the complete lifecycle of submitting a transaction to the Algorand network.
*
* Prerequisites:
* - LocalNet running (via `algokit localnet start`)
*/
import { algo } from '@algorandfoundation/algokit-utils';
import type { PendingTransactionResponse } from '@algorandfoundation/algokit-utils/algod-client';
import {
createAlgodClient,
createAlgorandClient,
formatMicroAlgo,
printError,
printHeader,
printInfo,
printStep,
printSuccess,
shortenAddress,
} from '../shared/utils.js';
/**
* Format a bigint as microAlgos and Algos
*/
function formatFee(microAlgos: bigint): string {
const algoValue = Number(microAlgos) / 1_000_000;
return `${microAlgos.toLocaleString('en-US')} µALGO (${algoValue.toFixed(6)} ALGO)`;
}
/**
* Wait for a transaction to be confirmed using pendingTransactionInformation
* This implements a polling loop to check transaction status.
*
* @param algod - The AlgodClient instance
* @param txId - The transaction ID to wait for
* @param maxRounds - Maximum number of rounds to wait (default: 10)
* @returns The PendingTransactionResponse when confirmed
*/
async function waitForConfirmation(
algod: ReturnType<typeof createAlgodClient>,
txId: string,
maxRounds: number = 10,
): Promise<PendingTransactionResponse> {
// Get the current status to know what round we're on
const status = await algod.status();
let currentRound = status.lastRound;
const endRound = currentRound + BigInt(maxRounds);
printInfo(` Starting at round: ${currentRound.toLocaleString('en-US')}`);
printInfo(` Will wait until round: ${endRound.toLocaleString('en-US')}`);
printInfo('');
while (currentRound < endRound) {
// Check the transaction status
const pendingInfo = await algod.pendingTransactionInformation(txId);
// Case 1: Transaction is confirmed (confirmedRound > 0)
if (pendingInfo.confirmedRound && pendingInfo.confirmedRound > 0n) {
printInfo(
` Transaction confirmed in round ${pendingInfo.confirmedRound.toLocaleString('en-US')}`,
);
return pendingInfo;
}
// Case 2: Transaction was rejected (poolError is not empty)
if (pendingInfo.poolError && pendingInfo.poolError.length > 0) {
throw new Error(`Transaction rejected: ${pendingInfo.poolError}`);
}
// Case 3: Transaction is still pending (confirmedRound = 0, poolError = "")
printInfo(` Round ${currentRound.toLocaleString('en-US')}: Transaction still pending...`);
// Wait for the next block
await algod.statusAfterBlock(currentRound);
currentRound++;
}
throw new Error(`Transaction ${txId} not confirmed after ${maxRounds} rounds`);
}
async function main() {
printHeader('Send and Confirm Transaction Example');
// Create clients
const algod = createAlgodClient();
const algorand = createAlgorandClient();
// =========================================================================
// Step 1: Get a funded account and create a receiver
// =========================================================================
printStep(1, 'Setting up sender and receiver accounts');
// 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: Get suggested transaction parameters
// =========================================================================
printStep(2, 'Getting suggested transaction parameters');
const suggestedParams = await algod.suggestedParams();
printInfo(`First valid round: ${suggestedParams.firstValid.toLocaleString('en-US')}`);
printInfo(`Last valid round: ${suggestedParams.lastValid.toLocaleString('en-US')}`);
printInfo(`Min fee: ${formatFee(suggestedParams.minFee)}`);
printInfo(`Genesis ID: ${suggestedParams.genesisId}`);
printInfo('');
// =========================================================================
// Step 3: Create a payment transaction using algosdk/algokit
// =========================================================================
printStep(3, 'Creating a payment transaction');
const paymentAmount = algo(1); // 1 ALGO
printInfo(
`Payment amount: ${paymentAmount.algo} ALGO (${paymentAmount.microAlgo.toLocaleString('en-US')} µALGO)`,
);
printInfo(`Sender: ${shortenAddress(sender.addr.toString())}`);
printInfo(`Receiver: ${shortenAddress(receiver.addr.toString())}`);
printInfo('');
// Build the transaction using AlgorandClient.createTransaction
// This creates an unsigned Transaction object
const paymentTxn = await algorand.createTransaction.payment({
sender: sender.addr,
receiver: receiver.addr,
amount: paymentAmount,
});
// Get the transaction ID before sending
const txId = paymentTxn.txId();
printInfo(`Transaction ID: ${txId}`);
// Sign the transaction using the sender's signer
const signedTxn = await sender.signer([paymentTxn], [0]);
printSuccess('Transaction signed successfully!');
printInfo('');
// =========================================================================
// Step 4: Submit the transaction using sendRawTransaction()
// =========================================================================
printStep(4, 'Submitting transaction with sendRawTransaction()');
try {
const submitResponse = await algod.sendRawTransaction(signedTxn);
printSuccess(`Transaction submitted successfully!`);
printInfo(`Transaction ID from response: ${submitResponse.txId}`);
printInfo('');
printInfo('sendRawTransaction() accepts:');
printInfo(' - A single signed transaction (Uint8Array)');
printInfo(' - An array of signed transactions (Uint8Array[])');
printInfo('Returns PostTransactionsResponse with txid field');
printInfo('');
} catch (error) {
printError(
`Failed to submit transaction: ${error instanceof Error ? error.message : String(error)}`,
);
printInfo('Common errors:');
printInfo(' - "txn dead" - Transaction validity window has passed');
printInfo(' - "overspend" - Sender has insufficient funds');
printInfo(' - "fee too small" - Transaction fee is below minimum');
throw error;
}
// =========================================================================
// Step 5: Check transaction status with pendingTransactionInformation()
// =========================================================================
printStep(5, 'Checking transaction status with pendingTransactionInformation()');
// First, let's check the initial status (may already be confirmed on LocalNet)
const initialStatus = await algod.pendingTransactionInformation(txId);
printInfo('Initial transaction status:');
printInfo(` confirmedRound: ${initialStatus.confirmedRound ?? 'undefined (not yet confirmed)'}`);
printInfo(
` poolError: "${initialStatus.poolError}" ${initialStatus.poolError ? '(ERROR!)' : '(empty = no error)'}`,
);
printInfo('');
printInfo('pendingTransactionInformation() returns PendingTransactionResponse with:');
printInfo(' - confirmedRound: The round the txn was confirmed (0 or undefined if pending)');
printInfo(' - poolError: Error message if txn was rejected (empty if OK)');
printInfo(' - txn: The signed transaction object');
printInfo(' - And other fields like rewards, inner transactions, etc.');
printInfo('');
// =========================================================================
// Step 6: Wait for confirmation using a polling loop
// =========================================================================
printStep(6, 'Implementing waitForConfirmation loop');
printInfo('The waitForConfirmation pattern:');
printInfo(' 1. Call pendingTransactionInformation(txId)');
printInfo(' 2. If confirmedRound > 0: Transaction confirmed!');
printInfo(' 3. If poolError is not empty: Transaction rejected!');
printInfo(' 4. Otherwise: Wait for next block with statusAfterBlock(round)');
printInfo(' 5. Repeat until confirmed, rejected, or timeout');
printInfo('');
let confirmedInfo: PendingTransactionResponse;
// On LocalNet in dev mode, the transaction may already be confirmed
if (initialStatus.confirmedRound && initialStatus.confirmedRound > 0n) {
printInfo('Transaction was already confirmed (LocalNet dev mode)');
confirmedInfo = initialStatus;
} else {
printInfo('Waiting for confirmation...');
printInfo('');
confirmedInfo = await waitForConfirmation(algod, txId, 10);
}
printSuccess('Transaction confirmed!');
printInfo('');
// =========================================================================
// Step 7: Display confirmed transaction details
// =========================================================================
printStep(7, 'Displaying confirmed transaction details');
printInfo('Confirmed Transaction Details:');
printInfo(` confirmedRound: ${confirmedInfo.confirmedRound?.toLocaleString('en-US')}`);
printInfo('');
// Display the transaction object details
printInfo('Transaction Object (txn):');
const txn = confirmedInfo.txn.txn;
printInfo(` type: ${txn.type}`);
printInfo(` sender: ${shortenAddress(txn.sender.toString())}`);
printInfo(` fee: ${formatFee(txn.fee ?? 0n)}`);
printInfo(` firstValid: ${txn.firstValid.toLocaleString('en-US')}`);
printInfo(` lastValid: ${txn.lastValid.toLocaleString('en-US')}`);
printInfo(` genesisId: ${txn.genesisId}`);
// Payment-specific fields
if (txn.payment) {
printInfo('');
printInfo('Payment Fields:');
printInfo(` receiver: ${shortenAddress(txn.payment.receiver.toString())}`);
printInfo(` amount: ${formatMicroAlgo(txn.payment.amount)}`);
}
printInfo('');
// Display rewards (if any)
if (confirmedInfo.senderRewards !== undefined) {
printInfo('Rewards:');
printInfo(` senderRewards: ${formatMicroAlgo(confirmedInfo.senderRewards)}`);
if (confirmedInfo.receiverRewards !== undefined) {
printInfo(` receiverRewards: ${formatMicroAlgo(confirmedInfo.receiverRewards)}`);
}
printInfo('');
}
// =========================================================================
// Step 8: Handle and display transaction errors
// =========================================================================
printStep(8, 'Demonstrating error handling (poolError)');
printInfo('The poolError field in PendingTransactionResponse indicates why a');
printInfo('transaction was rejected from the transaction pool.');
printInfo('');
printInfo('Common poolError values:');
printInfo(' "" (empty string) - Transaction is valid and in pool/confirmed');
printInfo(' "transaction already in ledger" - Duplicate transaction');
printInfo(' "txn dead" - Transaction validity window expired');
printInfo(' "overspend" - Sender has insufficient funds');
printInfo(' "fee too small" - Fee is below network minimum');
printInfo(' "asset frozen" - Asset is frozen for the account');
printInfo(' "logic eval error" - Smart contract evaluation failed');
printInfo('');
printInfo('Best practice: Always check poolError before assuming success');
printInfo('');
// Example of checking poolError
printInfo('Example error handling pattern:');
printInfo('```');
printInfo('const pendingInfo = await algod.pendingTransactionInformation(txId)');
printInfo('if (pendingInfo.poolError && pendingInfo.poolError.length > 0) {');
printInfo(' throw new Error(`Transaction rejected: ${pendingInfo.poolError}`)');
printInfo('}');
printInfo('if (pendingInfo.confirmedRound && pendingInfo.confirmedRound > 0) {');
printInfo(' console.log("Transaction confirmed!")');
printInfo('}');
printInfo('```');
printInfo('');
// =========================================================================
// Step 9: Verify the payment was received
// =========================================================================
printStep(9, 'Verifying the receiver got the funds');
const receiverInfo = await algod.accountInformation(receiver.addr.toString());
printInfo(`Receiver balance: ${formatMicroAlgo(receiverInfo.amount)}`);
if (receiverInfo.amount === paymentAmount.microAlgo) {
printSuccess(`Payment of ${paymentAmount.algo} ALGO received successfully!`);
} else {
printError(`Expected ${paymentAmount.microAlgo} µALGO but got ${receiverInfo.amount} µALGO`);
}
// =========================================================================
// Summary
// =========================================================================
printHeader('Summary');
printInfo('This example demonstrated:');
printInfo(' 1. sendRawTransaction(signedTxn) - Submit a signed transaction');
printInfo(' 2. pendingTransactionInformation(txId) - Check transaction status');
printInfo(' 3. waitForConfirmation loop - Poll until confirmed or rejected');
printInfo(' 4. Confirmed transaction details: confirmedRound, txn, fee');
printInfo(' 5. Error handling with poolError field');
printInfo('');
printInfo('Key PendingTransactionResponse fields:');
printInfo(' - confirmedRound: Round when confirmed (bigint, undefined if pending)');
printInfo(' - poolError: Error message if rejected (string, empty if OK)');
printInfo(' - txn: The SignedTransaction object');
printInfo(' - senderRewards: Rewards applied to sender (bigint)');
printInfo(' - receiverRewards: Rewards applied to receiver (bigint)');
printInfo(' - closingAmount: Amount sent to close-to address (bigint)');
printInfo('');
printInfo('Transaction Status Cases:');
printInfo(' - confirmedRound > 0: Transaction committed to ledger');
printInfo(' - confirmedRound = 0, poolError = "": Still pending in pool');
printInfo(' - confirmedRound = 0, poolError != "": Rejected from pool');
printInfo('');
printInfo('Best practices:');
printInfo(' - Always wait for confirmation before considering a transaction final');
printInfo(' - Check poolError for rejection reasons');
printInfo(' - Use appropriate timeout (validity window is typically 1000 rounds)');
printInfo(' - On LocalNet dev mode, transactions confirm immediately');
}
main().catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});