Структура програми на Rust
Програми Solana, написані на Rust, мають мінімальні структурні вимоги, що
дозволяє гнучко організовувати код. Єдина вимога полягає в тому, що програма
повинна мати entrypoint
, який визначає, де починається виконання програми.
Структура програми
Хоча немає суворих правил щодо структури файлів, програми Solana зазвичай слідують загальному шаблону:
entrypoint.rs
: Визначає точку входу, яка маршрутизує вхідні інструкції.state.rs
: Визначають специфічний для програми стан (дані облікового запису).instructions.rs
: Визначає інструкції, які програма може виконувати.processor.rs
: Визначає обробники інструкцій (функції), які реалізують бізнес-логіку для кожної інструкції.error.rs
: Визначає користувацькі помилки, які програма може повертати.
Ви можете знайти приклади в Бібліотеці програм Solana.
Приклад програми
Щоб продемонструвати, як створити нативну програму на Rust з кількома інструкціями, ми розглянемо просту програму-лічильник, яка реалізує дві інструкції:
InitializeCounter
: Створює та ініціалізує новий обліковий запис з початковим значенням.IncrementCounter
: Збільшує значення, що зберігається в існуючому обліковому записі.
Для простоти програма буде реалізована в одному файлі lib.rs
, хоча на практиці
ви можете розділити більші програми на кілька файлів.
Створення нової програми
Спочатку створіть новий проект Rust, використовуючи стандартну команду
cargo init
з прапорцем --lib
.
cargo init counter_program --lib
Перейдіть до каталогу проекту. Ви повинні побачити типові файли src/lib.rs
та
Cargo.toml
cd counter_program
Далі додайте залежність solana-program
. Це мінімальна залежність, необхідна
для створення програми Solana.
cargo add solana-program@1.18.26
Далі додайте наступний фрагмент до Cargo.toml
. Якщо ви не включите цю
конфігурацію, директорія target/deploy
не буде створена під час збірки
програми.
[lib]crate-type = ["cdylib", "lib"]
Ваш файл Cargo.toml
повинен виглядати наступним чином:
[package]name = "counter_program"version = "0.1.0"edition = "2021"[lib]crate-type = ["cdylib", "lib"][dependencies]solana-program = "1.18.26"
Точка входу програми
Точка входу програми Solana — це функція, яка викликається при виклику програми. Точка входу має наступне базове визначення, і розробники можуть вільно створювати власну реалізацію функції точки входу.
Для простоти використовуйте макрос
entrypoint!
з пакету solana_program
, щоб визначити точку входу у вашій програмі.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Замініть стандартний код у lib.rs
наступним кодом. Цей фрагмент:
- Імпортує необхідні залежності з
solana_program
- Визначає точку входу програми за допомогою макросу
entrypoint!
- Реалізує функцію
process_instruction
, яка направлятиме інструкції до відповідних функцій-обробників
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 {// Your program logicOk(())}
Макрос entrypoint!
вимагає функцію з наступним
сигнатурою типу
як аргумент:
pub type ProcessInstruction =fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
Коли програма Solana викликається, точка входу
десеріалізує
вхідні дані
(надані як байти) у три значення та передає їх функції
process_instruction
:
program_id
: Публічний ключ програми, яка викликається (поточна програма)accounts
:AccountInfo
для облікових записів, необхідних для інструкції, що викликаєтьсяinstruction_data
: Додаткові дані, передані програмі, які визначають інструкцію для виконання та її необхідні аргументи
Ці три параметри безпосередньо відповідають даним, які клієнти повинні надати при створенні інструкції для виклику програми.
Визначення стану програми
Під час створення програми Solana ви зазвичай почнете з визначення стану вашої програми - даних, які будуть зберігатися в облікових записах, створених і належних вашій програмі.
Стан програми визначається за допомогою структур Rust, які представляють макет даних облікових записів вашої програми. Ви можете визначити кілька структур для представлення різних типів облікових записів для вашої програми.
Під час роботи з обліковими записами вам потрібен спосіб перетворення типів даних вашої програми в байти та з байтів, що зберігаються в полі даних облікового запису:
- Серіалізація: Перетворення ваших типів даних у байти для зберігання в полі даних облікового запису
- Десеріалізація: Перетворення байтів, що зберігаються в обліковому записі, назад у ваші типи даних
Хоча ви можете використовувати будь-який формат серіалізації для розробки програм Solana, зазвичай використовується Borsh. Щоб використовувати Borsh у вашій програмі Solana:
- Додайте крейт
borsh
як залежність до вашогоCargo.toml
:
cargo add borsh
- Імпортуйте трейти Borsh і використовуйте макрос derive для реалізації трейтів для ваших структур:
use borsh::{BorshSerialize, BorshDeserialize};// Define struct representing our counter account's data#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Додайте структуру CounterAccount
до lib.rs
для визначення стану програми. Ця
структура буде використовуватися як в інструкціях ініціалізації, так і в
інструкціях інкременту.
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},};use borsh::{BorshSerialize, BorshDeserialize};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Визначення інструкцій
Інструкції відносяться до різних операцій, які може виконувати ваша програма Solana. Думайте про них як про публічні API для вашої програми - вони визначають, які дії можуть виконувати користувачі при взаємодії з вашою програмою.
Інструкції зазвичай визначаються за допомогою перерахування Rust, де:
- Кожен варіант перерахування представляє різну інструкцію
- Корисне навантаження варіанту представляє параметри інструкції
Зверніть увагу, що варіанти перерахування Rust нумеруються автоматично, починаючи з 0.
Нижче наведено приклад перерахування, що визначає дві інструкції:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}
Коли клієнт викликає вашу програму, він повинен надати instruction data (як буфер байтів), де:
- Перший байт визначає, який варіант інструкції виконати (0, 1 тощо)
- Решта байтів містять серіалізовані параметри інструкції (якщо потрібно)
Для перетворення instruction data (байтів) у варіант перерахування, зазвичай реалізують допоміжний метод. Цей метод:
- Відокремлює перший байт, щоб отримати варіант інструкції
- Зіставляє варіант і розбирає будь-які додаткові параметри з решти байтів
- Повертає відповідний варіант перерахування
Наприклад, метод unpack
для перерахування CounterInstruction
:
impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet initial_value = u64::from_le_bytes(rest.try_into().map_err(|_| ProgramError::InvalidInstructionData)?);Ok(Self::InitializeCounter { initial_value })}1 => Ok(Self::IncrementCounter), // No additional data needed_ => Err(ProgramError::InvalidInstructionData),}}}
Додайте наступний код до lib.rs
, щоб визначити інструкції для програми
лічильника.
use borsh::{BorshDeserialize, BorshSerialize};use solana_program::{account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg,program_error::ProgramError, pubkey::Pubkey,};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet initial_value = u64::from_le_bytes(rest.try_into().map_err(|_| ProgramError::InvalidInstructionData)?,);Ok(Self::InitializeCounter { initial_value })}1 => Ok(Self::IncrementCounter), // No additional data needed_ => Err(ProgramError::InvalidInstructionData),}}}
Обробники інструкцій
Обробники інструкцій — це функції, які містять бізнес-логіку для кожної
інструкції. Зазвичай функції-обробники називають process_<instruction_name>
,
але ви можете вибрати будь-яку схему іменування.
Додайте наступний код до lib.rs
. Цей код використовує перерахування
CounterInstruction
та метод unpack
, визначені на попередньому кроці, для
маршрутизації вхідних інструкцій до відповідних функцій-обробників:
entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Unpack instruction datalet instruction = CounterInstruction::unpack(instruction_data)?;// Match instruction typematch instruction {CounterInstruction::InitializeCounter { initial_value } => {process_initialize_counter(program_id, accounts, initial_value)?}CounterInstruction::IncrementCounter => process_increment_counter(program_id, accounts)?,};}fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {// Implementation details...Ok(())}fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {// Implementation details...Ok(())}
Далі додайте реалізацію функції process_initialize_counter
. Цей обробник
інструкцій:
- Створює та виділяє місце для нового облікового запису для зберігання даних лічильника
- Ініціалізує дані облікового запису значенням
initial_value
, переданим в інструкцію
// Initialize a new counter accountfn 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)?;// Size of our counter accountlet account_space = 8; // Size in bytes to store a u64// Calculate minimum balance for rent exemptionlet rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);// Create the counter accountinvoke(&system_instruction::create_account(payer_account.key, // Account paying for the new accountcounter_account.key, // Account to be createdrequired_lamports, // Amount of lamports to transfer to the new accountaccount_space as u64, // Size in bytes to allocate for the data fieldprogram_id, // Set program owner to our program),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;// Create a new CounterAccount struct with the initial valuelet counter_data = CounterAccount {count: initial_value,};// Get a mutable reference to the counter account's datalet mut account_data = &mut counter_account.data.borrow_mut()[..];// Serialize the CounterAccount struct into the account's datacounter_data.serialize(&mut account_data)?;msg!("Counter initialized with value: {}", initial_value);Ok(())}
Далі додайте реалізацію функції process_increment_counter
. Ця інструкція
збільшує значення існуючого облікового запису лічильника.
// Update an existing counter's valuefn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;// Verify account ownershipif counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}// Mutable borrow the account datalet mut data = counter_account.data.borrow_mut();// Deserialize the account data into our CounterAccount structlet mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;// Increment the counter valuecounter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;// Serialize the updated counter data back into the accountcounter_data.serialize(&mut &mut data[..])?;msg!("Counter incremented to: {}", counter_data.count);Ok(())}
Тестування інструкцій
Щоб протестувати інструкції програми, додайте такі залежності до Cargo.toml
.
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo add tokio --dev
Потім додайте наступний тестовий модуль до lib.rs
і запустіть cargo test-sbf
для виконання тестів. За бажанням використовуйте прапорець --nocapture
, щоб
побачити виведення у консолі.
cargo test-sbf -- --nocapture
#[cfg(test)]mod test {use super::*;use solana_program_test::*;use solana_sdk::{instruction::{AccountMeta, Instruction},signature::{Keypair, Signer},system_program,transaction::Transaction,};#[tokio::test]async fn test_counter_program() {let program_id = Pubkey::new_unique();let (mut banks_client, payer, recent_blockhash) = ProgramTest::new("counter_program",program_id,processor!(process_instruction),).start().await;// Create a new keypair to use as the address for our counter accountlet counter_keypair = Keypair::new();let initial_value: u64 = 42;// Step 1: Initialize the counterprintln!("Testing counter initialization...");// Create initialization instructionlet mut init_instruction_data = vec![0]; // 0 = initialize instructioninit_instruction_data.extend_from_slice(&initial_value.to_le_bytes());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),],);// Send transaction with initialize instructionlet mut transaction =Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], recent_blockhash);banks_client.process_transaction(transaction).await.unwrap();// Check account datalet account = banks_client.get_account(counter_keypair.pubkey()).await.expect("Failed to get counter account");if let Some(account_data) = account {let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data).expect("Failed to deserialize counter data");assert_eq!(counter.count, 42);println!("✅ Counter initialized successfully with value: {}",counter.count);}// Step 2: Increment the counterprintln!("Testing counter increment...");// Create increment instructionlet increment_instruction = Instruction::new_with_bytes(program_id,&[1], // 1 = increment instructionvec![AccountMeta::new(counter_keypair.pubkey(), true)],);// Send transaction with increment instructionlet mut transaction =Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], recent_blockhash);banks_client.process_transaction(transaction).await.unwrap();// Check account datalet account = banks_client.get_account(counter_keypair.pubkey()).await.expect("Failed to get counter account");if let Some(account_data) = account {let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data).expect("Failed to deserialize counter data");assert_eq!(counter.count, 43);println!("✅ Counter incremented successfully to: {}", counter.count);}}}
Приклад виводу:
running 1 test[2024-10-29T20:51:13.783708000Z INFO solana_program_test] "counter_program" SBF program from /counter_program/target/deploy/counter_program.so, modified 2 seconds, 169 ms, 153 µs and 461 ns ago[2024-10-29T20:51:13.855204000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1][2024-10-29T20:51:13.856052000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2][2024-10-29T20:51:13.856135000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success[2024-10-29T20:51:13.856242000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter initialized with value: 42[2024-10-29T20:51:13.856285000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 3791 of 200000 compute units[2024-10-29T20:51:13.856307000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success[2024-10-29T20:51:13.860038000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1][2024-10-29T20:51:13.860333000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter incremented to: 43[2024-10-29T20:51:13.860355000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 756 of 200000 compute units[2024-10-29T20:51:13.860375000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM successtest test::test_counter_program ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
Is this page helpful?