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

  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.

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.

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

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

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

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:

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:

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:

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

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:

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

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:

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

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 untuk counter_account
  • Mendeserialkan menjadi struct CounterAccount
  • Menambah bidang count sebanyak 1
  • Menserialkan struct CounterAccount kembali ke bidang data akun

Tambahkan kode berikut ke lib.rs untuk memperbarui fungsi 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(())
}

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.

Membuat program baru

Pertama, mari buat proyek Rust baru untuk program Solana kita.

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

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

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

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:

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:

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:

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

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:

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

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:

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

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 untuk counter_account
  • Mendeserialkan menjadi struct CounterAccount
  • Menambah bidang count sebanyak 1
  • Menserialkan struct CounterAccount kembali ke bidang data akun

Tambahkan kode berikut ke lib.rs untuk memperbarui fungsi 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(())
}

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.

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

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.

Terminal
$
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:

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

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:

lib.rs
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.

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

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

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:

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

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:

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

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:

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

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:

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

Jalankan pengujian dengan perintah berikut. Flag --nocapture akan mencetak output dari pengujian.

Terminal
$
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: 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

Tambahkan dependensi pengujian

Pertama, mari tambahkan dependensi yang diperlukan untuk pengujian. Kita akan menggunakan litesvm untuk pengujian dan solana-sdk.

Terminal
$
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:

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

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:

lib.rs
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.

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

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

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:

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

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:

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

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:

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

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:

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

Jalankan pengujian dengan perintah berikut. Flag --nocapture akan mencetak output dari pengujian.

Terminal
$
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: 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"

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.

Terminal
$
mkdir examples
$
touch examples/client.rs

Tambahkan konfigurasi berikut ke Cargo.toml:

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

Instal dependensi klien:

Terminal
$
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:

Terminal
$
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:

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

Membuat contoh klien

Mari kita buat klien Rust untuk berinteraksi dengan program yang telah kita deploy.

Terminal
$
mkdir examples
$
touch examples/client.rs

Tambahkan konfigurasi berikut ke Cargo.toml:

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

Instal dependensi klien:

Terminal
$
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:

Terminal
$
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:

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"

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.

Terminal
$
cargo build-sbf

Perintah ini mengompilasi program Anda dan menghasilkan dua file penting di target/deploy/:

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

Anda dapat melihat ID program Anda dengan menjalankan perintah berikut:

Terminal
$
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:

Terminal
$
solana config set -ul

Contoh output:

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

Sekarang mulai validator uji di terminal terpisah:

Terminal
$
solana-test-validator

Deploy program

Dengan validator yang berjalan, deploy program Anda ke cluster lokal:

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

Contoh output:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

Anda dapat memverifikasi deployment menggunakan perintah solana program show dengan ID program Anda:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Contoh output:

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

Jalankan klien

Dengan validator lokal yang masih berjalan, jalankan klien:

Terminal
$
cargo run --example client

Output yang diharapkan:

Requesting airdrop...
Airdrop confirmed
Initializing counter...
Counter initialized!
Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14k
Counter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9Zfofcy
Incrementing 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?

Daftar Isi

Edit Halaman