ABI Complex Nested Types
Description
Section titled “Description”This example demonstrates how to work with deeply nested ABI types combining arrays, tuples, and structs:
- Array of tuples: (uint64,string)[]
- Tuple containing arrays: (uint64[],string[])
- Nested structs with arrays
- Deeply nested type: ((uint64,bool)[],string,(address,uint256))[] Key concepts:
- Dynamic types (strings, dynamic arrays) always use head/tail encoding
- Offsets in head section point to data positions in tail section
- Nesting depth affects encoding complexity but follows consistent rules
- Round-trip encoding/decoding preserves all nested values
Prerequisites
Section titled “Prerequisites”- No LocalNet required
Run This Example
Section titled “Run This Example”From the repository root:
cd examplesnpm run example abi/14-complex-nested.ts/** * Example: ABI Complex Nested Types * * This example demonstrates how to work with deeply nested ABI types combining * arrays, tuples, and structs: * - Array of tuples: (uint64,string)[] * - Tuple containing arrays: (uint64[],string[]) * - Nested structs with arrays * - Deeply nested type: ((uint64,bool)[],string,(address,uint256))[] * * Key concepts: * - Dynamic types (strings, dynamic arrays) always use head/tail encoding * - Offsets in head section point to data positions in tail section * - Nesting depth affects encoding complexity but follows consistent rules * - Round-trip encoding/decoding preserves all nested values * * Prerequisites: * - No LocalNet required */
import { Address } from '@algorandfoundation/algokit-utils'import { ABIArrayDynamicType, ABIStructType, ABITupleType, ABIType,} from '@algorandfoundation/algokit-utils/abi'import { formatBytes, formatHex, printHeader, printInfo, printStep, printSuccess } from '../shared/utils.js'
function main() { printHeader('ABI Complex Nested Types Example')
// Step 1: Array of tuples - (uint64,string)[] printStep(1, 'Array of Tuples - (uint64,string)[]')
const arrayOfTuplesType = ABIType.from('(uint64,string)[]') as ABIArrayDynamicType
printInfo('Type: (uint64,string)[]') printInfo(` toString(): ${arrayOfTuplesType.toString()}`) printInfo(` isDynamic(): ${arrayOfTuplesType.isDynamic()}`) printInfo(` childType: ${arrayOfTuplesType.childType.toString()}`) printInfo(` childType.isDynamic(): ${arrayOfTuplesType.childType.isDynamic()} (because string is dynamic)`)
const tupleArrayValue: [bigint, string][] = [ [100n, 'First'], [200n, 'Second'], [300n, 'Third'], ]
const tupleArrayEncoded = arrayOfTuplesType.encode(tupleArrayValue)
printInfo(`\nInput: [[100n, "First"], [200n, "Second"], [300n, "Third"]]`) printInfo(`Encoded: ${formatHex(tupleArrayEncoded)}`) printInfo(`Total bytes: ${tupleArrayEncoded.length}`)
printInfo('\nByte layout (dynamic array of dynamic tuples):') printInfo(' [0-1] Array length prefix')
const numTuples = (tupleArrayEncoded[0] << 8) | tupleArrayEncoded[1] printInfo(` ${formatHex(tupleArrayEncoded.slice(0, 2))} = ${numTuples} elements`)
printInfo('\n HEAD SECTION (offsets to each tuple):') for (let i = 0; i < numTuples; i++) { const offsetPos = 2 + i * 2 const offset = (tupleArrayEncoded[offsetPos] << 8) | tupleArrayEncoded[offsetPos + 1] printInfo(` [${offsetPos}-${offsetPos + 1}] Tuple ${i} offset: ${formatHex(tupleArrayEncoded.slice(offsetPos, offsetPos + 2))} = ${offset}`) }
printInfo('\n TAIL SECTION (tuple data):') const headEnd = 2 + numTuples * 2 printInfo(` Starts at byte ${headEnd}`) printInfo(` Each tuple has: [uint64:8 bytes][string_offset:2 bytes][string_len:2][string_data:N]`)
// Decode and verify const tupleArrayDecoded = arrayOfTuplesType.decode(tupleArrayEncoded) as [bigint, string][]
printInfo('\nDecoded:') tupleArrayDecoded.forEach((tuple, i) => { printInfo(` [${i}]: [${tuple[0]}, "${tuple[1]}"]`) })
const tupleArrayMatch = tupleArrayDecoded.every( (tuple, i) => tuple[0] === tupleArrayValue[i][0] && tuple[1] === tupleArrayValue[i][1] ) printInfo(`Round-trip verified: ${tupleArrayMatch}`)
// Step 2: Tuple containing arrays - (uint64[],string[]) printStep(2, 'Tuple Containing Arrays - (uint64[],string[])')
const tupleWithArraysType = ABIType.from('(uint64[],string[])') as ABITupleType
printInfo('Type: (uint64[],string[])') printInfo(` toString(): ${tupleWithArraysType.toString()}`) printInfo(` isDynamic(): ${tupleWithArraysType.isDynamic()}`) printInfo(` childTypes: ${tupleWithArraysType.childTypes.length} elements`) tupleWithArraysType.childTypes.forEach((child, i) => { printInfo(` [${i}]: ${child.toString()} (isDynamic: ${child.isDynamic()})`) })
const tupleWithArraysValue: [bigint[], string[]] = [ [10n, 20n, 30n], ['Apple', 'Banana', 'Cherry'], ]
const tupleWithArraysEncoded = tupleWithArraysType.encode(tupleWithArraysValue)
printInfo(`\nInput: [[10n, 20n, 30n], ["Apple", "Banana", "Cherry"]]`) printInfo(`Encoded: ${formatBytes(tupleWithArraysEncoded, 16)}`) printInfo(`Total bytes: ${tupleWithArraysEncoded.length}`)
printInfo('\nByte layout (tuple with 2 dynamic children):') printInfo(' HEAD SECTION (2 offsets):')
const arr1Offset = (tupleWithArraysEncoded[0] << 8) | tupleWithArraysEncoded[1] const arr2Offset = (tupleWithArraysEncoded[2] << 8) | tupleWithArraysEncoded[3]
printInfo(` [0-1] uint64[] offset: ${formatHex(tupleWithArraysEncoded.slice(0, 2))} = ${arr1Offset}`) printInfo(` [2-3] string[] offset: ${formatHex(tupleWithArraysEncoded.slice(2, 4))} = ${arr2Offset}`)
printInfo('\n TAIL SECTION:') printInfo(` uint64[] at offset ${arr1Offset}: [len:2][elem1:8][elem2:8][elem3:8]`) printInfo(` string[] at offset ${arr2Offset}: [len:2][offsets...][string data...]`)
// Decode and verify const tupleWithArraysDecoded = tupleWithArraysType.decode(tupleWithArraysEncoded) as [bigint[], string[]]
printInfo('\nDecoded:') printInfo(` uint64[]: [${tupleWithArraysDecoded[0].join(', ')}]`) printInfo(` string[]: [${tupleWithArraysDecoded[1].map((s) => `"${s}"`).join(', ')}]`)
const tupleWithArraysMatch = tupleWithArraysDecoded[0].every((v, i) => v === tupleWithArraysValue[0][i]) && tupleWithArraysDecoded[1].every((v, i) => v === tupleWithArraysValue[1][i]) printInfo(`Round-trip verified: ${tupleWithArraysMatch}`)
// Step 3: Nested structs with arrays printStep(3, 'Nested Structs with Arrays')
// Create struct definitions const orderStruct = ABIStructType.fromStruct('Order', { Order: [ { name: 'orderId', type: 'uint64' }, { name: 'items', type: 'string[]' }, { name: 'quantities', type: 'uint32[]' }, ], })
printInfo('Order struct: { orderId: uint64, items: string[], quantities: uint32[] }') printInfo(` ABI type: ${orderStruct.name}`) printInfo(` isDynamic(): ${orderStruct.isDynamic()}`)
const orderValue = { orderId: 12345n, items: ['Widget', 'Gadget', 'Gizmo'], quantities: [2, 5, 1], }
const orderEncoded = orderStruct.encode(orderValue)
printInfo(`\nInput: { orderId: 12345, items: ["Widget", "Gadget", "Gizmo"], quantities: [2, 5, 1] }`) printInfo(`Encoded: ${formatBytes(orderEncoded, 20)}`) printInfo(`Total bytes: ${orderEncoded.length}`)
printInfo('\nByte layout (struct with static + dynamic fields):') printInfo(' HEAD SECTION:') printInfo(` [0-7] orderId (uint64): ${formatHex(orderEncoded.slice(0, 8))} = ${orderValue.orderId}`)
const itemsOffset = (orderEncoded[8] << 8) | orderEncoded[9] const quantitiesOffset = (orderEncoded[10] << 8) | orderEncoded[11]
printInfo(` [8-9] items offset: ${formatHex(orderEncoded.slice(8, 10))} = ${itemsOffset}`) printInfo(` [10-11] quantities offset: ${formatHex(orderEncoded.slice(10, 12))} = ${quantitiesOffset}`)
printInfo('\n TAIL SECTION:') printInfo(` items (string[]) at offset ${itemsOffset}`) printInfo(` quantities (uint32[]) at offset ${quantitiesOffset}`)
// Decode and verify const orderDecoded = orderStruct.decode(orderEncoded) as { orderId: bigint items: string[] quantities: number[] }
printInfo('\nDecoded:') printInfo(` orderId: ${orderDecoded.orderId}`) printInfo(` items: [${orderDecoded.items.map((s) => `"${s}"`).join(', ')}]`) printInfo(` quantities: [${orderDecoded.quantities.join(', ')}]`)
const orderMatch = orderDecoded.orderId === orderValue.orderId && orderDecoded.items.every((v, i) => v === orderValue.items[i]) && orderDecoded.quantities.every((v, i) => Number(v) === orderValue.quantities[i]) printInfo(`Round-trip verified: ${orderMatch}`)
// Step 4: Deeply nested type - ((uint64,bool)[],string,(address,uint256))[] printStep(4, 'Deeply Nested Type - ((uint64,bool)[],string,(address,uint256))[]')
const deeplyNestedType = ABIType.from('((uint64,bool)[],string,(address,uint256))[]') as ABIArrayDynamicType
printInfo('Type: ((uint64,bool)[],string,(address,uint256))[]') printInfo(` toString(): ${deeplyNestedType.toString()}`) printInfo(` isDynamic(): ${deeplyNestedType.isDynamic()}`)
const innerTupleType = deeplyNestedType.childType as ABITupleType printInfo(`\n Child tuple type: ${innerTupleType.toString()}`) printInfo(` Child tuple elements:`) innerTupleType.childTypes.forEach((child, i) => { printInfo(` [${i}]: ${child.toString()} (isDynamic: ${child.isDynamic()})`) })
// Create sample addresses const pubKey1 = new Uint8Array(32).fill(0xaa) const pubKey2 = new Uint8Array(32).fill(0xbb) const addr1 = new Address(pubKey1).toString() const addr2 = new Address(pubKey2).toString()
// Create deeply nested value type DeepNestedTuple = [[bigint, boolean][], string, [string, bigint]] const deeplyNestedValue: DeepNestedTuple[] = [ [ [[1n, true], [2n, false]], // (uint64,bool)[] 'First Entry', // string [addr1, 1000000000000000000n], // (address,uint256) ], [ [[10n, false], [20n, true], [30n, true]], // (uint64,bool)[] 'Second Entry', // string [addr2, 2000000000000000000n], // (address,uint256) ], ]
const deeplyNestedEncoded = deeplyNestedType.encode(deeplyNestedValue)
printInfo('\nInput:') printInfo(' [') printInfo(' [[[1n, true], [2n, false]], "First Entry", [addr1, 1e18n]],') printInfo(' [[[10n, false], [20n, true], [30n, true]], "Second Entry", [addr2, 2e18n]]') printInfo(' ]') printInfo(`\nEncoded: ${formatBytes(deeplyNestedEncoded, 24)}`) printInfo(`Total bytes: ${deeplyNestedEncoded.length}`)
printInfo('\nEncoding structure (simplified):') printInfo(' OUTER ARRAY:') printInfo(' [0-1] Array length prefix (2 elements)') printInfo(' [2-3] Offset to element 0') printInfo(' [4-5] Offset to element 1') printInfo(' [6+] Element data (each element is a complex tuple)')
printInfo('\n EACH INNER TUPLE ((uint64,bool)[],string,(address,uint256)):') printInfo(' HEAD: [arr_offset:2][string_offset:2][addr:32][uint256:32]') printInfo(' TAIL: [(uint64,bool)[] data][string data]')
printInfo('\n INNERMOST (uint64,bool)[]:') printInfo(' [len:2][elem0:9][elem1:9]... (each tuple is 8+1=9 bytes)')
// Decode and verify const deeplyNestedDecoded = deeplyNestedType.decode(deeplyNestedEncoded) as DeepNestedTuple[]
printInfo('\nDecoded:') deeplyNestedDecoded.forEach((entry, i) => { printInfo(` Entry ${i}:`) printInfo(` (uint64,bool)[]: [${entry[0].map((t) => `[${t[0]}, ${t[1]}]`).join(', ')}]`) printInfo(` string: "${entry[1]}"`) printInfo(` (address,uint256): [${entry[2][0].substring(0, 10)}..., ${entry[2][1]}]`) })
// Verify round-trip let deepMatch = deeplyNestedDecoded.length === deeplyNestedValue.length for (let i = 0; i < deeplyNestedValue.length && deepMatch; i++) { const orig = deeplyNestedValue[i] const dec = deeplyNestedDecoded[i] // Check (uint64,bool)[] deepMatch = deepMatch && orig[0].length === dec[0].length for (let j = 0; j < orig[0].length && deepMatch; j++) { deepMatch = deepMatch && orig[0][j][0] === dec[0][j][0] && orig[0][j][1] === dec[0][j][1] } // Check string deepMatch = deepMatch && orig[1] === dec[1] // Check (address,uint256) deepMatch = deepMatch && orig[2][0] === dec[2][0] && orig[2][1] === dec[2][1] } printInfo(`Round-trip verified: ${deepMatch}`)
// Step 5: Encoding size analysis printStep(5, 'Encoding Size Analysis')
printInfo('Comparing encoding sizes for different nesting levels:')
// Simple static tuple const simpleType = ABIType.from('(uint64,bool)') const simpleEncoded = simpleType.encode([1n, true]) printInfo(`\n (uint64,bool) = [1n, true]:`) printInfo(` Size: ${simpleEncoded.length} bytes (8 + 1 = 9, all static)`)
// Array of static tuples const staticArrayType = ABIType.from('(uint64,bool)[]') const staticArrayEncoded = staticArrayType.encode([[1n, true], [2n, false]]) printInfo(`\n (uint64,bool)[] = [[1n, true], [2n, false]]:`) printInfo(` Size: ${staticArrayEncoded.length} bytes (2 length + 2*9 elements = 20)`)
// Tuple with dynamic element const dynamicTupleType = ABIType.from('(uint64,string)') const dynamicTupleEncoded = dynamicTupleType.encode([1n, 'Hello']) printInfo(`\n (uint64,string) = [1n, "Hello"]:`) printInfo(` Size: ${dynamicTupleEncoded.length} bytes (8 + 2 offset + 2 len + 5 content = 17)`)
// Array of tuples with dynamic element const dynamicArrayType = ABIType.from('(uint64,string)[]') const dynamicArrayEncoded = dynamicArrayType.encode([[1n, 'Hi'], [2n, 'Bye']]) printInfo(`\n (uint64,string)[] = [[1n, "Hi"], [2n, "Bye"]]:`) printInfo(` Size: ${dynamicArrayEncoded.length} bytes`) printInfo(` Breakdown: 2 array_len + 2*2 offsets + 2*(8+2+2+N) per tuple`)
// Deep nesting size printInfo(`\n ((uint64,bool)[],string,(address,uint256))[] with 2 complex elements:`) printInfo(` Size: ${deeplyNestedEncoded.length} bytes`) printInfo(` Each element has: array of tuples + string + (address,uint256) tuple`)
// Step 6: Static vs dynamic arrays inside tuples printStep(6, 'Static vs Dynamic Arrays Inside Tuples')
// Tuple with static array const tupleWithStaticArrayType = ABIType.from('(uint64[3],bool)') const tupleWithStaticArrayValue: [bigint[], boolean] = [[1n, 2n, 3n], true] const tupleWithStaticArrayEncoded = tupleWithStaticArrayType.encode(tupleWithStaticArrayValue)
printInfo('Tuple with static array: (uint64[3],bool)') printInfo(` Input: [[1n, 2n, 3n], true]`) printInfo(` Encoded: ${formatHex(tupleWithStaticArrayEncoded)}`) printInfo(` Size: ${tupleWithStaticArrayEncoded.length} bytes (24 + 1 = 25, no offsets needed)`) printInfo(` isDynamic(): ${tupleWithStaticArrayType.isDynamic()} (static array doesnt make tuple dynamic)`)
// Tuple with dynamic array const tupleWithDynamicArrayType = ABIType.from('(uint64[],bool)') const tupleWithDynamicArrayValue: [bigint[], boolean] = [[1n, 2n, 3n], true] const tupleWithDynamicArrayEncoded = tupleWithDynamicArrayType.encode(tupleWithDynamicArrayValue)
printInfo('\nTuple with dynamic array: (uint64[],bool)') printInfo(` Input: [[1n, 2n, 3n], true]`) printInfo(` Encoded: ${formatHex(tupleWithDynamicArrayEncoded)}`) printInfo(` Size: ${tupleWithDynamicArrayEncoded.length} bytes (2 offset + 1 bool + 2 len + 24 data = 29)`) printInfo(` isDynamic(): ${tupleWithDynamicArrayType.isDynamic()}`)
printInfo('\nByte layout comparison:') printInfo(' Static (uint64[3],bool):') printInfo(` [0-7] uint64[0]: ${formatHex(tupleWithStaticArrayEncoded.slice(0, 8))}`) printInfo(` [8-15] uint64[1]: ${formatHex(tupleWithStaticArrayEncoded.slice(8, 16))}`) printInfo(` [16-23] uint64[2]: ${formatHex(tupleWithStaticArrayEncoded.slice(16, 24))}`) printInfo(` [24] bool: ${formatHex(tupleWithStaticArrayEncoded.slice(24, 25))}`)
printInfo('\n Dynamic (uint64[],bool):') const dynArrayOffset = (tupleWithDynamicArrayEncoded[0] << 8) | tupleWithDynamicArrayEncoded[1] printInfo(` [0-1] array offset: ${formatHex(tupleWithDynamicArrayEncoded.slice(0, 2))} = ${dynArrayOffset}`) printInfo(` [2] bool: ${formatHex(tupleWithDynamicArrayEncoded.slice(2, 3))}`) printInfo(` [3-4] array length: ${formatHex(tupleWithDynamicArrayEncoded.slice(3, 5))}`) printInfo(` [5-28] array data: ${formatHex(tupleWithDynamicArrayEncoded.slice(5))}`)
// Decode and verify both const staticArrayDecoded = tupleWithStaticArrayType.decode(tupleWithStaticArrayEncoded) as [bigint[], boolean] const dynamicArrayDecoded = tupleWithDynamicArrayType.decode(tupleWithDynamicArrayEncoded) as [bigint[], boolean]
printInfo('\nRound-trip verification:') printInfo(` Static array tuple: ${staticArrayDecoded[0].every((v, i) => v === tupleWithStaticArrayValue[0][i]) && staticArrayDecoded[1] === tupleWithStaticArrayValue[1]}`) printInfo(` Dynamic array tuple: ${dynamicArrayDecoded[0].every((v, i) => v === tupleWithDynamicArrayValue[0][i]) && dynamicArrayDecoded[1] === tupleWithDynamicArrayValue[1]}`)
// Step 7: Triple nesting verification printStep(7, 'Triple Nesting Verification')
// Type: ((uint64,bool)[])[] is an array of single-element tuples where each tuple contains (uint64,bool)[] // This means: outer array -> 1-element tuple -> dynamic array of (uint64,bool) tuples const tripleNestedType = ABIType.from('((uint64,bool)[])[]') as ABIArrayDynamicType
printInfo('Type: ((uint64,bool)[])[]') printInfo(' This is: array of 1-element tuples, where each tuple contains (uint64,bool)[]') printInfo(` toString(): ${tripleNestedType.toString()}`) printInfo(` isDynamic(): ${tripleNestedType.isDynamic()}`)
const tripleInnerTupleType = tripleNestedType.childType as ABITupleType printInfo(`\n childType: ${tripleInnerTupleType.toString()} (a 1-element tuple)`) printInfo(` childTypes[0]: ${tripleInnerTupleType.childTypes[0].toString()}`)
// Each element is a 1-element tuple containing an array of (uint64,bool) tuples type TripleNestedElement = [[bigint, boolean][]] const tripleNestedValue: TripleNestedElement[] = [ [[[1n, true], [2n, false]]], // 1-element tuple containing [(1,true), (2,false)] [[[10n, false]]], // 1-element tuple containing [(10,false)] [[[100n, true], [200n, true], [300n, false]]], // 1-element tuple containing 3 tuples ]
const tripleNestedEncoded = tripleNestedType.encode(tripleNestedValue)
printInfo('\nInput (each element is a tuple containing an array):') printInfo(' [') printInfo(' [[ [1n, true], [2n, false] ]], // tuple wrapping array of 2 tuples') printInfo(' [[ [10n, false] ]], // tuple wrapping array of 1 tuple') printInfo(' [[ [100n, true], [200n, true], [300n, false] ]] // tuple wrapping array of 3') printInfo(' ]') printInfo(`\nEncoded: ${formatBytes(tripleNestedEncoded, 20)}`) printInfo(`Total bytes: ${tripleNestedEncoded.length}`)
// Decode and verify const tripleNestedDecoded = tripleNestedType.decode(tripleNestedEncoded) as TripleNestedElement[]
printInfo('\nDecoded:') tripleNestedDecoded.forEach((outer, i) => { const innerArray = outer[0] // First (only) element of the tuple printInfo(` [${i}]: [[ ${innerArray.map((t) => `[${t[0]}, ${t[1]}]`).join(', ')} ]]`) })
let tripleMatch = tripleNestedDecoded.length === tripleNestedValue.length for (let i = 0; i < tripleNestedValue.length && tripleMatch; i++) { const origInner = tripleNestedValue[i][0] const decInner = tripleNestedDecoded[i][0] tripleMatch = tripleMatch && origInner.length === decInner.length for (let j = 0; j < origInner.length && tripleMatch; j++) { tripleMatch = tripleMatch && origInner[j][0] === decInner[j][0] && origInner[j][1] === decInner[j][1] } } printInfo(`Round-trip verified: ${tripleMatch}`)
// Step 8: Summary printStep(8, 'Summary')
printInfo('Complex nested types follow consistent encoding rules:')
printInfo('\nArray of tuples (T)[]:') printInfo(' - 2-byte length prefix (element count)') printInfo(' - If T is dynamic: head section with offsets, tail section with data') printInfo(' - If T is static: elements encoded consecutively after length')
printInfo('\nTuple containing arrays (T1[],T2[]):') printInfo(' - Head section: offsets for each dynamic child') printInfo(' - Tail section: array data in order') printInfo(' - Static arrays (T[N]) encode inline, dynamic arrays (T[]) use offsets')
printInfo('\nNested structs with arrays:') printInfo(' - Struct encoding identical to equivalent tuple') printInfo(' - Static fields inline, dynamic fields via offsets') printInfo(' - Nested arrays and strings all end up in tail section')
printInfo('\nDeeply nested types like ((uint64,bool)[],string,(address,uint256))[]:') printInfo(' - Outer array: 2-byte count + offsets to each inner tuple') printInfo(' - Each inner tuple: offsets for dynamic parts, inline for static') printInfo(' - Innermost arrays: 2-byte count + element data') printInfo(' - Round-trip encoding/decoding preserves all values at every nesting level')
printInfo('\nKey observations:') printInfo(' - Static types never need offsets (fixed position)') printInfo(' - Dynamic types always use 2-byte offsets relative to container start') printInfo(' - Nesting depth doesnt change rules, just adds layers') printInfo(' - All encoded bytes are deterministic for same input values')
printSuccess('ABI Complex Nested Types example completed successfully!')}
main()