Skip to content

ABI Complex Nested Types

← Back to ABI Encoding

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
  • No LocalNet required

From the repository root:

Terminal window
cd examples
npm run example abi/14-complex-nested.ts

View source on GitHub

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()