Layout of a Leo Project
Manifest
program.json is the Leo manifest file that configures our package.
{
"program": "hello.aleo",
"version": "0.1.0",
"description": "",
"license": "MIT",
"dependencies": null,
"dev_dependencies": null
}
The program ID in program is the official name that other developers will be able to look up after you have published your program.
"program": "hello.aleo",
Dependencies will be added to the field of the same name, as they are added. The dependencies are also pegged in the leo.lock file.
The src/ directory is where all of your Leo code will live. The main entry point of your project is a file in this directory appropriately named main.leo. Calls to many of the Leo CLI commands will require you to have this file within your project in order to succeed properly.
Programs
A program is a collection of code (its functions) and data (its types) that resides at a
program ID on the Aleo blockchain. A program is declared as program {name}.{network} { ... }.
The body of the program is delimited by curly braces {}.
import foo.aleo;
const FOO: u64 = 1u64;
struct Message {
sender: address,
object: u64,
}
fn compute(a: u64, b: u64) -> u64 {
return a + b + FOO;
}
program hello.aleo {
mapping account: address => u64;
record Token {
owner: address,
amount: u64,
}
fn mint_public(
public receiver: address,
public amount: u64,
) -> (Token, Final) {
let token: Token = Token { owner: receiver, amount };
return (token, final {
let current_amount: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, current_amount + amount);
});
}
@noupgrade
constructor() {}
}
The following must be declared inside the scope of a program in a Leo file:
- A single, mandatory
constructor - Mappings and Storage
- Records
- Entry point
fndeclarations
The following must be declared outside the scope of a program in a Leo file:
- Imports
- Constants
- Structs
- Helper
fndefinitions final fndefinitionsinterfacedefinitions
Declarations are locally accessible within a program file. If you need a declaration from another Leo file, you must import it.
Imports
You can import dependencies that are downloaded to the imports directory.
An import is declared as import {filename}.aleo;
The dependency resolver will pull the imported program from the network or the local filesystem.
import foo.aleo; // Import all `foo.aleo` declarations into the `hello.aleo` program.
program hello.aleo {
Program ID
A program ID is declared as {name}.{network}.
The first character of a name must be a lowercase letter.
name can only contain lowercase letters, numbers, and underscores, and must not contain a double underscore (__) or the keyword aleo in it.
Currently, aleo is the only supported network domain.
program hello.aleo; // valid
program Foo.aleo; // invalid
program baR.aleo; // invalid
program 0foo.aleo; // invalid
program 0_foo.aleo; // invalid
program _foo.aleo; // invalid
program foo__bar.aleo; // invalid
program aleo.aleo; // invalid
Modules
In addition to your main file, Leo also supports a module system as of v3.2.0.
Leaf modules (that is modules without submodules) must be defined in a single file (ex. foo.leo). Modules with submodules must be defined by an optional top-level .leo file and a subdirectory containing the submodules:
Take the following project as an example:
src
├── common.leo
├── main.leo
├── outer.leo
└── outer
└── inner.leo
Given the structure above, the following modules are defined:
| Filename | Type | Module Name | Access Location & Pattern |
|---|---|---|---|
common.leo | Module | common | main.leo : common::<item> |
outer.leo | Module | outer | main.leo : outer::<item> |
outer/inner.leo | Submodule | outer::inner | main.leo : outer::inner::<item> outer.leo : inner::<item> |
Within a package, paths between modules resolve relative to the file you are writing in. The compiler builds the target path by prepending the current module's path to whatever you write:
- From
main.leo(current module: empty),outer::inner::fooresolves to the package-level pathouter::inner::foo. - From
outer.leo(current module:outer),inner::fooresolves toouter::inner::foo. - From
outer/inner.leo(current module:outer::inner), a barefooresolves toouter::inner::foo.
A downward path from parent to child always works, for example from outer.leo to outer/inner.leo. However, upward and sideways paths from a submodule do not work. In outer/inner.leo, common::foo resolves to outer::inner::common::foo. Leo does not have syntax to reference an item outside the current module subtree.
export const X: u32 = 2u32;
struct S {
a: field,
}
fn increment(x: field) -> field {
return 1field;
}
A module file may only contain struct, const, and fn definitions.
Visibility
By default, module-level struct, const, fn, and interface declarations are private. Add the export modifier to give access to another source file or compilation unit:
export const MAX_VALUE: u32 = 100u32;
export struct SharedValue {
value: u32,
}
export fn clamp(value: u32) -> u32 {
return value > MAX_VALUE ? MAX_VALUE : value;
}
- Only the source file can use an unqualified item.
- Other modules in the same package and dependent packages can use an
exportitem. - Items in
program { … }are part of the program interface. Do not useexporton these items. - Leo includes exported helper function bytecode in the importer. It does not deploy the functions separately.
Accessing Submodules of Imported Programs
An imported program can use submodules. Use an extended locator path to access an exported struct, const, interface, or helper fn in these submodules:
program.aleo::submodule::item
For example, suppose provider.aleo has a submodule colors that defines a Color struct, a MAX_CH constant, and a blend helper:
export const MAX_CH: u32 = 255u32;
export struct Color {
r: u32,
g: u32,
b: u32,
}
export fn blend(a: Color, b: Color) -> Color {
return Color { r: (a.r + b.r) / 2u32, g: (a.g + b.g) / 2u32, b: (a.b + b.b) / 2u32 };
}
program provider.aleo {
fn sum_channels(c: colors::Color) -> u32 {
return c.r + c.g + c.b;
}
fn mix_colors(a: colors::Color, b: colors::Color) -> colors::Color {
return colors::blend(a, b);
}
@noupgrade
constructor() {}
}
A program that imports provider.aleo can reach the submodule struct, constant, and helper through the extended path, and can also call provider.aleo's top-level entry functions:
import provider.aleo;
program consumer.aleo {
// Struct and const from the submodule.
fn make_white() -> provider.aleo::colors::Color {
return provider.aleo::colors::Color {
r: provider.aleo::colors::MAX_CH,
g: provider.aleo::colors::MAX_CH,
b: provider.aleo::colors::MAX_CH,
};
}
// Top-level entry function from the provider.
fn mix(a: provider.aleo::colors::Color, b: provider.aleo::colors::Color) -> provider.aleo::colors::Color {
return provider.aleo::mix_colors(a, b);
}
// Submodule helper called directly — inlined into consumer's bytecode.
fn average(a: provider.aleo::colors::Color, b: provider.aleo::colors::Color) -> provider.aleo::colors::Color {
return provider.aleo::colors::blend(a, b);
}
@noupgrade
constructor() {}
}
Leo puts helper fn code from program.aleo::submodule::name(...) directly in the caller bytecode. These helpers are not separate on-chain calls and do not appear in the provider ABI. Only top-level entry functions in program provider.aleo { ... } remain in its on-chain interface.
Submodule paths can be arbitrarily deep — program.aleo::a::b::item is valid if program.aleo has a nested submodule a/b.leo. The same extended path syntax applies to library submodules (see Leo Libraries).
interface definitions may also be referenced through the same path syntax — both library submodules (program my_app.aleo: my_lib::interfaces::Adder { ... }) and imported program submodules (program my_app.aleo: other_prog.aleo::interfaces::Adder { ... }) work in a program header.