Component Part 1 में, हमने सीखा कि कॉन्ट्रैक्ट के भीतर एक कंपोनेंट कैसे बनाया और उपयोग किया जाता है, और यह प्रदर्शित किया कि कंपोनेंट्स Solidity में abstract contracts की तरह व्यवहार करते हैं। Component Part 2 में, हमने OpenZeppelin से पहले से बने कंपोनेंट्स का उपयोग करके एक टोकन कॉन्ट्रैक्ट बनाना सीखा।
अब तक, हमने कॉन्ट्रैक्ट स्तर पर कंपोनेंट्स का उपयोग किया है जहाँ कॉन्ट्रैक्ट कंपोनेंट्स को import करता है और उनके functions को कॉल करता है। लेकिन क्या होगा यदि हम एक ऐसा कंपोनेंट बनाना चाहें जो अन्य कंपोनेंट्स की कार्यक्षमता (functionality) का उपयोग करता हो?
उदाहरण के लिए, एक Staking Component पर विचार करें जिसे कई कॉन्ट्रैक्ट्स में पुन: उपयोग (reuse) किया जा सकता है। जब उपयोगकर्ता stake या unstake करते हैं, तो कंपोनेंट को उनके अकाउंट्स में टोकन ट्रांसफर करने और वहां से निकालने की आवश्यकता होती है। इसे यह भी सुनिश्चित करने की आवश्यकता है कि केवल कॉन्ट्रैक्ट का owner ही रिवॉर्ड रेट्स को अपडेट कर सके। इन दोनों के लिए अन्य कंपोनेंट्स को कॉल करने की आवश्यकता होती है। कंपोनेंट से कंपोनेंट इंटरेक्शन, Staking Component के लिए इन आवश्यकताओं को संभालने के लिए दो अलग-अलग कंपोनेंट्स को कॉल करना संभव बनाता है।
नीचे दिया गया चित्र तीन कंपोनेंट्स को इंटीग्रेट करने वाले Staking Contract को दर्शाता है: Staking Component स्टेकिंग लॉजिक को संभालता है और यह ERC20 Component (टोकन ट्रांसफर के लिए) और Ownable Component (यह सुनिश्चित करने के लिए कि केवल कॉन्ट्रैक्ट owner रिवॉर्ड रेट्स अपडेट कर सके) दोनों पर निर्भर करता है:

इस अध्याय के अंत तक, आप सीखेंगे कि कैसे:
- अन्य कंपोनेंट्स से सीधे कंपोनेंट्स को कॉल करें
- कॉन्ट्रैक्ट के भीतर कंपोनेंट डिपेंडेंसीज़ को मैनेज करें
- निर्दिष्ट करें कि कॉन्ट्रैक्ट के ABI में कौन से कंपोनेंट functions को एक्सपोज़ करना है
- कॉन्ट्रैक्ट डिप्लॉयमेंट के दौरान कंपोनेंट्स को इनिशियलाइज़ करें
कंपोनेंट्स अन्य कंपोनेंट्स को कैसे कॉल करते हैं
एक कंपोनेंट को जिम्मेदारी के एक क्षेत्र पर ध्यान केंद्रित करना चाहिए जैसे टोकन मैनेजमेंट, एक्सेस कंट्रोल, या स्टेकिंग लॉजिक। जब किसी कंपोनेंट को अन्य कंपोनेंट्स की कार्यक्षमता का उपयोग करने की आवश्यकता होती है, तो आप उन डिपेंडेंसीज़ को कंपोनेंट के इम्प्लीमेंटेशन सिग्नेचर में डिक्लेयर करते हैं। एक बार डिक्लेयर होने के बाद, कंपोनेंट उन डिपेंडेंसीज़ से functions को कॉल कर सकता है। उदाहरण के लिए, एक स्टेकिंग कंपोनेंट कॉन्ट्रैक्ट द्वारा इन इंटरेक्शन्स को कोऑर्डिनेट किए बिना, एक ERC20 कंपोनेंट से ट्रांसफर functions और एक ownable कंपोनेंट से ओनरशिप चेक्स को कॉल कर सकता है।
जब कोई उपयोगकर्ता 100 टोकन stake करता है, तो कंपोनेंट से कंपोनेंट इंटरेक्शन का फ्लो कुछ इस तरह दिखता है:

इस ट्यूटोरियल में, हम एक ही कॉन्ट्रैक्ट में ERC20 Component और Staking Component को एम्बेड करेंगे। यह एम्बेडेड दृष्टिकोण मुख्य रूप से कंपोनेंट से कंपोनेंट इंटरेक्शन को प्रदर्शित करने के लिए है। प्रोडक्शन में, स्टेकिंग कॉन्ट्रैक्ट्स आमतौर पर एक बाहरी (external) टोकन एड्रेस को स्वीकार करते हैं और कॉन्ट्रैक्ट डिस्पैचर्स का उपयोग करके उस अलग टोकन कॉन्ट्रैक्ट के साथ इंटरैक्ट करते हैं। हम लेख के अंत में बाहरी टोकन दृष्टिकोण और इसके अंतरों की व्याख्या करेंगे।
Staking Component बनाना
हम एक Staking Component बनाएंगे जो OpenZeppelin के ERC20 Component और Ownable Component पर निर्भर करता है, ताकि यह देखा जा सके कि व्यवहार में कंपोनेंट से कंपोनेंट इंटरेक्शन कैसे काम करता है, फिर इसे एक Staking Contract में इंटीग्रेट करेंगे।
हमारा Staking Component निम्नलिखित कार्यक्षमता प्रदान करेगा:
- Staking tokens: उपयोगकर्ता रिवॉर्ड कमाने के लिए टोकन stake कर सकते हैं। जब कोई उपयोगकर्ता stake करता है, तो टोकन उनके बैलेंस से कॉन्ट्रैक्ट में ट्रांसफर कर दिए जाते हैं।
- Unstaking tokens: उपयोगकर्ता किसी भी समय अपने टोकन unstake कर सकते हैं (कोई लॉक-अप अवधि नहीं)। अनस्टेक करते समय, स्टेक किए गए टोकन किसी भी जमा हुए रिवॉर्ड के साथ उपयोगकर्ता को वापस कर दिए जाते हैं।
- Setting reward rates: केवल कॉन्ट्रैक्ट का owner ही रिवॉर्ड रेट को अपडेट कर सकता है, जो यह निर्धारित करता है कि उपयोगकर्ता समय के साथ प्रति स्टेक किए गए टोकन पर कितने रिवॉर्ड टोकन कमाते हैं।
प्रोजेक्ट सेटअप
एक नया scarb प्रोजेक्ट बनाएँ और उसकी डायरेक्टरी में नेविगेट करें:
scarb new component_component
cd component_component
कंपोनेंट डिपेंडेंसीज़ जोड़ना
OpenZeppelin के ERC20 और Ownable कंपोनेंट्स का उपयोग करने के लिए, अपनी Scarb.toml फ़ाइल में [dependencies] के तहत OpenZeppelin डिपेंडेंसी जोड़ें:

Staking Interface
निम्नलिखित इंटरफ़ेस स्टेकिंग-विशिष्ट functions को परिभाषित करता है जिन्हें Staking Component लागू (implement) करेगा:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IStaking<TContractState> {
// User functions
fn stake(ref self: TContractState, amount: u256);
fn unstake(ref self: TContractState, amount: u256);
fn claim_rewards(ref self: TContractState);
// View functions
fn get_staked_balance(self: @TContractState, user: ContractAddress) -> u256;
fn get_total_staked(self: @TContractState) -> u256;
fn calculate_rewards(self: @TContractState, user: ContractAddress) -> u256;
fn get_reward_rate(self: @TContractState) -> u256;
// Admin function
fn set_reward_rate(ref self: TContractState, rate: u256);
}
इंटरफ़ेस को अपनी src/lib.cairo फ़ाइल में जोड़ें, फिर इसके नीचे निम्नलिखित कंपोनेंट स्ट्रक्चर जोड़ें:
#[starknet::component]
pub mod StakingComponent {
// Component implementation will go here
}
स्टोरेज सेटअप
प्रत्येक कंपोनेंट अपनी स्थिति (state) को ट्रैक करने के लिए अपने स्टोरेज स्ट्रक्चर को परिभाषित करता है। StakingComponent के लिए, हमें यह ट्रैक करने की आवश्यकता है कि प्रत्येक उपयोगकर्ता ने कितना stake किया है, उनके रिवॉर्ड्स की गणना अंतिम बार कब की गई थी, कॉन्ट्रैक्ट में स्टेक की गई कुल राशि, उपयोगकर्ताओं के जमा हुए रिवॉर्ड्स और रिवॉर्ड रेट। आइए StakingComponent के अंदर स्टोरेज स्ट्रक्चर को परिभाषित करें:
#[starknet::component]
pub mod StakingComponent {
use starknet::ContractAddress;
use starknet::storage::Map;
#[storage]
pub struct Storage {
staked_balances: Map<ContractAddress, u256>,
total_staked: u256,
reward_rate: u256,
last_update_time: Map<ContractAddress, u64>,
accumulated_rewards: Map<ContractAddress, u256>,
}
}
यहाँ बताया गया है कि प्रत्येक स्टोरेज फ़ील्ड क्या दर्शाता है:
staked_balances: एक मैपिंग जो ट्रैक करती है कि प्रत्येक उपयोगकर्ता ने कितने टोकन stake किए हैं। कुंजी (key) उपयोगकर्ता का एड्रेस है, और मूल्य (value) उनकी स्टेक की गई राशि है।total_staked: कॉन्ट्रैक्ट में सभी उपयोगकर्ताओं द्वारा स्टेक किए गए टोकन की कुल राशि।reward_rate: प्रति स्टेक किए गए टोकन पर प्रति सेकंड प्राप्त होने वाले रिवॉर्ड टोकन की राशि (1,000,000 द्वारा स्केल की गई)। इसे कॉन्ट्रैक्ट owner द्वारा अपडेट किया जा सकता है।last_update_time: उपयोगकर्ता एड्रेस से उनके अंतिम रिवॉर्ड अपडेट के टाइमस्टैम्प तक एक मैपिंग।accumulated_rewards: एक मैपिंग जो प्रत्येक उपयोगकर्ता द्वारा जमा किए गए लेकिन अभी तक क्लेम नहीं किए गए कुल रिवॉर्ड्स को ट्रैक करती है।
कंपोनेंट स्टोरेज और वेरिएबल नेमिंग पर एक नोट
Component Part 1 से याद करें कि #[substorage(v0)] एट्रिब्यूट कंपोनेंट का उपयोग करने वाले कॉन्ट्रैक्ट को उस कंपोनेंट की स्थिति (state) तक पहुंचने की अनुमति देता है।
कई कंपोनेंट्स को इंटीग्रेट करते समय, यदि दो कंपोनेंट्स समान नाम वाले स्टोरेज वेरिएबल्स को परिभाषित करते हैं, तो Cairo कंपाइलर एक संभावित टकराव (collision) के बारे में चेतावनी (warning) देगा:
warn: The path `component_a.variable_name` collides with existing path `component_b.variable_name`.
आप #[allow(starknet::colliding_storage_paths)] के साथ इस चेतावनी को दबा (suppress) सकते हैं, लेकिन यह टकराव को रोकता नहीं है; यह केवल चेतावनी को शांत करता है। दोनों वेरिएबल्स एक ही स्टोरेज लोकेशन की ओर इशारा करेंगे।
यही कारण है कि OpenZeppelin अपने कंपोनेंट्स में स्टोरेज वेरिएबल्स के आगे प्रीफ़िक्स (prefix) लगाता है (ERC20_total_supply, Ownable_owner, आदि)। प्रीफ़िक्स यह सुनिश्चित करते हैं कि भले ही कई कंपोनेंट्स का एक साथ उपयोग किया जाए, उनके स्टोरेज वेरिएबल्स के नाम अद्वितीय (unique) हों और वे आपस में न टकराएं।
इसलिए कंपोनेंट्स बनाते समय, वर्णनात्मक प्रीफ़िक्स या ऐसे नामों का उपयोग करें जो अन्य कंपोनेंट्स के स्टोरेज वेरिएबल्स के साथ कॉन्फ्लिक्ट (conflict) होने की संभावना नहीं रखते हैं।
इवेंट्स डिक्लेरेशन
उपयोगकर्ता कब टोकन stake और unstake करते हैं, यह ट्रैक करने के लिए, StakingComponent में निम्नलिखित इवेंट डेफिनिशन जोड़ें:
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Staked: Staked,
Unstaked: Unstaked
}
#[derive(Drop, starknet::Event)]
pub struct Staked {
pub user: ContractAddress,
pub amount: u256,
}
#[derive(Drop, starknet::Event)]
pub struct Unstaked {
pub user: ContractAddress,
pub amount: u256,
}
Staked उपयोगकर्ता के एड्रेस और राशि को लॉग करता है जब वे अपने टोकन stake करते हैं। Unstaked ऐसा ही तब करता है जब उपयोगकर्ता unstake करता है।
Staking Interface को लागू (Implement) करना
स्टेट वेरिएबल्स और इवेंट्स के साथ, अब हम पहले परिभाषित किए गए IStaking इंटरफ़ेस को लागू कर सकते हैं। हम खाली function स्टब्स बनाकर शुरू करेंगे ताकि हमारा कोड कंपाइल हो सके, फिर हम एक-एक करके प्रत्येक function को लागू करेंगे।
StakingComponent में निम्नलिखित इम्प्लीमेंटेशन ब्लॉक जोड़ें:
#[embeddable_as(StakingImpl)]
impl StakingImplImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
> of super::IStaking<ComponentState<TContractState>> {
fn stake(ref self: ComponentState<TContractState>, amount: u256) {
// Implementation will go here
}
fn unstake(ref self: ComponentState<TContractState>, amount: u256) {
// Implementation will go here
}
fn claim_rewards(ref self: ComponentState<TContractState>) {
// Implementation will go here
}
fn get_staked_balance(
self: @ComponentState<TContractState>, user: ContractAddress
) -> u256 {
0 // Placeholder
}
fn get_total_staked(self: @ComponentState<TContractState>) -> u256 {
0 // Placeholder
}
fn calculate_rewards(
self: @ComponentState<TContractState>, user: ContractAddress
) -> u256 {
0 // Placeholder
}
fn set_reward_rate(ref self: ComponentState<TContractState>, rate: u256) {
// Implementation will go here
}
fn get_reward_rate(self: @ComponentState<TContractState>) -> u256 {
0 // Placeholder
}
}
#[embeddable_as(StakingImpl)] एट्रिब्यूट Cairo को बताता है कि यह इम्प्लीमेंटेशन कॉन्ट्रैक्ट्स में एम्बेड करने के लिए उपलब्ध होना चाहिए।
कंपोनेंट डिपेंडेंसीज़ को डिक्लेयर करना
जैसा कि पहले बताया गया है, StakingComponent को अपने इम्प्लीमेंटेशन सिग्नेचर में ERC20Component और OwnableComponent को डिपेंडेंसी के रूप में डिक्लेयर करना होगा ताकि वह उनके functions को सीधे कॉल कर सके।
StakingComponent मॉड्यूल में ERC20Component, OwnableComponent, और starknet इम्पोर्ट्स जोड़ें:
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc20::ERC20Component;
use starknet::{get_caller_address, get_contract_address};
इसके बाद, इन कंपोनेंट्स को इम्प्लीमेंटेशन सिग्नेचर में डिपेंडेंसी के रूप में डिक्लेयर करें:
#[embeddable_as(StakingImpl)]
impl StakingImplImpl
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,//ADD THIS LINE
impl Ownable: OwnableComponent::HasComponent<TContractState>,//ADD THIS LINE
> of super::IStaking<ComponentState<TContractState>> {
// Functions here
}
impl ERC20: ERC20Component::HasComponent<TContractState> और impl Ownable: OwnableComponent::HasComponent<TContractState> पंक्तियाँ Cairo को बताती हैं कि StakingComponent का उपयोग करने वाले किसी भी कॉन्ट्रैक्ट में इन कंपोनेंट्स को भी शामिल करना आवश्यक है।
यहाँ तक का पूरा StakingComponent कोड इस प्रकार है:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IStaking<TContractState> {
// User functions
fn stake(ref self: TContractState, amount: u256);
fn unstake(ref self: TContractState, amount: u256);
fn claim_rewards(ref self: TContractState);
// View functions
fn get_staked_balance(self: @TContractState, user: ContractAddress) -> u256;
fn get_total_staked(self: @TContractState) -> u256;
fn calculate_rewards(self: @TContractState, user: ContractAddress) -> u256;
fn get_reward_rate(self: @TContractState) -> u256;
// Admin function
fn set_reward_rate(ref self: TContractState, rate: u256);
}
#[starknet::component]
pub mod StakingComponent {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc20::ERC20Component;
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address, get_contract_address};
#[storage]
pub struct Storage {
staked_balances: Map<ContractAddress, u256>,
total_staked: u256,
reward_rate: u256,
last_update_time: Map<ContractAddress, u64>,
accumulated_rewards: Map<ContractAddress, u256>,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Staked: Staked,
Unstaked: Unstaked,
}
#[derive(Drop, starknet::Event)]
pub struct Staked {
pub user: ContractAddress,
pub amount: u256,
}
#[derive(Drop, starknet::Event)]
pub struct Unstaked {
pub user: ContractAddress,
pub amount: u256,
}
#[embeddable_as(StakingImpl)]
impl StakingImplImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of super::IStaking<ComponentState<TContractState>> {
fn stake(
ref self: ComponentState<TContractState>, amount: u256,
) { // Implementation will go here
}
fn unstake(
ref self: ComponentState<TContractState>, amount: u256,
) { // Implementation will go here
}
fn claim_rewards(ref self: ComponentState<TContractState>) {// Implementation will go here
}
fn get_staked_balance(
self: @ComponentState<TContractState>, user: ContractAddress,
) -> u256 {
0 // Placeholder
}
fn get_total_staked(self: @ComponentState<TContractState>) -> u256 {
0 // Placeholder
}
fn calculate_rewards(self: @ComponentState<TContractState>, user: ContractAddress) -> u256 {
0 // Placeholder
}
fn set_reward_rate(
ref self: ComponentState<TContractState>, rate: u256,
) { // Implementation will go here
}
fn get_reward_rate(self: @ComponentState<TContractState>) -> u256 {
0 // Placeholder
}
}
}
stake function को लागू करना
stake function टोकन को उपयोगकर्ता से कॉन्ट्रैक्ट में ट्रांसफर करता है, उनके स्टेक किए गए बैलेंस को अपडेट करता है, और रिवॉर्ड कैलकुलेशन के लिए टाइमस्टैम्प रिकॉर्ड करता है:
fn stake(ref self: ComponentState<TContractState>, amount: u256) {
assert(amount > 0, 'Amount must be greater than 0');
let caller = get_caller_address();
let contract_address = get_contract_address();
// Transfer tokens from caller to contract
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
erc20._transfer(caller, contract_address, amount);
// Update staking balances
let current_stake = self.staked_balances.read(caller);
self.staked_balances.write(caller, current_stake + amount);
let total = self.total_staked.read();
self.total_staked.write(total + amount);
self.emit(Staked { user: caller, amount });
}
फ़ंक्शन यह सत्यापित करके शुरू होता है कि स्टेकिंग राशि शून्य से अधिक है, फिर कॉलर का एड्रेस और कॉन्ट्रैक्ट एड्रेस प्राप्त करता है। यह कॉलर से कॉन्ट्रैक्ट में टोकन ट्रांसफर करता है, कॉलर के स्टेक किए गए बैलेंस और कुल स्टेक की गई राशि को अपडेट करता है, और एक Staked इवेंट एमिट करता है।
कंपोनेंट से कंपोनेंट इंटरेक्शन कहाँ होता है
इन पंक्तियों पर ध्यान दें:
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
erc20._transfer(caller, contract_address, amount);
यहीं पर कंपोनेंट-से-कंपोनेंट इंटरेक्शन होता है। get_dep_component_mut! मैक्रो ERC20Component का एक म्यूटेबल रेफरेंस (mutable reference) प्राप्त करता है, जिससे हम उपयोगकर्ता से कॉन्ट्रैक्ट में टोकन ले जाने के लिए इसके _transfer function को कॉल कर सकते हैं। आइए इसे समझते हैं:
- The macro:
get_dep_component_mut!हमें उस कंपोनेंट का एक म्यूटेबल रेफरेंस देता है जिस पर हम निर्भर करते हैं। इससे हम इसके आंतरिक (internal) functions को कॉल कर सकते हैं। - The parameters:
ref selfकंपोनेंट की स्थिति (state) को संदर्भित करता हैERC20वह डिपेंडेंसी नाम है जिसे हमने इम्प्लीमेंटेशन सिग्नेचर में डिक्लेयर किया था
- Why we need the macro:
StakingComponentके अंदर, हम सीधेself.erc20._transfer(...)को कॉल नहीं कर सकते क्योंकि प्रत्येक कंपोनेंट का स्टोरेज कॉन्ट्रैक्ट के भीतर अलग रखा जाता है।get_dep_component_mut!मैक्रो हमेंERC20Componentका एक रेफरेंस दिलाता है ताकि हम इसके functions को कॉल कर सकें।
आपको आश्चर्य हो सकता है कि हम पारंपरिक transfer_from function के बजाय _transfer का उपयोग क्यों करते हैं। जैसा कि परिचय में बताया गया है, यह ट्यूटोरियल एक एम्बेडेड टोकन आर्किटेक्चर का उपयोग करता है जहाँ टोकन और स्टेकिंग लॉजिक एक ही कॉन्ट्रैक्ट का हिस्सा हैं। यह प्रभावित करता है कि हम किस ट्रांसफर विधि का उपयोग करते हैं। हम इसे और अधिक विस्तार से समझाएंगे और लेख में बाद में बाहरी टोकन को स्टेक करने के साथ इसकी तुलना करेंगे।
ट्रांसफर के बाद, हम उपयोगकर्ता के स्टेक किए गए बैलेंस को उनका वर्तमान स्टेक पढ़कर अपडेट करते हैं, हम नई राशि जोड़ते हैं, और इसे वापस लिख देते हैं। हम कुल स्टेक की गई राशि को भी अपडेट करते हैं और इस क्रिया को लॉग करने के लिए एक Staked इवेंट एमिट करते हैं।
यदि आप इस बिंदु पर कोड को कंपाइल करने का प्रयास करते हैं, तो आप देखेंगे कि _transfer एक एरर (error) थ्रो करता है। जब आप इसके ऊपर होवर करते हैं, तो आप देखेंगे:
Method `_transfer` not found on type `openzeppelin_token::erc20::erc20::ERC20Comp
onent::ComponentState::<TContractState>`. Did you import the correct trait and impl?
यह एरर इसलिए होता है क्योंकि _transfer ERC20Component का एक इंटरनल function है, और हमने उस trait को import नहीं किया है जो इसे लागू करता है। इसे ठीक करने के लिए, StakingComponent मॉड्यूल के शीर्ष पर निम्नलिखित import जोड़ें:
use openzeppelin::token::erc20::ERC20Component::InternalTrait as ERC20InternalTrait;
यह import हमें ERC20Component के इंटरनल functions जैसे _transfer और _mint तक पहुंच प्रदान करता है। यदि आप फिर से कंपाइल करते हैं, तो आपको एक और एरर मिलेगा जो कहता है:
Trait has no implementation in context: openzeppelin_token::erc20::erc20::ERC20Co
mponent::InternalTrait::<TContractState, ImplVarId(34881), ImplVarId(34882)> ….
ऐसा इसलिए होता है क्योंकि ERC20Component को इसके ERC20HooksTrait के इम्प्लीमेंटेशन की आवश्यकता होती है। यह trait उन हुक्स (hooks) को परिभाषित करता है जो टोकन ट्रांसफर से पहले और बाद में रन हो सकते हैं। चूंकि हमें अपने स्टेकिंग कॉन्ट्रैक्ट के लिए कस्टम हुक्स की आवश्यकता नहीं है, इसलिए हम OpenZeppelin द्वारा प्रदान किए गए खाली (empty) इम्प्लीमेंटेशन का उपयोग करेंगे।
ERC20HooksEmptyImpl को शामिल करने के लिए ERC20 import को अपडेट करें:
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
अब कोड सफलतापूर्वक कंपाइल होना चाहिए, और _transfer function अपेक्षित रूप से काम करेगा।
हालाँकि, stake function का इम्प्लीमेंटेशन अधूरा है। किसी उपयोगकर्ता के स्टेक को अपडेट करने से पहले, हमें सबसे पहले उनके जमा हुए रिवॉर्ड्स की गणना करनी होगी। आइए रिवॉर्ड कैलकुलेशन को संभालने के लिए इंटरनल हेल्पर functions बनाएं।
रिवॉर्ड कैलकुलेशन के लिए इंटरनल हेल्पर functions
कंपोनेंट्स में ऐसे इंटरनल functions हो सकते हैं जो केवल कंपोनेंट के भीतर या उसका उपयोग करने वाले कॉन्ट्रैक्ट द्वारा ही एक्सेस किए जा सकते हैं, जैसे ERC20Component में _transfer। ये functions सार्वजनिक इंटरफ़ेस का हिस्सा नहीं हैं और कॉन्ट्रैक्ट के ABI में दिखाई नहीं देंगे।
हम उन्हें #[generate_trait] एट्रिब्यूट का उपयोग करके परिभाषित करते हैं, जो इंटरनल functions को रखने के लिए स्वचालित रूप से एक trait उत्पन्न करता है। रिवॉर्ड कैलकुलेशन और अपडेट को संभालने के लिए मुख्य इम्प्लीमेंटेशन के नीचे निम्नलिखित इंटरनल इम्प्लीमेंटेशन ब्लॉक जोड़ें:
#[generate_trait]
pub impl InternalImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of InternalTrait<TContractState> {
// Internal functions will go here
}
इससे पहले कि हम रिवॉर्ड कैलकुलेशन हेल्पर्स को लागू करें, हमें एक इनिशियलाइज़र function की आवश्यकता है जो कॉन्ट्रैक्ट डिप्लॉय होने पर प्रारंभिक रिवॉर्ड रेट के साथ कंपोनेंट की प्रारंभिक स्थिति सेट कर सके:
#[generate_trait]
pub impl InternalImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of InternalTrait<TContractState> {
//NEWLY ADDED BELOW //
fn initializer(ref self: ComponentState<TContractState>, initial_reward_rate: u256) {
self.reward_rate.write(initial_reward_rate);
}
//other internal function will go here
}
कंपोनेंट इनिशियलाइज़र्स को समझना
कभी-कभी कंपोनेंट्स को केवल एक बार इनिशियलाइज़ेशन लॉजिक रन करने की आवश्यकता होती है। Solidity में, यह एक कंस्ट्रक्टर के साथ संभव है। जबकि Cairo कॉन्ट्रैक्ट्स भी कंस्ट्रक्टर्स को सपोर्ट करते हैं, कंपोनेंट्स नहीं करते हैं।
इसके बजाय, कंपोनेंट्स इनिशियलाइज़र्स का उपयोग करते हैं: रेगुलर functions जो कॉन्ट्रैक्ट के डिप्लॉय होने पर सेटअप को संभालते हैं। फ्रेमवर्क सिंगल एग्जीक्यूशन को लागू नहीं करता है, इसलिए कन्वेन्शन यह है कि इनिशियलाइज़र्स को केवल कॉन्ट्रैक्ट के कंस्ट्रक्टर से ही कॉल किया जाए। चूंकि कंस्ट्रक्टर्स डिप्लॉयमेंट के दौरान केवल एक बार चलते हैं, इसलिए यह सुनिश्चित करता है कि इनिशियलाइज़र्स को भी केवल एक बार ही कॉल किया जाए।
इस मामले में, OpenZeppelin से Ownable और ERC20 कंपोनेंट्स, हमारे कस्टम StakingComponent के साथ, सभी initializer नामक इनिशियलाइज़र function प्रदान करते हैं। क्योंकि यह एक रेगुलर function है, इसका नाम कुछ भी हो सकता है। initializer नाम एक कन्वेन्शन है।
इन इनिशियलाइज़र्स को कॉन्ट्रैक्ट के कंस्ट्रक्टर से तब कॉल किया जाएगा जब हम लेख में बाद में StakingContract बनाएंगे। किसी कंपोनेंट के इनिशियलाइज़र को कॉल करना भूल जाने से उसकी स्थिति अनइनिशियलाइज़्ड रह जाएगी, जिससे कॉन्ट्रैक्ट लॉजिक टूट सकता है या सुरक्षा कमजोरियां (security vulnerabilities) पैदा हो सकती हैं।
OpenZeppelin में Ownable कंपोनेंट इनिशियलाइज़र का एक स्निपेट इस तरह दिखता है:

Ownable इनिशियलाइज़र प्रारंभिक owner एड्रेस सेट करता है, इसलिए हमारे कंस्ट्रक्टर को एक पैरामीटर के रूप में एक owner एड्रेस लेना होगा।
पेंडिंग रिवॉर्ड्स की गणना करना
इनिशियलाइज़र सेट हो जाने के बाद, हम रिवॉर्ड कैलकुलेशन हेल्पर्स को लागू कर सकते हैं। _calculate_pending_rewards function गणना करता है कि किसी उपयोगकर्ता ने अपनी स्टेक की गई राशि और उनके अंतिम अपडेट के बाद बीते समय के आधार पर कितने रिवॉर्ड अर्जित किए हैं:
fn _calculate_pending_rewards(
self: @ComponentState<TContractState>, user: ContractAddress,
) -> u256 {
let staked = self.staked_balances.read(user);
if staked == 0 {
return self.accumulated_rewards.read(user);
}
let last_update = self.last_update_time.read(user);
let current_time = get_block_timestamp();
if last_update == 0 {
return 0;
}
let time_elapsed = current_time - last_update;
let reward_rate = self.reward_rate.read();
// Calculate new rewards: staked_amount * reward_rate * time_elapsed
let new_rewards = (staked * reward_rate * time_elapsed.into()) / 1000000;
let accumulated = self.accumulated_rewards.read(user);
accumulated + new_rewards
}
फ़ंक्शन सबसे पहले यह जांचता है कि क्या उपयोगकर्ता के पास कोई स्टेक किया गया टोकन है। यदि उनके पास नहीं है, तो यह पिछले स्टेक से उनके जमा हुए रिवॉर्ड्स वापस कर देता है।
फिर यह प्राप्त करता है कि उपयोगकर्ता के रिवॉर्ड अंतिम बार कब अपडेट किए गए थे और वर्तमान ब्लॉक टाइमस्टैम्प क्या है। यदि उपयोगकर्ता ने पहले कभी stake नहीं किया है (उनका last_update_time 0 है), तो फ़ंक्शन 0 लौटाता है क्योंकि गणना करने के लिए अभी तक कोई रिवॉर्ड नहीं है।
फ़ंक्शन अंतिम अपडेट के बाद से बीते समय की गणना करता है और वर्तमान रिवॉर्ड रेट प्राप्त करता है। रिवॉर्ड कैलकुलेशन इस सूत्र का उपयोग करता है: staked_amount * reward_rate * time_elapsed / 1000000। हम 1,000,000 से भाग देते हैं क्योंकि Cairo में कोई फ्लोटिंग-पॉइंट संख्याएँ नहीं हैं। भिन्नात्मक मानों (fractional values) को पूर्णांक (integers) के रूप में दर्शाने के लिए रिवॉर्ड रेट को 1,000,000 द्वारा स्केल किया गया है।
इसके बाद फ़ंक्शन किसी भी पूर्व संचित रिवॉर्ड को नए परिकलित रिवॉर्ड में जोड़ देता है और कुल लौटाता है।
हमें मौजूदा imports के साथ मॉड्यूल के शीर्ष पर get_block_timestamp import करने की आवश्यकता है:
use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address};
अब आइए update_rewards function लागू करें जो किसी उपयोगकर्ता के लिए किसी भी पेंडिंग रिवॉर्ड की गणना करने के लिए _calculate_pending_rewards का उपयोग करता है, उनके जमा हुए रिवॉर्ड्स और अंतिम अपडेट टाइमस्टैम्प को अपडेट करता है:
fn update_rewards(ref self: ComponentState<TContractState>, user: ContractAddress) {
let pending = self._calculate_pending_rewards(user);
self.accumulated_rewards.write(user, pending);
self.last_update_time.write(user, get_block_timestamp());
}
update_rewards लागू होने के साथ, हम वापस जा सकते हैं और उपयोगकर्ता के स्टेक को बदलने से पहले रिवॉर्ड अपडेट जोड़कर stake function को पूरा कर सकते हैं।
stake() function को पूरा करना
update_rewards कॉल को शामिल करने के लिए stake function को अपडेट करें:
fn stake(ref self: ComponentState<TContractState>, amount: u256) {
assert(amount > 0, 'Amount must be greater than 0');
let caller = get_caller_address();
let contract_address = get_contract_address();
// Update rewards before changing stake
self.update_rewards(caller); //ADD THIS LINE
// Transfer tokens from caller to contract
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
erc20._transfer(caller, contract_address, amount);
// Update staking balances
let current_stake = self.staked_balances.read(caller);
self.staked_balances.write(caller, current_stake + amount);
let total = self.total_staked.read();
self.total_staked.write(total + amount);
self.emit(Staked { user: caller, amount });
}
इसमें जोड़ी गई लाइन self.update_rewards(caller) है जिसे हम उपयोगकर्ता के स्टेक किए गए बैलेंस को बदलने से पहले कॉल करते हैं। यह सुनिश्चित करता है कि नए टोकन जोड़े जाने से पहले उपयोगकर्ता के पिछले स्टेक के आधार पर रिवॉर्ड की गणना की जाती है। इसके बिना, उपयोगकर्ता अपने पिछले स्टेक पर अर्जित रिवॉर्ड खो देंगे।
यहाँ तक का पूरा StakingComponent कोड इस प्रकार है:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IStaking<TContractState> {
// User functions
fn stake(ref self: TContractState, amount: u256);
fn unstake(ref self: TContractState, amount: u256);
fn claim_rewards(ref self: TContractState);
// View functions
fn get_staked_balance(self: @TContractState, user: ContractAddress) -> u256;
fn get_total_staked(self: @TContractState) -> u256;
fn calculate_rewards(self: @TContractState, user: ContractAddress) -> u256;
fn get_reward_rate(self: @TContractState) -> u256;
// Admin function
fn set_reward_rate(ref self: TContractState, rate: u256);
}
#[starknet::component]
pub mod StakingComponent {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc20::ERC20Component::InternalTrait as ERC20InternalTrait;
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address};
#[storage]
pub struct Storage {
staked_balances: Map<ContractAddress, u256>,
total_staked: u256,
reward_rate: u256,
last_update_time: Map<ContractAddress, u64>,
accumulated_rewards: Map<ContractAddress, u256>,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Staked: Staked,
Unstaked: Unstaked,
}
#[derive(Drop, starknet::Event)]
pub struct Staked {
pub user: ContractAddress,
pub amount: u256,
}
#[derive(Drop, starknet::Event)]
pub struct Unstaked {
pub user: ContractAddress,
pub amount: u256,
}
#[embeddable_as(StakingImpl)]
impl StakingImplImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of super::IStaking<ComponentState<TContractState>> {
/// @notice Stakes tokens into the contract and update rewards
/// @param amount The amount of tokens to stake
fn stake(ref self: ComponentState<TContractState>, amount: u256) {
assert(amount > 0, 'Amount must be greater than 0');
let caller = get_caller_address();
let contract_address = get_contract_address();
// Update rewards before changing stake
self.update_rewards(caller);
// This transfers from caller to contract without needing approval
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
erc20._transfer(caller, contract_address, amount);
// Update staking balances
let current_stake = self.staked_balances.read(caller);
self.staked_balances.write(caller, current_stake + amount);
let total = self.total_staked.read();
self.total_staked.write(total + amount);
self.emit(Staked { user: caller, amount });
}
fn unstake(
ref self: ComponentState<TContractState>, amount: u256,
) { // Implementation will go here
}
fn claim_rewards(ref self: ComponentState<TContractState>) { // Implementation will go here
}
fn get_staked_balance(
self: @ComponentState<TContractState>, user: ContractAddress,
) -> u256 {
0 // Placeholder
}
fn get_total_staked(self: @ComponentState<TContractState>) -> u256 {
0 // Placeholder
}
fn calculate_rewards(self: @ComponentState<TContractState>, user: ContractAddress) -> u256 {
0 // Placeholder
}
fn set_reward_rate(
ref self: ComponentState<TContractState>, rate: u256,
) { // Implementation will go here
}
fn get_reward_rate(self: @ComponentState<TContractState>) -> u256 {
0 // Placeholder
}
}
#[generate_trait]
pub impl InternalImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of InternalTrait<TContractState> {
//NEWLY ADDED BELOW //
fn initializer(ref self: ComponentState<TContractState>, initial_reward_rate: u256) {
self.reward_rate.write(initial_reward_rate);
}
fn _calculate_pending_rewards(
self: @ComponentState<TContractState>, user: ContractAddress,
) -> u256 {
let staked = self.staked_balances.read(user);
if staked == 0 {
return self.accumulated_rewards.read(user);
}
let last_update = self.last_update_time.read(user);
let current_time = get_block_timestamp();
if last_update == 0 {
return 0;
}
let time_elapsed = current_time - last_update;
let reward_rate = self.reward_rate.read();
// Calculate new rewards: staked_amount * reward_rate * time_elapsed
let new_rewards = (staked * reward_rate * time_elapsed.into()) / 1000000;
let accumulated = self.accumulated_rewards.read(user);
accumulated + new_rewards
}
fn update_rewards(ref self: ComponentState<TContractState>, user: ContractAddress) {
let pending = self._calculate_pending_rewards(user);
self.accumulated_rewards.write(user, pending);
self.last_update_time.write(user, get_block_timestamp());
}
}
}
unstake function को लागू करना
unstake function उपयोगकर्ताओं को कॉन्ट्रैक्ट से अपने स्टेक किए गए टोकन वापस निकालने (withdraw) की अनुमति देता है। आइए इसे लागू करें:
fn unstake(ref self: ComponentState<TContractState>, amount: u256) {
let caller = get_caller_address();
let current_stake = self.staked_balances.read(caller);
assert(amount > 0, 'Amount must be greater than 0');
assert(current_stake >= amount, 'Insufficient staked balance');
// Update rewards before changing stake
self.update_rewards(caller);
// Update staking balances
self.staked_balances.write(caller, current_stake - amount);
let total = self.total_staked.read();
self.total_staked.write(total - amount);
// Transfer tokens back to user using ERC20Component
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
let contract_address = get_contract_address();
erc20._transfer(contract_address, caller, amount);
self.emit(Unstaked { user: caller, amount });
}
फ़ंक्शन सबसे पहले कॉलर का एड्रेस और उनका वर्तमान स्टेक किया गया बैलेंस प्राप्त करता है। फिर यह राशि को वैलिडेट करता है और पुष्टि करता है कि उपयोगकर्ता के पास पर्याप्त स्टेक किए गए टोकन हैं।
स्टेक फ़ंक्शन की तरह ही, हम उपयोगकर्ता के स्टेक किए गए बैलेंस को संशोधित करने से पहले self.update_rewards(caller) को कॉल करते हैं। यह सुनिश्चित करता है कि विथड्रॉल से पहले उनके स्टेक के आधार पर रिवॉर्ड की गणना की जाती है।
रिवॉर्ड्स को अपडेट करने के बाद, फ़ंक्शन उपयोगकर्ता के स्टेक किए गए बैलेंस और कुल स्टेक की गई राशि को कम कर देता है। फिर यह ERC20Component के _transfer function का उपयोग करके टोकन को कॉन्ट्रैक्ट से वापस उपयोगकर्ता को ट्रांसफर कर देता है और एक Unstaked इवेंट एमिट करता है।
claim rewards function को लागू करना
claim_rewards function उपयोगकर्ताओं को अपने टोकन को कॉन्ट्रैक्ट में स्टेक रखते हुए अपने जमा हुए रिवॉर्ड्स का क्लेम करने की अनुमति देता है:
fn claim_rewards(ref self: ComponentState<TContractState>) {
let caller = get_caller_address();
self.update_rewards(caller);
let rewards = self.accumulated_rewards.read(caller);
assert(rewards > 0, 'No rewards to claim');
// Reset accumulated rewards
self.accumulated_rewards.write(caller, 0);
// Transfer reward tokens to user
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
let contract_address = get_contract_address();
erc20._transfer(contract_address, caller, rewards);
}
claim_rewards function सबसे पहले उपयोगकर्ता के रिवॉर्ड्स को अपडेट करता है ताकि यह सुनिश्चित हो सके कि सभी पेंडिंग रिवॉर्ड्स की गणना हो गई है। फिर यह उपयोगकर्ता के कुल जमा हुए रिवॉर्ड्स को पढ़ता है।
फ़ंक्शन वैलिडेट करता है कि उपयोगकर्ता के पास क्लेम करने के लिए रिवॉर्ड हैं, उनके जमा हुए रिवॉर्ड्स को शून्य पर रीसेट करता है, और _transfer का उपयोग करके रिवॉर्ड टोकन उपयोगकर्ता को ट्रांसफर कर देता है।
set reward rate function को लागू करना
set_reward_rate function कॉन्ट्रैक्ट owner को रिवॉर्ड रेट अपडेट करने की अनुमति देता है:
fn set_reward_rate(ref self: ComponentState<TContractState>, rate: u256) {
// Check ownership using OwnableComponent
let ownable = get_dep_component!(@self, Ownable);
ownable.assert_only_owner();
self.reward_rate.write(rate);
}
यह function एक अन्य कंपोनेंट-से-कंपोनेंट इंटरेक्शन को भी प्रदर्शित करता है। हम OwnableComponent तक पहुँचने के लिए get_dep_component!(@self, Ownable) का उपयोग करते हैं और इसके assert_only_owner function को कॉल करते हैं। यह फ़ंक्शन यदि कॉलर कॉन्ट्रैक्ट owner नहीं है तो पैनिक (panic) करेगा, जिससे अनधिकृत उपयोगकर्ताओं को रिवॉर्ड रेट बदलने से रोका जा सकेगा।
ध्यान दें कि हम यहां get_dep_component! (बिना _mut के) का उपयोग करते हैं, न कि get_dep_component_mut! का जिसका उपयोग हमने ERC20Component के साथ किया था। अंतर यह है:
get_dep_component_mut!एक म्यूटेबल रेफरेंस प्रदान करता है - इसका उपयोग तब करें जब आपको कंपोनेंट की स्थिति को संशोधित करने की आवश्यकता हो (जैसे टोकन ट्रांसफर करना)get_dep_component!एक इम्यूटेबल रेफरेंस प्रदान करता है - इसका उपयोग तब करें जब आपको केवल डेटा पढ़ने या उन functions को कॉल करने की आवश्यकता हो जो स्थिति को संशोधित नहीं करते हैं (जैसे ओनरशिप की जांच करना)
चूंकि assert_only_owner केवल owner एड्रेस को पढ़ता है और OwnableComponent की स्थिति को संशोधित नहीं करता है, इसलिए हम इम्यूटेबल वर्जन का उपयोग करते हैं।
यदि आप अब कोड को कंपाइल करते हैं, तो आपको एक एरर मिलेगा:
Method 'assert_only_owner' not found on type '@openzeppelin_access::ownable::owna
ble::OwnableComponent::ComponentState::<TContractState>'. Consider importing one
of the following traits: 'OwnableComponent::InternalTrait'
इसका मतलब है कि हमें मॉड्यूल के शीर्ष पर OwnableComponent के इंटरनल trait को import करने की आवश्यकता है:
use openzeppelin::access::ownable::OwnableComponent::InternalTrait as OwnableInternalTrait;
ओनरशिप चेक पास होने के बाद, फ़ंक्शन स्टोरेज में रिवॉर्ड रेट को अपडेट कर देता है।
view functions को लागू करना
हमें चार view functions लागू करने की आवश्यकता है:
get_staked_balanceयह लौटाता है कि किसी विशिष्ट उपयोगकर्ता ने कितने टोकन stake किए हैं,get_total_stakedसभी उपयोगकर्ताओं द्वारा स्टेक की गई कुल राशि लौटाता है,calculate_rewardsयह दिखाता है कि एक उपयोगकर्ता कितने रिवॉर्ड्स का क्लेम कर सकता है, औरget_reward_rateवर्तमान रिवॉर्ड रेट लौटाता है।
fn get_staked_balance(
self: @ComponentState<TContractState>, user: ContractAddress
) -> u256 {
self.staked_balances.read(user)
}
fn get_total_staked(self: @ComponentState<TContractState>) -> u256 {
self.total_staked.read()
}
fn calculate_rewards(
self: @ComponentState<TContractState>, user: ContractAddress
) -> u256 {
self._calculate_pending_rewards(user)
}
fn get_reward_rate(self: @ComponentState<TContractState>) -> u256 {
self.reward_rate.read()
}
ये इम्प्लीमेंटेशन्स स्टोरेज से पढ़ते हैं और अनुरोधित मान (requested values) लौटाते हैं। calculate_rewards function रिवॉर्ड्स की गणना करने के लिए इंटरनल _calculate_pending_rewards function का उपयोग करता है।
यहाँ पूरा StakingComponent कोड है:
use starknet::ContractAddress;
#[starknet::interface]
pub trait IStaking<TContractState> {
// User functions
fn stake(ref self: TContractState, amount: u256);
fn unstake(ref self: TContractState, amount: u256);
fn claim_rewards(ref self: TContractState);
// View functions
fn get_staked_balance(self: @TContractState, user: ContractAddress) -> u256;
fn get_total_staked(self: @TContractState) -> u256;
fn calculate_rewards(self: @TContractState, user: ContractAddress) -> u256;
fn get_reward_rate(self: @TContractState) -> u256;
// Admin function
fn set_reward_rate(ref self: TContractState, rate: u256);
}
#[starknet::component]
pub mod StakingComponent {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::access::ownable::OwnableComponent::InternalTrait as OwnableInternalTrait;
use openzeppelin::token::erc20::ERC20Component::InternalTrait as ERC20InternalTrait;
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::storage::{
Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address};
#[storage]
pub struct Storage {
staked_balances: Map<ContractAddress, u256>,
total_staked: u256,
reward_rate: u256,
last_update_time: Map<ContractAddress, u64>,
accumulated_rewards: Map<ContractAddress, u256>,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
Staked: Staked,
Unstaked: Unstaked
}
#[derive(Drop, starknet::Event)]
pub struct Staked {
pub user: ContractAddress,
pub amount: u256,
}
#[derive(Drop, starknet::Event)]
pub struct Unstaked {
pub user: ContractAddress,
pub amount: u256,
}
#[embeddable_as(StakingImpl)]
impl StakingImplImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of super::IStaking<ComponentState<TContractState>> {
/// @notice Stakes tokens into the contract and update rewards
/// @param amount The amount of tokens to stake
fn stake(ref self: ComponentState<TContractState>, amount: u256) {
assert(amount > 0, 'Amount must be greater than 0');
let caller = get_caller_address();
let contract_address = get_contract_address();
// Update rewards before changing stake
self.update_rewards(caller);
// This transfers from caller to contract without needing approval
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
erc20._transfer(caller, contract_address, amount);
// Update staking balances
let current_stake = self.staked_balances.read(caller);
self.staked_balances.write(caller, current_stake + amount);
let total = self.total_staked.read();
self.total_staked.write(total + amount);
self.emit(Staked { user: caller, amount });
}
/// @notice Unstakes tokens and transfers them back to user
/// @param amount The amount of tokens to unstake
fn unstake(ref self: ComponentState<TContractState>, amount: u256) {
let caller = get_caller_address();
let current_stake = self.staked_balances.read(caller);
assert(amount > 0, 'Amount must be greater than 0');
assert(current_stake >= amount, 'Insufficient staked balance');
// Update rewards before changing stake
self.update_rewards(caller);
// Update staking balances
self.staked_balances.write(caller, current_stake - amount);
let total = self.total_staked.read();
self.total_staked.write(total - amount);
// Transfer tokens back to user using ERC20Component
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
let contract_address = get_contract_address();
erc20._transfer(contract_address, caller, amount);
self.emit(Unstaked { user: caller, amount });
}
/// @notice Claims accumulated rewards for the caller
fn claim_rewards(ref self: ComponentState<TContractState>) {
let caller = get_caller_address();
self.update_rewards(caller);
let rewards = self.accumulated_rewards.read(caller);
assert(rewards > 0, 'No rewards to claim');
// Reset accumulated rewards
self.accumulated_rewards.write(caller, 0);
// Transfer reward tokens to user
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
let contract_address = get_contract_address();
erc20._transfer(contract_address, caller, rewards);
}
/// @notice Returns the staked balance of a specific user
/// @param user The address of the user
/// @return The amount of tokens staked by the user
fn get_staked_balance(
self: @ComponentState<TContractState>, user: ContractAddress,
) -> u256 {
self.staked_balances.read(user)
}
/// @notice Returns the total amount of tokens staked in the contract
/// @return The total staked amount
fn get_total_staked(self: @ComponentState<TContractState>) -> u256 {
self.total_staked.read()
}
/// @notice Calculates the pending rewards for a user
/// @param user The address of the user
/// @return The amount of pending rewards
fn calculate_rewards(self: @ComponentState<TContractState>, user: ContractAddress) -> u256 {
self._calculate_pending_rewards(user)
}
/// @notice Sets the reward rate (only callable by owner)
/// @param rate The new reward rate per second
fn set_reward_rate(ref self: ComponentState<TContractState>, rate: u256) {
// Check ownership using OwnableComponent
let ownable = get_dep_component!(@self, Ownable);
ownable.assert_only_owner();
self.reward_rate.write(rate);
}
/// @notice Returns the current reward rate
/// @return The reward rate per second
fn get_reward_rate(self: @ComponentState<TContractState>) -> u256 {
self.reward_rate.read()
}
}
#[generate_trait]
pub impl InternalImpl<
TContractState,
+HasComponent<TContractState>,
+Drop<TContractState>,
impl ERC20: ERC20Component::HasComponent<TContractState>,
impl Ownable: OwnableComponent::HasComponent<TContractState>,
> of InternalTrait<TContractState> {
// Initializes the staking component with an initial reward rate
fn initializer(ref self: ComponentState<TContractState>, initial_reward_rate: u256) {
self.reward_rate.write(initial_reward_rate);
}
// Updates the accumulated rewards for a user
fn update_rewards(ref self: ComponentState<TContractState>, user: ContractAddress) {
let pending = self._calculate_pending_rewards(user);
self.accumulated_rewards.write(user, pending);
self.last_update_time.write(user, get_block_timestamp());
}
// Calculates pending rewards based on staked amount and time elapsed
fn _calculate_pending_rewards(
self: @ComponentState<TContractState>, user: ContractAddress,
) -> u256 {
let staked = self.staked_balances.read(user);
if staked == 0 {
return self.accumulated_rewards.read(user);
}
let last_update = self.last_update_time.read(user);
let current_time = get_block_timestamp();
if last_update == 0 {
return 0;
}
let time_elapsed = current_time - last_update;
let reward_rate = self.reward_rate.read();
// Calculate new rewards: staked_amount * reward_rate * time_elapsed
let new_rewards = (staked * reward_rate * time_elapsed.into()) / 1000000;
let accumulated = self.accumulated_rewards.read(user);
accumulated + new_rewards
}
}
}
अब हमने StakingComponent पूरा कर लिया है, जिसमें वह सभी स्टेकिंग लॉजिक है जिसकी हमें आवश्यकता है। कंपोनेंट टोकन को स्टेक और अनस्टेक करने, रिवॉर्ड्स की गणना करने और क्लेम करने, और रिवॉर्ड रेट को प्रबंधित करने का काम संभालता है।
हालाँकि, किसी कंपोनेंट को अपने आप डिप्लॉय नहीं किया जा सकता है। हमें एक कॉन्ट्रैक्ट बनाना होगा जो हमारे StakingComponent को ERC20Component और OwnableComponent के साथ इंटीग्रेट करता है। यह कॉन्ट्रैक्ट एक डिप्लॉय करने योग्य स्मार्ट कॉन्ट्रैक्ट के रूप में काम करेगा जिसके साथ उपयोगकर्ता इंटरैक्ट करेंगे।
Staking Contract बनाना
StakingContract करेगा:
- उस
StakingComponentको शामिल करें जिसे हमने अभी बनाया है ERC20Componentको शामिल करें (स्टेकिंग टोकन के लिए)OwnableComponentको शामिल करें (एक्सेस कंट्रोल के लिए)- आवश्यक मापदंडों (parameters) के साथ सभी तीन कंपोनेंट्स को इनिशियलाइज़ करें
- उन functions को एक्सपोज़ करें जिन्हें हम चाहते हैं कि उपयोगकर्ता कॉल कर सकें
आइए StakingContract बनाएं जो इन सभी कंपोनेंट्स को एक साथ लाता है।
डिपेंडेंसीज़ को Import करना
नोट: इस वॉकथ्रू के लिए, हम StakingComponent और StakingContract दोनों को एक ही lib.cairo फ़ाइल में बना रहे हैं। एक बड़े प्रोजेक्ट में, आप इन्हें अलग-अलग फ़ाइलों (जैसे, staking_component.cairo और staking_contract.cairo) में व्यवस्थित कर सकते हैं, लेकिन सब कुछ एक फ़ाइल में रखने से इसे समझना आसान हो जाता है।
सबसे पहले, हमें उन सभी कंपोनेंट्स को import करने की आवश्यकता है जिनका हम उपयोग करेंगे:
#[starknet::contract]
mod StakingContract {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::{ContractAddress, get_contract_address};
use super::StakingComponent;
}
हम OpenZeppelin से OwnableComponent और ERC20Component को, हमारे द्वारा अभी बनाए गए StakingComponent के साथ import करते हैं।
कंपोनेंट्स को डिक्लेयर करना
इसके बाद, हम उन तीन कंपोनेंट्स को डिक्लेयर करते हैं जिनका उपयोग हमारा कॉन्ट्रैक्ट करेगा:
// Component declarations
component!(path: StakingComponent, storage: staking, event: StakingEvent);
component!(path: ERC20Component, storage: erc20, event: ERC20Event);
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
प्रत्येक component! मैक्रो एक कंपोनेंट डिक्लेयर करता है और निर्दिष्ट करता है:
- कंपोनेंट पाथ (किस कंपोनेंट का उपयोग करना है)
- स्टोरेज नाम (इस कंपोनेंट का स्टोरेज कहाँ रखा जाएगा)
- इवेंट का नाम (इस कंपोनेंट के इवेंट्स को क्या कहा जाए)
ERC20Component को कॉन्फ़िगर करना
ERC20Component के लिए आवश्यक है कि हम इसके ImmutableConfig trait को लागू करें। यह trait उन मानों (values) को कॉन्फ़िगर करता है जो कॉन्ट्रैक्ट स्टोरेज में स्टोर होने के बजाय कंपाइल टाइम पर फिक्स होते हैं। ERC20 DECIMALS मान डिप्लॉयमेंट के बाद कभी नहीं बदलता है, जिससे यह इस पैटर्न के लिए आदर्श बन जाता है।
// ERC20 Mixin configuration
impl ERC20ImmutableConfig of ERC20Component::ImmutableConfig {
const DECIMALS: u8 = 18;
}
यह टोकन को 18 डेसिमल्स का उपयोग करने के लिए सेट करता है, जो अधिकांश टोकन के लिए मानक (standard) है।
कंपोनेंट functions को एक्सपोज़ करना
अब हमें यह तय करने की आवश्यकता है कि प्रत्येक कंपोनेंट के कौन से functions सार्वजनिक रूप से एक्सेसिबल होने चाहिए। हम इम्प्लीमेंटेशन्स को एक्सपोज़ करने के लिए #[abi(embed_v0)] एट्रिब्यूट का उपयोग करते हैं:
// Expose staking functions publicly
#[abi(embed_v0)]
impl StakingImpl = StakingComponent::StakingImpl<ContractState>;
// Expose ERC20 functions publicly
#[abi(embed_v0)]
impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
// Expose Ownable functions publicly
#[abi(embed_v0)]
impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>;
ये पंक्तियाँ सभी स्टेकिंग functions, ERC-20 functions (जैसे transfer, balance_of), और ओनरशिप functions (जैसे transfer_ownership) को कॉन्ट्रैक्ट के सार्वजनिक इंटरफ़ेस (ABI) में उपलब्ध कराती हैं।
हमें इंटरनल इम्प्लीमेंटेशन्स को भी उपलब्ध कराने की आवश्यकता है ताकि कंपोनेंट्स एक-दूसरे के इंटरनल functions को कॉल कर सकें:
// Internal implementations
impl StakingInternalImpl = StakingComponent::InternalImpl<ContractState>;
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
स्टोरेज स्ट्रक्चर
प्रत्येक कंपोनेंट को अपने स्वयं के स्टोरेज स्पेस की आवश्यकता होती है। हम तीनों कंपोनेंट्स के लिए स्टोरेज डिक्लेयर करते हैं:
#[storage]
struct Storage {
#[substorage(v0)]
staking: StakingComponent::Storage,
#[substorage(v0)]
erc20: ERC20Component::Storage,
#[substorage(v0)]
ownable: OwnableComponent::Storage,
}
#[substorage(v0)] एट्रिब्यूट Cairo को बताता है कि प्रत्येक फ़ील्ड में एक कंपोनेंट का स्टोरेज स्ट्रक्चर है।
इवेंट्स
इसी तरह, हमें सभी कंपोनेंट्स से इवेंट्स को एग्रीगेट करने की आवश्यकता है:
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
#[flat]
StakingEvent: StakingComponent::Event,
#[flat]
ERC20Event: ERC20Component::Event,
#[flat]
OwnableEvent: OwnableComponent::Event,
}
कंस्ट्रक्टर
कॉन्ट्रैक्ट डिप्लॉय होने पर कंस्ट्रक्टर तीनों कंपोनेंट्स को इनिशियलाइज़ करता है:
#[constructor]
fn constructor(
ref self: ContractState,
owner: ContractAddress,
token_name: ByteArray,
token_symbol: ByteArray,
initial_reward_rate: u256,
initial_supply: u256,
) {
// Initialize ownership
self.ownable.initializer(owner);
// Initialize the ERC20 token
self.erc20.initializer(token_name, token_symbol);
// Mint initial supply to the contract for rewards
self.erc20.mint(get_contract_address(), initial_supply);
// Initialize staking with reward rate
self.staking.initializer(initial_reward_rate);
}
कंस्ट्रक्टर तीनों कंपोनेंट्स के लिए आवश्यक पैरामीटर लेता है: owner का एड्रेस, टोकन का नाम और सिंबल, प्रारंभिक रिवॉर्ड रेट और प्रारंभिक टोकन सप्लाई। यह प्रत्येक कंपोनेंट को इनिशियलाइज़ करता है और प्रारंभिक सप्लाई को कॉन्ट्रैक्ट में ही मिंट करता है। यह सुनिश्चित करता है कि जब उपयोगकर्ता stake और क्लेम करते हैं तो कॉन्ट्रैक्ट के पास रिवॉर्ड के रूप में भुगतान करने के लिए टोकन उपलब्ध हों।
एक मिंट function जोड़ना
हम owner को टोकन मिंट करने की अनुमति देने के लिए एक अतिरिक्त function जोड़ते हैं:
#[generate_trait]
#[abi(per_item)]
impl ExternalImpl of ExternalTrait {
#[external(v0)]
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) {
self.ownable.assert_only_owner();
self.erc20.mint(recipient, amount);
}
}
यह function जांचता है कि कॉलर owner है या नहीं, फिर निर्दिष्ट प्राप्तकर्ता (recipient) को टोकन मिंट करता है।
यहाँ पूरा StakingContract है:
#[starknet::contract]
mod StakingContract {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::{ContractAddress, get_contract_address};
use super::StakingComponent;
// Component declarations
component!(path: StakingComponent, storage: staking, event: StakingEvent);
component!(path: ERC20Component, storage: erc20, event: ERC20Event);
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
// ERC20 Mixin configuration
impl ERC20ImmutableConfig of ERC20Component::ImmutableConfig {
const DECIMALS: u8 = 18;
}
// Expose staking functions publicly
#[abi(embed_v0)]
impl StakingImpl = StakingComponent::StakingImpl<ContractState>;
// Expose ERC20 functions publicly
#[abi(embed_v0)]
impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
// Expose Ownable functions publicly
#[abi(embed_v0)]
impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>;
// Internal implementations
impl StakingInternalImpl = StakingComponent::InternalImpl<ContractState>;
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
staking: StakingComponent::Storage,
#[substorage(v0)]
erc20: ERC20Component::Storage,
#[substorage(v0)]
ownable: OwnableComponent::Storage,
}
#[event]
#[derive(Drop, starknet::Event)]
pub enum Event {
#[flat]
StakingEvent: StakingComponent::Event,
#[flat]
ERC20Event: ERC20Component::Event,
#[flat]
OwnableEvent: OwnableComponent::Event,
}
#[constructor]
fn constructor(
ref self: ContractState,
owner: ContractAddress,
token_name: ByteArray,
token_symbol: ByteArray,
initial_reward_rate: u256,
initial_supply: u256,
) {
// Initialize ownership
self.ownable.initializer(owner);
// Initialize the ERC20 token
self.erc20.initializer(token_name, token_symbol);
// Mint initial supply to the contract for rewards
self.erc20.mint(get_contract_address(), initial_supply);
// Initialize staking with reward rate
self.staking.initializer(initial_reward_rate);
}
#[generate_trait]
#[abi(per_item)]
impl ExternalImpl of ExternalTrait {
#[external(v0)]
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) {
self.ownable.assert_only_owner();
self.erc20.mint(recipient, amount);
}
}
}
हम transfer_from के बजाय ERC20Component के _transfer का उपयोग क्यों करते हैं
जबकि transfer_from एप्रूव्ड टोकन ट्रांसफर के लिए मानक (standard) ERC-20 function है, यह एक ही कॉन्ट्रैक्ट के भीतर कंपोनेंट-से-कंपोनेंट कॉल्स में सही ढंग से काम नहीं करता है।
एक सामान्य स्टेकिंग कॉन्ट्रैक्ट में, स्टेकिंग कॉन्ट्रैक्ट और टोकन अलग-अलग कॉन्ट्रैक्ट होते हैं। जब कोई उपयोगकर्ता STRK टोकन stake करता है:
- उपयोगकर्ता स्टेकिंग कॉन्ट्रैक्ट को एप्रूव करता है:
token.approve(staking_contract, amount) - स्टेकिंग कॉन्ट्रैक्ट कॉल करता है:
token.transfer_from(user, staking_contract, amount) - टोकन कॉन्ट्रैक्ट के अंदर,
transfer_from,get_caller_address()को कॉल करता है जो स्टेकिंग कॉन्ट्रैक्ट का एड्रेस लौटाता है - टोकन जांचता है:
allowances[user][staking_contract] >= amount - यदि एप्रूव हो जाता है, तो टोकन ट्रांसफर हो जाते हैं
यह इसलिए काम करता है क्योंकि टोकन कॉन्ट्रैक्ट के अंदर get_caller_address() स्टेकिंग कॉन्ट्रैक्ट (एक्सटर्नल कॉलर) लौटाता है, जो एप्रूवल से मेल खाता है।
कंपोनेंट-से-कंपोनेंट कॉल्स में क्या होता है
हमारे एम्बेडेड आर्किटेक्चर में, StakingComponent और ERC20Component एक ही कॉन्ट्रैक्ट का हिस्सा हैं। जब StakingComponent, ERC20Component को कॉल करता है, तो यह एक इंटरनल कॉल होती है, एक्सटर्नल नहीं।
ध्यान देने योग्य महत्वपूर्ण व्यवहार यह है कि get_caller_address() मूल एक्सटर्नल कॉलर का एड्रेस बनाए रखता है, तब भी जब उसे किसी कंपोनेंट के भीतर से कॉल किया जाता है। जब कोई उपयोगकर्ता stake() कॉल करता है:
- एक्सटर्नल कॉल कॉन्ट्रैक्ट में प्रवेश करती है:
get_caller_address()User लौटाता है - StakingComponent.stake() एग्जीक्यूट होता है:
get_caller_address()User लौटाता है - ERC20Component.transfer_from() एग्जीक्यूट होता है:
get_caller_address()अभी भी User लौटाता है
इसलिए transfer_from, allowances[user][contract] के बजाय allowances[user][user] की जांच करता है, जो काम नहीं करेगा।
कंपोनेंट कॉल्स इंटरनल डिस्पैच हैं, एक्सटर्नल कॉन्ट्रैक्ट कॉल्स नहीं, इसलिए कॉल चेन में कोई मध्यवर्ती (intermediate) कॉन्ट्रैक्ट एड्रेस नहीं होता है।
कंपोनेंट कॉल्स के लिए _transfer का उपयोग करना
_transfer इंटरनल function एलाउंस मैकेनिज्म को बायपास करता है और सीधे टोकन ट्रांसफर करता है:
fn stake(ref self: ComponentState<TContractState>, amount: u256) {
let mut erc20 = get_dep_component_mut!(ref self, ERC20);
erc20._transfer(caller, contract_address, amount);
// No allowance check needed
}
यह काम करता है क्योंकि जैसा कि पहले बताया गया है, हमारा कॉन्ट्रैक्ट ही टोकन है; एक ही कॉन्ट्रैक्ट टोकन बैलेंस और ट्रांसफर (ERC20Component) और स्टेकिंग लॉजिक (StakingComponent) दोनों को नियंत्रित करता है। _transfer function एप्रूवल मैकेनिज्म को पूरी तरह से बायपास कर देता है, इसलिए उपयोगकर्ताओं को स्टेक करने से पहले approve() कॉल करने की आवश्यकता नहीं होती है।
बाहरी (external) टोकन स्टेक करना
एक सामान्य स्टेकिंग कॉन्ट्रैक्ट में, आप एम्बेडेड ERC20Component का उपयोग करने के बजाय बाहरी (external) टोकन स्टेक करेंगे। इस मानक (standard) पैटर्न के साथ, आप stake() function में बाहरी टोकन के डिस्पैचर (IERC20Dispatcher { contract_address: token_address }) का उपयोग करते हैं, उस बाहरी कॉन्ट्रैक्ट पर transfer_from को कॉल करते हैं, और एप्रूवल मैकेनिज्म सही ढंग से काम करता है क्योंकि यह एक वास्तविक एक्सटर्नल कॉल है।
transfer_from एक्सटर्नल कॉन्ट्रैक्ट-से-कॉन्ट्रैक्ट कॉल्स के लिए है, एक ही कॉन्ट्रैक्ट के भीतर कंपोनेंट-से-कंपोनेंट कॉल्स के लिए नहीं। जब कंपोनेंट्स एक ही कॉन्ट्रैक्ट के भीतर इंटरैक्ट करते हैं, तो _transfer जैसे इंटरनल functions का उपयोग करें जो ऑथराइजेशन चेक के लिए get_caller_address() पर निर्भर नहीं करते हैं। एक्सटर्नल कॉन्ट्रैक्ट इंटरैक्शन और इंटरनल कंपोनेंट इंटरैक्शन के बीच एग्जीक्यूशन कॉन्टेक्स्ट और कॉलर सिमेंटिक्स भिन्न होते हैं, भले ही वे एक ही इंटरफ़ेस लागू करते हों।
निष्कर्ष
कंपोनेंट-से-कंपोनेंट इंटरैक्शन हमें पुन: प्रयोज्य (reusable) लॉजिक बनाने की अनुमति देता है जो अन्य कंपोनेंट्स पर निर्भर करता है। डिपेंडेंसीज़ को स्पष्ट रूप से डिक्लेयर करके, एक कंपोनेंट कॉन्ट्रैक्ट के माध्यम से कॉल्स को रूट करने के बजाय दूसरे के functions को सीधे कॉल कर सकता है।
इस ट्यूटोरियल में, हमने एक StakingComponent बनाया है जो ERC20Component और OwnableComponent पर डिपेंडेंसीज़ डिक्लेयर करता है। निर्भर कंपोनेंट्स को डिक्लेयर करने और कॉल करने का यह पैटर्न मॉड्यूलर, कंपोजेबल स्मार्ट कॉन्ट्रैक्ट्स बनाने के लिए मूलभूत (fundamental) है।
जैसा कि पहले बताया गया है, इस ट्यूटोरियल में हमने जिस एम्बेडेड टोकन दृष्टिकोण का उपयोग किया है, वह मुख्य रूप से प्रदर्शन उद्देश्यों (demonstration purposes) के लिए है; प्रोडक्शन स्टेकिंग कॉन्ट्रैक्ट्स आमतौर पर बाहरी (external) टोकन पैटर्न का उपयोग करते हैं।
कंपोनेंट्स तब सबसे अच्छा काम करते हैं जब हर एक किसी विशिष्ट कार्य (specific concern) को संभालता है। जब लॉजिक को कई कंपोनेंट्स के साथ इंटरैक्ट करने की आवश्यकता होती है, तो कोड को व्यवस्थित और पुन: प्रयोज्य रखने के लिए उन डिपेंडेंसीज़ को स्पष्ट रूप से डिक्लेयर करें।