Struktura programu w Rust
Programy Solana napisane w języku Rust mają minimalne wymagania strukturalne, co
pozwala na elastyczność w organizacji kodu. Jedynym wymogiem jest to, że program
musi posiadać entrypoint
, który definiuje, gdzie rozpoczyna się wykonanie
programu.
Struktura programu
Chociaż nie ma ścisłych zasad dotyczących struktury plików, programy Solana zazwyczaj przestrzegają wspólnego wzorca:
entrypoint.rs
: Definiuje punkt wejścia, który kieruje przychodzące instrukcje.state.rs
: Definiuje stan specyficzny dla programu (dane konta).instructions.rs
: Definiuje instrukcje, które program może wykonać.processor.rs
: Definiuje obsługę instrukcji (funkcje), które implementują logikę biznesową dla każdej instrukcji.error.rs
: Definiuje niestandardowe błędy, które program może zwrócić.
Przykłady można znaleźć w Solana Program Library.
Przykładowy program
Aby zademonstrować, jak zbudować natywny program w Rust z wieloma instrukcjami, przeprowadzimy Cię przez prosty program licznikowy, który implementuje dwie instrukcje:
InitializeCounter
: Tworzy i inicjalizuje nowe konto z początkową wartością.IncrementCounter
: Zwiększa wartość przechowywaną na istniejącym koncie.
Dla uproszczenia program zostanie zaimplementowany w jednym pliku lib.rs
, choć
w praktyce większe programy można podzielić na wiele plików.
Utwórz nowy program
Najpierw utwórz nowy projekt w Rust, używając standardowego polecenia
cargo init
z flagą --lib
.
cargo init counter_program --lib
Przejdź do katalogu projektu. Powinieneś zobaczyć domyślne pliki src/lib.rs
i
Cargo.toml
cd counter_program
Następnie dodaj zależność solana-program
. Jest to minimalna zależność wymagana
do stworzenia programu Solana.
cargo add solana-program@1.18.26
Następnie dodaj poniższy fragment do Cargo.toml
. Jeśli nie uwzględnisz tej
konfiguracji, katalog target/deploy
nie zostanie wygenerowany podczas
budowania programu.
[lib]crate-type = ["cdylib", "lib"]
Twój plik Cargo.toml
powinien wyglądać następująco:
[package]name = "counter_program"version = "0.1.0"edition = "2021"[lib]crate-type = ["cdylib", "lib"][dependencies]solana-program = "1.18.26"
Punkt wejścia programu
Punkt wejścia programu Solana to funkcja, która jest wywoływana, gdy program zostaje uruchomiony. Punkt wejścia ma następującą surową definicję, a deweloperzy mogą stworzyć własną implementację funkcji punktu wejścia.
Dla uproszczenia użyj makra
entrypoint!
z biblioteki solana_program
, aby zdefiniować punkt wejścia w swoim programie.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Zastąp domyślny kod w lib.rs
poniższym kodem. Ten fragment:
- Importuje wymagane zależności z
solana_program
- Definiuje punkt wejścia programu za pomocą makra
entrypoint!
- Implementuje funkcję
process_instruction
, która przekierowuje instrukcje do odpowiednich funkcji obsługujących
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(())}
Makro entrypoint!
wymaga funkcji z następującym
sygnaturą typu
jako argumentu:
pub type ProcessInstruction =fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
Gdy program Solana jest uruchamiany, punkt wejścia
deserializuje
dane wejściowe
(dostarczone jako bajty) na trzy wartości i przekazuje je do funkcji
process_instruction
:
program_id
: Klucz publiczny programu, który jest uruchamiany (bieżący program)accounts
:AccountInfo
dla kont wymaganych przez wywoływaną instrukcjęinstruction_data
: Dodatkowe dane przekazywane do programu, które określają instrukcję do wykonania i jej wymagane argumenty
Te trzy parametry bezpośrednio odpowiadają danym, które klienci muszą dostarczyć podczas tworzenia instrukcji do wywołania programu.
Zdefiniuj stan programu
Podczas tworzenia programu Solana zazwyczaj zaczynasz od zdefiniowania stanu swojego programu – danych, które będą przechowywane na kontach utworzonych i zarządzanych przez Twój program.
Stan programu definiuje się za pomocą struktur Rust, które reprezentują układ danych na kontach Twojego programu. Możesz zdefiniować wiele struktur, aby reprezentowały różne typy kont w Twoim programie.
Pracując z kontami, potrzebujesz sposobu na konwersję typów danych programu na surowe bajty przechowywane w polu danych konta i odwrotnie:
- Serializacja: Konwersja typów danych na bajty w celu przechowywania ich w polu danych konta
- Deserializacja: Konwersja bajtów przechowywanych w koncie z powrotem na Twoje typy danych
Chociaż do tworzenia programów Solana można używać dowolnego formatu serializacji, często stosowany jest Borsh. Aby użyć Borsh w swoim programie Solana:
- Dodaj zależność
borsh
do swojegoCargo.toml
:
cargo add borsh
- Zaimportuj cechy Borsh i użyj makra derive, aby zaimplementować te cechy dla swoich struktur:
use borsh::{BorshSerialize, BorshDeserialize};// Define struct representing our counter account's data#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Dodaj strukturę CounterAccount
do lib.rs
, aby zdefiniować stan programu. Ta
struktura będzie używana zarówno w instrukcjach inicjalizacji, jak i
inkrementacji.
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,}
Zdefiniuj instrukcje
Instrukcje odnoszą się do różnych operacji, które Twój program Solana może wykonywać. Można je traktować jako publiczne API Twojego programu – definiują one, jakie działania użytkownicy mogą podejmować podczas interakcji z Twoim programem.
Instrukcje są zazwyczaj definiowane za pomocą wyliczenia (enum) w Rust, gdzie:
- Każdy wariant wyliczenia reprezentuje inną instrukcję
- Ładunek wariantu reprezentuje parametry instrukcji
Zwróć uwagę, że warianty enum w Rust są numerowane domyślnie, zaczynając od 0.
Poniżej znajduje się przykład enum definiującego dwie instrukcje:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}
Kiedy klient wywołuje Twój program, musi dostarczyć dane instrukcji (jako bufor bajtów), gdzie:
- Pierwszy bajt identyfikuje, który wariant instrukcji wykonać (0, 1, itd.)
- Pozostałe bajty zawierają zserializowane parametry instrukcji (jeśli są wymagane)
Aby przekonwertować dane instrukcji (bajty) na wariant enum, często implementuje się metodę pomocniczą. Ta metoda:
- Rozdziela pierwszy bajt, aby uzyskać wariant instrukcji
- Dopasowuje wariant i analizuje dodatkowe parametry z pozostałych bajtów
- Zwraca odpowiedni wariant enum
Na przykład metoda unpack
dla enum 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),}}}
Dodaj następujący kod do lib.rs
, aby zdefiniować instrukcje dla programu
licznika.
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),}}}
Obsługa instrukcji
Obsługa instrukcji odnosi się do funkcji, które zawierają logikę biznesową dla
każdej instrukcji. Często funkcje obsługi nazywa się
process_<instruction_name>
, ale możesz wybrać dowolną konwencję nazewnictwa.
Dodaj następujący kod do lib.rs
. Kod ten wykorzystuje enum
CounterInstruction
i metodę unpack
zdefiniowaną w poprzednim kroku, aby
kierować przychodzące instrukcje do odpowiednich funkcji obsługi:
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(())}
Następnie dodaj implementację funkcji process_initialize_counter
. Ten
obsługiwacz instrukcji:
- Tworzy i przydziela miejsce na nowe konto do przechowywania danych licznika
- Inicjalizuje dane konta za pomocą
initial_value
przekazanych do instrukcji
// 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(())}
Następnie dodaj implementację funkcji process_increment_counter
. Ta instrukcja
zwiększa wartość istniejącego konta licznika.
// 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(())}
Testowanie instrukcji
Aby przetestować instrukcje programu, dodaj następujące zależności do
Cargo.toml
.
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo add tokio --dev
Następnie dodaj poniższy moduł testowy do lib.rs
i uruchom cargo test-sbf
,
aby wykonać testy. Opcjonalnie użyj flagi --nocapture
, aby zobaczyć komunikaty
wyjściowe w wynikach.
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);}}}
Przykładowy wynik:
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?