Introduction
Typhoon is a high-performance Solana Sealevel Framework designed for developers who need maximum efficiency and control. Built on top of pinocchio, Typhoon provides a thin abstraction layer that simplifies program development while maintaining a no-std environment and zero-cost abstractions.
Why Typhoon?
Traditional frameworks can often introduce significant overhead in terms of binary size and compute units. Typhoon addresses this by:
- Minimal Abstractions: Only what you need to build robust programs.
- High Performance: Optimized for the Solana VM.
- No-Std by Default: Ensuring compatibility with the most constrained environments.
- Procedural Macros: Simplifying boilerplate with powerful macros like
#[context]andbasic_router!.
Getting Started
To get started with Typhoon, you’ll need a working Rust environment set up for Solana development.
Installation
Add Typhoon to your Cargo.toml:
cargo add typhoon
Your First Program
Here is a simple “Hello World” program using Typhoon:
#![allow(unused)]
#![no_std]
fn main() {
use typhoon::prelude::*;
program_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
nostd_panic_handler!();
no_allocator!();
entrypoint!();
pub const ROUTER: EntryFn = basic_router! {
0 => hello_world
};
pub fn hello_world(ProgramIdArg(program_id): ProgramIdArg) -> ProgramResult {
solana_msg::sol_log("Hello World");
assert_eq!(program_id, &crate::ID);
Ok(())
}
}
This program defines a single instruction hello_world that logs a message. The basic_router! macro handles the dispatching logic based on the instruction discriminator.
Crates Overview
Typhoon is organized into several crates, each providing specific functionality for building Solana programs.
Core Crates
typhoon: The main entry point, re-exporting modules from other crates for ease of use.typhoon-accounts: Handles account-related abstractions and data structures.typhoon-context: Provides the infrastructure for instruction contexts.typhoon-errors: Defines the error handling system used throughout the framework.typhoon-traits: Core traits used for cross-compatible implementations.
Macro Crates
typhoon-context-macro: Implements the#[context]attribute macro.typhoon-account-macro: Macros for working with account states and data.typhoon-program-id-macro: Implements theprogram_id!macro.
Utility Crates
typhoon-utility: General-purpose utilities, including byte manipulation.typhoon-utility-traits: Common utility traits for efficient data handling.typhoon-discriminator: Logic for generating instruction and account discriminators.
Constraints
Constraints are a powerful feature in Typhoon that allow you to declaratively define validation and initialization logic for your instruction accounts. They are applied using the #[constraint(...)] attribute on fields within a #[context] struct.
At compile time, the macro expands constraints into efficient validation code, eliminating boilerplate while keeping your programs safe.
Quick Reference
| Constraint | Syntax | Description |
|---|---|---|
init | init | Initialize a new account |
init_if_needed | init_if_needed | Initialize only if account doesn’t exist |
payer | payer = <field> | Account that pays for initialization |
space | space = <expr> | Allocated byte size for new accounts |
seeds | seeds = [...] | PDA seed derivation |
seeded | seeded / seeded = [...] | PDA derivation via the Seeded trait |
bump | bump / bump = <expr> | PDA bump seed |
program | program = <expr> | Program ID for PDA derivation |
has_one | has_one = <field> | Validate account data field matches another account |
assert | assert = <expr> | Custom assertion on account data |
address | address = <expr> | Validate account address |
token::* | token::mint = ... / token::owner = ... | Token account validation |
mint::* | mint::decimals = ... / mint::authority = ... / mint::freeze_authority = ... | Mint account configuration |
associated_token::* | associated_token::mint = ... / associated_token::authority = ... | Associated token account derivation |
Account Initialization
init
Marks an account to be created and initialized. The account must be wrapped in Mut<> and must be a signer (either a keypair signer via UncheckedSigner<> or a PDA via seeds).
Requires payer. Optionally takes space (defaults to AccountType::SPACE).
The system program is automatically required when init is used.
#![allow(unused)]
fn main() {
#[context]
pub struct InitCounter {
pub payer: Mut<Signer>,
#[constraint(
init,
payer = payer,
)]
pub counter: Mut<UncheckedSigner<Account<Counter>>>,
pub system: Program<System>,
}
}
When initializing a PDA, combine init with seeds and bump:
#![allow(unused)]
fn main() {
#[context]
pub struct InitPda {
pub payer: Mut<Signer>,
#[constraint(
init,
payer = payer,
space = Counter::SPACE,
seeds = [b"counter".as_ref()],
bump
)]
pub counter: Mut<Account<Counter>>,
pub system: Program<System>,
}
}
init_if_needed
Conditionally initializes an account — only if it does not already exist. If the account is already initialized, it validates the account as normal instead.
This is particularly useful for associated token accounts that may or may not exist when the instruction is called.
#![allow(unused)]
fn main() {
#[context]
pub struct MaybeCreate {
pub payer: Mut<Signer>,
#[constraint(
init_if_needed,
payer = payer,
associated_token::mint = mint,
associated_token::authority = owner
)]
pub token_account: Mut<Account<TokenAccount>>,
pub mint: Account<Mint>,
pub owner: UncheckedAccount,
pub ata_program: Program<AtaTokenProgram>,
pub token_program: Program<TokenProgram>,
pub system_program: Program<System>,
}
}
payer
Specifies which account pays the rent for a newly created account. The payer must be a Mut<Signer>.
Syntax: payer = <field_name>
#![allow(unused)]
fn main() {
#[constraint(
init,
payer = authority, // `authority` field pays for this account
)]
}
space
Sets the number of bytes to allocate for the new account. If omitted, defaults to AccountType::SPACE which is derived from the struct size plus the 8-byte discriminator.
Syntax: space = <expr>
#![allow(unused)]
fn main() {
#[constraint(
init,
payer = payer,
space = 8 + core::mem::size_of::<MyData>()
)]
pub data: Mut<UncheckedSigner<Account<MyData>>>,
}
You can also reference a constant:
#![allow(unused)]
fn main() {
impl MyData {
const SPACE: usize = 8 + core::mem::size_of::<MyData>();
}
// ...
#[constraint(
init,
payer = payer,
space = MyData::SPACE
)]
}
PDA Constraints
seeds
Defines the seeds used to derive a Program Derived Address (PDA). Seeds can be specified as an array of byte slices or as a function call that returns the seeds.
Syntax: seeds = [<expr>, ...] or seeds = <fn_call>()
Array form — each element must be a byte slice (&[u8]):
#![allow(unused)]
fn main() {
#[constraint(
seeds = [
b"escrow",
maker.address().as_ref(),
&args.seed.to_le_bytes(),
],
bump
)]
pub escrow: Mut<Account<Escrow>>,
}
Function form — a function returning a seed array:
#![allow(unused)]
fn main() {
fn pda_seeds<'a>() -> [&'a [u8]; 1] {
[b"counter".as_ref()]
}
// ...
#[constraint(
init,
payer = payer,
seeds = pda_seeds(),
bump
)]
pub counter: Mut<Account<Counter>>,
}
seeded
Uses the Seeded trait (derived from #[key] attributes on your account struct) to automatically derive PDA seeds from the account’s state fields.
Syntax: seeded or seeded = [<additional_seeds>]
First, define your account struct with #[key] fields:
#![allow(unused)]
fn main() {
#[derive(NoUninit, AnyBitPattern, AccountState, Copy, Clone)]
#[repr(C)]
#[no_space]
pub struct Counter {
#[key]
pub admin: Address,
pub bump: u8,
_padding: [u8; 7],
pub count: u64,
}
}
Then use seeded in your constraints. Without arguments, it derives seeds from the existing account data:
#![allow(unused)]
fn main() {
#[context]
pub struct Increment {
pub admin: Signer,
#[constraint(seeded, bump = counter.data()?.bump, has_one = admin)]
pub counter: Mut<Account<Counter>>,
}
}
With additional seeds for initialization (where the account data doesn’t exist yet):
#![allow(unused)]
fn main() {
#[context]
#[args(admin: Address, bump: u8)]
pub struct Init {
pub payer: Mut<Signer>,
#[constraint(
init,
payer = payer,
space = Counter::SPACE,
seeded = [&args.admin],
bump
)]
pub counter: Mut<Account<Counter>>,
pub system: Program<System>,
}
}
bump
Controls how the PDA bump is resolved. Used with seeds or seeded.
Syntax: bump or bump = <expr>
Without a value — calls find_program_address to discover both the PDA address and bump. The bump is stored in ctx.bumps.<field_name> for later use:
#![allow(unused)]
fn main() {
#[constraint(
init,
payer = payer,
seeds = [b"counter".as_ref()],
bump // finds the bump automatically
)]
pub counter: Mut<Account<Counter>>,
// In the handler:
pub fn initialize(ctx: Init) -> ProgramResult {
// Access the discovered bump
ctx.counter.mut_data()?.bump = ctx.bumps.counter;
Ok(())
}
}
With a value — uses create_program_address with the provided bump, which is cheaper since it doesn’t need to iterate:
#![allow(unused)]
fn main() {
#[constraint(
seeds = [b"counter".as_ref()],
bump = counter.data()?.bump // use stored bump
)]
pub counter: Mut<Account<Counter>>,
}
program
Overrides the program ID used for PDA derivation. By default, the current program’s ID is used. This is useful when verifying PDAs owned by other programs.
Syntax: program = <expr>
#![allow(unused)]
fn main() {
#[constraint(
seeds = [b"metadata", authority.address().as_ref()],
bump,
program = other_program_id
)]
pub metadata: Account<Metadata>,
}
Validation Constraints
has_one
Validates that a field stored in the account’s deserialized data matches the address of another account in the context. The account data field must have the same name as the target context field.
Syntax: has_one = <field> or has_one = <field> @ <error_expr>
For this to work, the account struct must have a field with the same name as the referenced context field. At runtime, Typhoon checks that the stored address equals the context account’s address.
#![allow(unused)]
fn main() {
#[derive(AccountState, NoUninit, AnyBitPattern, Clone, Copy)]
#[repr(C)]
pub struct Counter {
pub count: u64,
pub admin: Address, // Must match `admin` account in context
pub bump: u8,
_padding: [u8; 7],
}
#[context]
pub struct Increment {
pub admin: Signer, // Checked against counter.admin
#[constraint(
has_one = admin,
seeds = [b"counter".as_ref()],
bump = counter.data()?.bump,
)]
pub counter: Mut<Account<Counter>>,
}
}
With a custom error:
#![allow(unused)]
fn main() {
#[derive(TyphoonError)]
pub enum MyError {
#[msg("Error: Invalid owner")]
InvalidOwner = 200,
}
#[constraint(
has_one = admin @ MyError::InvalidOwner,
)]
}
You can also use built-in Solana errors:
#![allow(unused)]
fn main() {
#[constraint(
has_one = admin @ ProgramError::IllegalOwner,
)]
}
assert
Evaluates a custom boolean expression against account data. The assertion fails the transaction if the expression evaluates to false.
Syntax: assert = <expr> or assert = <expr> @ <error_expr>
You can access deserialized account data via the .data()? method:
#![allow(unused)]
fn main() {
#[context]
pub struct Simple {
#[constraint(
assert = account.data()?.counter == 1,
)]
pub account: Account<RandomData>,
}
}
With a custom error:
#![allow(unused)]
fn main() {
#[constraint(
assert = account.data()?.counter > 0 @ MyError::InvalidCounter,
)]
}
address
Validates that the account’s public key matches the given address expression.
Syntax: address = <expr> or address = <expr> @ <error_expr>
Useful for checking against known constant addresses (e.g., compile-time derived PDAs):
#![allow(unused)]
fn main() {
use typhoon::prelude::*;
pub const RANDOM_PDA: (Address, u8) = find_program_address_const(&[b"random"], &crate::ID);
#[context]
pub struct Verify {
#[constraint(
address = &RANDOM_PDA.0
)]
pub account: Account<RandomData>,
}
}
Using address with assert together:
#![allow(unused)]
fn main() {
#[context]
pub struct Simple {
#[constraint(
assert = account.data()?.counter == 1,
address = &RANDOM_PDA.0
)]
pub account: Account<RandomData>,
}
}
SPL Token Constraints
These constraints are used when working with the SPL Token program. They require the typhoon-token crate.
Token Constraints
Validate and configure SPL token accounts. Use the token:: prefix.
token::mint
Validates that the token account’s mint matches the given mint account.
Syntax: token::mint = <field>
token::owner
Validates that the token account’s owner matches the given expression.
Syntax: token::owner = <expr>
Example — validating a token account:
#![allow(unused)]
fn main() {
use typhoon_token::{TokenAccount, TokenProgram};
#[context]
pub struct ValidateToken {
pub authority: Signer,
pub mint: Account<Mint>,
#[constraint(
token::mint = mint,
token::owner = authority.address(),
)]
pub token_account: Account<TokenAccount>,
}
}
Mint Constraints
Configure SPL mint accounts during initialization. Use the mint:: prefix.
mint::decimals
Sets the number of decimals for the mint. Defaults to 9 if not specified during initialization.
Syntax: mint::decimals = <expr>
mint::authority
Sets the mint authority.
Syntax: mint::authority = <expr>
mint::freeze_authority
Sets the freeze authority for the mint. Optional — if omitted, no freeze authority is set.
Syntax: mint::freeze_authority = <expr>
Example — initializing a mint:
#![allow(unused)]
fn main() {
use typhoon_token::{Mint, TokenProgram, SplCreateMint};
#[context]
#[args(MintArgs)]
pub struct CreateMint {
pub payer: Mut<Signer>,
pub owner: UncheckedAccount,
#[constraint(
init,
payer = payer,
mint::decimals = args.decimals,
mint::authority = escrow.address(),
mint::freeze_authority = owner.address()
)]
pub mint: Mut<UncheckedSigner<Account<Mint>>>,
pub escrow: Account<Escrow>,
pub token_program: Program<TokenProgram>,
pub system_program: Program<System>,
}
}
Associated Token Constraints
Derive and validate associated token account (ATA) addresses. Use the associated_token:: prefix.
associated_token::mint
Specifies which mint the ATA is associated with.
Syntax: associated_token::mint = <field>
associated_token::authority
Specifies the wallet/authority that owns the ATA.
Syntax: associated_token::authority = <field>
When combined with init or init_if_needed, the ATA is created automatically.
Example — creating an ATA if it doesn’t exist:
#![allow(unused)]
fn main() {
use typhoon_token::{
AtaTokenProgram, Mint, SplCreateToken, TokenAccount, TokenProgram,
};
#[context]
pub struct CreateVault {
pub payer: Mut<Signer>,
pub mint: Account<Mint>,
pub owner: Signer,
#[constraint(
init_if_needed,
payer = payer,
associated_token::mint = mint,
associated_token::authority = owner
)]
pub vault: Mut<Account<TokenAccount>>,
pub ata_program: Program<AtaTokenProgram>,
pub token_program: Program<TokenProgram>,
pub system_program: Program<System>,
}
}
Example — validating an existing ATA (without initialization):
#![allow(unused)]
fn main() {
#[context]
pub struct Refund {
pub maker: Mut<Signer>,
pub escrow: Mut<Account<Escrow>>,
pub mint_a: UncheckedAccount,
#[constraint(
associated_token::mint = mint_a,
associated_token::authority = escrow
)]
pub vault: Mut<Account<TokenAccount>>,
pub token_program: Program<TokenProgram>,
}
}
Custom Errors
Several constraints support attaching a custom error using the @ syntax. When the constraint check fails, the provided error is returned instead of the default.
Supported constraints: has_one, assert, address
Syntax: <constraint> @ <error_expr>
#![allow(unused)]
fn main() {
// Using a custom TyphoonError
#[derive(TyphoonError)]
pub enum MyError {
#[msg("The admin does not match")]
InvalidAdmin = 200,
#[msg("Counter must be positive")]
InvalidCounter = 201,
}
#[context]
pub struct Guarded {
pub admin: Signer,
#[constraint(
has_one = admin @ MyError::InvalidAdmin,
assert = counter.data()?.count > 0 @ MyError::InvalidCounter,
)]
pub counter: Mut<Account<Counter>>,
}
}
The bumps Field
When you use bump without providing a value (triggering find_program_address), Typhoon automatically generates a bumps struct on the context. Each PDA field gets a corresponding u8 bump value.
#![allow(unused)]
fn main() {
#[context]
pub struct Init {
pub payer: Mut<Signer>,
#[constraint(
init,
payer = payer,
seeds = [b"escrow", maker.address().as_ref(), &args.seed.to_le_bytes()],
bump // <-- bump is discovered and stored in ctx.bumps.escrow
)]
pub escrow: Mut<Account<Escrow>>,
pub system: Program<System>,
}
pub fn initialize(ctx: Init) -> ProgramResult {
*ctx.escrow.mut_data()? = Escrow {
bump: ctx.bumps.escrow, // save the bump for future use
// ...
};
Ok(())
}
}
Full Example: Escrow Program
Here’s a realistic example combining multiple constraints in an escrow program:
#![allow(unused)]
fn main() {
use {
escrow_interface::{state::Escrow, MakeArgs},
typhoon::prelude::*,
typhoon_token::{spl_instructions::Transfer, *},
};
#[context]
#[args(MakeArgs)]
pub struct Make {
pub maker: Mut<Signer>,
#[constraint(
init,
payer = maker,
seeds = [b"escrow", maker.address().as_ref(), &args.seed.to_le_bytes()],
bump
)]
pub escrow: Mut<Account<Escrow>>,
pub mint_a: Account<Mint>,
pub mint_b: Account<Mint>,
pub maker_ata_a: Mut<Account<TokenAccount>>,
#[constraint(
init_if_needed,
payer = maker,
associated_token::mint = mint_a,
associated_token::authority = escrow
)]
pub vault: Mut<Account<TokenAccount>>,
pub ata_program: Program<AtaTokenProgram>,
pub token_program: Program<TokenProgram>,
pub system_program: Program<System>,
}
pub fn make(ctx: Make) -> ProgramResult {
*ctx.escrow.mut_data()? = Escrow {
maker: *ctx.maker.address(),
mint_a: *ctx.mint_a.address(),
mint_b: *ctx.mint_b.address(),
seed: ctx.args.seed,
receive: ctx.args.receive,
bump: ctx.bumps.escrow,
};
Transfer {
from: ctx.maker_ata_a.as_ref(),
to: ctx.vault.as_ref(),
authority: ctx.maker.as_ref(),
amount: ctx.args.amount,
}
.invoke()?;
Ok(())
}
}
Examples Walkthrough
Typhoon includes several examples to help you understand how to build real-world programs. You can find them in the examples/ directory.
Core Examples
Hello World
A minimal example showing the basic structure of a Typhoon program. Covered in Getting Started.
Counter
Demonstrates how to maintain state in an account and increment a counter.
Transfer SOL
Shows how to handle SOL transfers between accounts.
Advanced Examples
Escrow
A more complex example involving multiple accounts, state transitions, and token transfers.
CPI (Cross-Program Invocation)
Demonstrates how to call other programs from within a Typhoon program.
Anchor CPI
Shows how Typhoon can interact with programs built using the Anchor framework.
Each example is designed to be self-contained and demonstrates a specific feature or pattern of the Typhoon framework.