Cross Program Invocation (CPI), Solana की वह शब्दावली है जिसका उपयोग एक प्रोग्राम द्वारा दूसरे प्रोग्राम के सार्वजनिक (public) फ़ंक्शन को कॉल करने के लिए किया जाता है।
हम पहले ही CPI कर चुके हैं जब हमने system program को transfer SOL ट्रांज़ैक्शन भेजा था। याद दिलाने के लिए यहाँ प्रासंगिक स्निपेट (snippet) दिया गया है:
pub fn send_sol(ctx: Context<SendSol>, amount: u64) -> Result<()> {
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.signer.to_account_info(),
to: ctx.accounts.recipient.to_account_info(),
}
);
let res = system_program::transfer(cpi_context, amount);
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::TransferFailed);
}
}
CpiContext में Cpi का शाब्दिक अर्थ “Cross program invocation” है।
System Program के अलावा किसी अन्य प्रोग्राम के सार्वजनिक फ़ंक्शन्स को कॉल करने का वर्कफ़्लो (workflow) बहुत अलग नहीं है — और हम इस ट्यूटोरियल में यही सिखाएंगे।
यह ट्यूटोरियल केवल इस बात पर केंद्रित है कि Anchor के साथ बनाए गए किसी दूसरे Solana प्रोग्राम को कैसे कॉल किया जाए। यदि दूसरा प्रोग्राम शुद्ध (pure) Rust के साथ विकसित किया गया था, तो निम्नलिखित गाइड काम नहीं करेगी।
हमारे चल रहे उदाहरण में, Alice प्रोग्राम, Bob प्रोग्राम के एक फ़ंक्शन को कॉल करेगा।
Bob प्रोग्राम
हम Anchor के CLI का उपयोग करके एक नया प्रोजेक्ट बनाने से शुरुआत करते हैं:
anchor init bob
फिर नीचे दिए गए कोड को bob/lib.rs में कॉपी-पेस्ट करें। अकाउंट में दो फ़ंक्शन्स हैं, पहला एक स्टोरेज अकाउंट को इनिशियलाइज़ (initialize) करने के लिए जो u64 को होल्ड करता है, और दूसरा add_and_store फ़ंक्शन जो दो u64 वेरिएबल्स लेता है, उन्हें एक साथ जोड़ता है, और उन्हें BobData स्ट्रक्ट (struct) द्वारा परिभाषित अकाउंट में स्टोर करता है।
use anchor_lang::prelude::*;
use std::mem::size_of;
// REPLACE WITH YOUR <PROGRAM_ID>declare_id!("8GYu5JYsvAYoinbFTvW4AACYB5GxGstz21FmZe3MNFn4");
#[program]
pub mod bob {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
msg!("Data Account Initialized: {}", ctx.accounts.bob_data_account.key());
Ok(())
}
pub fn add_and_store(ctx: Context<BobAddOp>, a: u64, b: u64) -> Result<()> {
let result = a + b;
// MODIFY/UPDATE THE DATA ACCOUNT
ctx.accounts.bob_data_account.result = result;
Ok(())
}
}
#[account]
pub struct BobData {
pub result: u64,
}
#[derive(Accounts)]
pub struct BobAddOp<'info> {
#[account(mut)]
pub bob_data_account: Account<'info, BobData>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = signer, space = size_of::<BobData>() + 8)]
pub bob_data_account: Account<'info, BobData>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
इस ट्यूटोरियल का लक्ष्य एक और प्रोग्राम alice बनाना है जो bob.add_and_store को कॉल करता हो।
प्रोजेक्ट (bob) के भीतर रहते हुए ही, anchor new कमांड का उपयोग करके एक नया प्रोग्राम बनाएं:
anchor new alice
कमांड लाइन को Created new program प्रिंट करना चाहिए।
Alice के लिए प्रोग्राम लिखना शुरू करने से पहले, नीचे दिए गए कोड स्निपेट को programs/alice/Cargo.toml पर स्थित Alice की Cargo.toml फ़ाइल के [dependencies] सेक्शन में जोड़ा जाना चाहिए।
[dependencies]
bob = {path = "../bob", features = ["cpi"]}
Anchor यहाँ बैकग्राउंड में काफी काम कर रहा है। अब Alice के पास Bob के सार्वजनिक फ़ंक्शन्स और Bob के स्ट्रक्ट्स (structs) की परिभाषा (definition) तक पहुंच है। आप इसे Solidity में किसी इंटरफ़ेस को इम्पोर्ट करने के समान मान सकते हैं ताकि हमें पता चल सके कि दूसरे कॉन्ट्रैक्ट के साथ कैसे इंटरैक्ट करना है।
नीचे हम Alice प्रोग्राम दिखाते हैं। सबसे ऊपर, Alice प्रोग्राम उस स्ट्रक्ट को इम्पोर्ट कर रहा है जो BobAddOp (जिसका उपयोग add_and_store के लिए किया जाता है) के लिए अकाउंट्स ले जाता है। कोड में मौजूद कमेंट्स पर ध्यान दें:
use anchor_lang::prelude::*;
// account struct for add_and_store
use bob::cpi::accounts::BobAddOp;
// The program definition for Bob
use bob::program::Bob;
// the account where Bob is storing the sum
use bob::BobData;
declare_id!("6wZDNWprmb9TAZYMAPpT23kHDPABvBLT8jbWQKLHEmBy");
#[program]
pub mod alice {
use super::*;
pub fn ask_bob_to_add(ctx: Context<AliceOp>, a: u64, b: u64) -> Result<()> {
let cpi_ctx = CpiContext::new(
ctx.accounts.bob_program.to_account_info(),
BobAddOp {
bob_data_account: ctx.accounts.bob_data_account.to_account_info(),
}
);
let res = bob::cpi::add_and_store(cpi_ctx, a, b);
// return an error if the CPI failed
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::CPIToBobFailed);
}
}
}
#[error_code]
pub enum Errors {
#[msg("cpi to bob failed")]
CPIToBobFailed,
}
#[derive(Accounts)]
pub struct AliceOp<'info> {
#[account(mut)]
pub bob_data_account: Account<'info, BobData>,
pub bob_program: Program<'info, Bob>,
}
यदि हम ask_bob_to_add की तुलना इस लेख के शीर्ष पर दिए गए उस कोड स्निपेट से करते हैं जहाँ हमने SOL ट्रांसफर करना दिखाया था, तो हमें कई समानताएँ दिखाई देती हैं।

एक CPI करने के लिए, निम्नलिखित चीज़ें आवश्यक हैं:
- लक्ष्य (target) प्रोग्राम का एक संदर्भ (
AccountInfoके रूप में) (red box) - लक्ष्य प्रोग्राम पर फ़ंक्शन को चलाने के लिए आवश्यक अकाउंट्स की सूची (
ctxस्ट्रक्ट जिसमें सभी अकाउंट्स शामिल हैं) (green box) - फ़ंक्शन को पास किए जाने वाले आर्गुमेंट्स (orange box)
CPI की टेस्टिंग
CPI का परीक्षण (test) करने के लिए निम्नलिखित Typescript कोड का उपयोग किया जा सकता है:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Bob } from "../target/types/bob";
import { Alice } from "../target/types/alice";
import { expect } from "chai";
describe("CPI from Alice to Bob", () => {
const provider = anchor.AnchorProvider.env();
// Configure the client to use the local cluster.
anchor.setProvider(provider);
const bobProgram = anchor.workspace.Bob as Program<Bob>;
const aliceProgram = anchor.workspace.Alice as Program<Alice>;
const dataAccountKeypair = anchor.web3.Keypair.generate();
it("Is initialized!", async () => {
// Add your test here.
const tx = await bobProgram.methods
.initialize()
.accounts({
bobDataAccount: dataAccountKeypair.publicKey,
signer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([dataAccountKeypair])
.rpc();
});
it("Can add numbers then double!", async () => {
// Add your test here.
const tx = await aliceProgram.methods
.askBobToAdd(new anchor.BN(4), new anchor.BN(2))
.accounts({
bobDataAccount: dataAccountKeypair.publicKey,
bobProgram: bobProgram.programId,
})
.rpc();
});
it("Can assert value in Bob's data account equals 4 + 2", async () => {
const BobAccountValue = (
await bobProgram.account.bobData
.fetch(dataAccountKeypair.publicKey)
).result.toNumber();
expect(BobAccountValue).to.equal(6);
});
});
एक लाइन में CPI करना
क्योंकि Alice को पास किए गए ctx अकाउंट में उन सभी अकाउंट्स का संदर्भ (reference) होता है जिनकी हमें ट्रांज़ैक्शन करने के लिए आवश्यकता होती है, हम उस स्ट्रक्ट के लिए एक impl के भीतर एक फ़ंक्शन बना सकते हैं जो CPI को पूरा करता है। याद रखें, सभी impl किसी स्ट्रक्ट में फ़ंक्शन्स को “अटैच” (attaches) करते हैं जो स्ट्रक्ट में मौजूद डेटा का उपयोग कर सकते हैं। चूँकि ctx स्ट्रक्ट AliceOp पहले से ही उन सभी अकाउंट्स को होल्ड करता है जिनकी Bob को ट्रांज़ैक्शन के लिए आवश्यकता होती है, हम सारे CPI कोड को ले जा (move कर) सकते हैं:
let cpi_ctx = CpiContext::new(
ctx.accounts.bob_program.to_account_info(),
BobAddOp {
bob_data_account: ctx.accounts.bob_data_account.to_account_info(),
}
);
एक impl में, कुछ इस तरह:
let cpi_ctx = CpiContext::new(
ctx.accounts.bob_program.to_account_info(),
BobAddOp {
bob_data_account: ctx.accounts.bob_data_account.to_account_info(),
}
);
use anchor_lang::prelude::*;
use bob::cpi::accounts::BobAddOp;
use bob::program::Bob;
use bob::BobData;
// REPLACE WITTH YOUR <PROGRAM_ID>declare_id!("B2BNs2GecG8Ux5EchDDFZakRWX2NDfy1RDhPCTJuJtr5");
#[program]
pub mod alice {
use super::*;
pub fn ask_bob_to_add(ctx: Context<AliceOp>, a: u64, b: u64) -> Result<()> {
// Calls the `bob_add_operation` function in bob program
let res = bob::cpi::bob_add_operation(ctx.accounts.add_function_ctx(), a, b);
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::CPIToBobFailed);
}
}
}
impl<'info> AliceOp<'info> {
pub fn add_function_ctx(&self) -> CpiContext<'_, '_, '_, 'info, BobAddOp<'info>> {
// The bob program we are interacting with
let cpi_program = self.bob_program.to_account_info();
// Passing the necessary account(s) to the `BobAddOp` account struct in Bob program
let cpi_account = BobAddOp {
bob_data_account: self.bob_data_account.to_account_info(),
};
// Creates a `CpiContext` object using the new method
CpiContext::new(cpi_program, cpi_account)
}
}
#[error_code]
pub enum Errors {
#[msg("cpi to bob failed")]
CPIToBobFailed,
}
#[derive(Accounts)]
pub struct AliceOp<'info> {
#[account(mut)]
pub bob_data_account: Account<'info, BobData>,
pub bob_program: Program<'info, Bob>,
}
हम “एक लाइन” में Bob को CPI कॉल करने में सक्षम हैं। यह तब काम आ सकता है जब Alice प्रोग्राम के अन्य हिस्से Bob को CPI करते हों — कोड को impl में ले जाने से हमें CpiContext बनाने के लिए कोड को बार-बार कॉपी और पेस्ट करने से रोका जा सकेगा।
RareSkills के साथ और जानें
यह ट्यूटोरियल Solana डेवलपमेंट सीखने की सीरीज़ का एक हिस्सा है।
मूल रूप से 17 मई, 2024 को प्रकाशित