Rust Program Structure

Solana programs written in Rust have minimal structural requirements, allowing for flexibility in how code is organized. The only requirement is that a program must have an entrypoint, which defines where the execution of a program begins.

Program Structure

While there are no strict rules for file structure, Solana programs typically follow a common pattern:

  • entrypoint.rs: Defines the entrypoint that routes incoming instructions.
  • state.rs: Defines program state (account data).
  • instructions.rs: Defines the instructions that the program can execute.
  • processor.rs: Defines the instruction handlers (functions) that implement the business logic for each instruction.
  • error.rs: Defines custom errors that the program can return.

For example, see the Token Program.

Example Program

To demonstrate how to build a native Rust program with multiple instructions, we'll walk through a simple counter program that implements two instructions:

  1. InitializeCounter: Creates and initializes a new account with an initial value.
  2. IncrementCounter: Increments the value stored in an existing account.

For simplicity, the program will be implemented in a single lib.rs file, though in practice you may want to split larger programs into multiple files.

Part 1: Writing the Program

Let's start by building the counter program. We'll create a program that can initialize a counter with a starting value and increment it.

Create a new program

First, let's create a new Rust project for our Solana program.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

You should see the default src/lib.rs and Cargo.toml files.

Update the edition field in Cargo.toml to 2021. Otherwise, you might encounter an error when building the program.

Add dependencies

Now let's add the necessary dependencies for building a Solana program. We need solana-program for the core SDK and borsh for serialization.

Terminal
$
cargo add solana-program@2.2.0
$
cargo add borsh

There is no requirement to use Borsh. However, it is a commonly used serialization library for Solana programs.

Configure crate-type

Solana programs must be compiled as dynamic libraries. Add the [lib] section to configure how Cargo builds the program.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

If you don't include this config, the target/deploy directory will not be generated when you build the program.

Setup program entrypoint

Every Solana program has an entrypoint, which is the function that gets called when the program is invoked. Let's start with adding the imports we'll need for the program and setting up the entrypoint.

Add the following code to lib.rs:

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Ok(())
}

The entrypoint macro handles the deserialization of the input data into the parameters of the process_instruction function.

A Solana program entrypoint has the following function signature. Developers are free to create their own implementation of the entrypoint function.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

Define program state

Now let's define the data structure that will be stored in our counter accounts. This is the data that will be stored in the data field of the account.

Add the following code to lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u64,
}

Define instruction enum

Let's define the instructions our program can execute. We'll use an enum where each variant represents a different instruction.

Add the following code to lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 },
IncrementCounter,
}

Implement instruction deserialization

Now we need to deserialize the instruction_data (raw bytes) into one of our CounterInstruction enum variants. The Borsh try_from_slice method handles this conversion automatically.

Update the process_instruction function to use Borsh deserialization:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
Ok(())
}

Route instructions to handlers

Now let's update the main process_instruction function to route instructions to their appropriate handler functions.

This routing pattern is common in Solana programs. The instruction_data is deserialized into a variant of an enum representing the instruction, then the appropriate handler function is called. Each handler function includes the implementation for that instruction.

Add the following code to lib.rs updating the process_instruction function and adding the handlers for the InitializeCounter and IncrementCounter instructions:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
match instruction {
CounterInstruction::InitializeCounter { initial_value } => {
process_initialize_counter(program_id, accounts, initial_value)?
}
CounterInstruction::IncrementCounter => {
process_increment_counter(program_id, accounts)?
}
};
Ok(())
}
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
Ok(())
}
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
Ok(())
}

Implement initialize handler

Let's implement the handler to create and initialize a new counter account. Since only the System Program can create accounts on Solana, we'll use a Cross Program Invocation (CPI), essentially calling another program from our program.

Our program makes a CPI to call the System Program's create_account instruction. The new account is created with our program as the owner, giving our program the ability to write to the account and initialize the data.

Add the following code to lib.rs updating the process_initialize_counter function:

lib.rs
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let account_space = 8;
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
invoke(
&system_instruction::create_account(
payer_account.key,
counter_account.key,
required_lamports,
account_space as u64,
program_id,
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
let counter_data = CounterAccount {
count: initial_value,
};
let mut account_data = &mut counter_account.data.borrow_mut()[..];
counter_data.serialize(&mut account_data)?;
msg!("Counter initialized with value: {}", initial_value);
Ok(())
}

This instruction is for demonstration purposes only. It does not include security and validation checks that are required for production programs.

Implement increment handler

Now let's implement the handler that increments an existing counter. This instruction:

  • Reads the account data field for the counter_account
  • Deserializes it into a CounterAccount struct
  • Increments the count field by 1
  • Serializes the CounterAccount struct back into the account's data field

Add the following code to lib.rs updating the process_increment_counter function:

lib.rs
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let mut data = counter_account.data.borrow_mut();
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
counter_data.serialize(&mut &mut data[..])?;
msg!("Counter incremented to: {}", counter_data.count);
Ok(())
}

This instruction is for demonstration purposes only. It does not include security and validation checks that are required for production programs.

Completed Program

Congratulations! You've built a complete Solana program that demonstrates the basic structure shared by all Solana programs:

  • Entrypoint: Defines where program execution begins and routes all incoming requests to appropriate instruction handlers
  • Instruction Handling: Defines instructions and their associated handlers functions
  • State Management: Defines account data structures and manages their state in program owned accounts
  • Cross Program Invocation (CPI): Calls the System Program to create new program owned accounts

The next step is to test the program to ensure everything works correctly.

Create a new program

First, let's create a new Rust project for our Solana program.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

You should see the default src/lib.rs and Cargo.toml files.

Update the edition field in Cargo.toml to 2021. Otherwise, you might encounter an error when building the program.

Add dependencies

Now let's add the necessary dependencies for building a Solana program. We need solana-program for the core SDK and borsh for serialization.

Terminal
$
cargo add solana-program@2.2.0
$
cargo add borsh

There is no requirement to use Borsh. However, it is a commonly used serialization library for Solana programs.

Configure crate-type

Solana programs must be compiled as dynamic libraries. Add the [lib] section to configure how Cargo builds the program.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

If you don't include this config, the target/deploy directory will not be generated when you build the program.

Setup program entrypoint

Every Solana program has an entrypoint, which is the function that gets called when the program is invoked. Let's start with adding the imports we'll need for the program and setting up the entrypoint.

Add the following code to lib.rs:

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Ok(())
}

The entrypoint macro handles the deserialization of the input data into the parameters of the process_instruction function.

A Solana program entrypoint has the following function signature. Developers are free to create their own implementation of the entrypoint function.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

Define program state

Now let's define the data structure that will be stored in our counter accounts. This is the data that will be stored in the data field of the account.

Add the following code to lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u64,
}

Define instruction enum

Let's define the instructions our program can execute. We'll use an enum where each variant represents a different instruction.

Add the following code to lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 },
IncrementCounter,
}

Implement instruction deserialization

Now we need to deserialize the instruction_data (raw bytes) into one of our CounterInstruction enum variants. The Borsh try_from_slice method handles this conversion automatically.

Update the process_instruction function to use Borsh deserialization:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
Ok(())
}

Route instructions to handlers

Now let's update the main process_instruction function to route instructions to their appropriate handler functions.

This routing pattern is common in Solana programs. The instruction_data is deserialized into a variant of an enum representing the instruction, then the appropriate handler function is called. Each handler function includes the implementation for that instruction.

Add the following code to lib.rs updating the process_instruction function and adding the handlers for the InitializeCounter and IncrementCounter instructions:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
match instruction {
CounterInstruction::InitializeCounter { initial_value } => {
process_initialize_counter(program_id, accounts, initial_value)?
}
CounterInstruction::IncrementCounter => {
process_increment_counter(program_id, accounts)?
}
};
Ok(())
}
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
Ok(())
}
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
Ok(())
}

Implement initialize handler

Let's implement the handler to create and initialize a new counter account. Since only the System Program can create accounts on Solana, we'll use a Cross Program Invocation (CPI), essentially calling another program from our program.

Our program makes a CPI to call the System Program's create_account instruction. The new account is created with our program as the owner, giving our program the ability to write to the account and initialize the data.

Add the following code to lib.rs updating the process_initialize_counter function:

lib.rs
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let account_space = 8;
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
invoke(
&system_instruction::create_account(
payer_account.key,
counter_account.key,
required_lamports,
account_space as u64,
program_id,
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
let counter_data = CounterAccount {
count: initial_value,
};
let mut account_data = &mut counter_account.data.borrow_mut()[..];
counter_data.serialize(&mut account_data)?;
msg!("Counter initialized with value: {}", initial_value);
Ok(())
}

This instruction is for demonstration purposes only. It does not include security and validation checks that are required for production programs.

Implement increment handler

Now let's implement the handler that increments an existing counter. This instruction:

  • Reads the account data field for the counter_account
  • Deserializes it into a CounterAccount struct
  • Increments the count field by 1
  • Serializes the CounterAccount struct back into the account's data field

Add the following code to lib.rs updating the process_increment_counter function:

lib.rs
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let mut data = counter_account.data.borrow_mut();
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
counter_data.serialize(&mut &mut data[..])?;
msg!("Counter incremented to: {}", counter_data.count);
Ok(())
}

This instruction is for demonstration purposes only. It does not include security and validation checks that are required for production programs.

Completed Program

Congratulations! You've built a complete Solana program that demonstrates the basic structure shared by all Solana programs:

  • Entrypoint: Defines where program execution begins and routes all incoming requests to appropriate instruction handlers
  • Instruction Handling: Defines instructions and their associated handlers functions
  • State Management: Defines account data structures and manages their state in program owned accounts
  • Cross Program Invocation (CPI): Calls the System Program to create new program owned accounts

The next step is to test the program to ensure everything works correctly.

Cargo.toml
lib.rs
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[dependencies]

Part 2: Testing the Program

Now let's test our counter program. We'll use LiteSVM, a testing framework that lets us test programs without deploying to a cluster.

Add test dependencies

First, let's add the dependencies needed for testing. We'll use litesvm for testing and solana-sdk.

Terminal
$
cargo add litesvm@0.6.1 --dev
$
cargo add solana-sdk@2.2.0 --dev

Create test module

Now let's add a test module to our program. We'll start with the basic scaffold and imports.

Add the following code to lib.rs, directly below the program code:

lib.rs
#[cfg(test)]
mod test {
use super::*;
use litesvm::LiteSVM;
use solana_sdk::{
account::ReadableAccount,
instruction::{AccountMeta, Instruction},
message::Message,
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
#[test]
fn test_counter_program() {
// Test implementation will go here
}
}

The #[cfg(test)] attribute ensures this code is only compiled when running tests.

Initialize test environment

Let's set up the test environment with LiteSVM and fund a payer account.

LiteSVM simulates the Solana runtime environment, allowing us to test our program without deploying to a real cluster.

Add the following code to lib.rs updating the test_counter_program function:

lib.rs
let mut svm = LiteSVM::new();
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to airdrop");

Load the program

Now we need to build and load our program into the test environment. Run the cargo build-sbf command to build the program. This will generate the counter_program.so file in the target/deploy directory.

Terminal
$
cargo build-sbf

Ensure the edition in Cargo.toml is set to 2021.

After building, we can load the program.

Update the test_counter_program function to load the program into the test environment.

lib.rs
let program_keypair = Keypair::new();
let program_id = program_keypair.pubkey();
svm.add_program_from_file(
program_id,
"target/deploy/counter_program.so"
).expect("Failed to load program");

You must run cargo build-sbf before running tests to generate the .so file. The test loads the compiled program.

Test initialization instruction

Let's test the initialization instruction by creating a new counter account with a starting value.

Add the following code to lib.rs updating the test_counter_program function:

lib.rs
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
println!("Testing counter initialization...");
let init_instruction_data =
borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value })
.expect("Failed to serialize instruction");
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&init_instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Initialize transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

Verify initialization

After initialization, let's verify the counter account was created correctly with the expected value.

Add the following code to lib.rs updating the test_counter_program function:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 42);
println!("Counter initialized successfully with value: {}", counter.count);

Test increment instruction

Now let's test the increment instruction to ensure it properly updates the counter value.

Add the following code to lib.rs updating the test_counter_program function:

lib.rs
println!("Testing counter increment...");
let increment_instruction_data =
borsh::to_vec(&CounterInstruction::IncrementCounter)
.expect("Failed to serialize instruction");
let increment_instruction = Instruction::new_with_bytes(
program_id,
&increment_instruction_data,
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Increment transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

Verify final results

Finally, let's verify that the increment worked correctly by checking the updated counter value.

Add the following code to lib.rs updating the test_counter_program function:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 43);
println!("Counter incremented successfully to: {}", counter.count);

Run the tests with the following command. The --nocapture flag prints the output of the test.

Terminal
$
cargo test -- --nocapture

Expected output:

Testing counter initialization...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program log: Counter initialized with value: 42",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter initialized successfully with value: 42
Testing counter increment...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program log: Counter incremented to: 43",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter incremented successfully to: 43

Add test dependencies

First, let's add the dependencies needed for testing. We'll use litesvm for testing and solana-sdk.

Terminal
$
cargo add litesvm@0.6.1 --dev
$
cargo add solana-sdk@2.2.0 --dev

Create test module

Now let's add a test module to our program. We'll start with the basic scaffold and imports.

Add the following code to lib.rs, directly below the program code:

lib.rs
#[cfg(test)]
mod test {
use super::*;
use litesvm::LiteSVM;
use solana_sdk::{
account::ReadableAccount,
instruction::{AccountMeta, Instruction},
message::Message,
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
#[test]
fn test_counter_program() {
// Test implementation will go here
}
}

The #[cfg(test)] attribute ensures this code is only compiled when running tests.

Initialize test environment

Let's set up the test environment with LiteSVM and fund a payer account.

LiteSVM simulates the Solana runtime environment, allowing us to test our program without deploying to a real cluster.

Add the following code to lib.rs updating the test_counter_program function:

lib.rs
let mut svm = LiteSVM::new();
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to airdrop");

Load the program

Now we need to build and load our program into the test environment. Run the cargo build-sbf command to build the program. This will generate the counter_program.so file in the target/deploy directory.

Terminal
$
cargo build-sbf

Ensure the edition in Cargo.toml is set to 2021.

After building, we can load the program.

Update the test_counter_program function to load the program into the test environment.

lib.rs
let program_keypair = Keypair::new();
let program_id = program_keypair.pubkey();
svm.add_program_from_file(
program_id,
"target/deploy/counter_program.so"
).expect("Failed to load program");

You must run cargo build-sbf before running tests to generate the .so file. The test loads the compiled program.

Test initialization instruction

Let's test the initialization instruction by creating a new counter account with a starting value.

Add the following code to lib.rs updating the test_counter_program function:

lib.rs
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
println!("Testing counter initialization...");
let init_instruction_data =
borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value })
.expect("Failed to serialize instruction");
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&init_instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Initialize transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

Verify initialization

After initialization, let's verify the counter account was created correctly with the expected value.

Add the following code to lib.rs updating the test_counter_program function:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 42);
println!("Counter initialized successfully with value: {}", counter.count);

Test increment instruction

Now let's test the increment instruction to ensure it properly updates the counter value.

Add the following code to lib.rs updating the test_counter_program function:

lib.rs
println!("Testing counter increment...");
let increment_instruction_data =
borsh::to_vec(&CounterInstruction::IncrementCounter)
.expect("Failed to serialize instruction");
let increment_instruction = Instruction::new_with_bytes(
program_id,
&increment_instruction_data,
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Increment transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

Verify final results

Finally, let's verify that the increment worked correctly by checking the updated counter value.

Add the following code to lib.rs updating the test_counter_program function:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 43);
println!("Counter incremented successfully to: {}", counter.count);

Run the tests with the following command. The --nocapture flag prints the output of the test.

Terminal
$
cargo test -- --nocapture

Expected output:

Testing counter initialization...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program log: Counter initialized with value: 42",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter initialized successfully with value: 42
Testing counter increment...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program log: Counter incremented to: 43",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter incremented successfully to: 43
Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "1.5.7"
solana-program = "2.2.0"
[dev-dependencies]
litesvm = "0.6.1"
solana-sdk = "2.2.0"

Part 3: Invoking the Program

Now let's add a client script to invoke the program.

Create client example

Let's create a Rust client to interact with our deployed program.

Terminal
$
mkdir examples
$
touch examples/client.rs

Add the following configuration to Cargo.toml:

Cargo.toml
[[example]]
name = "client"
path = "examples/client.rs"

Install the client dependencies:

Terminal
$
cargo add solana-client@2.2.0 --dev
$
cargo add tokio --dev

Implement client code

Now let's implement the client that will invoke our deployed program.

Run the following command to get your program ID from the keypair file:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

Add the client code to examples/client.rs and replace the program_id with the output of the previous command:

examples/client.rs
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
examples/client.rs
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
commitment_config::CommitmentConfig,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
use std::str::FromStr;
use counter_program::CounterInstruction;
#[tokio::main]
async fn main() {
// Replace with your actual program ID from deployment
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
// Connect to local cluster
let rpc_url = String::from("http://localhost:8899");
let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());
// Generate a new keypair for paying fees
let payer = Keypair::new();
// Request airdrop of 1 SOL for transaction fees
println!("Requesting airdrop...");
let airdrop_signature = client
.request_airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to request airdrop");
// Wait for airdrop confirmation
loop {
if client
.confirm_transaction(&airdrop_signature)
.unwrap_or(false)
{
break;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
println!("Airdrop confirmed");
println!("\nInitializing counter...");
let counter_keypair = Keypair::new();
let initial_value = 100u64;
// Serialize the initialize instruction data
let instruction_data = borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value })
.expect("Failed to serialize instruction");
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
let mut transaction =
Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));
let blockhash = client
.get_latest_blockhash()
.expect("Failed to get blockhash");
transaction.sign(&[&payer, &counter_keypair], blockhash);
match client.send_and_confirm_transaction(&transaction) {
Ok(signature) => {
println!("Counter initialized!");
println!("Transaction: {}", signature);
println!("Counter address: {}", counter_keypair.pubkey());
}
Err(err) => {
eprintln!("Failed to initialize counter: {}", err);
return;
}
}
println!("\nIncrementing counter...");
// Serialize the increment instruction data
let increment_data = borsh::to_vec(&CounterInstruction::IncrementCounter)
.expect("Failed to serialize instruction");
let increment_instruction = Instruction::new_with_bytes(
program_id,
&increment_data,
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
let mut transaction =
Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &counter_keypair], blockhash);
match client.send_and_confirm_transaction(&transaction) {
Ok(signature) => {
println!("Counter incremented!");
println!("Transaction: {}", signature);
}
Err(err) => {
eprintln!("Failed to increment counter: {}", err);
}
}
}

Create client example

Let's create a Rust client to interact with our deployed program.

Terminal
$
mkdir examples
$
touch examples/client.rs

Add the following configuration to Cargo.toml:

Cargo.toml
[[example]]
name = "client"
path = "examples/client.rs"

Install the client dependencies:

Terminal
$
cargo add solana-client@2.2.0 --dev
$
cargo add tokio --dev

Implement client code

Now let's implement the client that will invoke our deployed program.

Run the following command to get your program ID from the keypair file:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

Add the client code to examples/client.rs and replace the program_id with the output of the previous command:

examples/client.rs
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "1.5.7"
solana-program = "2.2.0"
[dev-dependencies]
litesvm = "0.6.1"
solana-sdk = "2.2.0"
solana-client = "2.2.0"
tokio = "1.47.1"
[[example]]
name = "client"
path = "examples/client.rs"

Part 4: Deploying the Program

Now that we have our program and client ready, let's build, deploy, and invoke the program.

Build the program

First, let's build our program.

Terminal
$
cargo build-sbf

This command compiles your program and generates two important files in target/deploy/:

counter_program.so # The compiled program
counter_program-keypair.json # Keypair for the program ID

You can view your program's ID by running the following command:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

Example output:

HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Start local validator

For development, we'll use a local test validator.

First, configure the Solana CLI to use localhost:

Terminal
$
solana config set -ul

Example output:

Config File: ~/.config/solana/cli/config.yml
RPC URL: http://localhost:8899
WebSocket URL: ws://localhost:8900/ (computed)
Keypair Path: ~/.config/solana/id.json
Commitment: confirmed

Now start the test validator in a separate terminal:

Terminal
$
solana-test-validator

Deploy the program

With the validator running, deploy your program to the local cluster:

Terminal
$
solana program deploy ./target/deploy/counter_program.so

Example output:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

You can verify the deployment using the solana program show command with your program ID:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Example output:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Owner: BPFLoaderUpgradeab1e11111111111111111111111
ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuM
Authority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1
Last Deployed In Slot: 16
Data Length: 82696 (0x14308) bytes
Balance: 0.57676824 SOL

Run the client

With the local validator still running, execute the client:

Terminal
$
cargo run --example client

Expected output:

Requesting airdrop...
Airdrop confirmed
Initializing counter...
Counter initialized!
Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14k
Counter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9Zfofcy
Incrementing counter...
Counter incremented!
Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS

With the local validator running, you can view the transactions on Solana Explorer using the output transaction signatures. Note the cluster on Solana Explorer must be set to "Custom RPC URL", which defaults to http://localhost:8899 that the solana-test-validator is running on.

Is this page helpful?

सामग्री तालिका

पृष्ठ संपादित करें