Структура програми на 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.

Точка входу програми Solana entrypoint має наступний підпис функції. Розробники можуть вільно створювати власну реалізацію функції 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), де кожен варіант представляє різну інструкцію.

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

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

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

Тепер нам потрібно десеріалізувати instruction_data (необроблені байти) в один із варіантів нашого переліку 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 десеріалізується у варіант переліку, що представляє інструкцію, після чого викликається відповідна функція-обробник. Кожна функція-обробник містить реалізацію для цієї інструкції.

Додайте наступний код до 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, ми використаємо Cross Program Invocation (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:

  • Точка входу: Визначає, де починається виконання програми та маршрутизує всі вхідні запити до відповідних обробників інструкцій
  • Обробка інструкцій: Визначає інструкції та пов'язані з ними функції обробників
  • Управління станом: Визначає структури даних акаунтів і керує їхнім станом в акаунтах, що належать програмі
  • Cross Program Invocation (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.

Точка входу програми Solana entrypoint має наступний підпис функції. Розробники можуть вільно створювати власну реалізацію функції 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), де кожен варіант представляє різну інструкцію.

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

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

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

Тепер нам потрібно десеріалізувати instruction_data (необроблені байти) в один із варіантів нашого переліку 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 десеріалізується у варіант переліку, що представляє інструкцію, після чого викликається відповідна функція-обробник. Кожна функція-обробник містить реалізацію для цієї інструкції.

Додайте наступний код до 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, ми використаємо Cross Program Invocation (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:

  • Точка входу: Визначає, де починається виконання програми та маршрутизує всі вхідні запити до відповідних обробників інструкцій
  • Обробка інструкцій: Визначає інструкції та пов'язані з ними функції обробників
  • Управління станом: Визначає структури даних акаунтів і керує їхнім станом в акаунтах, що належать програмі
  • Cross Program Invocation (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

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

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

Виконайте наступну команду, щоб отримати ID вашої програми з файлу 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

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

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

Виконайте наступну команду, щоб отримати ID вашої програми з файлу 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

Запуск локального validator

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

Спочатку налаштуйте 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

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

Terminal
$
solana-test-validator

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

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

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

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

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

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

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

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

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

Terminal
$
cargo run --example client

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

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

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

Is this page helpful?