Структура програми на Rust

Програми Solana, написані на Rust, мають мінімальні структурні вимоги, що дозволяє гнучко організовувати код. Єдина вимога полягає в тому, що програма повинна мати entrypoint, яка визначає, де починається виконання програми.

Структура програми

Хоча немає суворих правил щодо структури файлів, програми Solana зазвичай дотримуються загального шаблону:

  • entrypoint.rs: визначає точку входу, яка маршрутизує вхідні інструкції.
  • state.rs: визначає стан програми (дані акаунта).
  • instructions.rs: визначає інструкції, які програма може виконувати.
  • processor.rs: визначає обробники інструкцій (функції), які реалізують бізнес-логіку для кожної інструкції.
  • error.rs: визначає користувацькі помилки, які програма може повертати.

Наприклад, дивіться Token Program.

Приклад програми

Щоб продемонструвати, як створити нативну програму на Rust з кількома інструкціями, ми розглянемо просту програму лічильника, яка реалізує дві інструкції:

  1. InitializeCounter: створює та ініціалізує новий акаунт з початковим значенням.
  2. IncrementCounter: збільшує значення, збережене в існуючому акаунті.

Для простоти програма буде реалізована в одному файлі lib.rs, хоча на практиці ви можете розділити більші програми на кілька файлів.

Частина 1: написання програми

Почнемо зі створення програми лічильника. Ми створимо програму, яка може ініціалізувати лічильник зі стартовим значенням та збільшувати його.

Створення нової програми

Спочатку створімо новий Rust-проєкт для нашої Solana-програми.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Ви побачите стандартні файли src/lib.rs та Cargo.toml.

Оновіть поле edition у Cargo.toml на 2021. Інакше під час збірки програми може виникнути помилка.

Додавання залежностей

Тепер додамо необхідні залежності для збірки Solana-програми. Нам потрібні solana-program для основного SDK та borsh для серіалізації.

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

Використання Borsh не є обов'язковим. Проте це поширена бібліотека серіалізації для Solana-програм.

Налаштування crate-type

Solana-програми мають компілюватися як динамічні бібліотеки. Додайте секцію [lib], щоб налаштувати спосіб збірки програми через Cargo.

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

Якщо не додати цю конфігурацію, директорія target/deploy не буде створена під час збірки програми.

Налаштування точки входу програми

Кожна Solana-програма має точку входу — функцію, яка викликається під час виконання програми. Почнемо з додавання необхідних імпортів та налаштування точки входу.

Додайте наступний код до 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(())
}

Макрос entrypoint обробляє десеріалізацію даних input у параметри функції process_instruction.

Функція entrypoint Solana-програми має таку сигнатуру. Розробники можуть створювати власну реалізацію функції entrypoint.

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

Визначення стану програми

Тепер визначимо структуру даних, яка зберігатиметься в наших облікових записах лічильника. Це дані, які зберігатимуться в полі data облікового запису.

Додайте наступний код до lib.rs:

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

Визначення enum інструкцій

Визначимо інструкції, які може виконувати наша програма. Використаємо enum, де кожен варіант представляє окрему інструкцію.

Додайте наступний код до lib.rs:

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

Реалізація десеріалізації інструкцій

Тепер потрібно десеріалізувати instruction_data (необроблені байти) в один з варіантів нашого enum CounterInstruction. Метод Borsh try_from_slice автоматично обробляє це перетворення.

Оновіть функцію process_instruction, щоб використовувати десеріалізацію Borsh:

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(())
}

Маршрутизація інструкцій до обробників

Тепер оновимо основну функцію process_instruction, щоб направляти інструкції до відповідних функцій-обробників.

Цей шаблон маршрутизації є поширеним у програмах Solana. instruction_data десеріалізується у варіант enum, що представляє інструкцію, після чого викликається відповідна функція-обробник. Кожна функція-обробник містить реалізацію для цієї інструкції.

Додайте наступний код до lib.rs, оновлюючи функцію process_instruction та додаючи обробники для інструкцій InitializeCounter і IncrementCounter:

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(())
}

Реалізація обробника ініціалізації

Реалізуємо обробник для створення та ініціалізації нового облікового запису лічильника. Оскільки лише System Program може створювати облікові записи в Solana, використаємо міжпрограмний виклик (CPI), тобто викликатимемо іншу програму з нашої програми.

Наша програма виконує CPI для виклику інструкції create_account System Program. Новий акаунт створюється з нашою програмою як власником, що надає нашій програмі можливість записувати дані в акаунт та ініціалізувати їх.

Додайте наступний код до lib.rs, оновлюючи функцію process_initialize_counter:

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(())
}

Ця інструкція призначена лише для демонстраційних цілей. Вона не включає перевірки безпеки та валідації, які необхідні для продакшн-програм.

Реалізація обробника інкременту

Тепер реалізуємо обробник, який збільшує значення існуючого лічильника. Ця інструкція:

  • Зчитує поле data акаунта для counter_account
  • Десеріалізує його в структуру CounterAccount
  • Збільшує поле count на 1
  • Серіалізує структуру CounterAccount назад у поле data акаунта

Додайте наступний код до lib.rs, оновлюючи функцію process_increment_counter:

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(())
}

Ця інструкція призначена лише для демонстраційних цілей. Вона не включає перевірки безпеки та валідації, які необхідні для продакшн-програм.

Завершена програма

Вітаємо! Ви створили повноцінну програму Solana, яка демонструє базову структуру, спільну для всіх програм Solana:

  • Точка входу: визначає, де починається виконання програми, та направляє всі вхідні запити до відповідних обробників інструкцій
  • Обробка інструкцій: визначає інструкції та пов'язані з ними функції-обробники
  • Управління станом: визначає структури даних акаунтів та керує їхнім станом в акаунтах, що належать програмі
  • Міжпрограмний виклик (CPI): викликає System Program для створення нових акаунтів, що належать програмі

Наступний крок — протестувати програму, щоб переконатися, що все працює правильно.

Створення нової програми

Спочатку створімо новий Rust-проєкт для нашої Solana-програми.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Ви побачите стандартні файли src/lib.rs та Cargo.toml.

Оновіть поле edition у Cargo.toml на 2021. Інакше під час збірки програми може виникнути помилка.

Додавання залежностей

Тепер додамо необхідні залежності для збірки Solana-програми. Нам потрібні solana-program для основного SDK та borsh для серіалізації.

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

Використання Borsh не є обов'язковим. Проте це поширена бібліотека серіалізації для Solana-програм.

Налаштування crate-type

Solana-програми мають компілюватися як динамічні бібліотеки. Додайте секцію [lib], щоб налаштувати спосіб збірки програми через Cargo.

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

Якщо не додати цю конфігурацію, директорія target/deploy не буде створена під час збірки програми.

Налаштування точки входу програми

Кожна Solana-програма має точку входу — функцію, яка викликається під час виконання програми. Почнемо з додавання необхідних імпортів та налаштування точки входу.

Додайте наступний код до 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(())
}

Макрос entrypoint обробляє десеріалізацію даних input у параметри функції process_instruction.

Функція entrypoint Solana-програми має таку сигнатуру. Розробники можуть створювати власну реалізацію функції entrypoint.

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

Визначення стану програми

Тепер визначимо структуру даних, яка зберігатиметься в наших облікових записах лічильника. Це дані, які зберігатимуться в полі data облікового запису.

Додайте наступний код до lib.rs:

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

Визначення enum інструкцій

Визначимо інструкції, які може виконувати наша програма. Використаємо enum, де кожен варіант представляє окрему інструкцію.

Додайте наступний код до lib.rs:

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

Реалізація десеріалізації інструкцій

Тепер потрібно десеріалізувати instruction_data (необроблені байти) в один з варіантів нашого enum CounterInstruction. Метод Borsh try_from_slice автоматично обробляє це перетворення.

Оновіть функцію process_instruction, щоб використовувати десеріалізацію Borsh:

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(())
}

Маршрутизація інструкцій до обробників

Тепер оновимо основну функцію process_instruction, щоб направляти інструкції до відповідних функцій-обробників.

Цей шаблон маршрутизації є поширеним у програмах Solana. instruction_data десеріалізується у варіант enum, що представляє інструкцію, після чого викликається відповідна функція-обробник. Кожна функція-обробник містить реалізацію для цієї інструкції.

Додайте наступний код до lib.rs, оновлюючи функцію process_instruction та додаючи обробники для інструкцій InitializeCounter і IncrementCounter:

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(())
}

Реалізація обробника ініціалізації

Реалізуємо обробник для створення та ініціалізації нового облікового запису лічильника. Оскільки лише System Program може створювати облікові записи в Solana, використаємо міжпрограмний виклик (CPI), тобто викликатимемо іншу програму з нашої програми.

Наша програма виконує CPI для виклику інструкції create_account System Program. Новий акаунт створюється з нашою програмою як власником, що надає нашій програмі можливість записувати дані в акаунт та ініціалізувати їх.

Додайте наступний код до lib.rs, оновлюючи функцію process_initialize_counter:

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(())
}

Ця інструкція призначена лише для демонстраційних цілей. Вона не включає перевірки безпеки та валідації, які необхідні для продакшн-програм.

Реалізація обробника інкременту

Тепер реалізуємо обробник, який збільшує значення існуючого лічильника. Ця інструкція:

  • Зчитує поле data акаунта для counter_account
  • Десеріалізує його в структуру CounterAccount
  • Збільшує поле count на 1
  • Серіалізує структуру CounterAccount назад у поле data акаунта

Додайте наступний код до lib.rs, оновлюючи функцію process_increment_counter:

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(())
}

Ця інструкція призначена лише для демонстраційних цілей. Вона не включає перевірки безпеки та валідації, які необхідні для продакшн-програм.

Завершена програма

Вітаємо! Ви створили повноцінну програму Solana, яка демонструє базову структуру, спільну для всіх програм Solana:

  • Точка входу: визначає, де починається виконання програми, та направляє всі вхідні запити до відповідних обробників інструкцій
  • Обробка інструкцій: визначає інструкції та пов'язані з ними функції-обробники
  • Управління станом: визначає структури даних акаунтів та керує їхнім станом в акаунтах, що належать програмі
  • Міжпрограмний виклик (CPI): викликає System Program для створення нових акаунтів, що належать програмі

Наступний крок — протестувати програму, щоб переконатися, що все працює правильно.

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

Частина 2: тестування програми

Тепер давайте протестуємо нашу програму-лічильник. Ми використаємо LiteSVM — фреймворк для тестування, який дозволяє тестувати програми без розгортання на кластері.

Додавання залежностей для тестування

Спочатку додамо залежності, необхідні для тестування. Ми використаємо litesvm для тестування та solana-sdk.

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

Створення тестового модуля

Тепер додамо тестовий модуль до нашої програми. Почнемо з базової структури та імпортів.

Додайте наступний код до lib.rs, безпосередньо під кодом програми:

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
}
}

Атрибут #[cfg(test)] гарантує, що цей код компілюється лише під час запуску тестів.

Ініціалізація тестового середовища

Налаштуймо тестове середовище за допомогою LiteSVM та поповнимо обліковий запис платника.

LiteSVM симулює середовище виконання Solana, дозволяючи нам тестувати нашу програму без розгортання на реальному кластері.

Додайте наступний код до lib.rs, оновлюючи функцію test_counter_program:

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

Завантаження програми

Тепер нам потрібно зібрати та завантажити нашу програму в тестове середовище. Запустіть команду cargo build-sbf, щоб зібрати програму. Це згенерує файл counter_program.so у директорії target/deploy.

Terminal
$
cargo build-sbf

Переконайтеся, що edition у Cargo.toml встановлено на 2021.

Після збірки ми можемо завантажити програму.

Оновіть функцію test_counter_program, щоб завантажити програму в тестове середовище.

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");

Ви повинні запустити cargo build-sbf перед запуском тестів, щоб згенерувати файл .so. Тест завантажує скомпільовану програму.

Тестування інструкції ініціалізації

Давайте протестуємо інструкцію ініціалізації, створивши новий обліковий запис лічильника з початковим значенням.

Додайте наступний код до lib.rs, оновлюючи функцію test_counter_program:

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);

Перевірка ініціалізації

Після ініціалізації давайте перевіримо, що обліковий запис лічильника було створено правильно з очікуваним значенням.

Додайте наступний код до lib.rs, оновлюючи функцію test_counter_program:

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);

Тестування інструкції інкременту

Тепер давайте протестуємо інструкцію інкременту, щоб переконатися, що вона правильно оновлює значення лічильника.

Додайте наступний код до lib.rs, оновлюючи функцію test_counter_program:

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);

Перевірка фінальних результатів

Нарешті, давайте перевіримо, що інкремент спрацював правильно, перевіривши оновлене значення лічильника.

Додайте наступний код до lib.rs, оновлюючи функцію test_counter_program:

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);

Запустіть тести за допомогою наступної команди. Прапорець --nocapture виводить результат тесту.

Terminal
$
cargo test -- --nocapture

Очікуваний вивід:

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

Додавання залежностей для тестування

Спочатку додамо залежності, необхідні для тестування. Ми використаємо litesvm для тестування та solana-sdk.

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

Створення тестового модуля

Тепер додамо тестовий модуль до нашої програми. Почнемо з базової структури та імпортів.

Додайте наступний код до lib.rs, безпосередньо під кодом програми:

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
}
}

Атрибут #[cfg(test)] гарантує, що цей код компілюється лише під час запуску тестів.

Ініціалізація тестового середовища

Налаштуймо тестове середовище за допомогою LiteSVM та поповнимо обліковий запис платника.

LiteSVM симулює середовище виконання Solana, дозволяючи нам тестувати нашу програму без розгортання на реальному кластері.

Додайте наступний код до lib.rs, оновлюючи функцію test_counter_program:

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

Завантаження програми

Тепер нам потрібно зібрати та завантажити нашу програму в тестове середовище. Запустіть команду cargo build-sbf, щоб зібрати програму. Це згенерує файл counter_program.so у директорії target/deploy.

Terminal
$
cargo build-sbf

Переконайтеся, що edition у Cargo.toml встановлено на 2021.

Після збірки ми можемо завантажити програму.

Оновіть функцію test_counter_program, щоб завантажити програму в тестове середовище.

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");

Ви повинні запустити cargo build-sbf перед запуском тестів, щоб згенерувати файл .so. Тест завантажує скомпільовану програму.

Тестування інструкції ініціалізації

Давайте протестуємо інструкцію ініціалізації, створивши новий обліковий запис лічильника з початковим значенням.

Додайте наступний код до lib.rs, оновлюючи функцію test_counter_program:

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);

Перевірка ініціалізації

Після ініціалізації давайте перевіримо, що обліковий запис лічильника було створено правильно з очікуваним значенням.

Додайте наступний код до lib.rs, оновлюючи функцію test_counter_program:

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);

Тестування інструкції інкременту

Тепер давайте протестуємо інструкцію інкременту, щоб переконатися, що вона правильно оновлює значення лічильника.

Додайте наступний код до lib.rs, оновлюючи функцію test_counter_program:

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);

Перевірка фінальних результатів

Нарешті, давайте перевіримо, що інкремент спрацював правильно, перевіривши оновлене значення лічильника.

Додайте наступний код до lib.rs, оновлюючи функцію test_counter_program:

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);

Запустіть тести за допомогою наступної команди. Прапорець --nocapture виводить результат тесту.

Terminal
$
cargo test -- --nocapture

Очікуваний вивід:

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"

Частина 3: Виклик програми

Тепер давайте додамо клієнтський скрипт для виклику програми.

Створення клієнтського прикладу

Давайте створимо Rust-клієнт для взаємодії з нашою розгорнутою програмою.

Terminal
$
mkdir examples
$
touch examples/client.rs

Додайте наступну конфігурацію до Cargo.toml:

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

Встановіть клієнтські залежності:

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

Реалізація клієнтського коду

Тепер давайте реалізуємо клієнт, який викликатиме нашу розгорнуту програму.

Виконайте наступну команду, щоб отримати ідентифікатор вашої програми з файлу keypair:

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

Додайте клієнтський код до examples/client.rs і замініть program_id на вивід попередньої команди:

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);
}
}
}

Створення клієнтського прикладу

Давайте створимо Rust-клієнт для взаємодії з нашою розгорнутою програмою.

Terminal
$
mkdir examples
$
touch examples/client.rs

Додайте наступну конфігурацію до Cargo.toml:

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

Встановіть клієнтські залежності:

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

Реалізація клієнтського коду

Тепер давайте реалізуємо клієнт, який викликатиме нашу розгорнуту програму.

Виконайте наступну команду, щоб отримати ідентифікатор вашої програми з файлу keypair:

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

Додайте клієнтський код до examples/client.rs і замініть program_id на вивід попередньої команди:

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"

Частина 4: Розгортання програми

Тепер, коли наша програма та клієнт готові, давайте зберемо, розгорнемо та викличемо програму.

Збірка програми

Спочатку зберемо нашу програму.

Terminal
$
cargo build-sbf

Ця команда компілює вашу програму та генерує два важливі файли в target/deploy/:

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

Ви можете переглянути ID вашої програми, виконавши наступну команду:

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

Приклад виводу:

HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Запуск локального валідатора

Для розробки ми використовуватимемо локальний тестовий валідатор.

Спочатку налаштуйте Solana CLI для використання localhost:

Terminal
$
solana config set -ul

Приклад виводу:

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

Тепер запустіть тестовий валідатор в окремому терміналі:

Terminal
$
solana-test-validator

Розгортання програми

Коли валідатор запущено, розгорніть вашу програму на локальному кластері:

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

Приклад виводу:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

Ви можете перевірити розгортання за допомогою команди solana program show з вашим ідентифікатором програми:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Приклад виводу:

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

Запуск клієнта

Коли локальний валідатор все ще працює, виконайте клієнт:

Terminal
$
cargo run --example client

Очікуваний результат:

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

Коли локальний валідатор працює, ви можете переглянути транзакції в Solana Explorer, використовуючи вихідні підписи транзакцій. Зверніть увагу, що кластер у Solana Explorer має бути встановлений на "Custom RPC URL", який за замовчуванням використовує http://localhost:8899, на якому працює solana-test-validator.

Is this page helpful?

Керується

© 2026 Фонд Solana.
Всі права захищені.
Залишайтеся на зв'язку