Перейти к содержимому

Создайте NFT-маркетплейс на Linea

Узнайте, как создать и развернуть децентрализованный NFT-маркетплейс на Linea.

10 мин чтения
Создайте NFT-маркетплейс на Linea

NFT стали неотъемлемой частью экосистемы web3 после бума 2021 года, выйдя за рамки коллекционных предметов и найдя применение в реальных сценариях использования: доступ по членству, токенизация активов, игровые предметы и цифровая идентичность.

No-code платформы, такие как Phosphor, значительно упростили для создателей и разработчиков процесс минтинга, настройки и развёртывания NFT с помощью интуитивного Creator Studio и набора API, устраняя необходимость создавать кастомные NFT-контракты с нуля. Однако для разработчиков, стремящихся углубить понимание Solidity и нативных web3-решений, умение создавать NFT-маркетплейс остаётся фундаментальным навыком.

В этом руководстве мы пройдём через процесс разработки и развёртывания базового, но полностью функционального NFT-маркетплейса на Linea. По завершении вы получите знания, необходимые для запуска собственного маркетплейса, и более глубокое понимание разработки смарт-контрактов, децентрализованных приложений и web3-взаимодействий.

Предварительные требования

  • Необходимо базовое понимание Solidity.

  • На вашем компьютере должны быть установлены Nodejs и Foundry.

Обзор контракта

Этот NFT-маркетплейс будет поддерживать следующие функции:

  1. Листинг NFT для продажи: Пользователи могут устанавливать цену и выставлять свои NFT на продажу, при этом данные надёжно хранятся в смарт-контракте.

  2. Покупка NFT: Покупатели могут приобретать выставленные NFT, оплачивая необходимое количество Ether, что обеспечивает бесперебойное проведение транзакций.

  3. Пакетная покупка NFT: Несколько NFT можно приобрести в рамках одной транзакции для снижения газовых издержек.

  4. Снятие NFT с продажи: Владельцы могут убрать свои NFT с продажи, вернув их в свои кошельки.

Настройка проекта

Для этого проекта мы будем использовать фреймворк Foundry. После установки Foundry выполните приведённую ниже команду для инициализации нового проекта:

forge init market

Откройте папку market в Vscode или вашем любимом редакторе кода и удалите файлы scripts/counter.s.sol, src/counter.sol и test/counter.t.sol.

Установка всех зависимостей

forge install foundry-rs/forge-std --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commit

Полный код смарт-контракта NFT-маркетплейса

Создайте файл market.sol в папке src и добавьте в него приведённый ниже код.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "lib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {Test, console} from "forge-std/Test.sol";
contract NFTMarketplace  {
    struct Listing {
        address seller;
        uint256 price;
    }
    // NFT Contract => Token ID => Listing
    mapping(address => mapping(uint256 => Listing)) public listings;
    event Listed(address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
    event Purchase(address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);
    event Delisted(address indexed nftContract, uint256 indexed tokenId, address seller);
    modifier isSeller(address nftContract, uint256 tokenId, address spender) {
        require(listings[address(nftContract)][tokenId].seller == spender, "Not the seller");
        _;
    }
    modifier isListed(address nftContract, uint256 tokenId) {
        require(listings[nftContract][tokenId].price > 0, "Not listed");
        _;
    }
    modifier notListed(address nftContract, uint256 tokenId) {
        require(listings[nftContract][tokenId].price == 0, "Already listed");
        _;
    }
    /**
     * @dev List an NFT for sale.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to list.
     * @param price The price of the NFT in wei.
     */
    function listNFT(address nftContract, uint256 tokenId, uint256 price)
        external
        notListed(nftContract, tokenId)
    {
        require(price > 0, "Price must be greater than zero");
        require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "not owner");
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
        listings[nftContract][tokenId] = Listing(msg.sender, price);
        emit Listed(nftContract, tokenId, msg.sender, price);
    }
    /**
     * @dev Buy a listed NFT.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to buy.
     */
    function buyNFT(address nftContract, uint256 tokenId)
        public
        payable
        isListed(nftContract, tokenId)
    {
        Listing memory listing = listings[nftContract][tokenId];
        require(msg.value >= listing.price, "Insufficient funds");
        // Remove listing
        delete listings[nftContract][tokenId];
        // Transfer payment to the seller
        (bool sent, ) = listing.seller.call{value: listing.price}("");
        require(sent, "Failed to send Ether");
        // Transfer NFT to the buyer
        IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
        emit Purchase(nftContract, tokenId, msg.sender, listing.price);
    }

    function buyMultipleNFT(address nftContract, uint256 [] memory tokenIds)
        external
        payable
    {
        uint256 totalAmount;
        for (uint256 i; i < tokenIds.length; i++){
           Listing memory listing = listings[nftContract][tokenIds[i]];
           totalAmount += listing.price;
        }
        require(msg.value >= totalAmount, "Insufficient funds");
        for (uint256 i; i < tokenIds.length; i++){
           buyNFT(nftContract, tokenIds[i]);   
        }
    }
    /**
     * @dev Delist an NFT from the marketplace.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to delist.
     */
    function delistNFT(address nftContract, uint256 tokenId)
        external
        isListed(nftContract, tokenId)
        isSeller(nftContract, tokenId, msg.sender)
    {
        Listing memory listing = listings[nftContract][tokenId];

        // Remove listing
        delete listings[nftContract][tokenId];
        // Return NFT to the seller
        IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
        emit Delisted(nftContract, tokenId, listing.seller);
    }
}
Копировать

Ключевые особенности кода

Структура Listing Она необходима для реализации таких функций, как листинг, покупка и снятие с продажи в децентрализованном NFT-маркетплейсе. Структура обеспечивает понятный и эффективный способ управления листингами, гарантируя, что маркетплейс хранит запись о продавце и цене каждого предмета — оба параметра критически важны для его работы.

struct Listing {
        address seller; // NFT seller
        uint256 price;  // NFT price
    }
Копировать

Маппинг листингов Эта строка кода определяет вложенный маппинг в Solidity, который организует и хранит листинги для NFT-маркетплейса. Внешний маппинг использует адрес NFT-контракта в качестве ключа, позволяя смарт-контракту категоризировать листинги по соответствующим NFT-коллекциям. Это обеспечивает поддержку маркетплейсом нескольких NFT-коллекций.

Внутренний маппинг использует token ID (уникальный идентификатор каждого NFT) в качестве ключа, обеспечивая эффективное управление несколькими листингами в рамках одного NFT-контракта.

 // NFT Contract => Token ID => Listing
    mapping(address => mapping(uint256 => Listing)) public listings;
Копировать

Листинг NFT

Функция listNFT позволяет пользователям выставлять свои NFT на продажу на маркетплейсе. Она принимает три параметра: NFT-контракт, token ID и цену. NFT-контракт — это адрес коллекции, из которой пользователь хочет выставить NFT; token ID — уникальный идентификатор NFT в этой коллекции; цена — сумма, за которую пользователь хочет продать NFT.

Функция сначала проверяет, является ли пользователь владельцем NFT, и убеждается, что цена больше нуля. После прохождения этих проверок NFT передаётся от продавца маркетплейсу, маппинг листингов обновляется, и событие Listed генерируется с корректными параметрами.

function listNFT(address nftContract, uint256 tokenId, uint256 price)
        external
        notListed(nftContract, tokenId)
    {
        require(price > 0, "Price must be greater than zero");
        require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "not owner");
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
        listings[nftContract][tokenId] = Listing(msg.sender, price);
        emit Listed(nftContract, tokenId, msg.sender, price);
    }
Копировать

Покупка NFT

Функция buyNFT позволяет пользователям приобретать NFT, выставленные на маркетплейсе. Она принимает два параметра: nftContract — адрес смарт-контракта NFT, и tokenId — уникальный идентификатор приобретаемого NFT.

Функция сначала проверяет, что покупатель отправил достаточно Ether для покрытия цены NFT. После проверки листинг удаляется из маппинга листингов, платёж надёжно переводится продавцу, а NFT передаётся с адреса контракта маркетплейса на адрес покупателя, завершая покупку.

function buyNFT(address nftContract, uint256 tokenId)
        public
        payable
        isListed(nftContract, tokenId)
    {
        Listing memory listing = listings[nftContract][tokenId];
        require(msg.value >= listing.price, "Insufficient funds");
        // Remove listing
        delete listings[nftContract][tokenId];
        // Transfer payment to the seller
        (bool sent, ) = listing.seller.call{value: listing.price}("");
        require(sent, "Failed to send Ether");
        // Transfer NFT to the buyer
        IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
        emit Purchase(nftContract, tokenId, msg.sender, listing.price);
    }
Копировать

Покупка нескольких NFT

Функция buyMultipleNFT позволяет пользователю приобрести несколько NFT в рамках одной транзакции. Сначала она рассчитывает общую стоимость, суммируя цены всех NFT, перечисленных в массиве tokenIds. Затем функция проверяет, что покупатель отправил достаточно средств для покрытия общей суммы.

После проверки средств функция итерирует по массиву tokenIds, вызывая функцию buyNFT для каждого NFT. Это гарантирует, что каждый NFT приобретается индивидуально, платёж переводится продавцу, а NFT отправляются покупателю — следуя тому же процессу, что и функция buyNFT.

function buyMultipleNFT(address nftContract, uint256 [] memory tokenIds)
        external
        payable
    {
        uint256 totalAmount;
        for (uint256 i; i < tokenIds.length; i++){
           Listing memory listing = listings[nftContract][tokenIds[i]];
           totalAmount += listing.price;
        }
        require(msg.value >= totalAmount, "Insufficient funds");
        for (uint256 i; i < tokenIds.length; i++){
           buyNFT(nftContract, tokenIds[i]);   
        }
    }
Копировать

Снятие NFT с продажи

Функция delistNFT позволяет продавцу убрать свой NFT с маркетплейса. Сначала она проверяет, что NFT выставлен на продажу и что вызывающий является законным владельцем. После проверки листинг удаляется с маркетплейса, фактически снимая NFT с продажи.

Затем функция возвращает NFT продавцу, обеспечивая восстановление права собственности на актив. Наконец, генерируется событие Delisted, подтверждающее успешное удаление NFT с маркетплейса.

function delistNFT(address nftContract, uint256 tokenId)
        external
        isListed(nftContract, tokenId)
        isSeller(nftContract, tokenId, msg.sender)
    {
        Listing memory listing = listings[nftContract][tokenId];

        // Remove listing
        delete listings[nftContract][tokenId];
        // Return NFT to the seller
        IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
        emit Delisted(nftContract, tokenId, listing.seller);
    }
Копировать

Ключевые события

Контракт генерирует события для основных действий на маркетплейсе:

  • Listed: Срабатывает при выставлении NFT на продажу, фиксируя такие данные, как продавец, цена и информация об NFT.

  • Purchase: Генерируется при покупке NFT покупателем, обеспечивая отслеживаемость транзакций.

  • Delisted: Указывает на успешное удаление NFT с маркетплейса, поддерживая прозрачность.

event Listed(address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
    event Purchase(address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);
    event Delisted(address indexed nftContract, uint256 indexed tokenId, address seller
Копировать

Обзор набора тестов

Создайте файл NFT.sol в папке src и добавьте в него приведённый ниже код.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";
import "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract NFT is ERC721, Ownable {
    uint256 private _tokenIdCounter;
    string private _baseTokenURI;
    event Minted(address indexed to, uint256 tokenId);
    constructor(string memory name, string memory symbol, string memory baseTokenURI) ERC721(name, symbol) Ownable(msg.sender) {
        _baseTokenURI = baseTokenURI;
    }
    /// @dev Function to mint a new NFT
    function mint(address to) external onlyOwner {
        uint256 tokenId = _tokenIdCounter;
        _tokenIdCounter += 1;
        _safeMint(to, tokenId);
        emit Minted(to, tokenId);
    }
    /// @dev Override base token URI for metadata
    function _baseURI() internal view override returns (string memory) {
        return _baseTokenURI;
    }
    /// @dev Function to update the base token URI
    function setBaseTokenURI(string memory newBaseTokenURI) external onlyOwner {
        _baseTokenURI = newBaseTokenURI;
    }
    /// @dev Function to retrieve the current token ID count
    function getTokenCount() external view returns (uint256) {
        return _tokenIdCounter;
    }
}
Копировать

Создайте файл market.t.sol в папке test и добавьте в него приведённый ниже код.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {NFTMarketplace} from "../src/Market.sol";
import {NFT} from "../src/NFT.sol";
contract MarketTest is Test {
    NFTMarketplace public market;
    NFT public nft;
    address public user1 = makeAddr("user1");
    address public user2 = makeAddr("user2");
    address public user3 = makeAddr("user3");
    function setUp() public {
        nft = new NFT("TestNFT", "tnft", "");
        market = new NFTMarketplace();
        nft.mint(user1);
        nft.mint(user1);
        nft.mint(user1);
    }
    function test_Listing() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        market.listNFT(address(nft), 0, 1 ether);

    }

     function test_BuyingNFT() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        market.listNFT(address(nft), 0, 1 ether);

        vm.startPrank(user2);
        deal(user2, 10 ether);
        market.buyNFT{value: 1 ether}(address(nft), 0);
        assert(nft.ownerOf(0) == user2);

    }

      function test_BuyingMultipleNFT() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        nft.approve(address(market), 1);
        nft.approve(address(market), 2);
        market.listNFT(address(nft), 0, 1 ether);
        market.listNFT(address(nft), 1, 1 ether);
        market.listNFT(address(nft), 2, 1 ether);

        vm.startPrank(user2);
        deal(user2, 10 ether);
        uint256 [] memory nfts = new uint256[] (3);
        nfts[0] = 0;
        nfts[1] = 1;
        nfts[2] = 2; 
        market.buyMultipleNFT{value: 3 ether}(address(nft), nfts);     
    }

     function test_DelistingNFT() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        market.listNFT(address(nft), 0, 1 ether);
        assert(nft.ownerOf(0) == address(market));
        market.delistNFT(address(nft), 0);
        assert(nft.ownerOf(0) == address(user1));

    }
}
Копировать

Приведённый выше набор тестов обеспечивает корректную работу контракта NFT-маркетплейса по всем основным функциям. Вот разбор тестов:

Настройка тестовой среды

Функция setUp инициализирует среду, развёртывая контракты NFTMarketplace и NFT. Она минтит три NFT для user1 в целях тестирования. Развёртывается мок-контракт NFT (NFT) с функцией mint для генерации NFT, а контракт маркетплейса (NFTMarketplace) инстанцируется. User1 владеет всеми заминченными NFT, что позволяет тестировать функции листинга и снятия с продажи.

Тестирование листинга NFT

Функция test_Listing проверяет, что user1 может выставить NFT на продажу на маркетплейсе.

  • User1 одобряет маркетплейсу перевод tokenId 0.

  • User1 выставляет NFT по цене 1 Ether.

  • Тест подтверждает корректную работу функции listNFT и передачу NFT маркетплейсу.

Тестирование покупки одного NFT

Функция test_BuyingNFT симулирует покупку выставленного NFT пользователем user2.

  • User1 выставляет tokenId 0 за 1 Ether.

  • User2 получает 10 Ether и вызывает функцию buyNFT, отправляя 1 Ether для покупки NFT.

  • После покупки тест проверяет, что user2 является новым владельцем tokenId 0.

Тестирование покупки нескольких NFT

Функция test_BuyingMultipleNFT проверяет возможность покупки нескольких NFT в рамках одной транзакции.

  • User1 выставляет три NFT (tokenId 0, 1, 2) по 1 Ether каждый.

  • User2 вызывает buyMultipleNFT, передавая token ID и отправляя 3 Ether.

  • Тест проверяет, что user2 становится владельцем всех трёх NFT и что маркетплейс корректно обрабатывает несколько покупок.

Тестирование снятия NFT с продажи

Функция test_DelistingNFT проверяет, что user1 может снять свой NFT с маркетплейса.

  • User1 выставляет tokenId 0 за 1 Ether и вызывает delistNFT.

  • NFT возвращается user1, и тест подтверждает, что право собственности вернулось к user1.

Запуск тестов

forge test

Развёртывание и верификация на Linea

Добавьте необходимые переменные в файл .env, убедившись, что он находится в корневой директории проекта:

LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/ // Infura RPC endpoint for connecting to Linea 
PRIVATE_KEY=************************************************* // Wallet private key for signing transactions LINEA_API_KEY=************************* // API key for accessing Linea services via Infura
Копировать

Добавьте приведённые ниже данные в foundry.toml

[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
Копировать

Создайте Makefile и добавьте в него приведённые ниже данные:

deploy:
    forge create src/Market.sol:NFTMarketplace --rpc-url $(LINEA_RPC_URL) --private-key $(PRIVATE_KEY)

verify:; forge verify-contract --rpc-url $(LINEA_RPC_URL) --chain linea <contract address> src/Market.sol:NFTMarketplace
Копировать

Развёртывание

make deploy

Верификация

make verify

Заключение

Создание NFT-маркетплейса на Linea даёт разработчикам практический опыт в разработке смарт-контрактов, программировании на Solidity и web3-взаимодействиях. В этом руководстве мы рассмотрели основные функции децентрализованного маркетплейса, включая листинг, покупку, пакетные покупки и снятие NFT с продажи, обеспечивая при этом безопасность и эффективность транзакций.

Мы также прошли через реализацию и тестирование смарт-контракта с использованием Foundry, а также через процесс развёртывания и верификации на Linea. Следуя этому руководству, вы теперь обладаете базовыми знаниями для создания, тестирования и развёртывания собственного NFT-маркетплейса.

Для получения дополнительных практических руководств и глубокого погружения в разработку блокчейн-приложений посетите YouTube-канал MetaMask Developer и следите за нашими последними материалами в Twitter.

Переведено ИИ. Может содержать ошибки. Пожалуйста, всегда проверяйте информацию.

Оцените перевод

Автор статьи:

  • Kingsley Okonkwo

    Кингсли — сертифицированный разработчик блокчейна Ethereum, работающий техническим писателем в Consensys с особым акцентом на инструменты для разработчиков. До прихода в Consensys Кингсли работал фриланс-разработчиком бэкенда на нескольких платформах, включая Gigster и Braintrust. Имея за плечами 5 лет опыта в разработке программного обеспечения, он уверенно владеет такими инструментами, как JavaScript (Node.js и React), GoLang, SQL и Docker. Кингсли мастерски создаёт пошаговые руководства и практические гайды, используя новейшие технологии web3, чтобы помочь коллегам-разработчикам создавать более качественные dapps. В настоящее время проживая в Дубае, Кингсли в свободное время увлекается баскетболом и футболом.

    Читать все статьи