استاندارد ERC721Enumerable در سالیدیتی نسخهای پیشرفتهتر از ERC721 است که به قرارداد هوشمند این امکان را میدهد تا بتواند فهرستی از تمام NFTهایی که یک آدرس مالک آنهاست، ایجاد کند. در این مقاله، ساختار و عملکرد ERC721Enumerable را بررسی میکنیم و میبینیم چطور میتوان آن را در پروژههای ERC721 موجود پیاده سازی کرد. این مقاله بخشی از مسیر آموزش برنامه نویسی در حوزه قراردادهای هوشمند است و از پیاده سازی محبوب OpenZeppelin برای ERC721Enumerable به عنوان پایه توضیحات خود استفاده میکنیم.
پیش نیازها
از آنجا که ERC721Enumerable یک افزونه برای استاندارد ERC721 محسوب میشود، در این مقاله فرض بر این است که خواننده با استاندارد ERC721 در سالیدیتی آشنایی دارد یا مقاله قبلی ما درباره ERC721 را مطالعه کرده است.
روش Swap and Pop برای حذف از آرایه
در زبان Solidity، حذف یک عنصر از آرایه معمولاً با تکنیک «Swap and Pop» انجام میشود. در این روش، ابتدا آخرین عنصر آرایه را به جای عنصر مورد نظر برای حذف قرار می دهیم و سپس آخرین عنصر را از آرایه حذف (pop) می کنیم. این روش از نظر مصرف گس بسیار بهینهتر از جابهجایی همه عناصر به سمت چپ است.
در انیمیشن زیر، نحوه حذف عنصر با ایندکس ۱ (عدد ۵) با استفاده از این روش نشان داده شده است:
چرا به ERC721Enumerable نیاز داریم؟
برای درک بهتر اینکه چرا افزونهای مانند ERC721Enumerable ضروری است، بیایید یک سناریوی واقعی را بررسی کنیم.
فرض کنید میخواهیم تمام NFTهایی را که یک کیف پول خاص از یک قرارداد ERC721 دارد پیدا کنیم. با امکانات پایه استاندارد ERC721 چطور میتوان این کار را انجام داد؟
ابتدا باید تابع balanceOf()
را با آدرس مورد نظر صدا بزنیم تا تعداد NFTهایی که آن آدرس در اختیار دارد مشخص شود. اما این تابع فقط تعداد را برمیگرداند، نه شناسه آنها را.
سپس باید روی تمام شناسههای توکن (tokenID) موجود در قرارداد حلقه بزنیم و برای هر یک از آنها تابع ownerOf()
را اجرا کنیم تا ببینیم آیا به آن آدرس تعلق دارد یا نه.
اکنون فرض کنید کل عرضه این مجموعه ۱۰۰۰ توکن باشد و آدرس مورد نظر تنها مالک دو عدد از آنها باشد؛ مثلاً توکن شماره ۱ و توکن شماره ۱۰۰۰.
در این حالت باید ۱۰۰۰ بار تابع ownerOf()
را اجرا کنیم تا فقط دو NFT شناسایی شوند! این روش نه تنها ناکارآمد است، بلکه در مقیاسهای بزرگ بسیار پرهزینه از نظر گس خواهد بود.
برای یافتن این دو توکن (توکن شماره ۱ و توکن شماره ۱۰۰۰) که در مالکیت یک آدرس خاص هستند، باید روی تمام NFTهای موجود در قرارداد حلقه بزنیم و برای هر شناسه، تابع ownerOf()
را اجرا کنیم (از ۱ تا ۱۰۰۰). این فرآیند از نظر محاسباتی بسیار پرهزینه است.
از طرفی، همیشه نمیدانیم تمام tokenIDهای موجود در قرارداد چه هستند؛ بنابراین حتی ممکن است امکان اجرای این فرآیند وجود نداشته باشد.
در بخشهای بعدی، خواهیم دید که استاندارد ERC721Enumerable در سالیدیتی چگونه این مشکل را به شکلی بهینه حل میکند.
راه حل ساده برای پیگیری مالکیت توکن ها
یک راه حل ابتدایی برای ردیابی توکن هایی که هر آدرس در اختیار دارد، این است که یک mapping
از آدرس به لیستی از شناسههای NFT تعریف کنیم:
1 |
mapping(address owner => uint256[] ownedIDs) public ownedTokens; |
اما این روش به دلایل زیر ناکارآمد و ناقص است:
-
مصرف بالای گس در آرایههای بزرگ: اگر کاربر تعداد زیادی توکن داشته باشد، خواندن این آرایه در یک قرارداد هوشمند میتواند مقدار زیادی گس مصرف کند، چون ذخیره آرایههای طولانی در حافظه بسیار پرهزینه است.
-
روشهای بهینهتری برای ذخیرهسازی وجود دارد: بعدها خواهیم دید که میتوان دادهها را با استفاده از ساختارهای کمهزینهتری ذخیره کرد.
-
حذف توکن از آرایه دشوار است: اگر بخواهیم یک توکن خاص را از آرایه مربوط به یک کاربر حذف کنیم، باید کل آرایه را اسکن کنیم تا آن توکن را پیدا کنیم. این کار نیز در آرایههای بزرگ میتواند باعث تمام شدن گس شود.
استاندارد ERC721Enumerable در سالیدیتی برای حل مشکلات اول و دوم، بهجای استفاده از mapping
از آرایه استفاده میکند (که در بخش بعدی توضیح داده میشود). و برای حل مشکل سوم، یک ساختار داده اضافی در نظر گرفته شده که شناسه توکن را به اندیسی که در آن قرار دارد نگاشت میکند تا دسترسی و حذف آسانتر انجام شود.
استفاده از Mapping به جای آرایه
در سالیدیتی میتوان از mapping
به شکلی استفاده کرد که عملکردی شبیه به آرایه داشته باشد؛ به این صورت که کلیدها (keys) بهعنوان ایندکس (index) عمل میکنند و مقادیر (values) همان دادههایی هستند که در آن ایندکس ذخیره میشوند.
اگر در مثال قبلی، به جای آرایه از یک mapping استفاده کنیم، ایندکس های آرایه به عنوان کلید (key) در نظر گرفته می شوند و شناسه های توکن (tokenID) به عنوان مقدار (value) در آن ایندکس ذخیره می شوند.
در سالیدیتی، mapping نسبت به آرایه ها از نظر مصرف گس بهینه تر است. چرا که هنگام استفاده از آرایه، در هر بار دسترسی به یک ایندکس، بررسی ای به صورت ضمنی انجام می شود تا مطمئن شود مقدار i
کوچکتر از array.length
است. این بررسی، باعث افزایش مصرف گس می شود.
اما زمانی که از mapping استفاده می کنیم، چنین بررسی انجام نمی شود، در نتیجه مصرف گس کاهش می یابد.
با این حال، برخلاف آرایه ها، mapping به صورت پیش فرض ویژگی length
ندارد؛ یعنی نمی توان به راحتی تعداد کل NFTها یا عناصر ذخیره شده را پیگیری کرد. به همین دلیل، mapping همیشه جایگزین مناسبی برای آرایه ها نیست.
در بخش بعد، هر یک از ساختارهای داده ای مورد استفاده در استاندارد ERC721Enumerable را به صورت جداگانه بررسی خواهیم کرد.
ERC721Enumerable: ساختارهای داده
استاندارد ERC721Enumerable دو هدف اصلی را دنبال می کند:
-
ردیابی تمام tokenIDهایی که در قرارداد وجود دارند
-
ردیابی تمام tokenIDهایی که در مالکیت یک آدرس خاص هستند
برای دستیابی به هدف اول، از دو ساختار داده زیر استفاده می شود:
-
_allTokens
-
_allTokensIndex
و برای دستیابی به هدف دوم، از این دو ساختار داده استفاده می شود:
-
_ownedTokens
-
_ownedTokensIndex
برای ساده شدن توضیحات، در تمام مثال ها و توضیحات از یک مجموعه ثابت از tokenIDها استفاده می کنیم: یعنی ۲، ۵، ۹، ۷ و ۱.
آرایه _allTokens
آرایه _allTokens
به ما امکان میدهد که بتوانیم به صورت ترتیبی روی تمام NFTهای موجود در یک قرارداد پیمایش (iterate) انجام دهیم. این آرایهی خصوصی (private
) شامل تمام tokenIDهای موجود در قرارداد است، بدون توجه به این که در حال حاضر متعلق به چه کسی هستند.
در ابتدا، ترتیب tokenIDها در آرایهی _allTokens
بر اساس ترتیب مینت شدن آنها تعیین میشود. برای مثال، در دیاگرام بالا، tokenID شماره ۲ در ایندکس ۰ قرار دارد چون زودتر از بقیه مینت شده است.
البته باید توجه داشت که این ترتیب میتواند پس از سوزاندن (burn) یک توکن تغییر کند.
mapping مربوط به _allTokensIndex
mapping به نام _allTokensIndex
برای این طراحی شده که با گرفتن یک tokenID، ایندکس مربوط به آن را در آرایه _allTokens
برگرداند.
به جای آن که برای پیدا کردن ایندکس یک tokenID در آرایه _allTokens
، مجبور باشیم کل آرایه را پیمایش کنیم، میتوانیم مستقیماً از خود tokenID به عنوان کلید استفاده کنیم تا ایندکس آن را از طریق mapping _allTokensIndex
بهدست آوریم.
توانایی دسترسی سریع به ایندکس یک tokenID باعث میشود که تابع burn
بتواند آن توکن را به شکل بهینه و سریع از آرایه حذف کند.
در تصویر بالا، ساختار mapping بهوضوح نمایش داده شده است: برای مثال، tokenID شماره ۲ به ایندکس ۰ نگاشت میشود، چون اولین توکنی بوده که در قرارداد مینت شده است. این الگو برای همه توکنهایی که مینت میشوند ادامه پیدا میکند.
mapping مربوط به _ownedTokens
mapping به نام _ownedTokens
برای ردیابی tokenIDهایی استفاده میشود که متعلق به یک آدرس خاص هستند. این mapping بهصورت تو در تو تعریف شده است و ساختار آن به این صورت است: tokenID → ایندکس → address مالک
یعنی ابتدا آدرس مالک را مشخص میکنیم، سپس با استفاده از ایندکس (که بین صفر تا balanceOf
آن آدرس است) به tokenIDهایی میرسیم که آن آدرس در اختیار دارد.
در دیاگرام بالا، آدرس 0xAli3c3
مالک ۳ عدد NFT است و به همین دلیل، mapping آن شامل ۳ tokenID مختلف است. آدرس دیگر یعنی 0xb0b
فقط مالک یک توکن است، بنابراین mapping مربوط به آن شامل فقط یک tokenID میباشد.
برای نمونه، ایندکس شماره ۲ در mapping مربوط به آدرس 0xAli3c3
به tokenID شماره ۱ نگاشت میشود.
mapping مربوط به _ownedTokensIndex
درست همانطور که _allTokensIndex
تصویر معکوس آرایه _allTokens
بود، mapping به نام _ownedTokensIndex
نیز تصویر معکوس mapping مربوط به _ownedTokens
است.
این mapping از یک tokenID به ایندکس آن token در ساختار _ownedTokens
برای یک کاربر خاص نگاشت میدهد. نمودار زیر را در نظر بگیرید:
در دیاگرام فرضی بالا، اگر tokenID شماره ۲ یا ۹ را در mapping مربوط به _ownedTokensIndex
وارد کنیم، مقدار ۰ برمیگردد. چرا؟ چون این توکنها، اولین توکن های متعلق به Alice و Bob هستند.
دقیقاً مانند _allTokensIndex
، هدف از استفاده از این ساختار داده این است که بتوانیم به صورت سریع و بهینه، یک tokenID خاص را در _ownedTokens
پیدا کنیم تا در صورت نیاز آن را حذف کنیم (مثلاً وقتی کاربر آن را انتقال میدهد یا میسوزاند).
نکته مهم این است که تمام این ساختارهای داده به صورت private
تعریف شده اند، بنابراین از خارج قرارداد نمیتوان بهطور مستقیم با آنها تعامل داشت. در بخش بعدی، توابعی را بررسی خواهیم کرد که برای خواندن یا تغییر این ساختارها به کار میروند.
توابع ERC721Enumerable در سالیدیتی
بر اساس مستندات رسمی ERC721، استاندارد ERC721Enumerable سه تابع عمومی مهم دارد که امکان شمارش و فهرستبرداری از توکن ها را فراهم میکنند:
totalSupply()
تابع totalSupply()
برای دریافت تعداد کل توکن های NFT موجود در قرارداد استفاده میشود. این تابع طول آرایه _allTokens
را بازمیگرداند؛ یعنی مشخص میکند چند توکن تا این لحظه مینت شدهاند (چه مالک داشته باشند و چه نداشته باشند).
tokenByIndex()
تابع tokenByIndex()
در واقع یک wrapper ساده بر روی آرایه _allTokens
است. با دریافت یک ایندکس عددی، tokenID مربوط به آن ایندکس در آرایه _allTokens
را باز میگرداند.
به عنوان مثال اگر tokenByIndex(0)
فراخوانی شود، توکن مینتشده در ایندکس صفر (اولین توکن ایجادشده) را بازمیگرداند.
tokenOfOwnerByIndex()
تابع tokenOfOwnerByIndex()
نیز wrapper بر روی mapping مربوط به _ownedTokens
است. با دریافت آدرس یک مالک و یک ایندکس، tokenID مربوط به آن ایندکس برای آن آدرس خاص را بازمیگرداند.
در مثال بالا از mapping مربوط به _ownedTokens
، آدرس 0xAli3c3
مالک ۳ توکن است. اگر تابع tokenOfOwnerByIndex
با این آدرس و ایندکس عدد ۲ فراخوانی شود، tokenID شماره ۱ بازگردانده خواهد شد.
افزودن و حذف tokenIDها از لیستهای شمارشی (Enumeration)
علاوه بر توابع عمومیای که پیشتر معرفی شد، پیادهسازی OpenZeppelin از ERC721Enumerable شامل ۴ تابع خصوصی دیگر نیز هست که وظیفه آنها بهروزرسانی ساختارهای دادهای مثل _allTokens
و _ownedTokens
است. این توابع به صورت داخلی در تابع _update
فراخوانی میشوند تا مالکیت هر توکن به درستی در تمام ساختارها ثبت یا حذف شود.
ما وارد جزئیات تمام این توابع نخواهیم شد، چون این توابع بخشی از مشخصات استاندارد ERC721 نیستند. با این حال، اجازه بدهید نگاهی به یکی از آنها بیندازیم:
تابع _removeTokenFromOwnerEnumeration
این تابع در مواقعی استفاده میشود که یک توکن باید از لیست توکن های متعلق به یک آدرس حذف شود. این موقعیت معمولاً زمانی رخ میدهد که:
-
مالک توکن را منتقل میکند
-
یا توکن سوزانده (burn) میشود
در چنین شرایطی، توکن مربوطه دیگر نباید در لیست دارایی های آدرس قبلی باقی بماند، و این همان کاری است که _removeTokenFromOwnerEnumeration
انجام میدهد: بهروزرسانی و حذف صحیح tokenID
از mapping مربوط به مالک قبلی.
فرآیند حذف
قبل از حذف، تابع با استفاده از نگاشت _ownedTokensIndex
بررسی میکند که آیا tokenId
در آخرین ایندکس از توکن های متعلق به مالک قرار دارد یا نه. اگر اینطور نباشد، tokenId
با توکن قرارگرفته در آخرین ایندکس جابجا میشود.
این کار ضروری است، چون اگر tokenId
مستقیماً حذف شود، در ایندکس های توکن های مالک یک شکاف باقی میماند که باعث میشود تابع balanceOf()
هنگام فراخوانی با آدرس مالک، نتایج نادرست برگرداند.
بعد از این جابجایی، تابع tokenId
(که حالا در انتهای لیست قرار دارد) را از نگاشت های _ownedTokensIndex
و _ownedTokens
حذف میکند و در عمل آن توکن را از شمارش خارج میسازد.
سایر توابع موجود در این افزونه به شرح زیر هستند:
_addTokenToOwnerEnumeration
این تابع هنگام ضرب (mint) یا انتقال یک tokenId
به آدرس غیر صفر، آن را به ساختارهای _ownedTokens
و _ownedTokensIndex
اضافه میکند.
برای تعیین ایندکس اختصاص داده شده به tokenId
جدید، از تابع balanceOf()
استفاده میشود.
مثلاً اگر تابع balanceOf()
برای یک آدرس مقدار ۳ را برگرداند (یعنی آن آدرس ۳ توکن دارد)، پس tokenId
جدید در ایندکس ۳ قرار میگیرد (چون ایندکس گذاری از صفر شروع میشود).
_addTokenToAllTokensEnumeration
این تابع هنگام ضرب یک tokenId
آن را به ساختارهای داده ای که تمام توکن ها را پیگیری میکنند اضافه میکند (مانند _allTokensIndex
).
_removeTokenFromAllTokensEnumeration
از این تابع زمانی استفاده میشود که یک tokenId
سوزانده شود تا ساختارهای داده بهروزرسانی شوند.
تابع _removeTokenFromAllTokensEnumeration
از فرآیندی مشابه با _removeTokenFromOwnerEnumeration
برای حذف استفاده میکند.
کنار هم گذاشتن تکه ها: تابع _update
چهار تابع خصوصی که در بخش قبل بهطور خلاصه بررسی کردیم، در تابع _update
مورد استفاده قرار میگیرند تا عملیات ضرب (mint)، سوزاندن (burn)، یا انتقال (transfer) توکن های NFT را انجام دهند.
این تابع هر بار که مالکیت یک tokenId
تغییر میکند فراخوانی میشود. داخل این تابع، دو جفت شرط if
وجود دارد که عملکرد آنها به شرح زیر است:
شرطهای اول: بررسی آدرس فرستنده (Sender)
اولین جفت شرط بررسی میکند که آیا tokenId
در حال ضرب (mint) شدن است یا در حال انتقال. هدف این بخش، حذف tokenId
از ساختارهای داده مالک قبلی است. تخصیص مالک جدید به tokenId
در شرط بعدی انجام میشود.
حالت اول: توکن ضرب میشود (Mint)
اگر توکن در حال ضرب باشد، تابع _addTokenToAllTokensEnumeration
فراخوانی میشود. این تابع tokenId
را به آرایه های _allTokens
و نگاشت _allTokensIndex
اضافه میکند.
حالت دوم: توکن منتقل میشود (Transfer)
اگر توکن در حال انتقال باشد، تابع _removeTokenFromOwnerEnumeration
فراخوانی میشود. این تابع tokenId
را از ساختارهای _ownedTokens
و _ownedTokensIndex
مربوط به آدرس مالک قبلی حذف میکند (که به عنوان ورودی به تابع داده شده است).
شرطهای دوم: بررسی آدرس گیرنده (Receiver)
شرط اول به آدرسی که قرار است tokenId
به آن منتقل شود کاری ندارد. این شرط دوم است که بررسی میکند آیا tokenId
در حال سوزاندن (burn) است یا در حال انتقال به یک آدرس معتبر (غیر صفر).
حالت اول: توکن سوزانده میشود (Burn)
اگر توکن در حال سوزانده شدن باشد، تابع _removeTokenFromAllTokensEnumeration
فراخوانی میشود که tokenId
را از ساختارهای _allTokens
و _allTokensIndex
حذف میکند.
حالت دوم: توکن منتقل میشود (Transfer)
اگر توکن به یک آدرس معتبر (غیر صفر) منتقل شود، تابع _addTokenToOwnerEnumeration
فراخوانی میشود. این تابع tokenId
را به ساختارهای _ownedTokens
و _ownedTokensIndex
آدرس مقصد اضافه میکند.
افزودن ERC721Enumerable به پروژه شما
در این بخش یاد میگیریم که چگونه افزونه ERC721Enumerable
از کتابخانه OpenZeppelin را در دو مرحله به قرارداد ERC721 خود اضافه کنیم.
1. وارد کردن ERC721Enumerable
در ابتدای فایل قرارداد ERC721 خود، خط کد زیر را در کنار سایر import
ها اضافه کنید:
1 |
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; |
1 2 3 |
contract YourTokenName is ERC721, ERC721Enumerable{ } |
2. بازنویسی توابع (Overriding Functions)
افزودن ERC721Enumerable نیاز دارد که چند تابع از ERC721 بازنویسی (override) شوند. این توابع عبارتند از:
تابع _update
1 2 3 4 5 6 7 |
function _update( address to, uint256 tokenId, address auth ) internal override(ERC721, ERC721Enumerable) returns (address) { return super._update(to, tokenId, auth); } |
_increaseBalance
1 2 3 4 5 6 |
function _increaseBalance(address account, uint128 value) internal override(ERC721, ERC721Enumerable) { super._increaseBalance(account, value); } |
supportsInterface
1 2 3 4 5 6 7 8 |
function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) { return super.supportsInterface(interfaceId); } |
balanceOf()
سفارشی دارند (مانند ERC721Consecutive)، نمیتوانند همراه با افزونه ERC721Enumerable استفاده شوند، زیرا در عملکرد آن اختلال ایجاد میکنند.
شمارش با هزینه: نکات مهم در مورد افزونه ERC721Enumerable
در هر انتقال توکن، ساختارهای داده ای موجود در ERC721Enumerable باید بهروزرسانی شوند. این موضوع باعث میشود قرارداد مقدار قابل توجهی گس مصرف کند و سنگینتر شود.
با این حال، در پروژههایی که نیاز دارند شناسه توکن ها (tokenIDs) به صورت درون زنجیرهای (on-chain) قابل لیست شدن باشند، این هزینه اجتنابناپذیر است و باید آن را به عنوان بخشی از طراحی پذیرفت.
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۳۰ اردیبهشت ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++C
- ADO.NET
- Adobe Flash
- Ajax
- AngularJS
- apache
- ARM
- Asp.Net
- ASP.NET MVC
- AVR
- Bootstrap
- CCNA
- CCNP
- CMD
- CSS
- Dreameaver
- EntityFramework
- HTML
- IOS
- jquery
- Linq
- Mysql
- Oracle
- PHP
- PHPMyAdmin
- Rational Rose
- silver light
- SQL Server
- Stimulsoft Reports
- Telerik
- UML
- VB.NET&VB6
- WPF
- Xml
- آموزش های پروژه محور
- اتوکد
- الگوریتم تقریبی
- امنیت
- اندروید
- اندروید استودیو
- بک ترک
- بیسیک فور اندروید
- پایتون
- جاوا
- جاوا اسکریپت
- جوملا
- دلفی
- دوره آموزش Go
- دوره های رایگان پیشنهادی
- زامارین
- سئو
- ساخت CMS
- سی شارپ
- شبکه و مجازی سازی
- طراحی الگوریتم
- طراحی بازی
- طراحی وب
- فتوشاپ
- فریم ورک codeigniter
- فلاتر
- کانستراکت
- کریستال ریپورت
- لاراول
- معماری کامپیوتر
- مهندسی اینترنت
- هوش مصنوعی
- یونیتی
- کتاب های آموزشی
- Android
- ASP.NET
- AVR
- LINQ
- php
- Workflow
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس