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:

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

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

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

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

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:

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:

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:

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

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

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

Utwórz nowy program

Najpierw utwórzmy nowy projekt w języku Rust dla naszego programu Solana.

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

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

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

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:

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:

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:

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

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

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

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

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.

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 oraz importów.

Dodaj następujący kod do lib.rs, bezpośrednio poniżej kodu 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.

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:

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 komendę cargo build-sbf, aby zbudować program. Wygeneruje to 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 poprawnie utworzone 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 dział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 testowe

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 oraz importów.

Dodaj następujący kod do lib.rs, bezpośrednio poniżej kodu 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.

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:

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 komendę cargo build-sbf, aby zbudować program. Wygeneruje to 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 poprawnie utworzone 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 dział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ł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.

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 następujące polecenie, aby uzyskać ID programu z pliku keypair:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

Dodaj kod klienta do 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, aby wchodzić w interakcję 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 następujące polecenie, aby uzyskać ID programu z pliku keypair:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

Dodaj kod klienta do 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

Teraz, gdy mamy 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 zobaczyć 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 celów deweloperskich użyjemy lokalnego testowego validatora.

Najpierw skonfiguruj Solana CLI, aby używał 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 testowy validator w osobnym terminalu:

Terminal
$
solana-test-validator

Wdróż program

Gdy validator działa, 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 z użyciem ID swojego 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

Przy wciąż działającym lokalnym validatorze, 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

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?

Spis treści

Edytuj stronę