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