TLDR:
- Anchor is the standard framework for building Solana programs. It handles account validation, serialization, and security checks through Rust macros.
anchor init scaffolds a full workspace: program source, tests, deployment config, and TypeScript types.
- The
#[program] macro defines instructions, #[derive(Accounts)] defines account validation, and #[account] defines on-chain state.
- Anchor 1.0.0 (April 2026) ships breaking changes:
@anchor-lang/core replaces @coral-xyz/anchor, Surfpool replaces solana-test-validator, and LiteSVM is the default test template.
Prerequisites
Install the following before starting. The guide targets Anchor 1.0.0.
| Tool | Version | Install |
|---|
| Rust | 1.89.0+ | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| Solana CLI | 3.1.10+ | sh -c "\$(curl -sSfL https://release.anza.xyz/v3.1.10/install)" |
| Anchor CLI | 1.0.0 | cargo install avm --git https://github.com/solana-foundation/anchor --locked && avm install 1.0.0 && avm use 1.0.0 |
| Node.js | 22+ | nodejs.org |
| Yarn | 1.22+ | npm install -g yarn |
| Surfpool | 1.1.2+ | Installation instructions |
Verify your setup:
anchor --version # anchor-cli 1.0.0
solana --version # solana-cli 3.1.x
rustc --version # rustc 1.89.0+
surfpool --version # surfpool 1.1.2+
Anchor 1.0.0 uses Surfpool instead of solana-test-validator for anchor test. If Surfpool is not installed, anchor test will fail with a connection error. If you cannot install Surfpool, use anchor test --validator legacy to fall back to solana-test-validator.
Anchor 1.0.0 no longer requires the Solana CLI to be on your PATH for most commands. The anchor CLI ships native implementations of balance, airdrop, address, and deploy. You still need the Solana CLI for operations like solana-keygen and solana program close.
Creating a project
Scaffold a new Anchor workspace:
anchor init my-counter
cd my-counter
This creates a modular project structure with separate files for instructions, state, constants, and errors. If you prefer everything in a single lib.rs, use --template single:
anchor init my-counter --template single
The modular template (default) is recommended for production programs. It scales better as your program grows.
Test templates
Anchor 1.0.0 generates a LiteSVM Rust test by default. You can pick a different template:
anchor init my-counter --test-template mocha # TypeScript with Mocha
anchor init my-counter --test-template mollusk # Mollusk test library
Project structure
After anchor init, the workspace looks like this:
my-counter/
├── programs/
│ └── my-counter/
│ ├── src/
│ │ ├── lib.rs # Entry point, module declarations
│ │ ├── constants.rs # Program constants
│ │ ├── error.rs # Custom error codes
│ │ ├── instructions.rs # Re-exports instruction modules
│ │ ├── instructions/
│ │ │ └── initialize.rs # Instruction handler
│ │ └── state.rs # Account state definitions
│ └── Cargo.toml
├── tests/
│ └── my-counter.ts # Test file (or src/ for Rust tests)
├── target/
│ ├── deploy/ # Compiled .so and keypair
│ ├── idl/ # Generated IDL JSON
│ └── types/ # Generated TypeScript types
├── Anchor.toml # Workspace configuration
├── Cargo.toml # Rust workspace manifest
├── rust-toolchain.toml # Rust MSRV pinning
└── package.json
Key directories:
programs/ — your on-chain programs. A workspace can contain multiple programs.
target/deploy/ — the compiled program binary (.so file) and program keypair.
target/idl/ — the generated IDL JSON file that clients use to interact with your program.
target/types/ — TypeScript type definitions generated from the IDL.
Anchor.toml
The workspace configuration file controls which cluster you deploy to, which wallet signs transactions, and what scripts run during testing:
[toolchain]
package_manager = "yarn"
[features]
resolution = true
skip-lint = false
[programs.localnet]
my_counter = "YourProgramId111111111111111111111111111111"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 \"tests/**/*.ts\""
[hooks]
You can optionally pin versions in the [toolchain] section:
[toolchain]
anchor_version = "1.0.0"
solana_version = "3.1.10"
package_manager = "yarn"
The [registry] section has been removed in Anchor 1.0.0. If you see it in older examples, delete it.
Program anatomy
An Anchor program is built around four macros. Here is a complete counter program that demonstrates all of them:
use anchor_lang::prelude::*;
declare_id!("Cntr1111111111111111111111111111111111111111");
#[program]
pub mod my_counter {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = 0;
counter.authority = ctx.accounts.authority.key();
msg!("Counter initialized by {}", counter.authority);
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = counter.count.checked_add(1)
.ok_or(ErrorCode::Overflow)?;
msg!("Counter incremented to {}", counter.count);
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + Counter::INIT_SPACE,
seeds = [b"counter", authority.key().as_ref()],
bump,
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [b"counter", authority.key().as_ref()],
bump,
has_one = authority,
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
pub authority: Pubkey,
}
#[error_code]
pub enum ErrorCode {
#[msg("Counter overflow")]
Overflow,
}
The sections below break down each macro.
declare_id!
Sets the program’s on-chain address. By default, this matches the public key from /target/deploy/my_counter-keypair.json.
declare_id!("Cntr1111111111111111111111111111111111111111");
If the keypair and source code fall out of sync (for example, after cloning a repo), run:
Anchor 1.0.0 checks that the declare_id! value matches the keypair file during anchor build. A mismatch produces a compile error. Use anchor build --ignore-keys to skip this check if needed.
#[program]
Defines the module containing your instruction handlers. Each public function in this module becomes a callable instruction:
#[program]
pub mod my_counter {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// instruction logic
Ok(())
}
}
Every handler receives a Context<T> as its first parameter, where T is the accounts struct. Additional parameters become the instruction arguments that clients pass in.
The Context struct provides:
ctx.accounts — the validated accounts
ctx.program_id — the program’s public key
ctx.remaining_accounts — extra accounts not in the struct
ctx.bumps — PDA bump seeds found during validation
#[derive(Accounts)]
Defines which accounts an instruction requires and how to validate them. Each field specifies an account with its type and constraints:
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + Counter::INIT_SPACE,
seeds = [b"counter", authority.key().as_ref()],
bump,
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
Anchor validates all constraints before executing the instruction handler. If any check fails, the instruction aborts with a descriptive error.
Common account types:
| Type | Purpose |
|---|
Account<'info, T> | Deserialized account owned by this program |
Signer<'info> | Must have signed the transaction |
SystemAccount<'info> | Any system-owned account |
Program<'info, T> | An executable program account |
UncheckedAccount<'info> | No validation — use with explicit checks |
#[account]
Defines the data layout for accounts your program creates. Anchor adds an 8-byte discriminator as a prefix to distinguish account types:
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64, // 8 bytes
pub authority: Pubkey, // 32 bytes
}
The discriminator is the first 8 bytes of SHA256("account:Counter"). It is automatically set during init and verified during deserialization.
Account constraints
Constraints are the core of Anchor’s security model. They go inside #[account(...)] attributes and tell Anchor what to validate before executing your instruction.
Initialization constraints
Create a new account and set its discriminator:
#[account(
init,
payer = authority,
space = 8 + Counter::INIT_SPACE,
)]
pub counter: Account<'info, Counter>,
The init constraint requires:
payer — the account paying the rent
space — total bytes (8-byte discriminator + your data)
Use init_if_needed to create only if the account does not already exist. This requires the init-if-needed feature in your Cargo.toml:
[dependencies]
anchor-lang = { version = "1.0.0", features = ["init-if-needed"] }
PDA constraints
Validate that an account is a PDA derived from specific seeds:
#[account(
seeds = [b"counter", authority.key().as_ref()],
bump,
)]
pub counter: Account<'info, Counter>,
When combined with init, this creates the PDA account:
#[account(
init,
payer = authority,
space = 8 + Counter::INIT_SPACE,
seeds = [b"counter", authority.key().as_ref()],
bump,
)]
pub counter: Account<'info, Counter>,
Anchor finds and stores the canonical bump automatically. Access it in your handler with ctx.bumps.counter.
Use fixed-length seeds or include a separator between variable-length seeds. Without this, seeds = [b"ab", b"cd"] and seeds = [b"abcd"] produce the same PDA, which can lead to seed collision vulnerabilities.
Mutability and ownership
#[account(mut)] // Account must be mutable
#[account(mut, has_one = authority)] // Must be mutable AND counter.authority == authority.key()
#[account(owner = some_program.key())] // Account owned by a specific program
#[account(close = destination)] // Close account, send lamports to destination
Reallocation
Resize an existing account:
#[account(
mut,
realloc = 8 + NewLayout::INIT_SPACE,
realloc::payer = authority,
realloc::zero = false,
)]
pub data_account: Account<'info, NewLayout>,
SPL token constraints
Create or validate token accounts:
#[account(
init,
payer = authority,
token::mint = mint,
token::authority = authority,
)]
pub token_account: Account<'info, TokenAccount>,
#[account(
init,
payer = authority,
mint::decimals = 6,
mint::authority = authority,
)]
pub mint: Account<'info, Mint>,
Accessing instruction arguments in constraints
Use #[instruction(...)] to reference instruction arguments in your constraints:
#[derive(Accounts)]
#[instruction(title: String)]
pub struct CreatePost<'info> {
#[account(
init,
payer = author,
space = 8 + 32 + 4 + title.len(),
seeds = [b"post", author.key().as_ref(), title.as_bytes()],
bump,
)]
pub post: Account<'info, Post>,
#[account(mut)]
pub author: Signer<'info>,
pub system_program: Program<'info, System>,
}
In Anchor 1.0.0, the compiler enforces that #[instruction(...)] argument types and order match the handler signature. Mismatches that were previously silent now produce errors.
Space calculation
Every account needs enough space for the 8-byte discriminator plus its data fields.
Type sizes
| Type | Bytes |
|---|
bool | 1 |
u8 / i8 | 1 |
u16 / i16 | 2 |
u32 / i32 | 4 |
u64 / i64 | 8 |
u128 / i128 | 16 |
Pubkey | 32 |
[T; N] | size(T) * N |
Vec<T> | 4 + (size(T) * max_elements) |
String | 4 + max_bytes |
Option<T> | 1 + size(T) |
Enum | 1 + largest_variant |
Manual calculation
#[account]
pub struct GameState {
pub player: Pubkey, // 32
pub score: u64, // 8
pub level: u16, // 2
pub active: bool, // 1
}
// Total: 8 (discriminator) + 32 + 8 + 2 + 1 = 51 bytes
Using InitSpace
The InitSpace derive macro calculates space automatically. Use #[max_len(...)] for dynamic types:
#[account]
#[derive(InitSpace)]
pub struct UserProfile {
pub authority: Pubkey,
#[max_len(50)]
pub username: String, // 4 + 50 = 54 bytes
#[max_len(5, 50)]
pub interests: Vec<String>, // 4 + (5 * (4 + 50)) = 274 bytes
pub points: u64,
}
// In the accounts struct:
#[account(init, payer = authority, space = 8 + UserProfile::INIT_SPACE)]
pub profile: Account<'info, UserProfile>,
The #[max_len(5, 50)] on Vec<String> means: up to 5 strings, each up to 50 bytes.
Building and the IDL
Build your program:
This produces three outputs:
- Program binary —
target/deploy/my_counter.so — the compiled BPF bytecode.
- IDL file —
target/idl/my_counter.json — a JSON description of your program’s instructions, accounts, and types.
- TypeScript types —
target/types/my_counter.ts — type definitions generated from the IDL.
IDL structure
The IDL maps your Rust program to a format that clients can consume:
{
"address": "Cntr1111111111111111111111111111111111111111",
"metadata": {
"name": "my_counter",
"version": "0.1.0",
"spec": "0.1.0"
},
"instructions": [
{
"name": "initialize",
"discriminator": [175, 175, 109, 31, 13, 152, 155, 237],
"accounts": [
{ "name": "counter", "writable": true, "pda": { ... } },
{ "name": "authority", "writable": true, "signer": true },
{ "name": "system_program", "address": "11111111111111111111111111111111" }
],
"args": []
}
],
"accounts": [
{ "name": "Counter", "discriminator": [255, 176, 4, 245, ...] }
],
"types": [
{ "name": "Counter", "type": { "kind": "struct", "fields": [...] } }
]
}
Anchor 1.0.0 replaces the legacy IDL storage mechanism with the Program Metadata Program (PMP). When you run anchor program deploy, the IDL is uploaded automatically. To skip the upload:
anchor program deploy --no-idl
Testing
Anchor 1.0.0 defaults to LiteSVM for Rust-based testing. LiteSVM runs an in-process Solana VM—faster startup and teardown than solana-test-validator.
TypeScript tests
For TypeScript tests (generated with --test-template mocha), the test file uses @anchor-lang/core:
import * as anchor from "@anchor-lang/core";
import { Program } from "@anchor-lang/core";
import { MyCounter } from "../target/types/my_counter";
import { PublicKey } from "@solana/web3.js";
import assert from "assert";
describe("my-counter", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.myCounter as Program<MyCounter>;
const wallet = provider.wallet as anchor.Wallet;
it("initializes the counter", async () => {
const [counterPda] = PublicKey.findProgramAddressSync(
[Buffer.from("counter"), wallet.publicKey.toBuffer()],
program.programId
);
const tx = await program.methods
.initialize()
.accounts({
authority: wallet.publicKey,
})
.rpc();
const counter = await program.account.counter.fetch(counterPda);
assert.equal(counter.count.toNumber(), 0);
assert.ok(counter.authority.equals(wallet.publicKey));
console.log("Initialize tx:", tx);
});
it("increments the counter", async () => {
const [counterPda] = PublicKey.findProgramAddressSync(
[Buffer.from("counter"), wallet.publicKey.toBuffer()],
program.programId
);
const tx = await program.methods
.increment()
.accounts({
authority: wallet.publicKey,
})
.rpc();
const counter = await program.account.counter.fetch(counterPda);
assert.equal(counter.count.toNumber(), 1);
console.log("Increment tx:", tx);
});
});
Anchor 1.0.0 renamed the TypeScript package from @coral-xyz/anchor to @anchor-lang/core. All existing tutorials and Stack Overflow answers that show @coral-xyz/anchor need the import updated.
Running tests
Anchor 1.0.0 uses Surfpool as the default local validator for anchor test:
This starts Surfpool, builds and deploys your program, runs the test suite, and shuts down the validator.
To use the legacy solana-test-validator instead:
anchor test --validator legacy
To run tests against an already-running local validator:
anchor test --skip-local-validator
Deploying
Local deployment
Start a local validator and deploy:
solana-test-validator
# In a separate terminal:
anchor program deploy
If anchor program deploy fails with a 503 error on get_recommended_micro_lamport_fee, pass a manual compute unit price:anchor program deploy --with-compute-unit-price 1
This is a known issue that affects some local RPC configurations.
Deploying to devnet
- Set your cluster to devnet in
Anchor.toml:
[provider]
cluster = "Devnet"
wallet = "~/.config/solana/id.json"
- Fund your wallet:
solana airdrop 5 --url devnet
- Build and deploy:
anchor build
anchor program deploy
anchor program deploy in 1.0.0 automatically uploads the IDL to the on-chain Program Metadata Program. The IDL becomes fetchable by any client using Program.fetchIdl.
Deploying to mainnet with Chainstack
-
Deploy a Solana node on Chainstack or use the Chainstack Solana endpoint.
-
Set your cluster to your Chainstack endpoint in
Anchor.toml:
[provider]
cluster = "https://solana-mainnet.core.chainstack.com/YOUR_CHAINSTACK_KEY"
wallet = "~/.config/solana/id.json"
- Build and deploy:
anchor build
anchor program deploy
Mainnet deployment requires real SOL. Verify your program thoroughly on devnet before deploying to mainnet.
Client interaction
The IDL enables typed client interactions. Anchor’s TypeScript client (in the @anchor-lang/core package) deserializes accounts and instruction arguments automatically.
Connecting to a program
import * as anchor from "@anchor-lang/core";
import { Program } from "@anchor-lang/core";
import { Connection, PublicKey, Keypair } from "@solana/web3.js";
import { MyCounter } from "../target/types/my_counter";
import idl from "../target/idl/my_counter.json";
const CHAINSTACK_ENDPOINT = "CHAINSTACK_NODE_URL";
const connection = new Connection(CHAINSTACK_ENDPOINT, "confirmed");
// Load your wallet keypair from a JSON file (must be funded)
import fs from "fs";
const secretKey = Uint8Array.from(
JSON.parse(fs.readFileSync("/path/to/keypair.json", "utf-8"))
);
const wallet = new anchor.Wallet(Keypair.fromSecretKey(secretKey));
const provider = new anchor.AnchorProvider(connection, wallet, {
commitment: "confirmed",
});
const program = new Program<MyCounter>(idl as MyCounter, provider);
The anchor.workspace object is only available inside anchor test. For standalone scripts and frontend code, load the IDL JSON file directly and instantiate the program with new Program(idl, provider).
Sending transactions
The wallet must be funded before sending transactions. On devnet, request an airdrop first:
// Initialize
const tx = await program.methods
.initialize()
.accounts({
authority: wallet.publicKey,
})
.rpc();
console.log("Transaction signature:", tx);
Fetching accounts
const [counterPda] = PublicKey.findProgramAddressSync(
[Buffer.from("counter"), wallet.publicKey.toBuffer()],
program.programId
);
// Fetch a single account
const counter = await program.account.counter.fetch(counterPda);
console.log("Count:", counter.count.toNumber());
console.log("Authority:", counter.authority.toBase58());
// Fetch all counter accounts
const allCounters = await program.account.counter.all();
allCounters.forEach((item) => {
console.log("Address:", item.publicKey.toBase58());
console.log("Count:", item.account.count.toNumber());
});
// Fetch with filters
const myCounters = await program.account.counter.all([
{
memcmp: {
offset: 16, // 8 discriminator + 8 count
bytes: wallet.publicKey.toBase58(),
},
},
]);
Listening to events
If your program emits events with the emit! macro:
#[event]
pub struct CounterIncremented {
pub authority: Pubkey,
pub new_count: u64,
}
// In your instruction handler:
emit!(CounterIncremented {
authority: counter.authority,
new_count: counter.count,
});
Subscribe from the client:
const listener = program.addEventListener("counterIncremented", (event) => {
console.log("New count:", event.newCount.toNumber());
});
// Later, remove the listener:
program.removeEventListener(listener);
Common gotchas
CpiContext takes Pubkey, not AccountInfo
Anchor 1.0.0 removed the redundant AccountInfo copy from CpiContext. If you are adapting pre-1.0 code:
// Pre-1.0 (broken in 1.0.0)
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer { ... }
)
// Anchor 1.0.0
CpiContext::new(
ctx.accounts.token_program.key(),
Transfer { ... }
)
Duplicate mutable accounts are now rejected
Passing the same mutable account twice in an instruction errors by default. If you intentionally need this, opt in with dup:
#[derive(Accounts)]
pub struct SwapAccounts<'info> {
#[account(mut)]
pub account_a: Account<'info, TokenAccount>,
#[account(mut, dup)]
pub account_b: Account<'info, TokenAccount>,
}
AccountInfo is deprecated in Accounts structs
Using AccountInfo directly inside #[derive(Accounts)] now produces a compile warning. Use UncheckedAccount instead:
// Deprecated
pub some_account: AccountInfo<'info>,
// Preferred
/// CHECK: Validated in instruction logic
pub some_account: UncheckedAccount<'info>,
Space must include the 8-byte discriminator
Every init constraint needs 8 extra bytes for the discriminator:
// Wrong — account will be too small
#[account(init, payer = user, space = Counter::INIT_SPACE)]
// Correct
#[account(init, payer = user, space = 8 + Counter::INIT_SPACE)]
Error codes are limited to one block
Anchor 1.0.0 restricts #[error_code] to a single definition per program. If you have multiple error blocks, consolidate them:
#[error_code]
pub enum ErrorCode {
#[msg("Counter overflow")]
Overflow,
#[msg("Unauthorized")]
Unauthorized,
#[msg("Invalid input")]
InvalidInput,
}
Surfpool vs solana-test-validator
anchor test now uses Surfpool by default. If you encounter compatibility issues:
# Fall back to the legacy validator
anchor test --validator legacy
Surfpool is faster but less feature-complete than solana-test-validator. Use the legacy validator when you need full RPC method support or when cloning accounts from mainnet.