Codama Workflow
This repository uses Codama as the IDL and client-generation layer for Pina programs.
The flow has three stages:
- Generate Codama JSON from Rust programs (
pina idl). - Validate generated JSON against committed fixtures/tests.
- Render clients (JS and Dart with Codama renderers, Rust with
pina_codama_renderer, and standalone CPI crates withpina_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: regeneratescodama/idls/*.jsonfixtures and every client family for all examples by drivingpina generateonce per project. Each example’spina.tomlowns 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 toVariantAccountsunless#[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_instructionexists 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
processbody is read too: directassert_signer(),assert_writable(), andassert_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 asas_account::<T>()andwith_compact_account::<T, _>(). Writable inference also comes from mutable fields such as&'a mut AccountView. - A module-level helper function the
processbody 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_instructionor 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/idlsand thecodama/clientstree withpina 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.