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 linh hoạt trong cách tổ chức mã. Yêu cầu duy nhất là chương trình phải có một entrypoint, định nghĩa 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 cho cấu trúc tệp, các chương trình Solana thường tuân theo một mẫu phổ biến:

  • entrypoint.rs: Định nghĩa điểm vào để định tuyến các chỉ thị đế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 chỉ thị mà chương trình có thể thực thi.
  • processor.rs: Định nghĩa các trình xử lý chỉ thị (hàm) thực hiện logic nghiệp vụ cho mỗi chỉ thị.
  • 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 Chương trình Token.

Chương trình mẫu

Để minh họa cách xây dựng một chương trình Rust gốc với nhiều chỉ thị, chúng ta sẽ xem xét một chương trình bộ đếm đơn giản thực hiện hai chỉ thị:

  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 việc xây dựng chương trình bộ đế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 nó lên.

Tạo một 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 mặc định src/lib.rsCargo.toml.

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 xây dựng 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 để xây dựng chương trình Solana. Chúng ta cần solana-program cho SDK cốt lõi và borsh cho quá trình tuần tự hóa.

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 tuần tự hóa đượ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 thư viện động. Thêm phần [lib] để cấu hình cách Cargo xây dựng chương trình.

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

Nếu bạn không bao gồm cấu hình này, thư mục target/deploy sẽ không được tạo ra khi bạn xây dựng chương trình.

Thiết lập điểm vào chương trình

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

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 giải tuần tự hóa 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ó chữ ký hàm sau đây. Các nhà phát triển có thể tự do tạo triển khai riêng cho 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ờ chúng ta 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 instruction

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ã instruction

Bây giờ chúng ta cần giải mã instruction_data (dữ liệu thô) thành một trong các biến thể enum CounterInstruction của chúng ta. Phương thức Borsh try_from_slice 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ã 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 các instruction đến các trình xử lý

Bây giờ hãy cập nhật hàm chính process_instruction để đị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ã 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 việc 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 một CPI để gọi lệnh 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à chủ sở hữu, cho chương trình của chúng ta 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(())
}

Hướng dẫn 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 sản phẩm thực tế.

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

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

  • Đọc trường data của tài khoản cho counter_account
  • Giải tuần tự hóa nó thành một cấu trúc CounterAccount
  • Tăng trường count lên 1
  • Tuần tự hóa cấu trúc CounterAccount trở lại 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(())
}

Hướng dẫn 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 sản phẩm thực tế.

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 thể hiện 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ý chỉ thị thích hợp
  • Xử lý chỉ thị: Xác định các chỉ thị và các hàm xử lý liên quan
  • 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 của chương trình
  • Cross Program Invocation (CPI): Gọi System Program để tạo các tài khoản mới thuộc sở hữu của chương trình

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

Tạo một 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 mặc định src/lib.rsCargo.toml.

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 xây dựng 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 để xây dựng chương trình Solana. Chúng ta cần solana-program cho SDK cốt lõi và borsh cho quá trình tuần tự hóa.

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 tuần tự hóa đượ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 thư viện động. Thêm phần [lib] để cấu hình cách Cargo xây dựng chương trình.

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

Nếu bạn không bao gồm cấu hình này, thư mục target/deploy sẽ không được tạo ra khi bạn xây dựng chương trình.

Thiết lập điểm vào chương trình

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

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 giải tuần tự hóa 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ó chữ ký hàm sau đây. Các nhà phát triển có thể tự do tạo triển khai riêng cho 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ờ chúng ta 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 instruction

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ã instruction

Bây giờ chúng ta cần giải mã instruction_data (dữ liệu thô) thành một trong các biến thể enum CounterInstruction của chúng ta. Phương thức Borsh try_from_slice 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ã 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 các instruction đến các trình xử lý

Bây giờ hãy cập nhật hàm chính process_instruction để đị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ã 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 việc 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 một CPI để gọi lệnh 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à chủ sở hữu, cho chương trình của chúng ta 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(())
}

Hướng dẫn 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 sản phẩm thực tế.

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

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

  • Đọc trường data của tài khoản cho counter_account
  • Giải tuần tự hóa nó thành một cấu trúc CounterAccount
  • Tăng trường count lên 1
  • Tuần tự hóa cấu trúc CounterAccount trở lại 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(())
}

Hướng dẫn 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 sản phẩm thực tế.

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 thể hiện 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ý chỉ thị thích hợp
  • Xử lý chỉ thị: Xác định các chỉ thị và các hàm xử lý liên quan
  • 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 của chương trình
  • Cross Program Invocation (CPI): Gọi System Program để tạo các tài khoản mới thuộc sở hữu của chương trình

Bước tiếp theo là kiểm tra 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 tra chương trình

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

Thêm các phụ thuộc cho kiểm tra

Đầu tiên, hãy thêm các phụ thuộc cần thiết cho việc kiểm tra. Chúng ta sẽ sử dụng litesvm cho việc kiểm tra 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 phần 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 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à cấp vốn cho tài khoản thanh toán.

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 triển khai lên một cụm 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 xây dựng và tải chương trình vào môi trường kiểm thử. Chạy lệnh cargo build-sbf để xây dựng chương trình. Điều này sẽ tạo ra tệp counter_program.so trong thư mục target/deploy.

Terminal
$
cargo build-sbf

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

Sau khi xây dựng, chúng ta có thể tải chương trình.

Cập nhật hàm test_counter_program để tải 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 kiểm thử để tạo ra tệp .so. Bài kiểm thử sẽ tải chương trình đã được biên dịch.

Kiểm thử lệnh khởi tạo

Hãy kiểm thử lệnh khởi tạo bằng cách tạo một tài khoản bộ đếm mới với giá trị bắt đầ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 quá trình khởi tạo

Sau khi khởi tạo, hãy xác minh tài khoản bộ đếm đã đượ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 tra lệnh tăng

Bây giờ hãy kiểm tra lệnh tăng để đảm bảo nó cập nhật đúng giá trị bộ đếm.

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 rằng lệnh tăng đã hoạt động đúng bằng cách kiểm tra giá trị bộ đếm đã đượ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 sẽ in ra kết quả của bài kiểm tra.

Terminal
$
cargo test -- --nocapture

Kết quả mong đợi:

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 cho kiểm tra

Đầu tiên, hãy thêm các phụ thuộc cần thiết cho việc kiểm tra. Chúng ta sẽ sử dụng litesvm cho việc kiểm tra 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 phần 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 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à cấp vốn cho tài khoản thanh toán.

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 triển khai lên một cụm 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 xây dựng và tải chương trình vào môi trường kiểm thử. Chạy lệnh cargo build-sbf để xây dựng chương trình. Điều này sẽ tạo ra tệp counter_program.so trong thư mục target/deploy.

Terminal
$
cargo build-sbf

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

Sau khi xây dựng, chúng ta có thể tải chương trình.

Cập nhật hàm test_counter_program để tải 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 kiểm thử để tạo ra tệp .so. Bài kiểm thử sẽ tải chương trình đã được biên dịch.

Kiểm thử lệnh khởi tạo

Hãy kiểm thử lệnh khởi tạo bằng cách tạo một tài khoản bộ đếm mới với giá trị bắt đầ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 quá trình khởi tạo

Sau khi khởi tạo, hãy xác minh tài khoản bộ đếm đã đượ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 tra lệnh tăng

Bây giờ hãy kiểm tra lệnh tăng để đảm bảo nó cập nhật đúng giá trị bộ đếm.

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 rằng lệnh tăng đã hoạt động đúng bằng cách kiểm tra giá trị bộ đếm đã đượ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 sẽ in ra kết quả của bài kiểm tra.

Terminal
$
cargo test -- --nocapture

Kết quả mong đợi:

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ụ về 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 dependencies 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 ID chương trình từ tệp 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ụ về 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 dependencies 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 ID chương trình từ tệp 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 tệp 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 một validator kiểm thử 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 validator kiểm thử trong một terminal riêng biệt:

Terminal
$
solana-test-validator

Triển khai chương trình

Với validator đang chạy, hãy triển khai chương trình của bạn lên cụm 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 ID chương trình của bạn:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Ví dụ đầu ra:

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

Đầu ra dự kiến:

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?