एक Enumerable ERC721, अतिरिक्त कार्यक्षमता वाला एक ERC721 है जो एक smart contract को किसी पते (address) के स्वामित्व वाले सभी NFTs को सूचीबद्ध (list) करने में सक्षम बनाता है। यह लेख वर्णन करता है कि ERC721Enumerable कैसे कार्य करता है और हम इसे किसी मौजूदा ERC721 प्रोजेक्ट में कैसे एकीकृत (integrate) कर सकते हैं। स्पष्टीकरण के लिए हम OpenZeppelin के ERC721Enumerable के लोकप्रिय कार्यान्वयन (implementation) का उपयोग करेंगे।
पूर्व आवश्यकताएँ (Prerequisites)
चूँकि ERC721Enumerable, ERC721 का ही एक विस्तार (extension) है, यह लेख मानकर चलता है कि पाठक ने हमारा ERC721 article पढ़ा है या उसे ERC721 मानक (standard) के बारे में ज्ञान है।
Swap और Pop
Solidity में किसी सूची (list) से किसी आइटम को हटाना आमतौर पर अंतिम एलीमेंट को हटाए जाने वाले आइटम के स्थान पर कॉपी करके, और फिर ऐरे को पॉप (अंतिम एलीमेंट को हटाना) करके किया जाता है। सभी एलीमेंट्स को बाईं ओर शिफ्ट करना गैस (gas) के दृष्टिकोण से बहुत महंगा होता है। किसी सूची से डिलीट करने की प्रक्रिया को नीचे दिए गए एनीमेशन में दिखाया गया है, जो index 1 (संख्या 5) पर मौजूद आइटम को हटा देता है:
ERC721Enumerable क्यों?
यह समझने के लिए कि हमें ERC721Enumerable जैसे एक्सटेंशन की आवश्यकता क्यों है, आइए एक उदाहरण परिदृश्य पर विचार करें। यदि हमें किसी विशेष ERC721 कॉन्ट्रैक्ट से एक वॉलेट के स्वामित्व वाले सभी NFTs का पता लगाना हो, तो हम ERC721 में उपलब्ध कार्यक्षमता के साथ इसे कैसे करेंगे?
हमें टोकन मालिक के पते के साथ balanceOf() फ़ंक्शन को कॉल करना होगा, जो हमें उस पते के स्वामित्व वाले NFTs की संख्या देगा। फिर, हम ERC721 कॉन्ट्रैक्ट में सभी tokenIDs पर लूप चलाएंगे और इनमें से प्रत्येक tokenIDs के लिए ownerOf() फ़ंक्शन को कॉल करेंगे।
मान लीजिए कि NFTs की कुल सप्लाई 1000 है और एक पते के पास दो NFTs हैं, पहला और आखिरी। यानी, उसके पास tokenIDs #1 और #1000 का स्वामित्व है।

उस पते के स्वामित्व वाले 2 tokenIDs (टोकन #1 और टोकन #1000) को खोजने के लिए, हमें एक कॉन्ट्रैक्ट में सभी NFTs पर लूप करना होगा और उस ID (1 से 1000 तक) पर ownerOf() की क्वेरी करनी होगी, जो कम्प्यूटेशनल रूप से काफी महंगा है। इसके अलावा, हम हमेशा कॉन्ट्रैक्ट में मौजूद सभी tokenIDs को नहीं जानते हैं, इसलिए हम शायद ऐसा न कर पाएं।
आने वाले अनुभागों में, हम सीखेंगे कि ERC721Enumerable इस समस्या को कैसे हल करता है।
टोकन ओनरशिप को ट्रैक करने का सीधा (Naïve) समाधान
किसी पते के स्वामित्व वाले प्रत्येक टोकन को ट्रैक करने का सीधा (naïve) समाधान यह है कि पते (address) से लेकर स्वामित्व वाले NFTs की सूची तक एक मैपिंग (mapping) स्टोर की जाए।
mapping(address owner => uint256[] ownedIDs) public ownedTokens;
हालाँकि, यह समाधान निम्नलिखित कारणों से अक्षम (inefficient) और अधूरा है:
-
यदि उपयोगकर्ता के पास बहुत सारे टोकन हैं, तो उनके ऐरे को पढ़ने वाले एक smart contract की मेमोरी में उस बहुत लंबे ऐरे को स्टोर करते समय गैस खत्म हो सकती है।
-
डेटा की सूची को स्टोर करने के लिए अधिक गैस-कुशल तरीके मौजूद हैं (जिन पर आगे चर्चा की गई है)।
-
यदि हम उपयोगकर्ता की टोकन सूची से किसी विशेष टोकन को हटाना चाहते हैं, तो उसे खोजने के लिए हमें पूरी सूची को स्कैन करना होगा। यदि ऐरे बहुत लंबा है, तो हमारी गैस खत्म हो सकती है।
समस्या 1 और 2 को हल करने के लिए ERC721 Enumerable मैपिंग के बजाय एक ऐरे का उपयोग करता है (अगला अनुभाग देखें) और तीसरी समस्या को हल करने के लिए, एक अतिरिक्त डेटा स्ट्रक्चर की आवश्यकता होती है, जो tokenID को उसके मौजूद index पर मैप करता है।
एक ऐरे के रूप में मैपिंग का उपयोग करना
मैपिंग का उपयोग ऐरे के समान ही किया जा सकता है, जहाँ कुंजियाँ (keys) index होती हैं और values ऐरे में उस इंडेक्स पर स्टोर की गई वैल्यू होती हैं।

यदि हम ऊपर दिए गए अपने उदाहरण में ऐरे को मैपिंग से बदल देते हैं, तो ऐरे के indexes की (key) बन जाते हैं, और tokenIDs वैल्यू बन जाते हैं।
Solidity में, ऐरे की तुलना में मैपिंग अधिक गैस कुशल होती हैं। जब भी ऐरे को इंडेक्स किया जाता है (यानी, इंडेक्स i के लिए, यह जांचता है कि क्या i < array.length), तो ऐरे की लंबाई स्पष्ट रूप से जांची जाती है। यह जांच ऐरे के उपयोग की गैस लागत को बढ़ा देती है। ऐरे के रूप में मैपिंग का उपयोग करके, हम इस जांच को छोड़ देते हैं, और इस प्रकार गैस बचाते हैं।
हालाँकि, ऐरे के विपरीत, मैपिंग में कोई अंतर्निहित (built-in) लंबाई की प्रॉपर्टी नहीं होती है, जिसका उपयोग हम कॉन्ट्रैक्ट में NFTs की कुल संख्या को ट्रैक करने के लिए कर सकें। इसलिए, मैपिंग हमेशा ऐरे का एक अच्छा विकल्प नहीं होती हैं।
अगले अनुभाग में, हम ERC721Enumerable के प्रत्येक डेटा स्ट्रक्चर को व्यक्तिगत रूप से गहराई से समझेंगे।
ERC721Enumerable: डेटा स्ट्रक्चर्स
ERC721 Enumerable दो चीज़ों को ट्रैक करता है:
- अस्तित्व में मौजूद सभी
tokenIDs। - किसी पते के स्वामित्व वाले सभी
tokenIDs।
1 को पूरा करने के लिए, यह डेटा स्ट्रक्चर्स _allTokens और _allTokensIndex का उपयोग करता है।
2 को पूरा करने के लिए, यह डेटा स्ट्रक्चर्स _ownedTokens और _ownedTokensIndex का उपयोग करता है।

सरलता के लिए, हम हर उदाहरण और स्पष्टीकरण के लिए tokenIDs के एक ही सेट का उपयोग करेंगे, यानी 2, 5, 9, 7, और 1।
_allTokens ऐरे (array):

_allTokens ऐरे हमें एक कॉन्ट्रैक्ट में सभी NFTs पर क्रमिक रूप से (sequentially) पुनरावृत्ति (iterate) करने की अनुमति देता है। _allTokens प्राइवेट ऐरे हर मौजूदा tokenID को (उसकी ओनरशिप स्थिति की परवाह किए बिना) रखता है।
प्रारंभ में, _allTokens में tokenIDs का क्रम इस बात पर निर्भर करता है कि उन्हें कब मिंट (mint) किया गया था। उपरोक्त आरेख में, tokenID #2 इंडेक्स #0 पर है क्योंकि इसे अन्य tokenIDs से पहले मिंट किया गया था। tokenIDs के बर्न (burn) होने पर यह क्रम बदल सकता है।
_allTokensIndex मैपिंग (mapping):
_allTokensIndex मैपिंग, एक tokenID दिए जाने पर, _allTokens ऐरे में उस tokenID का इंडेक्स लौटाती है।
किसी tokenID के लिए इंडेक्स खोजने के लिए _allTokens पर लूप चलाने के बजाय, हम _allTokensIndex मैपिंग का उपयोग करके _allTokens में इसका इंडेक्स खोजने के लिए स्वयं tokenID का उपयोग कर सकते हैं।
tokenID को तेज़ी से खोजने में सक्षम होना burn फ़ंक्शन को tokenID को कुशलता से हटाने में सक्षम बनाता है।

ऊपर दिया गया आरेख tokenIDs को उनके संगत इंडेक्स मानों के मैपिंग को दर्शाता है। tokenID #2 0th इंडेक्स पर मैप होता है क्योंकि यह कॉन्ट्रैक्ट में मिंट किया गया पहला टोकन था। यह मैपिंग पैटर्न मिंट होने वाले हर टोकन के लिए जारी रहता है।
_ownedTokens मैपिंग (mapping):
_ownedTokens मैपिंग का उपयोग किसी पते के स्वामित्व वाले tokenIDs को ट्रैक करने के लिए किया जाता है। इसमें एक नेस्टेड मैपिंग होती है (यानी, owner -> index -> tokenID)। यह प्रत्येक owner पते को एक index पर मैप करता है, जो पते के टोकन बैलेंस की सीमा (range) के भीतर होता है। प्रत्येक इंडेक्स उस पते के स्वामित्व वाले tokenID पर मैप होता है।

उपरोक्त आरेख में, पता ‘0xAli3c3’ 3 NFTs का मालिक है, और इस प्रकार 3 tokenIDs के लिए एक मैपिंग है। दूसरा पता (0xb0b) केवल एक टोकन का मालिक है, और इस प्रकार एक tokenID के लिए मैपिंग है। #2 के इंडेक्स पर, ‘0xAli3c3’ पते के लिए नेस्टेड मैपिंग tokenID #1 पर मैप होती है।
_ownedTokensIndex मैपिंग (mapping):
जिस तरह _allTokensIndex, _allTokens की मिरर इमेज (mirror image) है, ठीक उसी तरह _ownedTokenIndex, _ownedTokens की मिरर इमेज है।
_ownedTokensIndex उस उपयोगकर्ता के लिए tokenIDs से _ownedTokens में उस टोकन के इंडेक्स तक की एक मैपिंग है। नीचे दिए गए आरेख पर विचार करें:

यदि हम tokenID 2 या 9 को _ownedTokensIndex में डालते हैं, तो हमें दोनों के लिए 0 वापस मिलता है, क्योंकि यह Alice और Bob दोनों के लिए “पहला स्वामित्व वाला टोकन” है।
साथ ही _allTokensIndex की तरह, इस डेटा स्ट्रक्चर का उद्देश्य _ownedTokens में एक विशिष्ट tokenID खोजना है ताकि हम इसे कुशलतापूर्वक हटा सकें (जैसे कि जब उपयोगकर्ता टोकन ट्रांसफर करता है या बर्न करता है)।
चूंकि ये डेटा स्ट्रक्चर प्राइवेट हैं, इसलिए इनके साथ सीधे इंटरैक्ट नहीं किया जा सकता है। अगले अनुभाग में, हम उन फ़ंक्शन्स को समझेंगे जो इन डेटा स्ट्रक्चर्स को पढ़ते हैं और उनमें हेरफेर (manipulate) करते हैं।
ERC721Enumerable: फ़ंक्शन्स (Functions)
ERC721 दस्तावेज़ (documentation) के अनुसार, ERC721Enumerable में तीन पब्लिक फ़ंक्शन्स होते हैं:
totalSupply()

इस फ़ंक्शन का उपयोग कॉन्ट्रैक्ट में मौजूद NFTs की कुल संख्या प्राप्त करने के लिए किया जाता है। यह _allTokens ऐरे की लंबाई (length) लौटाता है।
tokenByIndex()

tokenByIndex, _allTokens ऐरे के चारों ओर एक सरल रैपर (wrapper) है, जो इनपुट के रूप में एक इंडेक्स लेता है और _allTokens ऐरे में उस इंडेक्स पर स्टोर किए गए tokenID को लौटाता है।
tokenOfOwnerByIndex()

यह फ़ंक्शन कुछ इनपुट वैलिडेशन के साथ _ownedTokens मैपिंग के चारों ओर एक रैपर है।

_ownedTokens मैपिंग के उपरोक्त उदाहरण में, पते ‘0xAli3c3’ के पास 3 tokenIDs हैं। यदि फ़ंक्शन को इस पते और 2 के index के साथ कॉल किया जाता है, तो tokenID #1 वापस मिल जाता है।
एन्यूमरेशन (Enumeration) से tokenIDs को जोड़ना/हटाना
इन फ़ंक्शन्स के अलावा, OpenZeppelin के ERC721Enumerable इम्प्लीमेंटेशन में 4 अतिरिक्त प्राइवेट फ़ंक्शन्स शामिल हैं, जिनका उपयोग _update फ़ंक्शन द्वारा यह सुनिश्चित करने के लिए किया जाता है कि ERC721Enumerable में डेटा स्ट्रक्चर्स वर्तमान टोकन ओनरशिप को दर्शाते हैं।
हम इन सभी फ़ंक्शन्स के विवरण में नहीं जाएंगे, क्योंकि वे ERC721 विशिष्टता (specification) का हिस्सा नहीं हैं। हालाँकि, आइए उनमें से एक पर नज़र डालें:
removeTokenFromOwnerEnumeration()

इस फ़ंक्शन का उपयोग तब किया जाता है जब किसी पते के एन्यूमरेशन डेटा स्ट्रक्चर्स से tokenID को हटाने की आवश्यकता होती है। यदि कोई मालिक अपना NFT बेचता है या बर्न करता है, तो उस NFT के tokenID को मालिक के पते से अलग (dissociate) करने की आवश्यकता होती है, यहीं पर _removeTokenFromOwnerEnumeration काम में आता है।
डिलीशन की प्रक्रिया (The Deletion Process)
डिलीशन होने से पहले, फ़ंक्शन _ownedTokensIndex मैपिंग का उपयोग यह जांचने के लिए करता है कि क्या tokenId मालिक के स्वामित्व वाले tokenIDs में अंतिम इंडेक्स पर है। यदि यह अंतिम इंडेक्स पर नहीं है, तो इसे अंतिम इंडेक्स वाले tokenID के साथ स्वैप (swap) कर दिया जाता है।
यह आवश्यक है क्योंकि यदि tokenID को सीधे डिलीट कर दिया जाए, तो मालिक के टोकन-इंडेक्स में एक गैप रह जाएगा जिसके कारण जब मालिक के पते के साथ balanceOf() फ़ंक्शन को कॉल किया जाएगा तो वह गलत परिणाम लौटाएगा।
इस स्वैप के बाद, फ़ंक्शन _ownedTokensIndex और _ownedTokens से tokenID (जो अब अंतिम tokenID है) को हटा देता है, जिससे टोकन प्रभावी रूप से एन्यूमरेशन से हट जाता है।
एक्सटेंशन में ऐसे बाकी फ़ंक्शन्स इस प्रकार हैं:
_addTokenToOwnerEnumeration: जब भी कोई tokenID मिंट किया जाता है या किसी गैर-शून्य (non-zero) पते पर ट्रांसफर किया जाता है, तो यह _ownedTokens और _ownedTokensIndex में एक tokenID जोड़ता है।
यह उस index को निर्धारित करने के लिए balanceOf() फ़ंक्शन का उपयोग करता है जिसे नए मिंट किए गए tokenID को सौंपा जा सकता है।
balanceOf() उस पते के लिए 3 लौटाएगा जिसके पास 3 tokenIDs हैं। इसका मतलब है कि नए मिंट किए गए tokenID को इंडेक्स #3 असाइन किया जा सकता है (क्योंकि इंडेक्सिंग 0 से शुरू होती है)।

_addTokenToAllTokensEnumeration: जब भी कोई tokenID मिंट किया जाता है, तो यह सभी NFTs को ट्रैक करने वाले डेटा स्ट्रक्चर्स (जैसे, _allTokensIndex) में एक tokenID जोड़ता है।

_removeTokenFromAllTokensEnumeration: जब डेटा स्ट्रक्चर्स को अपडेट रखने के लिए किसी tokenID को बर्न किया जाता है, तब इसका उपयोग किया जाता है।
_removeTokenFromAllTokensEnumeration एक डिलीशन प्रक्रिया का पालन करता है जो _removeTokenFromOwnerEnumeration के समान है।

सभी हिस्सों को एक साथ जोड़ना: _update फ़ंक्शन
जिन चार प्राइवेट फ़ंक्शन्स के बारे में हमने पिछले अनुभाग में संक्षेप में सीखा था, उनका उपयोग _update फ़ंक्शन द्वारा NFTs को मिंट करने, बर्न करने या ट्रांसफर करने के लिए किया जाता है।

जब भी किसी tokenID का ओनरशिप बदलता है तो इसे लागू (invoke) किया जाता है। फ़ंक्शन में कंडीशनल स्टेटमेंट्स (conditional statements) के दो जोड़े हैं। आइए समझें कि वे क्या कर रहे हैं:
कंडीशनल स्टेटमेंट्स (Conditional Statements) #1: सेंडर एड्रेस की जांच करना
पहली जोड़ी यह जांचती है कि क्या tokenID मिंट किया जा रहा है या ट्रांसफर किया जा रहा है। यह पिछले मालिक के डेटा स्ट्रक्चर्स से tokenID को हटाने का काम संभालती है। tokenID को एक मालिक सौंपने का काम अगले कंडीशनल स्टेटमेंट में संभाला जाता है।
स्थिति 1 (Case 1): टोकन मिंट किया गया है
यदि इसे मिंट किया जा रहा है, तो यह _addTokenToAllTokensEnumeration को कॉल करता है, जो _allTokens और _allTokensIndex में tokenID को जोड़ता है।

स्थिति 2 (Case 2): टोकन ट्रांसफर किया गया है
यदि इसे ट्रांसफर किया जा रहा है, तो _removeTokenFromOwnerEnumeration को कॉल किया जाता है, जो previousOwner पते के _ownedTokens और _ownedTokensIndex से tokenID को हटा देता है, जिसे फ़ंक्शन इनपुट के रूप में लेता है।

कंडीशनल स्टेटमेंट्स (Conditional Statements) #2: रिसीवर एड्रेस की जांच करना
पहली शर्त उस पते से संबंधित नहीं है जिस पर tokenID ट्रांसफर किया जा रहा है। यह दूसरा कंडीशनल स्टेटमेंट है जो यह जांचता है कि क्या tokenID बर्न किया जा रहा है या गैर-शून्य पते पर ट्रांसफर किया जा रहा है।
स्थिति 1 (Case 1): टोकन बर्न किया गया है
यदि इसे बर्न किया जा रहा है, तो _removeTokenFromAllTokensEnumeration फ़ंक्शन को कॉल किया जाता है, जो _allTokens और _allTokensIndex से tokenID को हटा देता है।

स्थिति 2 (Case 2): टोकन ट्रांसफर किया गया है
यदि इसे गैर-शून्य पते पर ट्रांसफर किया जा रहा है, तो _addTokenToOwnerEnumeration को कॉल किया जाता है, जो to पते के _ownedTokens और _ownedTokensIndex में tokenID जोड़ता है।

अपने प्रोजेक्ट में ERC721Enumerable जोड़ना
इस अनुभाग में, हम सीखेंगे कि 2 चरणों में अपने ERC721 कॉन्ट्रैक्ट में OpenZeppelin का ERC721Enumerable एक्सटेंशन कैसे जोड़ा जाए।
1. ERC721Enumerable इंपोर्ट करें
अपनी ERC721 फ़ाइल के शीर्ष पर, अपने बाकी इंपोर्ट्स के साथ कोड की निम्नलिखित पंक्ति जोड़ें:
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
उसके बाद, कॉन्ट्रैक्ट को निम्नलिखित तरीके से परिभाषित करें:
contract YourTokenName is ERC721, ERC721Enumerable{
}
2. फ़ंक्शन्स को ओवरराइड (Overriding) करना
ERC721Enumerable को शामिल करने के लिए ERC721 के कुछ फ़ंक्शन्स को ओवरराइड करने की आवश्यकता होती है। ये फ़ंक्शन्स हैं:
function _update(
address to,
uint256 tokenId,
address auth
) internal override(ERC721, ERC721Enumerable) returns (address) {
return super._update(to, tokenId, auth);
}
function _increaseBalance(address account, uint128 value)
internal
override(ERC721, ERC721Enumerable)
{
super._increaseBalance(account, value);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
नोट: ERC721 के अन्य एक्सटेंशन जो एक कस्टम balanceOf() फ़ंक्शन को लागू करते हैं (उदा. ERC721Consecutive), उनका उपयोग ERC721Enumerable एक्सटेंशन के साथ नहीं किया जा सकता है क्योंकि वे इसकी कार्यक्षमता के साथ छेड़छाड़ करते हैं।
एन्यूमरेशन की कीमत: ERC721Enumerable एक्सटेंशन की चेतावनियां (Caveats)
प्रत्येक ट्रांसफर के लिए, ERC721Enumerable में डेटा स्ट्रक्चर्स को अपडेट करना पड़ता है। यह कॉन्ट्रैक्ट को गैस-भारी (gas-heavy) बनाता है, जिससे गैस की लागत काफी बढ़ जाती है। हालाँकि, जिन प्रोजेक्ट्स को ऑन-चेन (on-chain) tokenIDs को सूचीबद्ध करना ही होता है, उनके लिए यह एक आवश्यक खर्च है।
लेखकत्व (Authorship)
यह लेख RareSkills में रिसर्च इंटर्न, Poneta द्वारा लिखा गया था।
RareSkills के साथ और जानें
उन्नत Solidity कॉन्सेप्ट्स सीखने के लिए हमारा Solidity Bootcamp देखें।
मूल रूप से 27 मार्च, 2024 को प्रकाशित