Build Your First Solana Program

This quickstart uses the Anchor framework starter project generated by anchor init. You will create the project locally, run its tests, build the program, and walk through the program code that Anchor generates.

Prerequisites

Before you start, install the Solana development tools. The installation includes Rust, the Solana CLI, and the Anchor CLI.

Use Anchor CLI version 1.1.2 or higher for this template. Check your installed version:

Terminal
$
anchor --version

Create the project

Run the following commands in your terminal:

Terminal
$
anchor init my-program
$
cd my-program

The starter project includes one Solana program under programs/my-program. The program includes two instructions: one to initialize a counter account and one to increment the counter.

Some parts of the template demonstrate common Solana program patterns: deriving PDA account addresses, making a Cross Program Invocation (CPI) to transfer SOL, and using custom error checks to stop an instruction when a condition fails.

Anchor.toml
Cargo.toml
Cargo.toml
lib.rs
constants.rs
error.rs
instructions.rs
initialize.rs
increment.rs
state.rs
test_initialize.rs

Build Program

Run anchor build to compile the starter program:

Terminal
$
anchor build

The compiled program is written to target/deploy/my_program.so. When the program is deployed, the contents of this .so file are stored in an account onchain.

Run Test

Run the default test:

Terminal
$
anchor test

This template's Anchor.toml uses the Rust test command:

Anchor.toml
skip_local_validator = true
[scripts]
test = "cargo test"

The test loads the compiled program into LiteSVM, creates a payer, sends the initialize and increment instructions, then checks the counter account state.

Running anchor test also compiles the program, so you do not need to run anchor build first when testing locally.

Deploy Program

Local tests are the fastest feedback loop. When you are ready to deploy to a network, for example devnet, build first, then deploy to a cluster.

Deploying a Solana program requires SOL because the program is stored in an account, and the account must pay for the space it uses. On devnet, request free devnet SOL from the Solana Faucet or with the Solana CLI:

Terminal
$
solana airdrop 2 --url devnet
Terminal
$
anchor build
$
anchor deploy --provider.cluster devnet

Source files

The src directory contains the Solana program. Anchor's Program Structure docs explain the core macros used here, including declare_id!, #[program], #[derive(Accounts)], and #[account]. This section walks through the template files.

lib.rs

lib.rs is the program entrypoint. It connects the source files, defines the program address, and defines the program instructions that users can call.

programs/my-program/src/lib.rs
pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang::prelude::*;
pub use constants::*;
pub use instructions::*;
pub use state::*;
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");
#[program]
pub mod my_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
crate::instructions::initialize::handle_initialize(ctx)
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
crate::instructions::increment::handle_increment(ctx)
}
}
Anchor.toml
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
lib.rs
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");

The same program address appears in configuration and code. Anchor.toml tells Anchor which address to deploy or call for a cluster. declare_id! defines the program address in the program for security checks.

constants.rs

constants.rs keeps shared values in one place. In this template, COUNTER_SEED derives the counter PDA, HELLO_WORLD_LAMPORTS is transferred during initialization, and MAX_COUNT is checked before incrementing.

programs/my-program/src/constants.rs
use anchor_lang::prelude::*;
#[constant]
pub const COUNTER_SEED: &[u8] = b"counter";
#[constant]
pub const HELLO_WORLD_LAMPORTS: u64 = 1;
#[constant]
pub const MAX_COUNT: u64 = 10;
initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
// ...
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
// ...
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
Ok(())
}

initialize.rs uses two constants:

  • COUNTER_SEED derives the counter PDA address.
  • HELLO_WORLD_LAMPORTS sets the amount transferred from the payer to the counter account.
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
Ok(())
}

increment.rs uses MAX_COUNT as the counter's upper bound. If the current count is already at the maximum, require! returns CounterOverflow and the account data is not changed.

state.rs

state.rs defines custom data types for accounts the program creates and owns. The program defines instructions to create, initialize, and update that data, but the counter data is not stored inside the program itself. It is stored in a separate account with its own address.

programs/my-program/src/state.rs
use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
pub authority: Pubkey,
}
initialize.rs
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
Ok(())
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
ctx.accounts.counter.count += 1;
Ok(())
}

state.rs defines the Counter account data. The instruction files use that type when they create and update the account:

  • Counter::INIT_SPACE sizes the account for the fields defined in state.rs.
  • count and authority are the field values written when the account is initialized.
  • count += 1 updates the stored counter value after validation passes.

error.rs

error.rs defines the program's custom errors. In this template, the errors demonstrate how instruction handlers stop when a caller is not allowed to update the counter or the counter has already reached MAX_COUNT.

programs/my-program/src/error.rs
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Only the counter authority can update this counter")]
Unauthorized,
#[msg("Counter has reached the maximum value")]
CounterOverflow,
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

error.rs names the errors that the instruction can return:

  • ErrorCode::Unauthorized is returned when the signer is not the authority stored in the counter account.
  • ErrorCode::CounterOverflow is returned when the counter has already reached MAX_COUNT.

instructions.rs

instructions.rs connects the instruction files to the program crate so lib.rs can access the initialize and increment instruction code. Each instruction file defines the accounts required by that instruction and the handler logic that runs after Anchor validates those accounts.

programs/my-program/src/instructions.rs
pub mod initialize;
pub mod increment;
pub use initialize::*;
pub use increment::*;

initialize.rs

initialize.rs defines the accounts required to create the counter account, and then writes the account's first values. The #[derive(Accounts)] struct uses Anchor account constraints to say which accounts are required and how the new counter account is created.

programs/my-program/src/instructions/initialize.rs
use anchor_lang::prelude::*;
use crate::{constants::*, state::Counter};
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Account Context

The Initialize struct defines the accounts that must be included when a user calls the initialize instruction. Anchor checks these accounts before the handler runs.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Payer Account

The payer account pays to create the counter account. The Signer<'info> type means the payer must sign the transaction, and #[account(mut)] means the payer account can be changed because lamports will be deducted.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Counter Account

The counter account stores the Counter data from state.rs. init tells Anchor to create this account before the handler runs, and payer = payer tells Anchor which account pays for creation.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Account Size

The space constraint tells Anchor how much account data to allocate. Anchor stores an 8-byte discriminator first, then the bytes needed for the Counter fields. The discriminator lets Anchor recognize this account as a Counter account before it deserializes the account data.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Counter Address

The seeds and bump constraints define the expected PDA address for the counter account. Anchor verifies that the provided counter account matches that address. The template uses a PDA so users can derive the counter address from the program ID and seed, making the counter address deterministic.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

System Program

The system_program account is required because creating a new account uses the System Program.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Handler Function

The handle_initialize function runs after Anchor validates the accounts in Initialize. The ctx value gives the handler access to those checked accounts.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Initial Data

The handler writes the first values into the new counter account. The count starts at 0, and the payer becomes the authority allowed to increment the counter later.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Transfer Accounts

This transfer CPI is included only to demonstrate how a CPI passes accounts to another program. The Transfer struct lists the accounts used by the System Program transfer.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

CPI Context

CpiContext::new combines the program being called with the accounts passed to that program. This is the basic shape of a CPI: choose the program to invoke, collect the accounts that program expects, then pass both into the invocation. Here, the program being called is the System Program.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Invoke Transfer

anchor_lang::system_program::transfer invokes the System Program transfer instruction. In this template, the transfer is a small example of calling another program from your program. If the transfer CPI fails, the initialize instruction fails as well.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Log Message

msg! writes a message to the program logs. Ok(()) indicates the instruction returned successfully.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Account Context

The Initialize struct defines the accounts that must be included when a user calls the initialize instruction. Anchor checks these accounts before the handler runs.

Payer Account

The payer account pays to create the counter account. The Signer<'info> type means the payer must sign the transaction, and #[account(mut)] means the payer account can be changed because lamports will be deducted.

Counter Account

The counter account stores the Counter data from state.rs. init tells Anchor to create this account before the handler runs, and payer = payer tells Anchor which account pays for creation.

Account Size

The space constraint tells Anchor how much account data to allocate. Anchor stores an 8-byte discriminator first, then the bytes needed for the Counter fields. The discriminator lets Anchor recognize this account as a Counter account before it deserializes the account data.

Counter Address

The seeds and bump constraints define the expected PDA address for the counter account. Anchor verifies that the provided counter account matches that address. The template uses a PDA so users can derive the counter address from the program ID and seed, making the counter address deterministic.

System Program

The system_program account is required because creating a new account uses the System Program.

Handler Function

The handle_initialize function runs after Anchor validates the accounts in Initialize. The ctx value gives the handler access to those checked accounts.

Initial Data

The handler writes the first values into the new counter account. The count starts at 0, and the payer becomes the authority allowed to increment the counter later.

Transfer Accounts

This transfer CPI is included only to demonstrate how a CPI passes accounts to another program. The Transfer struct lists the accounts used by the System Program transfer.

CPI Context

CpiContext::new combines the program being called with the accounts passed to that program. This is the basic shape of a CPI: choose the program to invoke, collect the accounts that program expects, then pass both into the invocation. Here, the program being called is the System Program.

Invoke Transfer

anchor_lang::system_program::transfer invokes the System Program transfer instruction. In this template, the transfer is a small example of calling another program from your program. If the transfer CPI fails, the initialize instruction fails as well.

Log Message

msg! writes a message to the program logs. Ok(()) indicates the instruction returned successfully.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

increment.rs

increment.rs defines the accounts required to update an existing counter account. The handler checks that the signer is the stored authority, checks that the count has not reached the specified MAX_COUNT, and then increments the count.

programs/my-program/src/instructions/increment.rs
use anchor_lang::prelude::*;
use crate::{constants::*, error::ErrorCode, state::Counter};
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Account Context

The Increment struct defines the accounts that must be included when a user calls the increment instruction. Anchor checks these accounts before the handler runs.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Counter Account

The counter account stores the Counter data. The mut constraint allows the handler to update the stored count, and the seeds and bump constraints verify the counter PDA address.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Authority Signer

The authority account must sign the transaction. The handler later checks that this signer matches the authority stored in the counter account.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Handler Function

The handle_increment function runs after Anchor validates the accounts in Increment. The ctx value gives the handler access to those checked accounts.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Authority Check

The first check makes sure the signer is allowed to update this counter. If the signer's address does not match counter.authority, the instruction stops with ErrorCode::Unauthorized. This demonstrates application-level authorization: the program owns the counter data, but it implements a rule for which signer may change that data.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Max Count Check

The second check keeps the counter from going past MAX_COUNT. If the counter is already at the limit, the instruction stops with ErrorCode::CounterOverflow. This limit is an artificial rule in the template so you can see how custom errors stop an instruction before account data is changed.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Update Count

Only after both checks pass does the handler update the account data. This line adds one to the stored counter value.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Log Message

msg! writes the updated count to the program logs. Ok(()) indicates the instruction returned successfully.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Account Context

The Increment struct defines the accounts that must be included when a user calls the increment instruction. Anchor checks these accounts before the handler runs.

Counter Account

The counter account stores the Counter data. The mut constraint allows the handler to update the stored count, and the seeds and bump constraints verify the counter PDA address.

Authority Signer

The authority account must sign the transaction. The handler later checks that this signer matches the authority stored in the counter account.

Handler Function

The handle_increment function runs after Anchor validates the accounts in Increment. The ctx value gives the handler access to those checked accounts.

Authority Check

The first check makes sure the signer is allowed to update this counter. If the signer's address does not match counter.authority, the instruction stops with ErrorCode::Unauthorized. This demonstrates application-level authorization: the program owns the counter data, but it implements a rule for which signer may change that data.

Max Count Check

The second check keeps the counter from going past MAX_COUNT. If the counter is already at the limit, the instruction stops with ErrorCode::CounterOverflow. This limit is an artificial rule in the template so you can see how custom errors stop an instruction before account data is changed.

Update Count

Only after both checks pass does the handler update the account data. This line adds one to the stored counter value.

Log Message

msg! writes the updated count to the program logs. Ok(()) indicates the instruction returned successfully.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Test file

programs/my-program/tests/test_initialize.rs is a Rust integration test. It does not start a local validator. Instead, it loads the compiled .so file into LiteSVM, builds transactions that call the program, and reads the counter account after each transaction. The test builds instructions for a Solana transaction by specifying the program ID of the program to invoke, providing instruction data, and passing the required accounts.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);

The Initialize account context defines the accounts required by the initialize instruction. The test passes those same accounts to the generated my_program::accounts::Initialize helper:

  • payer is passed as payer: payer.pubkey().
  • counter is passed as counter.
  • system_program is passed as system_program::ID.

my_program::instruction::Initialize {}.data() creates the instruction data. This is where instruction arguments would be encoded, but this initialize instruction does not require any arguments.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);

The Increment account context defines the accounts required by the increment instruction. The test passes those same accounts to the generated my_program::accounts::Increment helper:

  • counter is passed as counter.
  • authority is passed as authority: payer.pubkey().

my_program::instruction::Increment {}.data() creates the instruction data. This is where instruction arguments would be encoded, but this increment instruction does not require any arguments.

programs/my-program/tests/test_initialize.rs
use {
anchor_lang::{
prelude::Pubkey,
solana_program::{instruction::Instruction, system_program},
AccountDeserialize, InstructionData, ToAccountMetas,
},
litesvm::LiteSVM,
solana_keypair::Keypair,
solana_message::{Message, VersionedMessage},
solana_signer::Signer,
solana_transaction::versioned::VersionedTransaction,
};
#[test]
fn test_initialize() {
let program_id = my_program::id();
let payer = Keypair::new();
let counter = Pubkey::find_program_address(
&[my_program::constants::COUNTER_SEED],
&program_id,
)
.0;
let mut svm = LiteSVM::new();
let bytes = include_bytes!(concat!(
env!("CARGO_TARGET_TMPDIR"),
"/../deploy/my_program.so"
));
svm.add_program(program_id, bytes).unwrap();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer.pubkey());
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 1);
assert_eq!(counter_state.authority, payer.pubkey());
}

Project configuration

The root project files tell Anchor and Cargo how to build, test, and deploy the program. For a full reference, see the Anchor docs for Anchor.toml configuration and the Anchor CLI.

Anchor.toml
skip_local_validator = true
[toolchain]
[features]
resolution = true
skip-lint = false
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "cargo test"
[hooks]

Is this page helpful?

Tabla de Contenidos

Editar Página