What is Quasar, who is it for, and why does it exist at all?
Quasar is a framework for writing programs that run on Solana. Think of it as the toolbox + rulebook that makes writing Solana programs less painful.
Solana already has a popular framework called Anchor. Quasar is a direct competitor, but with one obsession: spending as few compute units as possible.
The repo is a Cargo workspace — one repo, many crates, all compiled together. Here's what each folder is responsible for:
lang/src/". When asking about how macros work, say "look in derive/src/". This immediately cuts the search space in half.A colleague says "Quasar is just an Anchor clone." You want to correct them. What's the key architectural difference?
After this module you'll know which crate does what, so you can point AI at the right file.
Click any component below to see what it's responsible for and how to steer AI when working with it.
← Click any component to learn what it owns
When you run quasar build, the crates collaborate in a chain. Here's what they say to each other:
You want to add a new macro called #[authority] that automatically checks if an account's authority field matches the signer. Which crate do you touch first?
Trace a real user action — token escrow deposit — from client call to on-chain state change.
The most complete example in the repo is examples/escrow/. It implements a classic token escrow: Alice locks Token A, Bob sends Token B, they swap atomically. Three instructions: make, take, refund.
make instruction is sent with Alice's accounts. The program validates the PDA seeds, initializes the Escrow account with Alice's terms, and CPIs to the Token program to transfer Alice's tokens to a vault.
ctx.accounts.make_escrow(receive, &ctx.bumps) writes to the on-chain account. ctx.accounts.deposit_tokens(deposit) CPI-calls the Token program. All in examples/escrow/src/instructions/make.rs.
Press play to walk through every step from client call to final state:
Here's the actual lib.rs for the escrow, translated line by line:
#[program]
mod quasar_escrow {
use super::*;
#[instruction(discriminator = 0)]
pub fn make(
ctx: Ctx<Make>,
deposit: u64,
receive: u64,
) -> Result<(), ProgramError> {
ctx.accounts.make_escrow(
receive, &ctx.bumps
)?;
ctx.accounts.emit_event(
deposit, receive
)?;
ctx.accounts.deposit_tokens(deposit)
}
}
#[program] — "this module IS the on-chain program; generate its entrypoint and dispatch logic"
discriminator = 0 — this instruction's ID number. The first byte of every call must match or the program rejects it
Ctx<Make> — a wrapper holding all the accounts this instruction needs, already validated
make_escrow() — writes Alice's terms (what she wants, her bump seed) to the escrow account on-chain
emit_event() — logs the transaction so off-chain indexers can track it
deposit_tokens() — CPIs to the Token program: move Alice's tokens into the vault
lib.rs with discriminator N, create a corresponding Make-style accounts struct in instructions/, and wire up the method calls." That structure is exactly how every existing instruction is laid out.After make runs, an Escrow account is written to the blockchain. This is its exact shape:
#[account(discriminator = 1)]
pub struct Escrow {
pub maker: Address, // 32 bytes
pub mint_a: Address, // 32 bytes
pub mint_b: Address, // 32 bytes
pub maker_ta_b: Address, // 32 bytes
pub receive: u64, // 8 bytes
pub bump: u8, // 1 byte
}
#[account(discriminator = 1)] — the first byte of this account's data will always be 1, so the program can verify it's reading an Escrow and not some other account type
maker — Alice's wallet address. Who created this deal
mint_a / mint_b — the two token types involved in the swap
maker_ta_b — where to send Bob's tokens when the deal completes
receive — how many Token B Alice wants in return
bump — the PDA bump seed, saved so future instructions can re-derive the same address
[0,0,0,0] would look identical to an uninitialized account full of zeros — a security hole. The compiler catches this for you.Bob calls the take instruction. Which sequence is correct?
Quasar's macros are its superpower — understand what they generate so you know what you're actually writing.
A proc-macro in Rust is like a stamp. You press it on your code and it produces a much larger imprint. Quasar ships five core stamps, each in derive/src/:
Every instruction has an accounts struct. The #[derive(Accounts)] macro reads your field annotations and generates the entire parse_accounts() function:
#[derive(Accounts)]
pub struct Make<'info> {
#[account(mut, signer)]
maker: UncheckedAccount<'info>,
#[account(
init,
seeds = [b"escrow", maker],
payer = maker,
space = Escrow::SPACE,
)]
escrow: Account<'info, Escrow>,
#[account(address = system_program::ID)]
system_program: UncheckedAccount<'info>,
}
maker: check header byte = NOT_BORROWED | signer | writable. If not, error immediately.
escrow (init): if account doesn't exist, CPI to System Program to create it with the right rent. Then verify PDA seeds match [b"escrow", maker's key]. Write discriminator. Return pointer.
system_program: check that its address equals the known System Program ID. Reject anything else.
All of this runs in a single pass over the account slice — no heap allocation.
u32 and compares it to a pre-computed constant (NODUP_MUT_SIGNER etc.). One comparison checks signer + writable + not-duplicated simultaneously. This is like checking three ID cards in one glance instead of one at a time.A PDA is how Quasar programs create accounts they control. Like giving each escrow deal its own pigeonhole in a post office — always at the same slot, always only accessible to the program.
[b"escrow", maker_pubkey] — one escrow per maker wallet.[b'my-label', user_key]. Add it to the accounts struct with init, seeds = [...], payer = user, space = MyStruct::SPACE. Save the bump in the struct."You want to add a new on-chain account type called UserProfile. Which macro do you use, and what does it generate for you?
The engineering decisions that make Quasar unusual — and how they affect you as a builder.
This is Quasar's central design decision. Let's make it concrete.
Quasar uses unsafe for its zero-copy pointer casts. Three guarantees make this sound:
CPI is how Quasar programs talk to external programs (Token, System, etc.). Quasar's CPI builder is const-generic — the number of accounts is baked in at compile time, so it allocates on the stack, not the heap.
// Stack-allocated CPI — no heap needed
let cpi = TransferCpi::new(
system_program,
[from_account, to_account],
);
cpi.invoke_signed(&[
&[b"escrow", maker_key, &[bump]],
])?;
Build a CPI call targeting the System Program — the account list lives on the call stack (no memory allocation)
The [from_account, to_account] array size is fixed at compile time — the compiler knows exactly how much stack space to reserve
invoke_signed executes the CPI, passing our PDA's seeds as the "signature" that proves we have authority
The IDL is generated by quasar idl <path>. It parses your Rust source and produces three outputs:
target/idl/.src/idl_client.rs to build the instruction — don't construct the bytes manually." The generated client handles discriminators and account ordering for you.A junior dev on your team adds a new account type with #[account(discriminator = [0, 0, 0, 0])]. The code doesn't compile. They're confused. What do you tell them?
The `quasar` binary is your daily driver — know every command and you'll work 10× faster.
Cargo.toml, Quasar.toml, source files, test setup, and a program keypair. Saves your preferences for next time.cargo build-sbf. Prints binary size + delta: "Build complete in 1.2s (56.6 KB, -1.2 KB)". --debug is required for profiling.--source interleaves your Rust source code with the assembly. Useful for understanding exactly what the compiler produced.target/deploy/.Every Quasar project has a Quasar.toml that tells the CLI how to behave. It's small but important:
[project]
name = "my-program"
[toolchain]
type = "solana"
# or "upstream" for nightly BPF
[testing]
framework = "mollusk"
# or: "none", "quasarsvm-rust",
# "quasarsvm-web3js", "quasarsvm-kit"
name — the program's crate name. Used in build outputs and IDL filenames
toolchain: "solana" — use the stable Solana SDK build tool (cargo build-sbf)
toolchain: "upstream" — use nightly Rust's BPF target directly (for advanced use)
framework: "mollusk" — use Mollusk for tests: a fast in-process SVM simulator, no validator needed
Changing framework changes how quasar test runs — it knows whether to launch a TypeScript runner or cargo test
The profiler in profile/ statically walks the compiled binary's call graph using DWARF debug info. No transaction needed — it reads the binary like an X-ray.
quasar profile --expand and share the output. Then tell Claude: "The profiler shows Deposit::process gained 200 CU. Look at that function in instructions/deposit.rs and reduce compute usage."Tests use Mollusk — a fast in-process SVM. The test sets up fake accounts, builds an instruction using the generated IDL client, and checks the resulting account state:
fn setup() -> Mollusk {
Mollusk::new(
&crate::ID,
"../../target/deploy/quasar_escrow"
)
}
let result = mollusk.process_instruction(
&instruction,
&[(maker, maker_account), ...],
);
assert!(result.program_result.is_ok());
// Then inspect result.resulting_accounts
Create a Mollusk instance pointed at your compiled program binary
Build a fake account for the maker (just a struct with lamports and owner)
process_instruction runs the instruction through the SVM — same VM as mainnet, but in-process
resulting_accounts contains the final state of every account after the instruction ran — inspect these to verify your program wrote the right data
You've added a new close instruction to your escrow program. Tests pass but a teammate says the transaction now uses 500 extra CU. What's your debugging workflow?