learn.sol
Anchor Programs • First Anchor Program

First Anchor Program

Build a small counter program in Anchor one step at a time, understand every account in the instruction context, and verify the behavior with tests.

This is the lesson where Anchor stops being abstract.

You are going to build one small program, read every part of it, and test it properly.

The program is intentionally simple.

That is a feature, not a limitation.

A first Anchor program should teach the model clearly:

  • one state account
  • one authority
  • one initialization path
  • one update path
  • one test loop

What You Are Building

You will build a counter program with two instructions:

  1. initialize creates a counter account
  2. increment increases the stored value by 1

That is enough for the first real build.

You do not need decrement, reset, PDA seeds, or token logic yet.

The point is to understand the full path from account validation to state change.

Before You Touch The Code

Start from a fresh Anchor workspace that already passes:

anchor build
anchor test

If the default workspace still does not pass, stop here and fix setup first.

Do not layer program logic on top of a broken environment.

Step 1: Understand The Job Of This Program

This program stores two pieces of state:

  • authority: who controls the counter
  • count: the current numeric value

That gives us one simple rule:

only the stored authority should be allowed to increment the counter.

Everything in the code will support that rule.

Step 2: Replace The Generated Program With A Minimal Counter

Open:

  • programs/<your-program-name>/src/lib.rs

Replace the generated example with this structure.

use anchor_lang::prelude::*;

// Keep the generated program ID from your own workspace here.
declare_id!("<your-generated-program-id>");

#[program]
pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        counter.authority = ctx.accounts.authority.key();
        counter.count = 0;
        Ok(())
    }

    pub fn increment(ctx: Context<Increment>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        counter.count = counter
            .count
            .checked_add(1)
            .ok_or(CounterError::Overflow)?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,

    #[account(
        init,
        payer = authority,
        space = 8 + Counter::INIT_SPACE,
    )]
    pub counter: Account<'info, Counter>,

    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Increment<'info> {
    pub authority: Signer<'info>,

    #[account(mut, has_one = authority)]
    pub counter: Account<'info, Counter>,
}

#[account]
#[derive(InitSpace)]
pub struct Counter {
    pub authority: Pubkey,
    pub count: u64,
}

#[error_code]
pub enum CounterError {
    #[msg("Counter overflow")]
    Overflow,
}

Keep the declare_id! value aligned with the program ID generated in your workspace.

Do not paste a fake ID and assume Anchor will sort it out later.

Step 3: Read The Program In The Right Order

Most beginners read the file top to bottom and still miss the model.

Read it in this order instead:

  1. Counter state struct
  2. Initialize account context
  3. Increment account context
  4. initialize handler
  5. increment handler

That order makes the logic easier to parse.

First, the state

#[account]
#[derive(InitSpace)]
pub struct Counter {
    pub authority: Pubkey,
    pub count: u64,
}

This is the account data that will live on-chain.

Why each field exists:

  • authority stores who is allowed to control this counter later
  • count stores the actual value

Then, the initialization context

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,

    #[account(
        init,
        payer = authority,
        space = 8 + Counter::INIT_SPACE,
    )]
    pub counter: Account<'info, Counter>,

    pub system_program: Program<'info, System>,
}

This context is doing a lot of real work.

What each piece means:

  • authority: Signer<'info> means the caller must sign
  • #[account(mut)] means Anchor may debit lamports from that signer to fund account creation
  • init means create the new counter account
  • payer = authority means the signer pays rent for that new account
  • space = 8 + Counter::INIT_SPACE allocates enough bytes for the discriminator plus the stored data
  • system_program is required because account creation happens through the system program

That is one of the core Anchor ideas again.

The account context expresses the setup and validation before your business logic runs.

Then, the update context

#[derive(Accounts)]
pub struct Increment<'info> {
    pub authority: Signer<'info>,

    #[account(mut, has_one = authority)]
    pub counter: Account<'info, Counter>,
}

This is the security rule for the whole program.

What it means:

  • the caller must sign
  • the counter account can be mutated
  • the stored counter.authority must match the provided signer

If that has_one rule fails, increment never runs.

That is exactly what you want.

Step 4: Understand The Handlers

initialize

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let counter = &mut ctx.accounts.counter;
    counter.authority = ctx.accounts.authority.key();
    counter.count = 0;
    Ok(())
}

This handler does not do much.

That is good.

The context already handled the heavy setup work.

The handler only needs to write the initial state.

increment

pub fn increment(ctx: Context<Increment>) -> Result<()> {
    let counter = &mut ctx.accounts.counter;
    counter.count = counter
        .count
        .checked_add(1)
        .ok_or(CounterError::Overflow)?;
    Ok(())
}

Two important details here:

  • the handler only runs after the account checks pass
  • it uses checked_add instead of assuming overflow never matters

That second point is small, but it is the right habit.

Even a toy program should not silently wrap numeric state.

Step 5: Build Before You Write Tests

Run:

anchor build

Do this now, before touching the test file.

If the program does not compile, the test layer will only make the problem noisier.

What you should check after the build:

  • the build succeeds
  • the IDL in target/idl/ updates
  • no errors appear around missing derives, account space, or type mismatches

Step 6: Replace The Default Test With A Counter Test

Open:

  • tests/<your-program-name>.ts

Replace the generated test with this version.

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";

describe("counter", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.Counter as Program;
  const counter = anchor.web3.Keypair.generate();

  it("initializes the counter", async () => {
    await program.methods
      .initialize()
      .accounts({
        authority: provider.wallet.publicKey,
        counter: counter.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId,
      })
      .signers([counter])
      .rpc();

    const account = await program.account.counter.fetch(counter.publicKey);

    if (!account.authority.equals(provider.wallet.publicKey)) {
      throw new Error("expected authority to match provider wallet");
    }

    if (account.count.toNumber() !== 0) {
      throw new Error("expected count to start at 0");
    }
  });

  it("increments the counter", async () => {
    await program.methods
      .increment()
      .accounts({
        authority: provider.wallet.publicKey,
        counter: counter.publicKey,
      })
      .rpc();

    const account = await program.account.counter.fetch(counter.publicKey);

    if (account.count.toNumber() !== 1) {
      throw new Error("expected count to be 1 after increment");
    }
  });
});

Step 7: Read The Test Like A Client Flow

Do not treat the test file as magic either.

It is just a client calling your program.

What happens in the initialize test

await program.methods
  .initialize()
  .accounts({
    authority: provider.wallet.publicKey,
    counter: counter.publicKey,
    systemProgram: anchor.web3.SystemProgram.programId,
  })
  .signers([counter])
  .rpc();

This is the client saying:

  • call the initialize instruction
  • pass the authority signer
  • pass the new counter account public key
  • pass the system program
  • sign with the newly created counter keypair because the account is being created

That last line matters.

If you forget signers([counter]), account creation will fail because the new account keypair is not participating correctly in the transaction.

What happens after the instruction runs

const account = await program.account.counter.fetch(counter.publicKey);

This fetches the on-chain counter account and lets the test inspect the stored state.

That is how you prove the handler actually changed data.

Step 8: Run The Full Loop

Now run:

anchor test

If everything is wired correctly, the test flow should:

  • build the program
  • deploy it to localnet
  • run both tests
  • show success for initialize and increment

That is your first full Anchor loop.

Common Failures In This Lesson

Program name mismatch in the test

This line depends on the generated workspace name:

const program = anchor.workspace.Counter as Program;

If your actual program name is different, update it.

The test has to match your workspace, not a copied tutorial name.

Wrong declare_id!

If declare_id! is out of sync with the workspace configuration, build or deploy steps can fail in confusing ways.

Keep the generated program ID consistent.

Missing signers([counter])

The initialize path creates a new account.

If the new keypair does not sign, initialization fails.

Missing mut on the counter account

If the counter will be written to, the account context must mark it mutable.

Forgetting has_one = authority

If you remove this, anyone who can provide the account could increment it.

That is exactly the kind of access-control bug Anchor is meant to help you avoid.

Why This Lesson Matters

This one small program teaches the full Anchor pattern:

  • define state
  • define account contexts
  • express security rules in constraints
  • write thin handlers
  • prove the behavior with tests

That pattern is the foundation for everything later:

  • PDAs
  • token flows
  • CPI
  • escrow

One Good Extension, Not Five

If you want one extra exercise after this lesson, add:

  • set_count(value: u64)

That extension is useful because it forces you to repeat the exact same pattern with one instruction argument.

Do not add five more features yet.

The point of the first program is clarity.

What Comes Next

The next lesson changes one major thing.

Instead of letting accounts live at arbitrary keypairs, you will derive them deterministically with PDAs.

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px
    First Anchor Program | learn.sol