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": [...],
"functions": [...]
}
| 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 |
functions | Public entry points (entry fn declarations, not helper functions) |
The ABI only includes types that are referenced by the public interface. Internal helper structs not used in entry functions, 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" } }
}
}
}
Entry Functions
Entry functions define the public entry points:
{
"name": "transfer",
"has_final": 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 entry function)
Output types:
Plaintext- Primitive, array, struct, or optionalRecord- Record output (created by the entry function)Final- Entry function with afinal { }block returns aFinal
Entry functions with final { } blocks have has_final: true and return a Final:
{
"name": "mint_public",
"has_final": true,
"inputs": [
{ "name": "receiver", "ty": { "Plaintext": { "Primitive": "Address" } }, "mode": "Public" },
{ "name": "amount", "ty": { "Plaintext": { "Primitive": { "UInt": "U64" } } }, "mode": "Public" }
],
"outputs": [{ "ty": "Final", "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 |
Final | 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:
program example.aleo {
fn 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>;
fn append(value: u64) -> Final {
return final {
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:
program example.aleo {
fn 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,
}
fn mint_public(
public receiver: address,
public amount: u64
) -> Final {
return final {
let current: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, current + amount);
};
}
fn mint_private(receiver: address, amount: u64) -> Token {
return Token { owner: receiver, amount };
}
fn 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": [],
"functions": [
{
"name": "mint_public",
"has_final": true,
"inputs": [
{ "name": "receiver", "ty": { "Plaintext": { "Primitive": "Address" } }, "mode": "Public" },
{ "name": "amount", "ty": { "Plaintext": { "Primitive": { "UInt": "U64" } } }, "mode": "Public" }
],
"outputs": [{ "ty": "Final", "mode": "None" }]
},
{
"name": "mint_private",
"has_final": 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",
"has_final": 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_publichas afinal { }block (has_final: true) and returns aFinalin the ABImint_privateandtransfer_privatereturnRecordoutputstransfer_privatetakes aRecordinput (consuming the token)
See Also
- Leo Build Command - CLI reference for building programs
- Data Types - Leo type system reference
- The Finalization Model - Understanding the proof and finalization execution contexts