Cross Program Invocation (CPI)
يشير Cross Program Invocation (CPI) إلى قيام برنامج باستدعاء تعليمات برنامج آخر. وهذا يسمح بإمكانية تركيب برامج سولانا.
يمكنك التفكير في التعليمات كنقاط نهاية API يكشفها البرنامج للشبكة، و CPI كواجهة API تستدعي داخليًا واجهة API أخرى.
Cross Program Invocation
النقاط الرئيسية
- Cross Program Invocations تمكّن تعليمات برنامج سولانا من استدعاء تعليمات برنامج آخر مباشرة.
- امتيازات التوقيع من البرنامج المستدعي تمتد إلى البرنامج المستدعى.
- عند إجراء Cross Program Invocation، يمكن للبرامج التوقيع نيابة عن PDAs المشتقة من معرف البرنامج الخاص بها.
- يمكن للبرنامج المستدعى إجراء المزيد من CPIs إلى برامج أخرى، حتى عمق 4.
ما هو CPI؟
Cross Program Invocation (CPI) هو عندما يقوم برنامج باستدعاء تعليمات برنامج آخر.
كتابة تعليمات البرنامج باستخدام CPI تتبع نفس نمط بناء تعليمات لإضافتها إلى معاملة. في الخلفية، يجب أن تحدد كل تعليمات CPI:
- عنوان البرنامج: يحدد البرنامج المراد استدعاؤه
- الحسابات: تسرد كل حساب تقرأ منه التعليمات أو تكتب إليه، بما في ذلك البرامج الأخرى
- بيانات التعليمات: تحدد أي تعليمات يجب استدعاؤها على البرنامج، بالإضافة إلى أي بيانات تحتاجها التعليمات (وسائط الدالة)
عندما يقوم برنامج بإجراء Cross Program Invocation (CPI) إلى برنامج آخر:
- امتيازات المُوقِّع من المعاملة الأولية تمتد إلى البرنامج المستدعى (مثال: A->B)
- يمكن للبرنامج المستدعى إجراء المزيد من Cross Program Invocation إلى برامج أخرى، حتى عمق 4 (مثال: B->C, C->D)
- يمكن للبرامج "التوقيع" نيابة عن PDAs المشتقة من معرف البرنامج الخاص بها
تحدد بيئة تشغيل برنامج Solana ثابتًا
max_instruction_stack_depth
يسمى
MAX_INSTRUCTION_STACK_DEPTH
بقيمة 5. هذا يمثل الحد الأقصى لارتفاع مكدس استدعاء تعليمات البرنامج. يبدأ
ارتفاع المكدس عند 1 للمعاملة الأولية ويزداد بمقدار 1 في كل مرة يستدعي فيها
برنامج تعليمة أخرى. هذا الإعداد يحد من عمق الاستدعاء للـ CPIs إلى 4.
عند معالجة المعاملة، تمتد امتيازات الحساب من برنامج إلى آخر. إليك ما يعنيه ذلك:
لنفترض أن البرنامج A يتلقى تعليمة تحتوي على:
- حساب وقّع على المعاملة
- حساب يمكن الكتابة فيه (قابل للتعديل)
عندما يقوم البرنامج A بإجراء Cross Program Invocation إلى البرنامج B:
- يحصل البرنامج B على استخدام هذه الحسابات نفسها بأذوناتها الأصلية
- يمكن للبرنامج B التوقيع باستخدام حساب المُوقِّع
- يمكن للبرنامج B الكتابة في الحساب القابل للكتابة
- يمكن للبرنامج B حتى تمرير هذه الأذونات نفسها إلى الأمام إذا قام بإجراء Cross Program Invocation الخاصة به
Cross Program Invocation
تتعامل الدالة
invoke
مع Cross Program Invocation التي لا تتطلب موقعين PDA. تستدعي الدالة الوظيفة
invoke_signed
مع مصفوفة signers_seeds
فارغة، مما يشير إلى عدم الحاجة إلى
PDAs للتوقيع.
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {invoke_signed(instruction, account_infos, &[])}
توضح الأمثلة التالية كيفية إجراء Cross Program Invocation باستخدام إطار عمل Anchor و Native Rust. تتضمن البرامج المثال تعليمة واحدة تنقل SOL من حساب إلى آخر باستخدام Cross Program Invocation.
إطار عمل Anchor
تقدم الأمثلة التالية ثلاث طرق لإنشاء استدعاءات البرامج المتقاطعة (CPIs) في برنامج Anchor، كل منها بمستوى مختلف من التجريد. جميع الأمثلة تعمل بنفس الطريقة. الغرض الرئيسي هو إظهار تفاصيل تنفيذ الـ CPI.
- المثال 1: يستخدم
CpiContext
من Anchor ودالة مساعدة لبناء تعليمات الـ CPI. - المثال 2: يستخدم دالة
system_instruction::transfer
من حزمةsolana_program
لبناء تعليمات الـ CPI. المثال 1 يجرد هذا التنفيذ. - المثال 3: يبني تعليمات الـ CPI يدويًا. هذا النهج مفيد عندما لا توجد حزمة للمساعدة في بناء التعليمات.
use anchor_lang::prelude::*;use anchor_lang::system_program::{transfer, Transfer};declare_id!("9AvUNHjxscdkiKQ8tUn12QCMXtcnbR9BVGq3ULNzFMRi");#[program]pub mod cpi {use super::*;pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> {let from_pubkey = ctx.accounts.sender.to_account_info();let to_pubkey = ctx.accounts.recipient.to_account_info();let program_id = ctx.accounts.system_program.to_account_info();let cpi_context = CpiContext::new(program_id,Transfer {from: from_pubkey,to: to_pubkey,},);transfer(cpi_context, amount)?;Ok(())}}#[derive(Accounts)]pub struct SolTransfer<'info> {#[account(mut)]sender: Signer<'info>,#[account(mut)]recipient: SystemAccount<'info>,system_program: Program<'info, System>,}
لغة Rust الأصلية
يوضح المثال التالي كيفية إجراء CPI من برنامج مكتوب بلغة Rust الأصلية. يتضمن البرنامج تعليمة واحدة تنقل SOL من حساب إلى آخر باستخدام CPI. يستخدم ملف الاختبار LiteSVM لاختبار البرنامج.
use borsh::BorshDeserialize;use solana_program::{account_info::AccountInfo,entrypoint,entrypoint::ProgramResult,program::invoke,program_error::ProgramError,pubkey::Pubkey,system_instruction,};// Declare program entrypointentrypoint!(process_instruction);// Define program instructions#[derive(BorshDeserialize)]enum ProgramInstruction {SolTransfer { amount: u64 },}impl ProgramInstruction {fn unpack(input: &[u8]) -> Result<Self, ProgramError> {Self::try_from_slice(input).map_err(|_| ProgramError::InvalidInstructionData)}}pub fn process_instruction(_program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Deserialize instruction datalet instruction = ProgramInstruction::unpack(instruction_data)?;// Process instructionmatch instruction {ProgramInstruction::SolTransfer { amount } => {// Parse accountslet [sender_info, recipient_info, system_program_info] = accounts else {return Err(ProgramError::NotEnoughAccountKeys);};// Verify the sender is a signerif !sender_info.is_signer {return Err(ProgramError::MissingRequiredSignature);}// Create and invoke the transfer instructionlet transfer_ix = system_instruction::transfer(sender_info.key,recipient_info.key,amount,);invoke(&transfer_ix,&[sender_info.clone(),recipient_info.clone(),system_program_info.clone(),],)?;Ok(())}}}
استدعاءات البرامج المتقاطعة مع موقعي PDA
تتعامل دالة
invoke_signed
مع استدعاءات CPI التي تتطلب موقعي PDA. تأخذ الدالة البذور اللازمة لاشتقاق موقعي
PDA كـ signer_seeds
.
يمكنك الرجوع إلى صفحة عنوان مشتق من البرنامج للحصول على تفاصيل حول كيفية اشتقاق عناوين PDA.
pub fn invoke_signed(instruction: &Instruction,account_infos: &[AccountInfo],signers_seeds: &[&[&[u8]]],) -> ProgramResult {// --snip--invoke_signed_unchecked(instruction, account_infos, signers_seeds)}
عند معالجة تعليمة تتضمن CPI، يقوم وقت تشغيل Solana داخليًا باستدعاء
create_program_address
باستخدام signers_seeds
و program_id
للبرنامج المستدعي. عندما يتم التحقق من
صحة PDA، يتم
إضافة العنوان كموقع صالح.
توضح الأمثلة التالية كيفية إجراء CPI مع موقعي PDA باستخدام إطار عمل Anchor ولغة Rust الأصلية. تتضمن برامج المثال تعليمة واحدة تنقل SOL من PDA إلى حساب المستلم باستخدام CPI موقع من قبل PDA.
إطار عمل Anchor
تتضمن الأمثلة التالية ثلاثة أساليب لتنفيذ استدعاءات البرامج المتقاطعة (CPIs) في برنامج Anchor، كل منها بمستوى مختلف من التجريد. جميع الأمثلة متكافئة وظيفياً. الغرض الرئيسي هو توضيح تفاصيل تنفيذ CPI.
- المثال 1: يستخدم
CpiContext
من Anchor ودالة مساعدة لبناء تعليمات CPI. - المثال 2: يستخدم دالة
system_instruction::transfer
من حزمةsolana_program
لبناء تعليمات CPI. المثال 1 هو تجريد لهذا التنفيذ. - المثال 3: يبني تعليمات CPI يدوياً. هذا الأسلوب مفيد عندما لا تتوفر حزمة للمساعدة في بناء التعليمات التي تريد استدعاءها.
use anchor_lang::prelude::*;use anchor_lang::system_program::{transfer, Transfer};declare_id!("BrcdB9sV7z9DvF9rDHG263HUxXgJM3iCQdF36TcxbFEn");#[program]pub mod cpi {use super::*;pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> {let from_pubkey = ctx.accounts.pda_account.to_account_info();let to_pubkey = ctx.accounts.recipient.to_account_info();let program_id = ctx.accounts.system_program.to_account_info();let seed = to_pubkey.key();let bump_seed = ctx.bumps.pda_account;let signer_seeds: &[&[&[u8]]] = &[&[b"pda", seed.as_ref(), &[bump_seed]]];let cpi_context = CpiContext::new(program_id,Transfer {from: from_pubkey,to: to_pubkey,},).with_signer(signer_seeds);transfer(cpi_context, amount)?;Ok(())}}#[derive(Accounts)]pub struct SolTransfer<'info> {#[account(mut,seeds = [b"pda", recipient.key().as_ref()],bump,)]pda_account: SystemAccount<'info>,#[account(mut)]recipient: SystemAccount<'info>,system_program: Program<'info, System>,}
Rust الأصلي
يوضح المثال التالي كيفية إجراء CPI مع موقعين PDA من برنامج مكتوب بلغة Rust الأصلية. يتضمن البرنامج تعليمة واحدة تنقل SOL من حساب PDA إلى حساب المستلم باستخدام CPI موقع بواسطة PDA. يستخدم ملف الاختبار LiteSVM لاختبار البرنامج.
use borsh::BorshDeserialize;use solana_program::{account_info::AccountInfo,entrypoint,entrypoint::ProgramResult,program::invoke_signed,program_error::ProgramError,pubkey::Pubkey,system_instruction,};// Declare program entrypointentrypoint!(process_instruction);// Define program instructions#[derive(BorshDeserialize)]enum ProgramInstruction {SolTransfer { amount: u64 },}impl ProgramInstruction {fn unpack(input: &[u8]) -> Result<Self, ProgramError> {Self::try_from_slice(input).map_err(|_| ProgramError::InvalidInstructionData)}}pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Deserialize instruction datalet instruction = ProgramInstruction::unpack(instruction_data)?;// Process instructionmatch instruction {ProgramInstruction::SolTransfer { amount } => {// Parse accountslet [pda_account_info, recipient_info, system_program_info] = accounts else {return Err(ProgramError::NotEnoughAccountKeys);};// Derive PDA and verify it matches the account provided by clientlet recipient_pubkey = recipient_info.key;let seeds = &[b"pda", recipient_pubkey.as_ref()];let (expected_pda, bump_seed) = Pubkey::find_program_address(seeds, program_id);if expected_pda != *pda_account_info.key {return Err(ProgramError::InvalidArgument);}// Create the transfer instructionlet transfer_ix = system_instruction::transfer(pda_account_info.key,recipient_info.key,amount,);// Create signer seeds for PDAlet signer_seeds: &[&[&[u8]]] = &[&[b"pda", recipient_pubkey.as_ref(), &[bump_seed]]];// Invoke the transfer instruction with PDA as signerinvoke_signed(&transfer_ix,&[pda_account_info.clone(),recipient_info.clone(),system_program_info.clone(),],signer_seeds,)?;Ok(())}}}
Is this page helpful?