Aleo Cookbook — Complete Recipes
Overview
This skill provides complete, ready-to-use programs organized by task. Every recipe includes the full Leo source, CLI commands to test it, and expected behavior. Use these as starting templates and modify them for your specific needs.
Version & Canonical Syntax
- Target: Leo compiler >= 3.5.0
- Canonical syntax: Recipes use
transitionfor entry points and separateasync function finalize_NAME(...)blocks where public state is modified - Async pattern: When a transition modifies on-chain state, declare it as
async transition ... -> Futureand return a call tofinalize_NAME(...). Define the finalize logic in a separateasync function finalize_NAME(...)block - Mapping rule: Mapping operations are placed inside
async function(finalize) blocks only - Helper functions: Use
inlinefunctions inside theprogramblock for reusable off-chain logic - When docs conflict: Prefer the complete language constraints in
aleo_smart_contracts
Quick Glossary
- Record: Encrypted private state object with mandatory
owner: address - Mapping: Public on-chain key-value state available only in
async function(finalize) blocks - Shielding: Public-to-private conversion from mapping balances into records
- Unshielding: Private-to-public conversion from records into mapping balances
- Nullifier: Public spent marker emitted when a record is consumed
Recipe 1: Simple On-Chain Counter
Use case: Track a public counter per address. Simplest possible finalize example.
Leo Source (src/main.leo)
program counter.aleo {
@noupgrade
async constructor() {}
mapping counts: address => u64;
async transition increment(public amount: u64) -> Future {
let caller: address = self.caller;
return finalize_increment(caller, amount);
}
async function finalize_increment(caller: address, amount: u64) {
let current: u64 = Mapping::get_or_use(counts, caller, 0u64);
Mapping::set(counts, caller, current + amount);
}
async transition get_count(public addr: address) -> Future {
return finalize_get_count(addr);
}
async function finalize_get_count(addr: address) {
let val: u64 = Mapping::get_or_use(counts, addr, 0u64);
// Value is queryable via mapping read
}
}
Test
leo build
# `increment` is async and updates mappings in finalize, so use execute.
leo execute increment 5u64 --broadcast
Recipe 2: Private Token with Public/Private Bridges
Use case: Token with both public and private balances, with conversion between them. This is the most important pattern in Aleo development.
program token.aleo {
@noupgrade
async constructor() {}
record Token {
owner: address,
amount: u64,
}
mapping account: address => u64;
// Mint tokens to a public balance
async transition mint_public(
public receiver: address,
public amount: u64,
) -> Future {
return finalize_mint_public(receiver, amount);
}
async function finalize_mint_public(receiver: address, amount: u64) {
let current_amount: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, current_amount + amount);
}
// Transfer tokens between public balances
async transition transfer_public(
public receiver: address,
public amount: u64,
) -> Future {
let sender: address = self.caller;
return finalize_transfer_public(sender, receiver, amount);
}
async function finalize_transfer_public(sender: address, receiver: address, amount: u64) {
let sender_amount: u64 = Mapping::get_or_use(account, sender, 0u64);
assert(sender_amount >= amount);
Mapping::set(account, sender, sender_amount - amount);
let receiver_amount: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, receiver_amount + amount);
}
// Mint tokens as a private record
transition mint_private(
receiver: address,
amount: u64,
) -> Token {
return Token {
owner: receiver,
amount: amount,
};
}
// Transfer tokens between private records (UTXO model)
transition transfer_private(
sender_token: Token,
receiver: address,
amount: u64,
) -> (Token, Token) {
let change: u64 = sender_token.amount - amount;
let to_receiver: Token = Token {
owner: receiver,
amount: amount,
};
let to_sender: Token = Token {
owner: sender_token.owner,
amount: change,
};
return (to_receiver, to_sender);
}
// Convert public balance to private record (shielding)
async transition transfer_public_to_private(
public receiver: address,
public amount: u64,
) -> (Token, Future) {
let new_record: Token = Token {
owner: receiver,
amount: amount,
};
let sender: address = self.caller;
return (new_record, finalize_pub_to_priv(sender, amount));
}
async function finalize_pub_to_priv(sender: address, amount: u64) {
let current: u64 = Mapping::get_or_use(account, sender, 0u64);
assert(current >= amount);
Mapping::set(account, sender, current - amount);
}
// Convert private record to public balance (unshielding)
async transition transfer_private_to_public(
sender_token: Token,
public receiver: address,
public amount: u64,
) -> (Token, Future) {
let change: u64 = sender_token.amount - amount;
let change_record: Token = Token {
owner: sender_token.owner,
amount: change,
};
return (change_record, finalize_priv_to_pub(receiver, amount));
}
async function finalize_priv_to_pub(receiver: address, amount: u64) {
let current: u64 = Mapping::get_or_use(account, receiver, 0u64);
Mapping::set(account, receiver, current + amount);
}
}
Test Flow
leo build
# Mint 1000 tokens publicly (executes finalize on-chain)
leo execute mint_public "aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8s7pyjh9" 1000u64 --broadcast
# Transfer 200 publicly (executes finalize on-chain)
leo execute transfer_public "aleo19y2eyc2cycvdqmycqam60l6uexvfj468xcet65jnnzc5pn8g9ufqg2clp2" 200u64 --broadcast
# Shield 100 tokens (public -> private record, executes finalize on-chain)
leo execute transfer_public_to_private "aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8s7pyjh9" 100u64 --broadcast
Recipe 3: Access-Controlled Mint
Use case: Only a specific admin address can mint tokens.
program admin_token.aleo {
@noupgrade
async constructor() {}
record Token {
owner: address,
amount: u64,
}
mapping total_supply: bool => u64;
// Only the designated admin can mint
async transition mint(receiver: address, amount: u64) -> (Token, Future) {
// Use self.signer for user-level auth (safe in cross-program context)
assert_eq(
self.signer,
aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8s7pyjh9
);
let token: Token = Token {
owner: receiver,
amount: amount,
};
return (token, finalize_mint(amount));
}
async function finalize_mint(amount: u64) {
let supply: u64 = Mapping::get_or_use(total_supply, true, 0u64);
Mapping::set(total_supply, true, supply + amount);
}
}
Recipe 4: Lottery with On-Chain Randomness
Use case: A simple lottery using ChaCha::rand_bool() in finalize.
program lottery.aleo {
@noupgrade
async constructor() {}
mapping jackpot: bool => u64;
mapping winners: u8 => address;
mapping winner_count: bool => u8;
// Enter the lottery
async transition play() -> Future {
let player: address = self.caller;
return finalize_play(player);
}
async function finalize_play(player: address) {
// Random winner selection
let won: bool = ChaCha::rand_bool();
if won {
let count: u8 = Mapping::get_or_use(winner_count, true, 0u8);
assert(count < 5u8); // Max 5 winners
Mapping::set(winners, count, player);
Mapping::set(winner_count, true, count + 1u8);
}
}
}
Recipe 5: Bounded Interest Calculation
Use case: Calculate compound interest with a fixed upper-bound loop.
program interest.aleo {
@noupgrade
async constructor() {}
inline calculate_interest(
principal: u64,
rate_bps: u64,
periods: u64,
) -> u64 {
let result: u64 = principal;
// Fixed upper bound loop — only compute for actual periods
for i: u64 in 0u64..100u64 {
if i < periods {
let interest: u64 = result * rate_bps / 10000u64;
result = result + interest;
}
}
return result;
}
transition compute_interest(
public principal: u64,
public rate_bps: u64,
public periods: u64,
) -> u64 {
assert(periods <= 100u64);
return calculate_interest(principal, rate_bps, periods);
}
}
Test
# Calculate interest on 1000 tokens at 5% (500 bps) for 10 periods
leo run compute_interest 1000u64 500u64 10u64
Recipe 6: Cross-Program Credit Transfer
Use case: A program that calls credits.aleo to transfer credits.
import credits.aleo;
program payment.aleo {
@noupgrade
async constructor() {}
mapping payments: field => u64;
async transition pay(public receiver: address, public amount: u64) -> Future {
// Transfer credits via the system program.
let payer: address = self.caller;
let transfer: Future = credits.aleo/transfer_public(receiver, amount);
let payment_id: field = BHP256::hash_to_field(payer);
return finalize_pay(transfer, payment_id, amount);
}
async function finalize_pay(transfer: Future, payment_id: field, amount: u64) {
transfer.await();
let total: u64 = Mapping::get_or_use(payments, payment_id, 0u64);
Mapping::set(payments, payment_id, total + amount);
}
}
Recipe 7: Signature Verification
Use case: Verify an off-chain signature on-chain.
program verifier.aleo {
@noupgrade
async constructor() {}
transition verify_message(
sig: signature,
signer_addr: address,
message: field,
) -> bool {
let is_valid: bool = signature::verify(sig, signer_addr, message);
assert(is_valid);
return is_valid;
}
}
Each recipe is complete and compilable. Use them as starting points for your applications.
Common Recipe Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
| Mapping compiler error | Mapping operation placed outside async function (finalize) | Move mapping read/write into a separate async function finalize_NAME(...) block |
| Missing finalize function | Transition returns Future but no matching async function exists | Add async function finalize_NAME(...) with the on-chain logic |
| Input format rejected by SDK/CLI | Missing type suffix such as u64 | Provide explicit typed literals like 100u64 |
| Authorization check fails in composed calls | Used self.caller for user auth | Use self.signer for user-level authorization checks |
self.caller used in finalize | self.caller is only available in the transition body | Capture self.caller in the transition and pass it as an argument to the finalize function |
| Unexpected transaction rejection in reveal or transfer | Missing precondition assertion or wrong commitment data | Validate preconditions and recompute commitments from original value and salt |
Security Notes
- Keep private values in records and avoid storing sensitive values in public mappings
- Use
self.signerfor user authentication in access-controlled recipes - Never include production private keys in copied examples or test fixtures
- Treat commit-reveal salts and record plaintext as sensitive off-chain data
Performance Notes
- Start from the smallest recipe that matches the task and only compose extra features when required
- Prefer
Poseidon2::hash_to_fieldfor hashing-heavy flows unless compatibility requires alternatives - Keep loops bounded and minimal because Leo loops are unrolled at compile time
- Use
get_or_useto avoid failed transactions on missing mapping keys
- Full language reference:
aleo_smart_contracts - Privacy patterns:
aleo_privacy_patterns - Deployment:
aleo_deployment - Testing:
aleo_testing
Agent SOP: Recipe Adaptation Workflow
- Pick the closest recipe to the requested functionality before writing new code
- Preserve canonical structure: records/structs, mapping rules,
async transition/async functionfinalize pattern, and typed inputs - Adapt incrementally and compile after each structural change
- Add negative checks for access control, underflow, and missing-state cases
- Run
leo test, then useleo executefor async/finalize flows andleo runfor pure transition helpers - If adaptation fails, map the failure to the error matrix and apply the exact fix before further edits