diff --git a/.github/mlc_config.json b/.github/mlc_config.json new file mode 100644 index 000000000..ecdc00abb --- /dev/null +++ b/.github/mlc_config.json @@ -0,0 +1,5 @@ +{ + "retryOn429": true, + "retryCount": 3, + "fallbackRetryDelay": "30s" +} diff --git a/.github/workflows/ci-branches.yml b/.github/workflows/ci-branches.yml index f37809841..c45a7d030 100644 --- a/.github/workflows/ci-branches.yml +++ b/.github/workflows/ci-branches.yml @@ -15,3 +15,4 @@ jobs: - uses: gaurav-nelson/github-action-markdown-link-check@v1 with: folder-path: 'documentation' + config-file: '.github/mlc_config.json' diff --git a/documentation/cli/03_build.md b/documentation/cli/03_build.md index d78e5d9ed..50743e079 100644 --- a/documentation/cli/03_build.md +++ b/documentation/cli/03_build.md @@ -21,8 +21,11 @@ On invoking the build command, Leo automatically creates a `build/⁠` and `outp Leo 2 statements after dead code elimination. Leo The program checksum is: '[...]'. Leo ✅ Compiled '{PROGRAM_NAME}.aleo' into Aleo instructions. + Leo ✅ Generated ABI at 'build/abi.json'. ``` +The build also generates an **ABI file** at `build/abi.json` describing your program's public interface (transitions, mappings, and types). See the [ABI Generation guide](../guides/11_abi.md) for details on the format and type lowering specification. + ### Flags: ``` --offline diff --git a/documentation/guides/00_overview.md b/documentation/guides/00_overview.md index 8140d85c6..8cc8d451f 100644 --- a/documentation/guides/00_overview.md +++ b/documentation/guides/00_overview.md @@ -30,3 +30,5 @@ There's a lot to learn about Leo! To help tame the complexity, we've put togethe - [**Upgrading Programs**](./10_program_upgradability.md) - Coming soon! +- [**ABI Generation**](./11_abi.md) - Learn about the ABI format and type lowering for SDK integration. + diff --git a/documentation/guides/11_abi.md b/documentation/guides/11_abi.md new file mode 100644 index 000000000..816983b91 --- /dev/null +++ b/documentation/guides/11_abi.md @@ -0,0 +1,528 @@ +--- +id: abi +title: ABI Generation +sidebar_label: ABI Generation +--- +[general tags]: # (abi, build, sdk, tooling, types, lowering, integration) + +## 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: + +```json title="abi.json" +{ + "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) | + +:::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: + +```json +{ "Primitive": "Address" } +{ "Primitive": "Boolean" } +{ "Primitive": "Field" } +{ "Primitive": "Group" } +{ "Primitive": "Scalar" } +{ "Primitive": "Signature" } +``` + +Integer types include the signedness: + +```json +{ "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: + +```json +{ + "Array": { + "element": { "Primitive": "Field" }, + "length": 4 + } +} +``` + +Nested arrays are supported: + +```json +{ + "Array": { + "element": { + "Array": { + "element": { "Primitive": { "UInt": "U32" } }, + "length": 2 + } + }, + "length": 3 + } +} +``` + +### Structs + +Struct references include a path (supporting modules) and optionally the source program: + +```json +{ + "Struct": { + "path": ["Point"], + "program": "geometry" + } +} +``` + +For structs in modules: + +```json +{ + "Struct": { + "path": ["utils", "Vector3"], + "program": "geometry" + } +} +``` + +Struct definitions include all fields: + +```json +{ + "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: + +```json +{ + "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: + +```json +{ + "Optional": { "Primitive": "Field" } +} +``` + +### Mappings + +Mappings define on-chain key-value storage: + +```json +{ + "name": "balances", + "key": { "Primitive": "Address" }, + "value": { "Primitive": { "UInt": "U64" } } +} +``` + +### Storage Variables + +Storage variables can be plain values or vectors: + +```json +{ + "name": "counter", + "ty": { + "Plaintext": { "Primitive": { "UInt": "U32" } } + } +} +``` + +```json +{ + "name": "history", + "ty": { + "Vector": { + "Plaintext": { "Primitive": { "UInt": "U64" } } + } + } +} +``` + +### Transitions + +Transitions define the public entry points: + +```json +{ + "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`: + +```json +{ + "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:** +```leo showLineNumbers +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:** + +```leo showLineNumbers +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`) are lowered to two mappings: + +``` +storage vec: Vector + --> +mapping vec__: u32 => T // Elements indexed by position +mapping vec__len__: bool => u32 // Length stored at key `false` +``` + +**Leo source:** +```leo showLineNumbers +program example.aleo { + storage history: Vector; + + 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:** +```leo showLineNumbers +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`):** + +```leo showLineNumbers +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`):** + +```json title="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 + +- [Leo Build Command](./../cli/03_build.md) - CLI reference for building programs +- [Data Types](../language/03_data_types.md) - Leo type system reference +- [Async Programming Model](./01_async.md) - Understanding transitions and finalize