Cross-contract call वह तरीका है जिससे एक कॉन्ट्रैक्ट दूसरे कॉन्ट्रैक्ट के पब्लिक फंक्शन को कॉल करता है। इसका एक सामान्य उदाहरण एक लिक्विडिटी पूल (liquidity pool) है जो पूल में टोकन ट्रांसफर करने या निकालने के लिए ERC-20 टोकन कॉन्ट्रैक्ट को कॉल करता है।
इस लेख में, आप सीखेंगे कि Starknet पर cross-contract calls कैसे काम करते हैं और उन्हें अपने स्मार्ट कॉन्ट्रैक्ट्स में कैसे लागू (implement) किया जाए।
Cross-contract calls करने के तरीके
Starknet कॉन्ट्रैक्ट्स में cross-contract calls करने के दो तरीके हैं:
- Contract dispatcher का उपयोग करना
- सीधे
call_contract_syscallsyscall का उपयोग करना
आइए इनमें से प्रत्येक को विस्तार से समझें।
1. Contract dispatcher का उपयोग करना
dispatcher एक कंपाइलर द्वारा जनरेट किया गया struct है जो अन्य कॉन्ट्रैक्ट्स पर टाइप-सेफ (type-safe) कॉल्स को सक्षम करता है। यह एक ContractAddress को रैप (wrap) करता है और उस trait को इम्प्लीमेंट करता है जो कंपाइलर आपके #[starknet::interface] से जनरेट करता है।
Solidity में, आप लक्ष्य (target) कॉन्ट्रैक्ट के एड्रेस को उसके फंक्शन्स को कॉल करने के लिए एक इंटरफ़ेस प्रकार (interface type) में कास्ट करते हैं। Cairo का dispatcher भी इसी तरह काम करता है, सिवाय इसके कि कंपाइलर इसे आपके #[starknet::interface] से जनरेट करता है और आपके लिए कास्टिंग को संभालता है।
जब आप किसी अन्य कॉन्ट्रैक्ट के फंक्शन को कॉल करते हैं, तो आप बस उसे आर्ग्यूमेंट्स के साथ dispatcher पर इनवोक (invoke) करते हैं। आंतरिक रूप से (Internally), dispatcher:
- कंपाइल टाइम पर फंक्शन के नाम से फंक्शन सिलेक्टर (selector) की गणना करता है
- फंक्शन आर्ग्यूमेंट्स को
felt252वैल्यूज में सीरियलाइज़ (serialize) करता है - कॉन्ट्रैक्ट एड्रेस, फंक्शन सिलेक्टर और सीरियलाइज़ किए गए आर्ग्यूमेंट्स के साथ कॉल को निष्पादित (execute) करने के लिए
call_contract_syscallका उपयोग करता है - और वापस आए (returned)
Span<felt252>को अपेक्षित Cairo प्रकारों में वापस डीसीरियलाइज़ (deserialize) करता है
नीचे दिया गया आरेख (diagram) दिखाता है कि क्या होता है जब Contract A एक dispatcher के माध्यम से Contract B के फंक्शन को कॉल करता है:

उच्च स्तर (high-level) पर, आप एक cross-contract call उसी तरह करते हैं जैसे आप किसी भी सामान्य फंक्शन को कॉल करते हैं। dispatcher पर्दे के पीछे सिलेक्टर की गणना, सीरियलाइज़ेशन और डीसीरियलाइज़ेशन को संभालता है।
प्रत्येक कॉन्ट्रैक्ट इंटरफ़ेस के लिए, कंपाइलर कई dispatchers जनरेट करता है (पूरी सूची यहाँ देखें)। हम इन पर ध्यान केंद्रित करेंगे:
- Regular contract dispatcher: cross-contract calls करता है और विफलता (failure) पर पैनिक (panic) हो जाता है
- Safe contract dispatcher: cross-contract calls करता है और
Result<T, Array<felt252>>लौटाता है। इसके बाद आपका कोड रिज़ल्ट की जांच कर सकता है और पूरे ट्रांज़ैक्शन को रिवर्ट (revert) किए बिना विफलताओं को संभाल सकता है। हालाँकि, कुछ मामले ऐसे भी हैं जो अभी भी तुरंत रिवर्ट का कारण बनते हैं जिन्हें पकड़ा नहीं जा सकता। हम इस लेख में बाद में इन सीमाओं (limitations) पर चर्चा करेंगे।
Cross-contract calls को प्रदर्शित करने के लिए एक साधारण बैंक कॉन्ट्रैक्ट बनाना
हम एक बैंक कॉन्ट्रैक्ट के उदाहरण से समझेंगे जहां उपयोगकर्ता RareTokens (पिछले अध्याय से हमारा ERC-20 implementation) को जमा (deposit) और निकाल (withdraw) सकते हैं। सेटअप में दो कॉन्ट्रैक्ट्स शामिल हैं:
RareBank: मुख्य बैंकिंग कॉन्ट्रैक्ट।RareToken: ERC-20 टोकन कॉन्ट्रैक्ट।
RareBank जमा और निकासी के लिए RareToken कॉन्ट्रैक्ट पर फंक्शन्स को कॉल करने के लिए एक dispatcher का उपयोग करेगा।
आइए टोकन कॉन्ट्रैक्ट IRareToken और IRareBank दोनों के इंटरफ़ेस को परिभाषित करें:
use starknet::ContractAddress;
// RareToken ERC20 Interface
#[starknet::interface]
pub trait IRareToken<TContractState> {
fn total_supply(self: @TContractState) -> u256;
fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
fn transfer_from(ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256) -> bool;
fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool;
fn name(self: @TContractState) -> ByteArray;
fn symbol(self: @TContractState) -> ByteArray;
fn decimals(self: @TContractState) -> u8;
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; // For testing
}
// RareBank Interface
#[starknet::interface]
pub trait IRareBank<TContractState> {
fn deposit(ref self: TContractState, amount: u256);
fn withdraw(ref self: TContractState, amount: u256);
fn get_balance(self: @TContractState, user: ContractAddress) -> u256;
}
जब आप #[starknet::interface] के साथ एक इंटरफ़ेस परिभाषित करते हैं, तो कंपाइलर स्वचालित रूप से इसके लिए dispatcher प्रकार जनरेट करता है। जब आप use super::{I..} का उपयोग करके इंटरफ़ेस इम्पोर्ट करते हैं तो ये उपलब्ध हो जाते हैं।
हमारे मामले में, IRareBank और IRareToken को परिभाषित करने से उनके संबंधित dispatchers जनरेट होते हैं जिन्हें नीचे एनिमेशन में दिखाया गया है:
जैसा कि ऊपर देखा गया है, कंपाइलर कई dispatcher-संबंधित प्रकार जनरेट करता है, लेकिन हमारी चर्चा के लिए सबसे अधिक प्रासंगिक (हरे रंग में हाइलाइट किए गए) हैं:
IRareBank के लिए:
- सामान्य कॉन्ट्रैक्ट कॉल्स के लिए
IRareBankDispatcherजो त्रुटियों पर पैनिक होते हैं - उन कॉल्स के लिए
IRareBankSafeDispatcherजो त्रुटि प्रबंधन (error handling) के लिएResult<...>लौटाते हैं
IRareToken के लिए:
- सामान्य कॉल्स के लिए
IRareTokenDispatcher - त्रुटि प्रबंधन के साथ कॉल्स के लिए
IRareTokenSafeDispatcher
अन्य जनरेट किए गए प्रकार (जैसे Copy, Drop, Serde, आदि) इम्प्लीमेंटेशन विवरण हैं जिनका उपयोग Cairo आंतरिक रूप से इन dispatchers को ठीक से काम करने के लिए करता है।
बैंक कॉन्ट्रैक्ट में regular contract dispatcher का उपयोग करना
कॉन्ट्रैक्ट इम्प्लीमेंटेशन की ओर बढ़ते हुए, यहाँ आवश्यक इम्पोर्ट्स हैं:
#[starknet::contract]
mod RareBank {
use super::{IRareTokenDispatcher, IRareTokenDispatcherTrait};
}
हम IRareTokenDispatcher struct और IRareTokenDispatcherTrait दोनों को super:: का उपयोग करके इम्पोर्ट करते हैं क्योंकि वे उसी मॉड्यूल में जनरेट होते हैं जहाँ हमने अपने इंटरफेस परिभाषित किए हैं। यहाँ IRareTokenDispatcher struct है जिसे कंपाइलर जनरेट करता है (नारंगी रंग में हाइलाइट किया गया):

dispatcher struct लक्ष्य कॉन्ट्रैक्ट का एड्रेस रखता है (जो RareToken कॉन्ट्रैक्ट की ओर इशारा करेगा), और कंपाइलर संबंधित IRareTokenDispatcherTrait जनरेट करता है जिसमें सभी फंक्शन सिग्नेचर होते हैं जिन्हें हम IRareToken इंटरफ़ेस से कॉल कर सकते हैं:
trait IRareTokenDispatcherTrait<T> {
fn total_supply(self: T) -> u256;
fn balance_of(self: T, account: ContractAddress) -> u256;
fn allowance(self: T, owner: ContractAddress, spender: ContractAddress) -> u256;
fn transfer(self: T, recipient: ContractAddress, amount: u256) -> bool;
fn transfer_from(self: T, sender: ContractAddress, recipient: ContractAddress, amount: u256) -> bool;
fn approve(self: T, spender: ContractAddress, amount: u256) -> bool;
fn name(self: T) -> ByteArray;
fn symbol(self: T) -> ByteArray;
fn decimals(self: T) -> u8;
fn mint(self: T, recipient: ContractAddress, amount: u256) -> bool;
}
// The compiler also generates this implementation
impl IRareTokenDispatcherImpl of IRareTokenDispatcherTrait<IRareTokenDispatcher> {
fn transfer(self: IRareTokenDispatcher, recipient: ContractAddress, amount: u256) -> bool {
//logic goes in
}
// ... other function implementations
}
ध्यान दें कि सभी फंक्शन सिग्नेचर बिल्कुल उसी तरह मेल खाते हैं जो हमने अपने IRareToken इंटरफ़ेस में परिभाषित किया था, लेकिन self पैरामीटर @TContractState या ref TContractState से बदलकर केवल T हो जाता है।
जेनेरिक (generic) प्रकार का पैरामीटर T एक ही trait को विभिन्न dispatcher प्रकारों के लिए पुन: उपयोग (reuse) करने की अनुमति देता है। चूँकि कंपाइलर आपके इंटरफ़ेस से कई dispatcher वेरिएंट जनरेट करता है (जैसे IRareTokenDispatcher और IRareTokenSafeDispatcher), T का उपयोग करने का अर्थ है कि एक trait परिभाषा इन सभी वेरिएंट्स का काम कर सकती है। जब आप एक विशिष्ट dispatcher का उपयोग करते हैं, तो कंपाइलर T को उस ठोस प्रकार (concrete type) से बदल देता है।
dispatcher का self T है, वह जेनेरिक जो TContractState के बजाय लक्ष्य कॉन्ट्रैक्ट एड्रेस रखता है। कॉन्ट्रैक्ट इम्प्लीमेंटेशन के विपरीत, इसकी कॉन्ट्रैक्ट स्टेट तक कोई पहुँच नहीं होती है। इसके बजाय, यह फंक्शन कॉल्स को उस एड्रेस पर cross-contract calls में बदल देता है जो इसके पास होता है।
trait इम्प्लीमेंटेशन में प्रत्येक फंक्शन के लिए, कंपाइलर कोड जनरेट करता है जो:
- फंक्शन आर्ग्यूमेंट्स को calldata (
felt252वैल्यूज का एक array) में सीरियलाइज़ करता है - कॉन्ट्रैक्ट एड्रेस, फंक्शन सिलेक्टर और सीरियलाइज़्ड calldata के साथ
call_contract_syscallका उपयोग करके लो-लेवल कॉन्ट्रैक्ट कॉल करता है - लौटाए गए मान (returned value) को अपेक्षित रिटर्न प्रकार (return type) में डीसीरियलाइज़ करता है
यह वह “अनुवाद” (translation) प्रक्रिया है जिसका हमने पहले उल्लेख किया था: dispatcher हाई-लेवल Cairo फंक्शन कॉल्स को लो-लेवल syscalls में परिवर्तित करता है, उन्हें निष्पादित करता है, और परिणामों को वापस Cairo प्रकारों में परिवर्तित करता है।
यहाँ पूरा बैंक कॉन्ट्रैक्ट इम्प्लीमेंटेशन है। कंस्ट्रक्टर (constructor) एक ओनर (owner) और RareToken कॉन्ट्रैक्ट एड्रेस के साथ बैंक को सेटअप करता है। deposit() उपयोगकर्ता से टोकन जमा स्वीकार करता है, withdraw() टोकन वापस उपयोगकर्ता को ट्रांसफर करता है, और get_balance() उपयोगकर्ता का वर्तमान बैंक बैलेंस लौटाता है:
use starknet::ContractAddress;
// RareToken ERC20 Interface - defines functions we can call on the token contract
#[starknet::interface]
pub trait IRareToken<TContractState> {
fn total_supply(self: @TContractState) -> u256;
fn balance_of(self: @TContractState, account: ContractAddress) -> u256;
fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256;
fn transfer(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool;
fn transfer_from(ref self: TContractState, sender: ContractAddress, recipient: ContractAddress, amount: u256) -> bool;
fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool;
fn name(self: @TContractState) -> ByteArray;
fn symbol(self: @TContractState) -> ByteArray;
fn decimals(self: @TContractState) -> u8;
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256) -> bool; // For testing
}
// RareBank Interface - defines the bank's functions
#[starknet::interface]
pub trait IRareBank<TContractState> {
fn deposit(ref self: TContractState, amount: u256);
fn withdraw(ref self: TContractState, amount: u256);
fn get_balance(self: @TContractState, user: ContractAddress) -> u256;
}
// RareBank Contract - manages RareToken deposits and withdrawals
#[starknet::contract]
mod RareBank {
use starknet::{ContractAddress, get_caller_address, get_contract_address};
use starknet::storage::{
StoragePointerReadAccess, StoragePointerWriteAccess,
Map, StoragePathEntry
};
// import the generated dispatcher and trait for cross contract calls
use super::{IRareTokenDispatcher, IRareTokenDispatcherTrait};
#[storage]
struct Storage {
owner: ContractAddress,
rare_token: ContractAddress, // address of the RareToken contract we'll interact with
balances: Map<ContractAddress, u256>, // maps user addresses to their bank balances
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
DepositSuccessful: DepositSuccessful,
WithdrawSuccessful: WithdrawSuccessful,
}
#[derive(Drop, starknet::Event)]
struct DepositSuccessful {
user: ContractAddress,
amount: u256
}
#[derive(Drop, starknet::Event)]
struct WithdrawSuccessful {
user: ContractAddress,
amount: u256
}
// constructor sets up the bank with owner and RareToken contract address
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress, rare_token_address: ContractAddress) {
assert!(owner != 0.try_into().unwrap(), "address zero detected");
assert!(rare_token_address != 0.try_into().unwrap(), "address zero detected");
self.owner.write(owner);
self.rare_token.write(rare_token_address); // store the token contract address
}
// Implements the IRareBank interface and makes functions externally callable
#[abi(embed_v0)]
impl RareBankImpl of super::IRareBank<ContractState> {
fn deposit(ref self: ContractState, amount: u256) {
assert!(amount > 0, "can't deposit zero amount");
let caller = get_caller_address();
let this_contract = get_contract_address();
let rare_token_address = self.rare_token.read(); // get the stored token address
// create dispatcher instance pointing to the RareToken contract
let rare_token = IRareTokenDispatcher { contract_address: rare_token_address };
// cross contract call: transfer tokens from caller to this bank contract
// this calls the transfer_from function on the RareToken contract
// note: caller must have approved this contract to spend at least `amount` tokens
let success = rare_token.transfer_from(caller, this_contract, amount);
assert!(success, "transfer failed");
// update the caller's balance in our bank's storage
let prev_balance = self.balances.entry(caller).read();
self.balances.entry(caller).write(prev_balance + amount);
// emit DepositSuccessful event
self.emit(DepositSuccessful { user: caller, amount });
}
fn withdraw(ref self: ContractState, amount: u256) {
let caller = get_caller_address();
let rare_token_address = self.rare_token.read();
assert!(rare_token_address != 0.try_into().unwrap(), "RareToken not set");
// check if caller has sufficient balance in the bank
let user_balance = self.balances.entry(caller).read();
assert!(user_balance >= amount, "insufficient funds");
// update balance first
self.balances.entry(caller).write(user_balance - amount);
// create dispatcher instance pointing to the RareToken contract
let rare_token = IRareTokenDispatcher { contract_address: rare_token_address };
// cross contract call: transfer tokens from bank back to caller
// this calls the transfer function on the RareToken contract
let success = rare_token.transfer(caller, amount);
assert!(success, "transfer failed");
// emit WithdrawSuccessful event
self.emit(WithdrawSuccessful { user: caller, amount });
}
// view function to check user's balance in the bank
fn get_balance(self: @ContractState, user: ContractAddress) -> u256 {
self.balances.entry(user).read()
}
}
}
deposit() फंक्शन से नीचे दी गई इन पंक्तियों को देखें, यहाँ बताया गया है कि कैसे RareBank कॉलर (caller) से बैंक में टोकन ट्रांसफर करने के लिए RareToken कॉन्ट्रैक्ट पर transfer_from को कॉल करने के लिए dispatcher का उपयोग करता है:
// create a dispatcher instance pointing to the *RareToken* contract
let rare_token = IRareTokenDispatcher { contract_address: rare_token_address };
// use the dispatcher to call transfer_from on the *RareToken* contract
let success = rare_token.transfer_from(caller, this_contract, amount);
Solidity में इसके समतुल्य (equivalent) यह होगा:
// cast the address to the interface type
// transferFrom returns true on success, reverts on failure
IERC20 rareToken = IERC20(rareTokenAddress);
bool success = rareToken.transferFrom(msg.sender, address(this), amount);
Cairo और Solidity दोनों एक ही दृष्टिकोण (approach) का उपयोग करते हैं: बाहरी कॉन्ट्रैक्ट्स पर टाइप-सेफ कॉल्स करने के लिए इंटरफ़ेस परिभाषा के साथ कॉन्ट्रैक्ट एड्रेस को रैप करना। सिंटैक्स थोड़ा भिन्न है (Solidity IERC20(address) का उपयोग करके रैप करता है जबकि Cairo struct इनिशियलाइज़ेशन का उपयोग करता है), लेकिन मूल अवधारणा (underlying concept) समान है।
Dispatcher deposit() फंक्शन में कैसे काम करता है
Cairo के deposit() कोड में क्या होता है, वह यहाँ है:
- dispatcher इंस्टेंस बनाना: हम RareToken कॉन्ट्रैक्ट के एड्रेस के साथ
IRareTokenDispatcherको इंस्टेंशिएट (instantiate) करते हैं - फंक्शन को कॉल करना: जब हम
rare_token.transfer_from(caller, this_contract, amount)को कॉल करते हैं, तो dispatcher (IRareTokenDispatcherTrait) अनुवाद को संभालता है - अनुवाद प्रक्रिया (Translation process):
- dispatcher आर्ग्यूमेंट्स (
caller,this_contract,amount) कोfelt252array में सीरियलाइज़ करता है selector!("transfer_from")का उपयोग करके फंक्शन नाम से फंक्शन सिलेक्टर (एकfelt252हैश) की गणना करता है- इसके साथ
call_contract_syscallकॉल करता है:- RareToken कॉन्ट्रैक्ट एड्रेस
- गिना गया (Computed) फंक्शन सिलेक्टर
- सीरियलाइज़ किए गए आर्ग्यूमेंट्स
- लौटाए गए
felt252array को वापसboolरिज़ल्ट में डीसीरियलाइज़ करता है
- dispatcher आर्ग्यूमेंट्स (
एक बार cross-contract call सफल हो जाने के बाद, RareToken कॉन्ट्रैक्ट की स्टेट अपडेट हो जाती है (टोकन कॉलर से बैंक में ट्रांसफर हो जाते हैं), और बैंक कॉन्ट्रैक्ट अपने स्वयं के लॉजिक के साथ जारी रहता है:
// update the user's balance in the bank's storage
let prev_balance = self.balances.entry(caller).read();
self.balances.entry(caller).write(prev_balance + amount);
// emit DepositSuccessful event
self.emit(DepositSuccessful { user: caller, amount });
यही पैटर्न withdraw() फंक्शन पर लागू होता है:
let rare_token = IRareTokenDispatcher { contract_address: rare_token_address };
let success = rare_token.transfer(caller, amount);
Errors को संभालने के लिए safe contract dispatcher का उपयोग करना
regular contract dispatcher के विपरीत, जब आप safe dispatcher का उपयोग करके किसी फंक्शन को कॉल करते हैं, तो आपको सीधे परिणाम (result) नहीं मिलता है। इसके बजाय, आपको एक Result प्रकार मिलता है जो इनमें से कोई एक हो सकता है:
Ok(value): कॉल सफल रहा और एक वैल्यू लौटायाErr(error_data): कॉल विफल रहा और त्रुटि (error) जानकारी लौटाई
यह आपको त्रुटियों को स्वयं संभालने की सुविधा देता है। आप दोनों मामलों को संभालने के लिए match का उपयोग करते हैं और तय करते हैं कि त्रुटियां होने पर क्या करना है।
Safe dispatcher का उपयोग करने के लिए RareBank को एक्सटेंड करना
आइए withdraw_safe() फंक्शन बनाएं, एक ऐसा संस्करण (version) जो RareToken कॉन्ट्रैक्ट से त्रुटियों को संभालने के लिए safe dispatcher का उपयोग करता है। जब हम rare_token.transfer() को कॉल करते हैं और यह विफल हो जाता है (त्रुटि लौटाता है), तो पैनिक होने और पूरे ट्रांज़ैक्शन को रिवर्ट करने के बजाय, हम उपयोगकर्ता के बैंक बैलेंस को रीस्टोर (restore) करेंगे और एक त्रुटि इवेंट (error event) एमिट (emit) करेंगे।
मौजूदा dispatcher इम्पोर्ट्स के साथ RareToken safe dispatcher प्रकारों (IRareTokenSafeDispatcher और IRareTokenSafeDispatcherTrait) को इम्पोर्ट करें:
use super::{
IRareTokenDispatcher, IRareTokenDispatcherTrait,
IRareTokenSafeDispatcher, IRareTokenSafeDispatcherTrait, //NEWLY ADDED
};
इसके बाद, निकासी विफलताओं (withdrawal failures) को पकड़ने के लिए एक नया इवेंट परिभाषित करें (WithdrawFailed):
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
DepositSuccessful: DepositSuccessful,
WithdrawSuccessful: WithdrawSuccessful,
WithdrawFailed: WithdrawFailed, // New event
}
#[derive(Drop, starknet::Event)]
struct WithdrawFailed {
user: ContractAddress,
amount: u256,
error: Array<felt252>,
}
नए फंक्शन को शामिल करने के लिए IRareBank इंटरफ़ेस को अपडेट करें:
#[starknet::interface]
pub trait IRareBank<TContractState> {
fn deposit(ref self: TContractState, amount: u256);
fn withdraw(ref self: TContractState, amount: u256);
fn withdraw_safe(ref self: TContractState, amount: u256); // ADD THIS
fn get_balance(self: @TContractState, user: ContractAddress) -> u256;
}
अब withdraw_safe फंक्शन को इम्प्लीमेंट करें। safe dispatchers का उपयोग करते समय #[feature("safe_dispatcher")] एट्रिब्यूट (attribute) आवश्यक है। इसके बिना, आपको एक अनस्टेबल फीचर (unstable feature) का उपयोग करने के बारे में कंपाइलर वार्निंग मिलेगी। यह एट्रिब्यूट स्पष्ट रूप से इस फंक्शन के लिए safe dispatcher उपयोग को सक्षम बनाता है:
#[feature("safe_dispatcher")]
fn withdraw_safe(ref self: ContractState, amount: u256) {
let caller = get_caller_address();
let rare_token_address = self.rare_token.read();
// check if caller has sufficient balance in the bank
let user_balance = self.balances.entry(caller).read();
assert!(user_balance >= amount, "insufficient funds");
// update balance first
self.balances.entry(caller).write(user_balance - amount);
// CREATE SAFE DISPATCHER INSTANCE
let rare_token = IRareTokenSafeDispatcher { contract_address: rare_token_address };
match rare_token.transfer(caller, amount) {
Result::Ok(_) => {
// Transfer succeeded - RareToken always returns true on success
self.emit(WithdrawSuccessful { user: caller, amount });
},
Result::Err(error) => {
// Transfer panicked - restore balance and emit error
self.balances.entry(caller).write(user_balance);
self.emit(WithdrawFailed { user: caller, amount, error });
},
}
}
withdraw_safe फंक्शन उपयोगकर्ता के बैलेंस की जांच से शुरू होता है, फिर निकासी राशि को काट लेता है। हम RareToken कॉन्ट्रैक्ट को पॉइंट करते हुए एक safe dispatcher बनाते हैं और transfer() को कॉल करते हैं।
match स्टेटमेंट लौटाए गए Result को संभालता है:
Result::Ok(_): ट्रांसफर फंक्शन सफलतापूर्वक निष्पादित (executed) हो गया। RareToken कॉन्ट्रैक्ट काtransferफंक्शन सफल होने पर हमेशाtrueलौटाता है - सभी त्रुटि मामलों के कारण फंक्शन पैनिक हो जाता है।Result::Err(error): ट्रांसफर फंक्शन पैनिक हो गया। यह तब होता है जब:- सेंडर के पास अपर्याप्त बैलेंस होता है (
assert(sender_prev_balance >= amount)विफल हो जाता है) - ट्रांज़ैक्शन वेरिफिकेशन विफल हो जाता है
- ट्रांसफर फंक्शन में कोई अन्य एसेर्शन (assertion) विफल हो जाता है
- सेंडर के पास अपर्याप्त बैलेंस होता है (
जब हम त्रुटि पकड़ लेते हैं, तो हम उपयोगकर्ता के बैंक बैलेंस को वापस user_balance (निकासी में कटौती से पहले की राशि) पर रीस्टोर कर देते हैं और त्रुटि विवरण के साथ WithdrawFailed एमिट करते हैं।
जब safe dispatchers अभी भी रिवर्ट (revert) होते हैं
हालाँकि safe contract dispatchers cross-contract calls के दौरान कई त्रुटि परिदृश्यों (error scenarios) को संभालते हैं, कुछ सिस्टम-स्तरीय विफलताएं (system-level failures) पूरे ट्रांज़ैक्शन को Result::Err लौटाने के बजाय तुरंत रिवर्ट करने का कारण बनती हैं। इनमें शामिल हैं:
- ऐसे कॉन्ट्रैक्ट को कॉल करना जो निर्दिष्ट (specified) एड्रेस पर मौजूद नहीं है
- Cairo Zero कॉन्ट्रैक्ट्स द्वारा फेंकी गई त्रुटियां, जो
Resultत्रुटि हैंडलिंग का समर्थन नहीं करते हैं - जब कॉल किया गया कॉन्ट्रैक्ट आंतरिक रूप से अमान्य (invalid) पैरामीटर के साथ किसी कॉन्ट्रैक्ट को डिप्लॉय करने का प्रयास करता है
- जब कॉल किया गया कॉन्ट्रैक्ट आंतरिक रूप से ऐसे क्लास हैश (class hash) का उपयोग करके अपग्रेड करने का प्रयास करता है जो मौजूद नहीं है
ये सिस्टम-स्तरीय विफलताएं हैं जिन्हें safe dispatchers पकड़ नहीं सकते हैं। Safe contract dispatchers केवल सामान्य कॉन्ट्रैक्ट निष्पादन से त्रुटियों को पकड़ते हैं जैसे विफल एसेर्शन (failed assertions) या स्पष्ट रिवर्ट (explicit reverts)।
हमारे withdraw_safe उदाहरण में, यदि rare_token_address किसी ऐसे कॉन्ट्रैक्ट को पॉइंट करता है जो मौजूद नहीं है, तो जब हम rare_token.transfer() को कॉल करते हैं, तो ट्रांज़ैक्शन तुरंत रिवर्ट हो जाएगा, यहाँ तक कि safe dispatcher के साथ भी। इसी तरह, यदि आप किसी फ़ैक्ट्री कॉन्ट्रैक्ट को कॉल कर रहे थे जो अमान्य क्लास हैश के साथ डिप्लॉय करने का प्रयास करता है, तो वह भी तत्काल रिवर्ट का कारण बनेगा।
नोट: इन सीमाओं (limitations) को Starknet के भविष्य के संस्करणों में संबोधित किए जाने की उम्मीद है।
2. सीधे call_contract_syscall syscall का उपयोग करना
Cairo cross-contract calls करने के लिए call_contract_syscall का उपयोग करता है। जबकि contract dispatchers आंतरिक रूप से इस syscall का उपयोग करते हैं, हम इसे सीधे कॉल कर सकते हैं जब हमें सीरियलाइज़ेशन और डीसीरियलाइज़ेशन पर मैन्युअल नियंत्रण की आवश्यकता होती है।
call_contract_syscall का उपयोग करने के लिए, हमें निम्नलिखित को इम्पोर्ट करने की आवश्यकता है:
use starknet::{SyscallResultTrait, syscalls};
SyscallResultTrait syscall ऑपरेशन्स से परिणाम निकालने (extracting) के लिए .unwrap_syscall() मेथड प्रदान करता है, और syscalls मॉड्यूल में वह call_contract_syscall फंक्शन होता है जिसका उपयोग हम लो-लेवल कॉल करने के लिए करेंगे।
call_contract_syscall इम्प्लीमेंटेशन का उदाहरण
नीचे दिया गया कोड दिखाता है कि cross-contract call को निष्पादित करने के लिए हमारा deposit फंक्शन सीधे call_contract_syscall का उपयोग कैसे करता है:
fn deposit_with_direct_syscall(ref self: ContractState, amount: u256) {
assert!(amount > 0, "can't deposit zero amount");
let caller = get_caller_address();
let this_contract = get_contract_address();
let rare_token_address = self.rare_token.read();
// manually serialize function arguments into felt252 array
let mut call_data: Array<felt252> = array![];
Serde::serialize(@caller, ref call_data); // sender
Serde::serialize(@this_contract, ref call_data); // recipient
Serde::serialize(@amount, ref call_data); // amount
// === MAKE THE DIRECT SYSCALL === //
let mut res = syscalls::call_contract_syscall(
rare_token_address,
selector!("transfer_from"),
call_data.span(),
).unwrap_syscall();
// manually deserialize the response
let success: bool = Serde::<bool>::deserialize(ref res).unwrap();
assert!(success, "transfer failed");
// update balance and emit event
let prev_balance = self.balances.entry(caller).read();
self.balances.entry(caller).write(prev_balance + amount);
self.emit(DepositSuccessful { user: caller, amount });
}
call_contract_syscall के लिए तीन पैरामीटर्स की आवश्यकता होती है:
let mut res = syscalls::call_contract_syscall(
rare_token_address,
selector!("transfer_from"),
call_data.span(),
).unwrap_syscall();
- Contract address (
rare_token_address): कॉल करने के लिए लक्ष्य कॉन्ट्रैक्ट - Function selector:
selector!("function_name")का उपयोग करके गणना की जाती है - Calldata: फंक्शन आर्ग्यूमेंट्स
Span<felt252>के रूप में सीरियलाइज़ किए गए हैं
हमें Serde::serialize() का उपयोग करके आर्ग्यूमेंट्स के सीरियलाइज़ेशन और Serde::deserialize() का उपयोग करके रिस्पॉन्स के डीसीरियलाइज़ेशन को मैन्युअल रूप से संभालना होगा।
deposit_with_direct_syscall() बिल्कुल वही काम करता है जो हमारे regular contract dispatcher में deposit() करता है।
Cross-contract calls के लिए अनुशंसित (Recommended) विधि
मानक कॉन्ट्रैक्ट इंटरैक्शन (standard contract interactions) के लिए सीधे call_contract_syscall का उपयोग करने की अनुशंसा (recommend) नहीं की जाती है क्योंकि यह:
- मैन्युअल सीरियलाइज़ेशन/डीसीरियलाइज़ेशन की आवश्यकता रखता है
- कंपाइल-टाइम टाइप चेकिंग (compile-time type checking) का अभाव है
- dispatchers की तुलना में सीरियलाइज़ेशन त्रुटियों की अधिक संभावना है
इसके बजाय, contract dispatcher दृष्टिकोण का उपयोग करें:
let rare_token = IRareTokenDispatcher { contract_address: rare_token_address };
let success = rare_token.transfer_from(caller, this_contract, amount);
चूँकि contract dispatcher स्वचालित रूप से सीरियलाइज़ेशन और टाइप चेकिंग को संभालता है, प्रत्यक्ष (direct) syscalls का उपयोग केवल तभी किया जाना चाहिए जब dispatchers आपकी विशिष्ट आवश्यकताओं को पूरा नहीं कर सकते हैं।
निष्कर्ष (Wrapping Up)
हमने Starknet पर cross-contract calls करने के मुख्य तरीकों को कवर किया है: contract dispatchers (regular और safe) और direct syscalls।
Contract dispatchers अधिकांश उपयोग के मामलों (use cases) के लिए अनुशंसित (recommended) दृष्टिकोण हैं। जब कॉल्स का सफल होना आवश्यक हो तो regular dispatchers का उपयोग करें। जब आपको संपूर्ण ट्रांज़ैक्शन को रिवर्ट किए बिना विफलताओं को संभालने की आवश्यकता हो, तो safe dispatchers का उपयोग करें, यह ध्यान में रखते हुए कि कुछ सिस्टम-स्तरीय विफलताएं अभी भी तत्काल रिवर्ट का कारण बन सकती हैं जैसा कि हमने चर्चा की है। Direct syscalls उन विशेष आवश्यकताओं को पूरा करते हैं जिनमें स्पष्ट सीरियलाइज़ेशन नियंत्रण की आवश्यकता होती है।
Cross-contract calls के साथ निर्माण करते समय, हमेशा इनपुट को मान्य (validate) करें, त्रुटियों को सावधानीपूर्वक संभालें, और अपने कॉन्ट्रैक्ट्स को सुरक्षित रखने के लिए रिवर्ट या दुर्भावनापूर्ण (malicious) कॉन्ट्रैक्ट्स के प्रति सचेत रहें।