Component भाग 1 में, हमने सीखा कि एक सिंगल फाइल के अंदर component को कैसे बनाया और उपयोग किया जाता है। हमने स्क्रैच से एक CounterComponent बनाया और इसके storage, events और implementations को अपने contract में इंटीग्रेट किया।
Smart contracts में उपयोग किए जाने वाले अधिकांश components बाहरी लाइब्रेरी से आते हैं। OpenZeppelin Contracts for Cairo ownership, access control, token standards और अन्य के लिए components प्रदान करता है जिन्हें contracts में इम्पोर्ट किया जा सकता है, ठीक उसी तरह जैसे OpenZeppelin Contracts for Solidity में होता है।
इस ट्यूटोरियल में, आप सीखेंगे कि सब कुछ स्क्रैच से बनाने के बजाय OpenZeppelin से components को कैसे इम्पोर्ट और उपयोग करें; बाहरी crate components के लिए import paths को समझें; और boilerplate कोड जनरेट करने के लिए OpenZeppelin Wizard का उपयोग करें।
Dependencies को सेट अप करना
OpenZeppelin components को इम्पोर्ट करने से पहले, हमें अपने प्रोजेक्ट में एक dependency के रूप में OpenZeppelin Contracts लाइब्रेरी को जोड़ना होगा। Cairo अपने आधिकारिक पैकेज रजिस्ट्री के रूप में Scarbs.xyz का उपयोग करता है, ठीक उसी तरह जैसे JavaScript के लिए npm या Rust के लिए crates.io का उपयोग होता है।
एक नया scarb प्रोजेक्ट बनाएं और उसकी डायरेक्टरी में नेविगेट करें:
scarb new erc20_component
cd erc20_component
अपने प्रोजेक्ट डायरेक्टरी में Scarb.toml फाइल खोलें और [dependencies] सेक्शन के तहत निम्नलिखित एंट्री जोड़ें:
[dependencies]
starknet = "2.13.1"
openzeppelin = "2.0.0" //ADD THIS LINE

सिंटैक्स openzeppelin = "2.0.0" स्वचालित रूप से Scarbs.xyz (Cairo की आधिकारिक पैकेज रजिस्ट्री) से पैकेज प्राप्त करता है। वर्ज़न “2.0.0” यह निर्दिष्ट करता है कि किस OpenZeppelin Contracts रिलीज़ का उपयोग करना है। हम v2.0.0 का उपयोग कर रहे हैं, जो इस लेख को लिखते समय नवीनतम स्थिर (stable) रिलीज़ है। वर्तमान नवीनतम वर्ज़न के लिए Scarbs.xyz for OpenZeppelin या OpenZeppelin Contracts for Cairo releases page देखें।
Dependencies को डाउनलोड और कम्पाइल करने के लिए scarb build रन करें। बिल्ड सफल होने के बाद, dependencies तैयार हो जाती हैं और आप OpenZeppelin components को अपने contract में इम्पोर्ट कर सकते हैं।
OpenZeppelin Wizard के साथ ERC20 Token बनाना
हम OpenZeppelin components का उपयोग करके एक ERC20 token contract बनाएंगे। OpenZeppelin Wizard का उपयोग करके, हम contract कोड जनरेट करेंगे और फिर समझाएंगे कि components को कैसे इम्पोर्ट और इंटीग्रेट किया जाता है।
OpenZeppelin Wizard का उपयोग करना
OpenZeppelin Wizard एक इंटरैक्टिव वेब-आधारित टूल है जो contracts के लिए boilerplate कोड जनरेट करता है। स्क्रैच से बनाने के बजाय, यह हमें मनचाहे फीचर्स को चुनने की अनुमति देता है और उपयोग के लिए तैयार पूर्ण contract कोड तैयार करता है। यह Ownable, ERC20, ERC721 और अन्य फंक्शनैलिटी को लागू करने का एक तेज़ तरीका है।
हमारा टोकन इन तीन components का उपयोग करेगा:
- ERC20Component: Token फंक्शनैलिटी के लिए
- OwnableComponent: Access control के लिए
- PausableComponent: Token ट्रांसफर्स को pause/unpause करने के लिए
अब जब हम समझ गए हैं कि OpenZeppelin Wizard क्या करता है, तो आइए एक contract जनरेट करने के लिए इसका उपयोग करें। OpenZeppelin Wizard for Cairo, OpenZeppelin वेबसाइट के Wizard सबडोमेन पर उपलब्ध है। अपने ब्राउज़र में OpenZeppelin Wizard for Cairo पर जाएं और contract प्रकार के रूप में ‘ERC20’ चुनें।
‘SETTINGS’ सेक्शन में, नाम को अपने मनचाहे टोकन नाम में बदलें और सिंबल को अपडेट करें। ‘FEATURES’ सेक्शन में, Mintable और Pausable को चेक (☑️) करें; Ownable स्वचालित रूप से चेक हो जाता है।

ऊपर दाईं ओर से कोड कॉपी करें, और इसे अपने प्रोजेक्ट डायरेक्टरी में src/lib.cairo फाइल में पेस्ट करें। आपका जनरेट किया गया कोड आवश्यक imports, component declarations, storage structure, events, constructor, और कस्टम फंक्शन्स (pause, unpause, और mint) के साथ निम्नलिखित contract के समान दिखना चाहिए:
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts for Cairo ^2.0.0
#[starknet::contract]
mod RareToken {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::security::pausable::PausableComponent;
use openzeppelin::token::erc20::{DefaultConfig as ERC20DefaultConfig, ERC20Component};
use starknet::ContractAddress;
component!(path: ERC20Component, storage: erc20, event: ERC20Event);
component!(path: PausableComponent, storage: pausable, event: PausableEvent);
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
// External
#[abi(embed_v0)]
impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
#[abi(embed_v0)]
impl PausableImpl = PausableComponent::PausableImpl<ContractState>;
#[abi(embed_v0)]
impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>;
// Internal
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
impl PausableInternalImpl = PausableComponent::InternalImpl<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
erc20: ERC20Component::Storage,
#[substorage(v0)]
pausable: PausableComponent::Storage,
#[substorage(v0)]
ownable: OwnableComponent::Storage,
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
ERC20Event: ERC20Component::Event,
#[flat]
PausableEvent: PausableComponent::Event,
#[flat]
OwnableEvent: OwnableComponent::Event,
}
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress) {
self.erc20.initializer("RareToken", "RTK");
self.ownable.initializer(owner);
}
impl ERC20HooksImpl of ERC20Component::ERC20HooksTrait<ContractState> {
fn before_update(
ref self: ERC20Component::ComponentState<ContractState>,
from: ContractAddress,
recipient: ContractAddress,
amount: u256,
) {
let contract_state = self.get_contract();
contract_state.pausable.assert_not_paused();
}
}
#[generate_trait]
#[abi(per_item)]
impl ExternalImpl of ExternalTrait {
#[external(v0)]
fn pause(ref self: ContractState) {
self.ownable.assert_only_owner();
self.pausable.pause();
}
#[external(v0)]
fn unpause(ref self: ContractState) {
self.ownable.assert_only_owner();
self.pausable.unpause();
}
#[external(v0)]
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) {
self.ownable.assert_only_owner();
self.erc20.mint(recipient, amount);
}
}
}
बिना ज्यादा मेहनत किए, हमने mintable, pausable और access control फीचर्स के साथ पूरी तरह से काम करने वाला contract जनरेट कर लिया है।
जनरेट किए गए कोड के साथ, आइए समझते हैं कि OpenZeppelin components को कैसे इम्पोर्ट किया जाता है और contract में कैसे इंटीग्रेट किया जाता है।
जनरेट किए गए कोड को समझना
Components के साथ काम करते समय, तीन चरणों की आवश्यकता होती है:
- component को इम्पोर्ट करना,
component!मैक्रो का उपयोग करके अपने contract को इससे लिंक करना, और- अपने contract में उनके फंक्शन्स को एक्सपोज़ करने के लिए component implementations को एम्बेड करना
आइए देखें कि यह हमारे जनरेट किए गए RareToken contract में कैसे काम करता है।
चरण 1: Components को इम्पोर्ट करना
पहला चरण components को इम्पोर्ट करना है। नीचे दिए गए कोड में हाइलाइट किए गए import स्टेटमेंट्स OwnableComponent, PausableComponent और ERC20Component को contract के स्कोप में लाते हैं, जिससे उनकी फंक्शनैलिटी उपयोग के लिए उपलब्ध हो जाती है:

चरण 2: component! मैक्रो के साथ Components को लिंक करना
आवश्यक components को इम्पोर्ट करने के बाद, component! मैक्रो का उपयोग करके contract में components को सेट अप (लिंक) किया जाता है:

component! मैक्रो डिक्लेयर करता है कि हमारा contract प्रत्येक component से कैसे कनेक्ट होगा। यह तीन arguments लेता है:
path: Component का पाथ (जो इम्पोर्ट किया गया था)। इस मामले में:ERC20Component,PausableComponent, औरOwnableComponentstorage: Contract में storage वेरिएबल का नाम जो component के storage को पॉइंट करता है। किसी component के storage तक पहुंचने के लिए, आपको अपने contract के storage में एक वेरिएबल की आवश्यकता होती है जो component के storage को रेफरेंस करता हो

ऊपर दिए गए उदाहरण में, storage नाम erc20, pausable, और ownable का उपयोग किया गया था। इन नामों को कस्टमाइज़ किया जा सकता है, लेकिन उन्हें contract के storage struct में डिक्लेयर किए गए नामों से मैच करना चाहिए।
जैसा कि Component भाग 1 में चर्चा की गई थी, प्रत्येक storage फील्ड को #[substorage(v0)] के साथ एनोटेट किया जाता है ताकि यह इंगित किया जा सके कि यह component storage को रेफरेंस करता है।
3. event: Contract में event वैरिएंट का नाम जो component के events को पॉइंट करता है।
नीचे दिए गए स्क्रीनशॉट में, ध्यान दें कि शीर्ष पर (लाइन 11-13) हाइलाइट किए गए event नाम निचले भाग (लाइन 42, 44, 46) में हाइलाइट किए गए event वैरिएंट्स से कैसे मेल खाते हैं। component! मैक्रो में event पैरामीटर (उदा., ERC20Event) contract के event enum में वैरिएंट नाम से मैप होता है।

इस मामले में, ERC20Event, PausableEvent, और OwnableEvent का उपयोग किया गया था। Storage नामों की तरह, ये कुछ भी हो सकते हैं, लेकिन उन्हें contract के event enum में डिक्लेयर किए गए नामों से मैच करना चाहिए।
प्रत्येक event वैरिएंट पर लागू #[flat] एट्रिब्यूट यहाँ महत्वपूर्ण है। Events चैप्टर के “Using #[flat] attribute” सेक्शन को याद करें कि #[flat] एट्रिब्यूट यह बदल देता है कि event selectors की गणना कैसे की जाती है।
#[flat] के बिना, component events में पहली key के रूप में एक component ID शामिल होती है, और एक component वैरिएंट के सभी events बाहरी enum वैरिएंट नाम से गणना किए गए समान selector को साझा करते हैं। उदाहरण के लिए, ERC20Component से Transfer और Approval दोनों events अपने selector के रूप में starknetKeccak("ERC20Event") का उपयोग करेंगे, जिससे अकेले selector के आधार पर विभिन्न event प्रकारों के बीच अंतर करना असंभव हो जाएगा।
#[flat] के साथ, component ID प्रीफिक्स हटा दिया जाता है, और प्रत्येक event selector के लिए अपने स्वयं के struct नाम का उपयोग करता है: starknetKeccak("Transfer"), starknetKeccak("Approval")। यह सटीक event फ़िल्टरिंग को सक्षम बनाता है और बाहरी टूल्स और इंडेक्सर्स द्वारा अपेक्षित स्टैंडर्ड event संरचना से मेल खाता है।
चरण 3: Component Implementations
अब जनरेट किए गए कोड में component implementations को देखते हैं। ये दो प्रकार के होते हैं: external और internal। External implementations को contract के बाहर से कॉल किया जा सकता है, जबकि internal वालों को केवल contract के भीतर ही उपयोग किया जा सकता है।
जनरेट किए गए कोड में तीन external implementations शामिल हैं जो components की फंक्शनैलिटी को एक्सपोज़ करते हैं:
// External
#[abi(embed_v0)]
impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
#[abi(embed_v0)]
impl PausableImpl = PausableComponent::PausableImpl<ContractState>;
#[abi(embed_v0)]
impl OwnableMixinImpl = OwnableComponent::OwnableMixinImpl<ContractState>;
#[abi(embed_v0)] एट्रिब्यूट इन implementations को सार्वजनिक रूप से सुलभ बनाता है; उनके फंक्शन्स को contract के बाहर से कॉल किया जा सकता है। आइए प्रत्येक implementation को समझते हैं।
ERC20MixinImpl सभी आवश्यक ERC20 फंक्शनैलिटी को एक पैकेज में जोड़ता है:
ERC20Impl: इसमें मुख्य फंक्शन्स होते हैं जैसेtransfer,approve,balance_ofERC20MetadataImpl: इसमें Metadata फंक्शन्स होते हैं जैसेname,symbol,decimalsERC20CamelImpl: इसमें कम्पैटिबिलिटी के लिए Camel-case फंक्शन वर्ज़न्स होते हैं (उदा.,balanceOf,totalSupply)
ERC20 mixin का उपयोग करने से हम प्रत्येक implementation को अलग से एम्बेड करने से बच जाते हैं।
ERC20 mixin के अलावा, contract दो अन्य external implementations को एम्बेड करता है:
PausableImplcontract ऑपरेशन्स को रोकने के लिएpause(), उन्हें फिर से शुरू करने के लिएunpause(), और वर्तमान pause स्थिति की जांच करने के लिएis_paused()प्रदान करता है।OwnableMixinImplवर्तमान owner को देखने के लिएowner(), एक नए एड्रेस पर ओनरशिप ट्रांसफर करने के लिएtransfer_ownership(), और owner को पूरी तरह से हटाने के लिएrenounce_ownership()प्रदान करता है।
जनरेट किए गए कोड में ये internal implementations भी शामिल हैं:
// Internal
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
impl PausableInternalImpl = PausableComponent::InternalImpl<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;
ध्यान दें कि उपरोक्त implementation में #[abi(embed_v0)] नहीं है, ऐसा इसलिए है क्योंकि वे सार्वजनिक रूप से contract के बाहर से कॉल करने योग्य नहीं हैं।
Constructor
Constructor ERC20 component’s initializer के माध्यम से टोकन का नाम और सिंबल सेट करता है और Ownable component’s initializer के माध्यम से contract owner सेट करता है।
#[constructor]
fn constructor(
ref self: ContractState,
name: ByteArray,
symbol: ByteArray,
fixed_supply: u256,
recipient: ContractAddress,
owner: ContractAddress
) {
self.erc20.initializer(name, symbol);
self.erc20.mint(recipient, fixed_supply);
self.ownable.initializer(owner);
}
प्रत्येक initializer को केवल एक बार कॉल किया जा सकता है, जो डिप्लॉयमेंट के बाद इन सेटिंग्स को लॉक कर देता है।
ERC20 Hooks
Hooks ऐसे फंक्शन्स होते हैं जो कुछ ऑपरेशन्स के पहले या बाद में स्वचालित रूप से चलते हैं। ERC20 component एक ERC20HooksTrait प्रदान करता है जो आपको ऐसा लॉजिक जोड़ने की अनुमति देता है जो टोकन ट्रांसफर्स के दौरान चलता है।
before_update हुक
जनरेट किए गए कोड में एक before_update हुक होता है जो किसी भी टोकन ऑपरेशन से पहले यह जांचता है कि क्या contract पॉज़ (paused) है:
impl ERC20HooksImpl of ERC20Component::ERC20HooksTrait<ContractState> {
fn before_update(
ref self: ERC20Component::ComponentState<ContractState>,
from: ContractAddress,
recipient: ContractAddress,
amount: u256,
) {
let contract_state = self.get_contract();
contract_state.pausable.assert_not_paused();
}
}
before_update फंक्शन किसी भी टोकन बैलेंस परिवर्तन (transfers, mints, या burns) से पहले चलता है। इस implementation में:
self.get_contract()contract state प्राप्त करता हैcontract_state.pausable.assert_not_paused()जांचता है कि contract पॉज़ है या नहीं- यदि पॉज़ है, तो ट्रांज़ैक्शन रिवर्ट (revert) हो जाता है; यदि नहीं, तो ट्रांसफर आगे बढ़ता है
इस तरह pausable फीचर काम करता है; हर टोकन ऑपरेशन से पहले pause स्थिति की जांच करके, contract पॉज़ होने पर सभी ट्रांसफर्स को रोक सकता है।
Before और After Update Hooks
जनरेट किए गए कोड में before_update हुक को लागू किए बिना, pausable component contract में मौजूद तो होगा लेकिन वास्तव में टोकन ट्रांसफर्स को प्रभावित नहीं करेगा।
ERC20HooksTrait में एक after_update हुक भी शामिल है जो टोकन ऑपरेशन पूरा होने के बाद चलता है। हालाँकि इसका उपयोग इस contract में नहीं किया गया है, लेकिन आप इसे कस्टम लॉजिक जोड़ने के लिए लागू कर सकते हैं जो transfers, mints, या burns के बाद निष्पादित होता है।
Internal Component Functions को एक्सपोज़ करना
कुछ component फंक्शन्स जैसे pause() और mint() internal होते हैं; वे components के भीतर मौजूद होते हैं लेकिन सार्वजनिक रूप से सुलभ नहीं होते हैं। जनरेट किया गया कोड पब्लिक रैपर (wrapper) फंक्शन्स बनाता है जो owner-only एक्सेस कंट्रोल जोड़ते हुए इन ऑपरेशन्स को एक्सपोज़ करते हैं:
#[generate_trait]
#[abi(per_item)]
impl ExternalImpl of ExternalTrait {
#[external(v0)]
fn pause(ref self: ContractState) {
self.ownable.assert_only_owner();
self.pausable.pause();
}
#[external(v0)]
fn unpause(ref self: ContractState) {
self.ownable.assert_only_owner();
self.pausable.unpause();
}
#[external(v0)]
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) {
self.ownable.assert_only_owner();
self.erc20.mint(recipient, amount);
}
}
#[generate_trait] एट्रिब्यूट स्वचालित रूप से इस implementation से ExternalTrait इंटरफ़ेस जनरेट करता है, इसलिए आपको trait डेफ़िनिशन को मैन्युअल रूप से लिखने की आवश्यकता नहीं है।
#[abi(per_item)] एट्रिब्यूट प्रत्येक फंक्शन को ABI जनरेशन के लिए व्यक्तिगत रूप से मार्क करता है, और जब इसे प्रत्येक फंक्शन पर #[external(v0)] के साथ जोड़ा जाता है, तो उन्हें contract के पब्लिक इंटरफ़ेस का हिस्सा बना देता है। #[external(v0)] में v0 ABI वर्ज़न को निर्दिष्ट करता है।
Wrapper Functions कैसे काम करते हैं
प्रत्येक फंक्शन एक ही पैटर्न का पालन करता है: ओनरशिप वेरिफाई करें, फिर ऑपरेशन निष्पादित करें। उदाहरण के लिए, pause() यह वेरिफाई करने के लिए self.ownable.assert_only_owner() को कॉल करता है कि कॉलर owner है, फिर contract को पॉज़ करने के लिए self.pausable.pause() को कॉल करता है; यदि कॉलर owner नहीं है, तो ट्रांज़ैक्शन रिवर्ट हो जाता है।
इसी तरह, unpause() ओनरशिप वेरिफाई करता है और फिर contract को अनपॉज़ करता है, जबकि mint() ओनरशिप वेरिफाई करता है और फिर self.erc20.mint() का उपयोग करके निर्दिष्ट प्राप्तकर्ता एड्रेस पर नए टोकन मिंट करता है।
इन रैपर फंक्शन्स के बिना, internal component फंक्शन्स जैसे pause(), unpause(), और mint() मौजूद तो होंगे, लेकिन owner/deployer contract के बाहर से उनके साथ इंटरैक्ट नहीं कर पाएंगे।
Contract की टेस्टिंग
अब जब हमने टोकन contract सेट अप कर लिया है, तो आइए कुछ टेस्ट्स लिखें। हम अपने द्वारा जोड़े गए कस्टम फीचर्स की टेस्टिंग पर ध्यान केंद्रित करेंगे: pause(), unpause(), और mint() उनके एक्सेस कंट्रोल के साथ।
टेस्ट फाइल सेट अप करना
अपने प्रोजेक्ट डायरेक्टरी में tests/test_contract.cairo पर नेविगेट करें। बॉयलरप्लेट के साथ जनरेट किए गए टेस्ट्स को क्लियर करें, केवल बुनियादी imports छोड़ दें:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};
अपने टेस्ट्स में स्टैंडर्ड ERC-20 फंक्शन्स के साथ इंटरैक्ट करने के लिए, हमें OpenZeppelin से ERC-20 इंटरफ़ेस और उसके dispatcher trait को इम्पोर्ट करने की आवश्यकता है:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};
// NEWLY ADDED//
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
IERC20Dispatcher हमें अपने contract पर स्टैंडर्ड ERC-20 फंक्शन्स जैसे transfer, balance_of, और total_supply को कॉल करने की अनुमति देता है।
याद करें कि जनरेट किए गए contract ने कस्टम फंक्शन्स (pause, unpause, mint) के लिए स्वचालित रूप से traits बनाने के लिए #[generate_trait] एट्रिब्यूट का उपयोग किया था। इन traits को स्पष्ट रूप से contract में नहीं लिखा गया था, इसलिए टेस्ट्स में इन फंक्शन्स को कॉल करने के लिए, नीचे दिखाए अनुसार एक मैन्युअल इंटरफ़ेस डेफ़िनिशन की आवश्यकता होती है:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
// NEWLY ADDED //
// Define the interface for our custom functions
#[starknet::interface]
trait IRareToken<TContractState> {
fn pause(ref self: TContractState);
fn unpause(ref self: TContractState);
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256);
fn decimals(self: @TContractState) -> u8;
}
ऊपर दिए गए कोड में IRareToken इंटरफ़ेस टेस्ट वातावरण (environment) में कस्टम फंक्शन्स को एक्सपोज़ करता है। #[starknet::interface] एट्रिब्यूट dispatcher (IRareTokenDispatcher) और dispatcher trait (IRareTokenDispatcherTrait) जनरेट करता है जिसका उपयोग उन फंक्शन्स के साथ इंटरैक्ट करने के लिए किया जाएगा।
हमें टेस्ट्स के लिए एक समान (consistent) एड्रेसेस की आवश्यकता है। हर बार नए एड्रेस बनाने के बजाय टेस्ट एड्रेसेस प्रदान करने के लिए constants को डिफाइन करें:
const OWNER: ContractAddress = 'OWNER'.try_into().unwrap();
const USER: ContractAddress = 'USER'.try_into().unwrap();
const RECIPIENT: ContractAddress = 'RECIPIENT'.try_into().unwrap();
ये constants स्ट्रिंग लिटरल्स को contract एड्रेसेस में बदलते हैं।
अब हमें टेस्ट वातावरण में अपने टोकन contract को डिप्लॉय करने के लिए एक हेल्पर फंक्शन की आवश्यकता है। test_contract.cairo में deploy_token फंक्शन जोड़ें:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
#[starknet::interface]
trait IRareToken<TContractState> {
fn pause(ref self: TContractState);
fn unpause(ref self: TContractState);
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256);
fn decimals(self: @TContractState) -> u8;
}
const OWNER: ContractAddress = 'OWNER'.try_into().unwrap();
const USER: ContractAddress = 'USER'.try_into().unwrap();
const RECIPIENT: ContractAddress = 'RECIPIENT'.try_into().unwrap();
// NEWLY ADDED //
fn deploy_token() -> (ContractAddress, IERC20Dispatcher, IRareTokenDispatcher) {
let contract = declare("RareToken").unwrap().contract_class();
let mut constructor_args = array![OWNER.into()];
let (contract_address, _) = contract.deploy(@constructor_args).unwrap();
let token = IERC20Dispatcher { contract_address };
let rare_token = IRareTokenDispatcher { contract_address };
(contract_address, token, rare_token)
}
deploy_token RareToken contract को डिक्लेयर करने और उसकी contract class प्राप्त करने के लिए declare("RareToken").unwrap().contract_class() का उपयोग करता है, जो कम्पाइल किए गए contract कोड को लोड करता है।
इसके बाद, यह array![OWNER.into()] के साथ constructor arguments तैयार करता है, जो owner एड्रेस युक्त एक array बनाता है।

Constructor को एक पैरामीटर (owner एड्रेस) की उम्मीद होती है, इसलिए हम टेस्ट में .into() का उपयोग करके इसे felt252 में बदल देते हैं। टोकन का नाम “RareToken” और सिंबल “RTK” पहले से ही contract के constructor में हार्डकोड (hardcoded) हैं।
एक बार arguments तैयार हो जाने के बाद, contract.deploy(@constructor_args).unwrap() contract को डिप्लॉय करता है और contract एड्रेस लौटाता है। Contract के डिप्लॉय होने के साथ, हम उसी contract एड्रेस के लिए दो dispatchers बनाते हैं: स्टैंडर्ड ERC-20 फंक्शन्स के लिए IERC20Dispatcher और कस्टम फंक्शन्स जैसे pause(), unpause(), और mint() के लिए IRareTokenDispatcher।
यह फंक्शन contract एड्रेस और दोनों dispatchers युक्त एक tuple लौटाता है, जिससे हमें अपने टेस्ट्स में डिप्लॉय किए गए contract के साथ इंटरैक्ट करने के लिए आवश्यक सब कुछ मिल जाता है।
ट्रांसफर्स को रोकने के लिए pause() की टेस्टिंग
Pause फंक्शन सभी टोकन ऑपरेशन्स को रोक देता है, जो सुरक्षा घटनाओं या रखरखाव के दौरान उपयोगी होता है।
Contract फंक्शन्स को कॉल करते समय हमें अलग-अलग एड्रेसेस का रूप धारण (impersonate) करने की अनुमति देने के लिए snforge_std से अन्य imports के साथ start_cheat_caller_address और stop_cheat_caller_address को इम्पोर्ट करें:
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address,stop_cheat_caller_address};
अब एक ऐसा टेस्ट लिखते हैं जो यह वेरिफाई करता है कि contract के पॉज़ होने पर ट्रांसफर्स ब्लॉक हो जाते हैं:
#[test]
fn test_pause_prevents_transfer() {
let (contract_address, token, rare_token) = deploy_token();
// Get token decimals for proper amount calculation
let token_decimal = rare_token.decimals();
let amount_to_mint: u256 = 10000 * token_decimal.into();
// Mint tokens to USER
start_cheat_caller_address(contract_address, OWNER);
rare_token.mint(USER, amount_to_mint);
// Pause the contract
rare_token.pause();
stop_cheat_caller_address(contract_address);
// Try to transfer - should fail when paused
start_cheat_caller_address(contract_address, USER);
token.transfer(RECIPIENT, 100 * token_decimal.into()); // This should panic
}
टेस्ट deploy_token() के माध्यम से contract को डिप्लॉय करके शुरू होता है, जो contract एड्रेस और dispatchers लौटाता है जिनकी हमें contract के साथ इंटरैक्ट करने के लिए आवश्यकता होती है। इसके बाद हम rare_token.decimals() का उपयोग करके टोकन के दशमलव स्थान (decimal places) प्राप्त करते हैं। ERC-20 टोकन आमतौर पर 18 डेसिमल्स का उपयोग करते हैं, इसलिए 10000 * 10^18 का गुणा करने पर हमें 10,000 टोकन मिलते हैं।
आगे, हम OWNER का रूप धारण करने और USER को टोकन मिंट करने के लिए start_cheat_caller_address का उपयोग करते हैं। OWNER के रूप में कार्य करते हुए ही, हम pause() फंक्शन को सक्रिय करने के लिए pause() को कॉल करते हैं, फिर कॉलर एड्रेस को वापस डिफ़ॉल्ट पर रीसेट करने के लिए stop_cheat_caller_address का उपयोग करते हैं।
Contract के अब पॉज़ होने के साथ, हम फिर से start_cheat_caller_address का उपयोग करके USER का रूप धारण करते हैं और RECIPIENT को टोकन ट्रांसफर करने का प्रयास करते हैं। यह ट्रांसफर विफल (fail) होना चाहिए क्योंकि contract पॉज़ है, और हम ठीक यही वेरिफाई करना चाहते हैं।
जब आप scarb test test_pause_prevents_transfer रन करते हैं, तो आपको अपने टर्मिनल में यह त्रुटि (error) दिखनी चाहिए:

Contract सही ढंग से ट्रांसफर को अस्वीकार कर देता है क्योंकि यह पॉज़ है। त्रुटि संदेश (error message) OpenZeppelin के Pausable component से आता है। यदि आप OpenZeppelin Pausable component source code देखते हैं, तो आप देखेंगे कि यह ठीक वही त्रुटि है जो पॉज़ किए गए contract पर ऑपरेशन्स का प्रयास किए जाने पर फेंकी (thrown) जाती है:
fn assert_not_paused(self: @ComponentState<TContractState>) {
assert(!self.is_paused(), Errors::PAUSED);
}
हम स्पष्ट रूप से यह इंगित करने के लिए #[should_panic] एट्रिब्यूट का उपयोग करके टेस्ट में सुधार कर सकते हैं कि हम टेस्ट के पैनिक होने की उम्मीद करते हैं। इससे टेस्ट तब पास हो जाता है जब यह अपेक्षित त्रुटि के साथ पैनिक होता है:
#[test]
#[should_panic(expected: ('Pausable: paused',))]
fn test_pause_prevents_transfer() {
let (contract_address, token, rare_token) = deploy_token();
// Get token decimals for proper amount calculation
let token_decimal = rare_token.decimals();
let amount_to_mint: u256 = 10000 * token_decimal.into();
// Mint tokens to USER
start_cheat_caller_address(contract_address, OWNER);
rare_token.mint(USER, amount_to_mint);
// Pause the contract
rare_token.pause();
stop_cheat_caller_address(contract_address);
// Try to transfer - should fail when paused
start_cheat_caller_address(contract_address, USER);
token.transfer(RECIPIENT, 100 * token_decimal.into()); // This should panic
}
#[should_panic(expected: ('Pausable: paused',))] एट्रिब्यूट टेस्ट फ्रेमवर्क को बताता है:
- इस टेस्ट को पैनिक होना चाहिए
- पैनिक में
'Pausable: paused'त्रुटि संदेश होना चाहिए
यदि टेस्ट पैनिक नहीं होता है या किसी भिन्न त्रुटि के साथ पैनिक होता है, तो टेस्ट विफल हो जाएगा। अब जब आप scarb test test_pause_prevents_transfer रन करेंगे, तो आप टेस्ट को सफलतापूर्वक पास होते हुए देखेंगे।
ट्रांसफर्स की अनुमति देने के लिए unpause() की टेस्टिंग
किसी contract को पॉज़ करने के बाद, आपको सामान्य ऑपरेशन्स को फिर से शुरू करने की क्षमता की आवश्यकता होती है। यह टेस्ट वेरिफाई करता है कि अनपॉज़ (unpausing) करने के बाद, टोकन ट्रांसफर्स उम्मीद के मुताबिक काम करते हैं:
#[test]
fn test_unpause_allows_transfer() {
let (contract_address, token, rare_token) = deploy_token();
// Get token decimals for proper amount calculation
let token_decimal = rare_token.decimals();
let amount_to_mint: u256 = 1000 * token_decimal.into();
// Mint tokens to USER
start_cheat_caller_address(contract_address, OWNER);
rare_token.mint(USER, amount_to_mint);
// Pause then unpause the contract
rare_token.pause();
rare_token.unpause();
stop_cheat_caller_address(contract_address);
// Transfer should now succeed
start_cheat_caller_address(contract_address, USER);
token.transfer(RECIPIENT, 100 * token_decimal.into());
// Verify the transfer worked*
assert!(token.balance_of(USER) == 900 * token_decimal.into(), "User balance incorrect");
assert!(token.balance_of(RECIPIENT) == 100 * token_decimal.into(), "Recipient balance incorrect");
}
हम contract को डिप्लॉय करने और टोकन डेसिमल्स प्राप्त करने से शुरू करते हैं, फिर OWNER के रूप में USER को 1,000 टोकन मिंट करते हैं। इस टेस्ट में मुख्य अंतर यह है कि हम OWNER के रूप में कार्य करते हुए ही contract को पॉज़ करते हैं और तुरंत इसे अनपॉज़ कर देते हैं। stop_cheat_caller_address कॉल करने के बाद, हम USER का रूप धारण करने के लिए स्विच करते हैं और RECIPIENT को 100 टोकन ट्रांसफर करने का प्रयास करते हैं।
चूँकि contract अब पॉज़ नहीं है, इसलिए ट्रांसफर सफल होना चाहिए। हम बैलेंस की जांच करके इसे वेरिफाई करते हैं: USER के पास 900 टोकन शेष होने चाहिए (1000 - 100), और RECIPIENT को 100 टोकन मिलने चाहिए। assert! मैक्रो पुष्टि करता है कि ये बैलेंस सही हैं, यह सुनिश्चित करते हुए कि unpause फंक्शन सामान्य contract ऑपरेशन्स को ठीक से बहाल (restore) करता है।
scarb test test_unpause_allows_transfer के साथ टेस्ट रन करें और इसे पास होना चाहिए, जो इस बात की पुष्टि करता है कि पॉज़ मैकेनिज्म को सफलतापूर्वक चालू (on) और बंद (off) टॉगल किया जा सकता है।
pause() के लिए Access Control की टेस्टिंग
pause() जैसे फंक्शन्स जो contract ऑपरेशन्स को रोक सकते हैं, उन्हें उचित एक्सेस कंट्रोल की आवश्यकता होती है। केवल contract owner को ही contract को पॉज़ करने में सक्षम होना चाहिए। यह टेस्ट वेरिफाई करता है कि non-owners पॉज़ नहीं कर सकते:
#[test]
#[should_panic(expected: ('Caller is not the owner',))]
fn test_only_owner_can_pause() {
let (contract_address, _token, rare_token) = deploy_token();
// Try to pause as non-owner - should panic
start_cheat_caller_address(contract_address, USER);
rare_token.pause();
// no need to stop cheat since it doesn't reach here
}
यह टेस्ट सीधा (straightforward) लेकिन महत्वपूर्ण है। हम contract डिप्लॉय करते हैं, फिर तुरंत USER (owner नहीं) के रूप में pause() कॉल करने का प्रयास करते हैं। #[should_panic(expected: ('Caller is not the owner',))] एट्रिब्यूट टेस्ट फ्रेमवर्क को बताता है कि हम उम्मीद करते हैं कि यह एक विशिष्ट त्रुटि संदेश के साथ विफल होगा।
जब rare_token.pause() कॉल किया जाता है, तो यह आंतरिक रूप से Ownable component से self.ownable.assert_only_owner() को ट्रिगर करता है। चूँकि USER owner नहीं है, इसलिए यह assertion विफल हो जाता है और ट्रांज़ैक्शन उम्मीद के मुताबिक “Caller is not the owner” त्रुटि के साथ रिवर्ट हो जाता है।
scarb test test_only_owner_can_pause के साथ टेस्ट रन करें और इसे पास होना चाहिए, जो इस बात की पुष्टि करता है कि हमारा एक्सेस कंट्रोल सही तरीके से काम करता है।
यहाँ वह टेस्ट फाइल है जो हमने बनाई है:
use starknet::ContractAddress;
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address, stop_cheat_caller_address};
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
#[starknet::interface]
trait IRareToken<TContractState> {
fn pause(ref self: TContractState);
fn unpause(ref self: TContractState);
fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256);
fn decimals(self: @TContractState) -> u8;
}
const OWNER: ContractAddress = 'OWNER'.try_into().unwrap();
const USER: ContractAddress = 'USER'.try_into().unwrap();
const RECIPIENT: ContractAddress = 'RECIPIENT'.try_into().unwrap();
fn deploy_token() -> (ContractAddress, IERC20Dispatcher, IRareTokenDispatcher) {
let contract = declare("RareToken").unwrap().contract_class();
let mut constructor_args = array![OWNER.into()];
let (contract_address, _) = contract.deploy(@constructor_args).unwrap();
let token = IERC20Dispatcher { contract_address };
let rare_token = IRareTokenDispatcher { contract_address };
(contract_address, token, rare_token)
}
#[test]
#[should_panic(expected: ('Pausable: paused',))]
fn test_pause_prevents_transfer() {
let (contract_address, token, rare_token) = deploy_token();
// Get token decimals for proper amount calculation
let token_decimal = rare_token.decimals();
let amount_to_mint: u256 = 10000 * token_decimal.into();
// Mint tokens to USER
start_cheat_caller_address(contract_address, OWNER);
rare_token.mint(USER, amount_to_mint);
// Pause the contract
rare_token.pause();
stop_cheat_caller_address(contract_address);
// Try to transfer - should fail when paused
start_cheat_caller_address(contract_address, USER);
token.transfer(RECIPIENT, 100 * token_decimal.into()); // This should panic
}
#[test]
fn test_unpause_allows_transfer() {
let (contract_address, token, rare_token) = deploy_token();
// Get token decimals for proper amount calculation
let token_decimal = rare_token.decimals();
let amount_to_mint: u256 = 1000 * token_decimal.into();
// Mint tokens to USER
start_cheat_caller_address(contract_address, OWNER);
rare_token.mint(USER, amount_to_mint);
// Pause then unpause the contract
rare_token.pause();
rare_token.unpause();
stop_cheat_caller_address(contract_address);
// Transfer should now succeed
start_cheat_caller_address(contract_address, USER);
token.transfer(RECIPIENT, 100 * token_decimal.into());
// Verify the transfer worked
assert!(token.balance_of(USER) == 900 * token_decimal.into(), "User balance incorrect");
assert!(token.balance_of(RECIPIENT) == 100 * token_decimal.into(), "Recipient balance incorrect");
}
#[test]
#[should_panic(expected: ('Caller is not the owner',))]
fn test_only_owner_can_pause() {
let (contract_address, _token, rare_token) = deploy_token();
// Try to pause as non-owner - should panic
start_cheat_caller_address(contract_address, USER);
rare_token.pause();
}
होमवर्क: OpenZeppelin ERC-20 लाइब्रेरी बर्निंग (burning) को सपोर्ट करती है, लेकिन यह फंक्शन internal है। आपका काम यह है:
- एक पब्लिक रैपर फंक्शन जोड़कर contract में burn फंक्शन को एक्सपोज़ करें, ठीक वैसे ही जैसे
mint()को एक्सपोज़ किया गया है। - Burn
get_caller_address()से आना चाहिए। - Burn फंक्शनैलिटी के लिए टेस्ट्स लिखें:
- टेस्ट करें कि एक यूज़र अपने स्वयं के टोकन बर्न कर सकता है।
- टेस्ट करें कि बर्न करने से यूज़र का बैलेंस कम हो जाता है।
- टेस्ट करें कि बर्न करने से कुल सप्लाई (total supply) कम हो जाती है।
- टेस्ट करें कि contract के पॉज़ होने पर बर्निंग नहीं हो सकती है।
- टेस्ट करें कि यूज़र अपने पास मौजूद टोकन से अधिक टोकन बर्न नहीं कर सकता है।
यह लेख Cairo Programming on Starknet पर एक ट्यूटोरियल सीरीज़ का हिस्सा है।