Encoding/Decoding
Description
Section titled “Description”This example demonstrates how to serialize and deserialize transactions using the transact package:
- encodeTransaction() to get msgpack bytes with TX prefix
- encodeTransactionRaw() to get msgpack bytes without prefix
- decodeTransaction() to reconstruct transaction from bytes
- encodeSignedTransaction() and decodeSignedTransaction() for signed transactions
- txId() for calculating transaction ID
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/13-encoding-decoding.ts/** * Example: Encoding/Decoding * * This example demonstrates how to serialize and deserialize transactions * using the transact package: * - encodeTransaction() to get msgpack bytes with TX prefix * - encodeTransactionRaw() to get msgpack bytes without prefix * - decodeTransaction() to reconstruct transaction from bytes * - encodeSignedTransaction() and decodeSignedTransaction() for signed transactions * - txId() for calculating transaction ID * * Prerequisites: * - LocalNet running (via `algokit localnet start`) */
import { AlgorandClient } from '@algorandfoundation/algokit-utils';import { Transaction, TransactionType, assignFee, decodeSignedTransaction, decodeTransaction, encodeSignedTransaction, encodeTransaction, encodeTransactionRaw, type PaymentTransactionFields, type SignedTransaction,} from '@algorandfoundation/algokit-utils/transact';import { createAlgodClient, printHeader, printInfo, printStep, printSuccess, shortenAddress,} from '../shared/utils.js';
/** * Gets a funded account from LocalNet's KMD wallet */async function getLocalNetFundedAccount(algorand: AlgorandClient) { return await algorand.account.kmd.getLocalNetDispenserAccount();}
/** * Converts bytes to a hex string for display */function bytesToHex(bytes: Uint8Array, maxLength?: number): string { const hex = Buffer.from(bytes).toString('hex'); if (maxLength && hex.length > maxLength) { return `${hex.slice(0, maxLength)}...`; } return hex;}
/** * Compare two transactions field by field */function compareTransactions(original: Transaction, decoded: Transaction): boolean { // Compare basic fields if (original.type !== decoded.type) return false; if (original.sender.toString() !== decoded.sender.toString()) return false; if (original.firstValid !== decoded.firstValid) return false; if (original.lastValid !== decoded.lastValid) return false; if (original.fee !== decoded.fee) return false; if (original.genesisId !== decoded.genesisId) return false;
// Compare genesis hash if (original.genesisHash && decoded.genesisHash) { if (original.genesisHash.length !== decoded.genesisHash.length) return false; for (let i = 0; i < original.genesisHash.length; i++) { if (original.genesisHash[i] !== decoded.genesisHash[i]) return false; } } else if (original.genesisHash !== decoded.genesisHash) { return false; }
// Compare payment fields if present if (original.payment && decoded.payment) { if (original.payment.receiver.toString() !== decoded.payment.receiver.toString()) return false; if (original.payment.amount !== decoded.payment.amount) return false; } else if (original.payment !== decoded.payment) { return false; }
return true;}
async function main() { printHeader('Encoding/Decoding 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 accounts printStep(2, 'Get Accounts'); const sender = await getLocalNetFundedAccount(algorand); printInfo(`Sender address: ${shortenAddress(sender.addr.toString())}`);
const receiver = algorand.account.random(); printInfo(`Receiver address: ${shortenAddress(receiver.addr.toString())}`);
// Step 3: Create a transaction object printStep(3, 'Create Transaction Object'); const suggestedParams = await algod.suggestedParams();
const paymentFields: PaymentTransactionFields = { receiver: receiver.addr, amount: 1_000_000n, // 1 ALGO };
const transaction = new Transaction({ type: TransactionType.Payment, sender: sender.addr, firstValid: suggestedParams.firstValid, lastValid: suggestedParams.lastValid, genesisHash: suggestedParams.genesisHash, genesisId: suggestedParams.genesisId, payment: paymentFields, });
const txWithFee = assignFee(transaction, { feePerByte: suggestedParams.fee, minFee: suggestedParams.minFee, });
printInfo(`Transaction type: ${txWithFee.type}`); printInfo(`Amount: 1,000,000 microALGO`); printInfo(`Fee: ${txWithFee.fee} microALGO`);
// Step 4: Use encodeTransaction() to get msgpack bytes with TX prefix printStep(4, 'Encode Transaction with TX Prefix (encodeTransaction)');
const encodedWithPrefix = encodeTransaction(txWithFee); printInfo(`Encoded bytes length: ${encodedWithPrefix.length} bytes`); printInfo(`First bytes (hex): ${bytesToHex(encodedWithPrefix, 40)}`); printInfo(''); printInfo('The "TX" prefix (0x5458) is prepended for domain separation.'); printInfo('This prevents the same bytes from being valid in multiple contexts.'); printInfo( `TX in ASCII: "${String.fromCharCode(encodedWithPrefix[0])}${String.fromCharCode(encodedWithPrefix[1])}"`, );
// Step 5: Use encodeTransactionRaw() to get msgpack bytes without prefix printStep(5, 'Encode Transaction Raw (encodeTransactionRaw)');
const encodedRaw = encodeTransactionRaw(txWithFee); printInfo(`Raw encoded bytes length: ${encodedRaw.length} bytes`); printInfo(`First bytes (hex): ${bytesToHex(encodedRaw, 40)}`); printInfo(''); printInfo( `Difference in length: ${encodedWithPrefix.length - encodedRaw.length} bytes (TX prefix)`, ); printInfo('Use encodeTransactionRaw() when the signing tool adds its own prefix.');
// Step 6: Use decodeTransaction() to reconstruct from bytes printStep(6, 'Decode Transaction (decodeTransaction)');
// Decode from bytes with prefix const decodedFromPrefix = decodeTransaction(encodedWithPrefix); printInfo('Decoded from bytes with TX prefix:'); printInfo(` Type: ${decodedFromPrefix.type}`); printInfo(` Sender: ${shortenAddress(decodedFromPrefix.sender.toString())}`); printInfo(` Amount: ${decodedFromPrefix.payment?.amount} microALGO`); printInfo(` Fee: ${decodedFromPrefix.fee} microALGO`);
// Decode from raw bytes (without prefix) const decodedFromRaw = decodeTransaction(encodedRaw); printInfo(''); printInfo('Decoded from raw bytes (without prefix):'); printInfo(` Type: ${decodedFromRaw.type}`); printInfo(` Sender: ${shortenAddress(decodedFromRaw.sender.toString())}`); printInfo(` Amount: ${decodedFromRaw.payment?.amount} microALGO`); printInfo(''); printInfo('Note: decodeTransaction() auto-detects and handles both formats.');
// Step 7: Verify decoded transaction matches original printStep(7, 'Verify Decoded Transaction Matches Original');
const matchesOriginal = compareTransactions(txWithFee, decodedFromPrefix); if (matchesOriginal) { printSuccess('Decoded transaction matches original!'); } else { printInfo('Warning: Decoded transaction differs from original'); }
printInfo(''); printInfo('Field comparison:'); printInfo(` Type: ${txWithFee.type} === ${decodedFromPrefix.type} ✓`); printInfo( ` Sender: ${txWithFee.sender.toString() === decodedFromPrefix.sender.toString() ? '✓' : '✗'}`, ); printInfo( ` Receiver: ${txWithFee.payment?.receiver.toString() === decodedFromPrefix.payment?.receiver.toString() ? '✓' : '✗'}`, ); printInfo( ` Amount: ${txWithFee.payment?.amount === decodedFromPrefix.payment?.amount ? '✓' : '✗'}`, ); printInfo(` Fee: ${txWithFee.fee === decodedFromPrefix.fee ? '✓' : '✗'}`); printInfo(` First valid: ${txWithFee.firstValid === decodedFromPrefix.firstValid ? '✓' : '✗'}`); printInfo(` Last valid: ${txWithFee.lastValid === decodedFromPrefix.lastValid ? '✓' : '✗'}`);
// Step 8: Demonstrate encodeSignedTransaction() and decodeSignedTransaction() printStep(8, 'Encode and Decode Signed Transaction');
// Sign the transaction const signedTxBytes = await sender.signer([txWithFee], [0]); printInfo(`Signed transaction bytes length: ${signedTxBytes[0].length} bytes`);
// Decode the signed transaction const decodedSignedTx = decodeSignedTransaction(signedTxBytes[0]); printInfo(''); printInfo('Decoded SignedTransaction structure:'); printInfo(` txn.type: ${decodedSignedTx.txn.type}`); printInfo(` txn.sender: ${shortenAddress(decodedSignedTx.txn.sender.toString())}`); printInfo(` sig length: ${decodedSignedTx.sig?.length ?? 0} bytes (ed25519 signature)`);
// Re-encode the signed transaction const reEncodedSignedTx = encodeSignedTransaction(decodedSignedTx); printInfo(''); printInfo('Re-encoded signed transaction:'); printInfo(` Length: ${reEncodedSignedTx.length} bytes`);
// Verify re-encoded matches original let signedBytesMatch = reEncodedSignedTx.length === signedTxBytes[0].length; if (signedBytesMatch) { for (let i = 0; i < reEncodedSignedTx.length; i++) { if (reEncodedSignedTx[i] !== signedTxBytes[0][i]) { signedBytesMatch = false; break; } } }
if (signedBytesMatch) { printSuccess('Re-encoded signed transaction matches original!'); } else { printInfo('Re-encoded signed transaction differs (may be due to canonicalization)'); }
// Step 9: Show transaction ID calculation using txId() printStep(9, 'Calculate Transaction ID (txId)');
const txId = txWithFee.txId(); printInfo(`Transaction ID: ${txId}`); printInfo(''); printInfo('Transaction ID calculation:'); printInfo(' 1. Encode transaction with TX prefix'); printInfo(' 2. Hash the bytes using SHA-512/256'); printInfo(' 3. Base32 encode the hash (first 52 characters)'); printInfo(''); printInfo(`ID length: ${txId.length} characters`);
// Verify the decoded transaction has the same ID const decodedTxId = decodedFromPrefix.txId(); if (txId === decodedTxId) { printSuccess('Decoded transaction has same ID as original!'); } else { printInfo('Warning: Transaction IDs differ'); }
// Step 10: Demonstrate round-trip encoding with SignedTransaction structure printStep(10, 'Create and Encode SignedTransaction Manually');
// Create a SignedTransaction structure manually (for demonstration) const manualSignedTx: SignedTransaction = { txn: txWithFee, sig: decodedSignedTx.sig, // Reuse the signature from earlier };
const manualEncodedSignedTx = encodeSignedTransaction(manualSignedTx); printInfo(`Manually created SignedTransaction encoded: ${manualEncodedSignedTx.length} bytes`);
const manualDecodedSignedTx = decodeSignedTransaction(manualEncodedSignedTx); printInfo( `Decoded back: txn.type=${manualDecodedSignedTx.txn.type}, sig present=${!!manualDecodedSignedTx.sig}`, );
// Summary printStep(11, 'Summary'); printInfo(''); printInfo('Encoding functions:'); printInfo(' encodeTransaction(tx) - Returns msgpack bytes WITH "TX" prefix'); printInfo(' encodeTransactionRaw(tx) - Returns msgpack bytes WITHOUT prefix'); printInfo(' encodeSignedTransaction() - Encodes signed transaction for network'); printInfo(''); printInfo('Decoding functions:'); printInfo(' decodeTransaction(bytes) - Decodes bytes to Transaction'); printInfo(' (auto-detects prefix)'); printInfo(' decodeSignedTransaction(bytes) - Decodes bytes to SignedTransaction'); printInfo(''); printInfo('Other utilities:'); printInfo(' tx.txId() - Calculate transaction ID (hash of encoded bytes)'); printInfo(''); printInfo('Use cases:'); printInfo(' - Serialize transactions for storage or transmission'); printInfo(' - Deserialize transactions received from external sources'); printInfo(' - Calculate transaction IDs for tracking and verification'); printInfo(' - Inspect signed transactions to verify signature presence');
printSuccess('Encoding/Decoding example completed!');}
main().catch(error => { console.error('Error:', error); process.exit(1);});