Skip to content

Anchor Framework

LUMOS provides deep integration with the Anchor framework, allowing you to generate complete Solana programs with account contexts, IDL, and TypeScript clients.

Think of it this way:

  • Anchor = The framework that runs your Solana program
  • LUMOS = The tool that generates Anchor code from a schema

Why use both?

Without LUMOSWith LUMOS
Manually write Rust structsDefine once in .lumos schema
Manually write TypeScript typesAuto-generate TypeScript
Hope Rust ↔ TS stay in syncGuaranteed sync (same source)
Calculate account space by handAuto-calculated LEN constants
Write IDL manually or extractAuto-generate IDL from schema

LUMOS generates standard Anchor code - your program still uses anchor_lang, #[account], #[program], and everything else you know. The difference is you define your types once and generate the rest.


ScenarioRecommendation
Starting a new Anchor project✅ Use LUMOS - define schema first, generate code
Existing Anchor project, want type sync✅ Use LUMOS - adopt gradually for new types
Simple program, few account types⚠️ Either works - Anchor alone is fine
Complex program, many shared types✅ Use LUMOS - prevents drift between Rust/TS
Need custom IDL modifications⚠️ Start with LUMOS, customize after
Team with mixed Rust/TS developers✅ Use LUMOS - schema is readable by all

Bottom line: If you’re writing TypeScript clients that interact with your Anchor program, LUMOS saves you from maintaining duplicate type definitions.


When your schema uses #[account] attributes, LUMOS automatically generates Anchor-compatible code:

schema.lumos
#[solana]
#[account]
struct PlayerAccount {
wallet: PublicKey,
level: u16,
experience: u64,
}

This generates:

  • Rust structs with #[account] derive
  • Account LEN constants for space calculation
  • Anchor IDL JSON
  • Optional TypeScript client

game.lumos
#[solana]
#[account]
struct GameConfig {
authority: PublicKey,
max_players: u32,
entry_fee: u64,
prize_pool: u64,
}
#[solana]
#[account]
struct Player {
wallet: PublicKey,
game: PublicKey,
score: u64,
joined_at: i64,
}
#[solana]
enum GameInstruction {
Initialize { max_players: u32, entry_fee: u64 },
JoinGame,
UpdateScore { score: u64 },
ClaimPrize,
}
Terminal window
lumos anchor generate game.lumos \
--name game_program \
--typescript
game_program/
├── programs/game_program/
│ ├── src/
│ │ ├── lib.rs # Program entry point
│ │ └── state.rs # Account structs
│ └── Cargo.toml
├── target/idl/
│ └── game_program.json # Anchor IDL
└── client/
└── game_program.ts # TypeScript client

Generate a complete Anchor program:

Terminal window
lumos anchor generate <SCHEMA> [OPTIONS]
Options:
-o, --output <DIR> Output directory
-n, --name <NAME> Program name
-V, --version <VER> Program version (default: 0.1.0)
-a, --address <ADDR> Program address
--typescript Generate TypeScript client
--dry-run Preview without writing

Examples:

Terminal window
# Basic generation
lumos anchor generate game.lumos
# Full options
lumos anchor generate game.lumos \
--name my_game \
--version 1.0.0 \
--address "Game111111111111111111111111111111111111111" \
--typescript
# Preview generation
lumos anchor generate game.lumos --dry-run

Generate only the Anchor IDL:

Terminal window
lumos anchor idl <SCHEMA> [OPTIONS]
Options:
-o, --output <FILE> Output file path
-n, --name <NAME> Program name
-p, --pretty Pretty print JSON

Example:

Terminal window
lumos anchor idl game.lumos --pretty -o target/idl/game.json

Calculate account space constants:

Terminal window
lumos anchor space <SCHEMA> [OPTIONS]
Options:
-f, --format <FMT> Output format (text or rust)
-a, --account <NAME> Specific account type

Example:

Terminal window
lumos anchor space game.lumos --format rust

state.rs
use anchor_lang::prelude::*;
#[account]
#[derive(Default)]
pub struct GameConfig {
pub authority: Pubkey,
pub max_players: u32,
pub entry_fee: u64,
pub prize_pool: u64,
}
impl GameConfig {
pub const LEN: usize = 8 // discriminator
+ 32 // authority
+ 4 // max_players
+ 8 // entry_fee
+ 8; // prize_pool
}
lib.rs
use anchor_lang::prelude::*;
mod state;
use state::*;
declare_id!("Game111111111111111111111111111111111111111");
#[program]
pub mod game_program {
use super::*;
pub fn initialize(
ctx: Context<Initialize>,
max_players: u32,
entry_fee: u64,
) -> Result<()> {
let config = &mut ctx.accounts.config;
config.authority = ctx.accounts.authority.key();
config.max_players = max_players;
config.entry_fee = entry_fee;
config.prize_pool = 0;
Ok(())
}
// ... other instructions
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + GameConfig::LEN
)]
pub config: Account<'info, GameConfig>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
{
"version": "1.0.0",
"name": "game_program",
"instructions": [
{
"name": "initialize",
"accounts": [
{ "name": "config", "isMut": true, "isSigner": false },
{ "name": "authority", "isMut": true, "isSigner": true },
{ "name": "systemProgram", "isMut": false, "isSigner": false }
],
"args": [
{ "name": "maxPlayers", "type": "u32" },
{ "name": "entryFee", "type": "u64" }
]
}
],
"accounts": [
{
"name": "GameConfig",
"type": {
"kind": "struct",
"fields": [
{ "name": "authority", "type": "publicKey" },
{ "name": "maxPlayers", "type": "u32" },
{ "name": "entryFee", "type": "u64" },
{ "name": "prizePool", "type": "u64" }
]
}
}
]
}
game_program.ts
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { GameProgram } from "./types/game_program";
export class GameProgramClient {
program: Program<GameProgram>;
constructor(
connection: anchor.web3.Connection,
wallet: anchor.Wallet
) {
const provider = new anchor.AnchorProvider(connection, wallet, {});
this.program = new Program(IDL, PROGRAM_ID, provider);
}
async initialize(
maxPlayers: number,
entryFee: anchor.BN
): Promise<string> {
const config = anchor.web3.Keypair.generate();
return await this.program.methods
.initialize(maxPlayers, entryFee)
.accounts({
config: config.publicKey,
authority: this.program.provider.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([config])
.rpc();
}
// ... other methods
}

Marks a struct as an Anchor account:

#[solana]
#[account]
struct MyAccount {
data: u64,
}

LUMOS supports Anchor account constraints via schema annotations:

#[solana]
#[account]
#[init]
struct NewAccount {
owner: PublicKey,
data: u64,
}
#[solana]
#[account]
#[mut]
struct MutableAccount {
balance: u64,
}
#[solana]
#[account]
#[signer]
struct AuthorityAccount {
authority: PublicKey,
}

LUMOS automatically calculates account space including the 8-byte discriminator:

TypeSize
bool1 byte
u8 / i81 byte
u16 / i162 bytes
u32 / i324 bytes
u64 / i648 bytes
u128 / i12816 bytes
PublicKey32 bytes
String4 + length
Vec<T>4 + (count × size)
Option<T>1 + size

Example:

Terminal window
lumos anchor space game.lumos
Account Space Constants
=======================
GameConfig:
DISCRIMINATOR: 8 bytes
authority: 32 bytes
max_players: 4 bytes
entry_fee: 8 bytes
prize_pool: 8 bytes
──────────────────────────
TOTAL (LEN): 60 bytes

Terminal window
# In your Anchor project root
lumos anchor generate schemas/game.lumos \
--output programs/game/src/lumos_generated.rs
# Import in lib.rs
mod lumos_generated;
use lumos_generated::*;
Terminal window
# Build the program
anchor build
# Run tests
anchor test

Define your schema first, then generate code:

// Define complete data model
#[solana]
#[account]
struct User { ... }
#[solana]
#[account]
struct Post { ... }
// Generate, then build

Track both schema and generated code:

# Keep generated code in version control
# Don't ignore: programs/*/src/generated.rs

Add to your build process:

package.json
{
"scripts": {
"codegen": "lumos anchor generate schema.lumos",
"build": "npm run codegen && anchor build"
}
}
type UserId = PublicKey;
type Lamports = u64;
#[solana]
#[account]
struct Stake {
user: UserId,
amount: Lamports,
}

PDAs are accounts whose address is derived from seeds. Define the account structure in LUMOS, derive the PDA in your Anchor code:

schema.lumos
#[solana]
#[account]
struct UserProfile {
authority: PublicKey,
username: String,
reputation: u64,
created_at: i64,
}
// In your Anchor program
#[derive(Accounts)]
#[instruction(username: String)]
pub struct CreateProfile<'info> {
#[account(
init,
payer = authority,
space = 8 + UserProfile::LEN,
seeds = [b"profile", authority.key().as_ref()],
bump
)]
pub profile: Account<'info, UserProfile>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}

Model relationships using PublicKey references:

schema.lumos
#[solana]
#[account]
struct Collection {
authority: PublicKey,
name: String,
item_count: u32,
}
#[solana]
#[account]
struct Item {
collection: PublicKey, // Reference to parent
owner: PublicKey,
name: String,
attributes: Vec<u8>,
}

When working with SPL tokens alongside LUMOS-generated accounts:

schema.lumos
#[solana]
#[account]
struct Vault {
authority: PublicKey,
token_mint: PublicKey, // SPL token mint
token_account: PublicKey, // Associated token account
total_deposited: u64,
is_locked: bool,
}
// Anchor context
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut)]
pub vault: Account<'info, Vault>,
#[account(mut)]
pub vault_token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub user_token_account: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
pub authority: Signer<'info>,
}

Use LUMOS enums for program state:

schema.lumos
#[solana]
enum AuctionState {
Created,
Active { start_time: i64 },
Ended { winner: PublicKey, final_bid: u64 },
Cancelled,
}
#[solana]
#[account]
struct Auction {
authority: PublicKey,
item_mint: PublicKey,
state: AuctionState,
min_bid: u64,
current_bid: u64,
}

Ensure all referenced types are defined in the schema or imported.

// Bad - UndefinedType not in schema
struct MyAccount {
data: UndefinedType, // Error!
}
// Good - all types defined
struct MyData {
value: u64,
}
struct MyAccount {
data: MyData, // Works
}

Regenerate IDL after schema changes:

Terminal window
lumos anchor idl schema.lumos -o target/idl/program.json

Use lumos anchor space to verify:

Terminal window
lumos anchor space schema.lumos --format rust

This happens when the account struct definition changed but the on-chain account wasn’t migrated. Solutions:

  1. Development: Close and recreate the account
  2. Production: Implement account migration logic

Check your schema for:

  • Reserved Rust keywords used as field names (type, move, ref)
  • Circular type references
  • Missing #[solana] attribute
// Bad - 'type' is reserved
struct Token {
type: u8, // Error!
}
// Good - use different name
struct Token {
token_type: u8,
}

TypeScript types don’t match runtime values

Section titled “TypeScript types don’t match runtime values”

Ensure you regenerated TypeScript after schema changes:

Terminal window
lumos generate schema.lumos --lang ts -o client/types.ts

Anchor test fails with “Account not found”

Section titled “Anchor test fails with “Account not found””

The account might not be initialized. Check:

  1. Account is created before being accessed
  2. PDA seeds match between init and access
  3. Program ID is correct

”Program failed to complete” without details

Section titled “”Program failed to complete” without details”

Enable Anchor’s verbose logging:

Terminal window
RUST_LOG=solana_runtime::system_instruction_processor=trace anchor test

How to update schema without breaking existing accounts?

Section titled “How to update schema without breaking existing accounts?”

Follow these rules for backward compatibility:

  • ✅ Add new optional fields at the end
  • ✅ Add new enum variants at the end
  • ❌ Don’t remove or reorder fields
  • ❌ Don’t change field types

See Schema Migrations for detailed guidance.