A lightweight, zero-overhead framework for building Solana programs with Pinocchio.
Solzempic provides the structure and safety of a framework without the bloat. It implements the Action pattern (Build, Validate, Execute) and provides type-safe account wrappers while maintaining full control over compute unit usage.
Anchor is excellent for getting started, but comes with significant overhead:
- Magic discriminators and automatic deserialization add ~2000+ CUs per instruction
- IDL generation bloats binary size
- Implicit borsh serialization prevents zero-copy optimizations
- Hard to reason about exactly what code is generated
Vanilla Pinocchio gives maximum control, but requires writing boilerplate:
- Account validation logic repeated across instructions
- No structured pattern for instruction processing
- Easy to forget security checks
Solzempic provides just enough structure to eliminate boilerplate while maintaining zero overhead:
+------------------+---------------+------------------+
| Anchor | Solzempic | Vanilla Pinocchio|
+------------------+---------------+------------------+
| High abstraction | Right balance | No abstraction |
| Hidden costs | Explicit | Explicit |
| Magic macros | Thin macros | No macros |
| ~5000 CU/instr | ~100 CU/instr | ~100 CU/instr |
+------------------+---------------+------------------+
- Zero-overhead abstractions: All wrappers compile to the same code you'd write by hand
- Action pattern: Structured Build -> Validate -> Execute flow for every instruction
- Type-safe account wrappers:
AccountRef<T>,AccountRefMut<T>with ownership validation - Program-specific Framework trait: Configure your program ID once, use everywhere
- Validated program accounts:
SystemProgram,TokenProgram,Signeretc. with compile-time guarantees - Attribute macros:
#[instruction],#[params], and#[SolzempicEntrypoint]for ergonomic dispatch - IDL generation: Generate Anchor-compatible IDL from Rust decorators for SDK generation
no_stdcompatible: Works in constrained Solana runtime environment
┌─────────────────────────────────────────────────────────────┐
│ SOLZEMPIC FRAMEWORK │
└─────────────────────────────────────────────────────────────┘
│
┌────────────────────────────┼────────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Framework │ │ Action │ │ Account │
│ Trait │ │ Pattern │ │ Wrappers │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
│ ┌───────┴───────┐ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ define_ │ │ build() │ │ validate() │ │ AccountRef │
│ framework! │ │ │ │ │ │ AccountRefMut│
│ │ │ Accounts + │ │ Invariants │ │ ShardRef- │
│ Creates: │ │ params from │ │ PDA checks │ │ Context │
│ - Solzempic │ │ raw bytes │ │ Ownership │ └──────────────┘
│ - AccountRef │ └──────────────┘ └──────────────┘ │
│ - AccountRef │ │ │ │
│ Mut │ └───────┬───────┘ │
└──────────────┘ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ execute() │◄────────────────┘
│ │ │
└─────────────────►│ State changes│
│ CPI calls │
│ Token xfers │
└──────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ PROGRAM WRAPPERS │
├─────────────────┬─────────────────┬─────────────────┬──────────────┤
│ SystemProgram │ TokenProgram │ Signer │ Sysvars │
│ AtaProgram │ Mint │ Payer │ Clock │
│ AltProgram │ TokenAccount │ MutSigner │ Rent │
│ Lut │ Vault │ │ SlotHashes │
└─────────────────┴─────────────────┴─────────────────┴──────────────┘
Add to your Cargo.toml:
[dependencies]
solzempic = { version = "0.1" }
pinocchio = { version = "0.7" }
bytemuck = { version = "1.14", features = ["derive"] }In your program's lib.rs, use the #[SolzempicEntrypoint] attribute:
#![no_std]
use solzempic::SolzempicEntrypoint;
#[SolzempicEntrypoint("YourProgramId111111111111111111111111111")]
pub enum MyInstruction {
Initialize = 0,
Increment = 1,
}This single attribute generates:
ID: Pubkeyconstant andid() -> &'static PubkeyfunctionAccountRef<'a, T>,AccountRefMut<'a, T>,ShardRefContext<'a, T>,ShardRefMutContext<'a, T>type aliases#[repr(u8)]on the enumTryFrom<u8>and dispatch methods- The program entrypoint
#[solzempic::account(discriminator = 1)]
pub struct Counter {
pub discriminator: [u8; 8],
pub owner: Pubkey,
pub count: u64,
}The #[account(discriminator = N)] macro automatically:
- Adds
#[repr(C)],Pod,Zeroablederives - Implements
LoadableandAccounttraits - Generates
LEN,DISCRIMINATOR, anddiscriminator()method
Use the #[instruction] attribute on an impl block:
use solzempic::{instruction, params, Signer, ValidatedAccount};
use pinocchio::{AccountView, program_error::ProgramError, ProgramResult};
use solana_address::Address;
#[params]
pub struct IncrementParams {
pub amount: u64,
}
pub struct Increment<'a> {
pub counter: AccountRefMut<'a, Counter>,
pub owner: Signer<'a>,
}
#[instruction(IncrementParams)]
impl<'a> Increment<'a> {
fn build(accounts: &'a [AccountView], _params: &IncrementParams) -> Result<Self, ProgramError> {
if accounts.len() < 2 {
return Err(ProgramError::NotEnoughAccountKeys);
}
Ok(Self {
counter: AccountRefMut::load(&accounts[0])?,
owner: Signer::wrap(&accounts[1])?,
})
}
fn validate(&self, _program_id: &Address, _params: &IncrementParams) -> ProgramResult {
// Verify owner matches counter's owner
if self.owner.key() != &self.counter.get().owner {
return Err(ProgramError::IllegalOwner);
}
Ok(())
}
fn execute(&self, _program_id: &Address, params: &IncrementParams) -> ProgramResult {
self.counter.get_mut().count += params.amount;
Ok(())
}
}The #[SolzempicEntrypoint] macro generates everything needed. Your process_instruction is simply:
fn process_instruction(
program_id: &Address,
accounts: &[AccountView],
data: &[u8],
) -> ProgramResult {
MyInstruction::process(program_id, accounts, data)
}Every instruction follows the same three-phase pattern:
┌─────────────────────────────────────────────────────────────────────────┐
│ ACTION LIFECYCLE │
└─────────────────────────────────────────────────────────────────────────┘
│
┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ BUILD │ │ VALIDATE │ │ EXECUTE │
├───────────────────┤ ├───────────────────┤ ├───────────────────┤
│ • Deserialize │ │ • Check invariants│ │ • Modify state │
│ parameters │ │ • Verify PDAs │ │ • Transfer tokens │
│ • Load accounts │ ──► │ • Check ownership │ ──► │ • Create accounts │
│ • Wrap programs │ │ • Validate ranges │ │ • Emit events │
│ • Early validation│ │ • Business rules │ │ • CPI calls │
└───────────────────┘ └───────────────────┘ └───────────────────┘
│ │ │
│ FAIL FAST │ PURE CHECKS │ SIDE EFFECTS
│ (bad accounts = error) │ (no state changes) │ (point of no return)
▼ ▼ ▼
Phase 1: Build
- Extract parameters from instruction data (zero-copy via
parse_params) - Load and validate account types (
AccountRef::load,AccountRefMut::load) - Wrap program accounts (
Signer::wrap,TokenProgram::wrap) - Fail fast on structural errors
Phase 2: Validate
- Check business logic invariants
- Verify PDA derivations if needed
- Validate numerical ranges and relationships
- No state mutations allowed
Phase 3: Execute
- Perform all state changes
- Execute token transfers
- Make CPI calls
- This is the "point of no return"
pub struct AccountRef<'a, T: Loadable, F: Framework> {
pub info: &'a AccountInfo,
data: &'a [u8],
// ...
}
impl<'a, T: Loadable, F: Framework> AccountRef<'a, T, F> {
/// Load with full validation (ownership + discriminator)
pub fn load(info: &'a AccountInfo) -> Result<Self, ProgramError>;
/// Load without ownership check (for cross-program reads)
pub fn load_unchecked(info: &'a AccountInfo) -> Result<Self, ProgramError>;
/// Get typed reference to account data
pub fn get(&self) -> &T;
/// Check if account is a PDA with given seeds
pub fn is_pda(&self, seeds: &[&[u8]]) -> (bool, u8);
}impl<'a, T: Loadable, F: Framework> AccountRefMut<'a, T, F> {
/// Load with validation (ownership + discriminator + is_writable)
pub fn load(info: &'a AccountInfo) -> Result<Self, ProgramError>;
/// Get typed reference
pub fn get(&self) -> &T;
/// Get mutable typed reference
pub fn get_mut(&mut self) -> &mut T;
/// Reload after CPI (updates internal data pointer)
pub fn reload(&mut self);
}
impl<'a, T: Initializable, F: Framework> AccountRefMut<'a, T, F> {
/// Initialize a new account (writes discriminator, zeros rest)
pub fn init(info: &'a AccountInfo) -> Result<Self, ProgramError>;
/// Initialize if uninitialized, otherwise load
pub fn init_if_needed(info: &'a AccountInfo) -> Result<Self, ProgramError>;
/// Initialize a PDA account (create via CPI + initialize)
pub fn init_pda(
info: &'a AccountInfo,
payer: &AccountInfo,
system_program: &AccountInfo,
seeds: &[&[u8]],
space: usize,
) -> Result<Self, ProgramError>;
}For sharded data structures that need read-only access to prev/current/next:
pub struct ShardRefContext<'a, T: Loadable, F: Framework> {
pub prev: AccountRef<'a, T, F>,
pub current: AccountRef<'a, T, F>,
pub next: AccountRef<'a, T, F>,
}
impl<'a, T: Loadable, F: Framework> ShardRefContext<'a, T, F> {
pub fn new(prev: &'a AccountInfo, current: &'a AccountInfo, next: &'a AccountInfo) -> Result<Self, ProgramError>;
pub fn current(&self) -> &T;
pub fn prev(&self) -> &T;
pub fn next(&self) -> &T;
}For sharded data structures that need mutable access to prev/current/next:
pub struct ShardRefMutContext<'a, T: Loadable, F: Framework> {
pub prev: AccountRefMut<'a, T, F>,
pub current: AccountRefMut<'a, T, F>,
pub next: AccountRefMut<'a, T, F>,
}
impl<'a, T: Loadable, F: Framework> ShardRefMutContext<'a, T, F> {
pub fn new(prev: &'a AccountInfo, current: &'a AccountInfo, next: &'a AccountInfo) -> Result<Self, ProgramError>;
pub fn from_loaded(prev: AccountRefMut, current: AccountRefMut, next: AccountRefMut) -> Self;
pub fn current_mut(&mut self) -> &mut T;
pub fn prev_mut(&mut self) -> &mut T;
pub fn next_mut(&mut self) -> &mut T;
pub fn all_mut(&mut self) -> (&mut T, &mut T, &mut T);
}Common use cases for shard contexts:
- Orderbooks: Orders may need to move between price-range shards
- Linked lists: Insertions/deletions update prev/next pointers
- Rebalancing: Moving data between under/over-utilized shards
Solzempic provides comprehensive PDA support through the account wrappers:
Both AccountRef and AccountRefMut implement the is_pda() method via the AsAccountRef trait:
let seeds = &[b"user", owner.key().as_ref()];
let (is_valid, bump) = account.is_pda(seeds);
if !is_valid {
return Err(ProgramError::InvalidSeeds);
}
// Store bump for later use in CPI signing
let full_seeds = &[b"user", owner.key().as_ref(), &[bump]];Performance note: PDA derivation costs ~2000 CUs. If you validate frequently, consider storing the bump in your account data and using a simple key comparison instead.
Use AccountRefMut::init_pda() to create and initialize a PDA in one operation:
// Derive PDA and get bump
let (_, bump) = Pubkey::find_program_address(
&[b"market", base_mint.as_ref(), quote_mint.as_ref()],
&program_id,
);
// Include bump in seeds
let seeds: &[&[u8]] = &[
b"market",
base_mint.as_ref(),
quote_mint.as_ref(),
&[bump],
];
// Create PDA and initialize with discriminator
let mut market: AccountRefMut<Market> = AccountRefMut::init_pda(
market_account,
payer.info(),
system_program.info(),
seeds,
Market::LEN,
)?;
// Set initial values
market.get_mut().admin = *admin.key();For lower-level control, use the create_pda_account() function:
use solzempic::create_pda_account;
// Create without initialization
create_pda_account(payer, new_account, &program_id, space, seeds)?;The AsAccountRef trait provides a common interface for PDA validation on both read-only and writable accounts:
pub trait AsAccountRef<'a, T: Loadable, F: Framework> {
fn info(&self) -> &'a AccountView;
fn address(&self) -> &Address;
fn get(&self) -> &T;
fn is_pda(&self, seeds: &[&[u8]]) -> (bool, u8);
}This allows generic code to work with either AccountRef or AccountRefMut:
fn validate_pda<'a, T, F, A>(account: &A, seeds: &[&[u8]]) -> ProgramResult
where
T: Loadable,
F: Framework,
A: AsAccountRef<'a, T, F>,
{
let (is_valid, _bump) = account.is_pda(seeds);
if !is_valid {
return Err(ProgramError::InvalidSeeds);
}
Ok(())
}All program and sysvar accounts validate their identity on construction:
// Programs
let system = SystemProgram::wrap(&accounts[0])?; // Validates key == 11111...
let token = TokenProgram::wrap(&accounts[1])?; // Validates SPL Token or Token-2022
let ata = AtaProgram::wrap(&accounts[2])?; // Validates ATA program
// Signers
let signer = Signer::wrap(&accounts[3])?; // Validates is_signer flag
let payer = Payer::wrap(&accounts[4])?; // Alias for Signer
// Sysvars
let clock = ClockSysvar::wrap(&accounts[5])?; // Validates Clock sysvar ID
let rent = RentSysvar::wrap(&accounts[6])?; // Validates Rent sysvar ID
// Token accounts
let mint = Mint::wrap(&accounts[7])?; // Validates token program ownership
let token = TokenAccountRefMut::load(&accounts[8])?; // Validates token account
let token_account = TokenAccountRefMut::load(&accounts[9])?;The Framework trait allows account wrappers to know your program's ID without passing it everywhere:
pub trait Framework {
const PROGRAM_ID: Pubkey;
}
// define_framework! generates:
pub struct Solzempic;
impl Framework for Solzempic {
const PROGRAM_ID: Pubkey = YOUR_PROGRAM_ID;
}
// Which allows:
AccountRefMut::<MyAccount>::load(&account)? // Automatically checks owner == YOUR_PROGRAM_IDThe main entrypoint attribute that generates everything needed for your program:
#[SolzempicEntrypoint("Your11111111111111111111111111111111111111")]
pub enum MyInstruction {
Initialize = 0,
Transfer = 1,
Close = 2,
}
// Generates:
// - pub const ID: Address = ...
// - pub fn id() -> &'static Address
// - pub type AccountRef<'a, T> = ...
// - pub type AccountRefMut<'a, T> = ...
// - pub type ShardRefContext<'a, T> = ...
// - pub type ShardRefMutContext<'a, T> = ...
// - #[repr(u8)] on the enum
// - TryFrom<u8> for MyInstruction
// - MyInstruction::process() dispatch method
// - The program entrypointImplements the InstructionParams and Instruction traits on an impl block:
pub struct Transfer<'a> {
pub from: AccountRefMut<'a, TokenAccount>,
pub to: AccountRefMut<'a, TokenAccount>,
pub authority: Signer<'a>,
}
#[instruction(TransferParams)]
impl<'a> Transfer<'a> {
fn build(accounts: &'a [AccountView], params: &TransferParams) -> Result<Self, ProgramError> { ... }
fn validate(&self, program_id: &Address, params: &TransferParams) -> ProgramResult { ... }
fn execute(&self, program_id: &Address, params: &TransferParams) -> ProgramResult { ... }
}
// Generates InstructionParams and Instruction trait implementations
// Also generates IDL_NAME and IDL_PARAMS constants for IDL generationDefines instruction parameters with automatic IDL metadata generation:
use solzempic::params;
#[params]
pub struct TransferParams {
pub amount: u64,
pub memo: [u8; 32],
}
// Generates:
// - #[repr(C)]
// - #[derive(Clone, Copy)]
// - Pod + Zeroable impls
// - ParamsMeta impl with FIELDS constant for IDL generationFor unit structs (instructions with no parameters):
#[params]
pub struct CloseParams; // Also works!Solzempic supports generating Anchor-compatible IDL JSON from your Rust code. This enables using tools like Codama to generate TypeScript SDKs.
- Enable the
idlfeature in your program'sCargo.toml:
[features]
idl = ["solzempic/idl"]
[[bin]]
name = "gen_idl"
path = "src/bin/gen_idl.rs"
required-features = ["idl"]- Use
#[params]on all parameter structs:
#[solzempic::params]
pub struct MyInstructionParams {
pub amount: u64,
}- Add
#[instruction]to instruction struct definitions:
#[instruction]
pub struct MyInstruction<'a> {
pub account: AccountRefMut<'a, MyAccount>,
pub signer: Signer<'a>,
}- Create a
gen_idl.rsbinary:
//! Generate IDL JSON from Rust instruction metadata.
//! Run with: cargo run --bin gen_idl --features idl
use my_program::IDL_INSTRUCTIONS;
fn main() {
println!("{{");
println!(" \"version\": \"0.1.0\",");
println!(" \"name\": \"my_program\",");
println!(" \"instructions\": [");
for (i, instr) in IDL_INSTRUCTIONS.iter().enumerate() {
let comma = if i < IDL_INSTRUCTIONS.len() - 1 { "," } else { "" };
println!(" {{");
println!(" \"name\": \"{}\",", to_camel_case(instr.name));
println!(" \"discriminator\": {},", instr.discriminator);
println!(" \"accounts\": [");
for (j, acc) in instr.accounts.iter().enumerate() {
let acc_comma = if j < instr.accounts.len() - 1 { "," } else { "" };
println!(" {{");
println!(" \"name\": \"{}\",", acc.name);
println!(" \"isMut\": {},", acc.is_writable);
println!(" \"isSigner\": {}", acc.is_signer);
println!(" }}{}", acc_comma);
}
println!(" ],");
println!(" \"args\": [");
for (k, param) in instr.params.iter().enumerate() {
let param_comma = if k < instr.params.len() - 1 { "," } else { "" };
let type_json = rust_type_to_idl(param.type_name);
println!(" {{");
println!(" \"name\": \"{}\",", param.name);
println!(" \"type\": {}", type_json);
println!(" }}{}", param_comma);
}
println!(" ]");
println!(" }}{}", comma);
}
println!(" ]");
println!("}}");
}
fn to_camel_case(s: &str) -> String {
let mut result = String::new();
let mut first = true;
for c in s.chars() {
if first {
result.push(c.to_ascii_lowercase());
first = false;
} else {
result.push(c);
}
}
result
}
fn rust_type_to_idl(rust_type: &str) -> String {
match rust_type {
"u8" | "u16" | "u32" | "u64" | "u128" => format!("\"{}\"", rust_type),
"i8" | "i16" | "i32" | "i64" | "i128" => format!("\"{}\"", rust_type),
"bool" => "\"bool\"".to_string(),
"Address" | "Pubkey" => "\"publicKey\"".to_string(),
s if s.starts_with("[u8;") => {
let len = s.trim_start_matches("[u8;").trim_end_matches(']').trim();
format!("{{ \"array\": [\"u8\", {}] }}", len)
}
_ => format!("\"{}\"", rust_type),
}
}- Generate the IDL:
cargo run --bin gen_idl --features idl > idl.json#[params]generatesParamsMetaimpl with field names and types#[instruction]on struct definitions generatesNUM_ACCOUNTS,SHANK_ACCOUNTS, andshank_accounts()#[instruction]on impl blocks generatesInstructionParams,Instructiontraits, plusIDL_NAMEandIDL_PARAMS#[SolzempicEntrypoint]aggregates all instructions intoIDL_INSTRUCTIONS(whenidlfeature enabled)
The generated IDL is compatible with Anchor's format and can be consumed by Codama for SDK generation.
The IDL generation works well for simple programs but has limitations with compound account types:
Compound types expand to generic names:
When you use compound types like ShardRefMutContext<T>, the macro expands them to their component fields (prev, current, next). These get generic names in the IDL:
#[instruction]
pub struct MyInstruction<'a> {
pub shards: ShardRefMutContext<'a, MyHeader>, // Expands to leftShard, currentShard, rightShard
}Duplicate names with multiple compound fields:
If an instruction has multiple fields of the same compound type, you get duplicate account names:
#[instruction]
pub struct MatchOrders<'a> {
pub clmm_shards: ShardRefMutContext<'a, ClmmHeader>, // → leftShard, currentShard, rightShard
pub limit_shards: ShardRefMutContext<'a, LimitHeader>, // → leftShard, currentShard, rightShard (duplicates!)
}The generated IDL would have duplicate leftShard, currentShard, rightShard entries, which breaks SDK generation.
Recommendations for complex programs:
For programs with compound account types used multiple times:
- Manual IDL: Write the IDL by hand with unique prefixed names (
clmmShardPrev,limitShardPrev, etc.) - Post-process: Generate IDL and manually fix duplicate names
- Separate instructions: Split into multiple instructions each using one compound type
For simpler programs without these patterns, the automated IDL generation works well.
| Feature | Anchor | Solzempic |
|---|---|---|
| CU overhead | ~2000-5000 per instruction | ~100 (just your logic) |
| Binary size | Large (IDL embedded) | Minimal (IDL generated separately) |
| Account validation | Automatic, opaque | Explicit, transparent |
| Serialization | Borsh (copies data) | Zero-copy bytemuck |
| IDL generation | Built-in | Opt-in via idl feature |
| Learning curve | Low (magic) | Medium (explicit) |
| Debugging | Hard (generated code) | Easy (your code) |
| Flexibility | Constrained | Full control |
| Feature | Vanilla Pinocchio | Solzempic |
|---|---|---|
| Boilerplate | High (repeat validation) | Low (wrappers) |
| Structure | None (DIY) | Action pattern |
| Safety | Manual | Enforced by types |
| Program ID handling | Pass everywhere | Framework trait |
| IDL generation | Manual | Automated from decorators |
| Learning curve | High | Medium |
Solzempic adds no runtime overhead beyond what you'd write by hand:
- Account loading: Single borrow, no copies
- Parameter parsing: Zero-copy pointer cast
- Discriminator checks: Single byte comparison
- Program validation: Pubkey comparison (32-byte memcmp)
All wrapper methods are #[inline] and compile away in release builds.
Typical instruction overhead:
- Parse params: ~10 CUs
- Load AccountRefMut: ~50 CUs (borrow + discriminator check)
- Wrap Signer: ~20 CUs (is_signer check)
- Your business logic: varies
| Macro | Purpose |
|---|---|
#[SolzempicEntrypoint("...")] |
Main entrypoint - generates ID, type aliases, dispatch, entrypoint, and IDL_INSTRUCTIONS |
#[account(discriminator = ...)] |
Account struct with #[repr(C)], Pod/Zeroable, and Loadable impl |
#[instruction(Params)] |
Implements InstructionParams + Instruction traits and IDL constants |
#[instruction] (on struct) |
Generates NUM_ACCOUNTS, SHANK_ACCOUNTS, and shank_accounts() for IDL |
#[params] |
Defines instruction params with #[repr(C)], Pod/Zeroable, and ParamsMeta |
define_framework!(ID) |
Alternative: manually define framework type aliases |
define_account_types! { ... } |
Define account discriminator enum |
| Type | Purpose |
|---|---|
AccountRef<'a, T> |
Read-only typed account with ownership validation |
AccountRefMut<'a, T> |
Writable typed account with ownership + is_writable checks |
ShardRefContext<'a, T> |
Read-only prev/current/next triplet for sharded data |
ShardRefMutContext<'a, T> |
Mutable prev/current/next triplet for sharded data |
Writable<'a> |
Raw AccountView wrapper that validates is_writable |
ReadOnly<'a> |
Raw AccountView wrapper (semantic marker, no validation) |
| Type | Validates |
|---|---|
SystemProgram |
Key == System Program |
TokenProgram |
Key == SPL Token or Token-2022 |
AtaProgram |
Key == Associated Token Program |
AltProgram |
Key == Address Lookup Table Program |
Lut |
Address Lookup Table account |
| Type | Validates |
|---|---|
Signer |
is_signer flag is true |
Payer |
Alias for Signer (semantic clarity) |
MutSigner |
is_signer + is_writable flags are true |
| Type | Purpose |
|---|---|
Mint |
SPL Token mint account |
TokenAccountRefMut |
Writable token account with utility methods |
TokenAccountData |
Token account data struct |
Vault |
SPL Token vault account (ATA owned by PDA) |
SolVault |
SOL-holding account wrapper |
| Type | Sysvar |
|---|---|
ClockSysvar |
Clock (slot, timestamp, epoch) |
RentSysvar |
Rent parameters |
SlotHashesSysvar |
Recent slot hashes |
InstructionsSysvar |
Current transaction instructions |
RecentBlockhashesSysvar |
Recent blockhashes |
LastRestartSlotSysvar |
Last cluster restart slot |
| Trait | Purpose |
|---|---|
Instruction |
Three-phase pattern: build() → validate() → execute() |
InstructionParams |
Associates a params type with an instruction |
ParamsMeta |
Provides FIELDS constant for IDL generation |
Framework |
Program-specific configuration (program ID) |
Loadable |
POD types with discriminator byte |
Initializable |
Marker trait for types that can be initialized |
ValidatedAccount |
Common interface for validated wrappers |
AsAccountRef |
Common interface for account wrappers (PDA validation, data access) |
| Function | Purpose |
|---|---|
create_pda_account() |
Create and initialize a PDA via CPI |
transfer_lamports() |
Transfer SOL between accounts |
rent_exempt_minimum() |
Calculate rent-exempt minimum for size |
parse_params::<T>() |
Zero-copy parameter parsing |
| Constant | Value |
|---|---|
SYSTEM_PROGRAM_ID |
System Program |
TOKEN_PROGRAM_ID |
SPL Token Program |
TOKEN_2022_PROGRAM_ID |
Token-2022 Program |
ASSOCIATED_TOKEN_PROGRAM_ID |
ATA Program |
ADDRESS_LOOKUP_TABLE_PROGRAM_ID |
ALT Program |
CLOCK_SYSVAR_ID |
Clock sysvar |
RENT_SYSVAR_ID |
Rent sysvar |
SLOT_HASHES_SYSVAR_ID |
SlotHashes sysvar |
INSTRUCTIONS_SYSVAR_ID |
Instructions sysvar |
RECENT_BLOCKHASHES_SYSVAR_ID |
RecentBlockhashes sysvar |
LAST_RESTART_SLOT_SYSVAR_ID |
LastRestartSlot sysvar |
LAMPORTS_PER_BYTE |
Rent cost per byte |
MAX_ACCOUNT_SIZE |
Maximum account size (10MB) |
Solzempic uses Pinocchio's ProgramError throughout:
// Common errors returned by wrappers:
ProgramError::IllegalOwner // Account not owned by program
ProgramError::InvalidAccountData // Wrong discriminator or not writable
ProgramError::AccountAlreadyInitialized // init() on initialized account
ProgramError::IncorrectProgramId // Wrong program/sysvar ID
ProgramError::MissingRequiredSignature // Signer check failed
ProgramError::NotEnoughAccountKeys // Too few accounts passed
ProgramError::InvalidInstructionData // Params too shortDefine your own errors by implementing Into<ProgramError>:
#[repr(u32)]
pub enum MyError {
InvalidPrice = 1000,
OrderNotFound = 1001,
}
impl From<MyError> for ProgramError {
fn from(e: MyError) -> Self {
ProgramError::Custom(e as u32)
}
}- Fail fast in build(): Validate account structure early
- Keep validate() pure: No state changes, only checks
- Document account order: Use comments to specify expected accounts
- Use #[inline(always)]: For hot paths in execute()
- Prefer AccountRefMut::init_pda(): For PDA creation with initialization
- Call reload() after CPI: If you modify accounts via CPI
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Write tests for new functionality
- Ensure
cargo clippypasses - Submit a pull request
Licensed under the Apache License, Version 2.0. See LICENSE for details.
Built on top of Pinocchio, the minimal Solana program framework.
