October 29

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