Skip to content
Algorand Developer Portal

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