بنية برنامج Rust

تتميز برامج سولانا المكتوبة بلغة Rust بمتطلبات هيكلية بسيطة، مما يتيح المرونة في تنظيم الشيفرة. المتطلب الوحيد هو أن البرنامج يجب أن يحتوي على entrypoint، الذي يحدد نقطة بداية تنفيذ البرنامج.

بنية البرنامج

على الرغم من عدم وجود قواعد صارمة لهيكل الملفات، فإن برامج سولانا عادةً تتبع نمطًا شائعًا:

  • entrypoint.rs: يحدد نقطة الدخول التي توجه التعليمات الواردة.
  • state.rs: يحدد حالة البرنامج (بيانات الحساب).
  • instructions.rs: يحدد التعليمات التي يمكن للبرنامج تنفيذها.
  • processor.rs: يحدد معالجات التعليمات (الوظائف) التي تنفذ المنطق التجاري لكل تعليمة.
  • error.rs: يحدد الأخطاء المخصصة التي يمكن للبرنامج إرجاعها.

على سبيل المثال، انظر إلى برنامج الرمز المميز.

مثال على البرنامج

لتوضيح كيفية بناء برنامج Rust أصلي مع تعليمات متعددة، سنشرح برنامج عداد بسيط ينفذ تعليمتين:

  1. InitializeCounter: ينشئ ويهيئ حسابًا جديدًا بقيمة أولية.
  2. IncrementCounter: يزيد القيمة المخزنة في حساب موجود.

للتبسيط، سيتم تنفيذ البرنامج في ملف lib.rs واحد، على الرغم من أنه في الممارسة العملية قد ترغب في تقسيم البرامج الأكبر إلى ملفات متعددة.

الجزء الأول: كتابة البرنامج

لنبدأ ببناء برنامج العداد. سننشئ برنامجًا يمكنه تهيئة عداد بقيمة بدء وزيادتها.

إنشاء برنامج جديد

أولاً، دعنا ننشئ مشروع Rust جديد لبرنامج سولانا الخاص بنا.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

يجب أن ترى ملفات src/lib.rs و Cargo.toml الافتراضية.

قم بتحديث حقل edition في Cargo.toml إلى 2021. وإلا، قد تواجه خطأ عند بناء البرنامج.

إضافة التبعيات

الآن دعنا نضيف التبعيات الضرورية لبناء برنامج سولانا. نحتاج إلى solana-program للحصول على SDK الأساسي و borsh للتسلسل.

Terminal
$
cargo add solana-program@2.2.0
$
cargo add borsh

لا يوجد شرط لاستخدام Borsh. ومع ذلك، فهي مكتبة تسلسل شائعة الاستخدام لبرامج سولانا.

تكوين crate-type

يجب تجميع برامج سولانا كمكتبات ديناميكية. أضف قسم [lib] لتكوين كيفية بناء Cargo للبرنامج.

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

إذا لم تقم بتضمين هذا التكوين، فلن يتم إنشاء دليل target/deploy عند بناء البرنامج.

إعداد نقطة دخول البرنامج

كل برنامج سولانا له نقطة دخول، وهي الوظيفة التي يتم استدعاؤها عند تشغيل البرنامج. دعنا نبدأ بإضافة الاستيرادات التي سنحتاجها للبرنامج وإعداد نقطة الدخول.

أضف الكود التالي إلى 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(())
}

يتعامل ماكرو entrypoint مع إلغاء تسلسل بيانات input إلى معلمات وظيفة process_instruction.

يحتوي entrypoint لبرنامج سولانا على توقيع الوظيفة التالي. المطورون أحرار في إنشاء تنفيذهم الخاص لوظيفة entrypoint.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

تحديد حالة البرنامج

الآن دعنا نحدد بنية البيانات التي سيتم تخزينها في حسابات العداد الخاصة بنا. هذه هي البيانات التي سيتم تخزينها في حقل data الخاص بالحساب.

أضف الكود التالي إلى lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u64,
}

تعريف تعداد التعليمات

دعنا نحدد التعليمات التي يمكن لبرنامجنا تنفيذها. سنستخدم تعداداً (enum) حيث يمثل كل متغير تعليمة مختلفة.

أضف الكود التالي إلى lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 },
IncrementCounter,
}

تنفيذ فك تشفير التعليمات

الآن نحتاج إلى فك تشفير instruction_data (البايتات الخام) إلى أحد متغيرات التعداد CounterInstruction. تتعامل طريقة Borsh try_from_slice مع هذا التحويل تلقائياً.

قم بتحديث وظيفة process_instruction لاستخدام فك تشفير 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(())
}

توجيه التعليمات إلى المعالجات

الآن دعنا نحدث وظيفة process_instruction الرئيسية لتوجيه التعليمات إلى وظائف المعالجة المناسبة لها.

هذا النمط من التوجيه شائع في برامج سولانا. يتم فك تشفير instruction_data إلى متغير من تعداد يمثل التعليمة، ثم يتم استدعاء وظيفة المعالجة المناسبة. تتضمن كل وظيفة معالجة التنفيذ الخاص بتلك التعليمة.

أضف الكود التالي إلى lib.rs لتحديث وظيفة process_instruction وإضافة معالجات لتعليمات InitializeCounter و 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(())
}

تنفيذ معالج التهيئة

دعنا ننفذ المعالج لإنشاء وتهيئة حساب عداد جديد. بما أن System Program فقط هو الذي يمكنه إنشاء حسابات على سولانا، سنستخدم استدعاء عبر البرامج (Cross Program Invocation - CPI)، وهو في الأساس استدعاء برنامج آخر من برنامجنا.

يقوم برنامجنا بإجراء CPI لاستدعاء تعليمة create_account الخاصة بـ System Program. يتم إنشاء الحساب الجديد مع برنامجنا كمالك، مما يمنح برنامجنا القدرة على الكتابة في الحساب وتهيئة البيانات.

أضف الكود التالي إلى lib.rs لتحديث الدالة 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(())
}

هذه التعليمات لأغراض التوضيح فقط. لا تتضمن فحوصات الأمان والتحقق المطلوبة للبرامج الإنتاجية.

تنفيذ معالج الزيادة

الآن دعنا ننفذ المعالج الذي يزيد العداد الموجود. هذه التعليمات:

  • تقرأ حقل data للحساب counter_account
  • تحول البيانات إلى بنية CounterAccount
  • تزيد حقل count بمقدار 1
  • تعيد تحويل بنية CounterAccount مرة أخرى إلى حقل data للحساب

أضف الكود التالي إلى lib.rs لتحديث الدالة 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(())
}

هذه التعليمات لأغراض التوضيح فقط. لا تتضمن فحوصات الأمان والتحقق المطلوبة للبرامج الإنتاجية.

البرنامج المكتمل

تهانينا! لقد قمت ببناء برنامج سولانا كامل يوضح الهيكل الأساسي المشترك بين جميع برامج سولانا:

  • نقطة الدخول: تحدد أين يبدأ تنفيذ البرنامج وتوجه جميع الطلبات الواردة إلى معالجات التعليمات المناسبة
  • معالجة التعليمات: تحدد التعليمات ودوال المعالجة المرتبطة بها
  • إدارة الحالة: تحدد هياكل بيانات الحساب وتدير حالتها في الحسابات المملوكة للبرنامج
  • استدعاء البرامج المتقاطع (Cross Program Invocation): يستدعي System Program لإنشاء حسابات جديدة مملوكة للبرنامج

الخطوة التالية هي اختبار البرنامج للتأكد من أن كل شيء يعمل بشكل صحيح.

إنشاء برنامج جديد

أولاً، دعنا ننشئ مشروع Rust جديد لبرنامج سولانا الخاص بنا.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

يجب أن ترى ملفات src/lib.rs و Cargo.toml الافتراضية.

قم بتحديث حقل edition في Cargo.toml إلى 2021. وإلا، قد تواجه خطأ عند بناء البرنامج.

إضافة التبعيات

الآن دعنا نضيف التبعيات الضرورية لبناء برنامج سولانا. نحتاج إلى solana-program للحصول على SDK الأساسي و borsh للتسلسل.

Terminal
$
cargo add solana-program@2.2.0
$
cargo add borsh

لا يوجد شرط لاستخدام Borsh. ومع ذلك، فهي مكتبة تسلسل شائعة الاستخدام لبرامج سولانا.

تكوين crate-type

يجب تجميع برامج سولانا كمكتبات ديناميكية. أضف قسم [lib] لتكوين كيفية بناء Cargo للبرنامج.

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

إذا لم تقم بتضمين هذا التكوين، فلن يتم إنشاء دليل target/deploy عند بناء البرنامج.

إعداد نقطة دخول البرنامج

كل برنامج سولانا له نقطة دخول، وهي الوظيفة التي يتم استدعاؤها عند تشغيل البرنامج. دعنا نبدأ بإضافة الاستيرادات التي سنحتاجها للبرنامج وإعداد نقطة الدخول.

أضف الكود التالي إلى 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(())
}

يتعامل ماكرو entrypoint مع إلغاء تسلسل بيانات input إلى معلمات وظيفة process_instruction.

يحتوي entrypoint لبرنامج سولانا على توقيع الوظيفة التالي. المطورون أحرار في إنشاء تنفيذهم الخاص لوظيفة entrypoint.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

تحديد حالة البرنامج

الآن دعنا نحدد بنية البيانات التي سيتم تخزينها في حسابات العداد الخاصة بنا. هذه هي البيانات التي سيتم تخزينها في حقل data الخاص بالحساب.

أضف الكود التالي إلى lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u64,
}

تعريف تعداد التعليمات

دعنا نحدد التعليمات التي يمكن لبرنامجنا تنفيذها. سنستخدم تعداداً (enum) حيث يمثل كل متغير تعليمة مختلفة.

أضف الكود التالي إلى lib.rs:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 },
IncrementCounter,
}

تنفيذ فك تشفير التعليمات

الآن نحتاج إلى فك تشفير instruction_data (البايتات الخام) إلى أحد متغيرات التعداد CounterInstruction. تتعامل طريقة Borsh try_from_slice مع هذا التحويل تلقائياً.

قم بتحديث وظيفة process_instruction لاستخدام فك تشفير 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(())
}

توجيه التعليمات إلى المعالجات

الآن دعنا نحدث وظيفة process_instruction الرئيسية لتوجيه التعليمات إلى وظائف المعالجة المناسبة لها.

هذا النمط من التوجيه شائع في برامج سولانا. يتم فك تشفير instruction_data إلى متغير من تعداد يمثل التعليمة، ثم يتم استدعاء وظيفة المعالجة المناسبة. تتضمن كل وظيفة معالجة التنفيذ الخاص بتلك التعليمة.

أضف الكود التالي إلى lib.rs لتحديث وظيفة process_instruction وإضافة معالجات لتعليمات InitializeCounter و 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(())
}

تنفيذ معالج التهيئة

دعنا ننفذ المعالج لإنشاء وتهيئة حساب عداد جديد. بما أن System Program فقط هو الذي يمكنه إنشاء حسابات على سولانا، سنستخدم استدعاء عبر البرامج (Cross Program Invocation - CPI)، وهو في الأساس استدعاء برنامج آخر من برنامجنا.

يقوم برنامجنا بإجراء CPI لاستدعاء تعليمة create_account الخاصة بـ System Program. يتم إنشاء الحساب الجديد مع برنامجنا كمالك، مما يمنح برنامجنا القدرة على الكتابة في الحساب وتهيئة البيانات.

أضف الكود التالي إلى lib.rs لتحديث الدالة 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(())
}

هذه التعليمات لأغراض التوضيح فقط. لا تتضمن فحوصات الأمان والتحقق المطلوبة للبرامج الإنتاجية.

تنفيذ معالج الزيادة

الآن دعنا ننفذ المعالج الذي يزيد العداد الموجود. هذه التعليمات:

  • تقرأ حقل data للحساب counter_account
  • تحول البيانات إلى بنية CounterAccount
  • تزيد حقل count بمقدار 1
  • تعيد تحويل بنية CounterAccount مرة أخرى إلى حقل data للحساب

أضف الكود التالي إلى lib.rs لتحديث الدالة 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(())
}

هذه التعليمات لأغراض التوضيح فقط. لا تتضمن فحوصات الأمان والتحقق المطلوبة للبرامج الإنتاجية.

البرنامج المكتمل

تهانينا! لقد قمت ببناء برنامج سولانا كامل يوضح الهيكل الأساسي المشترك بين جميع برامج سولانا:

  • نقطة الدخول: تحدد أين يبدأ تنفيذ البرنامج وتوجه جميع الطلبات الواردة إلى معالجات التعليمات المناسبة
  • معالجة التعليمات: تحدد التعليمات ودوال المعالجة المرتبطة بها
  • إدارة الحالة: تحدد هياكل بيانات الحساب وتدير حالتها في الحسابات المملوكة للبرنامج
  • استدعاء البرامج المتقاطع (Cross Program Invocation): يستدعي System Program لإنشاء حسابات جديدة مملوكة للبرنامج

الخطوة التالية هي اختبار البرنامج للتأكد من أن كل شيء يعمل بشكل صحيح.

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

الجزء 2: اختبار البرنامج

الآن دعنا نختبر برنامج العداد الخاص بنا. سنستخدم LiteSVM، وهو إطار اختبار يتيح لنا اختبار البرامج دون نشرها على مجموعة.

إضافة تبعيات الاختبار

أولاً، دعنا نضيف التبعيات اللازمة للاختبار. سنستخدم litesvm للاختبار و solana-sdk.

Terminal
$
cargo add litesvm@0.6.1 --dev
$
cargo add solana-sdk@2.2.0 --dev

إنشاء وحدة اختبار

الآن دعنا نضيف وحدة اختبار إلى برنامجنا. سنبدأ بالهيكل الأساسي والاستيرادات.

أضف الكود التالي إلى lib.rs، مباشرة أسفل كود البرنامج:

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

السمة #[cfg(test)] تضمن أن هذا الكود يتم تجميعه فقط عند تشغيل الاختبارات.

تهيئة بيئة الاختبار

دعنا نقوم بإعداد بيئة الاختبار باستخدام LiteSVM وتمويل حساب الدافع.

يحاكي LiteSVM بيئة تشغيل سولانا، مما يسمح لنا باختبار برنامجنا دون النشر على مجموعة حقيقية.

أضف الكود التالي إلى lib.rs مع تحديث الدالة 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");

تحميل البرنامج

الآن نحتاج إلى بناء وتحميل برنامجنا في بيئة الاختبار. قم بتشغيل الأمر cargo build-sbf لبناء البرنامج. سيؤدي هذا إلى إنشاء ملف counter_program.so في دليل target/deploy.

Terminal
$
cargo build-sbf

تأكد من أن edition في Cargo.toml مضبوط على 2021.

بعد البناء، يمكننا تحميل البرنامج.

قم بتحديث الدالة test_counter_program لتحميل البرنامج في بيئة الاختبار.

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

يجب عليك تشغيل cargo build-sbf قبل تشغيل الاختبارات لإنشاء ملف .so. يقوم الاختبار بتحميل البرنامج المُجمّع.

اختبار تعليمة التهيئة

دعنا نختبر تعليمة التهيئة من خلال إنشاء حساب عداد جديد بقيمة ابتدائية.

أضف الكود التالي إلى lib.rs لتحديث الدالة 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);

التحقق من التهيئة

بعد التهيئة، دعنا نتحقق من إنشاء حساب العداد بشكل صحيح بالقيمة المتوقعة.

أضف الكود التالي إلى lib.rs لتحديث الدالة 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);

اختبار تعليمة الزيادة

الآن دعنا نختبر تعليمة الزيادة للتأكد من أنها تحدث قيمة العداد بشكل صحيح.

أضف الكود التالي إلى lib.rs لتحديث الدالة 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);

التحقق من النتائج النهائية

أخيرًا، دعنا نتحقق من أن الزيادة عملت بشكل صحيح من خلال فحص قيمة العداد المحدثة.

أضف الكود التالي إلى lib.rs لتحديث الدالة 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);

قم بتشغيل الاختبارات باستخدام الأمر التالي. العلامة --nocapture تطبع مخرجات الاختبار.

Terminal
$
cargo test -- --nocapture

المخرجات المتوقعة:

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

إضافة تبعيات الاختبار

أولاً، دعنا نضيف التبعيات اللازمة للاختبار. سنستخدم litesvm للاختبار و solana-sdk.

Terminal
$
cargo add litesvm@0.6.1 --dev
$
cargo add solana-sdk@2.2.0 --dev

إنشاء وحدة اختبار

الآن دعنا نضيف وحدة اختبار إلى برنامجنا. سنبدأ بالهيكل الأساسي والاستيرادات.

أضف الكود التالي إلى lib.rs، مباشرة أسفل كود البرنامج:

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

السمة #[cfg(test)] تضمن أن هذا الكود يتم تجميعه فقط عند تشغيل الاختبارات.

تهيئة بيئة الاختبار

دعنا نقوم بإعداد بيئة الاختبار باستخدام LiteSVM وتمويل حساب الدافع.

يحاكي LiteSVM بيئة تشغيل سولانا، مما يسمح لنا باختبار برنامجنا دون النشر على مجموعة حقيقية.

أضف الكود التالي إلى lib.rs مع تحديث الدالة 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");

تحميل البرنامج

الآن نحتاج إلى بناء وتحميل برنامجنا في بيئة الاختبار. قم بتشغيل الأمر cargo build-sbf لبناء البرنامج. سيؤدي هذا إلى إنشاء ملف counter_program.so في دليل target/deploy.

Terminal
$
cargo build-sbf

تأكد من أن edition في Cargo.toml مضبوط على 2021.

بعد البناء، يمكننا تحميل البرنامج.

قم بتحديث الدالة test_counter_program لتحميل البرنامج في بيئة الاختبار.

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

يجب عليك تشغيل cargo build-sbf قبل تشغيل الاختبارات لإنشاء ملف .so. يقوم الاختبار بتحميل البرنامج المُجمّع.

اختبار تعليمة التهيئة

دعنا نختبر تعليمة التهيئة من خلال إنشاء حساب عداد جديد بقيمة ابتدائية.

أضف الكود التالي إلى lib.rs لتحديث الدالة 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);

التحقق من التهيئة

بعد التهيئة، دعنا نتحقق من إنشاء حساب العداد بشكل صحيح بالقيمة المتوقعة.

أضف الكود التالي إلى lib.rs لتحديث الدالة 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);

اختبار تعليمة الزيادة

الآن دعنا نختبر تعليمة الزيادة للتأكد من أنها تحدث قيمة العداد بشكل صحيح.

أضف الكود التالي إلى lib.rs لتحديث الدالة 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);

التحقق من النتائج النهائية

أخيرًا، دعنا نتحقق من أن الزيادة عملت بشكل صحيح من خلال فحص قيمة العداد المحدثة.

أضف الكود التالي إلى lib.rs لتحديث الدالة 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);

قم بتشغيل الاختبارات باستخدام الأمر التالي. العلامة --nocapture تطبع مخرجات الاختبار.

Terminal
$
cargo test -- --nocapture

المخرجات المتوقعة:

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"

الجزء 3: استدعاء البرنامج

الآن دعنا نضيف نصًا برمجيًا للعميل لاستدعاء البرنامج.

إنشاء مثال للعميل

دعنا ننشئ عميل Rust للتفاعل مع برنامجنا المنشور.

Terminal
$
mkdir examples
$
touch examples/client.rs

أضف التكوين التالي إلى Cargo.toml:

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

قم بتثبيت تبعيات العميل:

Terminal
$
cargo add solana-client@2.2.0 --dev
$
cargo add tokio --dev

تنفيذ كود العميل

الآن دعنا ننفذ العميل الذي سيستدعي برنامجنا المنشور.

قم بتشغيل الأمر التالي للحصول على معرف البرنامج الخاص بك من ملف keypair:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

أضف كود العميل إلى examples/client.rs واستبدل program_id بمخرجات الأمر السابق:

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

إنشاء مثال للعميل

دعنا ننشئ عميل Rust للتفاعل مع برنامجنا المنشور.

Terminal
$
mkdir examples
$
touch examples/client.rs

أضف التكوين التالي إلى Cargo.toml:

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

قم بتثبيت تبعيات العميل:

Terminal
$
cargo add solana-client@2.2.0 --dev
$
cargo add tokio --dev

تنفيذ كود العميل

الآن دعنا ننفذ العميل الذي سيستدعي برنامجنا المنشور.

قم بتشغيل الأمر التالي للحصول على معرف البرنامج الخاص بك من ملف keypair:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

أضف كود العميل إلى examples/client.rs واستبدل program_id بمخرجات الأمر السابق:

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"

الجزء 4: نشر البرنامج

الآن بعد أن أصبح برنامجنا والعميل جاهزين، دعنا نقوم ببناء ونشر واستدعاء البرنامج.

بناء البرنامج

أولاً، دعنا نبني برنامجنا.

Terminal
$
cargo build-sbf

يقوم هذا الأمر بتجميع برنامجك وإنشاء ملفين مهمين في target/deploy/:

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

يمكنك عرض معرّف برنامجك عن طريق تشغيل الأمر التالي:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

مثال للمخرجات:

HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

بدء validator محلي

لأغراض التطوير، سنستخدم validator اختبار محلي.

أولاً، قم بتكوين واجهة سطر أوامر سولانا لاستخدام localhost:

Terminal
$
solana config set -ul

مثال للمخرجات:

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

الآن قم بتشغيل validator الاختبار في نافذة طرفية منفصلة:

Terminal
$
solana-test-validator

نشر البرنامج

مع تشغيل validator، قم بنشر برنامجك على المجموعة المحلية:

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

مثال للمخرجات:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

يمكنك التحقق من النشر باستخدام أمر solana program show مع معرّف برنامجك:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

مثال على النتائج:

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

تشغيل العميل

مع استمرار تشغيل validator المحلي، قم بتنفيذ العميل:

Terminal
$
cargo run --example client

النتائج المتوقعة:

Requesting airdrop...
Airdrop confirmed
Initializing counter...
Counter initialized!
Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14k
Counter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9Zfofcy
Incrementing counter...
Counter incremented!
Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS

مع تشغيل validator المحلي، يمكنك عرض المعاملات على مستكشف سولانا باستخدام توقيعات المعاملات الناتجة. لاحظ أنه يجب تعيين المجموعة في مستكشف سولانا إلى "عنوان URL مخصص لـ RPC"، والذي يكون افتراضيًا http://localhost:8899 الذي يعمل عليه solana-test-validator.

Is this page helpful?