Struktura programu w Rust

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:

  1. InitializeCounter: Tworzy i inicjalizuje nowe konto z wartością początkową.
  2. 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.

Terminal
$
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.

Terminal
$
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.

Cargo.toml
[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:

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:

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:

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:

lib.rs
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:

lib.rs
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:

lib.rs
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 data konta dla counter_account
  • Deserializuje je do struktury CounterAccount
  • Zwiększa pole count o 1
  • Serializuje strukturę CounterAccount z powrotem do pola data konta

Dodaj następujący kod do lib.rs, aktualizując funkcję process_increment_counter:

lib.rs
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.

Utwórz nowy program

Najpierw utwórzmy nowy projekt Rust dla naszego programu Solana.

Terminal
$
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.

Terminal
$
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.

Cargo.toml
[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:

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:

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:

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:

lib.rs
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:

lib.rs
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:

lib.rs
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 data konta dla counter_account
  • Deserializuje je do struktury CounterAccount
  • Zwiększa pole count o 1
  • Serializuje strukturę CounterAccount z powrotem do pola data konta

Dodaj następujący kod do lib.rs, aktualizując funkcję process_increment_counter:

lib.rs
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.

Cargo.toml
lib.rs
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[dependencies]

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.

Terminal
$
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:

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
}
}

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:

lib.rs
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.

Terminal
$
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.

lib.rs
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:

lib.rs
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:

lib.rs
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:

lib.rs
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:

lib.rs
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.

Terminal
$
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: 42
Testing 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

Dodaj zależności do testów

Najpierw dodajmy zależności potrzebne do testowania. Użyjemy litesvm do testowania oraz solana-sdk.

Terminal
$
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:

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
}
}

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:

lib.rs
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.

Terminal
$
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.

lib.rs
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:

lib.rs
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:

lib.rs
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:

lib.rs
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:

lib.rs
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.

Terminal
$
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: 42
Testing 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
Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "1.5.7"
solana-program = "2.2.0"
[dev-dependencies]
litesvm = "0.6.1"
solana-sdk = "2.2.0"

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.

Terminal
$
mkdir examples
$
touch examples/client.rs

Dodaj następującą konfigurację do Cargo.toml:

Cargo.toml
[[example]]
name = "client"
path = "examples/client.rs"

Zainstaluj zależności klienta:

Terminal
$
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:

Terminal
$
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:

examples/client.rs
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
examples/client.rs
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 deployment
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
// Connect to local cluster
let rpc_url = String::from("http://localhost:8899");
let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());
// Generate a new keypair for paying fees
let payer = Keypair::new();
// Request airdrop of 1 SOL for transaction fees
println!("Requesting airdrop...");
let airdrop_signature = client
.request_airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to request airdrop");
// Wait for airdrop confirmation
loop {
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 data
let 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 data
let 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);
}
}
}

Utwórz przykład klienta

Stwórzmy klienta w języku Rust do komunikacji z naszym wdrożonym programem.

Terminal
$
mkdir examples
$
touch examples/client.rs

Dodaj następującą konfigurację do Cargo.toml:

Cargo.toml
[[example]]
name = "client"
path = "examples/client.rs"

Zainstaluj zależności klienta:

Terminal
$
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:

Terminal
$
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:

examples/client.rs
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "1.5.7"
solana-program = "2.2.0"
[dev-dependencies]
litesvm = "0.6.1"
solana-sdk = "2.2.0"
solana-client = "2.2.0"
tokio = "1.47.1"
[[example]]
name = "client"
path = "examples/client.rs"

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.

Terminal
$
cargo build-sbf

To polecenie kompiluje Twój program i generuje dwa ważne pliki w target/deploy/:

counter_program.so # The compiled program
counter_program-keypair.json # Keypair for the program ID

Możesz wyświetlić ID swojego programu, uruchamiając następujące polecenie:

Terminal
$
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:

Terminal
$
solana config set -ul

Przykładowy wynik:

Config File: ~/.config/solana/cli/config.yml
RPC URL: http://localhost:8899
WebSocket URL: ws://localhost:8900/ (computed)
Keypair Path: ~/.config/solana/id.json
Commitment: confirmed

Teraz uruchom testowego validatora w osobnym terminalu:

Terminal
$
solana-test-validator

Wdróż program

Gdy validator jest uruchomiony, wdroż swój program do lokalnego klastra:

Terminal
$
solana program deploy ./target/deploy/counter_program.so

Przykładowy wynik:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

Możesz zweryfikować wdrożenie za pomocą polecenia solana program show oraz swojego ID programu:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Przykładowy wynik:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Owner: BPFLoaderUpgradeab1e11111111111111111111111
ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuM
Authority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1
Last Deployed In Slot: 16
Data Length: 82696 (0x14308) bytes
Balance: 0.57676824 SOL

Uruchom klienta

Gdy lokalny validator nadal działa, uruchom klienta:

Terminal
$
cargo run --example client

Oczekiwany wynik:

Requesting airdrop...
Airdrop confirmed
Initializing counter...
Counter initialized!
Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14k
Counter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9Zfofcy
Incrementing 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?

Spis treści

Edytuj stronę

Zarządzane przez

© 2026 Solana Foundation.
Wszelkie prawa zastrzeżone.
Bądź na bieżąco