Cross-Program Invocation (CPI) वह तरीका है जिससे प्रोग्राम सोलाना ब्लॉकचेन पर अन्य प्रोग्राम्स को कॉल करते हैं। इस ट्यूटोरियल में, हम सीखेंगे कि नेटिव Rust में CPI कॉल्स कैसे की जाती हैं।
हमने अपने पिछले Anchor ट्यूटोरियल्स में SOL ट्रांसफर करते समय या SPL Token प्रोग्राम के माध्यम से टोकन मिंट करते समय पहले ही CPI का उपयोग किया है। Anchor में, एक CPI कॉल इस प्रकार दिखती है:
let cpi_ctx = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
}
);
system_program::transfer(cpi_ctx, amount)?;
ऊपर दिया गया कोड System Program और ट्रांसफर के लिए आवश्यक अकाउंट्स के साथ एक CPI कॉन्टेक्स्ट बनाता है, फिर CPI को निष्पादित (execute) करने के लिए system_program::transfer को कॉल करता है।
यह ट्यूटोरियल बताता है कि जब आप Anchor में CPI कॉल्स करते हैं तो परदे के पीछे क्या होता है, फिर यह दिखाता है कि सोलाना के नेटिव CPI फ़ंक्शंस का सीधे उपयोग कैसे करें।
हम इसमें कवर करेंगे:
- सोलाना में दो मुख्य CPI फ़ंक्शंस:
invokeऔरinvoke_signed - Anchor इन मुख्य फ़ंक्शंस को कैसे एब्स्ट्रैक्ट (abstract) करता है
- एक व्यावहारिक उदाहरण दिखा कर नेटिव Rust में मैन्युअल रूप से CPI इंस्ट्रक्शंस कैसे बनाएं, जहां हम दो प्रोग्राम बनाएंगे: एक टारगेट प्रोग्राम जो 42 लौटाता है और एक कॉलर प्रोग्राम जो इसे CPI के माध्यम से कॉल करता है
आइए invoke और invoke_signed CPI फ़ंक्शंस को समझकर शुरुआत करें।
सोलाना के मुख्य CPI फ़ंक्शंस को समझना
Cross-Program Invocations करने के लिए सोलाना में दो मुख्य फ़ंक्शंस हैं:
invoke: उन CPI कॉल्स के लिए उपयोग किया जाता है जिन्हें PDA signing की आवश्यकता नहीं होती है (मूल ट्रांजेक्शन साइनर्स का उपयोग करता है)invoke_signed: उन CPI कॉल्स के लिए उपयोग किया जाता है जिन्हें PDA साइनिंग की आवश्यकता होती है (जब किसी प्रोग्राम को अपने द्वारा नियंत्रित PDA की ओर से साइन करने की आवश्यकता होती है)
आइए इन फ़ंक्शंस को विस्तार से देखें:
1. invoke फ़ंक्शन
invoke फ़ंक्शन अकाउंट्स और इंस्ट्रक्शन डेटा के साथ दूसरे प्रोग्राम को कॉल करता है। आप इसका उपयोग तब करते हैं जब आपके प्रोग्राम को ऑथराइजेशन (authorization) के लिए मूल ट्रांजेक्शन साइनर्स का उपयोग करके किसी अन्य प्रोग्राम को इनवोक करने की आवश्यकता होती है।
pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
) -> ProgramResult
पैरामीटर्स इस प्रकार हैं:
instruction: एकInstructionstruct जिसमें शामिल हैं:program_id: टारगेट प्रोग्राम की पब्लिक की (public key)accounts:AccountMetaस्ट्रक्ट्स (structs) का एक वेक्टर। प्रत्येक स्ट्रक्ट में तीन फ़ील्ड होते हैं:pubkey(अकाउंट की पब्लिक की),is_signer(क्या इस अकाउंट को ट्रांजेक्शन साइन करना चाहिए), औरis_writable(क्या प्रोग्राम इस अकाउंट को मॉडिफाई कर सकता है)data: इंस्ट्रक्शन डेटा वाला एक बाइट ऐरे। इसमें आमतौर पर एक डिस्क्रिमिनेटर (यह पहचानने के लिए कि किस इंस्ट्रक्शन को एक्ज़ीक्यूट करना है) शामिल होता है, जिसके बाद इंस्ट्रक्शन द्वारा अपेक्षित कोई भी पैरामीटर होता है। इसका सटीक लेआउट टारगेट प्रोग्राम द्वारा परिभाषित किया जाता है।
account_infos:AccountInfoस्ट्रक्ट्स का एक स्लाइस। इसमें इंस्ट्रक्शन केaccountsफ़ील्ड में संदर्भित सभी अकाउंट्स के साथ-साथ टारगेट प्रोग्राम का अकाउंट भी शामिल होना चाहिए। रनटाइम इनका उपयोग निष्पादन (execution) के दौरान वास्तविक अकाउंट डेटा तक पहुंचने के लिए करता है।
इंस्ट्रक्शन में AccountMeta सोलाना को बताता है कि आपको किन अकाउंट्स की आवश्यकता है और उनका उपयोग कैसे किया जाएगा। AccountInfo वास्तविक अकाउंट डेटा और स्टेट प्रदान करता है जिसे आपका प्रोग्राम पढ़ता या लिखता है।
2. invoke_signed फ़ंक्शन
invoke_signed फ़ंक्शन invoke की तरह ही अकाउंट्स और इंस्ट्रक्शन डेटा के साथ दूसरे प्रोग्राम को कॉल करता है, लेकिन इसका उपयोग तब किया जाता है जब आपके प्रोग्राम को PDA की ओर से साइन करना होता है। यह इस तरह काम करता है:
invoke_signedका उपयोग करते समय, आपको PDA को डिराइव (derive) करने के लिए उपयोग किए जाने वाले सीड्स (seeds) प्रदान करने होंगे- रनटाइम यह वेरिफाई करने के लिए इन सीड्स का उपयोग करता है कि आपके प्रोग्राम ने PDA को डिराइव किया है (यानी, PDA आपके प्रोग्राम का है)
- यह आपके प्रोग्राम को PDA की ओर से साइन करने की अनुमति देता है, क्योंकि PDAs में प्राइवेट कीज (private keys) नहीं होती हैं और वे सीधे साइन नहीं कर सकते हैं
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
signers_seeds: &[&[&[u8]]],
) -> ProgramResult
इस फ़ंक्शन में invoke के समान ही पैरामीटर्स हैं, जिसमें एक अतिरिक्त पैरामीटर शामिल है:
signers_seeds: उन PDAs के लिए डेरिवेशन सीड्स जिन्हें CPI इंस्ट्रक्शन को साइन करने की आवश्यकता होती है। रनटाइम इन सीड्स का उपयोग PDA को फिर से डिराइव करने और यह वेरिफाई करने के लिए करता है कि यह आपके प्रोग्राम से संबंधित है।
अब जब हम सोलाना द्वारा प्रदान किए जाने वाले नेटिव CPI फ़ंक्शंस को समझ गए हैं, तो आइए देखें कि Anchor उनका उपयोग कैसे करता है।
Anchor कैसे सोलाना के CPI फ़ंक्शंस को एब्स्ट्रैक्ट करता है
Anchor CPI कॉल्स बनाने की जटिलता को कम करता है, इसके लिए यह invoke और invoke_signed फ़ंक्शंस को रैप (wrap) करने वाले दो दृष्टिकोण प्रदान करता है:
1. रेगुलर CPI कॉल्स (मूल ट्रांजेक्शन साइनर्स का उपयोग करते हुए):
जब इनवोक किए गए प्रोग्राम द्वारा आवश्यक अकाउंट्स पहले से ही मूल ट्रांजेक्शन में साइनर होते हैं, तो Anchor नेटिव invoke फ़ंक्शन को रैप करने के लिए CpiContext::new() का उपयोग करता है। System Program के माध्यम से SOL ट्रांसफर करने का एक उदाहरण यहां दिया गया है:
let cpi_ctx = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
}
);
system_program::transfer(cpi_ctx, amount)?;
2. PDA-साइंड CPI कॉल्स (जब आपके प्रोग्राम को PDA की ओर से साइन करने की आवश्यकता होती है):
जब आपके प्रोग्राम द्वारा नियंत्रित PDA को इनवोक किए गए इंस्ट्रक्शन पर साइन करना होता है, तो Anchor नेटिव invoke_signed फ़ंक्शन को रैप करने के लिए CpiContext::new_with_signer() का उपयोग करता है। तीसरा पैरामीटर (&[&seeds]) PDA को डिराइव करने और उसके साथ साइन करने के लिए सीड्स प्रदान करता है:
let seeds = &[
b"seed-prefix",
payer.key.as_ref(),
&[bump],
];
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
&[&seeds],
);
token::transfer(cpi_ctx, amount)?;
किसी भी दृष्टिकोण के साथ इंस्ट्रक्शन कॉन्टेक्स्ट बनाने के बाद, आप कॉन्टेक्स्ट के साथ उपयुक्त Anchor CPI हेल्पर फ़ंक्शन (जैसे system_program::transfer या token::transfer) को कॉल करते हैं।
परदे के पीछे, Anchor सभी CPI कॉल्स के लिए invoke_signed का उपयोग करता है। ऐसा इसलिए है क्योंकि:
- बिना किसी साइनर सीड्स के, यह बिल्कुल
invokeकी तरह काम करता है - साइनर सीड्स के साथ, यह PDA साइनिंग को सक्षम करता है
इस एकीकृत दृष्टिकोण का मतलब है कि Anchor को सभी CPI ऑपरेशन्स के लिए केवल एक कोड पाथ की आवश्यकता होती है।
invoke फ़ंक्शन इस तरह से काम करता है क्योंकि इसका इम्प्लीमेंटेशन invoke_signed फ़ंक्शन का उपयोग करता है लेकिन PDA साइनर सीड्स के लिए एक खाली बाइट स्लाइस पास करता है।
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {
invoke_signed(instruction, account_infos, &[])
}
सोलाना के BPF रनटाइम के अंदर, invoke और invoke_signed दोनों sol_invoke_signed_rust syscall को कॉल करते हैं। यह सिस्कॉल (syscall) कॉलर के निष्पादन को निलंबित करके, टारगेट प्रोग्राम को इनवोक करके, कॉल स्टैक को मैनेज करके, और साइनर सीड्स प्रदान किए जाने पर PDA-डिराइव्ड सिग्नचेर्स को वेरिफाई करके वास्तविक क्रॉस-प्रोग्राम इनवोकेशन करता है। इसमें एक C भाषा का ABI वैरिएंट भी है, sol_invoke_signed_c, जो C प्रोग्रामिंग भाषा में लिखे गए प्रोग्राम्स के लिए समान व्यवहार को एक्सपोज़ करता है।
यह देखने के लिए कि Anchor invoke_signed का उपयोग कैसे करता है, आइए Anchor सोर्स कोड में system_program::transfer फ़ंक्शन का निरीक्षण करें। ध्यान दें कि यह system_instruction::transfer का उपयोग करके एक इंस्ट्रक्शन बनाता है, फिर ctx.signer_seeds के साथ invoke_signed को कॉल करता है:
pub fn transfer<'info>(
ctx: CpiContext<'_, '_, '_, 'info, Transfer<'info>>,
lamports: u64,
) -> Result<()> {
let ix = crate::solana_program::system_instruction::transfer(
ctx.accounts.from.key,
ctx.accounts.to.key,
lamports,
);
crate::solana_program::program::invoke_signed(
&ix,
&[ctx.accounts.from, ctx.accounts.to],
ctx.signer_seeds,
)
.map_err(Into::into)
}
यही समान पैटर्न Anchor SPL token crate से SPL टोकन transfer फ़ंक्शन में दिखाई देता है:
pub fn transfer<'info>(
ctx: CpiContext<'_, '_, '_, 'info, Transfer<'info>>,
amount: u64,
) -> Result<()> {
let ix = spl_token::instruction::transfer(
&spl_token::ID,
ctx.accounts.from.key,
ctx.accounts.to.key,
ctx.accounts.authority.key,
&[],
amount,
)?;
anchor_lang::solana_program::program::invoke_signed(
&ix,
&[ctx.accounts.from, ctx.accounts.to, ctx.accounts.authority],
ctx.signer_seeds,
)
.map_err(Into::into)
}
ध्यान दें कि दोनों उदाहरणों में, कोड लगातार ctx.signer_seeds के साथ invoke_signed का उपयोग करता है। जैसा कि पहले उल्लेख किया गया है, जब कोई सीड्स प्रदान नहीं किए जाते हैं (रेगुलर CPI), तो खाली signer_seeds invoke_signed को बिल्कुल invoke की तरह व्यवहार करने पर मजबूर करता है। जब सीड्स प्रदान किए जाते हैं (PDA साइनिंग), तो यह प्रोग्राम को अपने PDAs की ओर से साइन करने में सक्षम बनाता है।
अब हम समझ गए हैं कि Anchor क्रॉस-प्रोग्राम इनवोकेशन्स को कैसे एब्स्ट्रैक्ट करता है। आगे, हम सीखेंगे कि मैन्युअल रूप से CPI इंस्ट्रक्शंस कैसे बनाए जाते हैं।
नेटिव Rust में मैन्युअल रूप से CPI इंस्ट्रक्शंस बनाना
एक नेटिव Rust सोलाना प्रोग्राम में CPI कॉल का निर्माण करने और उसे निष्पादित करने के लिए, हमें एक इंस्ट्रक्शन बनाना होगा और फिर उपयुक्त CPI फ़ंक्शन को कॉल करना होगा।
ऐसा करने के लिए, हमें यह करना होगा:
- उस प्रोग्राम के
program_idके साथ एक इंस्ट्रक्शन बनाएं जिसे हम कॉल करना चाहते हैं, उन अकाउंट्स की एक सूची बनाएं जिन्हें प्रोग्राम को CPI के दौरान पढ़ना या लिखना होगा, और भेजने के लिए इंस्ट्रक्शन डेटा तैयार करें। - फिर हम इंस्ट्रक्शन और अकाउंट्स के साथ उपयुक्त CPI फ़ंक्शन (
invokeयाinvoke_signed) को कॉल करते हैं
जैसा कि इस ट्यूटोरियल की शुरुआत में बताया गया है, हम दो अलग-अलग सोलाना प्रोग्राम बनाएंगे जो एक साथ काम करते हैं:
- एक टारगेट प्रोग्राम: जो कॉल किए जाने पर 42 संख्या लौटाता है (हम बाद में देखेंगे कि कैसे)
- एक कॉलर प्रोग्राम: जो
invokeफ़ंक्शन का उपयोग करके टारगेट प्रोग्राम को CPI कॉल्स करता है।
दोनों प्रोग्राम्स को लागू करके, हम दोनों दृष्टिकोणों से संपूर्ण CPI प्रक्रिया को देख सकते हैं और समझ सकते हैं कि प्रोग्राम्स के बीच डेटा कैसे प्रवाहित होता है।
इससे पहले कि हम अपने प्रोग्राम बनाएं, आइए अपना प्रोजेक्ट स्ट्रक्चर सेट करें:
mkdir solana-cpi-example
cd solana-cpi-example
टारगेट प्रोग्राम बनाना
हम टारगेट प्रोग्राम को सरल रखेंगे, यह केवल कॉलर प्रोग्राम के लिए एक मान (value) लौटाएगा।
सबसे पहले, हम टारगेट प्रोग्राम डायरेक्टरी बनाते हैं और इसे इनिशियलाइज़ करते हैं (solana-cpi-example के अंदर):
mkdir target-program
cd target-program
cargo init --lib
target-program/Cargo.toml को अपडेट करें:
[package]
name = "target-program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
solana-program = "1.18"
target-program/src/lib.rs के कोड को नीचे दिए गए कोड से बदलें। इस प्रोग्राम में, हम:
solana_programक्रेट (crate) सेset_return_dataसहित आवश्यक सोलाना डिपेंडेंसीज़ (dependencies) इंपोर्ट करते हैं (हम नीचे दिए गए कोड के बाद इसे समझाएंगे)- एक
process_instructionफ़ंक्शन परिभाषित करते हैं जो:- 42 मान (value) के साथ एक वेरिएबल बनाता है, और
- इस मान को कॉलर को वापस लौटाने के लिए
set_return_dataका उपयोग करता है
// target-program/src/lib.rs
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
program::set_return_data,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
// Create the data we want to return
let return_value: u64 = 42;
let return_bytes = return_value.to_le_bytes();
// Set the return data that the calling program can access
set_return_data(&return_bytes);
Ok(())
}
set_return_data फ़ंक्शन
set_return_data फ़ंक्शन (solana_program क्रेट से) एक बफ़र में डेटा स्टोर करता है जिसे कॉलिंग प्रोग्राम CPI के लौटने के बाद पढ़ सकता है। अधिकतम रिटर्न डेटा साइज़ 1024 बाइट्स होता है।
हम अपने u64 मान को बाइट्स में बदलते हैं और इसे रिटर्न डेटा के रूप में सेट करते हैं। हमें इसकी आवश्यकता इसलिए है क्योंकि ProgramResult रिटर्न टाइप रनटाइम को केवल सफलता या विफलता का संकेत देता है, वास्तविक डेटा का नहीं। अतिरिक्त अकाउंट्स की आवश्यकता के बिना प्रोग्राम्स के बीच सीधे डेटा पासिंग को सक्षम करने के लिए सोलाना ने set_return_data पेश किया। Anchor में, यह आपके इंस्ट्रक्शन फ़ंक्शंस में रिटर्न टाइप्स के माध्यम से स्वचालित रूप से हैंडल किया जाता है।
जब हम आगे कॉलर प्रोग्राम बनाएंगे तो देखेंगे कि इस डेटा को कैसे प्राप्त किया जाए।
अब टारगेट प्रोग्राम को बिल्ड करें:
cargo build-sbf
कॉलर प्रोग्राम बनाना
इसके बाद, हम उस प्रोग्राम को लागू करेंगे जो टारगेट प्रोग्राम को CPI कॉल्स करता है। यह दिखाएगा कि एक प्रोग्राम दूसरे प्रोग्राम को कैसे कॉल कर सकता है।
कॉलर प्रोग्राम टारगेट प्रोग्राम से CPI कॉल्स और डेटा रिट्रीवल (data retrieval) को हैंडल करेगा।
आइए प्रोजेक्ट रूट पर वापस जाएं और अपना कॉलर प्रोग्राम बनाएं:
cd ..
mkdir caller-program
cd caller-program
cargo init --lib
caller-program/Cargo.toml को अपडेट करें:
[package]
name = "caller-program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
solana-program = "1.18"
caller-program/src/lib.rs के कोड को नीचे दिए गए कोड से बदलें। इस प्रोग्राम में, हम:
solana_programक्रेट सेget_return_dataसहित आवश्यक सोलाना डिपेंडेंसीज़ इंपोर्ट करते हैं- अपने प्रोग्राम के लिए एक
process_instructionफ़ंक्शन परिभाषित करते हैं जो:- अकाउंट्स ऐरे में पहले अकाउंट से टारगेट प्रोग्राम ID एक्सट्रेक्ट (extract) करता है
- बिना इंस्ट्रक्शन डेटा के एक CPI इंस्ट्रक्शन बनाता है
invoke()का उपयोग करके CPI कॉल करता हैget_return_data()का उपयोग करके लौटाया गया डेटा प्राप्त करता है- टारगेट प्रोग्राम से प्राप्त वास्तविक मान (value) को लॉग (log) करता है
हम कोड ब्लॉक के बाद CPI कंस्ट्रक्शन को विस्तार से समझेंगे।
// caller-program/src/lib.rs
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
instruction::{AccountMeta, Instruction},
msg,
program::{invoke, get_return_data},
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
msg!("Caller program: Making CPI call to target program");
// Extract the target program's program_id from the accounts array
let target_program_account = &accounts[0];
let target_program_id = *target_program_account.key;
// In production, verify the program ID matches expected value and is executable:
// assert!(target_program_id == EXPECTED_PROGRAM_ID);
// assert!(target_program_account.executable);
// Build CPI instruction with empty data
let instruction = Instruction {
program_id: target_program_id,
accounts: vec![], // No additional accounts needed for our simple target program
data: vec![], // No instruction data needed
};
// Make the CPI call using invoke
// We pass target_program_account (an AccountInfo from our accounts parameter)
// because invoke needs the AccountInfo of the program we're calling
invoke(&instruction, &[target_program_account.clone()])?;
// Get the return data from the called program
let (_, return_data) = get_return_data().unwrap();
let value = u64::from_le_bytes(return_data[0..8].try_into().unwrap());
msg!("Caller program: Target program returned {}", value);
Ok(())
}// caller-program/src/lib.rs
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
instruction::{AccountMeta, Instruction},
msg,
program::{invoke, get_return_data},
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
msg!("Caller program: Making CPI call to target program");
// Extract the target program's program_id from the accounts array
let target_program_account = &accounts[0];
let target_program_id = *target_program_account.key;
// In production, verify the program ID matches expected value and is executable:
// assert!(target_program_id == EXPECTED_PROGRAM_ID);
// assert!(target_program_account.executable);
// Build CPI instruction with empty data
let instruction = Instruction {
program_id: target_program_id,
accounts: vec![], // No additional accounts needed for our simple target program
data: vec![], // No instruction data needed
};
// Make the CPI call using invoke
// We pass target_program_account (an AccountInfo from our accounts parameter)
// because invoke needs the AccountInfo of the program we're calling
invoke(&instruction, &[target_program_account.clone()])?;
// Get the return data from the called program
let (_, return_data) = get_return_data().unwrap();
let value = u64::from_le_bytes(return_data[0..8].try_into().unwrap());
msg!("Caller program: Target program returned {}", value);
Ok(())
}
get_return_data फ़ंक्शन
get_return_data फ़ंक्शन हमारे कॉलर प्रोग्राम को उस प्रोग्राम से डेटा प्राप्त करने की अनुमति देता है जिसे हमने अभी कॉल किया है।
यह फ़ंक्शन Option<(Pubkey, Vec<u8>)> लौटाता है—एक टुपल (tuple) जिसमें डेटा सेट करने वाले प्रोग्राम की ID और स्वयं डेटा होता है। हमें केवल रिटर्न डेटा (दूसरा आइटम) से मतलब है, इसलिए हम इसे let (_, return_data) = get_return_data().unwrap() के रूप में डीस्ट्रक्चर (destructure) करते हैं। फिर हम उन बाइट्स को वापस अपने अपेक्षित डेटा प्रकार (इस मामले में u64) में बदल देते हैं।
cargo build-sbf
कॉलर प्रोग्राम में CPI कंस्ट्रक्शन का विश्लेषण
सबसे पहले, हमने अकाउंट्स ऐरे से टारगेट प्रोग्राम ID को एक्सट्रेक्ट किया:

फिर हमने वह इंस्ट्रक्शन बनाया जो हमारे CPI कॉल को परिभाषित करता है:

इस Instruction स्ट्रक्ट में तीन घटक (components) होते हैं:
program_id: उस प्रोग्राम का एड्रेस जिसे हम कॉल कर रहे हैं (इस मामले में टारगेट प्रोग्राम की ID)accounts: उन अकाउंट्स की सूची जिनकी हमारे इंस्ट्रक्शन को आवश्यकता है (इस मामले में खाली)data: पास करने के लिए कोई भी इंस्ट्रक्शन डेटा (यह भी खाली है)
अंत में, हमने invoke फ़ंक्शन का उपयोग करके CPI को निष्पादित किया:

invoke फ़ंक्शन ये पैरामीटर्स लेता है:
- हमारे द्वारा बनाया गया इंस्ट्रक्शन
- इंस्ट्रक्शन के लिए आवश्यक सभी अकाउंट इंफोज़ (account infos) की एक सूची
यदि कॉल किया गया प्रोग्राम ProgramError लौटाता है, तो त्रुटि (error) रनटाइम तक फैल जाती है, निष्पादन तुरंत रुक जाता है, और संपूर्ण ट्रांजेक्शन विफल हो जाता है। कंट्रोल कॉलर प्रोग्राम में वापस नहीं आता है।
लेकिन जब invoke फ़ंक्शन सफलतापूर्वक निष्पादित होता है, तो यह होता है:
- कॉलर प्रोग्राम इंस्ट्रक्शन और अकाउंट इंफोज़ के साथ
invoke()को कॉल करता है - सोलाना रनटाइम कॉलर प्रोग्राम को निलंबित करता है और निष्पादन (execution) को टारगेट प्रोग्राम में ट्रांसफर करता है
- टारगेट प्रोग्राम निष्पादित होता है, मान 42 को स्टोर करने के लिए
set_return_data(&return_bytes)को कॉल करता है, औरOk(())लौटाता है - रनटाइम कॉलर प्रोग्राम को वहीं से फिर से शुरू करता है जहाँ से उसने छोड़ा था (
invoke()कॉल के ठीक बाद) - कॉलर प्रोग्राम बफ़र से बाइट्स प्राप्त करने के लिए
get_return_data()को कॉल करता है - यह उन बाइट्स को वापस u64 मान (42) में परिवर्तित करता है और इसे लॉग करता है
दोनों प्रोग्राम्स को डिप्लॉय (Deploy) और टेस्ट करना
दोनों प्रोग्राम्स को डिप्लॉय करना
अब जब हमने दोनों प्रोग्राम लिख लिए हैं — टारगेट प्रोग्राम और CPI करने वाला कॉलर — हम यह टेस्ट करने के लिए उन्हें डिप्लॉय कर सकते हैं कि हमारा इम्प्लीमेंटेशन काम करता है या नहीं।
एक लोकल टेस्ट वैलिडेटर शुरू करने और लॉग देखने के लिए अलग-अलग टर्मिनल टैब में निम्नलिखित कमांड चलाएँ:
solana-test-validator # in a separate terminal
solana logs # in another terminal
पहले टारगेट प्रोग्राम को डिप्लॉय करें:
cd target-program
solana program deploy target/deploy/target_program.so
आप प्रोग्राम ID दिखाने वाला आउटपुट देखेंगे। इस प्रोग्राम ID को कॉपी करें—क्लाइंट टेस्ट सेट करते समय आपको इसकी आवश्यकता होगी।

कॉलर प्रोग्राम को डिप्लॉय करें:
cd ../caller-program
solana program deploy target/deploy/caller_program.so
इस प्रोग्राम ID को भी कॉपी करें।
दोनों प्रोग्राम्स की टेस्टिंग
दोनों प्रोग्राम डिप्लॉय होने के साथ, हमें अपने कॉलर प्रोग्राम को ट्रिगर करना होगा और परिणामों का निरीक्षण करना होगा। हम यह एक TypeScript क्लाइंट के साथ करेंगे।
हम एक TypeScript क्लाइंट बनाएंगे जो हमारे कॉलर प्रोग्राम को एक ट्रांजेक्शन भेजता है। यह क्लाइंट प्रक्रिया शुरू करता है: क्लाइंट कॉलर प्रोग्राम को कॉल करता है, जो फिर टारगेट प्रोग्राम को CPI कॉल्स करता है।
अब, आइए प्रोजेक्ट रूट पर वापस जाएं और अपना TypeScript क्लाइंट सेट करें:
cd ..
mkdir client && cd client
npm init -y
npm install @solana/web3.js typescript ts-node @types/node
client/package.json को अपडेट करें:
{
"scripts": {
"test": "ts-node client.ts"
}
}
client/tsconfig.json बनाएं:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["*.ts"]
}
आगे, client/client.ts बनाएं और नीचे दिया गया कोड जोड़ें। इस क्लाइंट कोड में, हम:
- लोकल सोलाना क्लस्टर से कनेक्शन सेट करते हैं
- टारगेट और कॉलर प्रोग्राम ID के लिए कॉन्स्टेंट्स (
TARGET_PROGRAM_IDऔरCALLER_PROGRAM_ID) परिभाषित करते हैं - टेस्टिंग के लिए SOL के साथ एक साइनर अकाउंट बनाते और उसे फंड करते हैं
- एक ऐसा इंस्ट्रक्शन बनाते हैं जो हमारे कॉलर प्रोग्राम को कॉल करता है
- टारगेट प्रोग्राम ID को एक अकाउंट के रूप में पास करते हैं ताकि कॉलर प्रोग्राम को पता चले कि CPI के माध्यम से किस प्रोग्राम को कॉल करना है
- ट्रांजेक्शन को निष्पादित करते हैं और ट्रांजेक्शन सिग्नेचर प्रदर्शित करते हैं
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
Transaction,
TransactionInstruction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
// Replace these with your actual program IDs
const TARGET_PROGRAM_ID = new PublicKey("YOUR_TARGET_PROGRAM_ID_HERE");
const CALLER_PROGRAM_ID = new PublicKey("YOUR_CALLER_PROGRAM_ID_HERE");
const connection = new Connection("http://localhost:8899", "confirmed");
async function testCPI() {
console.log("Testing Cross-Program Invocation\n");
// Create and fund a signer account
const signer = Keypair.generate();
console.log("Funding signer account...");
await connection.requestAirdrop(signer.publicKey, LAMPORTS_PER_SOL);
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log(`Signer: ${signer.publicKey.toString()}`);
console.log(`Target Program: ${TARGET_PROGRAM_ID.toString()}`);
console.log(`Caller Program: ${CALLER_PROGRAM_ID.toString()}\n`);
// Create instruction to call our caller program
// The caller program expects the target program ID as the first account
const instruction = new TransactionInstruction({
keys: [{ pubkey: TARGET_PROGRAM_ID, isSigner: false, isWritable: false }],
programId: CALLER_PROGRAM_ID,
data: Buffer.alloc(0), // No instruction data needed
});
const transaction = new Transaction().add(instruction);
console.log("Executing CPI test...");
const signature = await sendAndConfirmTransaction(
connection,
transaction,
[signer],
{
commitment: "confirmed",
preflightCommitment: "confirmed",
},
);
}
testCPI().catch(console.error);
सुनिश्चित करें कि आप TARGET_PROGRAM_ID और CALLER_PROGRAM_ID को अपने डिप्लॉयमेंट के वास्तविक प्रोग्राम ID से बदल दें।
अब टेस्ट चलाएँ:
npm run test # inside the client/ directory
यदि हम लॉग्स को देखें, तो हम देख सकते हैं कि कॉलर प्रोग्राम से टारगेट प्रोग्राम तक CPI कॉल सफल रही, और कॉलर प्रोग्राम को टारगेट प्रोग्राम से 42 मान प्राप्त हुआ, जिसे उसने लॉग किया।

प्रोग्राम निष्पादन का अवलोकन (Overview of the program execution)
जब हम अपना प्रोग्राम टेस्ट चलाते हैं, तो घटनाओं का एक क्रम होता है। यहाँ इसका एक अवलोकन दिया गया है कि क्या होता है:
- TypeScript क्लाइंट कॉलर प्रोग्राम को कॉल करता है
- कॉलर प्रोग्राम CPI इंस्ट्रक्शन तैयार करता है और
invoke()को कॉल करता है - सोलाना रनटाइम निष्पादन को टारगेट प्रोग्राम पर स्विच करता है
- टारगेट प्रोग्राम निष्पादित होता है, रिटर्न वैल्यू 42 स्टोर करता है,
Ok(())लौटाता है - अंत में, रनटाइम निष्पादन को वापस कॉलर प्रोग्राम पर स्विच कर देता है, जो 42 मान प्राप्त करता है और लॉग करता है
अगले कदम
हमने इस ट्यूटोरियल में invoke_signed का उपयोग नहीं किया क्योंकि इसके लिए नेटिव Rust में अकाउंट्स और PDAs बनाने की आवश्यकता होती है, जिसे हमने अभी तक कवर नहीं किया है। हम एक अन्य ट्यूटोरियल में invoke_signed का एक उदाहरण देखेंगे जब हम नेटिव Rust प्रोग्राम्स में PDAs बनाने के बारे में सीखेंगे।
अब, कॉलर प्रोग्राम बिल्ड करें:
यह फ़ंक्शन Option<(Pubkey, Vec<u8>)> लौटाता है—एक टुपल जिसमें डेटा सेट करने वाले प्रोग्राम की ID और स्वयं डेटा होता है। हमें केवल रिटर्न डेटा (दूसरा एलिमेंट) से मतलब है, इसलिए हम इसे let (_, return_data) = get_return_data().unwrap() के रूप में डीस्ट्रक्चर करते हैं। फिर हम उन बाइट्स को वापस अपने अपेक्षित डेटा प्रकार (इस मामले में u64) में बदल देते हैं।
get_return_data फ़ंक्शन हमारे कॉलर प्रोग्राम को उस प्रोग्राम से डेटा प्राप्त करने की अनुमति देता है जिसे हमने अभी कॉल किया है।
यह मैकेनिज्म प्रोग्राम्स को न केवल एक-दूसरे को कॉल करने की अनुमति देता है बल्कि कॉलर को वापस डेटा पास करने की भी अनुमति देता है।
- यह उन बाइट्स को वापस u64 मान (42) में परिवर्तित करता है और इसे लॉग करता है
- कॉलर प्रोग्राम बफ़र से बाइट्स प्राप्त करने के लिए
get_return_data()को कॉल करता है - रनटाइम कॉलर प्रोग्राम को वहीं से फिर से शुरू करता है जहाँ से उसने छोड़ा था (
invoke()कॉल के ठीक बाद) - टारगेट प्रोग्राम निष्पादित होता है, मान 42 को स्टोर करने के लिए
set_return_data(&return_bytes)को कॉल करता है, औरOk(())लौटाता है - सोलाना रनटाइम कॉलर प्रोग्राम को निलंबित करता है और निष्पादन को टारगेट प्रोग्राम में ट्रांसफर करता है
यह लेख Solana development पर एक ट्यूटोरियल सीरीज़ का हिस्सा है।