تقوم البرامج على السلسلة بنقل الرموز عبر إصدار
Cross Program Invocation (CPI) إلى Token Program. يبني برنامجك
تعليمة Token Program، ويوفّر الحسابات التي تحتاجها، ثم يستدعي invoke (أو
invoke_signed عندما يوقّع Program Derived Address). يقوم Token Program بعد
ذلك بتنفيذ تلك التعليمة بالصلاحيات التي يمنحها إياه برنامجك.
تتناول هذه الصفحة الجانب الخاص بالرموز في هذه العملية. للاطلاع على آلية CPI
نفسها — كيفية عمل invoke وinvoke_signed، وكيفية انتشار صلاحيات
التوقيع والكتابة، ونموذج تكلفة CPI — راجع
Cross Program Invocation.
الأنماط الشائعة
تتبع معظم عمليات CPI للرموز الشكل ذاته: بناء التعليمة، وتمرير token accounts
والصلاحية، ثم الاستدعاء. يوضح المثال أدناه نقل الرموز باستخدام
TransferChecked، الذي يتحقق من المسكوكة وعدد الخانات العشرية كجزء من عملية
النقل.
use anchor_lang::prelude::*;use anchor_spl::token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked};declare_id!("11111111111111111111111111111111");#[program]pub mod token_cpi {use super::*;pub fn transfer(ctx: Context<TokenTransfer>, amount: u64) -> Result<()> {let cpi_accounts = TransferChecked {from: ctx.accounts.source.to_account_info(),mint: ctx.accounts.mint.to_account_info(),to: ctx.accounts.destination.to_account_info(),authority: ctx.accounts.authority.to_account_info(),};let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(),cpi_accounts,);transfer_checked(cpi_ctx, amount, ctx.accounts.mint.decimals)?;Ok(())}}#[derive(Accounts)]pub struct TokenTransfer<'info> {#[account(mut)]pub source: InterfaceAccount<'info, TokenAccount>,pub mint: InterfaceAccount<'info, Mint>,#[account(mut)]pub destination: InterfaceAccount<'info, TokenAccount>,pub authority: Signer<'info>,pub token_program: Interface<'info, TokenInterface>,}
تتبع عمليات السك والحرق وإغلاق الحسابات النمط ذاته مع تعليمة مختلفة —
MintTo، وBurn، وCloseAccount في وحدة
anchor_spl::token
الخاصة بـ Anchor، أو المنشئات المقابلة في
pinocchio-token. للاطلاع على برامج
كاملة وقابلة للتشغيل تستدعي Token Program عبر CPI في كل من Anchor وRust الأصلي،
راجع أمثلة token program — ولا سيما مثال
"Transfer Tokens".
التوقيع بصلاحية Program Derived Address
عندما تكون الصلاحية على token account هي
Program Derived Address مملوكة لبرنامجك، يوقّع البرنامج على
CPI بنفسه باستخدام invoke_signed، مع تمرير بذور Program Derived Address
كبذور موقّعة. في Anchor، استخدم CpiContext::new_with_signer مع البذور
بدلاً من CpiContext::new. التعليمة متطابقة في بقية تفاصيلها مع الأمثلة
أعلاه. راجع CPIs with PDA Signers للاطلاع على
الآلية الكاملة.
التجميع الدفعي
تُنفِّذ تعليمة Batch عدة تعليمات من Token Program داخل استدعاء برنامج
واحد. ونظرًا لأن كل CPI يحمل تكلفة حوسبة ثابتة، فإن تشغيل عدة عمليات على الرموز
في دفعة CPI واحدة يستهلك وحدات حوسبة أقل
مقارنةً بإصدار CPI منفصل لكل عملية — راجع
نموذج تكلفة CPI لمعرفة كيف تتراكم تكاليف كل
CPI.
يُقلِّل انخفاض استهلاك الحوسبة من رسوم الأولوية التي تدفعها المعاملة لكل وحدة حوسبة، ويُحسِّن فرص إتمامها — وهذا أكثر أهمية للبرامج التي تُجري عمليات كثيرة على الرموز في تعليمة واحدة. تشمل الاستخدامات الشائعة توزيع تحويل على مستلمين متعددين، أو تنفيذ تدفق متعدد الخطوات للرموز (مثل المزامنة والتحويل والإغلاق) في CPI واحد.
لا تقبل Batch إلا تعليمات Token Program كعناصر فرعية، ولا يمكن أن تحتوي
الدفعة على دفعة أخرى. وبالنسبة للعمليات التي تُنقِل الرموز (كالتحويلات والإصدار
والحرق)، يتحقق البرنامج من أن الحسابات المتأثرة مملوكة لـ Token Program قبل
تنفيذها.
مرجع المصدر
استدعاء الدُّفعة عبر CPI
تُوفِّر حزمة pinocchio-token
بانيًا لـ Batch ضمن pinocchio_token::instructions::Batch. يمكنك
إضافة كل تعليمة فرعية إلى مخازن الدُّفعة ثم إصدار CPI واحد باستخدام
invoke.
تعتمد Batch على ثلاثة مخازن يوفِّرها المُستدعي: أحدها للبيانات المُتسلسلة
لـ instruction data، وآخر لبيانات وصف الحسابات الخاصة بكل تعليمة فرعية، وثالث
لعروض الحسابات المُمرَّرة إلى CPI. أنشئها كشرائح MaybeUninit، ومرِّرها إلى
Batch::new، ثم أضف العناصر الفرعية وادعُ:
use core::mem::MaybeUninit;use pinocchio::{account::AccountView, error::ProgramError, ProgramResult};use pinocchio_token::instructions::{Batch, IntoBatch, Transfer};/// Process SwapBatch.////// Performs two transfers in a single batch CPI instead of two separate/// `invoke()` calls.////// Data layout:/// [0..8] amount_a_to_b (u64 LE)/// [8..16] amount_b_to_a (u64 LE)////// Account layout:/// [0] source_a (writable) — token account A/// [1] source_b (writable) — token account B/// [2] authority_a (signer) — authority for account A/// [3] authority_b (signer) — authority for account B/// [4] token_program — SPL Token programpub fn process(accounts: &[AccountView], data: &[u8]) -> ProgramResult {if data.len() < 16 {return Err(ProgramError::InvalidInstructionData);}if accounts.len() < 5 {return Err(ProgramError::NotEnoughAccountKeys);}let amount_a_to_b = u64::from_le_bytes(data[0..8].try_into().unwrap());let amount_b_to_a = u64::from_le_bytes(data[8..16].try_into().unwrap());let source_a = &accounts[0];let source_b = &accounts[1];let authority_a = &accounts[2];let authority_b = &accounts[3];// Two child transfers, each with 3 accounts and 9 bytes of data// (1-byte discriminator + 8-byte amount).const NUM_CHILDREN: usize = 2;const CHILD_ACCOUNTS: usize = 3;const CHILD_DATA_LEN: usize = 9;// Data buffer: 1-byte batch discriminator + per child (2-byte header + data).const DATA_LEN: usize = 1 + NUM_CHILDREN * (2 + CHILD_DATA_LEN);// Account buffers: total accounts across all children.const ACCOUNTS_LEN: usize = NUM_CHILDREN * CHILD_ACCOUNTS;let mut data_buf = [MaybeUninit::uninit(); DATA_LEN];let mut ix_accounts_buf = [MaybeUninit::uninit(); ACCOUNTS_LEN];let mut accounts_buf = [MaybeUninit::uninit(); ACCOUNTS_LEN];let mut batch = Batch::new(&mut data_buf, &mut ix_accounts_buf, &mut accounts_buf)?;Transfer::new(source_a, source_b, authority_a, amount_a_to_b).into_batch(&mut batch)?;Transfer::new(source_b, source_a, authority_b, amount_b_to_a).into_batch(&mut batch)?;batch.invoke()?;Ok(())}
يكتب Batch::new مُميِّز الدُّفعة في البايت الأول من مخزن البيانات المؤقت
ويُعيد Batch الذي يتتبع مقدار ما استُخدم من كل مخزن مؤقت. كل منشئ تعليمات
Token Program (مثل Transfer وTransferChecked وMintTo
وBurn) يُنفِّذ سمة IntoBatch، لذا يُلحق into_batch بيانات تلك
التعليمة ومعلومات الحسابات وعروضها إلى الدُّفعة. يُصدر استدعاء invoke
الدُّفعةَ المُجمَّعة كـ CPI واحد إلى Token Program.
تُحدِّد المخازن المؤقتة سعة الدُّفعة. حدِّد حجم مخزن البيانات المؤقت بـ
1 + Σ(2 + child_data_len) بايت (المُميِّز، بالإضافة إلى ترويسة مكوَّنة من
بايتين وبيانات كل عنصر فرعي مُتسلسَل)، وحدِّد حجم كل مخزن مؤقت للحسابات بإجمالي
عدد الحسابات عبر جميع العناصر الفرعية. إذا لم يتسع عنصر فرعي، يُعيد
into_batch القيمة ProgramError::InvalidArgument. للمرجعية،
Batch::MAX_DATA_LEN يساوي 10 كيبيبايت وBatch::MAX_ACCOUNTS_LEN يساوي
الحد الأقصى لعدد حسابات CPI في وقت التشغيل.
للدُّفعات التي لا يُعرف حجمها إلا في وقت التشغيل، فعِّل ميزة alloc في الحزمة
واستخدم BatchState::new(accounts_len, data_len) لتخصيص المخازن المؤقتة على
الكومة، ثم استدعِ as_batch للحصول على Batch.
عندما تكون سلطة تعليمة فرعية عبارة عن PDA مملوكة لبرنامجك، وقِّع على CPI
باستخدام invoke_signed بدلاً من invoke، مع تمرير بذور PDA كإدخالات
Signer.
تخطيط instruction data
تُرمِّز الدُّفعة عناصرها الفرعية واحداً تلو الآخر خلف مُميِّز 255. تُمرَّر
جميع الحسابات بشكل مسطَّح وبالترتيب، ويتم تقطيعها بواسطة num_accounts لكل عنصر
فرعي. يُنتج المنشئ هذا التنسيق الشبكي تماماً:
[255] // Batch discriminator// For each child instruction, in order:// [num_accounts: u8] // accounts this instruction consumes// [data_len: u8] // length of this instruction's data// [data: u8; data_len]// the instruction data (begins with its own discriminator)
تتحدَّد سعة الدُّفعة بالمخازن المؤقتة المُمرَّرة إلى Batch::new: يجب أن
يحتوي مخزن البيانات المؤقت على المُميِّز بالإضافة إلى ترويسة وبيانات كل عنصر
فرعي، ويجب أن تحتوي مخازن الحسابات المؤقتة على حسابات جميع العناصر الفرعية.
حدِّد أحجامها للدُّفعة الأكبر التي تبنيها، أو استخدم BatchState (مع ميزة
alloc) لتحديد الحجم في وقت التشغيل. لا يزال CPI المُجمَّع خاضعاً لحدود
الحسابات والحجم الخاصة بالمعاملة، لذا للتوزيع واسع النطاق استخدم Address Lookup
Tables لاستيعاب مزيد من الحسابات.
Is this page helpful?