Programy Solana napisane w języku Rust mają minimalne wymagania strukturalne, co
daje elastyczność w organizacji kodu. Jedynym wymaganiem 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 podążają za wspólnym schematem:
entrypoint.rs: Definiuje punkt wejścia, który przekierowuje przychodzące instrukcje.state.rs: Definiuje stan programu (dane konta).instructions.rs: Definiuje instrukcje, które program może wykonać.processor.rs: Definiuje obsługiwacze 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 pokazać, jak zbudować natywny program w Rust z wieloma instrukcjami, przejdziemy przez prosty program licznika, który implementuje dwie instrukcje:
InitializeCounter: Tworzy i inicjalizuje nowe konto z wartością początkową.IncrementCounter: Zwiększa wartość przechowywaną na istniejącym koncie.
Dla uproszczenia program zostanie zaimplementowany w pojedynczym pliku lib.rs,
choć w praktyce większe programy warto podzielić na kilka plików.
Część 1: Pisanie programu
Zacznijmy od zbudowania programu licznika. Stworzymy program, który może zainicjować licznik wartością początkową i go zwiększać.
Utwórz nowy program
Najpierw utwórzmy nowy projekt Rust dla naszego programu Solana.
$cargo new counter_program --lib$cd counter_program
Powinieneś zobaczyć domyślne pliki src/lib.rs oraz Cargo.toml.
Zaktualizuj pole edition w pliku Cargo.toml na 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 jako głównego SDK oraz borsh do serializacji.
$cargo add solana-program@2.2.0$cargo add borsh
Nie ma wymogu używania Borsh. Jednak jest to często wykorzystywana biblioteka serializacyjna w programach Solana.
Skonfiguruj crate-type
Programy Solana muszą być kompilowane jako biblioteki dynamiczne. Dodaj sekcję
[lib], aby skonfigurować sposób budowania programu przez Cargo.
[lib]crate-type = ["cdylib", "lib"]
Jeśli nie dodasz tej konfiguracji, katalog target/deploy nie zostanie wygenerowany podczas budowania programu.
Skonfiguruj punkt wejścia programu
Każdy program Solana ma punkt wejścia, czyli funkcję wywoływaną przy uruchomieniu programu. Zacznijmy od dodania potrzebnych importów i skonfigurowania punktu wejścia.
Dodaj poniższy kod do pliku 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
Teraz zdefiniujmy strukturę danych, która będzie przechowywana w naszych kontach
liczników. To są dane, które będą zapisane 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 reprezentuje 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
naszych wariantów enuma CounterInstruction. Metoda Borsh try_from_slice
obsługuje tę konwersję automatycznie.
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 je funkcji
Teraz zaktualizujmy 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 jest deserializowany 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ługę instrukcji InitializeCounter oraz 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 obsługę inicjalizacji
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 System
Program. Nowe konto jest tworzone z naszym programem jako właścicielem, co daje
naszemu programowi możliwość zapisu do konta 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 służy wyłącznie do celów demonstracyjnych. Nie zawiera zabezpieczeń ani walidacji wymaganych w programach produkcyjnych.
Zaimplementuj funkcję obsługującą inkrementację
Teraz zaimplementujmy handler, który inkrementuje istniejący licznik. Ta instrukcja:
- Odczytuje pole
datakonta dlacounter_account - Deserializuje je do struktury
CounterAccount - Zwiększa pole
counto 1 - Serializuje strukturę
CounterAccountz powrotem do poladatakonta
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 służy wyłącznie do celów demonstracyjnych. Nie zawiera zabezpieczeń ani walidacji wymaganych w programach produkcyjnych.
Program ukończony
Gratulacje! Zbudowałeś kompletny program Solana, który demonstruje podstawową strukturę wspólną dla wszystkich programów Solana:
- Entrypoint: Definiuje miejsce rozpoczęcia wykonywania programu i kieruje wszystkie przychodzące żądania do odpowiednich handlerów instrukcji
- Obsługa instrukcji: Definiuje instrukcje i powiązane z nimi funkcje obsługujące
- Zarządzanie stanem: Definiuje struktury danych kont 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 licznika. Skorzystamy z LiteSVM, frameworka do testowania, który pozwala testować programy bez wdrażania ich na klastrze.
Dodaj zależności do testów
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 i importów.
Dodaj poniższy kod do lib.rs, bezpośrednio pod kodem 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.
Zainicjuj środowisko testowe
Skonfigurujmy środowisko testowe z LiteSVM i zasilmy konto płatnika.
LiteSVM symuluje środowisko uruchomieniowe Solana, umożliwiając testowanie programu bez wdrażania go na prawdziwym klastrze.
Dodaj poniższy 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
polecenie cargo build-sbf, aby zbudować program. To wygeneruje 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 utworzone poprawnie 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 zadział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ływanie programu
Dodajmy teraz skrypt klienta do wywołania programu.
Utwórz przykład klienta
Stwórzmy klienta w języku Rust do komunikacji 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 poniższe polecenie, aby uzyskać identyfikator programu z pliku keypair:
$solana address -k ./target/deploy/counter_program-keypair.json
Dodaj kod klienta do pliku 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
Skoro mamy już 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 wyświetlić 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 developmentu użyjemy lokalnego testowego validatora.
Najpierw skonfiguruj Solana CLI, aby używało 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 testowego validatora w osobnym terminalu:
$solana-test-validator
Wdróż program
Gdy validator jest uruchomiony, 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 oraz
swojego ID 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
Gdy lokalny validator nadal działa, uruchom klienta:
$cargo run --example client
Oczekiwany wynik:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
Gdy lokalny validator jest uruchomiony, możesz przeglądać transakcje w
Solana Explorer korzystając z
podpisów transakcji z wyniku. Pamiętaj, że klaster w Solana Explorer musi być
ustawiony na „Custom RPC URL”, który domyślnie wskazuje na
http://localhost:8899, na którym uruchomiony jest solana-test-validator.
Is this page helpful?