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 yang masuk.state.rs
: Mendefinisikan state khusus 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.
Anda dapat menemukan contoh di Solana Program Library.
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.
Membuat Program Baru
Pertama, buat proyek Rust baru menggunakan perintah standar cargo init
dengan
flag --lib
.
cargo init counter_program --lib
Navigasikan ke direktori proyek. Anda akan melihat file default src/lib.rs
dan
Cargo.toml
cd counter_program
Selanjutnya, tambahkan dependensi solana-program
. Ini adalah dependensi
minimum yang diperlukan untuk membangun program Solana.
cargo add solana-program@1.18.26
Selanjutnya, tambahkan snippet berikut ke Cargo.toml
. Jika Anda tidak
menyertakan konfigurasi ini, direktori target/deploy
tidak akan dibuat saat
Anda membangun program.
[lib]crate-type = ["cdylib", "lib"]
File Cargo.toml
Anda seharusnya terlihat seperti berikut:
[package]name = "counter_program"version = "0.1.0"edition = "2021"[lib]crate-type = ["cdylib", "lib"][dependencies]solana-program = "1.18.26"
Entrypoint Program
Entrypoint program Solana adalah fungsi yang dipanggil ketika program dijalankan. Entrypoint memiliki definisi dasar berikut dan pengembang bebas untuk membuat implementasi mereka sendiri dari fungsi entrypoint.
Untuk kemudahan, gunakan
entrypoint!
makro dari crate solana_program
untuk mendefinisikan entrypoint dalam program
Anda.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Ganti kode default di lib.rs
dengan kode berikut. Snippet ini:
- Mengimpor dependensi yang diperlukan dari
solana_program
- Mendefinisikan entrypoint program menggunakan makro
entrypoint!
- Mengimplementasikan fungsi
process_instruction
yang akan mengarahkan instruksi ke fungsi handler yang sesuai
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 {// Your program logicOk(())}
Makro entrypoint!
memerlukan fungsi dengan
tipe signature
sebagai argumen:
pub type ProcessInstruction =fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
Ketika program Solana dijalankan, entrypoint
mendeserialkan
data input
(yang disediakan sebagai bytes) menjadi tiga nilai dan meneruskannya ke fungsi
process_instruction
:
program_id
: Public key dari program yang dijalankan (program saat ini)accounts
:AccountInfo
untuk akun yang diperlukan oleh instruksi yang dijalankaninstruction_data
: Data tambahan yang diteruskan ke program yang menentukan instruksi untuk dieksekusi dan argumen yang diperlukan
Ketiga parameter ini secara langsung berhubungan dengan data yang harus disediakan oleh klien saat membangun instruksi untuk menjalankan program.
Mendefinisikan State Program
Saat membangun program Solana, Anda biasanya akan mulai dengan mendefinisikan state program Anda - data yang akan disimpan dalam akun yang dibuat dan dimiliki oleh program Anda.
State program didefinisikan menggunakan struct Rust yang merepresentasikan tata letak data dari akun program Anda. Anda dapat mendefinisikan beberapa struct untuk merepresentasikan jenis akun yang berbeda untuk program Anda.
Saat bekerja dengan akun, Anda memerlukan cara untuk mengonversi tipe data program Anda ke dan dari byte mentah yang disimpan di field data akun:
- Serialisasi: Mengonversi tipe data Anda menjadi byte untuk disimpan di field data akun
- Deserialisasi: Mengonversi byte yang disimpan dalam akun kembali menjadi tipe data Anda
Meskipun Anda dapat menggunakan format serialisasi apa pun untuk pengembangan program Solana, Borsh umumnya digunakan. Untuk menggunakan Borsh dalam program Solana Anda:
- Tambahkan crate
borsh
sebagai dependensi keCargo.toml
Anda:
cargo add borsh
- Impor trait Borsh dan gunakan macro derive untuk mengimplementasikan trait tersebut untuk struct Anda:
use borsh::{BorshSerialize, BorshDeserialize};// Define struct representing our counter account's data#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Tambahkan struct CounterAccount
ke lib.rs
untuk mendefinisikan state
program. Struct ini akan digunakan dalam instruksi inisialisasi dan increment.
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},};use borsh::{BorshSerialize, BorshDeserialize};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Mendefinisikan Instruksi
Instruksi mengacu pada operasi berbeda yang dapat dilakukan oleh program Solana Anda. Anggap saja sebagai API publik untuk program Anda - mereka mendefinisikan tindakan apa yang dapat dilakukan pengguna saat berinteraksi dengan program Anda.
Instruksi biasanya didefinisikan menggunakan enum Rust di mana:
- Setiap varian enum merepresentasikan instruksi yang berbeda
- Payload varian merepresentasikan parameter instruksi
Perhatikan bahwa varian enum Rust secara implisit dinomori mulai dari 0.
Berikut adalah contoh enum yang mendefinisikan dua instruksi:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}
Ketika klien memanggil program Anda, mereka harus menyediakan instruction data (sebagai buffer byte) di mana:
- Byte pertama mengidentifikasi varian instruksi mana yang akan dieksekusi (0, 1, dll.)
- Byte-byte yang tersisa berisi parameter instruksi yang diserialisasi (jika diperlukan)
Untuk mengkonversi instruction data (byte) menjadi varian enum, umumnya diimplementasikan metode pembantu. Metode ini:
- Memisahkan byte pertama untuk mendapatkan varian instruksi
- Melakukan pencocokan pada varian dan mengurai parameter tambahan dari byte-byte yang tersisa
- Mengembalikan varian enum yang sesuai
Sebagai contoh, metode unpack
untuk enum CounterInstruction
:
impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet initial_value = u64::from_le_bytes(rest.try_into().map_err(|_| ProgramError::InvalidInstructionData)?);Ok(Self::InitializeCounter { initial_value })}1 => Ok(Self::IncrementCounter), // No additional data needed_ => Err(ProgramError::InvalidInstructionData),}}}
Tambahkan kode berikut ke lib.rs
untuk mendefinisikan instruksi untuk program
counter.
use borsh::{BorshDeserialize, BorshSerialize};use solana_program::{account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg,program_error::ProgramError, pubkey::Pubkey,};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet initial_value = u64::from_le_bytes(rest.try_into().map_err(|_| ProgramError::InvalidInstructionData)?,);Ok(Self::InitializeCounter { initial_value })}1 => Ok(Self::IncrementCounter), // No additional data needed_ => Err(ProgramError::InvalidInstructionData),}}}
Handler Instruksi
Handler instruksi mengacu pada fungsi yang berisi logika bisnis untuk setiap
instruksi. Umumnya fungsi handler diberi nama process_<instruction_name>
,
tetapi Anda bebas memilih konvensi penamaan apa pun.
Tambahkan kode berikut ke lib.rs
. Kode ini menggunakan enum
CounterInstruction
dan metode unpack
yang didefinisikan pada langkah
sebelumnya untuk mengarahkan instruksi yang masuk ke fungsi handler yang sesuai:
entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Unpack instruction datalet instruction = CounterInstruction::unpack(instruction_data)?;// Match instruction typematch instruction {CounterInstruction::InitializeCounter { initial_value } => {process_initialize_counter(program_id, accounts, initial_value)?}CounterInstruction::IncrementCounter => process_increment_counter(program_id, accounts)?,};}fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {// Implementation details...Ok(())}fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {// Implementation details...Ok(())}
Selanjutnya, tambahkan implementasi fungsi process_initialize_counter
. Handler
instruksi ini:
- Membuat dan mengalokasikan ruang untuk akun baru untuk menyimpan data counter
- Menginisialisasi data akun dengan
initial_value
yang diberikan ke instruksi
// Initialize a new counter accountfn 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)?;// Size of our counter accountlet account_space = 8; // Size in bytes to store a u64// Calculate minimum balance for rent exemptionlet rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);// Create the counter accountinvoke(&system_instruction::create_account(payer_account.key, // Account paying for the new accountcounter_account.key, // Account to be createdrequired_lamports, // Amount of lamports to transfer to the new accountaccount_space as u64, // Size in bytes to allocate for the data fieldprogram_id, // Set program owner to our program),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;// Create a new CounterAccount struct with the initial valuelet counter_data = CounterAccount {count: initial_value,};// Get a mutable reference to the counter account's datalet mut account_data = &mut counter_account.data.borrow_mut()[..];// Serialize the CounterAccount struct into the account's datacounter_data.serialize(&mut account_data)?;msg!("Counter initialized with value: {}", initial_value);Ok(())}
Selanjutnya, tambahkan implementasi fungsi process_increment_counter
.
Instruksi ini menambah nilai dari akun counter yang sudah ada.
// Update an existing counter's valuefn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;// Verify account ownershipif counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}// Mutable borrow the account datalet mut data = counter_account.data.borrow_mut();// Deserialize the account data into our CounterAccount structlet mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;// Increment the counter valuecounter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;// Serialize the updated counter data back into the accountcounter_data.serialize(&mut &mut data[..])?;msg!("Counter incremented to: {}", counter_data.count);Ok(())}
Pengujian Instruksi
Untuk menguji instruksi program, tambahkan dependensi berikut ke Cargo.toml
.
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo add tokio --dev
Kemudian tambahkan modul pengujian berikut ke lib.rs
dan jalankan
cargo test-sbf
untuk menjalankan pengujian. Secara opsional, gunakan flag
--nocapture
untuk melihat pernyataan print dalam output.
cargo test-sbf -- --nocapture
#[cfg(test)]mod test {use super::*;use solana_program_test::*;use solana_sdk::{instruction::{AccountMeta, Instruction},signature::{Keypair, Signer},system_program,transaction::Transaction,};#[tokio::test]async fn test_counter_program() {let program_id = Pubkey::new_unique();let (mut banks_client, payer, recent_blockhash) = ProgramTest::new("counter_program",program_id,processor!(process_instruction),).start().await;// Create a new keypair to use as the address for our counter accountlet counter_keypair = Keypair::new();let initial_value: u64 = 42;// Step 1: Initialize the counterprintln!("Testing counter initialization...");// Create initialization instructionlet mut init_instruction_data = vec![0]; // 0 = initialize instructioninit_instruction_data.extend_from_slice(&initial_value.to_le_bytes());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),],);// Send transaction with initialize instructionlet mut transaction =Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], recent_blockhash);banks_client.process_transaction(transaction).await.unwrap();// Check account datalet account = banks_client.get_account(counter_keypair.pubkey()).await.expect("Failed to get counter account");if let Some(account_data) = account {let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data).expect("Failed to deserialize counter data");assert_eq!(counter.count, 42);println!("✅ Counter initialized successfully with value: {}",counter.count);}// Step 2: Increment the counterprintln!("Testing counter increment...");// Create increment instructionlet increment_instruction = Instruction::new_with_bytes(program_id,&[1], // 1 = increment instructionvec![AccountMeta::new(counter_keypair.pubkey(), true)],);// Send transaction with increment instructionlet mut transaction =Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], recent_blockhash);banks_client.process_transaction(transaction).await.unwrap();// Check account datalet account = banks_client.get_account(counter_keypair.pubkey()).await.expect("Failed to get counter account");if let Some(account_data) = account {let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data).expect("Failed to deserialize counter data");assert_eq!(counter.count, 43);println!("✅ Counter incremented successfully to: {}", counter.count);}}}
Contoh output:
running 1 test[2024-10-29T20:51:13.783708000Z INFO solana_program_test] "counter_program" SBF program from /counter_program/target/deploy/counter_program.so, modified 2 seconds, 169 ms, 153 µs and 461 ns ago[2024-10-29T20:51:13.855204000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1][2024-10-29T20:51:13.856052000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2][2024-10-29T20:51:13.856135000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success[2024-10-29T20:51:13.856242000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter initialized with value: 42[2024-10-29T20:51:13.856285000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 3791 of 200000 compute units[2024-10-29T20:51:13.856307000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success[2024-10-29T20:51:13.860038000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1][2024-10-29T20:51:13.860333000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter incremented to: 43[2024-10-29T20:51:13.860355000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 756 of 200000 compute units[2024-10-29T20:51:13.860375000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM successtest test::test_counter_program ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
Is this page helpful?