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/
├── my_program/ # your program's own build directory
│ ├── my_program.aleo # compiled Aleo bytecode
│ ├── abi.json # ABI for your program
│ └── interfaces/
│ ├── MyInterface.json # Per-interface ABI (locally defined)
│ └── parent_program/
│ └── ParentInterface.json # Per-interface ABI (transitive parent)
└── foo/ # an imported dependency
├── foo.aleo # imported program bytecode
└── abi.json # ABI for the imported program

Every program - your own and each dependency - gets its own build/{program}/ directory with the same shape.

  • build/{program}/{program}.aleo - compiled bytecode for each program.
  • build/{program}/abi.json - ABI for each program.
  • build/{program}/interfaces/ - per-interface ABI JSON files, one per interface declared locally (top-level or in a module) and one per transitive parent interface implemented through scope.parents. External parent interfaces are nested under their owning program's directory. The interfaces/ directory is cleared and regenerated on every build, so renamed or deleted interfaces never leave stale files behind.

ABI generation is automatic on every build - no flags required.

Generating ABIs from Compiled Bytecode

The standalone leo abi command generates an ABI JSON document from any .aleo file — useful when you have a deployed program's bytecode but not its source. The same code path is exposed as a WebAssembly binding through the leo-aleo-abi-wasm crate, so browser tooling (wallets, explorers) can produce ABIs from bytecode without shelling out to the CLI.

ABI Format

The ABI is a JSON object with the following top-level structure:

abi.json
{
"program": "token.aleo",
"implements": [...],
"structs": [...],
"records": [...],
"mappings": [...],
"storage_variables": [...],
"functions": [...],
"views": [...]
}
FieldDescription
programProgram identifier (e.g., "token.aleo")
implementsFully qualified interfaces the program implements
structsStruct type definitions used in the public interface
recordsRecord type definitions
mappingsOn-chain key-value storage declarations
storage_variablesStorage variable declarations
functionsPublic entry points (entry fn declarations, not helper functions)
viewsRead-only view fn entry points; same shape as functions
info

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

Output types:

  • Plaintext - Primitive, array, struct, or optional
  • Record - Record output (created by the entry function)
  • Final - Entry function with a final { } block returns a Final

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 TypeAleo Type
addressaddress
boolboolean
fieldfield
groupgroup
scalarscalar
signaturesignature
i8 - i128i8 - i128
u8 - u128u8 - u128
[T; N][T; N]
struct FooFoo
record BarBar.record
Finalfuture

Optional Lowering

Leo's optional type (T?) is lowered to a struct with two fields:

T?  -->  struct { is_some: bool, val: T }

Leo source (a helper fn — entry-point fns cannot take an Optional directly):

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: [u64];

fn append(value: u64) -> Final {
return final {
history.push(value);
};
}

@noupgrade
constructor() {}
}

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:

program example.aleo {
fn swap(a: u32, b: u32) -> (u32, u32) {
return (b, a);
}

@noupgrade
constructor() {}
}

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,
}

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 };
}

@noupgrade
constructor() {}
}

Generated ABI (build/token/abi.json):

abi.json
{
"program": "token.aleo",
"implements": [],
"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",
"is_final": true,
"const_parameters": [],
"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",
"is_final": false,
"const_parameters": [],
"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.aleo"
}
},
"mode": "None"
}
]
},
{
"name": "transfer_private",
"is_final": false,
"const_parameters": [],
"inputs": [
{
"name": "token",
"ty": {
"Record": {
"path": [
"Token"
],
"program": "token.aleo"
}
},
"mode": "None"
},
{
"name": "receiver",
"ty": {
"Plaintext": {
"Primitive": "Address"
}
},
"mode": "None"
}
],
"outputs": [
{
"ty": {
"Record": {
"path": [
"Token"
],
"program": "token.aleo"
}
},
"mode": "None"
}
]
}
]
}

Key observations:

  • Only Token record is included (no internal helper types)
  • mint_public has a final { } block (has_final: true) and returns a Final in the ABI
  • mint_private and transfer_private return Record outputs
  • transfer_private takes a Record input (consuming the token)

See Also