Skip to content

ABI Tuple Type

← Back to ABI Encoding

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

From the repository root:

Terminal window
cd examples
npm run example abi/07-tuple-type.ts

View source on GitHub

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