Struktur Program Rust
Program Solana yang ditulis dalam Rust memiliki persyaratan struktural minimal,
yang memungkinkan fleksibilitas dalam pengorganisasian kode. Satu-satunya
persyaratan adalah program harus memiliki entrypoint
, yang mendefinisikan di
mana eksekusi program dimulai.
Struktur Program
Meskipun tidak ada aturan ketat untuk struktur file, program Solana biasanya mengikuti pola umum:
entrypoint.rs
: Mendefinisikan entrypoint yang mengarahkan instruksi masuk.state.rs
: Mendefinisikan state program (data akun).instructions.rs
: Mendefinisikan instruksi yang dapat dijalankan oleh program.processor.rs
: Mendefinisikan handler instruksi (fungsi) yang mengimplementasikan logika bisnis untuk setiap instruksi.error.rs
: Mendefinisikan error kustom yang dapat dikembalikan oleh program.
Sebagai contoh, lihat Token Program.
Contoh Program
Untuk mendemonstrasikan cara membangun program Rust native dengan beberapa instruksi, kita akan membahas program counter sederhana yang mengimplementasikan dua instruksi:
InitializeCounter
: Membuat dan menginisialisasi akun baru dengan nilai awal.IncrementCounter
: Menambah nilai yang disimpan dalam akun yang sudah ada.
Untuk kesederhanaan, program akan diimplementasikan dalam satu file lib.rs
,
meskipun dalam praktiknya Anda mungkin ingin membagi program yang lebih besar
menjadi beberapa file.
Bagian 1: Menulis Program
Mari mulai dengan membangun program counter. Kita akan membuat program yang dapat menginisialisasi counter dengan nilai awal dan menambahkannya.
Membuat program baru
Pertama, mari buat proyek Rust baru untuk program Solana kita.
$cargo new counter_program --lib$cd counter_program
Anda akan melihat file default src/lib.rs
dan Cargo.toml
.
Perbarui kolom edition
di Cargo.toml
menjadi 2021. Jika tidak, Anda
mungkin mengalami error saat membangun program.
Tambahkan dependensi
Sekarang mari tambahkan dependensi yang diperlukan untuk membangun program
Solana. Kita membutuhkan solana-program
untuk SDK inti dan borsh
untuk
serialisasi.
$cargo add solana-program@2.2.0$cargo add borsh
Tidak ada keharusan untuk menggunakan Borsh. Namun, ini adalah pustaka serialisasi yang umum digunakan untuk program Solana.
Konfigurasi crate-type
Program Solana harus dikompilasi sebagai pustaka dinamis. Tambahkan bagian
[lib]
untuk mengonfigurasi cara Cargo membangun program.
[lib]crate-type = ["cdylib", "lib"]
Jika Anda tidak menyertakan konfigurasi ini, direktori target/deploy tidak akan dibuat saat Anda membangun program.
Siapkan entrypoint program
Setiap program Solana memiliki entrypoint, yaitu fungsi yang dipanggil saat program dijalankan. Mari mulai dengan menambahkan impor yang kita perlukan untuk program dan menyiapkan entrypoint.
Tambahkan kode berikut ke 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
menangani deserialisasi data input
menjadi parameter dari fungsi
process_instruction
.
Sebuah entrypoint
program Solana memiliki tanda tangan fungsi berikut.
Pengembang bebas membuat implementasi mereka sendiri dari fungsi entrypoint
.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Tentukan state program
Sekarang mari tentukan struktur data yang akan disimpan dalam akun penghitung
kita. Ini adalah data yang akan disimpan di bidang data
dari akun.
Tambahkan kode berikut ke lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {pub count: u64,}
Tentukan enum instruksi
Mari tentukan instruksi yang dapat dijalankan oleh program kita. Kita akan menggunakan enum di mana setiap varian mewakili instruksi yang berbeda.
Tambahkan kode berikut ke lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 },IncrementCounter,}
Implementasikan deserialisasi instruksi
Sekarang kita perlu mendeserialkan instruction_data
(byte mentah) menjadi
salah satu varian enum CounterInstruction
kita. Metode Borsh try_from_slice
menangani konversi ini secara otomatis.
Perbarui fungsi process_instruction
untuk menggunakan deserialisasi Borsh:
pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {let instruction = CounterInstruction::try_from_slice(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;Ok(())}
Arahkan instruksi ke handler
Sekarang mari perbarui fungsi utama process_instruction
untuk mengarahkan
instruksi ke fungsi handler yang sesuai.
Pola perutean ini umum dalam program Solana. instruction_data
dideserialkan
menjadi varian dari enum yang mewakili instruksi, kemudian fungsi handler yang
sesuai dipanggil. Setiap fungsi handler mencakup implementasi untuk instruksi
tersebut.
Tambahkan kode berikut ke lib.rs
untuk memperbarui fungsi
process_instruction
dan menambahkan handler untuk instruksi
InitializeCounter
dan IncrementCounter
:
pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {let instruction = CounterInstruction::try_from_slice(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;match instruction {CounterInstruction::InitializeCounter { initial_value } => {process_initialize_counter(program_id, accounts, initial_value)?}CounterInstruction::IncrementCounter => {process_increment_counter(program_id, accounts)?}};Ok(())}fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {Ok(())}fn process_increment_counter(program_id: &Pubkey,accounts: &[AccountInfo],) -> ProgramResult {Ok(())}
Implementasikan handler inisialisasi
Mari implementasikan handler untuk membuat dan menginisialisasi akun penghitung baru. Karena hanya System Program yang dapat membuat akun di Solana, kita akan menggunakan Cross Program Invocation (CPI), pada dasarnya memanggil program lain dari program kita.
Program kita membuat CPI untuk memanggil instruksi create_account
dari System
Program. Akun baru dibuat dengan program kita sebagai pemiliknya, memberikan
program kita kemampuan untuk menulis ke akun dan menginisialisasi data.
Tambahkan kode berikut ke lib.rs
untuk memperbarui fungsi
process_initialize_counter
:
fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;let payer_account = next_account_info(accounts_iter)?;let system_program = next_account_info(accounts_iter)?;let account_space = 8;let rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);invoke(&system_instruction::create_account(payer_account.key,counter_account.key,required_lamports,account_space as u64,program_id,),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;let counter_data = CounterAccount {count: initial_value,};let mut account_data = &mut counter_account.data.borrow_mut()[..];counter_data.serialize(&mut account_data)?;msg!("Counter initialized with value: {}", initial_value);Ok(())}
Instruksi ini hanya untuk tujuan demonstrasi. Instruksi ini tidak menyertakan pemeriksaan keamanan dan validasi yang diperlukan untuk program produksi.
Implementasikan handler increment
Sekarang mari kita implementasikan handler yang menambah counter yang sudah ada. Instruksi ini:
- Membaca bidang
data
akun untukcounter_account
- Mendeserialkan menjadi struct
CounterAccount
- Menambah bidang
count
sebanyak 1 - Menserialkan struct
CounterAccount
kembali ke bidangdata
akun
Tambahkan kode berikut ke lib.rs
untuk memperbarui fungsi
process_increment_counter
:
fn process_increment_counter(program_id: &Pubkey,accounts: &[AccountInfo],) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;if counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}let mut data = counter_account.data.borrow_mut();let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;counter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;counter_data.serialize(&mut &mut data[..])?;msg!("Counter incremented to: {}", counter_data.count);Ok(())}
Instruksi ini hanya untuk tujuan demonstrasi. Instruksi ini tidak menyertakan pemeriksaan keamanan dan validasi yang diperlukan untuk program produksi.
Program Selesai
Selamat! Anda telah membangun program Solana lengkap yang mendemonstrasikan struktur dasar yang dimiliki oleh semua program Solana:
- Entrypoint: Mendefinisikan di mana eksekusi program dimulai dan mengarahkan semua permintaan masuk ke handler instruksi yang sesuai
- Penanganan Instruksi: Mendefinisikan instruksi dan fungsi handler terkait
- Manajemen State: Mendefinisikan struktur data akun dan mengelola state mereka dalam akun yang dimiliki program
- Cross Program Invocation (CPI): Memanggil System Program untuk membuat akun baru yang dimiliki program
Langkah selanjutnya adalah menguji program untuk memastikan semuanya berfungsi dengan benar.
Bagian 2: Menguji Program
Sekarang mari kita uji program counter kita. Kita akan menggunakan LiteSVM, sebuah framework pengujian yang memungkinkan kita menguji program tanpa men-deploy ke cluster.
Tambahkan dependensi pengujian
Pertama, mari tambahkan dependensi yang diperlukan untuk pengujian. Kita akan
menggunakan litesvm
untuk pengujian dan solana-sdk
.
$cargo add litesvm@0.6.1 --dev$cargo add solana-sdk@2.2.0 --dev
Membuat modul pengujian
Sekarang mari tambahkan modul pengujian ke program kita. Kita akan mulai dengan kerangka dasar dan impor yang diperlukan.
Tambahkan kode berikut ke lib.rs
, langsung di bawah kode program:
#[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}}
Atribut #[cfg(test)]
memastikan kode ini hanya dikompilasi saat menjalankan
pengujian.
Menginisialisasi lingkungan pengujian
Mari siapkan lingkungan pengujian dengan LiteSVM dan danai akun pembayar.
LiteSVM mensimulasikan lingkungan runtime Solana, memungkinkan kita menguji program tanpa perlu men-deploy ke cluster yang sebenarnya.
Tambahkan kode berikut ke lib.rs
dengan memperbarui fungsi
test_counter_program
:
let mut svm = LiteSVM::new();let payer = Keypair::new();svm.airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to airdrop");
Memuat program
Sekarang kita perlu membangun dan memuat program kita ke dalam lingkungan
pengujian. Jalankan perintah cargo build-sbf
untuk membangun program. Ini akan
menghasilkan file counter_program.so
di direktori target/deploy
.
$cargo build-sbf
Pastikan edition
di Cargo.toml
diatur ke 2021
.
Setelah membangun, kita dapat memuat program.
Perbarui fungsi test_counter_program
untuk memuat program ke dalam lingkungan
pengujian.
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");
Anda harus menjalankan cargo build-sbf
sebelum menjalankan pengujian untuk
menghasilkan file .so
. Pengujian memuat program yang telah dikompilasi.
Menguji instruksi inisialisasi
Mari menguji instruksi inisialisasi dengan membuat akun penghitung baru dengan nilai awal.
Tambahkan kode berikut ke lib.rs
untuk memperbarui fungsi
test_counter_program
:
let counter_keypair = Keypair::new();let initial_value: u64 = 42;println!("Testing counter initialization...");let init_instruction_data =borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value }).expect("Failed to serialize instruction");let initialize_instruction = Instruction::new_with_bytes(program_id,&init_instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true),AccountMeta::new(payer.pubkey(), true),AccountMeta::new_readonly(system_program::id(), false),],);let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));let transaction = Transaction::new(&[&payer, &counter_keypair],message,svm.latest_blockhash());let result = svm.send_transaction(transaction);assert!(result.is_ok(), "Initialize transaction should succeed");let logs = result.unwrap().logs;println!("Transaction logs:\n{:#?}", logs);
Verifikasi inisialisasi
Setelah inisialisasi, mari verifikasi bahwa akun counter telah dibuat dengan benar dengan nilai yang diharapkan.
Tambahkan kode berikut ke lib.rs
untuk memperbarui fungsi
test_counter_program
:
let account = svm.get_account(&counter_keypair.pubkey()).expect("Failed to get counter account");let counter: CounterAccount = CounterAccount::try_from_slice(account.data()).expect("Failed to deserialize counter data");assert_eq!(counter.count, 42);println!("Counter initialized successfully with value: {}", counter.count);
Uji instruksi increment
Sekarang mari uji instruksi increment untuk memastikan bahwa instruksi tersebut memperbarui nilai counter dengan benar.
Tambahkan kode berikut ke lib.rs
untuk memperbarui fungsi
test_counter_program
:
println!("Testing counter increment...");let increment_instruction_data =borsh::to_vec(&CounterInstruction::IncrementCounter).expect("Failed to serialize instruction");let increment_instruction = Instruction::new_with_bytes(program_id,&increment_instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true)],);let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));let transaction = Transaction::new(&[&payer, &counter_keypair],message,svm.latest_blockhash());let result = svm.send_transaction(transaction);assert!(result.is_ok(), "Increment transaction should succeed");let logs = result.unwrap().logs;println!("Transaction logs:\n{:#?}", logs);
Verifikasi hasil akhir
Terakhir, mari verifikasi bahwa increment telah berhasil dengan memeriksa nilai counter yang telah diperbarui.
Tambahkan kode berikut ke lib.rs
untuk memperbarui fungsi
test_counter_program
:
let account = svm.get_account(&counter_keypair.pubkey()).expect("Failed to get counter account");let counter: CounterAccount = CounterAccount::try_from_slice(account.data()).expect("Failed to deserialize counter data");assert_eq!(counter.count, 43);println!("Counter incremented successfully to: {}", counter.count);
Jalankan pengujian dengan perintah berikut. Flag --nocapture
akan mencetak
output dari pengujian.
$cargo test -- --nocapture
Output yang diharapkan:
Testing counter initialization...Transaction logs:["Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]","Program 11111111111111111111111111111111 invoke [2]","Program 11111111111111111111111111111111 success","Program log: Counter initialized with value: 42","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",]Counter initialized successfully with value: 42Testing counter increment...Transaction logs:["Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]","Program log: Counter incremented to: 43","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",]Counter incremented successfully to: 43
Bagian 3: Memanggil Program
Sekarang mari tambahkan skrip klien untuk memanggil program.
Membuat contoh klien
Mari kita buat klien Rust untuk berinteraksi dengan program yang telah kita deploy.
$mkdir examples$touch examples/client.rs
Tambahkan konfigurasi berikut ke Cargo.toml
:
[[example]]name = "client"path = "examples/client.rs"
Instal dependensi klien:
$cargo add solana-client@2.2.0 --dev$cargo add tokio --dev
Implementasi kode klien
Sekarang mari kita implementasikan klien yang akan memanggil program yang telah kita deploy.
Jalankan perintah berikut untuk mendapatkan ID program dari file keypair:
$solana address -k ./target/deploy/counter_program-keypair.json
Tambahkan kode klien ke examples/client.rs
dan ganti program_id
dengan
output dari perintah sebelumnya:
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");
use solana_client::rpc_client::RpcClient;use solana_sdk::{commitment_config::CommitmentConfig,instruction::{AccountMeta, Instruction},pubkey::Pubkey,signature::{Keypair, Signer},system_program,transaction::Transaction,};use std::str::FromStr;use counter_program::CounterInstruction;#[tokio::main]async fn main() {// Replace with your actual program ID from deploymentlet program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");// Connect to local clusterlet rpc_url = String::from("http://localhost:8899");let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());// Generate a new keypair for paying feeslet payer = Keypair::new();// Request airdrop of 1 SOL for transaction feesprintln!("Requesting airdrop...");let airdrop_signature = client.request_airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to request airdrop");// Wait for airdrop confirmationloop {if client.confirm_transaction(&airdrop_signature).unwrap_or(false){break;}std::thread::sleep(std::time::Duration::from_millis(500));}println!("Airdrop confirmed");println!("\nInitializing counter...");let counter_keypair = Keypair::new();let initial_value = 100u64;// Serialize the initialize instruction datalet instruction_data = borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value }).expect("Failed to serialize instruction");let initialize_instruction = Instruction::new_with_bytes(program_id,&instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true),AccountMeta::new(payer.pubkey(), true),AccountMeta::new_readonly(system_program::id(), false),],);let mut transaction =Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));let blockhash = client.get_latest_blockhash().expect("Failed to get blockhash");transaction.sign(&[&payer, &counter_keypair], blockhash);match client.send_and_confirm_transaction(&transaction) {Ok(signature) => {println!("Counter initialized!");println!("Transaction: {}", signature);println!("Counter address: {}", counter_keypair.pubkey());}Err(err) => {eprintln!("Failed to initialize counter: {}", err);return;}}println!("\nIncrementing counter...");// Serialize the increment instruction datalet increment_data = borsh::to_vec(&CounterInstruction::IncrementCounter).expect("Failed to serialize instruction");let increment_instruction = Instruction::new_with_bytes(program_id,&increment_data,vec![AccountMeta::new(counter_keypair.pubkey(), true)],);let mut transaction =Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], blockhash);match client.send_and_confirm_transaction(&transaction) {Ok(signature) => {println!("Counter incremented!");println!("Transaction: {}", signature);}Err(err) => {eprintln!("Failed to increment counter: {}", err);}}}
Bagian 4: Men-deploy Program
Sekarang kita telah memiliki program dan klien yang siap, mari kita build, deploy, dan panggil programnya.
Build program
Pertama, mari kita build program kita.
$cargo build-sbf
Perintah ini mengompilasi program Anda dan menghasilkan dua file penting di
target/deploy/
:
counter_program.so # The compiled programcounter_program-keypair.json # Keypair for the program ID
Anda dapat melihat ID program Anda dengan menjalankan perintah berikut:
$solana address -k ./target/deploy/counter_program-keypair.json
Contoh output:
HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Mulai validator lokal
Untuk pengembangan, kita akan menggunakan validator uji lokal.
Pertama, konfigurasikan Solana CLI untuk menggunakan localhost:
$solana config set -ul
Contoh output:
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed
Sekarang mulai validator uji di terminal terpisah:
$solana-test-validator
Deploy program
Dengan validator yang berjalan, deploy program Anda ke cluster lokal:
$solana program deploy ./target/deploy/counter_program.so
Contoh output:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
Anda dapat memverifikasi deployment menggunakan perintah solana program show
dengan ID program Anda:
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Contoh output:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
Jalankan klien
Dengan validator lokal yang masih berjalan, jalankan klien:
$cargo run --example client
Output yang diharapkan:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
Dengan validator lokal yang berjalan, Anda dapat melihat transaksi di
Solana Explorer menggunakan tanda
tangan transaksi yang dihasilkan. Perhatikan bahwa cluster pada Solana Explorer
harus diatur ke "Custom RPC URL", yang secara default adalah
http://localhost:8899
tempat solana-test-validator
berjalan.
Is this page helpful?