بنية برنامج Rust

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

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

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

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

على سبيل المثال، راجع Token Program.

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

لتوضيح كيفية بناء برنامج 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. ومع ذلك، فهي مكتبة تسلسل شائعة الاستخدام لبرامج سولانا.

تكوين نوع الحزمة

يجب تجميع برامج سولانا كمكتبات ديناميكية. أضف قسم [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,
}

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

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

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

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

تنفيذ إلغاء تسلسل التعليمات

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

حدّث دالة 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 فقط هو من يمكنه إنشاء حسابات على سولانا، سنستخدم استدعاء برنامج متقاطع (CPI)، أي استدعاء برنامج آخر من برنامجنا.

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

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

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

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

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

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

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

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

أولاً، لنقم بإنشاء مشروع 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. ومع ذلك، فهي مكتبة تسلسل شائعة الاستخدام لبرامج سولانا.

تكوين نوع الحزمة

يجب تجميع برامج سولانا كمكتبات ديناميكية. أضف قسم [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,
}

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

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

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

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

تنفيذ إلغاء تسلسل التعليمات

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

حدّث دالة 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 فقط هو من يمكنه إنشاء حسابات على سولانا، سنستخدم استدعاء برنامج متقاطع (CPI)، أي استدعاء برنامج آخر من برنامجنا.

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

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

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

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

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

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

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

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

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

الآن لنختبر برنامج العداد الخاص بنا. سنستخدم 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

تشغيل المدقق المحلي

للتطوير، سنستخدم مدقق اختبار محلي.

أولاً، قم بتكوين واجهة سطر أوامر سولانا لاستخدام 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

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

Terminal
$
solana-test-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

تشغيل العميل

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

Terminal
$
cargo run --example client

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

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

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

Is this page helpful?

تدار بواسطة

© 2026 مؤسسة سولانا.
جميع الحقوق محفوظة.
تواصل معنا