Cross Program Invocation

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

عند إجراء CPI، يمكن للبرنامج التوقيع نيابة عن PDA مشتق من معرف البرنامج الخاص به. تمتد امتيازات التوقيع هذه من البرنامج المستدعي إلى البرنامج المستدعى.

مثال على استدعاء البرامج المتقاطعةمثال على استدعاء البرامج المتقاطعة

عند إجراء CPI، تمتد امتيازات الحساب من برنامج إلى آخر. لنفترض أن البرنامج A يتلقى تعليمات مع حساب موقّع وحساب قابل للكتابة. ثم يقوم البرنامج A بإجراء CPI إلى البرنامج B. الآن، يمكن للبرنامج B استخدام نفس الحسابات مثل البرنامج A، مع أذوناتها الأصلية. (مما يعني أن البرنامج B يمكنه التوقيع باستخدام حساب الموقّع ويمكنه الكتابة في الحساب القابل للكتابة.) إذا قام البرنامج B بإجراء CPIs الخاصة به، فيمكنه تمرير نفس الأذونات للأمام، حتى عمق 4.

الحد الأقصى لارتفاع استدعاء تعليمات البرنامج يسمى max_instruction_stack_depth وهو محدد بثابت MAX_INSTRUCTION_STACK_DEPTH بقيمة 5.

يبدأ ارتفاع المكدس عند 1 للمعاملة الأولية ويزداد بمقدار 1 في كل مرة يستدعي فيها البرنامج تعليمات أخرى، مما يحد من عمق الاستدعاء لـ CPIs إلى 4.

CPIs مع موقعي PDA

عند إجراء استدعاء CPI يتطلب موقع PDA، يتم استخدام دالة invoke_signed. تأخذ هذه الدالة بذور التوقيع المستخدمة لاشتقاق PDAs الموقعة. يقوم وقت تشغيل سولانا داخلياً باستدعاء create_program_address باستخدام signers_seeds و program_id للبرنامج المستدعي. عندما يتم التحقق من صحة PDA، يتم إضافته كموقع صالح.

Invoke signed
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo],
signers_seeds: &[&[&[u8]]],
) -> ProgramResult {
// --snip--
invoke_signed_unchecked(instruction, account_infos, signers_seeds)
}

توضح الأمثلة أدناه كيفية إجراء CPI مع موقعين PDA باستخدام Anchor و Rust الأصلي. يتضمن كل مثال تعليمة واحدة لتحويل SOL من PDA إلى حساب المستلم، باستخدام CPI موقع بواسطة PDA.

Anchor

توضح الأمثلة التالية ثلاثة أساليب لتنفيذ استدعاءات CPI في برنامج Anchor. الأمثلة متكافئة وظيفياً، لكن كل منها يوضح مستوى مختلف من التجريد.

  • المثال 1: يستخدم CpiContext من Anchor ودالة مساعدة.
  • المثال 2: يستخدم دالة system_instruction::transfer من حزمة solana_program. (المثال 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 entrypoint
entrypoint!(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 data
let instruction = ProgramInstruction::unpack(instruction_data)?;
// Process instruction
match instruction {
ProgramInstruction::SolTransfer { amount } => {
// Parse accounts
let [pda_account_info, recipient_info, system_program_info] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Derive PDA and verify it matches the account provided by client
let 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 instruction
let transfer_ix = system_instruction::transfer(
pda_account_info.key,
recipient_info.key,
amount,
);
// Create signer seeds for PDA
let signer_seeds: &[&[&[u8]]] = &[&[b"pda", recipient_pubkey.as_ref(), &[bump_seed]]];
// Invoke the transfer instruction with PDA as signer
invoke_signed(
&transfer_ix,
&[
pda_account_info.clone(),
recipient_info.clone(),
system_program_info.clone(),
],
signer_seeds,
)?;
Ok(())
}
}
}

استدعاءات CPI بدون موقعين PDA

عندما لا يتطلب استدعاء CPI موقعين PDA، يتم استخدام دالة invoke. تقوم دالة invoke باستدعاء دالة invoke_signed مع مصفوفة signers_seeds فارغة. تشير المصفوفة الفارغة للموقعين إلى أنه لا توجد حاجة لـ PDAs للتوقيع.

Invoke function
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {
invoke_signed(instruction, account_infos, &[])
}

توضح الأمثلة أدناه كيفية إجراء استدعاء عبر البرامج (CPI) باستخدام Anchor و Native Rust. تتضمن تعليمة واحدة تنقل SOL من حساب إلى آخر.

Anchor

تعرض الأمثلة التالية ثلاث طرق لتنفيذ استدعاءات عبر البرامج (CPIs) في برنامج Anchor. الأمثلة متكافئة وظيفياً، لكن كل منها يوضح مستوى مختلفاً من التجريد.

  • المثال 1: يستخدم CpiContext من Anchor ودالة مساعدة.
  • المثال 2: يستخدم دالة system_instruction::transfer من حزمة solana_program.
  • المثال 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) من برنامج مكتوب بلغة Native Rust. يتضمن تعليمة واحدة تنقل SOL من حساب إلى آخر. (يستخدم ملف الاختبار LiteSVM لاختبار البرنامج.)

use borsh::BorshDeserialize;
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
};
// Declare program entrypoint
entrypoint!(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 data
let instruction = ProgramInstruction::unpack(instruction_data)?;
// Process instruction
match instruction {
ProgramInstruction::SolTransfer { amount } => {
// Parse accounts
let [sender_info, recipient_info, system_program_info] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Verify the sender is a signer
if !sender_info.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Create and invoke the transfer instruction
let 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(())
}
}
}

Is this page helpful?

جدول المحتويات

تعديل الصفحة

تدار بواسطة

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