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 mieć 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 podążają za wspólnym wzorcem:
entrypoint.rs
: Definiuje punkt wejścia, który kieruje przychodzącymi instrukcjami.state.rs
: Definiuje stan 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ć.
Na przykład zobacz Token Program.
Przykładowy program
Aby zademonstrować, jak zbudować natywny program w języku Rust z wieloma instrukcjami, przejdziemy 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 możesz chcieć podzielić większe programy na wiele plików.
Część 1: Pisanie programu
Zacznijmy od budowy programu licznikowego. Stworzymy program, który może zainicjalizować licznik z wartością początkową i go zwiększać.
Utwórz nowy program
Najpierw utwórzmy nowy projekt w języku Rust dla naszego programu Solana.
$cargo new counter_program --lib$cd counter_program
Powinieneś zobaczyć domyślne pliki src/lib.rs
i Cargo.toml
.
Zaktualizuj pole edition
w pliku Cargo.toml
do wartości 2021. W przeciwnym
razie możesz napotkać błąd podczas budowania programu.
Dodaj zależności
Teraz dodajmy niezbędne zależności do budowy programu Solana. Potrzebujemy
solana-program
dla podstawowego SDK oraz borsh
do serializacji.
$cargo add solana-program@2.2.0$cargo add borsh
Nie ma obowiązku używania Borsh. Jednak jest to powszechnie używana biblioteka serializacji dla programów Solana.
Skonfiguruj typ crate
Programy Solana muszą być kompilowane jako biblioteki dynamiczne. Dodaj sekcję
[lib]
, aby skonfigurować sposób, w jaki Cargo buduje program.
[lib]crate-type = ["cdylib", "lib"]
Jeśli nie uwzględnisz tej konfiguracji, katalog target/deploy nie zostanie wygenerowany podczas budowania programu.
Skonfiguruj punkt wejścia programu
Każdy program Solana ma punkt wejścia, który jest funkcją wywoływaną podczas uruchamiania programu. Zacznijmy od dodania importów, które będą potrzebne do programu, oraz skonfigurowania punktu wejścia.
Dodaj następujący kod do 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(())}
Makro
entrypoint
obsługuje deserializację danych input
do parametrów funkcji
process_instruction
.
Program Solana entrypoint
ma następującą sygnaturę funkcji. Programiści mogą
stworzyć własną implementację funkcji entrypoint
.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Zdefiniuj stan programu
Zdefiniujmy teraz strukturę danych, która będzie przechowywana w naszych kontach
liczników. Te dane będą przechowywane w polu data
konta.
Dodaj następujący kod do lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {pub count: u64,}
Zdefiniuj enum instrukcji
Zdefiniujmy instrukcje, które nasz program może wykonać. Użyjemy enuma, gdzie każdy wariant będzie reprezentował inną instrukcję.
Dodaj następujący kod do lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 },IncrementCounter,}
Zaimplementuj deserializację instrukcji
Teraz musimy zdeserializować instruction_data
(surowe bajty) do jednego z
wariantów enuma CounterInstruction
. Metoda Borsh try_from_slice
automatycznie obsługuje tę konwersję.
Zaktualizuj funkcję process_instruction
, aby używała deserializacji 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(())}
Przekieruj instrukcje do obsługujących funkcji
Zaktualizujmy teraz główną funkcję process_instruction
, aby przekierowywała
instrukcje do odpowiednich funkcji obsługujących.
Ten wzorzec przekierowywania jest powszechny w programach Solana.
instruction_data
zostaje zdeserializowany do wariantu enuma reprezentującego
instrukcję, a następnie wywoływana jest odpowiednia funkcja obsługująca. Każda
funkcja obsługująca zawiera implementację dla danej instrukcji.
Dodaj następujący kod do lib.rs
, aktualizując funkcję process_instruction
i
dodając obsługujące funkcje dla instrukcji InitializeCounter
i
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(())}
Zaimplementuj funkcję obsługującą inicjalizację
Zaimplementujmy funkcję obsługującą tworzenie i inicjalizację nowego konta licznika. Ponieważ tylko System Program może tworzyć konta na Solanie, użyjemy Cross Program Invocation (CPI), czyli wywołania innego programu z naszego programu.
Nasz program wykonuje CPI, aby wywołać instrukcję create_account
z System
Program. Nowe konto jest tworzone z naszym programem jako właścicielem, co daje
naszemu programowi możliwość zapisu na konto i inicjalizacji danych.
Dodaj następujący kod do lib.rs
, aktualizując funkcję
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(())}
Ta instrukcja jest przeznaczona wyłącznie do celów demonstracyjnych. Nie zawiera zabezpieczeń ani kontroli walidacji wymaganych w programach produkcyjnych.
Implementacja obsługi inkrementacji
Teraz zaimplementujmy obsługę, która zwiększa istniejący licznik. Ta instrukcja:
- Odczytuje pole konta
data
dlacounter_account
- Deserializuje je do struktury
CounterAccount
- Zwiększa pole
count
o 1 - Serializuje strukturę
CounterAccount
z powrotem do poladata
konta
Dodaj następujący kod do lib.rs
, aktualizując funkcję
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(())}
Ta instrukcja jest przeznaczona wyłącznie do celów demonstracyjnych. Nie zawiera zabezpieczeń ani kontroli walidacji wymaganych w programach produkcyjnych.
Ukończony program
Gratulacje! Zbudowałeś kompletny program Solana, który demonstruje podstawową strukturę wspólną dla wszystkich programów Solana:
- Punkt wejścia: Definiuje, gdzie rozpoczyna się wykonywanie programu i kieruje wszystkie przychodzące żądania do odpowiednich obsług instrukcji
- Obsługa instrukcji: Definiuje instrukcje i ich powiązane funkcje obsługi
- Zarządzanie stanem: Definiuje struktury danych konta i zarządza ich stanem w kontach należących do programu
- Cross Program Invocation (CPI): Wywołuje System Program w celu tworzenia nowych kont należących do programu
Następnym krokiem jest przetestowanie programu, aby upewnić się, że wszystko działa poprawnie.
Część 2: Testowanie programu
Teraz przetestujmy nasz program licznikowy. Użyjemy LiteSVM, frameworka testowego, który pozwala na testowanie programów bez wdrażania ich do klastra.
Dodaj zależności testowe
Najpierw dodajmy zależności potrzebne do testowania. Użyjemy litesvm
do
testowania oraz solana-sdk
.
$cargo add litesvm@0.6.1 --dev$cargo add solana-sdk@2.2.0 --dev
Utwórz moduł testowy
Dodajmy teraz moduł testowy do naszego programu. Zaczniemy od podstawowego szkieletu oraz importów.
Dodaj następujący kod do lib.rs
, bezpośrednio poniżej kodu programu:
#[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}}
Atrybut #[cfg(test)]
zapewnia, że ten kod jest kompilowany tylko podczas
uruchamiania testów.
Zainicjalizuj środowisko testowe
Skonfigurujmy środowisko testowe za pomocą LiteSVM i zasilmy konto płatnika.
LiteSVM symuluje środowisko wykonawcze Solana, umożliwiając testowanie naszego programu bez wdrażania go na rzeczywisty klaster.
Dodaj następujący kod do lib.rs
, aktualizując funkcję test_counter_program
:
let mut svm = LiteSVM::new();let payer = Keypair::new();svm.airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to airdrop");
Załaduj program
Teraz musimy zbudować i załadować nasz program do środowiska testowego. Uruchom
komendę cargo build-sbf
, aby zbudować program. Wygeneruje to plik
counter_program.so
w katalogu target/deploy
.
$cargo build-sbf
Upewnij się, że edition
w Cargo.toml
jest ustawione na 2021
.
Po zbudowaniu możemy załadować program.
Zaktualizuj funkcję test_counter_program
, aby załadować program do środowiska
testowego.
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");
Musisz uruchomić cargo build-sbf
przed uruchomieniem testów, aby wygenerować
plik .so
. Test ładuje skompilowany program.
Przetestuj instrukcję inicjalizacji
Przetestujmy instrukcję inicjalizacji, tworząc nowe konto licznika z wartością początkową.
Dodaj następujący kod do lib.rs
, aktualizując funkcję 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);
Zweryfikuj inicjalizację
Po inicjalizacji zweryfikujmy, czy konto licznika zostało poprawnie utworzone z oczekiwaną wartością.
Dodaj następujący kod do lib.rs
, aktualizując funkcję 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);
Przetestuj instrukcję inkrementacji
Teraz przetestujmy instrukcję inkrementacji, aby upewnić się, że poprawnie aktualizuje wartość licznika.
Dodaj następujący kod do lib.rs
, aktualizując funkcję 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);
Zweryfikuj końcowe wyniki
Na koniec zweryfikujmy, czy inkrementacja działała poprawnie, sprawdzając zaktualizowaną wartość licznika.
Dodaj następujący kod do lib.rs
, aktualizując funkcję 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);
Uruchom testy za pomocą następującego polecenia. Flaga --nocapture
wyświetla
wynik testu.
$cargo test -- --nocapture
Oczekiwany wynik:
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
Część 3: Wywołanie programu
Teraz dodajmy skrypt klienta, aby wywołać program.
Utwórz przykład klienta
Stwórzmy klienta w języku Rust, aby wchodzić w interakcję z naszym wdrożonym programem.
$mkdir examples$touch examples/client.rs
Dodaj następującą konfigurację do Cargo.toml
:
[[example]]name = "client"path = "examples/client.rs"
Zainstaluj zależności klienta:
$cargo add solana-client@2.2.0 --dev$cargo add tokio --dev
Zaimplementuj kod klienta
Teraz zaimplementujmy klienta, który wywoła nasz wdrożony program.
Uruchom następujące polecenie, aby uzyskać ID programu z pliku keypair:
$solana address -k ./target/deploy/counter_program-keypair.json
Dodaj kod klienta do examples/client.rs
i zamień program_id
na wynik
poprzedniego polecenia:
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);}}}
Część 4: Wdrażanie programu
Teraz, gdy mamy gotowy program i klienta, zbudujmy, wdrożmy i wywołajmy program.
Zbuduj program
Najpierw zbudujmy nasz program.
$cargo build-sbf
To polecenie kompiluje Twój program i generuje dwa ważne pliki w
target/deploy/
:
counter_program.so # The compiled programcounter_program-keypair.json # Keypair for the program ID
Możesz zobaczyć ID swojego programu, uruchamiając następujące polecenie:
$solana address -k ./target/deploy/counter_program-keypair.json
Przykładowy wynik:
HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Uruchom lokalny validator
Do celów deweloperskich użyjemy lokalnego testowego validatora.
Najpierw skonfiguruj Solana CLI, aby używał localhost:
$solana config set -ul
Przykładowy wynik:
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed
Teraz uruchom testowy validator w osobnym terminalu:
$solana-test-validator
Wdróż program
Gdy validator działa, wdroż swój program do lokalnego klastra:
$solana program deploy ./target/deploy/counter_program.so
Przykładowy wynik:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
Możesz zweryfikować wdrożenie za pomocą polecenia solana program show
z
użyciem ID swojego programu:
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Przykładowy wynik:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
Uruchom klienta
Przy wciąż działającym lokalnym validatorze, uruchom klienta:
$cargo run --example client
Oczekiwany wynik:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
Przy działającym lokalnym validatorze możesz przeglądać transakcje w
Solana Explorer używając podpisów
transakcji z wyniku. Upewnij się, że klaster w Solana Explorer jest ustawiony na
"Custom RPC URL", który domyślnie wskazuje na http://localhost:8899
, na którym
działa solana-test-validator
.
Is this page helpful?