Програми Solana, написані на Rust, мають мінімальні структурні вимоги, що
дозволяє гнучко організовувати код. Єдина вимога полягає в тому, що програма
повинна мати entrypoint, яка визначає, де починається виконання програми.
Структура програми
Хоча немає суворих правил щодо структури файлів, програми Solana зазвичай дотримуються загального шаблону:
entrypoint.rs: визначає точку входу, яка маршрутизує вхідні інструкції.state.rs: визначає стан програми (дані акаунта).instructions.rs: визначає інструкції, які програма може виконувати.processor.rs: визначає обробники інструкцій (функції), які реалізують бізнес-логіку для кожної інструкції.error.rs: визначає користувацькі помилки, які програма може повертати.
Наприклад, дивіться Token Program.
Приклад програми
Щоб продемонструвати, як створити нативну програму на Rust з кількома інструкціями, ми розглянемо просту програму лічильника, яка реалізує дві інструкції:
InitializeCounter: створює та ініціалізує новий акаунт з початковим значенням.IncrementCounter: збільшує значення, збережене в існуючому акаунті.
Для простоти програма буде реалізована в одному файлі lib.rs, хоча на практиці
ви можете розділити більші програми на кілька файлів.
Частина 1: написання програми
Почнемо зі створення програми лічильника. Ми створимо програму, яка може ініціалізувати лічильник зі стартовим значенням та збільшувати його.
Створення нової програми
Спочатку створімо новий Rust-проєкт для нашої Solana-програми.
$cargo new counter_program --lib$cd counter_program
Ви побачите стандартні файли src/lib.rs та Cargo.toml.
Оновіть поле edition у Cargo.toml на 2021. Інакше під час збірки програми
може виникнути помилка.
Додавання залежностей
Тепер додамо необхідні залежності для збірки Solana-програми. Нам потрібні
solana-program для основного SDK та borsh для серіалізації.
$cargo add solana-program@2.2.0$cargo add borsh
Використання Borsh не є обов'язковим. Проте це поширена бібліотека серіалізації для Solana-програм.
Налаштування crate-type
Solana-програми мають компілюватися як динамічні бібліотеки. Додайте секцію
[lib], щоб налаштувати спосіб збірки програми через Cargo.
[lib]crate-type = ["cdylib", "lib"]
Якщо не додати цю конфігурацію, директорія target/deploy не буде створена під час збірки програми.
Налаштування точки входу програми
Кожна Solana-програма має точку входу — функцію, яка викликається під час виконання програми. Почнемо з додавання необхідних імпортів та налаштування точки входу.
Додайте наступний код до 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:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {pub count: u64,}
Визначення enum інструкцій
Визначимо інструкції, які може виконувати наша програма. Використаємо enum, де кожен варіант представляє окрему інструкцію.
Додайте наступний код до 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:
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:
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:
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:
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 для створення нових акаунтів, що належать програмі
Наступний крок — протестувати програму, щоб переконатися, що все працює правильно.
Частина 2: тестування програми
Тепер давайте протестуємо нашу програму-лічильник. Ми використаємо LiteSVM — фреймворк для тестування, який дозволяє тестувати програми без розгортання на кластері.
Додавання залежностей для тестування
Спочатку додамо залежності, необхідні для тестування. Ми використаємо litesvm
для тестування та solana-sdk.
$cargo add litesvm@0.6.1 --dev$cargo add solana-sdk@2.2.0 --dev
Створення тестового модуля
Тепер додамо тестовий модуль до нашої програми. Почнемо з базової структури та імпортів.
Додайте наступний код до 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:
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.
$cargo build-sbf
Переконайтеся, що edition у Cargo.toml встановлено на 2021.
Після збірки ми можемо завантажити програму.
Оновіть функцію test_counter_program, щоб завантажити програму в тестове
середовище.
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:
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:
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:
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:
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 виводить
результат тесту.
$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: 42Testing 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
Частина 3: Виклик програми
Тепер давайте додамо клієнтський скрипт для виклику програми.
Створення клієнтського прикладу
Давайте створимо Rust-клієнт для взаємодії з нашою розгорнутою програмою.
$mkdir examples$touch examples/client.rs
Додайте наступну конфігурацію до Cargo.toml:
[[example]]name = "client"path = "examples/client.rs"
Встановіть клієнтські залежності:
$cargo add solana-client@2.2.0 --dev$cargo add tokio --dev
Реалізація клієнтського коду
Тепер давайте реалізуємо клієнт, який викликатиме нашу розгорнуту програму.
Виконайте наступну команду, щоб отримати ідентифікатор вашої програми з файлу keypair:
$solana address -k ./target/deploy/counter_program-keypair.json
Додайте клієнтський код до examples/client.rs і замініть program_id на вивід
попередньої команди:
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");
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 deploymentlet program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");// Connect to local clusterlet rpc_url = String::from("http://localhost:8899");let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());// Generate a new keypair for paying feeslet payer = Keypair::new();// Request airdrop of 1 SOL for transaction feesprintln!("Requesting airdrop...");let airdrop_signature = client.request_airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to request airdrop");// Wait for airdrop confirmationloop {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 datalet 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 datalet 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);}}}
Частина 4: Розгортання програми
Тепер, коли наша програма та клієнт готові, давайте зберемо, розгорнемо та викличемо програму.
Збірка програми
Спочатку зберемо нашу програму.
$cargo build-sbf
Ця команда компілює вашу програму та генерує два важливі файли в
target/deploy/:
counter_program.so # The compiled programcounter_program-keypair.json # Keypair for the program ID
Ви можете переглянути ID вашої програми, виконавши наступну команду:
$solana address -k ./target/deploy/counter_program-keypair.json
Приклад виводу:
HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Запуск локального валідатора
Для розробки ми використовуватимемо локальний тестовий валідатор.
Спочатку налаштуйте Solana CLI для використання localhost:
$solana config set -ul
Приклад виводу:
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed
Тепер запустіть тестовий валідатор в окремому терміналі:
$solana-test-validator
Розгортання програми
Коли валідатор запущено, розгорніть вашу програму на локальному кластері:
$solana program deploy ./target/deploy/counter_program.so
Приклад виводу:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
Ви можете перевірити розгортання за допомогою команди solana program show з
вашим ідентифікатором програми:
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Приклад виводу:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
Запуск клієнта
Коли локальний валідатор все ще працює, виконайте клієнт:
$cargo run --example client
Очікуваний результат:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
Коли локальний валідатор працює, ви можете переглянути транзакції в
Solana Explorer, використовуючи
вихідні підписи транзакцій. Зверніть увагу, що кластер у Solana Explorer має
бути встановлений на "Custom RPC URL", який за замовчуванням використовує
http://localhost:8899, на якому працює solana-test-validator.
Is this page helpful?