Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Codama Workflow

This repository uses Codama as the IDL and client-generation layer for Pina programs.

The flow has three stages:

  1. Generate Codama JSON from Rust programs (pina idl).
  2. Validate generated JSON against committed fixtures/tests.
  3. Render clients (JS with Codama renderers, Rust with pina_codama_renderer).

In This Repository

Generate and validate the whole workspace flow with devenv scripts:

# Generate Codama IDLs for all examples.
codama:idl:all

# Generate Rust + JS clients.
codama:clients:generate

# Generate IDLs + Rust/JS clients in one command.
pina codama generate

# Run the complete Codama pipeline.
codama:test

# Run IDL fixture drift + validation checks used by CI.
test:idl

# Run Quasar SVM generated-client e2e checks alongside LiteSVM.
pnpm run test:quasar-svm

Supporting scripts:

  • scripts/generate-codama-idls.sh: regenerates codama/idls/*.json fixtures for all examples.
  • scripts/verify-codama-idls.sh: regenerates IDLs/clients, verifies fixtures via Rust and JS tests, and enforces deterministic no-diff output.

In a Separate Project

You do not need to copy this entire repository to use Codama with Pina.

1. Generate IDL from your program

pina idl --path ./programs/my_program --output ./idls/my_program.json

2. Generate JS clients with Codama

pnpm add -D codama @codama/renderers-js
import { renderVisitor as renderJsVisitor } from "@codama/renderers-js";
import { createFromFile } from "codama";

const codama = await createFromFile("./idls/my_program.json");
await codama.accept(renderJsVisitor("./clients/js/my_program"));

3. Generate Pina-style Rust clients (optional)

This repository ships crates/pina_codama_renderer, which emits Rust models aligned with Pina’s discriminator-first, fixed-size POD layouts.

cargo run --manifest-path ./crates/pina_codama_renderer/Cargo.toml -- \
  --idl ./idls/my_program.json \
  --output ./clients/rust

You can pass multiple --idl flags or --idl-dir.

Renderer Constraints

pina_codama_renderer intentionally targets fixed-size layouts. Unsupported patterns produce explicit errors (for example variable-length strings/bytes, unsupported endian/number forms, and non-fixed arrays).

Extractor coverage

The extractor currently supports these dispatch shapes:

  • Canonical routed arms: Variant => Accounts::try_from(accounts)?.process(data)
  • Grouped routed arms: VariantA | VariantB => SharedAccounts::try_from(accounts)?.process(data)
  • Accountless arms: Variant => { let _ = Payload::try_from_bytes(data)?; Ok(()) }
  • Instruction-only fallback: if Pina finds #[instruction] structs but no recognizable dispatch map, it still emits zero-account instruction nodes from those payload structs.

Keep in mind:

  • Account metadata is only inferred for routed Accounts::try_from(accounts) arms.
  • Signer/PDA/default-account inference still depends on direct self.field.assert_*() chains inside impl ProcessAccountInfos.
  • Writable inference comes from either direct assert_writable() chains or mutable #[derive(Accounts)] fields such as &'a mut AccountView.
  • If you hide routing or validation behind helper layers, instruction nodes may still exist, but account metadata becomes less complete.

Source shapes that extract cleanly

Use the same program shapes described in crates/pina_cli/rules.md to keep IDL extraction predictable.

Multi-file layout

#![allow(unused)]
fn main() {
// src/lib.rs
use pina::*;

mod accounts;
mod instructions;
mod pda;
mod state;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
}

Canonical dispatch

#![allow(unused)]
fn main() {
#[cfg(feature = "bpf-entrypoint")]
pub mod entrypoint {
	use super::*;

	nostd_entrypoint!(process_instruction);

	pub fn process_instruction(
		program_id: &Address,
		accounts: &mut [AccountView],
		data: &[u8],
	) -> ProgramResult {
		let ix: MyInstruction = parse_instruction(program_id, &ID, data)?;

		// Prefer one routed arm per variant when possible.
		match ix {
			MyInstruction::Initialize => InitializeAccounts::try_from(accounts)?.process(data),
			MyInstruction::Update => UpdateAccounts::try_from(accounts)?.process(data),
		}
	}
}
}

Grouped dispatch with shared accounts

#![allow(unused)]
fn main() {
match ix {
	MyInstruction::Initialize => InitializeAccounts::try_from(accounts)?.process(data),
	MyInstruction::Toggle | MyInstruction::Update => {
		UpdateAccounts::try_from(accounts)?.process(data)
	}
}
}

Accountless dispatch

#![allow(unused)]
fn main() {
match ix {
	MyInstruction::Ping => {
		let _ = PingInstruction::try_from_bytes(data)?;
		Ok(())
	}
	MyInstruction::Initialize => InitializeAccounts::try_from(accounts)?.process(data),
}
}

Validation chains

#![allow(unused)]
fn main() {
impl<'a> ProcessAccountInfos<'a> for InitializeAccounts<'a> {
	fn process(self, data: &[u8]) -> ProgramResult {
		let args = InitializeInstruction::try_from_bytes(data)?;
		let seeds = my_seeds!(self.authority.address().as_ref(), args.bump);

		self.authority.assert_signer()?;
		self.system_program.assert_address(&system::ID)?;
		self.token_program.assert_address(&token::ID)?;
		self.ata_program
			.assert_address(&associated_token_account::ID)?;
		self.state
			.assert_empty()?
			.assert_writable()?
			.assert_seeds_with_bump(seeds, &ID)?;

		Ok(())
	}
}
}

PDA seed helpers

#![allow(unused)]
fn main() {
const MY_SEED: &[u8] = b"my";

#[macro_export]
macro_rules! my_seeds {
	($authority:expr) => {
		&[MY_SEED, $authority]
	};
	($authority:expr, $bump:expr) => {
		&[MY_SEED, $authority, &[$bump]]
	};
}
}

Discriminators and account layouts

#![allow(unused)]
fn main() {
#[discriminator]
pub enum MyInstruction {
	Initialize = 0,
	Update = 1,
}

#[discriminator]
pub enum MyAccountType {
	MyState = 1,
}

#[instruction(discriminator = MyInstruction, variant = Initialize)]
pub struct InitializeInstruction {
	pub bump: u8,
}

#[instruction(discriminator = MyInstruction, variant = Update)]
pub struct UpdateInstruction {
	pub value: PodU64,
}

#[account(discriminator = MyAccountType)]
pub struct MyState {
	pub bump: u8,
	pub value: PodU64,
}
}

For the full checklist and rationale, see crates/pina_cli/rules.md.

CI Coverage

test:idl treats the generated IDL as an API contract. It checks that:

  • every example regenerates deterministically into codama/idls, codama/clients/js, and codama/clients/rust
  • generated JSON passes Codama’s JS validator
  • generated JS clients typecheck
  • generated Rust clients compile
  • for every example, generated instruction/account/error counts match the source declarations:
    • #[instruction]
    • #[account]
    • #[error]

That last count-parity check is important because it catches silent extraction regressions where a program still produces valid JSON, but one or more instruction surfaces disappear.