Dokumentasi SolanaMengembangkan ProgramProgram Rust

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:

  1. InitializeCounter: Membuat dan menginisialisasi akun baru dengan nilai awal.
  2. 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.

Terminal
cargo init counter_program --lib

Navigasikan ke direktori proyek. Anda akan melihat file default src/lib.rs dan Cargo.toml

Terminal
cd counter_program

Selanjutnya, tambahkan dependensi solana-program. Ini adalah dependensi minimum yang diperlukan untuk membangun program Solana.

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

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

File Cargo.toml Anda seharusnya terlihat seperti berikut:

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

  1. Mengimpor dependensi yang diperlukan dari solana_program
  2. Mendefinisikan entrypoint program menggunakan makro entrypoint!
  3. Mengimplementasikan fungsi process_instruction yang akan mengarahkan instruksi ke fungsi handler yang sesuai
lib.rs
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 logic
Ok(())
}

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 dijalankan
  • instruction_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:

  1. Tambahkan crate borsh sebagai dependensi ke Cargo.toml Anda:
Terminal
cargo add borsh
  1. 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.

lib.rs
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 logic
Ok(())
}
#[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 0
IncrementCounter, // 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:

  1. Memisahkan byte pertama untuk mendapatkan varian instruksi
  2. Melakukan pencocokan pada varian dan mengurai parameter tambahan dari byte-byte yang tersisa
  3. 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 byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let 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.

lib.rs
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 logic
Ok(())
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}
impl CounterInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// Get the instruction variant from the first byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let 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:

lib.rs
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Unpack instruction data
let instruction = CounterInstruction::unpack(instruction_data)?;
// Match instruction type
match 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:

  1. Membuat dan mengalokasikan ruang untuk akun baru untuk menyimpan data counter
  2. Menginisialisasi data akun dengan initial_value yang diberikan ke instruksi

lib.rs
// Initialize a new counter account
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)?;
// Size of our counter account
let account_space = 8; // Size in bytes to store a u64
// Calculate minimum balance for rent exemption
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
// Create the counter account
invoke(
&system_instruction::create_account(
payer_account.key, // Account paying for the new account
counter_account.key, // Account to be created
required_lamports, // Amount of lamports to transfer to the new account
account_space as u64, // Size in bytes to allocate for the data field
program_id, // Set program owner to our program
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
// Create a new CounterAccount struct with the initial value
let counter_data = CounterAccount {
count: initial_value,
};
// Get a mutable reference to the counter account's data
let mut account_data = &mut counter_account.data.borrow_mut()[..];
// Serialize the CounterAccount struct into the account's data
counter_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.

lib.rs
// Update an existing counter's value
fn 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 ownership
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// Mutable borrow the account data
let mut data = counter_account.data.borrow_mut();
// Deserialize the account data into our CounterAccount struct
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
// Increment the counter value
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
// Serialize the updated counter data back into the account
counter_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.

Terminal
cargo add solana-program-test@1.18.26 --dev
cargo add solana-sdk@1.18.26 --dev
cargo 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.

Terminal
cargo test-sbf -- --nocapture

lib.rs
#[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 account
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
// Step 1: Initialize the counter
println!("Testing counter initialization...");
// Create initialization instruction
let mut init_instruction_data = vec![0]; // 0 = initialize instruction
init_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 instruction
let 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 data
let 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 counter
println!("Testing counter increment...");
// Create increment instruction
let increment_instruction = Instruction::new_with_bytes(
program_id,
&[1], // 1 = increment instruction
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
// Send transaction with increment instruction
let 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 data
let 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:

Terminal
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 success
test test::test_counter_program ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s

Is this page helpful?

Daftar Isi

Edit Halaman