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 and Dart with Codama renderers, Rust with pina_codama_renderer, and standalone CPI crates with pina_cpi_renderer).

In This Repository

Generate and validate the whole workspace flow with devenv scripts:

# Generate IDLs and clients for all examples.
codama:idl:all

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

# Generate one project's configured clients.
pina generate

# Run the complete generation and validation 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-pina-clients.sh: regenerates codama/idls/*.json fixtures and every client family for all examples by driving pina generate once per project. Each example’s pina.toml owns its IDL and client output paths.
  • scripts/verify-pina-clients.sh: regenerates IDLs/clients, verifies fixtures and generated clients with Rust, JS, and Dart tests, and enforces deterministic no-diff output (including untracked files).

The generated Dart package lives in codama/clients/dart. It exposes one package-root library per example, pins dependency resolution in pubspec.lock, and checks all 26 example IDLs as a single inventory. CI runs dart format, dart analyze --fatal-infos, and dart test over the checked-in output.

Generation is driven entirely by the Pina CLI. pina generate discovers a project through its pina.toml, refreshes its IDL, and renders only the configured client ecosystems, which keeps the repository dogfooding the same command that users run. Generating a whole repository means running the command once per project.

For project-aware generation, [clients] in pina.toml controls mode (auto, create, update, or overwrite) and scaffold, with optional overrides under [clients.cpi], [clients.rust], [clients.typescript], and [clients.dart]. Dart is also the Flutter target. Update mode replaces only generated sources, so user-owned manifests and entrypoints can be customized without being rewritten. See Project Configuration for the complete schema.

Compute unit budgets

Every example commits a compute-units.json recorded by pina test --record-compute-units: the most compute units each instruction consumed in a successful Surfpool simulation. IDL generation attaches each measurement and the limit derived from it as a pinaComputeUnits plugin node on the instruction ({ "measured": 379, "limit": 800 }). Codama’s validators and the upstream JavaScript and Dart renderers carry plugin nodes through untouched, so the IDLs stay standard Codama.

The Rust renderer turns each plugin into <NAME>_MEASURED_COMPUTE_UNITS and <NAME>_COMPUTE_UNIT_LIMIT constants plus a crate-level set_compute_unit_limit_instruction. Pina’s post-processing adds the same constants and a get<Program>ComputeUnitLimit(instructions) helper to the TypeScript and Dart clients, and the generated CLIs request each instruction’s limit automatically. An instruction no successful test sent has no measurement, and its clients request the runtime default. See compute unit limits for the formula and the staleness warning.

After changing an example, record again before regenerating so its limits describe the current program:

pina test --record-compute-units --project examples/counter_program
pina generate --project examples/counter_program --npx node

Solana Kit dependencies

Pina pins codama-renderers-dart@0.5.6, which includes upstream support for pre/post-offset collection length codecs, and uses Solana Kit Dart packages at ">=0.10.0 <1.0.0" — the range the generated sources are verified against. This preserves shared compact headers with multiple dynamic tails in addition to schema normalization, package exports, discriminator enforcement, exact instruction decoding, capacity-aware account decoding, fixed-capacity overflow rejection, canonical boolean, option, and UTF-8 codecs, and wide-enum support. No renderer patch or renderer-specific dependency override is required.

The generated-client contract suite verifies those behaviors directly. Dependency upgrades must continue to pass the same byte-level contracts without patches, Git overrides, or generated-source rewrites.

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"));

Generated clients are an untrusted boundary. The checked-in contract suite requires encoders to reject fixed and compact capacity overflow. Decoders enforce discriminators, declared capacities, canonical boolean and option tags, and strict UTF-8. They do not enforce exact top-level lengths everywhere: TypeScript account and instruction decoders accept trailing bytes, and Dart account decoders do too, while the on-chain try_from_bytes requires an exact length. A renderer version that cannot satisfy those contracts is rejected instead of producing a client with a different wire format.

Decoding is not attribution. No generated decoder checks which program owns an account, so compare the fetched account’s owner with the program address before trusting decoded state, or fetch a PDA through its generated fetch…FromSeeds helper. Program-level event parsers (parse<Program>EventsFromLogs in TypeScript and Dart) take a transaction’s complete, ordered logs and decode a Program data: line only while the program is the innermost invocation, because any other program, including one it calls through CPI, can log bytes that start with the same discriminator. The per-event parse<Event>FromLog helpers decode a single line without that attribution. Each earlier version of a versioned event is its own <Event>V<n> event node in the IDL with its own codec; the program-level parser routes every record to the event for its discriminator and version, and throws on a version no generated event describes instead of misreading it.

3. Generate Dart clients with Codama

pina generate renders the same IDLs into the checked-in Dart package at codama/clients/dart. Each program has a package-root entrypoint, for example:

import 'package:pina_codama_clients/profile_program.dart';

Run codama:test to resolve the committed lockfile, format the generated Dart, analyze it with fatal infos, and execute the byte-level contract tests.

4. Generate Pina-style Rust clients (optional)

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

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

You can pass multiple --idl flags or --idl-dir. Add --no-scaffold to emit only src/generated; use --mode overwrite for an intentional clean regeneration.

Renderer constraints

pina_codama_renderer supports fixed layouts and Pina’s bounded compact-account grammar. It rejects unbounded collections, unsupported dynamic nesting, unsupported endian or number forms, and ambiguous layouts.

Extractor coverage

The extractor currently supports these dispatch shapes:

  • Generated dispatch: an #[discriminator(entrypoint)] enum, whose variants route to VariantAccounts unless #[dispatch(accounts = OtherAccounts)] overrides them
  • Canonical routed arms: Variant => Accounts::try_from((program_id, accounts))?.process(data)
  • Grouped routed arms: VariantA | VariantB => SharedAccounts::try_from((program_id, accounts))?.process(data)
  • Versioned routed arms: Variant => Instruction::process_versioned(Accounts::try_from((program_id, accounts))?, data), the form a hand-written dispatcher uses for an instruction that keeps its migration envelope
  • Accountless arms: Variant => { let _ = Payload::try_from_bytes(data)?; Ok(()) }
  • Accountless entrypoint fallback: if a single process_instruction exists but has no recognizable dispatch map, Pina emits zero-account instruction nodes from the declared payload structs.

Keep in mind:

  • Account metadata is inferred from the Accounts::try_from((program_id, accounts)) conversion an arm performs. The conversion is read wherever the arm performs it, including when it is bound to a local first. An arm that converts into two different structs has no single account layout and is emitted without accounts.
  • Signer, writable, and known default-account metadata can be declared with #[pina(validate(...))] on #[derive(Accounts)] fields.
  • The process body is read too: direct assert_signer(), assert_writable(), and assert_address() chains, the PDA validators and loaders #[pda] generates (assert_seeds, assert_stored_bump, load_pda, load_checked_pda, with_stored_bump_pda, with_checked_pda, and their mutable forms), PDA creation builders, and typed loads such as as_account::<T>() and with_compact_account::<T, _>(). Writable inference also comes from mutable fields such as &'a mut AccountView.
  • A module-level helper function the process body passes an account field to is analysed as part of that body, up to four calls deep. Methods, associated functions, and helpers whose name is declared more than once are not followed.
  • A field loaded as a #[pda] account type belongs to that PDA. Generated clients derive its default address only when the processor pins the address: it validates the account against its seeds, or every seed of the PDA is a constant. A field inferred as a PDA must resolve to a declared #[pda]; generation fails instead of emitting an incomplete link.
  • If you hide routing or validation behind method calls or closures, instruction nodes may still exist, but account metadata becomes less complete.
  • Multiple files containing process_instruction or an #[discriminator(entrypoint)] enum, malformed or unresolved #[pda] attributes, missing package names, and missing unconditional modules are rejected as ambiguous or incomplete inputs.

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((program_id, accounts))?.process(data)
			}
			MyInstruction::Update => {
				UpdateAccounts::try_from((program_id, accounts))?.process(data)
			}
		}
	}
}
}

Grouped dispatch with shared accounts

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

The equivalent client-visible constraints can be declared next to the fields:

#![allow(unused)]
fn main() {
#[derive(Accounts)]
pub struct InitializeAccounts<'a> {
	#[pina(validate(signer))]
	pub authority: &'a AccountView,

	#[pina(validate(writable))]
	pub state: &'a AccountView,

	#[pina(validate(program = system::ID))]
	pub system_program: &'a AccountView,
}
}

Value bounds, owners, data lengths, relationships, and custom hooks remain runtime-only because Codama does not represent them as instruction account metadata.

PDA seed helpers

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

#[macro_export]
macro_rules! my_seeds {
	($authority:expr) => {
		&[SEED_MY, $authority]
	};
	($authority:expr, $bump:expr) => {
		&[SEED_MY, $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::Initialize)]
pub struct InitializeInstruction {
	pub bump: u8,
}

#[instruction(discriminator = MyInstruction::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 and the codama/clients tree with pina generate
  • generated JSON passes Codama’s JS validator
  • generated JS clients typecheck
  • generated Rust, CPI, and Rust CLI clients compile, and the Rust CLI crates pass their tests
  • generated Dart clients resolve with the lockfile, format cleanly, pass static analysis, and pass codec contract tests
  • 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.