Skip to content
Algorand Developer Portal

ABI Dynamic Array Type

← Back to ABI Encoding

This example demonstrates how to encode and decode variable-length arrays using ABIArrayDynamicType:

  • uint64[]: Dynamic array of unsigned 64-bit integers
  • string[]: Dynamic array of strings (nested dynamic types)
  • address[]: Dynamic array of Algorand addresses Key characteristics of dynamic arrays:
  • Variable length determined at runtime
  • 2-byte (uint16) length prefix indicating number of elements
  • For static element types: elements encoded consecutively after length
  • For dynamic element types: head/tail encoding pattern is used Head/Tail encoding (for arrays containing dynamic elements):
  • Length prefix: 2 bytes indicating number of elements
  • Head section: Contains offsets (2 bytes each) pointing to where each element starts in tail
  • Tail section: Contains the actual encoded elements
  • No LocalNet required

From the repository root:

Terminal window
cd examples
npm run example abi/06-dynamic-array.ts

View source on GitHub

06-dynamic-array.ts
/**
* Example: ABI Dynamic Array Type
*
* This example demonstrates how to encode and decode variable-length arrays using ABIArrayDynamicType:
* - uint64[]: Dynamic array of unsigned 64-bit integers
* - string[]: Dynamic array of strings (nested dynamic types)
* - address[]: Dynamic array of Algorand addresses
*
* Key characteristics of dynamic arrays:
* - Variable length determined at runtime
* - 2-byte (uint16) length prefix indicating number of elements
* - For static element types: elements encoded consecutively after length
* - For dynamic element types: head/tail encoding pattern is used
*
* Head/Tail encoding (for arrays containing dynamic elements):
* - Length prefix: 2 bytes indicating number of elements
* - Head section: Contains offsets (2 bytes each) pointing to where each element starts in tail
* - Tail section: Contains the actual encoded elements
*
* Prerequisites:
* - No LocalNet required
*/
import { Address } from '@algorandfoundation/algokit-utils';
import { ABIArrayDynamicType, ABIType, ABIUintType } from '@algorandfoundation/algokit-utils/abi';
import {
formatBytes,
formatHex,
printHeader,
printInfo,
printStep,
printSuccess,
} from '../shared/utils.js';
function main() {
printHeader('ABI Dynamic Array Type Example');
// Step 1: ABIArrayDynamicType properties
printStep(1, 'ABIArrayDynamicType Properties');
const uint64ArrayType = ABIType.from('uint64[]') as ABIArrayDynamicType;
const stringArrayType = ABIType.from('string[]') as ABIArrayDynamicType;
const addressArrayType = ABIType.from('address[]') as ABIArrayDynamicType;
printInfo('uint64[]:');
printInfo(` toString(): ${uint64ArrayType.toString()}`);
printInfo(` childType: ${uint64ArrayType.childType.toString()}`);
printInfo(` isDynamic(): ${uint64ArrayType.isDynamic()}`);
printInfo(` childType.isDynamic(): ${uint64ArrayType.childType.isDynamic()}`);
printInfo('\nstring[]:');
printInfo(` toString(): ${stringArrayType.toString()}`);
printInfo(` childType: ${stringArrayType.childType.toString()}`);
printInfo(` isDynamic(): ${stringArrayType.isDynamic()}`);
printInfo(` childType.isDynamic(): ${stringArrayType.childType.isDynamic()}`);
printInfo('\naddress[]:');
printInfo(` toString(): ${addressArrayType.toString()}`);
printInfo(` childType: ${addressArrayType.childType.toString()}`);
printInfo(` isDynamic(): ${addressArrayType.isDynamic()}`);
printInfo(` childType.isDynamic(): ${addressArrayType.childType.isDynamic()}`);
// Step 2: uint64[] encoding with static elements
printStep(2, 'uint64[] Encoding - Static Element Type');
const uint64Values = [1000n, 2000n, 3000n];
const uint64Encoded = uint64ArrayType.encode(uint64Values);
const uint64Decoded = uint64ArrayType.decode(uint64Encoded) as bigint[];
printInfo(`Input: [${uint64Values.join(', ')}]`);
printInfo(`Encoded: ${formatHex(uint64Encoded)}`);
printInfo(`Total bytes: ${uint64Encoded.length}`);
// Break down the encoding
const uint64LengthPrefix = uint64Encoded.slice(0, 2);
const uint64ElementData = uint64Encoded.slice(2);
printInfo('\nByte layout:');
printInfo(
` [0-1] Length prefix: ${formatHex(uint64LengthPrefix)} = ${(uint64LengthPrefix[0] << 8) | uint64LengthPrefix[1]} elements`,
);
printInfo(` [2-25] Element data: ${formatHex(uint64ElementData)}`);
// Show individual elements
printInfo('\nElement breakdown (8 bytes each):');
for (let i = 0; i < uint64Values.length; i++) {
const start = 2 + i * 8;
const elementBytes = uint64Encoded.slice(start, start + 8);
printInfo(
` [${start}-${start + 7}] Element ${i}: ${formatHex(elementBytes)} = ${uint64Values[i]}`,
);
}
printInfo(`\nDecoded: [${uint64Decoded.join(', ')}]`);
printInfo(`Round-trip verified: ${uint64Decoded.every((v, i) => v === uint64Values[i])}`);
// Step 3: Demonstrate 2-byte length prefix
printStep(3, 'Length Prefix - 2 Bytes (uint16 Big-Endian)');
printInfo('The length prefix encodes the NUMBER of elements (not byte size)');
printInfo('Format: uint16 big-endian (high byte first)');
// Show different array lengths
const testLengths = [0, 1, 3, 256, 1000];
for (const len of testLengths) {
const testArray = Array.from({ length: len }, (_, i) => BigInt(i));
const encoded = uint64ArrayType.encode(testArray);
const prefix = encoded.slice(0, 2);
const decodedLength = (prefix[0] << 8) | prefix[1];
printInfo(
` ${len} elements: prefix = ${formatHex(prefix)} = (${prefix[0]} << 8) | ${prefix[1]} = ${decodedLength}`,
);
}
// Step 4: string[] encoding - demonstrates head/tail encoding
printStep(4, 'string[] Encoding - Head/Tail Pattern');
const stringValues = ['Hello', 'World', 'ABI'];
const stringEncoded = stringArrayType.encode(stringValues);
const stringDecoded = stringArrayType.decode(stringEncoded) as string[];
printInfo(`Input: [${stringValues.map(s => `"${s}"`).join(', ')}]`);
printInfo(`Encoded: ${formatHex(stringEncoded)}`);
printInfo(`Total bytes: ${stringEncoded.length}`);
// Break down the encoding
const stringLengthPrefix = stringEncoded.slice(0, 2);
const numElements = (stringLengthPrefix[0] << 8) | stringLengthPrefix[1];
printInfo('\nByte layout with head/tail encoding:');
printInfo(` [0-1] Length prefix: ${formatHex(stringLengthPrefix)} = ${numElements} elements`);
// Head section: contains offsets for each element
// Each offset is 2 bytes, offsets are relative to start of array data (after length prefix)
printInfo('\n HEAD SECTION (offsets to each element):');
const headStart = 2;
const headSize = numElements * 2; // 2 bytes per offset
for (let i = 0; i < numElements; i++) {
const offsetPos = headStart + i * 2;
const offsetBytes = stringEncoded.slice(offsetPos, offsetPos + 2);
const offset = (offsetBytes[0] << 8) | offsetBytes[1];
printInfo(
` [${offsetPos}-${offsetPos + 1}] Offset ${i}: ${formatHex(offsetBytes)} = ${offset} (points to byte ${headStart + offset})`,
);
}
// Tail section: contains actual string data
printInfo('\n TAIL SECTION (actual string data):');
const tailStart = headStart + headSize;
let currentPos = tailStart;
for (let i = 0; i < numElements; i++) {
// Read string length prefix (2 bytes)
const strLenBytes = stringEncoded.slice(currentPos, currentPos + 2);
const strLen = (strLenBytes[0] << 8) | strLenBytes[1];
// Read string content
const strContent = stringEncoded.slice(currentPos + 2, currentPos + 2 + strLen);
const strEnd = currentPos + 2 + strLen - 1;
printInfo(` [${currentPos}-${strEnd}] String ${i}: "${stringValues[i]}"`);
printInfo(` Length prefix: ${formatHex(strLenBytes)} = ${strLen} bytes`);
printInfo(` Content: ${formatHex(strContent)}`);
currentPos += 2 + strLen;
}
printInfo(`\nDecoded: [${stringDecoded.map(s => `"${s}"`).join(', ')}]`);
printInfo(`Round-trip verified: ${stringDecoded.every((v, i) => v === stringValues[i])}`);
// Step 5: Compare encoding of arrays with different lengths
printStep(5, 'Dynamic Sizing - Arrays of Different Lengths');
const arrayLengths = [0, 1, 3, 5];
printInfo('uint64[] arrays of different lengths:');
for (const len of arrayLengths) {
const arr = Array.from({ length: len }, (_, i) => BigInt(i + 1));
const encoded = uint64ArrayType.encode(arr);
const expectedBytes = 2 + len * 8; // 2 byte prefix + 8 bytes per element
printInfo(
` ${len} elements: ${encoded.length} bytes (expected: 2 + ${len}*8 = ${expectedBytes})`,
);
}
printInfo('\nstring[] arrays of different lengths:');
const strArrays = [[], ['A'], ['Hello', 'World'], ['One', 'Two', 'Three']];
for (const arr of strArrays) {
const encoded = stringArrayType.encode(arr);
// For string[], bytes = 2 (array length) + 2*n (offsets) + sum of (2 + strlen) for each string
const offsetsSize = arr.length * 2;
const stringsSize = arr.reduce((sum, s) => sum + 2 + new TextEncoder().encode(s).length, 0);
const expectedBytes = 2 + offsetsSize + stringsSize;
printInfo(
` ${arr.length} strings ${JSON.stringify(arr)}: ${encoded.length} bytes (expected: ${expectedBytes})`,
);
}
// Step 6: address[] encoding - static element type
printStep(6, 'address[] Encoding - Static Element Type');
// Create sample addresses
const pubKey1 = new Uint8Array(32).fill(0x11);
const pubKey2 = new Uint8Array(32).fill(0x22);
const addr1 = new Address(pubKey1).toString();
const addr2 = new Address(pubKey2).toString();
const addressValues = [addr1, addr2];
const addressEncoded = addressArrayType.encode(addressValues);
const addressDecoded = addressArrayType.decode(addressEncoded) as string[];
printInfo(`Input: ${addressValues.length} addresses`);
printInfo(` [0]: ${addr1}`);
printInfo(` [1]: ${addr2}`);
printInfo(`Encoded: ${formatBytes(addressEncoded, 16)}`);
printInfo(`Total bytes: ${addressEncoded.length}`);
// Break down encoding
const addrLengthPrefix = addressEncoded.slice(0, 2);
printInfo('\nByte layout:');
printInfo(
` [0-1] Length prefix: ${formatHex(addrLengthPrefix)} = ${(addrLengthPrefix[0] << 8) | addrLengthPrefix[1]} elements`,
);
printInfo(` [2-33] Address 0: 32 bytes`);
printInfo(` [34-65] Address 1: 32 bytes`);
printInfo(` Expected: 2 + 2*32 = ${2 + 2 * 32} bytes`);
printInfo('\nDecoded:');
printInfo(` [0]: ${addressDecoded[0]}`);
printInfo(` [1]: ${addressDecoded[1]}`);
printInfo(`Round-trip verified: ${addressDecoded.every((v, i) => v === addressValues[i])}`);
// Step 7: Creating ABIArrayDynamicType programmatically
printStep(7, 'Creating ABIArrayDynamicType Programmatically');
const customArrayType = new ABIArrayDynamicType(new ABIUintType(32));
printInfo('Created with: new ABIArrayDynamicType(new ABIUintType(32))');
printInfo(` toString(): ${customArrayType.toString()}`);
printInfo(` childType: ${customArrayType.childType.toString()}`);
printInfo(` isDynamic(): ${customArrayType.isDynamic()}`);
const customValues = [100, 200, 300, 400];
const customEncoded = customArrayType.encode(customValues);
const customDecoded = customArrayType.decode(customEncoded) as bigint[];
printInfo(`\nEncode [${customValues.join(', ')}]:`);
printInfo(` Encoded: ${formatHex(customEncoded)}`);
printInfo(` Total bytes: ${customEncoded.length} (2 prefix + 4*4 elements)`);
printInfo(` Decoded: [${customDecoded.join(', ')}]`);
printInfo(
` Round-trip verified: ${customDecoded.every((v, i) => BigInt(v) === BigInt(customValues[i]))}`,
);
// Step 8: Empty arrays
printStep(8, 'Empty Dynamic Arrays');
const emptyUint64 = uint64ArrayType.encode([]);
const emptyString = stringArrayType.encode([]);
const emptyAddress = addressArrayType.encode([]);
printInfo('Empty arrays encode to just the length prefix (0):');
printInfo(` uint64[]: ${formatHex(emptyUint64)} (${emptyUint64.length} bytes)`);
printInfo(` string[]: ${formatHex(emptyString)} (${emptyString.length} bytes)`);
printInfo(` address[]: ${formatHex(emptyAddress)} (${emptyAddress.length} bytes)`);
// Verify decoding
const decodedEmptyUint64 = uint64ArrayType.decode(emptyUint64) as bigint[];
const decodedEmptyString = stringArrayType.decode(emptyString) as string[];
printInfo(`\nDecoding empty arrays:`);
printInfo(` uint64[]: length = ${decodedEmptyUint64.length}`);
printInfo(` string[]: length = ${decodedEmptyString.length}`);
// Step 9: Summary
printStep(9, 'Summary');
printInfo('ABIArrayDynamicType key properties:');
printInfo(' - childType: The type of each element');
printInfo(' - isDynamic(): Always returns true');
printInfo(' - No "length" property (unlike ABIArrayStaticType)');
printInfo('\nDynamic array encoding format:');
printInfo(' - 2-byte length prefix (number of elements, big-endian)');
printInfo(' - For static child types: elements encoded consecutively');
printInfo(' - For dynamic child types: head/tail encoding');
printInfo('\nHead/Tail encoding (for dynamic elements like string[]):');
printInfo(' - Head: array of 2-byte offsets (one per element)');
printInfo(' - Tail: actual encoded elements');
printInfo(' - Offsets are relative to start of data (after length prefix)');
printInfo('\nEncoded size:');
printInfo(' - Static elements: 2 + (elementSize * numElements)');
printInfo(' - Dynamic elements: 2 + (2 * numElements) + sum(elementSizes)');
printInfo('\nCreating dynamic array types:');
printInfo(' - ABIType.from("uint64[]") - parse from string');
printInfo(' - new ABIArrayDynamicType(childType) - programmatic');
printSuccess('ABI Dynamic Array Type example completed successfully!');
}
main();