NFT стали неотъемлемой частью экосистемы web3 после бума 2021 года, выйдя за рамки коллекционных предметов и найдя применение в реальных сценариях использования: доступ по членству, токенизация активов, игровые предметы и цифровая идентичность.
No-code платформы, такие как Phosphor, значительно упростили для создателей и разработчиков процесс минтинга, настройки и развёртывания NFT с помощью интуитивного Creator Studio и набора API, устраняя необходимость создавать кастомные NFT-контракты с нуля. Однако для разработчиков, стремящихся углубить понимание Solidity и нативных web3-решений, умение создавать NFT-маркетплейс остаётся фундаментальным навыком.
В этом руководстве мы пройдём через процесс разработки и развёртывания базового, но полностью функционального NFT-маркетплейса на Linea. По завершении вы получите знания, необходимые для запуска собственного маркетплейса, и более глубокое понимание разработки смарт-контрактов, децентрализованных приложений и web3-взаимодействий.
Предварительные требования
Необходимо базовое понимание Solidity.
На вашем компьютере должны быть установлены Nodejs и Foundry.
Обзор контракта
Этот NFT-маркетплейс будет поддерживать следующие функции:
Листинг NFT для продажи: Пользователи могут устанавливать цену и выставлять свои NFT на продажу, при этом данные надёжно хранятся в смарт-контракте.
Покупка NFT: Покупатели могут приобретать выставленные NFT, оплачивая необходимое количество Ether, что обеспечивает бесперебойное проведение транзакций.
Пакетная покупка NFT: Несколько NFT можно приобрести в рамках одной транзакции для снижения газовых издержек.
Снятие 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 и добавьте в него приведённый ниже код.
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;
}
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");
_;
}
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);
}
function buyNFT(address nftContract, uint256 tokenId)
public
payable
isListed(nftContract, tokenId)
{
Listing memory listing = listings[nftContract][tokenId];
require(msg.value >= listing.price, "Insufficient funds");
delete listings[nftContract][tokenId];
(bool sent, ) = listing.seller.call{value: listing.price}("");
require(sent, "Failed to send Ether");
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]);
}
}
function delistNFT(address nftContract, uint256 tokenId)
external
isListed(nftContract, tokenId)
isSeller(nftContract, tokenId, msg.sender)
{
Listing memory listing = listings[nftContract][tokenId];
delete listings[nftContract][tokenId];
IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
emit Delisted(nftContract, tokenId, listing.seller);
}
}
Ключевые особенности кода
Структура Listing
Она необходима для реализации таких функций, как листинг, покупка и снятие с продажи в децентрализованном NFT-маркетплейсе. Структура обеспечивает понятный и эффективный способ управления листингами, гарантируя, что маркетплейс хранит запись о продавце и цене каждого предмета — оба параметра критически важны для его работы.
struct Listing {
address seller;
uint256 price;
}
Маппинг листингов
Эта строка кода определяет вложенный маппинг в Solidity, который организует и хранит листинги для NFT-маркетплейса. Внешний маппинг использует адрес NFT-контракта в качестве ключа, позволяя смарт-контракту категоризировать листинги по соответствующим NFT-коллекциям. Это обеспечивает поддержку маркетплейсом нескольких NFT-коллекций.
Внутренний маппинг использует token ID (уникальный идентификатор каждого NFT) в качестве ключа, обеспечивая эффективное управление несколькими листингами в рамках одного NFT-контракта.
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");
delete listings[nftContract][tokenId];
(bool sent, ) = listing.seller.call{value: listing.price}("");
require(sent, "Failed to send Ether");
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];
delete listings[nftContract][tokenId];
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 и добавьте в него приведённый ниже код.
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;
}
function mint(address to) external onlyOwner {
uint256 tokenId = _tokenIdCounter;
_tokenIdCounter += 1;
_safeMint(to, tokenId);
emit Minted(to, tokenId);
}
function _baseURI() internal view override returns (string memory) {
return _baseTokenURI;
}
function setBaseTokenURI(string memory newBaseTokenURI) external onlyOwner {
_baseTokenURI = newBaseTokenURI;
}
function getTokenCount() external view returns (uint256) {
return _tokenIdCounter;
}
}
Создайте файл market.t.sol в папке test и добавьте в него приведённый ниже код.
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/
PRIVATE_KEY=*************************************************
Добавьте приведённые ниже данные в 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.