Starknet में, Contract Storage वह स्थायी (persistent) मेमोरी है जहाँ आपके स्मार्ट कॉन्ट्रैक्ट की स्टेट (state) रहती है। एक फंक्शन के अंदर घोषित वेरिएबल्स के विपरीत, जो निष्पादन (execution) के बाद गायब हो जाते हैं, स्टोरेज में डेटा ब्लॉकचेन पर स्थायी रूप से रहता है।
हालाँकि, केवल एक वेरिएबल घोषित करना पर्याप्त नहीं है। Cairo में कॉन्ट्रैक्ट स्टोरेज के साथ प्रभावी ढंग से इंटरैक्ट करने के लिए, कंपाइलर को दो प्रकार के लॉजिक की आवश्यकता होती है:
- Data Representation: स्टोरेज के लिए डेटा टाइप को कैसे सीरियलाइज़ (serialize) और डीसीरियलाइज़ (deserialize) किया जाए। इसे
starknet::Storetrait द्वारा हैंडल किया जाता है। - Access Logic: विशिष्ट स्टोरेज स्लॉट से वास्तव में कैसे पढ़ा (read) या लिखा (write) जाए। इसे Access Traits के एक सेट द्वारा हैंडल किया जाता है।
इंटीजर्स, bool, felt252, ByteArray, आदि जैसे प्रकारों के लिए, Cairo पहले से ही starknet::Store trait का कार्यान्वयन (implementation) प्रदान करता है। नतीजतन, इन प्रकारों का उपयोग बिना किसी अतिरिक्त काम के सीधे कॉन्ट्रैक्ट के स्टोरेज में किया जा सकता है। उदाहरण के लिए, नीचे दिए गए कॉन्ट्रैक्ट में, felt252 और u256 दोनों वैध स्टोरेज सदस्य हैं क्योंकि वे पहले से ही trait को इम्प्लीमेंट करते हैं।
#[storage]
struct Storage {
num1: felt252,
num2: u256,
}
हालाँकि, जब स्टोरेज में जटिल प्रकारों जैसे कि मैपिंग, एरे, या उपयोगकर्ता-परिभाषित स्ट्रक्ट्स (user-defined structs) के साथ काम किया जाता है, तो हमें या तो trait को डिराइव (derive) करना होगा या स्टोरेज में हमारे प्रकार का प्रतिनिधित्व करने के लिए Cairo द्वारा प्रदान किए गए विशेष प्रकार का उपयोग करना होगा। इन मामलों पर बाद के अनुभागों में विस्तार से चर्चा की जाएगी।
यह लेख उन विभिन्न प्रकारों को कवर करेगा जिनका उपयोग स्टोरेज में किया जा सकता है और प्रत्येक प्रकार को स्टोरेज में उपयोग किए जाने के लिए किन traits की आवश्यकता होती है।
Storage Access Traits
Access traits यह निर्धारित करते हैं कि एक्सेस किए जा रहे प्रकार के आधार पर स्टोरेज से वैल्यूज को कैसे पढ़ा या लिखा जाता है। Cairo आंतरिक रूप से इन ऑपरेशन्स को रिज़ॉल्व करने के लिए विभिन्न access traits का उपयोग करता है, जो इस बात पर निर्भर करता है कि क्या हम उन प्रकारों के साथ इंटरैक्ट कर रहे हैं जो पहले से ही starknet::Store trait को इम्प्लीमेंट करते हैं या विशेष प्रकारों के साथ।
यहाँ इन access traits का एक त्वरित विवरण (breakdown) दिया गया है, हम बाद में विस्तार से जानेंगे:
StoragePointerReadAccessऔरStoragePointerWriteAccess: सरल प्रकारों या कस्टम स्ट्रक्ट्स में वैल्यूज को पढ़ने और लिखने के लिए उपयोग किया जाता है जोstarknet::Storeको इम्प्लीमेंट करते हैं।StorageMapReadAccessऔरStorageMapWriteAccess: स्टोरेज में मैपिंग (key-value) प्रकारों को पढ़ने और लिखने का कार्य संभालते हैं।StoragePathEntry: नेस्टेड मैपिंग तक एक्सेस को रिज़ॉल्व करने में मदद करता है।VecTraitऔरMutableVecTrait: स्टोरेज में डायनामिक एरे (dynamics array) तक एक्सेस प्रदान करते हैं।
अब जब हम जानते हैं कि Cairo के स्टोरेज में उपयोग किए जाने वाले किसी भी प्रकार को starknet::Store trait को इम्प्लीमेंट करना चाहिए और इसे पढ़ने या लिखने के लिए उपयुक्त access trait को आयात (import) करने की आवश्यकता होती है, तो आइए देखें कि कौन से प्रकार पहले से ही starknet::Store trait को इम्प्लीमेंट करते हैं, इससे पहले कि हम इस पर आगे बढ़ें कि कॉन्ट्रैक्ट स्तर पर रीड और राइट कैसे काम करते हैं।
Types that Implement starknet::Store Trait
निम्नलिखित Cairo प्रकार starknet::Store trait को इम्प्लीमेंट करते हैं:
- felt252
- अनसाइन्ड (unsigned) और साइन्ड (signed) इंटीजर्स
- bool
- bytes31
- ByteArray
- ContractAddress
- Tuple
चूँकि उपरोक्त प्रकार पहले से ही starknet::Store trait को इम्प्लीमेंट करते हैं, इसलिए स्टोरेज को पढ़ने और उसमें लिखने के लिए केवल read() और write() विधियों (methods) को उपलब्ध कराने के लिए आवश्यक access traits को इम्पोर्ट करने की आवश्यकता होती है।
एक ऐसे कॉन्ट्रैक्ट पर विचार करें जो ऊपर सूचीबद्ध प्रकारों का उपयोग करके कई स्टेट वेरिएबल्स घोषित करता है। नीचे दिया गया स्निपेट (snippet) दिखाता है कि स्टोरेज में प्रत्येक प्रकार को कैसे घोषित किया जा सकता है:
#[starknet::contract]
mod HelloStarknet {
use starknet::ContractAddress;
#[storage]
struct Storage {
// felt252: Field element
user_id: felt252,
// u256: 256-bit unsigned integer
total_supply: u256,
// bool: Boolean value for true/false conditions
is_paused: bool,
// bytes31: Fixed-size byte array (31 bytes), for storing short strings/data
contract_name: bytes31,
// ByteArray: for storing long strings
contract_description: ByteArray,
// ContractAddress: Starknet contract address type
owner_address: ContractAddress,
// Tuple: Groups multiple values together
version_info: (u8, i8) // (unsigned integer, signed integer)
}
}
अब जब हमने देख लिया है कि Cairo के स्टोरेज में विभिन्न डेटा प्रकारों को कैसे घोषित किया जा सकता है, तो अगला कदम यह समझना है कि उनके साथ कैसे काम किया जाए, अर्थात, वास्तव में स्टोरेज में वैल्यूज को कैसे लिखा जाए और बाद में उन्हें वापस कैसे पढ़ा जाए।
Write Operation
इससे पहले कि हम ऊपर घोषित किसी भी स्टेट वेरिएबल में लिख सकें, हमें सबसे पहले StoragePointerWriteAccess trait को इम्पोर्ट करना होगा। यह trait स्टोरेज वेरिएबल्स पर .write(value) विधि को सक्षम बनाता है, जिससे हम सीधे उनके स्टोरेज पॉइंटर्स के माध्यम से वैल्यूज असाइन कर सकते हैं।
यह starknet::storage मॉड्यूल में उपलब्ध है:
// Import `StoragePointerWriteAccess` trait
use starknet::storage::StoragePointerWriteAccess;
यहाँ बताया गया है कि विभिन्न सरल प्रकारों पर राइट ऑपरेशन्स कैसे करें (नए जोड़े गए कोड को /* NEWLY ADDED */ कमेंट के साथ एनोटेट किया गया है):
#[starknet::contract]
mod HelloStarknet {
use starknet::ContractAddress;
// Import `StoragePointerWriteAccess` trait
use starknet::storage::StoragePointerWriteAccess; /* NEWLY ADDED */
#[storage]
struct Storage {
user_id: felt252,
total_supply: u256,
is_paused: bool,
contract_name: bytes31,
contract_description: ByteArray,
owner_address: ContractAddress,
version_info: (u8, i8)
}
/* NEWLY ADDED */
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn write_vars(ref self: ContractState) {
// Writing to felt252
self.user_id.write(12345);
// Writing to u256
self.total_supply.write(1000000_u256);
// Writing to bool
self.is_paused.write(false);
// Writing to bytes31 (short string)
self.contract_name.write('HelloContract'.try_into().unwrap());
// Writing to ByteArray (long string)
self.contract_description.write("This is a very very very long textttt");
// Writing to ContractAddress
self.owner_address.write(0x1234.try_into().unwrap());
// Writing to tuple
self.version_info.write((1_u8, -2_i8));
}
}
}
Read Operations
रीड ऑपरेशन्स के लिए, हमें StoragePointerReadAccess trait को इम्पोर्ट करने की आवश्यकता है, जो हमें ऊपर घोषित प्रकारों पर .read() विधि का उपयोग करने देता है:
use starknet::storage::StoragePointerReadAccess;
पिछले अनुभाग के कॉन्ट्रैक्ट का विस्तार करते हुए, नीचे दिया गया कोड StoragePointerReadAccess trait को इम्पोर्ट करता है और स्टेट वेरिएबल्स से वैल्यू पढ़ता है (नए जोड़े गए कोड को /* NEWLY ADDED */ कमेंट के साथ एनोटेट किया गया है):
#[starknet::contract]
mod HelloStarknet {
use starknet::ContractAddress;
// Import `StoragePointerWriteAccess` trait
use starknet::storage::{
StoragePointerWriteAccess,
/* NEWLY ADDED */
StoragePointerReadAccess,
};
#[storage]
struct Storage {
user_id: felt252,
total_supply: u256,
is_paused: bool,
contract_name: bytes31,
contract_description: ByteArray,
owner_address: ContractAddress,
version_info: (u8, i8)
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn write_vars(ref self: ContractState) {
self.user_id.write(12345);
self.total_supply.write(1000000_u256);
self.is_paused.write(false);
self.contract_name.write('HelloContract'.try_into().unwrap());
self.contract_description.write("This is a very very very long textttt");
self.owner_address.write(0x1234.try_into().unwrap());
self.version_info.write((1_u8, -2_i8));
}
/* NEWLY ADDED */
fn read_vars(self: @ContractState) {
// felt252: Reading user ID returns a field element (0 to P-1 range)
let _ = self.user_id.read();
// u256: Reading large integer, useful for token balances and big numbers
let _ = self.total_supply.read();
// bool: Reading boolean state, returns true or false
let _ = self.is_paused.read();
// bytes31: Reading fixed-size byte array, used for short strings
let _ = self.contract_name.read();
// ByteArray: Reading dynamic-size byte array, used for long strings
let _ = self.contract_description.read();
// ContractAddress: Reading Starknet address, type-safe contract/user address
let _ = self.owner_address.read();
// Tuple: Reading compound type returns both values as (u8, i8) pair
let _ = self.version_info.read();
}
}
}
Mapping and Vec
डिक्शनरीज़ और एरे जैसे कलेक्शन प्रकारों को सीधे Cairo कॉन्ट्रैक्ट स्टोरेज में स्टोर नहीं किया जा सकता है। ऐसा इसलिए है क्योंकि वे डायनामिक मेमोरी लेआउट का उपयोग करते हैं जो डिफ़ॉल्ट रूप से स्टोरेज सिस्टम द्वारा समर्थित नहीं है। इसके बजाय, Cairo स्टोरेज में कलेक्शन्स के साथ काम करने के लिए विशेष प्रकार प्रदान करता है: Map और Vec।
ये विशेष प्रकार starknet::storage मॉड्यूल में उपलब्ध हैं, और कॉन्ट्रैक्ट स्टोरेज में मैपिंग और वेक्टर्स (vectors) घोषित करने के लिए उपयोग किए जाते हैं। इससे पहले कि हम अपने Storage स्ट्रक्ट में उनमें से किसी का उपयोग कर सकें, हमें नीचे दिखाए गए अनुसार कोर लाइब्रेरी से इन विशेष प्रकारों को स्पष्ट रूप से (explicitly) इम्पोर्ट करना होगा:
use starknet::storage::{ Map, Vec };
ध्यान दें कि
MapऔरVecको एक साथ इम्पोर्ट करने की आवश्यकता नहीं है, आप अपने उपयोग के मामले (use case) के आधार पर केवल उसी को इम्पोर्ट कर सकते हैं जिसकी आपको आवश्यकता है। उदाहरण के लिए, यदि आपके कॉन्ट्रैक्ट को केवल मैपिंग की आवश्यकता है, तो केवल Map प्रकार को इम्पोर्ट करना ठीक है।
एक बार जब हम Map और Vec को इम्पोर्ट कर लेते हैं, तो हम उन्हें स्टोरेज स्ट्रक्ट के अंदर उपयोग कर सकते हैं, कुछ इस तरह:
use starknet::storage::{ Map, Vec };
#[storage]
struct Storage {
// mapping(address => uint256) my_map;
my_map: Map<ContractAddress, u256>,
// uint64[] my_vec;
my_vec: Vec<u64>,
}
उपरोक्त कोड में प्रत्येक डिक्लेरेशन के ऊपर कमेंट में बताया गया है कि Solidity में Map और Vec प्रकारों को कैसे घोषित किया जाता है, जो इसके समतुल्य है। Map प्रकार दो जेनेरिक पैरामीटर लेता है: KeyType और ValueType। हमारे उदाहरण में, ContractAddress कुंजी (key) है और u256 वैल्यू है, जिसका अर्थ है कि यह मैप प्रत्येक एड्रेस के लिए u256 राशि (amount) स्टोर करता है। दूसरी ओर, Vec प्रकार, एक एकल प्रकार लेता है और उस प्रकार के तत्वों के एक एरे को दर्शाता है। हमारे उदाहरण में, यह 64-बिट अनसाइन्ड इंटीजर्स का एक एरे है।
ध्यान दें कि Cairo में Solidity और अन्य भाषाओं की तरह एक पारंपरिक “फिक्स्ड एरे” (fixed array) स्टोरेज प्रकार नहीं है।
इन स्टेट वेरिएबल्स के सेट होने के बाद, आइए देखें कि रीड और राइट ऑपरेशन्स का उपयोग करके उनके साथ कैसे इंटरैक्ट किया जाए।
Read and Write Operations on the Map Type
हमारे उदाहरण में, my_map एक एड्रेस से u256 वैल्यू की मैपिंग को दर्शाता है, ठीक उसी तरह जैसे हम Solidity में mapping(address => uint256) को परिभाषित करते हैं।
my_map: Map<ContractAddress, u256>
इन ऑपरेशन्स को करने से पहले, हमें सबसे पहले आवश्यक access traits को इम्पोर्ट करने की आवश्यकता है जो स्टोरेज से पढ़ने और उसमें लिखने में सक्षम बनाते हैं। वे भी starknet::storage मॉड्यूल में उपलब्ध हैं।
#[starknet::contract]
mod HelloStarknet {
// IMPORT MAP TYPE AND NECESSARY ACCESS TRAITS
use starknet::storage::{
Map,
StorageMapWriteAccess, // Enables .write(key, value) operations
StorageMapReadAccess, // Enables .read(key) operations
};
}
ये traits .write(key, value) और .read(key) जैसे मेथड्स को सक्षम करते हैं, जिनका हम आने वाले उदाहरणों में उपयोग करेंगे। उन्हें इम्पोर्ट किए बिना, हम अपने स्टोरेज कलेक्शन पर इनमें से कोई भी ऑपरेशन नहीं कर पाएंगे।
इसके सेट हो जाने पर, अब हम Map प्रकार के लिए राइट और रीड ऑपरेशन्स को इम्प्लीमेंट कर सकते हैं।
Write Operation
हम StorageMapWriteAccess द्वारा प्रदान की गई .write(key, value) विधि का उपयोग करते हैं। इसका सिंटैक्स (syntax) सीधा है:
self.my_map.write(key, value);
यहाँ बताया गया है कि प्रत्येक भाग क्या करता है:
self.my_mapस्टोरेज स्ट्रक्ट में घोषितMapको संदर्भित करता है।.write(...)वह विधि है जो मैप को अपडेट करती है।keyवह आइडेंटिफायर (identifier) है (हमारे मामले में, एकContractAddress) जिसके तहत वैल्यू स्टोर की जाएगी।valueवह वास्तविक डेटा है (हमारे मामले में, एकu256) जिसे सेव किया जाता है।
write() को की गई प्रत्येक कॉल या तो एक नया key-value पेयर सम्मिलित करेगी या यदि कुंजी पहले से मौजूद है तो मौजूदा वैल्यू को ओवरराइट (overwrite) कर देगी।
नीचे एक उदाहरण दिया गया है कि फंक्शन में .write() का उपयोग कैसे करें:
#[starknet::contract]
mod HelloStarknet {
//...
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn write_to_mapping(
ref self: ContractState,
user: ContractAddress,
amount: u256
) {
self.my_map.write(user, amount); // write operation
}
}
}
यह दिए गए user एड्रेस के लिए amount स्टोर करता है। आंतरिक रूप से (under the hood), Cairo कुंजी (key) के आधार पर इसे उपयुक्त स्टोरेज स्लॉट में लिखने का कार्य संभालता है।
Read Operation
हम StorageMapReadAccess द्वारा प्रदान की गई .read(key) विधि का उपयोग करते हैं। सिंटैक्स कुछ इस तरह दिखता है:
self.my_map.read(key);
यहाँ इसका विवरण (breakdown) दिया गया है कि यह क्या करता है:
self.my_mapस्टोरेज स्ट्रक्ट में मैप इंस्टेंस (instance) को संदर्भित करता है।.read(...)स्टोर की गई वैल्यू को एक्सेस करता है।keyवह आइडेंटिफायर है जिसे हम खोजना चाहते हैं, हमारे मामले में, एकContractAddressक्योंकि हमने उसे मैप की कुंजी के रूप में उपयोग किया है।
read() विधि कुंजी से जुड़ी वैल्यू लौटाती है। यदि कुंजी को पहले नहीं लिखा गया है, तो यह मैप के valueType के लिए डिफ़ॉल्ट वैल्यू लौटाती है (उदाहरण के लिए, u256 के लिए 0)।
उदाहरण:
#[starknet::contract]
mod HelloStarknet {
// ...
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn get_value(self: @ContractState, user: ContractAddress) -> u256 {
self.my_map.read(user) // read operation
}
}
}
यह फंक्शन किसी दिए गए user एड्रेस के लिए स्टोर की गई वैल्यू को पढ़ता है और उसे लौटाता है।
Nested Maps Operations
स्टोरेज में नेस्टेड मैपिंग (nested mapping) को पढ़ने या लिखने के लिए StoragePathEntry नामक एक अतिरिक्त access trait की आवश्यकता होती है। यह trait .entry(key) विधि को सक्षम बनाता है, जो किसी दी गई कुंजी के अंतर्गत स्टोर किए गए आंतरिक मैप्स (inner maps) तक एक्सेस प्रदान करता है।
दूसरे शब्दों में, जब हम नेस्टेड मैपिंग से निपटते हैं जहाँ वैल्यू स्वयं एक Map होती है, तो हम इसे सीधे .read() या .write() के साथ एक्सेस नहीं कर सकते। इसके बजाय, हमें आंतरिक परत (inner layer) तक पहुँचने के लिए पहले .entry(key) को कॉल करना होगा, फिर उस पर ऑपरेशन्स करने होंगे।
Declare a Nested Mapping
आइए स्टोरेज में अपनी नेस्टेड मैपिंग घोषित करें। यह एक टू-लेवल (two-level) मैप होगा जहाँ बाहरी मैप (outer map) कुंजी के रूप में एक ContractAddress (उपयोगकर्ता का एड्रेस) का उपयोग करता है, आंतरिक मैप (inner map) भी कुंजी के रूप में एक ContractAddress (एक टोकन एड्रेस) का उपयोग करता है और स्टोर की गई वैल्यू एक u256 है जो टोकन बैलेंस को दर्शाती है:
#[storage]
struct Storage {
// user_address => token_address => balance
two_level_mapping: Map<ContractAddress, Map<ContractAddress, u256>>,
}
Importing Required Trait
use starknet::storage::StoragePathEntry;
अनुक्रम (sequence) में अगली कुंजी का स्टोरेज पाथ (storage path) प्राप्त करने के लिए StoragePathEntry, .entry(key) विधि को सक्षम बनाता है।
हालाँकि .entry() हमें नेस्टेड लेयर (nested layer) तक एक्सेस देता है, लेकिन रीड या राइट ऑपरेशन्स करने के लिए यह अपने आप में पर्याप्त नहीं है। हमें अभी भी उन traits को इम्पोर्ट करने की आवश्यकता है जो उन विशिष्ट विधियों को सक्षम करते हैं। हमें किन सटीक traits की आवश्यकता होगी, यह इस बात पर निर्भर करता है कि हम ये ऑपरेशन्स कैसे कर रहे हैं।
हम स्टोरेज में नेस्टेड मैपिंग को पढ़ने और लिखने के दो तरीकों को देखेंगे।
Write and Read Operation
Method 1: हमेशा N परतों के लिए .entry() को चेन (chain) करता है (पूरी तरह से वैल्यू तक)
यह दृष्टिकोण प्रत्येक मैप लेयर के माध्यम से कई .entry() कॉल्स को चेन (chain) करता है, जब तक कि यह स्टोरेज स्लॉट तक नहीं पहुंच जाता। फिर, यह स्टोर की गई वैल्यू के साथ सीधे इंटरैक्ट करने के लिए .write(value) या .read() का उपयोग करता है।
#[starknet::contract]
mod HelloStarknet {
use starknet::ContractAddress;
// IMPORT TRAITS
use starknet::storage::{
Map,
StoragePathEntry, // Enables .entry(key)
/* ADDITIONAL TRAITS */
StoragePointerWriteAccess, // Enables .write(value)
StoragePointerReadAccess, // Enables .read()
};
#[storage]
struct Storage {
// user_address => token_address => balance
two_level_mapping: Map<ContractAddress, Map<ContractAddress, u256>>,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn write_nested_map(
ref self: ContractState,
key1: ContractAddress,
key2: ContractAddress,
value: u256,
) {
// WRITE OPERATION
self.two_level_mapping.entry(key1).entry(key2).write(value);
}
fn read_nested_map(
ref self: ContractState,
key1: ContractAddress,
key2: ContractAddress,
) -> u256 {
// READ OPERATION
self.two_level_mapping.entry(key1).entry(key2).read()
}
}
}
रीड और राइट ऑपरेशन्स के लिए, दोनों फंक्शन्स दो बार .entry() विधि का उपयोग करते हैं:
- पहला
.entry(key1)बाहरी मैप को एक्सेस करता है - दूसरा
.entry(key2)एक विशिष्ट स्टोरेज स्लॉट तक पहुंचने के लिए आंतरिक मैप में ड्रिल (drill) करता है
एक बार जब हम उस सटीक स्टोरेज स्थान पर पहुँच जाते हैं, तो हम उपयोग करते हैं:
- उस स्लॉट में सीधे वैल्यू लिखने के लिए
StoragePointerWriteAccesstrait द्वारा सक्षम.write(value)। - उस स्लॉट से वैल्यू पढ़ने के लिए
StoragePointerReadAccesstrait द्वारा सक्षम.read()।
Method 2: N-1 परतों के लिए .entry() को चेन करता है (सबसे भीतरी मैप पर रुकते हुए)
यह दृष्टिकोण .entry() विधि का उपयोग करके प्रत्येक मैप लेयर के माध्यम से तब तक ड्रिल डाउन करता है जब तक कि यह आंतरिक मैपिंग तक नहीं पहुँच जाता, फिर इसे पूर्ण (whole) रूप में मानता है और .write(key, value) और .read(key) का उपयोग करके सीधे इसके साथ इंटरैक्ट करता है।
#[starknet::contract]
mod HelloStarknet {
use starknet::ContractAddress;
// IMPORT TRAITS
use starknet::storage::{
Map,
StoragePathEntry, // Enables .entry(key) method
/* ADDITIONAL TRAITS */
StorageMapWriteAccess, // Enables .write(key, value) method
StorageMapReadAccess, // Enables .read(key) method
};
#[storage]
struct Storage {
// user_address => token_address => balance
two_level_mapping: Map<ContractAddress, Map<ContractAddress, u256>>,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn write_nested_map(
ref self: ContractState,
key1: ContractAddress,
key2: ContractAddress,
value: u256,
) {
// WRITE OPERATION
self.two_level_mapping.entry(key1).write(key2, value);
}
fn read_nested_map(
ref self: ContractState,
key1: ContractAddress,
key2: ContractAddress,
) -> u256 {
// READ OPERATION
self.two_level_mapping.entry(key1).read(key2)
}
}
}
रीड और राइट ऑपरेशन्स के लिए, ऊपर दिए गए कोड में प्रत्येक फंक्शन एक बार .entry(key1) विधि का उपयोग करता है:
- यह
.entry(key1)आंतरिक मैप तक एक्सेस देता है।
एक बार जब हमारे पास उस आंतरिक मैप का रेफरेंस आ जाता है, तो हम इसे एक नियमित Map की तरह मानते हैं:
StorageMapWriteAccesstrait द्वारा सक्षम.write(key2, value)वैल्यू कोkey2के अंतर्गत स्टोर करता है।StorageMapReadAccesstrait द्वारा सक्षम.read(key2)key2के अंतर्गत स्टोर की गई वैल्यू को पुनः प्राप्त (retrieve) करता है।
यह विधि आंतरिक वैल्यू को एक संपूर्ण मैप के रूप में मानती है, न कि एकल स्टोरेज स्लॉट के रूप में।
दोनों विधियाँ वैध हैं, डेवलपर को केवल इस आधार पर उपयुक्त traits को इम्पोर्ट करने की आवश्यकता है कि वे नेस्टेड मैपिंग के साथ कैसे इंटरैक्ट करने की योजना बनाते हैं।
इसके बाद, हम यह पता लगाएंगे कि Vec प्रकार पर समान ऑपरेशन्स कैसे करें, जिसमें नए तत्वों (elements) को पुश (push) करना और विशिष्ट इंडेक्स (indices) से पढ़ना शामिल है।
Read and Write Operations on the Vec Type
Cairo में Vec प्रकार का उपयोग कॉन्ट्रैक्ट स्टोरेज में एक ग्रोएबल (growable) एरे को दर्शाने के लिए किया जाता है, जो Solidity में uint64[] जैसे डायनामिक एरे (dynamic arrays) के समान है। यह सामान्य ऑपरेशन्स का समर्थन करता है जैसे नए तत्वों को जोड़ना (appending), इंडेक्स द्वारा आइटम एक्सेस करना और एरे के अंत से तत्वों को हटाना।
अपने उदाहरण के साथ जारी रखने के लिए, हम अपने स्टोरेज डिक्लेरेशन में Vec प्रकार के साथ इंटरैक्ट करेंगे:
#[storage]
struct Storage {
// Solidity equivalent: uint64[] my_vec;
my_vec: Vec<u64>,
}
लेकिन उससे पहले, हमारे पास Vec प्रकार से जुड़े दो traits हैं: VecTrait और MutableVecTrait।
VecTrait स्टोरेज में वेक्टर्स (vectors) के साथ इंटरैक्ट करने के लिए रीड-ओनली विधियाँ प्रदान करता है। इसमें शामिल हैं:
.len()– वेक्टर में तत्वों की वर्तमान संख्या लौटाता है। इसका रिटर्न टाइपu64है।.get(index)– सुरक्षित रूप से दिए गए इंडेक्स पर तत्व का पॉइंटर लौटाता है। यदि इंडेक्स आउट ऑफ बाउंड्स (out of bounds) है तोNoneलौटाता है।.at(index)– दिए गए इंडेक्स पर तत्व का पॉइंटर लौटाता है, लेकिन यदि इंडेक्स अमान्य है तो यह panic कर जाता है।
MutableVecTrait म्यूटेटिंग (mutating) विधियों को जोड़कर VecTrait का विस्तार करता है जो आपको स्टोरेज में वेक्टर की सामग्री (contents) को संशोधित (modify) करने की अनुमति देती हैं। इनमें शामिल हैं:
.push(value)– वेक्टर के अंत में एक नया तत्व जोड़ता है।.pop()– अंतिम तत्व को हटाता है और लौटाता है, या यदि वेक्टर खाली है तोNoneलौटाता है।.allocate()– वेक्टर के अंत में एक नए तत्व के लिए जगह आरक्षित (reserve) करता है और एक राइटेबल पॉइंटर (writable pointer) लौटाता है, जो जटिल या नेस्टेड प्रकारों के लिए उपयोगी है।
वेक्टर ऑपरेशन के आधार पर, हमें StoragePointerWriteAccess या StoragePointerReadAccess जैसे access traits को भी इम्पोर्ट करने की आवश्यकता हो सकती है।
अगले उप-अनुभागों (subsections) में, हम इन traits का उपयोग करके तत्वों को जोड़ने (appending), पढ़ने, अपडेट करने और हटाने जैसे सामान्य ऑपरेशन्स के उदाहरणों से गुजरेंगे।
my_vec Vector में एक नई वैल्यू पुश करना (Pushing a New Value)
push_number फंक्शन में उपयोग की जाने वाली push विधि पहले वेक्टर की लंबाई बढ़ाती है, फिर वेक्टर के अंत में एक नए स्टोरेज स्लॉट में वैल्यू लिखती है।
// IMPORT TRAITS
use starknet::storage::{Vec, MutableVecTrait};
fn push_number(ref self: ContractState, value: u64) {
// PUSH OPERATION
self.my_vec.push(value);
}
मौजूदा इंडेक्स से पढ़ना (Reading from an Existing Index)
यदि हम किसी मौजूदा इंडेक्स पर वैल्यू प्राप्त करना चाहते हैं, तो हम पॉइंटर प्राप्त करने के लिए .get() या .at() का उपयोग कर सकते हैं, फिर इसकी वैल्यू को पढ़ने के लिए .read() का उपयोग कर सकते हैं:
// IMPORT TRAITS
use starknet::storage::{Vec, MutableVecTrait, StoragePointerReadAccess};
fn read_my_vec(self: @ContractState, index: u64) -> u64 {
// VEC READ OPERATION
self.my_vec.at(index).read() // Will panic if index is out of bounds
}
अभ्यास (Exercise): हमने StoragePointerReadAccess trait क्यों जोड़ा?
मौजूदा इंडेक्स पर वैल्यू अपडेट करना (Updating a Value at an Existing Index)
किसी मौजूदा इंडेक्स पर वैल्यू अपडेट करने के लिए, हम पॉइंटर प्राप्त करने के लिए .get() या .at() का उपयोग कर सकते हैं, फिर इसकी वैल्यू को संशोधित करने के लिए .write(value) का उपयोग कर सकते हैं:
// IMPORT TRAITS
use starknet::storage::{Vec, MutableVecTrait, StoragePointerWriteAccess};
fn write_my_vec(ref self: ContractState, index: u64, val: u64) -> u64 {
// VEC WRITE OPERATION
self.my_vec.at(index).write(val) // Will panic if index is out of bounds
}
वेक्टर की लंबाई प्राप्त करना (Getting the Vector’s Length)
.len() वेक्टर में तत्वों की वर्तमान संख्या को u64 के रूप में लौटाता है।
// IMPORT TRAITS
use starknet::storage::{Vec, MutableVecTrait};
fn get_vec_len(self: @ContractState) -> u64 {
// RETURN VEC LENGTH
self.my_vec.len()
}
अंतिम तत्व को पॉप करना (Popping the Last Element)
use starknet::storage::{Vec, MutableVecTrait};
fn pop_last(ref self: ContractState) {
// POP OPERATION
let _ = self.my_vec.pop();
}
.pop() वेक्टर में अंतिम स्थान पर स्टोर की गई वैल्यू को प्राप्त करता है, वेक्टर की लंबाई को कम करता है और फिर प्राप्त की गई वैल्यू को लौटाता है या यदि वेक्टर खाली है तो None लौटाता है।
Struct and Enum Type in Storage
उन प्रकारों के विपरीत जो डिफ़ॉल्ट रूप से starknet::Store trait को इम्प्लीमेंट करते हैं (u8, bool, felt252, आदि), स्ट्रक्ट्स (structs) के लिए आपको स्पष्ट रूप से trait को डिराइव करने की आवश्यकता होती है, अन्यथा, उस स्ट्रक्ट को स्टोरेज में उपयोग करने का कोई भी प्रयास कंपाइल समय (compile time) पर विफल हो जाएगा।
किसी स्ट्रक्ट को स्टोरेज में पढ़ने या लिखने के लिए, उसे आवश्यक रीड और राइट फंक्शन्स को इम्प्लीमेंट करना चाहिए जो कि starknet::Store trait स्वचालित रूप से करता है।
Cairo स्टोरेज में एक स्ट्रक्ट को स्टोर करना संभव बनाने के लिए, हमें इसकी परिभाषा के ऊपर इस एट्रिब्यूट (attribute) #[derive(starknet::Store)] को जोड़कर trait को डिराइव करना होगा:
#[derive(starknet::Store)]
struct User {
id: u32,
name: bytes31,
is_admin: bool,
}
एक बार ऐसा हो जाने के बाद, स्ट्रक्ट का उपयोग स्टोरेज से संबंधित ऑपरेशन्स में किया जा सकता है, जिसमें मैपिंग और एरे में प्रकार के रूप में #[storage] स्ट्रक्ट के अंदर उपयोग करना शामिल है।
नीचे एक उदाहरण कॉन्ट्रैक्ट दिया गया है जो दर्शाता है कि स्टोरेज में कस्टम स्ट्रक्ट को कैसे घोषित किया जाए, आवश्यक traits को कैसे इम्पोर्ट किया जाए, और उस स्ट्रक्ट पर रीड और राइट ऑपरेशन्स कैसे किए जाएं।
#[starknet::contract]
mod HelloStarknet {
// IMPORT TRAITS
use starknet::storage::{
StoragePointerReadAccess, // Enables .read()
StoragePointerWriteAccess // Eabales .write(value)
};
// CUSTOM STRUCT DEFINITION
#[derive(starknet::Store)]
struct UserData {
id: u32,
name: bytes31,
is_admin: bool,
}
#[storage]
struct Storage {
// CUSTOM STRUCT DECLARATION
user: UserData,
}
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
// WRITE OPERATION
fn write_struct(ref self: ContractState, _id: u32, _name: bytes31, _is_admin: bool) {
self.user.id.write(_id); // Write to field 1
self.user.name.write(_name); // Write to field 2
self.user.is_admin.write(_is_admin); // Write to field 3
}
// READ OPERATION
fn read_struct(ref self: ContractState) -> (u32, bytes31, bool) {
let id = self.user.id.read(); // Read from field 1
let name = self.user.name.read(); // Read from field 2
let is_admin = self.user.is_admin.read(); // Read from field 3
(id, name, is_admin)
}
}
}
ध्यान दें कि हमने निम्नलिखित traits को इम्पोर्ट किया है:
use starknet::storage::{
StoragePointerReadAccess, // Enables .read() on storage paths
StoragePointerWriteAccess // Enables .write(value) on storage paths
};
हम इन traits को इम्पोर्ट करते हैं क्योंकि स्ट्रक्ट फील्ड्स सरल प्रकार (simple types) हैं, और उनके बिना, .read() और .write(value) जैसी कॉल्स कंपाइल नहीं होंगी।
Write Operation
write_struct फंक्शन के अंदर:
// WRITE OPERATION
fn write_struct(
ref self: ContractState,
_id: u32,
_name: bytes31,
_is_admin: bool
) {
self.user.id.write(_id); // Write to field 1
self.user.name.write(_name); // Write to field 2
self.user.is_admin.write(_is_admin); // Write to field 3
}
प्रत्येक कॉल स्टोर किए गए स्ट्रक्ट में एक विशिष्ट फील्ड में एक नई वैल्यू लिखती है। यह दर्शाता है कि भले ही स्ट्रक्ट को एक ऑब्जेक्ट के रूप में स्टोर किया जाता है, फिर भी इसके फील्ड्स को स्वतंत्र रूप से एक्सेस और अपडेट किया जा सकता है।
Read Operation
यह read_struct फंक्शन प्रत्येक फील्ड को व्यक्तिगत रूप से पढ़ता है और उन्हें एक टपल (tuple) के रूप में लौटाता है:
fn read_struct(ref self: ContractState) -> (u32, bytes31, bool) {
let id = self.user.id.read(); // Read from field 1
let name = self.user.name.read(); // Read from field 2
let is_admin = self.user.is_admin.read(); // Read from field 3
(id, name, is_admin)
}
Enum Type
एनम्स (Enums) स्ट्रक्ट्स के समान पैटर्न का पालन करते हैं, उन्हें स्टोर करने के लिए हमें स्पष्ट रूप से starknet::Store को डिराइव करना चाहिए। प्रत्येक वेरिएंट प्रकार को starknet::Store trait को भी इम्प्लीमेंट करना चाहिए। इसके अतिरिक्त, क्योंकि एनम्स में ऐसा डेटा हो सकता है जिसे वैल्यूज के बदले जाने या ड्रॉप किए जाने पर उचित क्लीनअप (cleanup) की आवश्यकता होती है, इसलिए हमें Drop trait को भी डिराइव करने की आवश्यकता है।
यहाँ एनम को परिभाषित करने और इसे स्टोरेज में उपयोग करने का एक बुनियादी उदाहरण दिया गया है:
#[starknet::contract]
mod HelloStarknet {
// DEFINE ENUM
#[derive(starknet::Store, Drop)]
enum UserRole {
Admin,
Mod,
#[default]
User,
}
#[storage]
struct Storage {
// DECLARE ENUM
my_role: UserRole,
}
}
हमारे एनम परिभाषा में, हम #[default] एट्रिब्यूट शामिल करते हैं, जो किसी भी एनम के लिए आवश्यक है जिसका उपयोग स्टोरेज में किया जाएगा। यह एट्रिब्यूट वेरिएंट्स में से एक को डिफ़ॉल्ट वैल्यू के रूप में चिह्नित (mark) करता है (हमारे मामले में, User वेरिएंट) जो तब असाइन किया जाता है जब स्टोरेज वैल्यू सेट नहीं की गई हो।
Write and Read Operations on Enum
निम्नलिखित कोड स्टोरेज में एनम को पढ़ने और लिखने के लिए आवश्यक access traits को इम्पोर्ट करता है, इसके बाद दो फंक्शन्स हैं जो इन ऑपरेशन्स को करते हैं:
// IMPORT TRAITS
use starknet::storage::{
StoragePointerReadAccess, // Enables .read() on enum stored in storage
StoragePointerWriteAccess, // Enables .write(value) on enum stored in storage
};
// WRITE OPERATION
fn write_enum(ref self: ContractState) {
// Write the Admin variant to storage
self.my_role.write(UserRole::Admin);
}
// READ OPERATION
fn read_enum(self: @ContractState) {
// Read the current value of the enum from storage
let _ = self.my_role.read();
}
Cairo में, Vec या Map जैसे कलेक्शन प्रकारों को स्ट्रक्ट में फील्ड्स के रूप में या एनम में वेरिएंट के रूप में शामिल नहीं किया जा सकता है क्योंकि वे डायनामिक मेमोरी लेआउट पर निर्भर करते हैं जिन्हें स्टोरेज सिस्टम डिफ़ॉल्ट रूप से समर्थन नहीं करता है।
// STRUCT: This will NOT work ❌ - Vec has dynamic size
#[derive(starknet::Store)]
struct InvalidUser {
name: felt252,
balance: u256,
friends: Vec<ContractAddress>, // ERROR: Cannot store Vec in struct
tokenBal: Map<ContractAddress, u256>, // ERROR: Cannot store Map in struct
}
// ENUM: This will also NOT work ❌ - Map has dynamic size
#[derive(starknet::Store)]
enum InvalidUserRole {
Admin: Map<felt252, bool>, // ERROR: Cannot store Map in enum variant
#[default]
User,
}
यदि हमें किसी स्ट्रक्ट के अंदर कलेक्शन्स को स्टोर करने की आवश्यकता है, तो हमें स्टोरेज नोड (storage node) नामक एक विशेष प्रकार के स्ट्रक्ट का उपयोग करना होगा।
Storage Nodes
स्टोरेज नोड्स अभी भी स्ट्रक्ट्स हैं, लेकिन एक मुख्य अंतर के साथ: उनमें Vec और Map जैसे डायनामिक कलेक्शन प्रकार हो सकते हैं। नियमित उपयोगकर्ता-परिभाषित स्ट्रक्ट्स जिनकी हमने पहले चर्चा की थी और जो कलेक्शन्स का समर्थन नहीं करते हैं, उनके विपरीत स्टोरेज नोड्स को विशेष रूप से उन्हें संभालने के लिए डिज़ाइन किया गया है, जो उन्हें स्टोरेज में नेस्टेड या डायनामिक डेटा के प्रबंधन के लिए आदर्श बनाता है।
Defining Storage Nodes
स्टोरेज नोड को परिभाषित करने के लिए, हम नियमित स्ट्रक्ट डिराइव्स (struct derives) के बजाय #[starknet::storage_node] एट्रिब्यूट का उपयोग करते हैं:
// Storage node - CAN contain collections
#[starknet::storage_node]
struct UserStorageNode {
name: felt252,
balance: u256,
friends: Vec<ContractAddress>, // ✅ Now allowed!
tokenBal: Map<ContractAddress, u256>, // ✅ Also allowed!
}
#[starknet::storage_node] एट्रिब्यूट कलेक्शन प्रकार के समर्थन की अनुमति देता है और स्वचालित रूप से आवश्यक स्टोरेज लॉजिक को संभालता है।
Using Storage Nodes
एक बार परिभाषित होने के बाद, स्टोरेज नोड्स को #[storage] स्ट्रक्ट के अंदर किसी भी अन्य प्रकार की तरह घोषित किया जा सकता है। उदाहरण के लिए, हम ऊपर परिभाषित स्टोरेज नोड प्रकार (UserStorageNode) के साथ एक स्टोरेज वेरिएबल user_data घोषित करेंगे, कुछ इस तरह:
#[storage]
struct Storage {
user_data: UserStorageNode,
}
इसके बाद, हम दिखाएंगे कि user_data स्टोरेज वेरिएबल को कैसे इनिशियलाइज़ (initialize) करें और उससे कैसे पढ़ें।
Storage Node Operations
Writing to Storage Nodes
स्टोरेज नोड्स में लिखने के लिए, हम स्टोरेज वेरिएबल (हमारे मामले में, user_data) के माध्यम से सीधे उनके फील्ड्स को एक्सेस करते हैं, फिर नीचे दिखाए गए अनुसार फील्ड प्रकार के आधार पर .write(key, value), .push(), या .entry(key).write(value) जैसे उपयुक्त स्टोरेज विधियों का उपयोग करते हैं:
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn write_nodes(ref self: ContractState) {
// Write to simple fields (felt252 and u256)
self.user_data.name.write(3);
self.user_data.balance.write(1000_u256);
// Push a new address to the friends vector
self.user_data.friends.push(get_caller_address());
// Write to nested map using either of the two valid approaches
// Approach 1
self.user_data.tokenBal.entry(get_caller_address()).write(23);
// Approach 2
self.user_data.tokenBal.write(get_caller_address(), 23);
}
}
Reading from Storage Nodes
स्टोरेज नोड्स से पढ़ना समान पैटर्न का पालन करता है: हम प्रत्येक फील्ड को सीधे एक्सेस करते हैं और सरल वैल्यूज के लिए .read(), विशिष्ट वेक्टर तत्व के लिए .at(index), या मैपिंग के लिए .read(key) कॉल करते हैं।
#[abi(embed_v0)]
impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
fn read_nodes(self: @ContractState) {
// Read simple values
let _ = self.user_data.name.read();
let _ = self.user_data.balance.read();
// Read a value from the vector at index 0
let _ = self.user_data.friends.at(0);
// Read token balance from the nested map
let _ = self.user_data.tokenBal.read(get_caller_address());
}
}
अभ्यास (Exercise): ऊपर स्टोरेज नोड्स में रीड और राइट ऑपरेशन्स को देखकर, निम्नलिखित ऑपरेशन्स को करने के लिए आवश्यक traits की सूची बनाएं:
.write(value).push(value).entry(key).write(key, value).read().at(index).read(key)
Conclusion
निष्कर्ष के रूप में (To wrap up), यहाँ Cairo में सबसे अधिक उपयोग किए जाने वाले स्टोरेज एक्सेस traits का सारांश दिया गया है। प्रत्येक trait विशिष्ट विधियों को सक्षम बनाता है जो हमें सरल प्रकारों, Map, Vec, स्ट्रक्ट्स और एनम्स जैसे स्टोरेज प्रकारों के साथ इंटरैक्ट करने की अनुमति देते हैं। इस आधार पर कि हम स्टोरेज से कैसे पढ़ना या उसमें कैसे लिखना चाहते हैं, हमें नीचे सूचीबद्ध उपयुक्त traits को इम्पोर्ट करने की आवश्यकता होगी:
| Trait | Enables Method(s) | उद्देश्य |
|---|---|---|
StoragePointerReadAccess |
.read() |
स्टोरेज पाथ (सरल प्रकार या स्ट्रक्ट फील्ड्स) से एक वैल्यू पढ़ें। |
StoragePointerWriteAccess |
.write(value) |
स्टोरेज पाथ (सरल प्रकार या स्ट्रक्ट फील्ड्स) में एक वैल्यू लिखें। |
StorageMapReadAccess |
.read(key) |
कुंजी (key) द्वारा Map से एक वैल्यू पढ़ें। |
StorageMapWriteAccess |
.write(key, value) |
कुंजी द्वारा Map में एक वैल्यू लिखें। |
StoragePathEntry |
.entry(key) |
नेस्टेड स्टोरेज (जैसे, नेस्टेड Map या स्टोरेज नोड) में गहराई तक नेविगेट करें। |
VecTrait |
.len(), .get(index), .at(index) |
Vec तक रीड-ओनली एक्सेस: लंबाई जांचें, वैकल्पिक रूप से या सीधे तत्व प्राप्त करें। |
MutableVecTrait |
.push(value), .pop(), .allocate() |
Vec को म्यूटेट करें: जोड़ें, हटाएं, या किसी तत्व के लिए जगह तैयार करें। |
यह लेख Cairo Programming on Starknet पर एक ट्यूटोरियल श्रृंखला का हिस्सा है