مشروع تطبيقي: برمجة عقد ذكي لسك (Minting) مجموعة NFTs لصور فنية

دقائق القراءة: 6

مشروع تطبيقي: برمجة عقد ذكي لسك مجموعة NFTs لصور فنية

في هذا المشروع سنبني عقداً ذكياً عملياً لسك مجموعة صور فنية بصيغة NFTs اعتماداً على معيار ERC-721: ما هي الرموز غير القابلة للاستبدال (NFTs) برمجياً؟. الفكرة ليست مجرد كتابة كود قابل للنشر، بل فهم البنية الصحيحة لعقد إنتاجي يوازن بين الأمان، وضبط الإمداد الكلي، وربط البيانات الوصفية الخاصة بالصور الفنية بطريقة قابلة للتوسع.

إذا كنت قد مررت سابقاً على مدخل إلى Web3: ما هو البلوكتشين ولماذا يغير شكل الإنترنت والأنظمة المالية؟ فستدرك أن هذا النوع من التطبيقات يمثل حالة استخدام مباشرة لـ Blockchain في إثبات الملكية الرقمية. أما من جهة التطوير، فسنستخدم بنية قائمة على استخدام مكتبة OpenZeppelin لكتابة عقود ذكية آمنة ومختبرة مسبقاً لتقليل مخاطر الأخطاء الشائعة.

تصور المشروع قبل كتابة الكود

سنفترض أننا نريد إطلاق مجموعة فنية تحتوي على عدد محدود من الأعمال، مثل 500 صورة. كل عملية سك تنتج رمزاً فريداً يحمل tokenId مختلفاً، ويرتبط بملف بيانات وصفية خارجي يحتوي اسم العمل الفني وصورته ووصفه وخصائصه.

من الناحية العملية، العقد الذكي يجب أن يحقق عدة شروط: تحديد سقف أقصى للسك، فرض سعر لكل عملية سك إذا كان البيع مدفوعاً، تمكين المالك من إيقاف وفتح السك، وإتاحة سحب الأموال المجمعة. هنا سنستفيد من مفاهيم المعدلات (Modifiers): حماية الدوال برمجياً والتعامل مع الأخطاء وإرجاع الأموال: استخدام require, assert, revert.

المتطلبات البرمجية والأدوات

يمكنك تنفيذ هذا المشروع عبر Remix IDE أو داخل بيئة محلية باستخدام Hardhat. إذا كنت ما زلت تجهز البيئة، فراجع إعداد بيئة التطوير: تثبيت محفظة MetaMask والاتصال بشبكات الاختبار (Testnets) ثم الحصول على عملات تجريبية مجانية (Faucet) للبدء في نشر واختبار العقود الذكية.

  • مكتبة OpenZeppelin Contracts
  • مترجم Solidity ^0.8.20
  • محفظة MetaMask
  • اختبار عبر Ethers.js أو من خلال واجهة Remix

بناء العقد الذكي لمجموعة الصور الفنية

سنستخدم الوراثة من عقود جاهزة مثل ERC721 وOwnable. إن كنت تريد أساساً نظرياً أعمق، فراجع الوراثة (Inheritance): بناء عقود ذكية متقدمة بالاعتماد على أكواد عقود سابقة والدوال (Functions) في Solidity: من يمكنه قراءة وتعديل بيانات العقد؟.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

contract ArtCollectionNFT is ERC721, Ownable {
    using Strings for uint256;

    uint256 public nextTokenId;
    uint256 public immutable maxSupply;
    uint256 public mintPrice;
    bool public mintEnabled;

    string private baseTokenURI;

    event NFTMinted(address indexed minter, uint256 indexed tokenId);
    event MintStatusChanged(bool enabled);
    event BaseURIUpdated(string newBaseURI);
    event MintPriceUpdated(uint256 newPrice);

    constructor(
        string memory name_,
        string memory symbol_,
        string memory baseURI_,
        uint256 maxSupply_,
        uint256 mintPrice_
    ) ERC721(name_, symbol_) Ownable(msg.sender) {
        baseTokenURI = baseURI_;
        maxSupply = maxSupply_;
        mintPrice = mintPrice_;
        mintEnabled = false;
        nextTokenId = 1;
    }

    function mintNFT(uint256 quantity) external payable {
        require(mintEnabled, "Mint is not enabled");
        require(quantity > 0, "Quantity must be greater than zero");
        require(nextTokenId + quantity - 1 <= maxSupply, "Max supply exceeded");
        require(msg.value == mintPrice * quantity, "Incorrect ETH value");

        for (uint256 i = 0; i < quantity; i++) {
            uint256 tokenId = nextTokenId;
            nextTokenId++;
            _safeMint(msg.sender, tokenId);
            emit NFTMinted(msg.sender, tokenId);
        }
    }

    function ownerMint(address to, uint256 quantity) external onlyOwner {
        require(to != address(0), "Invalid recipient");
        require(quantity > 0, "Quantity must be greater than zero");
        require(nextTokenId + quantity - 1 <= maxSupply, "Max supply exceeded");

        for (uint256 i = 0; i < quantity; i++) {
            uint256 tokenId = nextTokenId;
            nextTokenId++;
            _safeMint(to, tokenId);
            emit NFTMinted(to, tokenId);
        }
    }

    function setMintEnabled(bool enabled) external onlyOwner {
        mintEnabled = enabled;
        emit MintStatusChanged(enabled);
    }

    function setMintPrice(uint256 newPrice) external onlyOwner {
        mintPrice = newPrice;
        emit MintPriceUpdated(newPrice);
    }

    function setBaseURI(string calldata newBaseURI) external onlyOwner {
        baseTokenURI = newBaseURI;
        emit BaseURIUpdated(newBaseURI);
    }

    function totalMinted() external view returns (uint256) {
        return nextTokenId - 1;
    }

    function _baseURI() internal view override returns (string memory) {
        return baseTokenURI;
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_ownerOf(tokenId) != address(0), "Token does not exist");
        return string(abi.encodePacked(_baseURI(), tokenId.toString(), ".json"));
    }

    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "No funds available");

        (bool success, ) = payable(owner()).call{value: balance}("");
        require(success, "Withdrawal failed");
    }
}

شرح البنية الداخلية للعقد

1) إدارة الإمداد وسلسلة المعرفات

المتغير nextTokenId يبدأ من 1 ويزداد مع كل سك. هذه الطريقة أوضح من الاعتماد على فهارس معقدة، كما أنها تسهل تكوين روابط metadata مثل 1.json و2.json.

وضعنا أيضاً متغير maxSupply كقيمة immutable حتى لا يتم العبث بعدد القطع الفنية بعد النشر. هذه نقطة ثقة مهمة جداً للمستخدمين والمشترين.

2) ربط الصور الفنية بالبيانات الوصفية

الدالة tokenURI تقوم بتجميع baseURI مع رقم الرمز وامتداد .json. هذا الأسلوب شائع جداً لأن ملفات البيانات الوصفية تكون مخزنة على IPFS أو خادم ثابت.

مثال ملف الوصف قد يتضمن الاسم والوصف ورابط الصورة وقائمة السمات. العقد لا يخزن الصورة نفسها، بل يشير إليها فقط، لأن تخزين الملفات الكبيرة على السلسلة مكلف جداً من ناحية التكاليف (Gas Fees): كيف يحسب البلوكتشين تكلفة تنفيذ الأكواد؟.

3) السك المدفوع وسحب الإيرادات

الدالة mintNFT تستقبل عدداً من الوحدات المراد سكها وتتحقق من حالة البيع والسعر وعدد القطع المتبقية. استخدمنا payable لأن العملية تتضمن استقبال ETH، ويمكنك التوسع في هذه النقطة عبر استلام وإرسال الأموال (Ether) برمجياً: فهم الدوال payable و fallback.

أما الدالة withdraw فتنقل الرصيد إلى مالك العقد باستخدام call، وهي طريقة أكثر مرونة من بعض البدائل القديمة عند تحويل الأموال.

قبل نشر أي عقد NFT على شبكة حقيقية، تأكد من مراجعة منطق السك المتكرر، وحدود الكمية في كل معاملة، وصلاحيات تغيير baseURI. أي خطأ هنا قد يسمح بسك غير محدود أو بتغيير البيانات الوصفية بشكل يضر بثقة المشترين.

كيف تختبر العقد قبل النشر

الاختبار ليس خطوة تجميلية، بل جزء أساسي من بناء الثقة. سواء استخدمت Hardhat أو Remix، ركز على السيناريوهات التالية:

  1. محاولة السك قبل تفعيل البيع.
  2. محاولة السك بمبلغ أقل أو أكبر من السعر الصحيح.
  3. سك عدد يتجاوز maxSupply.
  4. قراءة tokenURI بعد السك والتأكد من صحة المسار.
  5. اختبار السحب المالي من حساب المالك فقط.

كما يفضّل تسجيل عمليات السك عبر Events حتى تتمكن الواجهة الأمامية من التقاطها بسهولة، وهو ما شرحناه في الأحداث (Events): كيف يخبر العقد الذكي واجهة الموقع (React) بأن شيئاً ما قد حدث؟.

تحسين استهلاك الغاز في مشاريع NFT

من الأخطاء الشائعة كتابة عقد يحاول تخزين كل شيء داخلياً، أو إضافة حلقات غير ضرورية في مسارات كثيرة الاستخدام. صحيح أن لدينا حلقة داخل السك الجماعي، لكننا أبقيناها محدودة ومباشرة. كذلك استخدمنا دوال قراءة من نوع view حيث يلزم، ويمكنك مراجعة أنواع الدوال : فهم view و pure لتوفير رسوم الـ Gas لفهم أثر ذلك على التكلفة.

لتقليل Gas، تجنب تخزين سلاسل نصية طويلة لكل tokenId داخل العقد إذا كان بالإمكان الاكتفاء بـ baseURI موحد. كما أن تقليل عدد عمليات الكتابة إلى storage ينعكس مباشرة على كلفة التنفيذ.

ربط العقد بواجهة Web3

بعد النشر يمكنك بناء صفحة سك بسيطة تتصل بالعقد باستخدام Ethers.js. تقوم الواجهة بقراءة حالة mintEnabled والسعر الحالي والعدد المسكوك، ثم ترسل معاملة إلى mintNFT. بهذه الطريقة يتحول العقد إلى نواة تطبيق DApp متكامل.

ومن المهم هنا أن يكون وصف المجموعة، وسياسة الاستخدام، وحقوق الصور الفنية، ومعلومات الفريق واضحة داخل الموقع. هذه التفاصيل لا تؤثر فقط على ثقة المستخدم، بل ترفع أيضاً جودة المحتوى وفق ممارسات EEAT لأنها تظهر الخبرة والمصداقية والشفافية.

خاتمة عملية

هذا المشروع يضعك أمام نموذج حقيقي لعقد سك مجموعة فنية باستخدام Solidity ومعيار ERC-721. لقد جمعنا بين إدارة الإمداد، وتفعيل السك، وتسعير المعاملات، وربط metadata، وآلية سحب الإيرادات ضمن بنية قابلة للتطوير.

الخطوة التالية الطبيعية هي إضافة whitelist، أو دعم royalties، أو بناء لوحة إدارة أمامية للمشروع. لكن الأساس الصحيح يبدأ دائماً من عقد ذكي منظم، مفهوم، ومختبر جيداً قبل الانتقال إلى الإطلاق الفعلي على الشبكات العامة.

3 comments

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *