Tài liệu SolanaPhát triển chương trìnhChương trình Rust

Cấu trúc chương trình Rust

Các chương trình Solana được viết bằng Rust có yêu cầu cấu trúc tối thiểu, cho phép tính linh hoạt trong cách tổ chức mã nguồn. Yêu cầu duy nhất là chương trình phải có một entrypoint, xác định nơi bắt đầu thực thi của chương trình.

Cấu trúc chương trình

Mặc dù không có quy tắc nghiêm ngặt về cấu trúc tệp, các chương trình Solana thường tuân theo một mẫu chung:

  • entrypoint.rs: Định nghĩa entrypoint để định tuyến các lệnh đến.
  • state.rs: Định nghĩa trạng thái chương trình (dữ liệu tài khoản).
  • instructions.rs: Định nghĩa các lệnh mà chương trình có thể thực thi.
  • processor.rs: Định nghĩa các trình xử lý lệnh (hàm) triển khai logic nghiệp vụ cho từng lệnh.
  • error.rs: Định nghĩa các lỗi tùy chỉnh mà chương trình có thể trả về.

Ví dụ, xem Token Program.

Chương trình ví dụ

Để minh họa cách xây dựng một chương trình Rust native với nhiều lệnh, chúng ta sẽ tìm hiểu một chương trình đếm đơn giản triển khai hai lệnh:

  1. InitializeCounter: Tạo và khởi tạo một tài khoản mới với giá trị ban đầu.
  2. IncrementCounter: Tăng giá trị được lưu trữ trong một tài khoản hiện có.

Để đơn giản, chương trình sẽ được triển khai trong một tệp lib.rs duy nhất, mặc dù trong thực tế bạn có thể muốn chia các chương trình lớn hơn thành nhiều tệp.

Phần 1: Viết chương trình

Hãy bắt đầu bằng cách xây dựng chương trình đếm. Chúng ta sẽ tạo một chương trình có thể khởi tạo bộ đếm với giá trị bắt đầu và tăng giá trị đó.

Tạo chương trình mới

Đầu tiên, hãy tạo một dự án Rust mới cho chương trình Solana của chúng ta.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Bạn sẽ thấy các tệp src/lib.rsCargo.toml mặc định.

Cập nhật trường edition trong Cargo.toml thành 2021. Nếu không, bạn có thể gặp lỗi khi build chương trình.

Thêm các phụ thuộc

Bây giờ hãy thêm các phụ thuộc cần thiết để build chương trình Solana. Chúng ta cần solana-program cho core SDK và borsh cho serialization.

Terminal
$
cargo add solana-program@2.2.0
$
cargo add borsh

Không bắt buộc phải sử dụng Borsh. Tuy nhiên, đây là thư viện serialization được sử dụng phổ biến cho các chương trình Solana.

Cấu hình crate-type

Các chương trình Solana phải được biên dịch dưới dạng dynamic library. Thêm phần [lib] để cấu hình cách Cargo build chương trình.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

Nếu bạn không thêm cấu hình này, thư mục target/deploy sẽ không được tạo khi bạn build chương trình.

Thiết lập entrypoint của chương trình

Mọi chương trình Solana đều có một entrypoint, đó là hàm được gọi khi chương trình được thực thi. Hãy bắt đầu bằng cách thêm các import cần thiết cho chương trình và thiết lập entrypoint.

Thêm đoạn mã sau vào 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(())
}

Macro entrypoint xử lý việc deserialization dữ liệu input thành các tham số của hàm process_instruction.

Một entrypoint của chương trình Solana có function signature như sau. Các developer hoàn toàn có thể tự tạo implementation riêng của hàm entrypoint.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

Định nghĩa trạng thái chương trình

Bây giờ hãy định nghĩa cấu trúc dữ liệu sẽ được lưu trữ trong các tài khoản bộ đếm của chúng ta. Đây là dữ liệu sẽ được lưu trữ trong trường data của tài khoản.

Thêm đoạn mã sau vào lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u64,
}

Định nghĩa enum lệnh

Hãy định nghĩa các lệnh mà chương trình của chúng ta có thể thực thi. Chúng ta sẽ sử dụng một enum trong đó mỗi biến thể đại diện cho một lệnh khác nhau.

Thêm đoạn mã sau vào lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 },
IncrementCounter,
}

Triển khai giải mã hóa lệnh

Bây giờ chúng ta cần giải mã hóa instruction_data (các byte thô) thành một trong các biến thể enum CounterInstruction của chúng ta. Phương thức try_from_slice của Borsh xử lý việc chuyển đổi này một cách tự động.

Cập nhật hàm process_instruction để sử dụng giải mã hóa 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(())
}

Định tuyến lệnh đến các trình xử lý

Bây giờ hãy cập nhật hàm process_instruction chính để định tuyến các lệnh đến các hàm xử lý thích hợp của chúng.

Mẫu định tuyến này phổ biến trong các chương trình Solana. instruction_data được giải mã hóa thành một biến thể của enum đại diện cho lệnh, sau đó hàm xử lý thích hợp được gọi. Mỗi hàm xử lý bao gồm triển khai cho lệnh đó.

Thêm đoạn mã sau vào lib.rs cập nhật hàm process_instruction và thêm các trình xử lý cho các lệnh InitializeCounterIncrementCounter:

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

Triển khai trình xử lý khởi tạo

Hãy triển khai trình xử lý để tạo và khởi tạo một tài khoản bộ đếm mới. Vì chỉ có System Program mới có thể tạo tài khoản trên Solana, chúng ta sẽ sử dụng Cross Program Invocation (CPI), về cơ bản là gọi một chương trình khác từ chương trình của chúng ta.

Chương trình của chúng ta thực hiện CPI để gọi instruction create_account của System Program. Tài khoản mới được tạo với chương trình của chúng ta làm chủ sở hữu, cho phép chương trình có khả năng ghi vào tài khoản và khởi tạo dữ liệu.

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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(())
}

Instruction này chỉ nhằm mục đích minh họa. Nó không bao gồm các kiểm tra bảo mật và xác thực cần thiết cho các chương trình production.

Triển khai trình xử lý tăng

Bây giờ hãy triển khai trình xử lý tăng giá trị bộ đếm hiện có. Instruction này:

  • Đọc trường data của tài khoản cho counter_account
  • Deserialize nó thành struct CounterAccount
  • Tăng trường count lên 1
  • Serialize struct CounterAccount trở lại vào trường data của tài khoản

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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(())
}

Instruction này chỉ nhằm mục đích minh họa. Nó không bao gồm các kiểm tra bảo mật và xác thực cần thiết cho các chương trình production.

Chương trình hoàn chỉnh

Chúc mừng! Bạn đã xây dựng một chương trình Solana hoàn chỉnh minh họa cấu trúc cơ bản được chia sẻ bởi tất cả các chương trình Solana:

  • Entrypoint: Xác định nơi bắt đầu thực thi chương trình và định tuyến tất cả các yêu cầu đến đến các trình xử lý instruction phù hợp
  • Xử lý instruction: Xác định các instruction và các hàm xử lý liên quan của chúng
  • Quản lý trạng thái: Xác định cấu trúc dữ liệu tài khoản và quản lý trạng thái của chúng trong các tài khoản thuộc sở hữu chương trình
  • Cross Program Invocation (CPI): Gọi System Program để tạo các tài khoản thuộc sở hữu chương trình mới

Bước tiếp theo là kiểm thử chương trình để đảm bảo mọi thứ hoạt động chính xác.

Tạo chương trình mới

Đầu tiên, hãy tạo một dự án Rust mới cho chương trình Solana của chúng ta.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Bạn sẽ thấy các tệp src/lib.rsCargo.toml mặc định.

Cập nhật trường edition trong Cargo.toml thành 2021. Nếu không, bạn có thể gặp lỗi khi build chương trình.

Thêm các phụ thuộc

Bây giờ hãy thêm các phụ thuộc cần thiết để build chương trình Solana. Chúng ta cần solana-program cho core SDK và borsh cho serialization.

Terminal
$
cargo add solana-program@2.2.0
$
cargo add borsh

Không bắt buộc phải sử dụng Borsh. Tuy nhiên, đây là thư viện serialization được sử dụng phổ biến cho các chương trình Solana.

Cấu hình crate-type

Các chương trình Solana phải được biên dịch dưới dạng dynamic library. Thêm phần [lib] để cấu hình cách Cargo build chương trình.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

Nếu bạn không thêm cấu hình này, thư mục target/deploy sẽ không được tạo khi bạn build chương trình.

Thiết lập entrypoint của chương trình

Mọi chương trình Solana đều có một entrypoint, đó là hàm được gọi khi chương trình được thực thi. Hãy bắt đầu bằng cách thêm các import cần thiết cho chương trình và thiết lập entrypoint.

Thêm đoạn mã sau vào 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(())
}

Macro entrypoint xử lý việc deserialization dữ liệu input thành các tham số của hàm process_instruction.

Một entrypoint của chương trình Solana có function signature như sau. Các developer hoàn toàn có thể tự tạo implementation riêng của hàm entrypoint.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

Định nghĩa trạng thái chương trình

Bây giờ hãy định nghĩa cấu trúc dữ liệu sẽ được lưu trữ trong các tài khoản bộ đếm của chúng ta. Đây là dữ liệu sẽ được lưu trữ trong trường data của tài khoản.

Thêm đoạn mã sau vào lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u64,
}

Định nghĩa enum lệnh

Hãy định nghĩa các lệnh mà chương trình của chúng ta có thể thực thi. Chúng ta sẽ sử dụng một enum trong đó mỗi biến thể đại diện cho một lệnh khác nhau.

Thêm đoạn mã sau vào lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 },
IncrementCounter,
}

Triển khai giải mã hóa lệnh

Bây giờ chúng ta cần giải mã hóa instruction_data (các byte thô) thành một trong các biến thể enum CounterInstruction của chúng ta. Phương thức try_from_slice của Borsh xử lý việc chuyển đổi này một cách tự động.

Cập nhật hàm process_instruction để sử dụng giải mã hóa 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(())
}

Định tuyến lệnh đến các trình xử lý

Bây giờ hãy cập nhật hàm process_instruction chính để định tuyến các lệnh đến các hàm xử lý thích hợp của chúng.

Mẫu định tuyến này phổ biến trong các chương trình Solana. instruction_data được giải mã hóa thành một biến thể của enum đại diện cho lệnh, sau đó hàm xử lý thích hợp được gọi. Mỗi hàm xử lý bao gồm triển khai cho lệnh đó.

Thêm đoạn mã sau vào lib.rs cập nhật hàm process_instruction và thêm các trình xử lý cho các lệnh InitializeCounterIncrementCounter:

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

Triển khai trình xử lý khởi tạo

Hãy triển khai trình xử lý để tạo và khởi tạo một tài khoản bộ đếm mới. Vì chỉ có System Program mới có thể tạo tài khoản trên Solana, chúng ta sẽ sử dụng Cross Program Invocation (CPI), về cơ bản là gọi một chương trình khác từ chương trình của chúng ta.

Chương trình của chúng ta thực hiện CPI để gọi instruction create_account của System Program. Tài khoản mới được tạo với chương trình của chúng ta làm chủ sở hữu, cho phép chương trình có khả năng ghi vào tài khoản và khởi tạo dữ liệu.

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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(())
}

Instruction này chỉ nhằm mục đích minh họa. Nó không bao gồm các kiểm tra bảo mật và xác thực cần thiết cho các chương trình production.

Triển khai trình xử lý tăng

Bây giờ hãy triển khai trình xử lý tăng giá trị bộ đếm hiện có. Instruction này:

  • Đọc trường data của tài khoản cho counter_account
  • Deserialize nó thành struct CounterAccount
  • Tăng trường count lên 1
  • Serialize struct CounterAccount trở lại vào trường data của tài khoản

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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(())
}

Instruction này chỉ nhằm mục đích minh họa. Nó không bao gồm các kiểm tra bảo mật và xác thực cần thiết cho các chương trình production.

Chương trình hoàn chỉnh

Chúc mừng! Bạn đã xây dựng một chương trình Solana hoàn chỉnh minh họa cấu trúc cơ bản được chia sẻ bởi tất cả các chương trình Solana:

  • Entrypoint: Xác định nơi bắt đầu thực thi chương trình và định tuyến tất cả các yêu cầu đến đến các trình xử lý instruction phù hợp
  • Xử lý instruction: Xác định các instruction và các hàm xử lý liên quan của chúng
  • Quản lý trạng thái: Xác định cấu trúc dữ liệu tài khoản và quản lý trạng thái của chúng trong các tài khoản thuộc sở hữu chương trình
  • Cross Program Invocation (CPI): Gọi System Program để tạo các tài khoản thuộc sở hữu chương trình mới

Bước tiếp theo là kiểm thử chương trình để đảm bảo mọi thứ hoạt động chính xác.

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

Phần 2: Kiểm thử chương trình

Bây giờ hãy kiểm thử chương trình counter của chúng ta. Chúng ta sẽ sử dụng LiteSVM, một framework kiểm thử cho phép kiểm thử các chương trình mà không cần deploy lên cluster.

Thêm các phụ thuộc kiểm thử

Đầu tiên, hãy thêm các phụ thuộc cần thiết cho việc kiểm thử. Chúng ta sẽ sử dụng litesvm để kiểm thử và solana-sdk.

Terminal
$
cargo add litesvm@0.6.1 --dev
$
cargo add solana-sdk@2.2.0 --dev

Tạo module kiểm thử

Bây giờ hãy thêm một module kiểm thử vào chương trình của chúng ta. Chúng ta sẽ bắt đầu với cấu trúc cơ bản và các import.

Thêm đoạn mã sau vào lib.rs, ngay bên dưới mã chương trình:

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

Thuộc tính #[cfg(test)] đảm bảo mã này chỉ được biên dịch khi chạy các bài kiểm thử.

Khởi tạo môi trường kiểm thử

Hãy thiết lập môi trường kiểm thử với LiteSVM và nạp tiền cho tài khoản payer.

LiteSVM mô phỏng môi trường runtime của Solana, cho phép chúng ta kiểm thử chương trình mà không cần deploy lên cluster thực.

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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");

Tải chương trình

Bây giờ chúng ta cần build và tải chương trình vào môi trường kiểm thử. Chạy lệnh cargo build-sbf để build chương trình. Lệnh này sẽ tạo ra file counter_program.so trong thư mục target/deploy.

Terminal
$
cargo build-sbf

Đảm bảo edition trong Cargo.toml được đặt thành 2021.

Sau khi build, chúng ta có thể load chương trình.

Cập nhật hàm test_counter_program để load chương trình vào môi trường kiểm thử.

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");

Bạn phải chạy cargo build-sbf trước khi chạy test để tạo file .so. Test sẽ load chương trình đã được compile.

Kiểm thử instruction khởi tạo

Hãy kiểm thử instruction khởi tạo bằng cách tạo một counter account mới với giá trị ban đầu.

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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);

Xác minh khởi tạo

Sau khi khởi tạo, hãy xác minh counter account đã được tạo đúng cách với giá trị mong đợi.

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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);

Kiểm thử instruction tăng giá trị

Bây giờ hãy kiểm thử instruction tăng giá trị để đảm bảo nó cập nhật giá trị counter một cách chính xác.

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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);

Xác minh kết quả cuối cùng

Cuối cùng, hãy xác minh việc tăng giá trị đã hoạt động đúng bằng cách kiểm tra giá trị counter đã được cập nhật.

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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);

Chạy các bài kiểm tra với lệnh sau. Cờ --nocapture in ra kết quả của bài kiểm tra.

Terminal
$
cargo test -- --nocapture

Đầu ra dự kiến:

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

Thêm các phụ thuộc kiểm thử

Đầu tiên, hãy thêm các phụ thuộc cần thiết cho việc kiểm thử. Chúng ta sẽ sử dụng litesvm để kiểm thử và solana-sdk.

Terminal
$
cargo add litesvm@0.6.1 --dev
$
cargo add solana-sdk@2.2.0 --dev

Tạo module kiểm thử

Bây giờ hãy thêm một module kiểm thử vào chương trình của chúng ta. Chúng ta sẽ bắt đầu với cấu trúc cơ bản và các import.

Thêm đoạn mã sau vào lib.rs, ngay bên dưới mã chương trình:

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

Thuộc tính #[cfg(test)] đảm bảo mã này chỉ được biên dịch khi chạy các bài kiểm thử.

Khởi tạo môi trường kiểm thử

Hãy thiết lập môi trường kiểm thử với LiteSVM và nạp tiền cho tài khoản payer.

LiteSVM mô phỏng môi trường runtime của Solana, cho phép chúng ta kiểm thử chương trình mà không cần deploy lên cluster thực.

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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");

Tải chương trình

Bây giờ chúng ta cần build và tải chương trình vào môi trường kiểm thử. Chạy lệnh cargo build-sbf để build chương trình. Lệnh này sẽ tạo ra file counter_program.so trong thư mục target/deploy.

Terminal
$
cargo build-sbf

Đảm bảo edition trong Cargo.toml được đặt thành 2021.

Sau khi build, chúng ta có thể load chương trình.

Cập nhật hàm test_counter_program để load chương trình vào môi trường kiểm thử.

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");

Bạn phải chạy cargo build-sbf trước khi chạy test để tạo file .so. Test sẽ load chương trình đã được compile.

Kiểm thử instruction khởi tạo

Hãy kiểm thử instruction khởi tạo bằng cách tạo một counter account mới với giá trị ban đầu.

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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);

Xác minh khởi tạo

Sau khi khởi tạo, hãy xác minh counter account đã được tạo đúng cách với giá trị mong đợi.

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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);

Kiểm thử instruction tăng giá trị

Bây giờ hãy kiểm thử instruction tăng giá trị để đảm bảo nó cập nhật giá trị counter một cách chính xác.

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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);

Xác minh kết quả cuối cùng

Cuối cùng, hãy xác minh việc tăng giá trị đã hoạt động đúng bằng cách kiểm tra giá trị counter đã được cập nhật.

Thêm đoạn mã sau vào lib.rs cập nhật hàm 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);

Chạy các bài kiểm tra với lệnh sau. Cờ --nocapture in ra kết quả của bài kiểm tra.

Terminal
$
cargo test -- --nocapture

Đầu ra dự kiến:

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"

Phần 3: Gọi chương trình

Bây giờ hãy thêm một script client để gọi chương trình.

Tạo ví dụ client

Hãy tạo một client Rust để tương tác với chương trình đã triển khai của chúng ta.

Terminal
$
mkdir examples
$
touch examples/client.rs

Thêm cấu hình sau vào Cargo.toml:

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

Cài đặt các dependency của client:

Terminal
$
cargo add solana-client@2.2.0 --dev
$
cargo add tokio --dev

Triển khai mã client

Bây giờ hãy triển khai client sẽ gọi chương trình đã triển khai của chúng ta.

Chạy lệnh sau để lấy program ID từ file keypair:

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

Thêm mã client vào examples/client.rs và thay thế program_id bằng kết quả của lệnh trước:

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

Tạo ví dụ client

Hãy tạo một client Rust để tương tác với chương trình đã triển khai của chúng ta.

Terminal
$
mkdir examples
$
touch examples/client.rs

Thêm cấu hình sau vào Cargo.toml:

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

Cài đặt các dependency của client:

Terminal
$
cargo add solana-client@2.2.0 --dev
$
cargo add tokio --dev

Triển khai mã client

Bây giờ hãy triển khai client sẽ gọi chương trình đã triển khai của chúng ta.

Chạy lệnh sau để lấy program ID từ file keypair:

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

Thêm mã client vào examples/client.rs và thay thế program_id bằng kết quả của lệnh trước:

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"

Phần 4: Triển khai chương trình

Bây giờ chúng ta đã có chương trình và client sẵn sàng, hãy build, triển khai và gọi chương trình.

Build chương trình

Đầu tiên, hãy build chương trình của chúng ta.

Terminal
$
cargo build-sbf

Lệnh này biên dịch chương trình của bạn và tạo ra hai file quan trọng trong target/deploy/:

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

Bạn có thể xem ID chương trình của mình bằng cách chạy lệnh sau:

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

Kết quả ví dụ:

HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Khởi động validator cục bộ

Để phát triển, chúng ta sẽ sử dụng test validator cục bộ.

Đầu tiên, cấu hình Solana CLI để sử dụng localhost:

Terminal
$
solana config set -ul

Kết quả ví dụ:

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

Bây giờ khởi động test validator trong một terminal riêng:

Terminal
$
solana-test-validator

Triển khai chương trình

Với validator đang chạy, triển khai chương trình của bạn lên cluster cục bộ:

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

Kết quả ví dụ:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

Bạn có thể xác minh việc triển khai bằng lệnh solana program show với program ID của bạn:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Kết quả ví dụ:

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

Chạy client

Với validator cục bộ vẫn đang chạy, thực thi client:

Terminal
$
cargo run --example client

Kết quả mong đợi:

Requesting airdrop...
Airdrop confirmed
Initializing counter...
Counter initialized!
Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14k
Counter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9Zfofcy
Incrementing counter...
Counter incremented!
Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS

Với validator cục bộ đang chạy, bạn có thể xem các giao dịch trên Solana Explorer bằng cách sử dụng chữ ký giao dịch đầu ra. Lưu ý rằng cluster trên Solana Explorer phải được đặt thành "Custom RPC URL", mặc định là http://localhost:8899solana-test-validator đang chạy.

Is this page helpful?

Quản lý bởi

© 2026 Solana Foundation.
Đã đăng ký bản quyền.
Kết nối