ABI Generation
Overview
The Leo compiler generates an Application Binary Interface (ABI) alongside compiled bytecode. The ABI is a JSON file that describes the public interface of your program, enabling downstream tooling to interact with deployed programs without needing access to the original source code.
Use cases:
- SDK generation (Rust, TypeScript, etc.)
- Type-safe transaction construction
- Program introspection and documentation
- Tooling integration (explorers, wallets, IDEs)
Build Outputs
When you run leo build, the compiler generates ABI files alongside the compiled .aleo bytecode:
build/
├── main.aleo # Compiled Aleo bytecode
├── abi.json # ABI for your program
└── imports/
├── foo.aleo # Imported program bytecode
└── foo.abi.json # ABI for imported program
build/abi.json- ABI for your main programbuild/imports/{program}.abi.json- ABIs for each imported dependency
ABI generation is automatic on every build - no flags required.
ABI Format
The ABI is a JSON object with the following top-level structure:
{
"program": "token.aleo",
"structs": [...],
"records": [...],
"mappings": [...],
"storage_variables": [...],
"transitions": [...]
}
| Field | Description |
|---|---|
program | Program identifier (e.g., "token.aleo") |
structs | Struct type definitions used in the public interface |
records | Record type definitions |
mappings | On-chain key-value storage declarations |
storage_variables | Storage variable declarations |
transitions | Public entry points (only transitions, not internal functions) |
The ABI only includes types that are referenced by the public interface. Internal helper structs not used in transitions, mappings, or storage are automatically pruned.
Type Reference
Primitives
Primitive types are represented directly:
{ "Primitive": "Address" }
{ "Primitive": "Boolean" }
{ "Primitive": "Field" }
{ "Primitive": "Group" }
{ "Primitive": "Scalar" }
{ "Primitive": "Signature" }
Integer types include the signedness:
{ "Primitive": { "Int": "I8" } }
{ "Primitive": { "Int": "I16" } }
{ "Primitive": { "Int": "I32" } }
{ "Primitive": { "Int": "I64" } }
{ "Primitive": { "Int": "I128" } }
{ "Primitive": { "UInt": "U8" } }
{ "Primitive": { "UInt": "U16" } }
{ "Primitive": { "UInt": "U32" } }
{ "Primitive": { "UInt": "U64" } }
{ "Primitive": { "UInt": "U128" } }
Arrays
Fixed-length arrays include the element type and length:
{
"Array": {
"element": { "Primitive": "Field" },
"length": 4
}
}
Nested arrays are supported:
{
"Array": {
"element": {
"Array": {
"element": { "Primitive": { "UInt": "U32" } },
"length": 2
}
},
"length": 3
}
}
Structs
Struct references include a path (supporting modules) and optionally the source program:
{
"Struct": {
"path": ["Point"],
"program": "geometry"
}
}
For structs in modules:
{
"Struct": {
"path": ["utils", "Vector3"],
"program": "geometry"
}
}
Struct definitions include all fields:
{
"path": ["Point"],
"fields": [
{ "name": "x", "ty": { "Primitive": { "Int": "I32" } } },
{ "name": "y", "ty": { "Primitive": { "Int": "I32" } } }
]
}
Records
Records are similar to structs but include a visibility mode for each field:
{
"path": ["Token"],
"fields": [
{ "name": "owner", "ty": { "Primitive": "Address" }, "mode": "None" },
{ "name": "amount", "ty": { "Primitive": { "UInt": "U64" } }, "mode": "Public" },
{ "name": "data", "ty": { "Primitive": "Field" }, "mode": "Private" }
]
}
Mode values:
"None"- Default visibility (private for records)"Constant"- Publicly visible constant"Private"- Encrypted, visible only to owner"Public"- Visible on-chain
Optional
Optional types (T?) are represented as:
{
"Optional": { "Primitive": "Field" }
}
Mappings
Mappings define on-chain key-value storage:
{
"name": "balances",
"key": { "Primitive": "Address" },
"value": { "Primitive": { "UInt": "U64" } }
}
Storage Variables
Storage variables can be plain values or vectors:
{
"name": "counter",
"ty": {
"Plaintext": { "Primitive": { "UInt": "U32" } }
}
}
{
"name": "history",
"ty": {
"Vector": {
"Plaintext": { "Primitive": { "UInt": "U64" } }
}
}
}
Transitions
Transitions define the public entry points:
{
"name": "transfer",
"is_async": false,
"inputs": [
{
"name": "receiver",
"ty": { "Plaintext": { "Primitive": "Address" } },
"mode": "Public"
},
{
"name": "amount",
"ty": { "Plaintext": { "Primitive": { "UInt": "U64" } } },
"mode": "Public"
}
],
"outputs": [
{
"ty": { "Plaintext": { "Primitive": { "UInt": "U64" } } },
"mode": "Public"
}
]
}
Input types:
Plaintext- Primitive, array, struct, or optionalRecord- Record input (consumed by the transition)
Output types:
Plaintext- Primitive, array, struct, or optionalRecord- Record output (created by the transition)Future- Async transition returns a future
Async transitions have is_async: true and return a Future:
{
"name": "mint_public",
"is_async": true,
"inputs": [
{ "name": "receiver", "ty": { "Plaintext": { "Primitive": "Address" } }, "mode": "Public" },
{ "name": "amount", "ty": { "Plaintext": { "Primitive": { "UInt": "U64" } } }, "mode": "Public" }
],
"outputs": [
{ "ty": "Future", "mode": "None" }
]
}
Type Lowering Specification
The ABI uses Leo types (the high-level representation). When interacting with the Aleo VM directly, downstream tooling must apply transformations to understand the on-chain representation.
Leo to Aleo Type Mapping
Most Leo types map directly to Aleo types:
| Leo Type | Aleo Type |
|---|---|
address | address |
bool | boolean |
field | field |
group | group |
scalar | scalar |
signature | signature |
i8 - i128 | i8 - i128 |
u8 - u128 | u8 - u128 |
[T; N] | [T; N] |
struct Foo | Foo |
record Bar | Bar.record |
Future | future |
Optional Lowering
Leo's optional type (T?) is lowered to a struct with two fields:
T? --> struct { is_some: bool, val: T }
Leo source:
transition process(value: u32?) -> u32 {
return value.unwrap_or(0u32);
}
Aleo representation:
struct "u32?" {
is_some as boolean;
val as u32;
}
function process:
input r0 as "u32?".private;
// ...
When is_some is false, val contains the zero value of the underlying type.
Nested optional example:
let arr: [u64?; 2] = [1u64, none];
Lowers to an array of structs:
[
"u64?" { is_some: true, val: 1u64 },
"u64?" { is_some: false, val: 0u64 }
]
Storage Vector Lowering
Leo's storage vectors (storage vec: Vector<T>) are lowered to two mappings:
storage vec: Vector<T>
-->
mapping vec__: u32 => T // Elements indexed by position
mapping vec__len__: bool => u32 // Length stored at key `false`
Leo source:
program example.aleo {
storage history: Vector<u64>;
async transition append(value: u64) -> Future {
return finalize_append(value);
}
async function finalize_append(value: u64) {
history.push(value);
}
}
Aleo representation:
mapping history__:
key as u32.public;
value as u64.public;
mapping history__len__:
key as boolean.public;
value as u32.public;
To read a storage vector:
- Get length from
{name}__len__at keyfalse - Read elements from
{name}__at indices0tolength - 1
Tuple Expansion
Tuples are expanded into multiple registers in Aleo bytecode:
(T1, T2, T3) --> r0: T1, r1: T2, r2: T3
Leo source:
transition swap(a: u32, b: u32) -> (u32, u32) {
return (b, a);
}
Aleo bytecode:
function swap:
input r0 as u32.private;
input r1 as u32.private;
output r1 as u32.private;
output r0 as u32.private;
When constructing transactions, tuple inputs/outputs become separate arguments in order.
Example: Token Program
Here's a complete example showing a Leo program and its generated ABI.
Leo source (token.leo):
program token.aleo {
mapping account: address => u64;
record Token {
owner: address,
amount: u64,
}
async transition mint_public(
public receiver: address,
public amount: u64
) -> Future {
return finalize_mint_public(receiver, amount);
}
async function finalize_mint_public(
public receiver: address,
public amount: u64
) {
let current: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, current + amount);
}
transition mint_private(receiver: address, amount: u64) -> Token {
return Token { owner: receiver, amount };
}
transition transfer_private(token: Token, receiver: address) -> Token {
return Token { owner: receiver, amount: token.amount };
}
}
Generated ABI (build/abi.json):
{
"program": "token.aleo",
"structs": [],
"records": [
{
"path": ["Token"],
"fields": [
{ "name": "owner", "ty": { "Primitive": "Address" }, "mode": "None" },
{ "name": "amount", "ty": { "Primitive": { "UInt": "U64" } }, "mode": "None" }
]
}
],
"mappings": [
{
"name": "account",
"key": { "Primitive": "Address" },
"value": { "Primitive": { "UInt": "U64" } }
}
],
"storage_variables": [],
"transitions": [
{
"name": "mint_public",
"is_async": true,
"inputs": [
{ "name": "receiver", "ty": { "Plaintext": { "Primitive": "Address" } }, "mode": "Public" },
{ "name": "amount", "ty": { "Plaintext": { "Primitive": { "UInt": "U64" } } }, "mode": "Public" }
],
"outputs": [
{ "ty": "Future", "mode": "None" }
]
},
{
"name": "mint_private",
"is_async": false,
"inputs": [
{ "name": "receiver", "ty": { "Plaintext": { "Primitive": "Address" } }, "mode": "None" },
{ "name": "amount", "ty": { "Plaintext": { "Primitive": { "UInt": "U64" } } }, "mode": "None" }
],
"outputs": [
{ "ty": { "Record": { "path": ["Token"], "program": "token" } }, "mode": "None" }
]
},
{
"name": "transfer_private",
"is_async": false,
"inputs": [
{ "name": "token", "ty": { "Record": { "path": ["Token"], "program": "token" } }, "mode": "None" },
{ "name": "receiver", "ty": { "Plaintext": { "Primitive": "Address" } }, "mode": "None" }
],
"outputs": [
{ "ty": { "Record": { "path": ["Token"], "program": "token" } }, "mode": "None" }
]
}
]
}
Key observations:
- Only
Tokenrecord is included (no internal helper types) mint_publicis async (is_async: true) and returns aFuturemint_privateandtransfer_privatereturnRecordoutputstransfer_privatetakes aRecordinput (consuming the token)- The
finalize_mint_publicfunction is internal and not exposed in the ABI
See Also
- Leo Build Command - CLI reference for building programs
- Data Types - Leo type system reference
- Async Programming Model - Understanding transitions and finalize