DEV Community

Divine Igbinoba
Divine Igbinoba

Posted on

How to Create an IDL for your Pinocchio Solana Program in 2026

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:

  1. Hand-writing the IDL JSON

Every instruction or account change means updating JSON manually. This becomes painful almost immediately.

  1. Using Shank

Shank extracts annotations and emits JSON, but it doesn't generate useful program constants like:

  • DISCRIMINATOR
  • SPACE

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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]
]
Enter fullscreen mode Exit fullscreen mode

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_program
  • token_program
  • token_2022_program
  • associated_token_program
  • rent
  • clock

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
)
Enter fullscreen mode Exit fullscreen mode

Correct:

escrow(
    mut,
    pda = ["escrow", maker, seed, bump],
    state = Escrow
)
Enter fullscreen mode Exit fullscreen mode

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 = [...])]
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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 = ...;
Enter fullscreen mode Exit fullscreen mode

Keep extraction together before any other logic.


Step 5. Generate the IDL

From your project directory:

pinocchio-idl generate
Enter fullscreen mode Exit fullscreen mode

Useful options:

pinocchio-idl generate \
    --manifest-path path/to/Cargo.toml \
    --out target/idl/my_program.idl.json \
    --src path/to/src
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

#[p_constant]

Expose on-chain constants directly inside the IDL.

#[p_constant]
pub const MAX_ESCROW_DURATION: u64 =
    60 * 60 * 24 * 30;
Enter fullscreen mode Exit fullscreen mode

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)