Cairo में Components, Solidity के abstract contracts की तरह व्यवहार करते हैं। वे storage, events और functions को परिभाषित कर सकते हैं और उनके साथ काम कर सकते हैं, लेकिन उन्हें अपने आप डिप्लॉय (deploy) नहीं किया जा सकता है। Components का मुख्य उद्देश्य लॉजिक को अलग करना (उदा. re-usability) है, बिल्कुल उसी तरह जैसे Solidity में abstract contracts करते हैं।
निम्नलिखित Solidity कोड पर विचार करें:
abstract contract C {
uint256 balance;
function increase_balance(uint256 amount) public {
require(amount != 0, "amount cannot be zero");
balance = balance + amount;
}
function get_balance() public view returns (uint256) {
return x;
}
}
contract D is C {
}
कॉन्ट्रैक्ट C को डिप्लॉय नहीं किया जा सकता क्योंकि यह abstract है। हालाँकि, यदि D को डिप्लॉय किया जाता है, तो D में C की सभी कार्यक्षमता (functionality) और स्टेट (state) होगी। विशेष रूप से, D में पब्लिक functions increase_balance() और get() होंगे जो वैसे ही काम करेंगे जैसा C में परिभाषित किया गया है।
D को C के सभी functions, events और storage प्राप्त हो गए हैं।
आज हम जो कॉन्ट्रैक्ट बनाएंगे, वह ऊपर दिखाए गए Solidity कोड का Cairo समतुल्य (equivalent) है।
Minimal Component का उदाहरण
एक खाली डायरेक्टरी बनाएँ और उसके अंदर scarb init रन करें।
नीचे दिए गए कोड को src/lib.cairo में पेस्ट करें। scarb test के साथ जनरेट किए गए टेस्ट रन करें; वे सभी पास होने चाहिए।
यह कोड क्या करता है, यहाँ बताया गया है:
- यह दो functions के साथ एक इंटरफ़ेस डिक्लेयर करता है जो कॉन्ट्रैक्ट में स्टोर किए गए बैलेंस को बढ़ाता है और रिटर्न करता है।
- यह एक component बनाता है जो अपना स्वयं का storage
xपरिभाषित करता है और read/write ऑपरेशन्स का उपयोग करकेincreaseऔरget_balancefunctions को इम्प्लीमेंट करता है। - कॉन्ट्रैक्ट component को इम्पोर्ट करता है, इसके storage और events को रजिस्टर करता है, और इसके ABI के माध्यम से component के इम्प्लीमेंटेशन को एक्सपोज़ (expose) करता है।
कोड के बाद हम समझाएंगे कि ये सब एक साथ कैसे काम करते हैं:
// SAME TRAIT SCARB CREATES BY DEFAULT
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
/// Increase contract balance.
fn increase_balance(ref self: TContractState, amount: felt252);
/// Retrieve contract balance.
fn get_balance(self: @TContractState) -> felt252;
}
// COMPONENT IS NEW
#[starknet::component]
pub mod CounterComponent {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
pub struct Storage {
x: felt252,
}
#[event]
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {}
#[embeddable_as(CounterImplMixin)]
impl CounterImpl<TContractState, +HasComponent<TContractState>> of super::IHelloStarknet<ComponentState<TContractState>> {
fn get_balance(self: @ComponentState<TContractState>) -> felt252 {
self.x.read()
}
fn increase_balance(ref self: ComponentState<TContractState>, amount: felt252) {
assert(amount != 0, 'Amount cannot be 0');
self.x.write(self.x.read() + amount);
}
}
}
// THIS CONTRACT HAS NO FUNCTIONALITY, IT ONLY USES THE COMPONENT
#[starknet::contract]
mod HelloStarknet {
use super::CounterComponent;
component!(path: CounterComponent, storage: counter, event: CounterEvent);
#[abi(embed_v0)]
impl CounterImpl = CounterComponent::CounterImplMixin<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
counter: CounterComponent::Storage,
}
#[event]
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
#[flat]
CounterEvent: CounterComponent::Event,
}
}
उपरोक्त कॉन्ट्रैक्ट का विश्लेषण (Breakdown)
IHelloStarknet इंटरफ़ेस
फ़ाइल के शीर्ष (top) पर मौजूद trait उसी के समान है जो Scarb डिफ़ॉल्ट रूप से बनाता है, इसमें कोई बदलाव नहीं किया गया है।
हमने इसे नहीं बदला क्योंकि टेस्ट फ़ाइलें विशेष रूप से इस इंटरफ़ेस को इम्पोर्ट करती हैं। एक अलग नाम का उपयोग करने से टेस्ट कम्पाइल (compile) नहीं होंगे:
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
/// Increase contract balance.
fn increase_balance(ref self: TContractState, amount: felt252);
/// Retrieve contract balance.
fn get_balance(self: @TContractState) -> felt252;
}
Counter Component
CounterComponent (Solidity में “abstract contract” के समान जिसे हमने पहले देखा था) लगभग उसी कॉन्ट्रैक्ट के समान है जिसे Scarb डिफ़ॉल्ट रूप से बनाता है। इसके अंतरों को कोड ब्लॉक्स के बाद समझाया गया है।
CounterComponent:
#[starknet::component]
pub mod CounterComponent {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
pub struct Storage {
x: felt252,
}
#[event]
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {}
#[embeddable_as(CounterImplMixin)]
impl CounterImpl<TContractState, +HasComponent<TContractState>> of super::IHelloStarknet<ComponentState<TContractState>> {
fn get_balance(self: @ComponentState<TContractState>) -> felt252 {
self.x.read()
}
fn increase_balance(ref self: ComponentState<TContractState>, amount: felt252) {
assert(amount != 0, 'Amount cannot be 0');
self.x.write(self.x.read() + amount);
}
}
}
Scarb द्वारा बनाया गया डिफ़ॉल्ट कॉन्ट्रैक्ट:
#[starknet::contract]
mod HelloStarknet {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
balance: felt252,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn increase_balance(ref self: ContractState, amount: felt252) {
assert(amount != 0, 'Amount cannot be 0');
self.balance.write(self.balance.read() + amount);
}
fn get_balance(self: @ContractState) -> felt252 {
self.balance.read()
}
}
}
यहाँ CounterComponent और Scarb द्वारा जनरेट किए गए डिफ़ॉल्ट कॉन्ट्रैक्ट के बीच अंतर दिए गए हैं:
- Component को
#[starknet::component]एट्रिब्यूट के साथ एनोटेट (annotate) किया गया है- कॉन्ट्रैक्ट को
#[starknet::contract]एट्रिब्यूट के साथ एनोटेट किया गया है
- कॉन्ट्रैक्ट को
- Component में
implके पास#[embeddable_as(CounterImplMixin)]एट्रिब्यूट है- कॉन्ट्रैक्ट के पास
#[abi(embed_v0)]एट्रिब्यूट है
- कॉन्ट्रैक्ट के पास
- Component में,
CounterImplके पास traitimpl CounterImpl<TContractState, +HasComponent<TContractState>> of super::IHelloStarknet<ComponentState<TContractState>>है- कॉन्ट्रैक्ट के पास trait
impl HelloStarknetImpl of super::IHelloStarknet<ContractState>है
- कॉन्ट्रैक्ट के पास trait
- Component एक खाली event ब्लॉक डिक्लेयर करता है भले ही यह events का उपयोग नहीं करता है
- कॉन्ट्रैक्ट्स event ब्लॉक को छोड़ सकते हैं (omit कर सकते हैं), लेकिन एक component ऐसा नहीं कर सकता। व्यावहारिक रूप से (In practice), अधिकांश वास्तविक दुनिया (real-world) के components में events होंगे। सादगी बनाए रखने के लिए अभी हम event को खाली रख रहे हैं।
आगे ऊपर सूचीबद्ध अंतरों का विस्तृत विवरण दिया गया है।
#[starknet::component] vs #[starknet::contract]
यदि हम एक कॉन्ट्रैक्ट के बजाय एक component बनाने का इरादा रखते हैं, तो कम्पाइलर को मॉड्यूल का प्रकार पता होना चाहिए। mod ब्लॉक को #[starknet::component] के साथ एनोटेट करने से कम्पाइलर को पता चलता है कि हम एक component बना रहे हैं, जबकि [starknet::contract] कम्पाइलर को बताता है कि हम एक कॉन्ट्रैक्ट बना रहे हैं।
#[embeddable_as(CounterImplMixin)]
यह एट्रिब्यूट एक कॉन्ट्रैक्ट को component से एक impl को “अंदर लाने (bring in)” की अनुमति देता है।
// Counter Mixin
#[abi(embed_v0)]
impl CounterImpl = CounterComponent::CounterImplMixin<ContractState>;
कॉन्ट्रैक्ट में: CounterComponent मॉड्यूल CounterComponent को संदर्भित करता है और CounterImplMixin उस Impl को संदर्भित करता है जिसे यह अंदर ला रहा है (“mixing in”)।
CounterImplMixin नाम ऐच्छिक (arbitrary) है।
हम component में #[embeddable_as(FooBar)] लिख सकते थे और कॉन्ट्रैक्ट में निम्नलिखित कोड डाल सकते थे:
#[abi(embed_v0)]
impl CounterImpl = CounterComponent::FooBar<ContractState>;
यदि हम विभिन्न उद्देश्यों के लिए अलग-अलग impl ब्लॉक्स को एक्सपोज़ करना चाहते हैं, तो हम एक component के भीतर कई
embeddable_asइम्प्लीमेंटेशन्स परिभाषित कर सकते हैं (हम अगले लेख में इसका एक उदाहरण दिखाएंगे)।
“Mixin” कोई भाषा का निर्माण (language construct) या कोई ऐसा शब्द नहीं है जिसे कम्पाइलर पहचानता हो। यह Cairo में एक impl के लिए एक मुहावरेदार (idiomatic) शब्दावली है जिसे एक component से कॉन्ट्रैक्ट में शामिल किया गया है, और वह impl कॉन्ट्रैक्ट में नए “पब्लिक” functions को एक्सपोज़ करेगा। एक कॉन्ट्रैक्ट एक ऐसे impl को शामिल कर सकता है जो किसी भी बाहरी (external) functions को एक्सपोज़ नहीं करता है, लेकिन इसे “mixin” नहीं माना जाएगा।
कॉन्ट्रैक्ट में #[abi(embed_v0)], काउंटर impl से functions को एक्सपोज़ करता है। यदि हम #[abi(embed_v0)] को निम्न प्रकार शामिल नहीं करते हैं:
// #[abi(embed_v0)] commented out
impl CounterImpl = CounterComponent::Counter<ContractState>;
हमारा कोड अभी भी कम्पाइल होगा, लेकिन इसमें कोई पब्लिक functions नहीं होंगे, इसलिए टेस्ट पास नहीं होंगे।
Component में impl डेफ़िनेशन को समझना
उपरोक्त component में impl की डेफ़िनेशन इस प्रकार दिखती है:
impl CounterImpl<TContractState, +HasComponent<TContractState>> of super::IHelloStarknet<ComponentState<TContractState>>
यह पहली बार में डरावना लग सकता है, खासकर यदि आपकी Rust की पृष्ठभूमि नहीं है। अच्छी खबर यह है कि यह ज्यादातर बॉयलरप्लेट (boilerplate) है, आप इस पैटर्न का सभी components में पुनः उपयोग (reuse) करेंगे, आपको इसे फिर से लिखने की आवश्यकता नहीं होगी। लेकिन हमें पता होना चाहिए कि इसका क्या अर्थ है।
एक component में, हर impl इस स्ट्रक्चर का पालन करता है:
impl {ImplName}<TContractState, +HasComponent<TContractState>> of {PathToTrait}::{TraitName}<ComponentState<TContractState>>
आइए इसे समझते हैं:
{ImplName}वह नाम है जो आप इम्प्लीमेंटेशन ब्लॉक को देते हैं। यह कुछ भी हो सकता है जो आप चुनते हैं।TContractStateकॉन्ट्रैक्ट के स्टेट (state) के प्रकार को दर्शाता है।+HasComponent<TContractState>कम्पाइलर को बताता है कि इस component का उपयोग करने वाले कॉन्ट्रैक्ट में इसका स्टेट शामिल है।of {PathToTrait}::{TraitName}इम्प्लीमेंटेशन को उस trait से लिंक करता है जो component के इंटरफ़ेस को परिभाषित करता है।ComponentState<TContractState>का अर्थ है कि trait कॉन्ट्रैक्ट स्टेट के उस हिस्से पर काम करता है जो component का है।
हमारे उदाहरण में:
{PathToTrait}यहाँsuperहै क्योंकि trait को उसी फ़ाइल में डिक्लेयर किया गया है।{TraitName}यहाँIHelloStarknetहै, क्योंकि टेस्ट इसी विशिष्ट trait नाम की उम्मीद करते हैं।
एक बार जब आप इस पैटर्न को समझ लेते हैं, तो जब भी आप किसी component के लिए इम्प्लीमेंटेशन डिक्लेयर करते हैं तो आप इसका पुनः उपयोग (reuse) कर सकते हैं।
कॉन्ट्रैक्ट component का उपयोग कैसे करता है
यहाँ कॉन्ट्रैक्ट कोड फिर से दिया गया है:
#[starknet::contract]
mod HelloStarknet {
use super::CounterComponent;
component!(path: CounterComponent, storage: counter, event: CounterEvent);
#[abi(embed_v0)]
impl CounterImpl = CounterComponent::CounterImplMixin<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
counter: CounterComponent::Storage,
}
#[event]
#[derive(Drop, Debug, PartialEq, starknet::Event)]
pub enum Event {
#[flat]
CounterEvent: CounterComponent::Event,
}
}
Solidity में, जब कोई कॉन्ट्रैक्ट किसी abstract contract से इनहेरिट (inherit) करता है तो functions, storage variables और events स्वचालित रूप से “अंदर आ (pulled in)” जाते हैं। Cairo में ऐसा नहीं है।
एक component को “अंदर लाने (bring in)” के लिए, हमें इस चेकलिस्ट का पालन करने की आवश्यकता है:
useके साथ component को इम्पोर्ट करें। इस मामले में, यहuse super::CounterComponentहै। यह केवल कोड को उपलब्ध (available) बनाता है और इसे component में इंटीग्रेट नहीं करता है- हमें
component!मैक्रो को डिक्लेयर करना होगा। इसे आगे संक्षेप में समझाया जाएगा - पब्लिक functions को मिक्स (mixed in) किया जाना चाहिए
- Storage को
#[substorage(v0)]के रूप में एम्बेड किया जाना चाहिए - Events को
#[flat]के साथ एम्बेड किया जाना चाहिए
इनमें से कोई भी चरण (step) वैकल्पिक (optional) नहीं है। नीचे चेकलिस्ट में प्रत्येक आइटम का विस्तृत विवरण दिया गया है।
Component को इम्पोर्ट करना
हमारे CounterComponent का नाम “CounterComponent” रखना वैकल्पिक है। इसे “SparklingWaterIsTasty” भी कहा जा सकता है और यह ठीक रहेगा। हालाँकि, हम जिस component नाम को इम्पोर्ट करते हैं, वही नाम इनके लिए उपयोग किया जाना चाहिए:
component!मेंpath- यह Mixin, Storage और Event का स्रोत होना चाहिए जैसा कि नीचे हाइलाइट किया गया है

impl को इम्पोर्ट करना
अपने कॉन्ट्रैक्ट में component के बाहरी (external) functions को शामिल करने के लिए, हमें निम्नलिखित कार्य करने होंगे:
- एक
implडिक्लेयर करें और इसे#[abi(embed_v0)]एट्रिब्यूट (नीचे नारंगी रंग में) के साथ बाहरी (external) बनाएं - Component से
CounterImplMixinको अंदर लाएँ।CounterImplMixinनाम component के#[embeddable_as(CounterImplMixin)]में डिक्लेयर किए गए नाम से मेल खाना चाहिए। Component में impl नाम से मेल खाने का मतलब यह गारंटी नहीं है कि इम्पोर्ट काम करेगा, आपकोembeddable_asमैक्रो में डिक्लेयर किए गए नाम का उपयोग करना होगा। - अंत में, हम
ContractStateको “पास” करके (जैसा कि नीचे सफेद बॉक्स में दिखाया गया है)CounterImplMixinmixin को कॉन्ट्रैक्ट storage तक “पहुँच (access)” देते हैं।

Storage को इम्पोर्ट करना
कॉन्ट्रैक्ट इनहेरिटेंस (जो storage को स्वचालित रूप से इम्पोर्ट करता है) के विपरीत, Cairo में यह मैन्युअल रूप से किया जाना चाहिए।
कॉन्ट्रैक्ट का सभी storage कॉन्ट्रैक्ट में #[storage] लेबल वाले struct में मौजूद होता है। हालाँकि component में भी एक #[storage] struct होता है, यह “गिना (count)” नहीं जाता क्योंकि वह एक component में है।
सौभाग्य से, हमें हर एक storage वेरिएबल को अलग-अलग इम्पोर्ट करने की आवश्यकता नहीं है। हम #[substorage(v0)] एट्रिब्यूट के साथ “एक ही बार में (in one go)” storage को इम्पोर्ट कर लेते हैं।
अब आइए दिखाते हैं कि storage कैसे इम्पोर्ट किया जाता है:
- Component से इम्पोर्ट किए गए सभी storage के लिए कॉन्ट्रैक्ट के storage struct के अंदर एक की (key) होनी चाहिए। इस की (key) का नाम
component!मैक्रो (नीचे हरा बॉक्स और तीर) में डिक्लेयर किए गए नाम से मेल खाना चाहिए। इसे कुछ भी नाम दिया जा सकता है, लेकिन उन्हेंcomponent!में डिक्लेयर की गई वैल्यू और struct में की (key) के बीच सुसंगत (consistent) होना चाहिए।counterनाम स्वयं ऐच्छिक है। इसी लिंक के माध्यम से कम्पाइलर को यह भी पता चलता है किcounterके अंदर का storage कहीं और परिभाषित किया गया था। - Component (कॉन्ट्रैक्ट का नहीं) के storage struct को अंदर लाने के लिए, हम इसे struct में
CounterComponent::Storageके रूप में वैल्यू की तरह रखते हैं (नीचे पीला और बैंगनी बॉक्स)। ध्यान दें कि यहाँStorage, component में मौजूद struct का नाम है।

Events को इम्पोर्ट करना
Events को इम्पोर्ट करना भी storage को इम्पोर्ट करने के समान ही पैटर्न का पालन करता है:
component!मैक्रो में डिक्लेयर किया गयाCounterEventकॉन्ट्रैक्ट केEventenum में संबंधित आइटम से मेल खाना चाहिए। यह वन-टू-वन मिलान ही वह तरीका है जिससे कम्पाइलर को पता चलता है कि event कॉन्ट्रैक्ट के बाहर परिभाषित किया गया है।CounterEventनाम ऐच्छिक है, लेकिन हम जो भी नाम चुनते हैं वहcomponent!मैक्रो और enum वैरिएंट दोनों में बिल्कुल समान दिखना चाहिए।- एंट्री के ऊपर
#[flat](जैसा कि नीचे नारंगी बॉक्स में दिखाया गया है) एट्रिब्यूट एक आवश्यक बॉयलरप्लेट है जो कम्पाइलर को component के events को आपके कॉन्ट्रैक्ट के event स्ट्रक्चर में नेस्ट (nest) करने के बजाय फ़्लैट (flatten) करने के लिए कहता है। CounterComponentवह तरीका है जिससे हमEventको अंदर लाते हैं। मैजेंटा रंग मेंEventवहEventenum है जिसे component में डिक्लेयर किया गया है।

सारांश (Summary)
एक component अपने स्वयं के functions, storage और events बनाता है लेकिन इसे कॉन्ट्रैक्ट के रूप में डिप्लॉय नहीं किया जा सकता है।
एक इम्पोर्ट का उपयोग करके और component! के साथ संदर्भ (references) डिक्लेयर करके एक component को कॉन्ट्रैक्ट में इम्पोर्ट किया जा सकता है।
Functions, storage और events को अलग-अलग इम्पोर्ट किया जाना चाहिए।
Functions को इम्पोर्ट करने के लिए, #[abi(embed_v0)] के साथ डिक्लेयर किया गया एक नया impl बनाएँ और इम्प्लीमेंटेशन को #[embeddable_as(mixin_name)] में निर्दिष्ट mixin नाम पर सेट करें।
Storage इम्पोर्ट करने के लिए, #[substorage(v0)] के साथ कॉन्ट्रैक्ट के storage में एक नई की (key) बनाएँ। Storage के लिए की (key) को वही नाम सेट करें जो component! मैक्रो में storage: के लिए डिक्लेयर किया गया है। फिर वैल्यू को component में storage struct के पाथ (path) पर सेट करें।
Events को इम्पोर्ट करने के लिए, event enum में एक नई एंट्री बनाएँ और उस पर #[flat] एट्रिब्यूट लागू करें। एंट्री को वही नाम सेट करें जो component! मैक्रो में event: के लिए डिक्लेयर किया गया है। फिर टाइप को component में enum के पाथ पर सेट करें।
यह लेख Cairo Programming on Starknet पर एक ट्यूटोरियल सीरीज़ का हिस्सा है।