A step-by-step guide to generating Anchor and Codama-compatible IDLs from raw Pinocchio programs without writing a single line of manual JSON.
If you've written a Solana program with Pinocchio, you already know the tradeoff you made:
- Raw performance
- Zero-dependency execution
- Automatic IDL generation
Anchor gives you an IDL for free.
Pinocchio doesn't because it's deliberately a library, not a framework.
An IDL (Interface Definition Language) is what allows TypeScript clients, explorers, and tools like Codama to understand your program's instructions, accounts, errors, and types without parsing your Rust source.
This guide walks through generating a fully Anchor- and Codama-compatible IDL using pinocchio-idl.
Why You Need This
Without a dedicated tool, Pinocchio developers usually end up choosing between two bad options:
- Hand-writing the IDL JSON
Every instruction or account change means updating JSON manually. This becomes painful almost immediately.
- Using Shank
Shank extracts annotations and emits JSON, but it doesn't generate useful program constants like:
DISCRIMINATORSPACE
It also doesn't produce native Codama output.
pinocchio-idl solves both problems.
It generates:
- Anchor-compatible IDLs
- Native Codama JSON
- Optional compile-time constants
- Optional validation guards
...all from lightweight Rust macros.
Step 1. Install the CLI
cargo install pinocchio-idl
This installs two binaries:
| Binary | Command |
|---|---|
| pinocchio-idl | pinocchio-idl generate |
| cargo-pinocchio-idl | cargo pinocchio-idl generate |
Both behave identically.
Verify the installation:
pinocchio-idl --version
Step 2. Add the Macro Crate
The CLI performs generation, but the macros mark your source code so the CLI knows what to extract.
Add the dependency:
# Cargo.toml
[dependencies]
pinocchio-idl-macros = "0.1.0"
Step 3. Annotate Account State
Every account that should appear inside your IDL receives #[p_state].
use pinocchio_idl_macros::p_state;
#[p_state]
pub struct Counter {
pub count: u64,
}
By itself, this is purely descriptive.
It simply tells the CLI:
"Include this struct in the generated IDL."
Nothing else is generated.
Automatically Generate Constants
If you also want the macro to inject useful constants:
#[p_state(inject)]
pub struct Escrow {
pub seed: u64,
pub maker: Pubkey,
pub mint_a: Pubkey,
pub mint_b: Pubkey,
pub receive: u64,
pub bump: u8,
}
The macro expands to something like:
impl Escrow {
pub const SPACE: usize = 8 + 32 + 32 + 32 + 8 + 1;
pub const DISCRIMINATOR: [u8; 8] =
[31, 213, 123, 187, 186, 22, 218, 155];
}
Without inject, no extra code is generated.
With inject, these constants become part of your compiled program.
Step 4. Annotate Your Instruction
Most of the IDL metadata lives on the instruction itself.
use pinocchio_idl_macros::p_instruction;
#[p_instruction(
id = 0,
accounts = [
payer(signer, mut),
counter(mut, state = Counter),
system_program
],
data = [
action: u8 = data[0]
]
)]
pub fn process_increment(
accounts: &mut [AccountView],
data: &[u8],
) -> ProgramResult {
// Your existing Pinocchio logic
Ok(())
}
Account Constraints
Each account entry can define constraints.
| Constraint | Syntax | Meaning |
|---|---|---|
| Writable | mut |
Account must be writable |
| Signer | signer |
Must sign the transaction |
| PDA | pda = [...] |
PDA verification |
| ATA | ata = [...] |
Associated token account |
| Linked State | state = StructName |
Connects account to #[p_state]
|
| Fixed Address | address = "..." |
Uses a known address |
Instruction Data
Instead of manual byte slicing:
data = [
action: u8 = data[0]
]
You explicitly describe the instruction layout.
The generator now knows both:
- the IDL schema
- how the instruction reads its arguments
Built-in Program Accounts
Certain account names are automatically recognized.
For example:
system_programtoken_programtoken_2022_programassociated_token_programrentclock
Their canonical addresses are inserted automatically.
No manual address specification required.
Important PDA Rule
Unlike Anchor, pinocchio-idl does not search for PDA bumps.
This is incorrect:
escrow(
mut,
pda = ["escrow", maker, seed],
state = Escrow
)
Correct:
escrow(
mut,
pda = ["escrow", maker, seed, bump],
state = Escrow
)
The bump must always be supplied explicitly.
This avoids runtime bump-search loops and reduces compute cost.
Compile-Time Security Injection (Optional)
If you enable:
#[p_instruction(inject, id = 0, accounts = [...])]
the macro inserts validation code before your handler executes.
Examples include:
if accounts.len() < 5 {
return Err(ProgramError::NotEnoughAccountKeys);
}
if !maker.is_signer() {
return Err(ProgramError::MissingRequiredSignature);
}
if !escrow.is_writable() {
return Err(ProgramError::MissingRequiredSignature);
}
This is ordinary Rust generated at compile time.
No runtime framework.
No dynamic dispatch.
No trait objects.
One Structural Requirement
Account extraction must happen contiguously at the top of your handler.
Don't interleave logging or other statements between account extraction.
For example:
Bad
let payer = ...;
msg!("hello");
let escrow = ...;
Keep extraction together before any other logic.
Step 5. Generate the IDL
From your project directory:
pinocchio-idl generate
Useful options:
pinocchio-idl generate \
--manifest-path path/to/Cargo.toml \
--out target/idl/my_program.idl.json \
--src path/to/src
Internally, the CLI:
- walks your source tree
- parses Rust using
syn - extracts annotated items
- writes the final JSON
No rustc invocation required.
Step 6. Generate Native Codama JSON
If your tooling expects Codama directly:
pinocchio-idl generate --format codama
Step 7. Keep Your IDL in Sync
Generate the IDL automatically in CI.
name: Generate IDL
on:
push:
branches: ["main"]
jobs:
generate-idl:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo install pinocchio-idl
- run: pinocchio-idl generate
- uses: actions/upload-artifact@v4
with:
name: program-idl
path: idl.json
Two More Useful Macros
#[p_error]
Generates the IDL's errors section.
#[p_error]
pub enum EscrowError {
/// The escrow has already been taken.
AlreadyTaken,
/// The offer amount is zero.
ZeroAmount,
}
#[p_constant]
Expose on-chain constants directly inside the IDL.
#[p_constant]
pub const MAX_ESCROW_DURATION: u64 =
60 * 60 * 24 * 30;
What You End Up With
A single idl.json containing:
- Instructions
- Accounts
- Types
- Errors
- Constants
...structured exactly like an Anchor-generated IDL, while keeping your program completely framework-free.
Feed that IDL into Codama and generate fully typed SDKs in TypeScript, Rust, or other supported languages—without paying Anchor's runtime or compute costs.
Conclusion
pinocchio-idl bridges one of the biggest gaps when building Solana programs with Pinocchio. You get the low-level control and performance benefits of a library while regaining the tooling ecosystem that an IDL unlocks for clients, explorers, and SDK generators.
If you're already writing Pinocchio programs, adding a few annotations can save you from maintaining manual JSON forever.
Top comments (0)