ABI Tuple Type
Description
Section titled “Description”This example demonstrates how to encode and decode tuples using ABITupleType:
- Tuples with mixed static types: (uint64,bool,address)
- Tuples with dynamic types: (uint64,string,bool)
- Nested tuples: ((uint64,bool),string) Key characteristics of tuple encoding:
- Static-only tuples: all elements encoded consecutively, fixed size
- Tuples with dynamic elements: head/tail encoding pattern
- Head: static values inline + offsets for dynamic values
- Tail: actual data for dynamic elements
- Nested tuples: inner tuples are encoded first, then treated as their component ARC-4 specification: Tuples are sequences of types enclosed in parentheses.
Prerequisites
Section titled “Prerequisites”- No LocalNet required
Run This Example
Section titled “Run This Example”From the repository root:
cd examplesnpm run example abi/07-tuple-type.ts/** * Example: ABI Tuple Type * * This example demonstrates how to encode and decode tuples using ABITupleType: * - Tuples with mixed static types: (uint64,bool,address) * - Tuples with dynamic types: (uint64,string,bool) * - Nested tuples: ((uint64,bool),string) * * Key characteristics of tuple encoding: * - Static-only tuples: all elements encoded consecutively, fixed size * - Tuples with dynamic elements: head/tail encoding pattern * - Head: static values inline + offsets for dynamic values * - Tail: actual data for dynamic elements * - Nested tuples: inner tuples are encoded first, then treated as their component * * ARC-4 specification: Tuples are sequences of types enclosed in parentheses. * * Prerequisites: * - No LocalNet required */
import { Address } from '@algorandfoundation/algokit-utils'import { ABIBoolType, ABIStringType, ABITupleType, ABIType, ABIUintType } from '@algorandfoundation/algokit-utils/abi'import { formatHex, printHeader, printInfo, printStep, printSuccess } from '../shared/utils.js'
function main() { printHeader('ABI Tuple Type Example')
// Step 1: ABITupleType properties printStep(1, 'ABITupleType Properties')
const staticTupleType = ABIType.from('(uint64,bool,address)') as ABITupleType const dynamicTupleType = ABIType.from('(uint64,string,bool)') as ABITupleType const nestedTupleType = ABIType.from('((uint64,bool),string)') as ABITupleType
printInfo('(uint64,bool,address) - all static types:') printInfo(` toString(): ${staticTupleType.toString()}`) printInfo(` childTypes.length: ${staticTupleType.childTypes.length}`) staticTupleType.childTypes.forEach((child, i) => { printInfo(` [${i}]: ${child.toString()} (isDynamic: ${child.isDynamic()})`) }) printInfo(` isDynamic(): ${staticTupleType.isDynamic()}`) printInfo(` byteLen(): ${staticTupleType.byteLen()}`)
printInfo('\n(uint64,string,bool) - contains dynamic type:') printInfo(` toString(): ${dynamicTupleType.toString()}`) printInfo(` childTypes.length: ${dynamicTupleType.childTypes.length}`) dynamicTupleType.childTypes.forEach((child, i) => { printInfo(` [${i}]: ${child.toString()} (isDynamic: ${child.isDynamic()})`) }) printInfo(` isDynamic(): ${dynamicTupleType.isDynamic()} (because string is dynamic)`)
printInfo('\n((uint64,bool),string) - nested tuple with dynamic:') printInfo(` toString(): ${nestedTupleType.toString()}`) printInfo(` childTypes.length: ${nestedTupleType.childTypes.length}`) nestedTupleType.childTypes.forEach((child, i) => { printInfo(` [${i}]: ${child.toString()} (isDynamic: ${child.isDynamic()})`) }) printInfo(` isDynamic(): ${nestedTupleType.isDynamic()}`)
// Step 2: Static tuple encoding - (uint64,bool,address) printStep(2, 'Static Tuple Encoding - (uint64,bool,address)')
// Create a sample address const pubKey = new Uint8Array(32).fill(0xab) const sampleAddress = new Address(pubKey).toString()
const staticValue: [bigint, boolean, string] = [1000n, true, sampleAddress] const staticEncoded = staticTupleType.encode(staticValue) const staticDecoded = staticTupleType.decode(staticEncoded) as [bigint, boolean, string]
printInfo(`Input: [${staticValue[0]}, ${staticValue[1]}, ${staticValue[2].substring(0, 10)}...]`) printInfo(`Encoded: ${formatHex(staticEncoded)}`) printInfo(`Total bytes: ${staticEncoded.length}`)
// Break down the encoding printInfo('\nByte layout (all static, no head/tail separation):') printInfo(` [0-7] uint64: ${formatHex(staticEncoded.slice(0, 8))} = ${staticValue[0]}`) printInfo(` [8] bool: ${formatHex(staticEncoded.slice(8, 9))} = ${staticValue[1]} (0x80=true, 0x00=false)`) printInfo(` [9-40] address: ${formatHex(staticEncoded.slice(9, 41))}`) printInfo(` Expected size: 8 + 1 + 32 = 41 bytes`)
printInfo(`\nDecoded: [${staticDecoded[0]}, ${staticDecoded[1]}, ${staticDecoded[2].substring(0, 10)}...]`) printInfo(`Round-trip verified: ${staticDecoded[0] === staticValue[0] && staticDecoded[1] === staticValue[1] && staticDecoded[2] === staticValue[2]}`)
// Step 3: Dynamic tuple encoding - (uint64,string,bool) printStep(3, 'Dynamic Tuple Encoding - (uint64,string,bool)')
const dynamicValue: [bigint, string, boolean] = [42n, 'Hello ABI', false] const dynamicEncoded = dynamicTupleType.encode(dynamicValue) const dynamicDecoded = dynamicTupleType.decode(dynamicEncoded) as [bigint, string, boolean]
printInfo(`Input: [${dynamicValue[0]}, "${dynamicValue[1]}", ${dynamicValue[2]}]`) printInfo(`Encoded: ${formatHex(dynamicEncoded)}`) printInfo(`Total bytes: ${dynamicEncoded.length}`)
printInfo('\nHead/Tail encoding pattern:') printInfo('HEAD SECTION (static values + offset for dynamic):')
// uint64 is static - 8 bytes printInfo(` [0-7] uint64 (static): ${formatHex(dynamicEncoded.slice(0, 8))} = ${dynamicValue[0]}`)
// string is dynamic - 2-byte offset pointing to tail const stringOffset = (dynamicEncoded[8] << 8) | dynamicEncoded[9] printInfo(` [8-9] string offset: ${formatHex(dynamicEncoded.slice(8, 10))} = ${stringOffset} (points to tail)`)
// bool is static - 1 byte printInfo(` [10] bool (static): ${formatHex(dynamicEncoded.slice(10, 11))} = ${dynamicValue[2]}`)
printInfo('\nTAIL SECTION (dynamic value data):')
// String data starts at the offset const stringLenBytes = dynamicEncoded.slice(stringOffset, stringOffset + 2) const stringLen = (stringLenBytes[0] << 8) | stringLenBytes[1] const stringContentBytes = dynamicEncoded.slice(stringOffset + 2, stringOffset + 2 + stringLen)
printInfo(` [${stringOffset}-${stringOffset + 1}] string length: ${formatHex(stringLenBytes)} = ${stringLen} bytes`) printInfo(` [${stringOffset + 2}-${stringOffset + 1 + stringLen}] string content: ${formatHex(stringContentBytes)} = "${dynamicValue[1]}"`)
printInfo(`\nDecoded: [${dynamicDecoded[0]}, "${dynamicDecoded[1]}", ${dynamicDecoded[2]}]`) printInfo(`Round-trip verified: ${dynamicDecoded[0] === dynamicValue[0] && dynamicDecoded[1] === dynamicValue[1] && dynamicDecoded[2] === dynamicValue[2]}`)
// Step 4: Nested tuple encoding - ((uint64,bool),string) printStep(4, 'Nested Tuple Encoding - ((uint64,bool),string)')
const nestedValue: [[bigint, boolean], string] = [[999n, true], 'Nested!'] const nestedEncoded = nestedTupleType.encode(nestedValue) const nestedDecoded = nestedTupleType.decode(nestedEncoded) as [[bigint, boolean], string]
printInfo(`Input: [[${nestedValue[0][0]}, ${nestedValue[0][1]}], "${nestedValue[1]}"]`) printInfo(`Encoded: ${formatHex(nestedEncoded)}`) printInfo(`Total bytes: ${nestedEncoded.length}`)
printInfo('\nNested tuple encoding:') printInfo(' Inner tuple (uint64,bool) is static - encoded inline in head') printInfo(' String is dynamic - offset in head, data in tail')
printInfo('\nHEAD SECTION:') // Inner tuple is static: 8 bytes (uint64) + 1 byte (bool) = 9 bytes printInfo(` [0-7] inner.uint64: ${formatHex(nestedEncoded.slice(0, 8))} = ${nestedValue[0][0]}`) printInfo(` [8] inner.bool: ${formatHex(nestedEncoded.slice(8, 9))} = ${nestedValue[0][1]}`)
// String offset const nestedStringOffset = (nestedEncoded[9] << 8) | nestedEncoded[10] printInfo(` [9-10] string offset: ${formatHex(nestedEncoded.slice(9, 11))} = ${nestedStringOffset}`)
printInfo('\nTAIL SECTION:') const nestedStrLenBytes = nestedEncoded.slice(nestedStringOffset, nestedStringOffset + 2) const nestedStrLen = (nestedStrLenBytes[0] << 8) | nestedStrLenBytes[1] const nestedStrContent = nestedEncoded.slice(nestedStringOffset + 2, nestedStringOffset + 2 + nestedStrLen)
printInfo(` [${nestedStringOffset}-${nestedStringOffset + 1}] string length: ${formatHex(nestedStrLenBytes)} = ${nestedStrLen} bytes`) printInfo(` [${nestedStringOffset + 2}-${nestedStringOffset + 1 + nestedStrLen}] string content: ${formatHex(nestedStrContent)} = "${nestedValue[1]}"`)
printInfo(`\nDecoded: [[${nestedDecoded[0][0]}, ${nestedDecoded[0][1]}], "${nestedDecoded[1]}"]`) printInfo(`Round-trip verified: ${nestedDecoded[0][0] === nestedValue[0][0] && nestedDecoded[0][1] === nestedValue[0][1] && nestedDecoded[1] === nestedValue[1]}`)
// Step 5: Accessing tuple elements after decoding printStep(5, 'Accessing Tuple Elements After Decoding')
printInfo('Decoded values are returned as arrays, access by index:')
const mixedTuple = ABIType.from('(uint64,bool,string,address)') as ABITupleType const mixedValue: [bigint, boolean, string, string] = [123n, false, 'test', sampleAddress] const mixedEncoded = mixedTuple.encode(mixedValue) const mixedDecoded = mixedTuple.decode(mixedEncoded) as [bigint, boolean, string, string]
printInfo(`\nDecoded tuple (uint64,bool,string,address):`) printInfo(` element[0] (uint64): ${mixedDecoded[0]} (type: ${typeof mixedDecoded[0]})`) printInfo(` element[1] (bool): ${mixedDecoded[1]} (type: ${typeof mixedDecoded[1]})`) printInfo(` element[2] (string): "${mixedDecoded[2]}" (type: ${typeof mixedDecoded[2]})`) printInfo(` element[3] (address): ${mixedDecoded[3].substring(0, 10)}... (type: ${typeof mixedDecoded[3]})`)
// Nested tuple element access printInfo('\nAccessing nested tuple elements:') printInfo(` nestedDecoded[0]: inner tuple as array`) printInfo(` nestedDecoded[0][0]: ${nestedDecoded[0][0]} (inner uint64)`) printInfo(` nestedDecoded[0][1]: ${nestedDecoded[0][1]} (inner bool)`) printInfo(` nestedDecoded[1]: "${nestedDecoded[1]}" (outer string)`)
// Step 6: Byte layout comparison - static vs dynamic tuples printStep(6, 'Byte Layout Comparison')
printInfo('STATIC TUPLE (uint64,bool,address):') printInfo(` Layout: [uint64:8 bytes][bool:1 byte][address:32 bytes]`) printInfo(` Total: ${staticTupleType.byteLen()} bytes (fixed size)`) printInfo(` No head/tail separation - all data inline`)
printInfo('\nDYNAMIC TUPLE (uint64,string,bool):') printInfo(' Layout: HEAD + TAIL') printInfo(' HEAD: [uint64:8][string_offset:2][bool:1] = 11 bytes') printInfo(' TAIL: [string_len:2][string_data:N]') printInfo(' Total: 11 + 2 + string_length bytes') printInfo(` Example with "Hello ABI" (9 bytes): ${dynamicEncoded.length} bytes`)
// Show how different string lengths affect total size printInfo('\nSize varies with dynamic content:') const testStrings = ['', 'Hi', 'Hello World', 'A longer string for testing']
for (const str of testStrings) { const testVal: [bigint, string, boolean] = [1n, str, true] const testEncoded = dynamicTupleType.encode(testVal) const expectedSize = 11 + 2 + new TextEncoder().encode(str).length printInfo(` "${str}" (${str.length} chars): ${testEncoded.length} bytes (expected: ${expectedSize})`) }
// Step 7: Creating ABITupleType programmatically printStep(7, 'Creating ABITupleType Programmatically')
const customTupleType = new ABITupleType([new ABIUintType(32), new ABIBoolType(), new ABIStringType()])
printInfo('Created with: new ABITupleType([new ABIUintType(32), new ABIBoolType(), new ABIStringType()])') printInfo(` toString(): ${customTupleType.toString()}`) printInfo(` childTypes.length: ${customTupleType.childTypes.length}`) printInfo(` isDynamic(): ${customTupleType.isDynamic()}`)
const customValue: [number, boolean, string] = [500, true, 'Custom'] const customEncoded = customTupleType.encode(customValue) const customDecoded = customTupleType.decode(customEncoded) as [bigint, boolean, string]
printInfo(`\nEncode [${customValue[0]}, ${customValue[1]}, "${customValue[2]}"]:`) printInfo(` Encoded: ${formatHex(customEncoded)}`) printInfo(` Total bytes: ${customEncoded.length}`) printInfo(` Decoded: [${customDecoded[0]}, ${customDecoded[1]}, "${customDecoded[2]}"]`)
// Step 8: Multiple dynamic elements in a tuple printStep(8, 'Multiple Dynamic Elements')
const multiDynamicType = ABIType.from('(string,uint64,string)') as ABITupleType const multiDynamicValue: [string, bigint, string] = ['First', 42n, 'Second'] const multiDynamicEncoded = multiDynamicType.encode(multiDynamicValue) const multiDynamicDecoded = multiDynamicType.decode(multiDynamicEncoded) as [string, bigint, string]
printInfo(`Input: ["${multiDynamicValue[0]}", ${multiDynamicValue[1]}, "${multiDynamicValue[2]}"]`) printInfo(`Encoded: ${formatHex(multiDynamicEncoded)}`) printInfo(`Total bytes: ${multiDynamicEncoded.length}`)
printInfo('\nHead/Tail layout with multiple dynamic elements:') printInfo('HEAD: [string1_offset:2][uint64:8][string2_offset:2] = 12 bytes')
const str1Offset = (multiDynamicEncoded[0] << 8) | multiDynamicEncoded[1] const str2Offset = (multiDynamicEncoded[10] << 8) | multiDynamicEncoded[11]
printInfo(` [0-1] string1 offset: ${formatHex(multiDynamicEncoded.slice(0, 2))} = ${str1Offset}`) printInfo(` [2-9] uint64: ${formatHex(multiDynamicEncoded.slice(2, 10))} = ${multiDynamicValue[1]}`) printInfo(` [10-11] string2 offset: ${formatHex(multiDynamicEncoded.slice(10, 12))} = ${str2Offset}`)
printInfo('\nTAIL: [string1_data][string2_data]') printInfo(` String 1 at offset ${str1Offset}: "${multiDynamicValue[0]}"`) printInfo(` String 2 at offset ${str2Offset}: "${multiDynamicValue[2]}"`)
printInfo(`\nDecoded: ["${multiDynamicDecoded[0]}", ${multiDynamicDecoded[1]}, "${multiDynamicDecoded[2]}"]`) printInfo(`Round-trip verified: ${multiDynamicDecoded[0] === multiDynamicValue[0] && multiDynamicDecoded[1] === multiDynamicValue[1] && multiDynamicDecoded[2] === multiDynamicValue[2]}`)
// Step 9: Summary printStep(9, 'Summary')
printInfo('ABITupleType key properties:') printInfo(' - childTypes: array of types for each element') printInfo(' - isDynamic(): true if ANY child is dynamic') printInfo(' - byteLen(): only valid for static tuples')
printInfo('\nTuple encoding patterns:') printInfo(' Static tuples (all elements static):') printInfo(' - Elements encoded consecutively') printInfo(' - Fixed total size = sum of element sizes') printInfo(' - No offsets needed')
printInfo('\n Dynamic tuples (at least one dynamic element):') printInfo(' - HEAD: static values inline + 2-byte offsets for dynamic') printInfo(' - TAIL: actual data for dynamic elements') printInfo(' - Offsets are relative to start of tuple encoding')
printInfo('\nNested tuples:') printInfo(' - Inner tuples encoded as single units') printInfo(' - Static inner tuples: inline in head') printInfo(' - Dynamic inner tuples: offset in head, data in tail')
printInfo('\nDecoded values:') printInfo(' - Returned as arrays (tuples)') printInfo(' - Access elements by index: decoded[0], decoded[1], etc.') printInfo(' - Nested tuples: decoded[0][0] for inner elements')
printInfo('\nCreating tuple types:') printInfo(' - ABIType.from("(uint64,bool)") - parse from string') printInfo(' - new ABITupleType([child1, child2, ...]) - programmatic')
printSuccess('ABI Tuple Type example completed successfully!')}
main()