NFT Маркетплейс с нуля: архитектура и основные компоненты
Содержание
1. Введение
2. Архитектура проекта
3. Смарт-контракты
4. Бэкенд
5. Фронтенд
6. Интеграция с IPFS
7. Тестирование
8. Безопасность
9. Заключение
Введение
NFT маркетплейс - это платформа, где пользователи могут создавать, продавать и покупать NFT токены. В этой статье мы рассмотрим создание базового NFT маркетплейса с нуля, включая все основные компоненты: смарт-контракты, бэкенд и фронтенд.
Архитектура проекта
Основные компоненты:
- Смарт-контракты (Solidity)
- Бэкенд (Node.js)
- Фронтенд (React)
- IPFS для хранения метаданных
- База данных (PostgreSQL)
Схема взаимодействия компонентов:
[Пользователь] <-> [Фронтенд] <-> [Web3.js] <-> [Смарт-контракты] ↕ ↕ [Бэкенд] <-> [База данных] ↕ [IPFS]
Смарт-контракты
NFT контракт (ERC-721)
// solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; contract NFTMarketplace is ERC721, ReentrancyGuard { using Counters for Counters.Counter; Counters.Counter private _tokenIds; Counters.Counter private _itemsSold; address payable owner; uint256 listingPrice = 0.025 ether; struct MarketItem { uint256 tokenId; address payable seller; address payable owner; uint256 price; bool sold; } mapping(uint256 => MarketItem) private idToMarketItem; event MarketItemCreated( uint256 indexed tokenId, address seller, address owner, uint256 price, bool sold ); constructor() ERC721("Market Items", "MTK") { owner = payable(msg.sender); } function createMarketItem(string memory tokenURI, uint256 price) public payable nonReentrant returns (uint256) { require(price > 0, "Price must be at least 1 wei"); require(msg.value == listingPrice, "Price must be equal to listing price"); _tokenIds.increment(); uint256 newTokenId = _tokenIds.current(); _mint(msg.sender, newTokenId); _setTokenURI(newTokenId, tokenURI); idToMarketItem[newTokenId] = MarketItem( newTokenId, payable(msg.sender), payable(address(this)), price, false ); _transfer(msg.sender, address(this), newTokenId); emit MarketItemCreated( newTokenId, msg.sender, address(this), price, false ); return newTokenId; } function createMarketSale(uint256 tokenId) public payable nonReentrant { uint256 price = idToMarketItem[tokenId].price; require(msg.value == price, "Please submit the asking price"); idToMarketItem[tokenId].seller.transfer(msg.value); _transfer(address(this), msg.sender, tokenId); idToMarketItem[tokenId].owner = payable(msg.sender); idToMarketItem[tokenId].sold = true; _itemsSold.increment(); payable(owner).transfer(listingPrice); } }
Бэкенд
Структура API (Node.js + Express)
// javascript // server.js const express = require('express'); const cors = require('cors'); const ethers = require('ethers'); const { create } = require('ipfs-http-client'); const app = express(); app.use(cors()); app.use(express.json()); // IPFS клиент const ipfs = create({ host: 'ipfs.infura.io', port: 5001, protocol: 'https' }); // Подключение к сети Ethereum const provider = new ethers.providers.JsonRpcProvider(process.env.ETHEREUM_RPC_URL); const contract = new ethers.Contract( process.env.CONTRACT_ADDRESS, CONTRACT_ABI, provider ); // API для загрузки метаданных в IPFS app.post('/api/upload', async (req, res) => { try { const { name, description, image } = req.body; const metadata = { name, description, image, attributes: [] }; const result = await ipfs.add(JSON.stringify(metadata)); res.json({ success: true, ipfsHash: result.path }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // API для получения списка NFT app.get('/api/nfts', async (req, res) => { try { const nfts = await contract.fetchMarketItems(); const items = await Promise.all(nfts.map(async i => { const tokenUri = await contract.tokenURI(i.tokenId); const metadata = await fetch(tokenUri).then(res => res.json()); return { tokenId: i.tokenId.toString(), seller: i.seller, owner: i.owner, price: ethers.utils.formatUnits(i.price.toString(), 'ether'), name: metadata.name, description: metadata.description, image: metadata.image }; })); res.json(items); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); const PORT = process.env.PORT || 3001; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
Фронтенд
React компоненты
// jsx // NFTCard.js import React from 'react'; import { ethers } from 'ethers'; import { useWeb3React } from '@web3-react/core'; const NFTCard = ({ nft, onPurchase }) => { const { account } = useWeb3React(); return ( <div className="nft-card"> <img src={nft.image} alt={nft.name} /> <h3>{nft.name}</h3> <p>{nft.description}</p> <p>Price: {nft.price} ETH</p> {account && nft.seller !== account && !nft.sold && ( <button onClick={() => onPurchase(nft)}> Buy Now </button> )} </div> ); };
// CreateNFT.js import React, { useState } from 'react'; import { useWeb3React } from '@web3-react/core'; import { ethers } from 'ethers'; const CreateNFT = ({ contract }) => { const [formData, setFormData] = useState({ name: '', description: '', price: '', image: null }); const handleSubmit = async (e) => { e.preventDefault(); try { // Загрузка изображения в IPFS const imageResult = await uploadToIPFS(formData.image); // Создание метаданных const metadata = { name: formData.name, description: formData.description, image: `ipfs://${imageResult.path}` }; // Загрузка метаданных в IPFS const metadataResult = await uploadToIPFS(JSON.stringify(metadata)); // Создание NFT const price = ethers.utils.parseUnits(formData.price, 'ether'); const transaction = await contract.createMarketItem( `ipfs://${metadataResult.path}`, price ); await transaction.wait(); // Очистка формы setFormData({ name: '', description: '', price: '', image: null }); } catch (error) { console.error('Error creating NFT:', error); } }; return ( <form onSubmit={handleSubmit}> <input type="text" placeholder="Name" value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} /> <textarea placeholder="Description" value={formData.description} onChange={e => setFormData({...formData, description: e.target.value})} /> <input type="number" placeholder="Price in ETH" value={formData.price} onChange={e => setFormData({...formData, price: e.target.value})} /> <input type="file" onChange={e => setFormData({...formData, image: e.target.files[0]})} /> <button type="submit">Create NFT</button> </form> ); };
Интеграция с IPFS
Загрузка файлов в IPFS
// javascript const uploadToIPFS = async (file) => { try { // Если файл это объект File if (file instanceof File) { const buffer = await file.arrayBuffer(); const result = await ipfs.add(buffer); return result; } // Если файл это строка (например, JSON) const result = await ipfs.add(file); return result; } catch (error) { console.error('Error uploading to IPFS:', error); throw error; } };
Тестирование
Тесты смарт-контракта
// javascript const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("NFTMarketplace", function () { let NFTMarketplace; let nftMarketplace; let owner; let addr1; let addr2; beforeEach(async function () { NFTMarketplace = await ethers.getContractFactory("NFTMarketplace"); [owner, addr1, addr2] = await ethers.getSigners(); nftMarketplace = await NFTMarketplace.deploy(); await nftMarketplace.deployed(); }); describe("Minting", function () { it("Should create and execute market sale", async function () { const listingPrice = ethers.utils.parseUnits("0.025", "ether"); const auctionPrice = ethers.utils.parseUnits("1", "ether"); await nftMarketplace.createMarketItem( "https://example.com/token/1", auctionPrice, { value: listingPrice } ); await nftMarketplace.connect(addr1).createMarketSale(1, { value: auctionPrice }); const item = await nftMarketplace.idToMarketItem(1); expect(item.sold).to.equal(true); expect(item.owner).to.equal(addr1.address); }); }); });
Безопасность
Основные меры безопасности:
1. Использование OpenZeppelin для стандартных контрактов
2. Защита от атак повторного входа (ReentrancyGuard)
3. Проверка входных данных
4. Безопасная обработка платежей
5. Управление доступом
Пример безопасной обработки платежей:
// solidity function withdraw() public onlyOwner { uint256 balance = address(this).balance; require(balance > 0, "No balance to withdraw"); (bool success, ) = owner.call{value: balance}(""); require(success, "Transfer failed"); }
Заключение
Создание NFT маркетплейса требует комплексного подхода и понимания различных технологий:
- Смарт-контракты Ethereum
- Web3 интеграция
- Децентрализованное хранение данных (IPFS)
- Современный веб-стек (React, Node.js)
- Безопасность и тестирование
При разработке важно уделить особое внимание:
- Безопасности смарт-контрактов
- Пользовательскому опыту
- Масштабируемости
- Обработке метаданных
- Тестированию всех компонентов
Что можно попробовать доработать самому?
1. Добавление поиска и фильтрации NFT
2. Реализация аукционов
3. Поддержка коллекций
4. Интеграция с популярными кошельками
5. Система рейтингов и отзывов
6. Поддержка различных сетей (Polygon, BSC)
Подпишись !!!
Спасибо за чтение ! Подпишись что бы не пропускать дальнейшие статьи!
Телеграм: https://t.me/one_eyes