Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
name, description
| name | description |
|---|---|
| lez-framework-template | Use when working inside a project scaffolded with scaffold's lez-framework template — Anchor-on-Solana parallel for LEZ with |
LEZ-Framework Template Development
This skill activates when the agent is working inside a project scaffolded with lgs new <name> --template lez-framework. The template uses LEZ Framework for an ergonomic developer experience similar to Anchor on Solana. For driving the lgs CLI itself, use the lgs-cli skill.
When to Use
Identify a lez-framework project by all of:
scaffold.tomlcontainsframework = "lez-framework".crates/lez-client-gen/exists (host-side client generator crate).idl/directory exists with one<program>.jsonper program (e.g.idl/lez_counter.json).methods/guest/src/bin/<program>.rsandsrc/bin/run_<program>.rsexist (same convention as the default template).src/lib.rsdeclares#[lez_program] mod <program> { ... }with#[instruction]handlers.
If any of these are missing (especially framework = "lez-framework" in scaffold.toml), switch to lez-template instead.
Why This Template Exists
The default template is the bare LEZ standalone surface: you write everything (instruction dispatch, account derivation, IDL by hand, client stubs by hand). The lez-framework template is a declarative wrapper that:
- Eliminates instruction-dispatch boilerplate (
#[lez_program]/#[instruction]). - Annotates account constraints and PDA derivation declaratively (
#[account(…)]). - Generates IDL JSON at compile time (exposed as
PROGRAM_IDL_JSONand persisted underidl/). - Generates host-side client bindings under
src/generated/from the IDL.
It's the right choice when you'd otherwise be writing repetitive boilerplate. Drop down to default only for primitives the framework hasn't surfaced or for extreme guest-binary size constraints.
What This Template Produces
File-tree highlights from templates/lez-framework/:
<project>/
├── Cargo.toml # main workspace; includes crates/
├── rust-toolchain.toml
├── scaffold.toml # framework = "lez-framework"
├── .scaffold/
├── src/
│ ├── lib.rs # #[lez_program] mod ... + runner_support
│ ├── generated/ # client bindings (output of `lgs build client`)
│ └── bin/
│ └── run_lez_counter.rs # host-side runner with init/increment subcommands
├── crates/
│ └── lez-client-gen/ # host crate that turns idl/*.json → src/generated/
├── idl/
│ └── lez_counter.json # auto-generated IDL (don't hand-edit)
└── methods/
└── guest/src/bin/lez_counter.rs # risc0 guest binary
Macro Vocabulary
From templates/lez-framework/src/lib.rs — the lez_counter reference example:
use lez_framework::prelude::*;
use lez_framework::error::{LezError, LezResult};
use lez_framework_core::types::LezOutput;
use nssa_core::program::AccountPostState;
use nssa_core::account::AccountWithMetadata;
#[lez_program]
mod lez_counter {
#[allow(unused_imports)]
use super::*;
#[instruction]
pub fn initialize(
#[account(init, pda = literal("counter"))]
counter: AccountWithMetadata,
#[account(signer)]
authority: AccountWithMetadata,
) -> LezResult {
Ok(LezOutput::states_only(vec![
AccountPostState::new_claimed(counter.account.clone()),
AccountPostState::new(authority.account.clone()),
]))
}
#[instruction]
pub fn increment(
#[account(mut, pda = literal("counter"))]
counter: AccountWithMetadata,
#[account(signer)]
authority: AccountWithMetadata,
amount: u64,
) -> LezResult {
let mut counter_post = counter.account.clone();
counter_post.balance += amount as u128;
Ok(LezOutput::states_only(vec![
AccountPostState::new(counter_post),
AccountPostState::new(authority.account.clone()),
]))
}
}
| Annotation | Generates |
|---|---|
#[lez_program] mod <name> { … } |
Program-level scaffolding: instruction enum, dispatch, IDL constant PROGRAM_IDL_JSON. The mod name is the program name (must match methods/guest/src/bin/<name>.rs). |
#[instruction] pub fn <handler>(…) |
Instruction enum variant with PascalCase name (initialize → Initialize); discriminator computed from the name; argument schema (non-account params) is the variant payload. |
#[account(init, pda = literal("<seed>"))] |
Account claimed (zero-balance, default visibility) at the literal PDA; claim_if_default = true. |
#[account(mut, pda = literal("<seed>"))] |
Existing PDA account; mutable; claim_if_default = false. |
#[account(signer)] |
Authority account; auth = true. |
LezResult / Ok(LezOutput::states_only(vec![…])) |
Return type for instruction handlers. Use AccountPostState::new(…) for unchanged authorities and AccountPostState::new_claimed(…) when claiming a freshly-derived account. |
Non-account function parameters (e.g. amount: u64 on increment) become args in the IDL.
IDL Pipeline
The IDL is the contract between the program and any client. It's written to idl/<program>.json at compile time (visible in tests via PROGRAM_IDL_JSON). Schema (from idl/lez_counter.json):
{
"spec": "lssa-idl/0.1.0",
"metadata": { "name": "lez_counter", "version": "0.1.0" },
"program": { "name": "lez_counter" },
"instructions": [
{
"name": "initialize",
"variant": "Initialize",
"discriminator": [220, 59, 207, 236, 108, 250, 47, 100],
"accounts": [
{ "name": "counter", "ty": "AccountWithMetadata",
"auth": false, "claim_if_default": true, "mutable": false, "visibility": ["public"] },
{ "name": "authority", "ty": "AccountWithMetadata",
"auth": true, "claim_if_default": false, "mutable": false, "visibility": ["public"] }
],
"args": [],
"execution": { "private_owned": false, "public": true }
}
],
"errors": [],
"types": []
}
Regenerate the IDL whenever the program surface changes:
lgs build idl # writes idl/<program>.json from the macro-extracted PROGRAM_IDL_JSON
Look for Wrote IDL ... lines in the command output to confirm regeneration. Missing markers or empty IDL is a regression.
Client Generation
lgs build client # regenerates IDL first, then runs crates/lez-client-gen
# to produce host-side bindings under src/generated/
Client artefacts under src/generated/ reflect the current contents of idl/. Don't hand-edit them — treat the macro layer as the source of truth and regenerate.
Build / Deploy
For the LEZ template, lgs build automatically runs IDL regeneration and client generation as part of the pipeline (per DOGFOODING scenario L1). You usually don't need to call build idl / build client manually except when iterating on IDL alone.
lgs setup
lgs localnet start
lgs build # cargo build + IDL regen + client gen
lgs deploy # auto-discovers methods/guest/src/bin/lez_counter.rs
Reference Example: Running lez_counter
The runner at src/bin/run_lez_counter.rs exposes init and increment subcommands:
export NSSA_WALLET_HOME_DIR="$(pwd)/.scaffold/wallet"
lgs wallet -- account new public # capture the base58 account id
cargo run --bin run_lez_counter -- init --to <account-id>
cargo run --bin run_lez_counter -- increment --counter <account-id> --authority <account-id> --amount 5
Caveat (per DOGFOODING scenario L4): as of writing, the
run_lez_counterrunner containsTODOplaceholders for actual transaction submission. Don't be surprised if subcommands accept input but only print diagnostic messages without submitting. When transaction submission is implemented, follow the sameverification hint:pattern as default-template runners (lgs wallet -- account get --account-id <id>).
Differences vs. default Template
| Concern | default |
lez-framework |
|---|---|---|
| Instruction dispatch | hand-written | generated by #[lez_program] |
| Account derivation | hand-written | declarative #[account(…)] |
| IDL | none | auto-generated under idl/ |
| Client bindings | hand-written runners | generated under src/generated/ from IDL |
| Workspace | excludes methods/ |
includes crates/; methods/ still its own crate |
lgs build |
cargo build + auto-build methods/ |
adds IDL regen + client gen |
| Recommended for | low-level / size-critical zk programs | most projects (less boilerplate, type-safe clients) |
Common Gotchas
- Mod name must match the guest binary.
#[lez_program] mod lez_counterrequiresmethods/guest/src/bin/lez_counter.rs. Renaming one without the other breakslgs deploydiscovery. - Don't hand-edit
idl/*.json— the nextlgs build(orlgs build idl) overwrites it from the macro-derivedPROGRAM_IDL_JSON. Edit the source instead and regenerate. - Don't hand-edit
src/generated/— regenerated bylgs build client. - Account derivation order matters.
#[account(init, pda = …)]accounts that depend on others should appear after their inputs in the handler signature. - Visibility defaults to
public. If you need private execution, configureexecution.private_owned = true(currently driven by macro inputs not shown in the counter example). crates/workspace members. IfCargo.tomldoesn't includecrates/lez-client-gen,lgs build clientwill not find the generator. Check the workspacemembersarray.
Adding a New Instruction
- Add a
#[instruction] pub fn <name>(…) -> LezResult { … }inside the#[lez_program]mod insrc/lib.rs. Mirror the account-annotation patterns frominitialize/increment. lgs build idlto regenerateidl/<program>.json. Verify the new instruction appears with the expectedaccounts/args/discriminator.lgs build clientto regenerate host-side bindings.- Extend
src/bin/run_<program>.rswith a new subcommand that uses the regenerated client. - Test:
lgs build && lgs deploy && cargo run --bin run_<program> -- <new-subcommand> ….
Adding a New Program
A project can host multiple #[lez_program] modules, each one its own program with its own guest binary:
- New
methods/guest/src/bin/<new_program>.rs. - New
#[lez_program] mod <new_program> { … }insrc/lib.rs(or a sibling.rsfile pulled in viamod). - New
src/bin/run_<new_program>.rs. lgs buildregenerates IDL + clients across all programs;lgs deploydiscovers the new one automatically.
Key Rules
- The
#[lez_program] modname is the program name. Keep it in lockstep withmethods/guest/src/bin/<name>.rs. idl/andsrc/generated/are derived. Never hand-edit them; regenerate vialgs build idl/lgs build client.- Account annotations are the contract.
pda = literal(…),init,mut,signerflags drive both the IDL and runtime behavior. - Use
LezOutput::states_only(…)as the default success return; reach for richer variants only when needed. - Run
cargo testto dump the IDL — the test__lssa_idl_printprintsPROGRAM_IDL_JSONbetween--- LSSA IDL BEGIN/END ---markers, useful for debugging IDL regressions. - Don't bypass the framework for ad-hoc dispatch. If you find yourself writing manual instruction matching, drop down to the
defaulttemplate instead. NSSA_WALLET_HOME_DIRis required for directcargo run. Same as the default template.