ابنِ برنامجك الأول على سولانا

يستخدم هذا الدليل السريع مشروع البداية الخاص بـ إطار عمل Anchor الذي يُنشئه anchor init. ستقوم بإنشاء المشروع محليًا، وتشغيل اختباراته، وبناء البرنامج، والتجوُّل عبر كود البرنامج الذي يُولِّده Anchor.

المتطلبات الأساسية

قبل أن تبدأ، ثبِّت أدوات تطوير سولانا. يشمل التثبيت Rust وواجهة سطر أوامر سولانا وواجهة سطر أوامر Anchor.

استخدم الإصدار 1.1.2 أو أعلى من واجهة سطر أوامر Anchor لهذا القالب. تحقق من الإصدار المثبت لديك:

Terminal
$
anchor --version

إنشاء المشروع

شغِّل الأوامر التالية في طرفيتك:

Terminal
$
anchor init my-program
$
cd my-program

يتضمن مشروع البداية برنامج سولانا واحدًا ضمن programs/my-program. يحتوي البرنامج على تعليمتين: إحداهما لتهيئة حساب عداد والأخرى لزيادة قيمة العداد.

تُوضح بعض أجزاء القالب أنماطًا شائعة في برامج سولانا: اشتقاق عناوين حسابات PDA، وإجراء Cross Program Invocation (CPI) لنقل SOL، واستخدام فحوصات أخطاء مخصصة لإيقاف التعليمة عند فشل أحد الشروط.

Anchor.toml
Cargo.toml
Cargo.toml
lib.rs
constants.rs
error.rs
instructions.rs
initialize.rs
increment.rs
state.rs
test_initialize.rs

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

شغِّل anchor build لتصريف برنامج البداية:

Terminal
$
anchor build

يُكتب البرنامج المُصرَّف إلى target/deploy/my_program.so. عند نشر البرنامج، يُخزَّن محتوى ملف .so هذا في حساب على السلسلة.

تشغيل الاختبار

شغّل الاختبار الافتراضي:

Terminal
$
anchor test

يستخدم Anchor.toml الخاص بهذا القالب أمر اختبار Rust:

Anchor.toml
skip_local_validator = true
[scripts]
test = "cargo test"

يقوم الاختبار بتحميل البرنامج المُجمَّع في LiteSVM، وإنشاء دافع، وإرسال تعليمتَي initialize وincrement، ثم التحقق من حالة حساب العداد.

يقوم تشغيل anchor test أيضاً بتجميع البرنامج، لذا لا تحتاج إلى تشغيل anchor build أولاً عند الاختبار محلياً.

نشر البرنامج

تُعدّ الاختبارات المحلية أسرع حلقة للتغذية الراجعة. عندما تكون مستعداً للنشر على شبكة، مثل devnet، قم أولاً بالبناء ثم انشر على الكلاستر.

يتطلب نشر برنامج سولانا SOL لأن البرنامج مخزّن في program account، ويجب على الحساب دفع تكلفة المساحة التي يستخدمها. على devnet، اطلب SOL مجاني من Solana Faucet أو باستخدام Solana CLI:

Terminal
$
solana airdrop 2 --url devnet
Terminal
$
anchor build
$
anchor deploy --provider.cluster devnet

الملفات المصدرية

يحتوي دليل src على برنامج سولانا. تشرح وثائق هيكل البرنامج الخاصة بـ Anchor الماكرو الأساسية المستخدمة هنا، بما فيها declare_id!، و#[program]، و#[derive(Accounts)]، و#[account]. يستعرض هذا القسم ملفات القالب.

lib.rs

lib.rs هو نقطة دخول البرنامج. يربط الملفات المصدرية، ويحدد عنوان البرنامج، ويعرّف تعليمات البرنامج التي يمكن للمستخدمين استدعاؤها.

programs/my-program/src/lib.rs
pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang::prelude::*;
pub use constants::*;
pub use instructions::*;
pub use state::*;
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");
#[program]
pub mod my_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
crate::instructions::initialize::handle_initialize(ctx)
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
crate::instructions::increment::handle_increment(ctx)
}
}
Anchor.toml
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
lib.rs
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");

تظهر نفس عنوان البرنامج في الإعدادات والكود. Anchor.toml يخبر Anchor بالعنوان الذي سيتم نشره أو استدعاؤه على الشبكة. declare_id! يُعرِّف عنوان البرنامج داخل البرنامج نفسه لأغراض التحقق الأمني.

constants.rs

يحتفظ constants.rs بالقيم المشتركة في مكان واحد. في هذا القالب، يشتق COUNTER_SEED عنوان PDA الخاص بالعداد، ويُنقل HELLO_WORLD_LAMPORTS أثناء التهيئة، ويُفحص MAX_COUNT قبل عملية الزيادة.

programs/my-program/src/constants.rs
use anchor_lang::prelude::*;
#[constant]
pub const COUNTER_SEED: &[u8] = b"counter";
#[constant]
pub const HELLO_WORLD_LAMPORTS: u64 = 1;
#[constant]
pub const MAX_COUNT: u64 = 10;
initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
// ...
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
// ...
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
Ok(())
}

يستخدم initialize.rs ثابتَين:

  • COUNTER_SEED يستخرج عنوان PDA الخاص بالعداد.
  • HELLO_WORLD_LAMPORTS يحدد المبلغ المحوَّل من الدافع إلى حساب العداد.
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
Ok(())
}

يستخدم increment.rs الثابتَ MAX_COUNT بوصفه الحدَّ الأقصى للعداد. إذا كان العدد الحالي قد بلغ الحد الأقصى بالفعل، فإن require! يُعيد CounterOverflow ولا يتغير بيانات الحساب.

state.rs

يُعرِّف state.rs أنواع بيانات مخصصة للحسابات التي يُنشئها البرنامج ويمتلكها. يُعرِّف البرنامج تعليمات لإنشاء تلك البيانات وتهيئتها وتحديثها، غير أن بيانات العداد لا تُخزَّن داخل البرنامج نفسه، بل تُخزَّن في حساب منفصل له عنوانه الخاص.

programs/my-program/src/state.rs
use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
pub authority: Pubkey,
}
initialize.rs
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
Ok(())
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
ctx.accounts.counter.count += 1;
Ok(())
}

state.rs يُعرِّف بيانات حساب Counter. تستخدم ملفات التعليمات هذا النوع عند إنشاء الحساب وتحديثه:

  • Counter::INIT_SPACE يُحدِّد حجم الحساب وفقاً للحقول المُعرَّفة في state.rs.
  • count و authority هي قيم الحقول المكتوبة عند تهيئة الحساب.
  • count += 1 يُحدِّث قيمة العداد المخزَّنة بعد اجتياز التحقق.

error.rs

error.rs يُعرِّف الأخطاء المخصَّصة للبرنامج. في هذا القالب، توضِّح الأخطاء كيف تتوقف معالجات التعليمات عندما لا يكون للمُستدعي صلاحية تحديث العداد، أو عندما يكون العداد قد بلغ MAX_COUNT بالفعل.

programs/my-program/src/error.rs
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Only the counter authority can update this counter")]
Unauthorized,
#[msg("Counter has reached the maximum value")]
CounterOverflow,
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

error.rs يُسمِّي الأخطاء التي يمكن أن تُرجعها التعليمة:

  • ErrorCode::Unauthorized يُرجَع عندما لا يكون الموقِّع هو الجهة المخوَّلة المخزَّنة في حساب العداد.
  • ErrorCode::CounterOverflow يُرجَع عندما يكون العداد قد بلغ MAX_COUNT بالفعل.

instructions.rs

instructions.rs يربط ملفات التعليمات بـ crate الخاص بالبرنامج حتى يتمكن lib.rs من الوصول إلى كود تعليمات initialize وincrement. يُعرِّف كل ملف تعليمات الحسابات المطلوبة لتلك التعليمة ومنطق المعالج الذي يعمل بعد أن يتحقق Anchor من صحة تلك الحسابات.

programs/my-program/src/instructions.rs
pub mod initialize;
pub mod increment;
pub use initialize::*;
pub use increment::*;

initialize.rs

initialize.rs يُعرِّف الحسابات المطلوبة لإنشاء حساب العداد، ثم يكتب القيم الأولية للحساب. تستخدم بنية #[derive(Accounts)] قيود الحساب الخاصة بـ Anchor لتحديد الحسابات المطلوبة وكيفية إنشاء حساب العداد الجديد.

programs/my-program/src/instructions/initialize.rs
use anchor_lang::prelude::*;
use crate::{constants::*, state::Counter};
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Account Context

تُعرِّف بنية Initialize الحسابات التي يجب تضمينها عندما يستدعي مستخدم تعليمة initialize. يتحقق Anchor من هذه الحسابات قبل تشغيل المعالج.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Payer Account

يدفع حساب payer تكلفة إنشاء حساب العداد. يعني النوع Signer<'info> أن الدافع يجب أن يوقّع المعاملة، و #[account(mut)] يعني أن حساب الدافع قابل للتعديل لأنه سيتم خصم lamports منه.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Counter Account

يخزّن حساب counter بيانات Counter من state.rs. init يخبر Anchor بإنشاء هذا الحساب قبل تشغيل المعالج، و payer = payer يخبر Anchor بالحساب الذي يدفع تكلفة الإنشاء.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

حجم الحساب

يخبر القيد space Anchor بمقدار بيانات الحساب المراد تخصيصها. يخزّن Anchor أولاً محدِّداً بحجم 8 بايت، ثم البايتات المطلوبة لحقول Counter. يتيح المحدِّد لـ Anchor التعرف على هذا الحساب باعتباره حساب Counter قبل إلغاء تسلسل بيانات الحساب.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

عنوان العداد

يحدد القيدان seeds و bump عنوان PDA المتوقع لحساب العداد. يتحقق Anchor من أن حساب counter المقدَّم يتطابق مع ذلك العنوان. يستخدم القالب PDA حتى يتمكن المستخدمون من اشتقاق عنوان العداد من معرّف البرنامج وseed، مما يجعل عنوان العداد حتمياً.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

System Program

حساب system_program مطلوب لأن إنشاء حساب جديد يستخدم System Program.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

دالة المعالج

تعمل الدالة handle_initialize بعد أن يتحقق Anchor من صحة الحسابات في Initialize. تمنح قيمة ctx المعالجَ إمكانية الوصول إلى تلك الحسابات المتحقق منها.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

البيانات الأولية

يكتب المعالج القيم الأولى في حساب العداد الجديد. يبدأ العداد عند 0، ويصبح الدافع هو الجهة المخوّلة بزيادة العداد لاحقاً.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

حسابات التحويل

يُضمَّن CPI الخاص بالتحويل هذا فقط لتوضيح كيفية تمرير CPI للحسابات إلى برنامج آخر. تسرد بنية Transfer الحسابات التي يستخدمها System Program في عملية التحويل.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

سياق CPI

CpiContext::new يجمع البرنامج المُستدعى مع الحسابات التي يتم تمريرها إلى ذلك البرنامج. هذا هو الشكل الأساسي لـ CPI: اختر البرنامج المراد استدعاؤه، اجمع الحسابات التي يتوقعها ذلك البرنامج، ثم مرّر كليهما إلى الاستدعاء. هنا، البرنامج المُستدعى هو System Program.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Invoke Transfer

anchor_lang::system_program::transfer يستدعي تعليمة التحويل الخاصة بـ System Program. في هذا القالب، يُعدّ التحويل مثالاً بسيطاً على استدعاء برنامج آخر من برنامجك. إذا فشل CPI الخاص بالتحويل، فإن تعليمة initialize تفشل أيضاً.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Log Message

msg! يكتب رسالةً في سجلات البرنامج. Ok(()) يشير إلى أن التعليمة أُنجزت بنجاح.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Account Context

تُعرِّف بنية Initialize الحسابات التي يجب تضمينها عندما يستدعي مستخدم تعليمة initialize. يتحقق Anchor من هذه الحسابات قبل تشغيل المعالج.

Payer Account

يدفع حساب payer تكلفة إنشاء حساب العداد. يعني النوع Signer<'info> أن الدافع يجب أن يوقّع المعاملة، و #[account(mut)] يعني أن حساب الدافع قابل للتعديل لأنه سيتم خصم lamports منه.

Counter Account

يخزّن حساب counter بيانات Counter من state.rs. init يخبر Anchor بإنشاء هذا الحساب قبل تشغيل المعالج، و payer = payer يخبر Anchor بالحساب الذي يدفع تكلفة الإنشاء.

حجم الحساب

يخبر القيد space Anchor بمقدار بيانات الحساب المراد تخصيصها. يخزّن Anchor أولاً محدِّداً بحجم 8 بايت، ثم البايتات المطلوبة لحقول Counter. يتيح المحدِّد لـ Anchor التعرف على هذا الحساب باعتباره حساب Counter قبل إلغاء تسلسل بيانات الحساب.

عنوان العداد

يحدد القيدان seeds و bump عنوان PDA المتوقع لحساب العداد. يتحقق Anchor من أن حساب counter المقدَّم يتطابق مع ذلك العنوان. يستخدم القالب PDA حتى يتمكن المستخدمون من اشتقاق عنوان العداد من معرّف البرنامج وseed، مما يجعل عنوان العداد حتمياً.

System Program

حساب system_program مطلوب لأن إنشاء حساب جديد يستخدم System Program.

دالة المعالج

تعمل الدالة handle_initialize بعد أن يتحقق Anchor من صحة الحسابات في Initialize. تمنح قيمة ctx المعالجَ إمكانية الوصول إلى تلك الحسابات المتحقق منها.

البيانات الأولية

يكتب المعالج القيم الأولى في حساب العداد الجديد. يبدأ العداد عند 0، ويصبح الدافع هو الجهة المخوّلة بزيادة العداد لاحقاً.

حسابات التحويل

يُضمَّن CPI الخاص بالتحويل هذا فقط لتوضيح كيفية تمرير CPI للحسابات إلى برنامج آخر. تسرد بنية Transfer الحسابات التي يستخدمها System Program في عملية التحويل.

سياق CPI

CpiContext::new يجمع البرنامج المُستدعى مع الحسابات التي يتم تمريرها إلى ذلك البرنامج. هذا هو الشكل الأساسي لـ CPI: اختر البرنامج المراد استدعاؤه، اجمع الحسابات التي يتوقعها ذلك البرنامج، ثم مرّر كليهما إلى الاستدعاء. هنا، البرنامج المُستدعى هو System Program.

Invoke Transfer

anchor_lang::system_program::transfer يستدعي تعليمة التحويل الخاصة بـ System Program. في هذا القالب، يُعدّ التحويل مثالاً بسيطاً على استدعاء برنامج آخر من برنامجك. إذا فشل CPI الخاص بالتحويل، فإن تعليمة initialize تفشل أيضاً.

Log Message

msg! يكتب رسالةً في سجلات البرنامج. Ok(()) يشير إلى أن التعليمة أُنجزت بنجاح.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

increment.rs

increment.rs يُعرّف الحسابات المطلوبة لتحديث حساب عداد موجود. يتحقق المعالج من أن الموقّع هو الجهة المخوّلة المخزّنة، ويتحقق من أن العدد لم يبلغ الحد MAX_COUNT المحدد، ثم يزيد العدد.

programs/my-program/src/instructions/increment.rs
use anchor_lang::prelude::*;
use crate::{constants::*, error::ErrorCode, state::Counter};
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Account Context

يُعرّف هيكل Increment الحسابات التي يجب تضمينها عندما يستدعي المستخدم تعليمة increment. يتحقق Anchor من هذه الحسابات قبل تشغيل المعالج.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Counter Account

يخزّن حساب counter بيانات Counter. يتيح قيد mut للمعالج تحديث العدد المخزّن، ويتحقق قيدا seeds وbump من عنوان PDA الخاص بالعداد.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Authority Signer

يجب أن يوقّع حساب authority على المعاملة. يتحقق المعالج لاحقاً من أن هذا الموقّع يطابق الجهة المخوّلة المخزّنة في حساب العداد.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

دالة المعالج

تعمل دالة handle_increment بعد أن يتحقق Anchor من الحسابات في Increment. تمنح قيمة ctx المعالجَ إمكانية الوصول إلى تلك الحسابات التي تم التحقق منها.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

التحقق من الصلاحية

يتحقق الفحص الأول من أن الموقِّع مخوَّل بتحديث هذا العداد. إذا كان عنوان الموقِّع لا يتطابق مع counter.authority، تتوقف التعليمة مع ErrorCode::Unauthorized. يوضح هذا التفويضَ على مستوى التطبيق: يمتلك البرنامج بيانات العداد، لكنه ينفذ قاعدة تحدد أي موقِّع يمكنه تغيير تلك البيانات.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

التحقق من الحد الأقصى للعداد

يمنع الفحص الثاني العداد من تجاوز MAX_COUNT. إذا كان العداد قد بلغ الحد بالفعل، تتوقف التعليمة مع ErrorCode::CounterOverflow. هذا الحد قاعدة اصطناعية في القالب تتيح لك رؤية كيف تُوقف الأخطاء المخصصة تنفيذ التعليمة قبل تغيير بيانات الحساب.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

تحديث العداد

فقط بعد اجتياز كلا الفحصين يقوم المعالج بتحديث بيانات الحساب. يضيف هذا السطر واحداً إلى قيمة العداد المخزَّنة.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

رسالة السجل

تكتب msg! العداد المحدَّث إلى سجلات البرنامج. تشير Ok(()) إلى أن التعليمة أُعيدت بنجاح.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Account Context

يُعرّف هيكل Increment الحسابات التي يجب تضمينها عندما يستدعي المستخدم تعليمة increment. يتحقق Anchor من هذه الحسابات قبل تشغيل المعالج.

Counter Account

يخزّن حساب counter بيانات Counter. يتيح قيد mut للمعالج تحديث العدد المخزّن، ويتحقق قيدا seeds وbump من عنوان PDA الخاص بالعداد.

Authority Signer

يجب أن يوقّع حساب authority على المعاملة. يتحقق المعالج لاحقاً من أن هذا الموقّع يطابق الجهة المخوّلة المخزّنة في حساب العداد.

دالة المعالج

تعمل دالة handle_increment بعد أن يتحقق Anchor من الحسابات في Increment. تمنح قيمة ctx المعالجَ إمكانية الوصول إلى تلك الحسابات التي تم التحقق منها.

التحقق من الصلاحية

يتحقق الفحص الأول من أن الموقِّع مخوَّل بتحديث هذا العداد. إذا كان عنوان الموقِّع لا يتطابق مع counter.authority، تتوقف التعليمة مع ErrorCode::Unauthorized. يوضح هذا التفويضَ على مستوى التطبيق: يمتلك البرنامج بيانات العداد، لكنه ينفذ قاعدة تحدد أي موقِّع يمكنه تغيير تلك البيانات.

التحقق من الحد الأقصى للعداد

يمنع الفحص الثاني العداد من تجاوز MAX_COUNT. إذا كان العداد قد بلغ الحد بالفعل، تتوقف التعليمة مع ErrorCode::CounterOverflow. هذا الحد قاعدة اصطناعية في القالب تتيح لك رؤية كيف تُوقف الأخطاء المخصصة تنفيذ التعليمة قبل تغيير بيانات الحساب.

تحديث العداد

فقط بعد اجتياز كلا الفحصين يقوم المعالج بتحديث بيانات الحساب. يضيف هذا السطر واحداً إلى قيمة العداد المخزَّنة.

رسالة السجل

تكتب msg! العداد المحدَّث إلى سجلات البرنامج. تشير Ok(()) إلى أن التعليمة أُعيدت بنجاح.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

ملف الاختبار

programs/my-program/tests/test_initialize.rs هو اختبار تكامل بلغة Rust. لا يُشغّل validator محلياً. بدلاً من ذلك، يُحمّل ملف .so المُجمَّع في LiteSVM، ويبني المعاملات التي تستدعي البرنامج، ويقرأ حساب العداد بعد كل معاملة. يبني الاختبار التعليمات لمعاملة سولانا عن طريق تحديد معرّف البرنامج المراد استدعاؤه، وتوفير instruction data، وتمرير الحسابات المطلوبة.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);

سياق حساب Initialize يحدد الحسابات المطلوبة من قِبَل تعليمة initialize. يمرر الاختبار تلك الحسابات نفسها إلى المساعد my_program::accounts::Initialize المُولَّد:

  • payer يُمرَّر بوصفه payer: payer.pubkey().
  • counter يُمرَّر بوصفه counter.
  • system_program يُمرَّر بوصفه system_program::ID.

my_program::instruction::Initialize {}.data() يُنشئ instruction data. هنا يتم ترميز وسيطات التعليمة، غير أن تعليمة initialize هذه لا تتطلب أي وسيطات.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);

سياق حساب Increment يحدد الحسابات المطلوبة من قِبَل تعليمة increment. يمرر الاختبار تلك الحسابات نفسها إلى المساعد my_program::accounts::Increment المُولَّد:

  • counter يُمرَّر بوصفه counter.
  • authority يُمرَّر بوصفه authority: payer.pubkey().

my_program::instruction::Increment {}.data() يُنشئ instruction data. هنا يتم ترميز وسيطات التعليمة، غير أن تعليمة increment هذه لا تتطلب أي وسيطات.

programs/my-program/tests/test_initialize.rs
use {
anchor_lang::{
prelude::Pubkey,
solana_program::{instruction::Instruction, system_program},
AccountDeserialize, InstructionData, ToAccountMetas,
},
litesvm::LiteSVM,
solana_keypair::Keypair,
solana_message::{Message, VersionedMessage},
solana_signer::Signer,
solana_transaction::versioned::VersionedTransaction,
};
#[test]
fn test_initialize() {
let program_id = my_program::id();
let payer = Keypair::new();
let counter = Pubkey::find_program_address(
&[my_program::constants::COUNTER_SEED],
&program_id,
)
.0;
let mut svm = LiteSVM::new();
let bytes = include_bytes!(concat!(
env!("CARGO_TARGET_TMPDIR"),
"/../deploy/my_program.so"
));
svm.add_program(program_id, bytes).unwrap();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer.pubkey());
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 1);
assert_eq!(counter_state.authority, payer.pubkey());
}

إعداد المشروع

تخبر ملفات المشروع الجذرية كلاً من Anchor وCargo بكيفية بناء البرنامج واختباره ونشره. للاطلاع على مرجع كامل، راجع توثيق Anchor الخاص بـ إعداد Anchor.toml وAnchor CLI.

Anchor.toml
skip_local_validator = true
[toolchain]
[features]
resolution = true
skip-lint = false
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "cargo test"
[hooks]

Is this page helpful?