Skip to main content

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 program
  • build/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:

abi.json
{
"program": "token.aleo",
"structs": [...],
"records": [...],
"mappings": [...],
"storage_variables": [...],
"transitions": [...]
}
FieldDescription
programProgram identifier (e.g., "token.aleo")
structsStruct type definitions used in the public interface
recordsRecord type definitions
mappingsOn-chain key-value storage declarations
storage_variablesStorage variable declarations
transitionsPublic entry points (only transitions, not internal functions)
info

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 optional
  • Record - Record input (consumed by the transition)

Output types:

  • Plaintext - Primitive, array, struct, or optional
  • Record - 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 TypeAleo Type
addressaddress
boolboolean
fieldfield
groupgroup
scalarscalar
signaturesignature
i8 - i128i8 - i128
u8 - u128u8 - u128
[T; N][T; N]
struct FooFoo
record BarBar.record
Futurefuture

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:

  1. Get length from {name}__len__ at key false
  2. Read elements from {name}__ at indices 0 to length - 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;
tip

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

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 Token record is included (no internal helper types)
  • mint_public is async (is_async: true) and returns a Future
  • mint_private and transfer_private return Record outputs
  • transfer_private takes a Record input (consuming the token)
  • The finalize_mint_public function is internal and not exposed in the ABI

See Also