Основное руководство по созданию аукционного сайта NFT с помощью React, Solidity и CometChat

Основное руководство по созданию аукционного сайта NFT с помощью React, Solidity и CometChat

27 декабря 2022 г.

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

Auctioning NFT Item on Marketplace

Предпосылки

Чтобы следовать этому руководству, на вашем локальном компьютере должны быть установлены перечисленные ниже элементы. NodeJs не подлежит обсуждению, остальные можно установить, следуя этому руководству, поэтому убедитесь, что он у вас установлен и работает.

* NodeJs * Реагировать * Прочность * Попутный ветер * Кометчат SDK * Каска * ЭфирыJs * Метамаска * Пряжа

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

Клонируйте стартовый комплект с помощью приведенной ниже команды в папку ваших проектов:

git clone https://github.com/Daltonic/tailwind_ethers_starter_kit <PROJECT_NAME>
cd <PROJECT_NAME>

Затем откройте проект в VS Code или в предпочитаемом вами редакторе кода. Найдите файл package.json и обновите его с помощью приведенных ниже кодов.

{
  "name": "Auction",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },
  "dependencies": {
    "@cometchat-pro/chat": "^3.0.10",
    "@nomiclabs/hardhat-ethers": "^2.1.0",
    "@nomiclabs/hardhat-waffle": "^2.0.3",
    "axios": "^1.2.1",
    "ethereum-waffle": "^3.4.4",
    "ethers": "^5.6.9",
    "hardhat": "^2.10.1",
    "ipfs-http-client": "55.0.1-rc.2",
    "moment": "^2.29.4",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-hooks-global-state": "^1.0.2",
    "react-icons": "^4.7.1",
    "react-identicons": "^1.2.5",
    "react-moment": "^1.1.2",
    "react-router-dom": "6",
    "react-scripts": "5.0.0",
    "react-toastify": "^9.1.1",
    "web-vitals": "^2.1.4"
  },
  "devDependencies": {
    "@faker-js/faker": "^7.6.0",
    "@openzeppelin/contracts": "^4.5.0",
    "@tailwindcss/forms": "0.4.0",
    "assert": "^2.0.0",
    "autoprefixer": "10.4.2",
    "babel-polyfill": "^6.26.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "babel-preset-stage-3": "^6.24.1",
    "babel-register": "^6.26.0",
    "buffer": "^6.0.3",
    "chai": "^4.3.6",
    "chai-as-promised": "^7.1.1",
    "crypto-browserify": "^3.12.0",
    "dotenv": "^16.0.0",
    "express": "^4.18.2",
    "express-fileupload": "^1.4.0",
    "https-browserify": "^1.0.0",
    "mnemonics": "^1.1.3",
    "os-browserify": "^0.3.0",
    "postcss": "8.4.5",
    "process": "^0.11.10",
    "react-app-rewired": "^2.1.11",
    "sharp": "^0.31.2",
    "stream-browserify": "^3.0.0",
    "stream-http": "^3.2.0",
    "tailwindcss": "3.0.18",
    "url": "^0.11.0",
    "uuid": "^9.0.0"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Чтобы установить все необходимые зависимости, указанные в файле package.json, выполните в терминале команду **yarn install**.

Настройка SDK CometChat

CometChat SDK

Выполните следующие действия, чтобы настроить CometChat SDK; в конце вы должны сохранить ключи вашего приложения как переменную среды.

ШАГ 1: Перейдите на Панель управления CometChat и создайте учетную запись.

Register a new CometChat account if you do not have one

ШАГ 2: Войдите в панель управления CometChat только после регистрации.

Log in to the CometChat Dashboard with your created account

ШАГ 3:

На панели управления добавьте новое приложение под названием Auction

.

Create a new CometChat app - Step 1

Create a new CometChat app - Step 2

ШАГ 4: Выберите только что созданное приложение из списка.

Select your created app

ШАГ 5: Из краткого руководства скопируйте APP_ID, REGION и AUTH_KEY в свой файл .env. См. изображение и фрагмент кода.

Copy the the APP_ID, REGION, and AUTH_KEY

Замените ключи-заполнители REACT_COMET_CHAT соответствующими значениями.

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

Файл .env должен быть создан в корне вашего проекта.

Настройка скрипта Hardhat

Откройте файл hardhat.config.js в корне этого проекта и замените его содержимое следующими настройками.

require("@nomiclabs/hardhat-waffle");
require('dotenv').config()

module.exports = {
  defaultNetwork: "localhost",
  networks: {
    hardhat: {
    },
    localhost: {
      url: "http://127.0.0.1:8545"
    },
  },
  solidity: {
    version: '0.8.11',
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  paths: {
    sources: "./src/contracts",
    artifacts: "./src/abis"
  },
  mocha: {
    timeout: 40000
  }
}

Приведенный выше скрипт инструктирует каску по этим трем важным правилам.

  • Сети: этот блок содержит настройки для выбранных вами сетей. При развертывании вам потребуется указать сеть для доставки смарт-контрактов.
  • Solidity: здесь описывается версия компилятора, которую будет использовать каска для компиляции кодов смарт-контрактов в bytecodes и abi.< /li>
  • Пути: это просто информирует hardhat о расположении ваших смарт-контрактов, а также о местоположении для дампа вывода компилятора, который является ABI.

Посмотрите это видео о том, как создать децентрализованную автономную организацию.

https://youtu.be/Gm442Ihv1GU?embedable=true

Вы также можете подписаться на канал, чтобы получать больше подобных видео.

Разработка смарт-контракта

Давайте создадим смарт-контракт для этого проекта, создав новую папку с именем contracts в каталоге src проекта.

Внутри папки контрактов создайте файл с именем 'DappAuction.sol', который будет содержать код, определяющий поведение смарт-контракта. Скопируйте и вставьте следующий код в файл 'DappAuction.sol'. Полный код показан ниже."

//SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract Auction is ERC721URIStorage, ReentrancyGuard {
    using Counters for Counters.Counter;
    Counters.Counter private totalItems;

    address companyAcc;
    uint listingPrice = 0.02 ether;
    uint royalityFee;
    mapping(uint => AuctionStruct) auctionedItem;
    mapping(uint => bool) auctionedItemExist;
    mapping(string => uint) existingURIs;
    mapping(uint => BidderStruct[]) biddersOf;

    constructor(uint _royaltyFee) ERC721("Daltonic Tokens", "DAT") {
        companyAcc = msg.sender;
        royalityFee = _royaltyFee;
    }

    struct BidderStruct {
        address bidder;
        uint price;
        uint timestamp;
        bool refunded;
        bool won;
    }

    struct AuctionStruct {
        string name;
        string description;
        string image;
        uint tokenId;
        address seller;
        address owner;
        address winner;
        uint price;
        bool sold;
        bool live;
        bool biddable;
        uint bids;
        uint duration;
    }

    event AuctionItemCreated(
        uint indexed tokenId,
        address seller,
        address owner,
        uint price,
        bool sold
    );

    function getListingPrice() public view returns (uint) {
        return listingPrice;
    }

    function setListingPrice(uint _price) public {
        require(msg.sender == companyAcc, "Unauthorized entity");
        listingPrice = _price;
    }

    function changePrice(uint tokenId, uint price) public {
        require(
            auctionedItem[tokenId].owner == msg.sender,
            "Unauthorized entity"
        );
        require(
            getTimestamp(0, 0, 0, 0) > auctionedItem[tokenId].duration,
            "Auction still Live"
        );
        require(price > 0 ether, "Price must be greater than zero");

        auctionedItem[tokenId].price = price;
    }

    function mintToken(string memory tokenURI) internal returns (bool) {
        totalItems.increment();
        uint tokenId = totalItems.current();

        _mint(msg.sender, tokenId);
        _setTokenURI(tokenId, tokenURI);

        return true;
    }

    function createAuction(
        string memory name,
        string memory description,
        string memory image,
        string memory tokenURI,
        uint price
    ) public payable nonReentrant {
        require(price > 0 ether, "Sales price must be greater than 0 ethers.");
        require(
            msg.value >= listingPrice,
            "Price must be up to the listing price."
        );
        require(mintToken(tokenURI), "Could not mint token");

        uint tokenId = totalItems.current();

        AuctionStruct memory item;
        item.tokenId = tokenId;
        item.name = name;
        item.description = description;
        item.image = image;
        item.price = price;
        item.duration = getTimestamp(0, 0, 0, 0);
        item.seller = msg.sender;
        item.owner = msg.sender;

        auctionedItem[tokenId] = item;
        auctionedItemExist[tokenId] = true;

        payTo(companyAcc, listingPrice);

        emit AuctionItemCreated(tokenId, msg.sender, address(0), price, false);
    }

    function offerAuction(
        uint tokenId,
        bool biddable,
        uint sec,
        uint min,
        uint hour,
        uint day
    ) public {
        require(
            auctionedItem[tokenId].owner == msg.sender,
            "Unauthorized entity"
        );
        require(
            auctionedItem[tokenId].bids == 0,
            "Winner should claim prize first"
        );

        if (!auctionedItem[tokenId].live) {
            setApprovalForAll(address(this), true);
            IERC721(address(this)).transferFrom(
                msg.sender,
                address(this),
                tokenId
            );
        }

        auctionedItem[tokenId].bids = 0;
        auctionedItem[tokenId].live = true;
        auctionedItem[tokenId].sold = false;
        auctionedItem[tokenId].biddable = biddable;
        auctionedItem[tokenId].duration = getTimestamp(sec, min, hour, day);
    }

    function placeBid(uint tokenId) public payable {
        require(
            msg.value >= auctionedItem[tokenId].price,
            "Insufficient Amount"
        );
        require(
            auctionedItem[tokenId].duration > getTimestamp(0, 0, 0, 0),
            "Auction not available"
        );
        require(auctionedItem[tokenId].biddable, "Auction only for bidding");

        BidderStruct memory bidder;
        bidder.bidder = msg.sender;
        bidder.price = msg.value;
        bidder.timestamp = getTimestamp(0, 0, 0, 0);

        biddersOf[tokenId].push(bidder);
        auctionedItem[tokenId].bids++;
        auctionedItem[tokenId].price = msg.value;
        auctionedItem[tokenId].winner = msg.sender;
    }

    function claimPrize(uint tokenId, uint bid) public {
        require(
            getTimestamp(0, 0, 0, 0) > auctionedItem[tokenId].duration,
            "Auction still Live"
        );
        require(
            auctionedItem[tokenId].winner == msg.sender,
            "You are not the winner"
        );

        biddersOf[tokenId][bid].won = true;
        uint price = auctionedItem[tokenId].price;
        address seller = auctionedItem[tokenId].seller;

        auctionedItem[tokenId].winner = address(0);
        auctionedItem[tokenId].live = false;
        auctionedItem[tokenId].sold = true;
        auctionedItem[tokenId].bids = 0;
        auctionedItem[tokenId].duration = getTimestamp(0, 0, 0, 0);

        uint royality = (price * royalityFee) / 100;
        payTo(auctionedItem[tokenId].owner, (price - royality));
        payTo(seller, royality);
        IERC721(address(this)).transferFrom(address(this), msg.sender, tokenId);
        auctionedItem[tokenId].owner = msg.sender;

        performRefund(tokenId);
    }

    function performRefund(uint tokenId) internal {
        for (uint i = 0; i < biddersOf[tokenId].length; i++) {
            if (biddersOf[tokenId][i].bidder != msg.sender) {
                biddersOf[tokenId][i].refunded = true;
                payTo(
                    biddersOf[tokenId][i].bidder,
                    biddersOf[tokenId][i].price
                );
            } else {
                biddersOf[tokenId][i].won = true;
            }
            biddersOf[tokenId][i].timestamp = getTimestamp(0, 0, 0, 0);
        }

        delete biddersOf[tokenId];
    }

    function buyAuctionedItem(uint tokenId) public payable nonReentrant {
        require(
            msg.value >= auctionedItem[tokenId].price,
            "Insufficient Amount"
        );
        require(
            auctionedItem[tokenId].duration > getTimestamp(0, 0, 0, 0),
            "Auction not available"
        );
        require(!auctionedItem[tokenId].biddable, "Auction only for purchase");

        address seller = auctionedItem[tokenId].seller;
        auctionedItem[tokenId].live = false;
        auctionedItem[tokenId].sold = true;
        auctionedItem[tokenId].bids = 0;
        auctionedItem[tokenId].duration = getTimestamp(0, 0, 0, 0);

        uint royality = (msg.value * royalityFee) / 100;
        payTo(auctionedItem[tokenId].owner, (msg.value - royality));
        payTo(seller, royality);
        IERC721(address(this)).transferFrom(
            address(this),
            msg.sender,
            auctionedItem[tokenId].tokenId
        );

        auctionedItem[tokenId].owner = msg.sender;
    }

    function getAuction(uint id) public view returns (AuctionStruct memory) {
        require(auctionedItemExist[id], "Auctioned Item not found");
        return auctionedItem[id];
    }

    function getAllAuctions()
        public
        view
        returns (AuctionStruct[] memory Auctions)
    {
        uint totalItemsCount = totalItems.current();
        Auctions = new AuctionStruct[](totalItemsCount);

        for (uint i = 0; i < totalItemsCount; i++) {
            Auctions[i] = auctionedItem[i + 1];
        }
    }

    function getUnsoldAuction()
        public
        view
        returns (AuctionStruct[] memory Auctions)
    {
        uint totalItemsCount = totalItems.current();
        uint totalSpace;
        for (uint i = 0; i < totalItemsCount; i++) {
            if (!auctionedItem[i + 1].sold) {
                totalSpace++;
            }
        }

        Auctions = new AuctionStruct[](totalSpace);

        uint index;
        for (uint i = 0; i < totalItemsCount; i++) {
            if (!auctionedItem[i + 1].sold) {
                Auctions[index] = auctionedItem[i + 1];
                index++;
            }
        }
    }

    function getMyAuctions()
        public
        view
        returns (AuctionStruct[] memory Auctions)
    {
        uint totalItemsCount = totalItems.current();
        uint totalSpace;
        for (uint i = 0; i < totalItemsCount; i++) {
            if (auctionedItem[i + 1].owner == msg.sender) {
                totalSpace++;
            }
        }

        Auctions = new AuctionStruct[](totalSpace);

        uint index;
        for (uint i = 0; i < totalItemsCount; i++) {
            if (auctionedItem[i + 1].owner == msg.sender) {
                Auctions[index] = auctionedItem[i + 1];
                index++;
            }
        }
    }

    function getSoldAuction()
        public
        view
        returns (AuctionStruct[] memory Auctions)
    {
        uint totalItemsCount = totalItems.current();
        uint totalSpace;
        for (uint i = 0; i < totalItemsCount; i++) {
            if (auctionedItem[i + 1].sold) {
                totalSpace++;
            }
        }

        Auctions = new AuctionStruct[](totalSpace);

        uint index;
        for (uint i = 0; i < totalItemsCount; i++) {
            if (auctionedItem[i + 1].sold) {
                Auctions[index] = auctionedItem[i + 1];
                index++;
            }
        }
    }

    function getLiveAuctions()
        public
        view
        returns (AuctionStruct[] memory Auctions)
    {
        uint totalItemsCount = totalItems.current();
        uint totalSpace;
        for (uint i = 0; i < totalItemsCount; i++) {
            if (auctionedItem[i + 1].duration > getTimestamp(0, 0, 0, 0)) {
                totalSpace++;
            }
        }

        Auctions = new AuctionStruct[](totalSpace);

        uint index;
        for (uint i = 0; i < totalItemsCount; i++) {
            if (auctionedItem[i + 1].duration > getTimestamp(0, 0, 0, 0)) {
                Auctions[index] = auctionedItem[i + 1];
                index++;
            }
        }
    }

    function getBidders(uint tokenId)
        public
        view
        returns (BidderStruct[] memory)
    {
        return biddersOf[tokenId];
    }

    function getTimestamp(
        uint sec,
        uint min,
        uint hour,
        uint day
    ) internal view returns (uint) {
        return
            block.timestamp +
            (1 seconds * sec) +
            (1 minutes * min) +
            (1 hours * hour) +
            (1 days * day);
    }

    function payTo(address to, uint amount) internal {
        (bool success, ) = payable(to).call{value: amount}("");
        require(success);
    }
}

Давайте рассмотрим некоторые особенности того, что происходит в приведенном выше смарт-контракте. Доступны следующие элементы:

Импорт контрактов Ниже приведены смарт-контракты, импортированные из библиотеки openzeppelin:

* Счетчики: для отслеживания всех NFT на платформе. * ERC721: это стандарт для невзаимозаменяемых токенов в блокчейне Ethereum. Он определяет набор функций и событий, которые должен иметь смарт-контракт, реализующий стандарт ERC721. * ERC721URIStorage: это смарт-контракт, в котором хранится URI (унифицированный идентификатор ресурса) токена ERC721. * ReentrancyGuard: этот импорт защищает наш смарт-контракт от атак с повторным входом.

Переменные состояния

  • Totalitems: эта переменная содержит записи о количестве NFT, доступных в нашем смарт-контракте.
  • CompanyAcc: содержит запись адреса кошелька развертывателя.
  • ListingPrice: содержит цену за создание и листинг NFT на платформе.
  • RoyalityFee: это процент роялти, который продавец NFT получает с каждой продажи.

Сопоставления

  • AuctionedItem: содержит все данные NFT, отчеканенные на нашей платформе.
  • AuctionedItemExist: используется для подтверждения существования NFT.
  • ExistingURI: содержит созданные URI метаданных.
  • BiddersOf: список участников аукциона.

Структуры и события

  • BidderStruct: описывает информацию о конкретном участнике торгов.
  • AuctionStruct: описывает информацию о конкретном элементе NFT.
  • AuctionItemCreated: событие, регистрирующее информацию о только что созданном NFT.

Функции

  • Constructor(): инициализирует смарт-контракт с учетной записью компании, установленным гонораром, а также именем и символом токена.
  • GetListingPrice(): возвращает цену, установленную для создания NFT на платформе.
  • SetListingPrice(): используется для обновления цены чеканки для создания NFT.
  • ChangePrice(): используется для изменения стоимости определенного NFT.
  • MintToken(): используется для создания нового токена.
  • CreateAuction(): используется для создания нового аукциона с использованием созданного идентификатора токена.
  • OfferAuction(): используется для размещения товара NFT на рынке.
  • PlaceBid(): используется для ставок на аукционе.
  • ClaimPrize(): используется для передачи NFT участникам, предложившим самую высокую цену.
  • PerformRefund(): используется для возмещения средств участникам торгов, которые не стали победителями каждого аукциона.
  • BuyAuctionedItem(): используется для покупки NFT, проданных напрямую.
  • GetAuction(): возвращает аукцион по идентификатору токена.
  • GetAllAuctions(): возвращает все доступные аукционы из контракта.
  • GetUnsoldAuction() Возвращает все непроданные аукционы.
  • GetSoldAuction(): возвращает все проданные аукционы.
  • GetMyAuctions(): возвращает все аукционы, принадлежащие вызывающей функции.
  • GetLiveAuctions(): возвращает все аукционы, представленные на рынке.
  • GetBidders(): возвращает участников определенного аукциона, указав идентификатор токена.
  • GetTimestamp(): возвращает отметку времени для определенной даты.
  • PayTo(): ​​отправляет эфиры на определенную учетную запись.

Настройка сценария развертывания

Перейдите в папку scripts, а затем в файл deploy.js и вставьте в него приведенный ниже код. Если вы не можете найти папку сценария, создайте ее, создайте файл deploy.js и вставьте в него следующий код.

const { ethers } = require('hardhat')
const fs = require('fs')

async function main() {
  const royaltyFee = 5
  const Contract = await ethers.getContractFactory('Auction')
  const contract = await Contract.deploy(royaltyFee)

  await contract.deployed()

  const address = JSON.stringify({ address: contract.address }, null, 4)
  fs.writeFile('./src/abis/contractAddress.json', address, 'utf8', (err) => {
    if (err) {
      console.error(err)
      return
    }
    console.log('Deployed contract address', contract.address)
  })
}

main().catch((error) => {
  console.error(error)
  process.exitCode = 1
})

При запуске в качестве команды Hardhat приведенный выше скрипт развернет смарт-контракт Auction.sol в вашей локальной сети блокчейна.

Следуя приведенным выше инструкциям, откройте терминал, указывающий на этот проект, и запустите перечисленные ниже команды отдельно на двух терминалах. Вы можете сделать это прямо из своего редактора в VS Code. Посмотрите на команду ниже.

yarn hardhat node # Terminal 1
yarn hardhat run scripts/deploy.js # Terminal 2

Если предыдущие команды были успешно выполнены, вы должны увидеть следующую активность на своем терминале. Пожалуйста, смотрите изображение ниже.

Step One

Step Two

Настройка приложения Infuria

ШАГ 1. Перейдите на сайт Infuria и создайте учетную запись.< /p>

Login to your infuria account

ШАГ 2. На панели управления создайте новый проект.

Create a new project step 1

Create a new project step 2

ШАГ 3. Скопируйте Id проекта и ваш секрет API в файл .env в указанном ниже формате и сохраните .

Файл окружения

INFURIA_PID=***************************
INFURIA_API=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

Разработка API обработки изображений

Нам нужен способ генерировать метаданные из изображения, которое мы собираемся сделать NFT. Проблема в том, что JavaScript в браузере не может дать нам желаемого результата. Нам потребуется сценарий NodeJs, который поможет нам обработать изображения, сгенерировать метаданные, развернуть в IPFS и вернуть URI токена в качестве ответа API. Не нужно много говорить, позвольте мне показать вам, как реализовать этот API.

Во-первых, вам понадобятся следующие библиотеки, которые уже установлены в этом проекте с помощью команды yarn install, которую вы выполнили ранее.

* Express(): позволяет создавать серверы и совместно использовать ресурсы. * Express-Fileupload(): позволяет загружать файлы, такие как загрузка изображения. * Cors(): включает обмен запросами между источниками. * Fs(): Разрешает доступ к файловой системе нашей локальной машины. * Dotenv(): включить управление переменными окружения. * Sharp(): позволяет обрабатывать изображения с различными размерами и расширениями. * Faker(): позволяет генерировать случайные и поддельные данные. * IpfsClient(): позволяет загружать файлы в IPFS.

Теперь давайте напишем несколько важных функций скрипта, которые помогут нам преобразовывать изображения, а также другую информацию, такую ​​как их имена, описания, цены, идентификаторы и т. д., в эквивалентные им метаданные.

Создайте папку с именем "api" в корне вашего проекта, затем создайте в ней новый файл с именем metadata.js и вставьте внутрь приведенный ниже код.

Файл Metadata.js

const sharp = require('sharp')
const { faker } = require('@faker-js/faker')
const ipfsClient = require('ipfs-http-client')

const auth =
  'Basic ' +
  Buffer.from(process.env.INFURIA_PID + ':' + process.env.INFURIA_API).toString(
    'base64',
  )
const client = ipfsClient.create({
  host: 'ipfs.infura.io',
  port: 5001,
  protocol: 'https',
  headers: {
    authorization: auth,
  },
})
const attributes = {
  weapon: [
    'Stick',
    'Knife',
    'Blade',
    'Club',
    'Ax',
    'Sword',
    'Spear',
    'Halberd',
  ],
  environment: [
    'Space',
    'Sky',
    'Deserts',
    'Forests',
    'Grasslands',
    'Mountains',
    'Oceans',
    'Rainforests',
  ],
  rarity: Array.from(Array(6).keys()),
}
const toMetadata = ({ id, name, description, price, image }) => ({
  id,
  name,
  description,
  price,
  image,
  demand: faker.random.numeric({ min: 10, max: 100 }),
  attributes: [
    {
      trait_type: 'Environment',
      value: attributes.environment.sort(() => 0.5 - Math.random())[0],
    },
    {
      trait_type: 'Weapon',
      value: attributes.weapon.sort(() => 0.5 - Math.random())[0],
    },
    {
      trait_type: 'Rarity',
      value: attributes.rarity.sort(() => 0.5 - Math.random())[0],
    },
    {
      display_type: 'date',
      trait_type: 'Created',
      value: Date.now(),
    },
    {
      display_type: 'number',
      trait_type: 'generation',
      value: 1,
    },
  ],
})
const toWebp = async (image) => await sharp(image).resize(500).webp().toBuffer()
const uploadToIPFS = async (data) => {
  const created = await client.add(data)
  return `https://ipfs.io/ipfs/${created.path}`
}

exports.toWebp = toWebp
exports.toMetadata = toMetadata
exports.uploadToIPFS = uploadToIPFS

Теперь давайте воспользуемся этими функциями в основном файле NodeJs ниже.

Файл App.js Создайте еще один скрипт с именем app.js в этой папке API и вставьте приведенные ниже коды; здесь будет находиться управляющая логика API.

require('dotenv').config()
const cors = require('cors')
const fs = require('fs').promises
const express = require('express')
const fileupload = require('express-fileupload')
const { toWebp, toMetadata, uploadToIPFS } = require('./metadata')

const app = express()

app.use(cors())
app.use(fileupload())
app.use(express.json())
app.use(express.static('public'))
app.use(express.urlencoded({ extended: true }))

app.post('/process', async (req, res) => {
  try {
    const name = req.body.name
    const description = req.body.description
    const price = req.body.price
    const image = req.files.image

    if (!name || !description || !price || !image) {
      return res
        .status(400)
        .send('name, description, and price must not be empty')
    }

    let params

    await toWebp(image.data).then(async (data) => {
      const imageURL = await uploadToIPFS(data)

      params = {
        id: Date.now(),
        name,
        description,
        price,
        image: imageURL,
      }
    })

    fs.writeFile('token.json', JSON.stringify(toMetadata(params)))
      .then(() => {
        fs.readFile('token.json')
          .then(async (data) => {
            const metadataURI = await uploadToIPFS(data)
            console.log({ ...toMetadata(params), metadataURI })
            return res.status(201).json({ ...toMetadata(params), metadataURI })
          })
          .catch((error) => console.log(error))
      })
      .catch((error) => console.log(error))
  } catch (error) {
    console.log(error)
    return res.status(400).json({ error })
  }
})

app.listen(9000, () => {
  console.log('Listen on the port 9000...')
})

Библиотека IPFS использует шлюз Infuria для загрузки файлов в IPFS, который мы уже настроили в файле .env.

Теперь запустите node api/app.js на терминале, чтобы запустить службу API, как показано на изображении ниже.

API receiving Image and Metadata Info from the Frontend

Импорт закрытых ключей в метамаску

Чтобы использовать Metamask с локальной сетью Hardhat, представленной как Localhost:8545, выполните следующие действия для ее настройки.

Запустите yarn hardhat node на своем терминале, чтобы запустить локальный блокчейн-сервер. Вы должны увидеть на терминале изображение, похожее на приведенное ниже.

Hardhat Node Started

Скопируйте закрытый ключ аккаунта в ноль (0) и импортируйте его в свою метамаску, см. изображение ниже.

Step 1: Click on the Import account Option

Step 2: Enter the private key from the Hardhat Server

Step 3: Result

Теперь вы можете повторить описанные выше шаги и импортировать до трех или четырех аккаунтов в зависимости от ваших потребностей.

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

Capturing Smart Contract Development

Разработка внешнего интерфейса

Теперь мы будем использовать React для создания внешнего интерфейса нашего проекта, используя смарт-контракт и связанную с ним информацию, которая была размещена в сети и сгенерирована в виде артефактов (включая байт-коды и ABI). Мы сделаем это, следуя пошаговому процессу.

Компоненты

В каталоге src создайте новую папку с именем **components** для размещения всех перечисленных ниже компонентов React.

Компонент заголовка

Header Component

Теперь создайте компонент в папке компонентов с именем Header.jsx и вставьте следующие коды ниже. Дизайн всех этих компонентов был создан с использованием CSS-фреймворка Tailwind.

import { Link } from 'react-router-dom'
import { connectWallet } from '../services/blockchain'
import { truncate, useGlobalState } from '../store'

const Header = () => {
  const [connectedAccount] = useGlobalState('connectedAccount')
  return (
    <nav className="w-4/5 flex flex-row md:justify-center justify-between items-center py-4 mx-auto">
      <div className="md:flex-[0.5] flex-initial justify-center items-center">
        <Link to="/" className="text-white">
          <span className="px-2 py-1 font-bold text-3xl italic">Dapp</span>
          <span className="py-1 font-semibold italic">Auction-NFT</span>
        </Link>
      </div>

      <ul
        className="md:flex-[0.5] text-white md:flex
      hidden list-none flex-row justify-between 
      items-center flex-initial"
      >
        <Link to="/" className="mx-4 cursor-pointer">Market</Link>
        <Link to="/collections" className="mx-4 cursor-pointer">Collection</Link>
        <Link className="mx-4 cursor-pointer">Artists</Link>
        <Link className="mx-4 cursor-pointer">Community</Link>
      </ul>

      {connectedAccount ? (
        <button
          className="shadow-xl shadow-black text-white
          bg-green-500 hover:bg-green-700 md:text-xs p-2
          rounded-full cursor-pointer text-xs sm:text-base"
        >
          {truncate(connectedAccount, 4, 4, 11)}
        </button>
      ) : (
        <button
          className="shadow-xl shadow-black text-white
          bg-green-500 hover:bg-green-700 md:text-xs p-2
          rounded-full cursor-pointer text-xs sm:text-base"
          onClick={connectWallet}
        >
          Connect Wallet
        </button>
      )}
    </nav>
  )
}
export default Header

Основной компонент

Hero Component

Далее создайте еще один компонент в папке компонентов с именем Hero.jsx и вставьте приведенные ниже коды.

import { toast } from 'react-toastify'
import { BsArrowRightShort } from 'react-icons/bs'
import picture0 from '../assets/images/picture0.png'
import { setGlobalState, useGlobalState } from '../store'
import { loginWithCometChat, signUpWithCometChat } from '../services/chat'

const Hero = () => {
  return (
    <div className="flex flex-col items-start md:flex-row w-4/5 mx-auto mt-11">
      <Banner />
      <Bidder />
    </div>
  )
}

const Bidder = () => (
  <div
    className="w-full text-white overflow-hidden bg-gray-800 rounded-md shadow-xl 
    shadow-black md:w-3/5 lg:w-2/5 md:mt-0 font-sans"
  >
    <img src={picture0} alt="nft" className="object-cover w-full h-60" />
    <div
      className="shadow-lg shadow-gray-400 border-4 border-[#ffffff36] 
      flex flex-row justify-between items-center px-3"
    >
      <div className="p-2">
        Current Bid
        <div className="font-bold text-center">2.231 ETH</div>
      </div>
      <div className="p-2">
        Auction End
        <div className="font-bold text-center">20:10</div>
      </div>
    </div>
    <div
      className="bg-green-500 w-full h-[40px] p-2 text-center 
    font-bold font-mono "
    >
      Place a Bid
    </div>
  </div>
)

const Banner = () => {
  const [currentUser] = useGlobalState('currentUser')

  const handleLogin = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await loginWithCometChat()
          .then((user) => {
            setGlobalState('currentUser', user)
            console.log(user)
            resolve()
          })
          .catch((err) => {
            console.log(err)
            reject()
          })
      }),
      {
        pending: 'Signing in...',
        success: 'Logged in successful 👌',
        error: 'Error, are you signed up? 🤯',
      },
    )
  }

  const handleSignup = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await signUpWithCometChat()
          .then((user) => {
            console.log(user)
            resolve(user)
          })
          .catch((err) => {
            console.log(err)
            reject(err)
          })
      }),
      {
        pending: 'Signing up...',
        success: 'Signned up successful 👌',
        error: 'Error, maybe you should login instead? 🤯',
      },
    )
  }

  return (
    <div
      className="flex flex-col md:flex-row w-full justify-between 
        items-center mx-auto"
    >
      <div className="">
        <h1 className="text-white font-semibold text-5xl py-1">
          Discover, Collect
        </h1>
        <h1 className="font-semibold text-4xl mb-5 text-white py-1">
          and Sell
          <span className="text-green-500 px-1">NFTs</span>.
        </h1>
        <p className="text-white  font-light">
          More than 100+ NFT available for collect
        </p>
        <p className="text-white mb-11 font-light">& sell, get your NFT now.</p>
        <div className="flex flew-row text-5xl mb-4">
          {!currentUser ? (
            <div className="flex justify-start items-center space-x-2">
              <button
                className="text-white text-sm p-2 bg-green-500 rounded-sm w-auto 
                flex flex-row justify-center items-center shadow-md shadow-gray-700"
                onClick={handleLogin}
              >
                Login Now
              </button>
              <button
                className="text-white text-sm p-2 flex flex-row shadow-md shadow-gray-700
                justify-center items-center bg-[#ffffff36] rounded-sm w-auto"
                onClick={handleSignup}
              >
                Signup Now
              </button>
            </div>
          ) : (
            <button
              className="text-white text-sm p-2 bg-green-500 rounded-sm w-auto 
              flex flex-row justify-center items-center shadow-md shadow-gray-700"
              onClick={() => setGlobalState('boxModal', 'scale-100')}
            >
              Create NFT
              <BsArrowRightShort className="font-bold animate-pulse" />
            </button>
          )}
        </div>
        <div className="flex items-center justify-between w-3/4 mt-5">
          <div>
            <p className="text-white font-bold">100k+</p>
            <small className="text-gray-300">Auction</small>
          </div>
          <div>
            <p className="text-white font-bold">210k+</p>
            <small className="text-gray-300">Rare</small>
          </div>
          <div>
            <p className="text-white font-bold">120k+</p>
            <small className="text-gray-300">Artist</small>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Hero

Компонент произведений искусства

Artworks Component

Снова создайте компонент в папке компонентов с именем Artworks.jsx и вставьте приведенные ниже коды.

https://gist.github.com/Daltonic/fd5ec32b945d58138e4dd6c44d8440e6?embedable=true

Компонент нижнего колонтитула

Footer Component

Затем создайте компонент в папке компонентов с именем Footer.jsx и вставьте приведенные ниже коды.

https://gist.github.com/Daltonic/ee3c4c3274d2451afa2e726670bd1a27

Другой компонент

Ниже перечислены компоненты, поддерживающие полную функциональность остальной части этого приложения.

Компонент обратного отсчета Этот компонент отвечает за отображение таймера обратного отсчета для всех NFT, см. приведенные ниже коды.

https://gist.github.com/Daltonic/7e7e215b5c15beac40a17b621cffc35b

Пустой компонент

The Empty Component

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

const Empty = () => {
  return (
    <div className="w-4/5 h-48 py-10 mx-auto justify-center">
      <h4 className="text-xl capitalize text-white mb-4">Nothing here bring some artworks</h4>
    </div>
  )
}

export default Empty

Создание NFT

Creating an NFT

Чтобы написать компонент Create NFT, используйте следующие коды. Это будет модальное окно, которое принимает изображение, заголовок, описание и цену перед отправкой в ​​блокчейн.

Перед сохранением в блокчейне данные, собранные из формы, отправляются в API NodeJs, который преобразует их в метаданные и развертывает в IPFS.

Затем в папке компонентов создайте новый файл с именем «CreateNFT.jsx» и вставьте в него следующий код.

import axios from 'axios'
import { useState } from 'react'
import { toast } from 'react-toastify'
import { FaTimes } from 'react-icons/fa'
import picture6 from '../assets/images/picture6.png'
import { setGlobalState, useGlobalState } from '../store'
import { createNftItem } from '../services/blockchain'

const CreateNFT = () => {
  const [boxModal] = useGlobalState('boxModal')
  const [name, setName] = useState('')
  const [description, setDescription] = useState('')
  const [price, setPrice] = useState('')
  const [fileUrl, setFileUrl] = useState('')
  const [imgBase64, setImgBase64] = useState(null)

  const handleSubmit = async (e) => {
    e.preventDefault()
    if (!name || !price || !description || !fileUrl) return

    const formData = new FormData()
    formData.append('name', name)
    formData.append('price', price)
    formData.append('description', description)
    formData.append('image', fileUrl)

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await axios
          .post('http://localhost:9000/process', formData)
          .then(async (res) => {
            await createNftItem(res.data)
              .then(async () => {
                closeModal()
                resolve()
              })
              .catch(() => reject())
            reject()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Minting & saving data to chain...',
        success: 'Minting completed, will reflect within 30sec 👌',
        error: 'Encountered error 🤯',
      },
    )
  }

  const changeImage = async (e) => {
    const reader = new FileReader()
    if (e.target.files[0]) reader.readAsDataURL(e.target.files[0])

    reader.onload = (readerEvent) => {
      const file = readerEvent.target.result
      setImgBase64(file)
      setFileUrl(e.target.files[0])
    }
  }

  const closeModal = () => {
    setGlobalState('boxModal', 'scale-0')
    resetForm()
  }

  const resetForm = () => {
    setFileUrl('')
    setImgBase64(null)
    setName('')
    setPrice('')
    setDescription('')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
        justify-center bg-black bg-opacity-50 transform
        transition-transform duration-300 ${boxModal}`}
    >
      <div className="bg-[#151c25] shadow-xl shadow-[#25bd9c] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form onSubmit={handleSubmit} className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-gray-400 italic">Add NFT</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-gray-400" />
            </button>
          </div>

          <div className="flex flex-row justify-center items-center rounded-xl mt-5">
            <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
              <img
                alt="NFT"
                className="h-full w-full object-cover cursor-pointer"
                src={imgBase64 || picture6}
              />
            </div>
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <label className="block">
              <span className="sr-only">Choose profile photo</span>
              <input
                type="file"
                accept="image/png, image/gif, image/jpeg, image/webp"
                className="block w-full text-sm text-slate-500
                  file:mr-4 file:py-2 file:px-4
                  file:rounded-full file:border-0
                  file:text-sm file:font-semibold
                  file:bg-[#19212c] file:text-gray-300
                  hover:file:bg-[#1d2631]
                  cursor-pointer focus:ring-0 focus:outline-none"
                onChange={changeImage}
                required
              />
            </label>
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0 px-4 py-2"
              type="text"
              name="name"
              placeholder="Title"
              onChange={(e) => setName(e.target.value)}
              value={name}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0 px-4 py-2"
              type="number"
              name="price"
              step={0.01}
              min={0.01}
              placeholder="Price (Eth)"
              onChange={(e) => setPrice(e.target.value)}
              value={price}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <textarea
              className="block w-full text-sm resize-none
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0 h-18 py-2 px-4"
              type="text"
              name="description"
              placeholder="Description"
              onChange={(e) => setDescription(e.target.value)}
              value={description}
              required
            ></textarea>
          </div>

          <button
            type="submit"
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-[#25bd9c]
              py-2 px-5 rounded-full
              drop-shadow-xl border border-transparent
              hover:bg-transparent hover:text-[#ffffff]
              hover:border hover:border-[#25bd9c]
              focus:outline-none focus:ring mt-5"
          >
            Mint Now
          </button>
        </form>
      </div>
    </div>
  )
}

export default CreateNFT

Предложение NFT на рынке

Offering an NFT Item

Этот компонент отвечает за предложение новых товаров на рынке. Используя форму, он принимает продолжительность, в течение которой вы хотите, чтобы ваш NFT работал на рынке. По истечении этого срока активный NFT исчезнет с рынка. См. приведенные ниже коды.

import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { toast } from 'react-toastify'
import { offerItemOnMarket } from '../services/blockchain'
import { setGlobalState, useGlobalState } from '../store'

const OfferItem = () => {
  const [auction] = useGlobalState('auction')
  const [offerModal] = useGlobalState('offerModal')
  const [period, setPeriod] = useState('')
  const [biddable, setBiddable] = useState('')
  const [timeline, setTimeline] = useState('')

  const handleSubmit = async (e) => {
    e.preventDefault()
    if (!period || !biddable || !timeline) return

    const params = {
      biddable: biddable == 'true',
    }

    if (timeline == 'sec') {
      params.sec = Number(period)
      params.min = 0
      params.hour = 0
      params.day = 0
    } else if (timeline == 'min') {
      params.sec = 0
      params.min = Number(period)
      params.hour = 0
      params.day = 0
    } else if (timeline == 'hour') {
      params.sec = 0
      params.min = 0
      params.hour = Number(period)
      params.day = 0
    } else {
      params.sec = 0
      params.min = 0
      params.hour = 0
      params.day = Number(period)
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await offerItemOnMarket({ ...auction, ...params })
          .then(async () => {
            closeModal()
            resolve()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Processing...',
        success: 'Offered on Market, will reflect within 30sec 👌',
        error: 'Encountered error 🤯',
      },
    )
  }

  const closeModal = () => {
    setGlobalState('offerModal', 'scale-0')
    setPeriod('')
    setBiddable('')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
        justify-center bg-black bg-opacity-50 transform
        transition-transform timeline-300 ${offerModal}`}
    >
      <div className="bg-[#151c25] shadow-xl shadow-[#25bd9c] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form onSubmit={handleSubmit} className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-gray-400 italic">
              {auction?.name}
            </p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-gray-400" />
            </button>
          </div>

          <div className="flex flex-row justify-center items-center rounded-xl mt-5">
            <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
              <img
                alt="NFT"
                className="h-full w-full object-cover cursor-pointer"
                src={auction?.image}
              />
            </div>
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0 px-4 py-2"
              type="number"
              name="period"
              min={1}
              placeholder="Days E.g 7"
              onChange={(e) => setPeriod(e.target.value)}
              value={period}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <select
              className="block w-full text-sm
              text-slate-500 bg-transparent border-0
              focus:outline-none focus:ring-0 px-4 py-2"
              name="biddable"
              onChange={(e) => setTimeline(e.target.value)}
              value={timeline}
              required
            >
              <option value="" hidden>
                Select Duration
              </option>
              <option value="sec">Seconds</option>
              <option value="min">Minutes</option>
              <option value="hour">Hours</option>
              <option value="day">Days</option>
            </select>
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <select
              className="block w-full text-sm
              text-slate-500 bg-transparent border-0
              focus:outline-none focus:ring-0 px-4 py-2"
              name="biddable"
              onChange={(e) => setBiddable(e.target.value)}
              value={biddable}
              required
            >
              <option value="" hidden>
                Select Biddability
              </option>
              <option value={true}>Yes</option>
              <option value={false}>No</option>
            </select>
          </div>

          <button
            type="submit"
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-[#25bd9c]
              py-2 px-5 rounded-full
              drop-shadow-xl border border-transparent
              hover:bg-transparent hover:text-[#ffffff]
              hover:border hover:border-[#25bd9c]
              focus:outline-none focus:ring mt-5"
          >
            Offer Item
          </button>
        </form>
      </div>
    </div>
  )
}

export default OfferItem

Размещение ставок

Placing Bids

Этот компонент позволяет пользователю делать ставки и участвовать в аукционе NFT. Это достигается за счет использования модального окна, которое получает цену, которую пользователь намеревается предложить, если она находится в пределах срока для торгов. См. приведенные ниже коды.

import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { toast } from 'react-toastify'
import { bidOnNFT } from '../services/blockchain'
import { setGlobalState, useGlobalState } from '../store'

const PlaceBid = () => {
  const [auction] = useGlobalState('auction')
  const [bidBox] = useGlobalState('bidBox')
  const [price, setPrice] = useState('')

  const closeModal = () => {
    setGlobalState('bidBox', 'scale-0')
    setPrice('')
  }

  const handleBidPlacement = async (e) => {
    e.preventDefault()
    if (!price) return

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await bidOnNFT({ ...auction, price })
          .then(() => {
            resolve()
            closeModal()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Processing...',
        success: 'Bid placed successful, will reflect within 30sec 👌',
        error: 'Encountered error 🤯',
      },
    )
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
        justify-center bg-black bg-opacity-50 transform
        transition-transform duration-300 ${bidBox}`}
    >
      <div className="bg-[#151c25] shadow-xl shadow-[#25bd9c] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form onSubmit={handleBidPlacement} className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-gray-400 italic">
              {auction?.name}
            </p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-gray-400" />
            </button>
          </div>

          <div className="flex flex-row justify-center items-center rounded-xl mt-5">
            <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
              <img
                alt="NFT"
                className="h-full w-full object-cover cursor-pointer"
                src={auction?.image}
              />
            </div>
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0 px-4 py-2"
              type="number"
              name="price"
              step={0.01}
              min={0.01}
              placeholder="Price (Eth)"
              onChange={(e) => setPrice(e.target.value)}
              value={price}
              required
            />
          </div>

          <button
            type="submit"
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-[#25bd9c]
              py-2 px-5 rounded-full
              drop-shadow-xl border border-transparent
              hover:bg-transparent hover:text-[#ffffff]
              hover:border hover:border-[#25bd9c]
              focus:outline-none focus:ring mt-5"
          >
            Place Bid
          </button>
        </form>
      </div>
    </div>
  )
}

export default PlaceBid

Изменение цены NFT

Changing NFT Price

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

import { useState } from 'react'
import { toast } from 'react-toastify'
import { FaTimes } from 'react-icons/fa'
import { updatePrice } from '../services/blockchain'
import { setGlobalState, useGlobalState } from '../store'

const ChangePrice = () => {
  const [auction] = useGlobalState('auction')
  const [priceModal] = useGlobalState('priceModal')
  const [price, setPrice] = useState('')

  const handleSubmit = async (e) => {
    e.preventDefault()
    if (!price) return

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await updatePrice({ ...auction, price })
          .then(async () => {
            closeModal()
            resolve()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Processing...',
        success: 'Price updated, will reflect within 30sec 👌',
        error: 'Encountered error 🤯',
      },
    )
  }

  const closeModal = () => {
    setGlobalState('priceModal', 'scale-0')
    setPrice('')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
        justify-center bg-black bg-opacity-50 transform
        transition-transform timeline-300 ${priceModal}`}
    >
      <div className="bg-[#151c25] shadow-xl shadow-[#25bd9c] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form onSubmit={handleSubmit} className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-gray-400 italic">
              {auction?.name}
            </p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-gray-400" />
            </button>
          </div>

          <div className="flex flex-row justify-center items-center rounded-xl mt-5">
            <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
              <img
                alt="NFT"
                className="h-full w-full object-cover cursor-pointer"
                src={auction?.image}
              />
            </div>
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0 px-4 py-2"
              type="number"
              name="price"
              step={0.01}
              min={0.01}
              placeholder="Days E.g 2.3 ETH"
              onChange={(e) => setPrice(e.target.value)}
              value={price}
              required
            />
          </div>

          <button
            type="submit"
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-[#25bd9c]
              py-2 px-5 rounded-full
              drop-shadow-xl border border-transparent
              hover:bg-transparent hover:text-[#ffffff]
              hover:border hover:border-[#25bd9c]
              focus:outline-none focus:ring mt-5"
          >
            Change Price
          </button>
        </form>
      </div>
    </div>
  )
}

export default ChangePrice

Компонент чата

Live Chat with CometChat SDK

Наконец, для компонентов есть компонент чата, которым управляет CometChat SDK. См. приведенные ниже коды.

import { useEffect, useState } from 'react'
import Identicon from 'react-identicons'
import { toast } from 'react-toastify'
import { getMessages, listenForMessage, sendMessage } from '../services/chat'
import { truncate, useGlobalState } from '../store'

const Chat = ({ id, group }) => {
  const [message, setMessage] = useState('')
  const [messages, setMessages] = useState([])
  const [connectedAccount] = useGlobalState('connectedAccount')

  const handleSubmit = async (e) => {
    e.preventDefault()

    if (!message) return
    await sendMessage(`pid_${id}`, message)
      .then(async (msg) => {
        setMessages((prevState) => [...prevState, msg])
        setMessage('')
        scrollToEnd()
      })
      .catch((error) => {
        toast.error('Encountered Error, check the console')
        console.log(error)
      })
  }

  useEffect(async () => {
    await getMessages(`pid_${id}`)
      .then((msgs) => {
        setMessages(msgs)
        scrollToEnd()
      })
      .catch((error) => console.log(error))

    await listenForMessage(`pid_${id}`)
      .then((msg) => {
        setMessages((prevState) => [...prevState, msg])
        scrollToEnd()
      })
      .catch((error) => console.log(error))
  }, [])

  const scrollToEnd = () => {
    const elmnt = document.getElementById('messages-container')
    elmnt.scrollTop = elmnt.scrollHeight
  }

  return (
    <div>
      <h2 className="mt-12 px-2 py-1 font-bold text-2xl italic">NFT-World</h2>
      <h4 className="px-2 font-semibold text-xs">Join the Live Chat</h4>
      <div
        className="bg-gray-800 bg-opacity-50 w-full
        rounded-md p-2 sm:p-8 mt-5 shadow-md shadow-[#25bd9c]"
      >
        <div
          id="messages-container"
          className="h-[calc(100vh_-_30rem)] overflow-y-auto"
        >
          {messages.map((msg, i) => (
            <Message
              isOwner={msg.sender.uid == connectedAccount}
              owner={msg.sender.uid}
              msg={msg.text}
              key={i}
            />
          ))}
        </div>

        <form
          onSubmit={handleSubmit}
          className="flex flex-row justify-between items-center bg-gray-800 rounded-md"
        >
          <input
            className="block w-full text-sm resize-none
            text-slate-100 bg-transparent border-0
              focus:outline-none focus:ring-0 h-15 px-4 py-4"
            type="text"
            name="Leave a Message"
            placeholder={
              !group?.hasJoined
                ? 'Join group first to chat...'
                : 'Leave a Message...'
            }
            disabled={!group?.hasJoined}
            value={message}
            onChange={(e) => setMessage(e.target.value)}
            required
          />
          <button type="submit" hidden>
            Send
          </button>
        </form>
      </div>
    </div>
  )
}

const Message = ({ msg, owner, isOwner }) => (
  <div>
    <div className="flex justify-start items-center space-x-1 mb-2">
      <Identicon
        string={owner}
        className="h-5 w-5 object-contain bg-gray-800 rounded-full"
        size={18}
      />
      <div className="space-x-1">
        <span className="text-[#25bd9c] font-bold">
          {isOwner ? '@You' : truncate(owner, 4, 4, 11)}
        </span>
        <span className="text-gray-200 text-xs">{msg}</span>
      </div>
    </div>
  </div>
)

export default Chat

Страницы

Это приложение имеет около трех представлений или страниц; давайте организуем все вышеперечисленные компоненты в соответствующие представления, используя следующие шаги. Сначала создайте папку views в каталоге src и создайте страницы, которые скоро будут обсуждаться.

Главная страница

The Home View

Представление Home объединяет два основных компонента: компоненты Hero и Artworks. См. приведенные ниже коды.

import Artworks from '../components/Artworks'
import Empty from '../components/Empty'
import Hero from '../components/Hero'
import { useGlobalState } from '../store'

const Home = () => {
  const [auctions] = useGlobalState('auctions')
  return (
    <div>
      <Hero />
      {auctions.length > 0 ? <Artworks auctions={auctions} /> : <Empty />}
    </div>
  )
}

export default Home

Просмотр коллекций

The Collection View

В этом представлении отображаются все NFT, принадлежащие конкретному пользователю. Он позволяет пользователю управлять NFT, например, предлагать ли его на рынке или изменять его цену. См. приведенные ниже коды.

import { useEffect } from 'react'
import Empty from '../components/Empty'
import { useGlobalState } from '../store'
import Artworks from '../components/Artworks'
import { loadCollections } from '../services/blockchain'

const Collections = () => {
  const [collections] = useGlobalState('collections')
  useEffect(async () => {
    await loadCollections()
  })
  return (
    <div>
      {collections.length > 0 ? (
        <Artworks title="Your Collections" auctions={collections} showOffer />
      ) : (
        <Empty />
      )}
    </div>
  )
}

export default Collections

Просмотр NFT

NFT View

Наконец, это представление содержит компонент чата, а также другие важные компоненты, как показано в коде ниже.

import { useEffect } from 'react'
import Chat from '../components/Chat'
import { toast } from 'react-toastify'
import Identicons from 'react-identicons'
import { useNavigate, useParams } from 'react-router-dom'
import Countdown from '../components/Countdown'
import { setGlobalState, truncate, useGlobalState } from '../store'
import {
  buyNFTItem,
  claimPrize,
  getBidders,
  loadAuction,
} from '../services/blockchain'
import { createNewGroup, getGroup, joinGroup } from '../services/chat'

const Nft = () => {
  const { id } = useParams()
  const [group] = useGlobalState('group')
  const [bidders] = useGlobalState('bidders')
  const [auction] = useGlobalState('auction')
  const [currentUser] = useGlobalState('currentUser')
  const [connectedAccount] = useGlobalState('connectedAccount')

  useEffect(async () => {
    await loadAuction(id)
    await getBidders(id)
    await getGroup(`pid_${id}`)
      .then((group) => setGlobalState('group', group))
      .catch((error) => console.log(error))
  }, [])

  return (
    <>
      <div
        className="grid sm:flex-row md:flex-row lg:grid-cols-2 gap-6
        md:gap-4 lg:gap-3 py-2.5 text-white font-sans capitalize
        w-4/5 mx-auto mt-5 justify-between items-center"
      >
        <div
          className=" text-white h-[400px] bg-gray-800 rounded-md shadow-xl 
        shadow-black md:w-4/5 md:items-center lg:w-4/5 md:mt-0"
        >
          <img
            src={auction?.image}
            alt={auction?.name}
            className="object-contain w-full h-80 mt-10"
          />
        </div>
        <div className="">
          <Details auction={auction} account={connectedAccount} />

          {bidders.length > 0 ? (
            <Bidders bidders={bidders} auction={auction} />
          ) : null}

          <CountdownNPrice auction={auction} />

          <ActionButton auction={auction} account={connectedAccount} />
        </div>
      </div>
      <div className="w-4/5 mx-auto">
        {currentUser ? <Chat id={id} group={group} /> : null}
      </div>
    </>
  )
}

const Details = ({ auction, account }) => (
  <div className="py-2">
    <h1 className="font-bold text-lg mb-1">{auction?.name}</h1>
    <p className="font-semibold text-sm">
      <span className="text-green-500">
        @
        {auction?.owner == account
          ? 'you'
          : auction?.owner
          ? truncate(auction?.owner, 4, 4, 11)
          : ''}
      </span>
    </p>
    <p className="text-sm py-2">{auction?.description}</p>
  </div>
)

const Bidders = ({ bidders, auction }) => {
  const handlePrizeClaim = async (id) => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await claimPrize({ tokenId: auction?.tokenId, id })
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Processing...',
        success: 'Price claim successful, will reflect within 30sec 👌',
        error: 'Encountered error 🤯',
      },
    )
  }

  return (
    <div className="flex flex-col">
      <span>Top Bidders</span>
      <div className="h-[calc(100vh_-_40.5rem)] overflow-y-auto">
        {bidders.map((bid, i) => (
          <div key={i} className="flex justify-between items-center">
            <div className="flex justify-start items-center my-1 space-x-1">
              <Identicons
                className="h-5 w-5 object-contain bg-gray-800 rounded-full"
                size={18}
                string={bid.bidder}
              />
              <span className="font-medium text-sm mr-3">
                {truncate(bid.bidder, 4, 4, 11)}
              </span>
              <span className="text-green-400 font-medium text-sm">
                {bid.price} ETH
              </span>
            </div>

            {bid.bidder == auction?.winner &&
            !bid.won &&
            Date.now() > auction?.duration ? (
              <button
                type="button"
                className="shadow-sm shadow-black text-white
            bg-green-500 hover:bg-green-700 md:text-xs p-1
              rounded-sm text-sm cursor-pointer font-light"
                onClick={() => handlePrizeClaim(i)}
              >
                Claim Prize
              </button>
            ) : null}
          </div>
        ))}
      </div>
    </div>
  )
}

const CountdownNPrice = ({ auction }) => {
  return (
    <div className="flex justify-between items-center py-5 ">
      <div>
        <span className="font-bold">Current Price</span>
        <p className="text-sm font-light">{auction?.price}ETH</p>
      </div>

      <div className="lowercase">
        <span className="font-bold">
          {auction?.duration > Date.now() ? (
            <Countdown timestamp={auction?.duration} />
          ) : (
            '00:00:00'
          )}
        </span>
      </div>
    </div>
  )
}

const ActionButton = ({ auction, account }) => {
  const [group] = useGlobalState('group')
  const [currentUser] = useGlobalState('currentUser')
  const navigate = useNavigate()

  const onPlaceBid = () => {
    setGlobalState('auction', auction)
    setGlobalState('bidBox', 'scale-100')
  }

  const handleNFTpurchase = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await buyNFTItem(auction)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Processing...',
        success: 'Purchase successful, will reflect within 30sec 👌',
        error: 'Encountered error 🤯',
      },
    )
  }

  const handleCreateGroup = async () => {
    if (!currentUser) {
      navigate('/')
      toast.warning('You need to login or sign up first.')
      return
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await createNewGroup(`pid_${auction?.tokenId}`, auction?.name)
          .then((gp) => {
            setGlobalState('group', gp)
            resolve(gp)
          })
          .catch((error) => reject(new Error(error)))
      }),
      {
        pending: 'Creating...',
        success: 'Group created 👌',
        error: 'Encountered error 🤯',
      },
    )
  }

  const handleJoineGroup = async () => {
    if (!currentUser) {
      navigate('/')
      toast.warning('You need to login or sign up first.')
      return
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await joinGroup(`pid_${auction?.tokenId}`)
          .then((gp) => {
            setGlobalState('group', gp)
            resolve(gp)
          })
          .catch((error) => reject(new Error(error)))
      }),
      {
        pending: 'Joining...',
        success: 'Group Joined 👌',
        error: 'Encountered error 🤯',
      },
    )
  }

  return auction?.owner == account ? (
    <div className="flex justify-start items-center space-x-2 mt-2">
      {!group ? (
        <button
          type="button"
          className="shadow-sm shadow-black text-white
          bg-red-500 hover:bg-red-700 md:text-xs p-2.5
          rounded-sm cursor-pointer font-light"
          onClick={handleCreateGroup}
        >
          Create Group
        </button>
      ) : null}
    </div>
  ) : (
    <div className="flex justify-start items-center space-x-2 mt-2">
      {!group?.hasJoined ? (
        <button
          type="button"
          className="shadow-sm shadow-black text-white
          bg-gray-500 hover:bg-gray-700 md:text-xs p-2.5
          rounded-sm cursor-pointer font-light"
          onClick={handleJoineGroup}
        >
          Join Group
        </button>
      ) : null}

      {auction?.biddable && auction?.duration > Date.now() ? (
        <button
          type="button"
          className="shadow-sm shadow-black text-white
          bg-gray-500 hover:bg-gray-700 md:text-xs p-2.5
          rounded-sm cursor-pointer font-light"
          onClick={onPlaceBid}
        >
          Place a Bid
        </button>
      ) : null}

      {!auction?.biddable && auction?.duration > Date.now() ? (
        <button
          type="button"
          className="shadow-sm shadow-black text-white
          bg-red-500 hover:bg-red-700 md:text-xs p-2.5
          rounded-sm cursor-pointer font-light"
          onClick={handleNFTpurchase}
        >
          Buy NFT
        </button>
      ) : null}
    </div>
  )
}

export default Nft

Обновление файла App.jsx

Обновите файл приложения с помощью приведенных ниже кодов, чтобы объединить все компоненты и страницы вместе.

import Nft from './views/Nft'
import Home from './views/Home'
import Header from './components/Header'
import Footer from './components/Footer'
import { useEffect, useState } from 'react'
import PlaceBid from './components/PlaceBid'
import Collections from './views/Collections'
import CreateNFT from './components/CreateNFT'
import { ToastContainer } from 'react-toastify'
import { Route, Routes } from 'react-router-dom'
import { isWallectConnected, loadAuctions } from './services/blockchain'
import { setGlobalState, useGlobalState } from './store'
import OfferItem from './components/OfferItem'
import ChangePrice from './components/ChangePrice'
import { checkAuthState } from './services/chat'

function App() {
  const [loaded, setLoaded] = useState(false)
  const [auction] = useGlobalState('auction')
  useEffect(async () => {
    await isWallectConnected()
    await loadAuctions().finally(() => setLoaded(true))
    await checkAuthState()
      .then((user) => setGlobalState('currentUser', user))
      .catch((error) => setGlobalState('currentUser', null))
    console.log('Blockchain Loaded')
  }, [])

  return (
    <div
      className="min-h-screen bg-gradient-to-t from-gray-800 bg-repeat
    via-[#25bd9c] to-gray-900 bg-center subpixel-antialiased"
    >
      <Header />
      {loaded ? (
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/collections" element={<Collections />} />
          <Route path="/nft/:id" element={<Nft />} />
        </Routes>
      ) : null}
      <CreateNFT />
      {auction ? (
        <>
          <PlaceBid />
          <OfferItem />
          <ChangePrice />
        </>
      ) : null}
      <Footer />
      <ToastContainer
        position="bottom-center"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
        theme="dark"
      />
    </div>
  )
}
export default App

Обновление файлов Index.jsx и CSS

Используйте приведенные ниже коды для обновления файлов index.jsx и index.css соответственно.

@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap');

* html {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: 'Open Sans', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.gradient-bg-hero {
  background-color: #151c25;
  background-image: radial-gradient(
      at 0% 0%,
      hsl(302deg 25% 18%) 0,
      transparent 50%
    ),
    radial-gradient(at 50% 0%, hsl(0deg 39% 30%) 0, transparent 50%),
    radial-gradient(at 100% 0%, hsla(339, 49%, 30%, 1) 0, transparent 50%);
}

.gradient-bg-artworks {
  background-color: #0f0e13;
  background-image: radial-gradient(
      at 50% 50%,
      hsl(302deg 25% 18%) 0,
      transparent 50%
    ),
    radial-gradient(at 0% 0%, hsla(253, 16%, 7%, 1) 0, transparent 50%),
    radial-gradient(at 50% 50%, hsla(339, 39%, 25%, 1) 0, transparent 50%);
}

.gradient-bg-footer {
  background-color: #151c25;
  background-image: radial-gradient(
      at 0% 100%,
      hsl(0deg 39% 30%) 0,
      transparent 53%
    ),
    radial-gradient(at 50% 150%, hsla(339, 49%, 30%, 1) 0, transparent 50%);
}

.text-gradient {
  background: -webkit-linear-gradient(#eee, #333);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.lds-dual-ring {
  display: inline-block;
}
.lds-dual-ring:after {
  content: ' ';
  display: block;
  width: 64px;
  height: 64px;
  margin: 8px;
  border-radius: 50%;
  border: 6px solid #fff;
  border-color: #fff transparent #fff transparent;
  animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

@tailwind base;
@tailwind components;
@tailwind utilities;

import './index.css'
import App from './App'
import React from 'react'
import ReactDOM from 'react-dom'
import 'react-toastify/dist/ReactToastify.css'
import { initCometChat } from './services/chat'
import { BrowserRouter } from 'react-router-dom'

initCometChat().then(() => {
  ReactDOM.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>,
    document.getElementById('root'),
  )
})

Добавление служб приложений

В этом приложении у нас есть две службы: чат и блокчейн, как показано в кодах ниже. Просто создайте новую папку с именем services в каталоге src и поместите в нее следующие файлы, используя приведенные ниже коды.

Службы чата

import { CometChat } from '@cometchat-pro/chat'
import { getGlobalState } from '../store'

const CONSTANTS = {
  APP_ID: process.env.REACT_APP_COMET_CHAT_APP_ID,
  REGION: process.env.REACT_APP_COMET_CHAT_REGION,
  Auth_Key: process.env.REACT_APP_COMET_CHAT_AUTH_KEY,
}

const initCometChat = async () => {
  const appID = CONSTANTS.APP_ID
  const region = CONSTANTS.REGION

  const appSetting = new CometChat.AppSettingsBuilder()
    .subscribePresenceForAllUsers()
    .setRegion(region)
    .build()

  await CometChat.init(appID, appSetting)
    .then(() => console.log('Initialization completed successfully'))
    .catch((error) => console.log(error))
}

const loginWithCometChat = async () => {
  const authKey = CONSTANTS.Auth_Key
  const UID = getGlobalState('connectedAccount')

  return new Promise(async (resolve, reject) => {
    await CometChat.login(UID, authKey)
      .then((user) => resolve(user))
      .catch((error) => reject(error))
  })
}

const signUpWithCometChat = async () => {
  const authKey = CONSTANTS.Auth_Key
  const UID = getGlobalState('connectedAccount')
  const user = new CometChat.User(UID)

  user.setName(UID)
  return new Promise(async (resolve, reject) => {
    await CometChat.createUser(user, authKey)
      .then((user) => resolve(user))
      .catch((error) => reject(error))
  })
}

const logOutWithCometChat = async () => {
  return new Promise(async (resolve, reject) => {
    await CometChat.logout()
      .then(() => resolve())
      .catch(() => reject())
  })
}

const checkAuthState = async () => {
  return new Promise(async (resolve, reject) => {
    await CometChat.getLoggedinUser()
      .then((user) => resolve(user))
      .catch((error) => reject(error))
  })
}

const createNewGroup = async (GUID, groupName) => {
  const groupType = CometChat.GROUP_TYPE.PUBLIC
  const password = ''
  const group = new CometChat.Group(GUID, groupName, groupType, password)

  return new Promise(async (resolve, reject) => {
    await CometChat.createGroup(group)
      .then((group) => resolve(group))
      .catch((error) => reject(error))
  })
}

const getGroup = async (GUID) => {
  return new Promise(async (resolve, reject) => {
    await CometChat.getGroup(GUID)
      .then((group) => resolve(group))
      .catch((error) => reject(error))
  })
}

const joinGroup = async (GUID) => {
  const groupType = CometChat.GROUP_TYPE.PUBLIC
  const password = ''

  return new Promise(async (resolve, reject) => {
    await CometChat.joinGroup(GUID, groupType, password)
      .then((group) => resolve(group))
      .catch((error) => reject(error))
  })
}

const getMessages = async (UID) => {
  const limit = 30
  const messagesRequest = new CometChat.MessagesRequestBuilder()
    .setGUID(UID)
    .setLimit(limit)
    .build()

  return new Promise(async (resolve, reject) => {
    await messagesRequest
      .fetchPrevious()
      .then((messages) => resolve(messages.filter((msg) => msg.type == 'text')))
      .catch((error) => reject(error))
  })
}

const sendMessage = async (receiverID, messageText) => {
  const receiverType = CometChat.RECEIVER_TYPE.GROUP
  const textMessage = new CometChat.TextMessage(
    receiverID,
    messageText,
    receiverType,
  )
  return new Promise(async (resolve, reject) => {
    await CometChat.sendMessage(textMessage)
      .then((message) => resolve(message))
      .catch((error) => reject(error))
  })
}

const listenForMessage = async (listenerID) => {
  return new Promise(async (resolve, reject) => {
    CometChat.addMessageListener(
      listenerID,
      new CometChat.MessageListener({
        onTextMessageReceived: (message) => resolve(message),
      }),
    )
  })
}

export {
  initCometChat,
  loginWithCometChat,
  signUpWithCometChat,
  logOutWithCometChat,
  getMessages,
  sendMessage,
  checkAuthState,
  createNewGroup,
  getGroup,
  joinGroup,
  listenForMessage,
}

Блокчейн-сервис

import abi from '../abis/src/contracts/Auction.sol/Auction.json'
import address from '../abis/contractAddress.json'
import { getGlobalState, setGlobalState } from '../store'
import { ethers } from 'ethers'
import { checkAuthState, logOutWithCometChat } from './chat'

const { ethereum } = window
const ContractAddress = address.address
const ContractAbi = abi.abi
let tx

const toWei = (num) => ethers.utils.parseEther(num.toString())
const fromWei = (num) => ethers.utils.formatEther(num)

const getEthereumContract = async () => {
  const connectedAccount = getGlobalState('connectedAccount')

  if (connectedAccount) {
    const provider = new ethers.providers.Web3Provider(ethereum)
    const signer = provider.getSigner()
    const contract = new ethers.Contract(ContractAddress, ContractAbi, signer)

    return contract
  } else {
    return getGlobalState('contract')
  }
}

const isWallectConnected = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const accounts = await ethereum.request({ method: 'eth_accounts' })
    setGlobalState('connectedAccount', accounts[0]?.toLowerCase())

    window.ethereum.on('chainChanged', (chainId) => {
      window.location.reload()
    })

    window.ethereum.on('accountsChanged', async () => {
      setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
      await isWallectConnected()
      await loadCollections()
      await logOutWithCometChat()
      await checkAuthState()
        .then((user) => setGlobalState('currentUser', user))
        .catch((error) => setGlobalState('currentUser', null))
    })

    if (accounts.length) {
      setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
    } else {
      alert('Please connect wallet.')
      console.log('No accounts found.')
    }
  } catch (error) {
    reportError(error)
  }
}

const connectWallet = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
    setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
  } catch (error) {
    reportError(error)
  }
}

const createNftItem = async ({
  name,
  description,
  image,
  metadataURI,
  price,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEthereumContract()
    tx = await contract.createAuction(
      name,
      description,
      image,
      metadataURI,
      toWei(price),
      {
        from: connectedAccount,
        value: toWei(0.02),
      },
    )
    await tx.wait()
    await loadAuctions()
  } catch (error) {
    reportError(error)
  }
}

const updatePrice = async ({ tokenId, price }) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEthereumContract()
    tx = await contract.changePrice(tokenId, toWei(price), {
      from: connectedAccount,
    })
    await tx.wait()
    await loadAuctions()
  } catch (error) {
    reportError(error)
  }
}

const offerItemOnMarket = async ({
  tokenId,
  biddable,
  sec,
  min,
  hour,
  day,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEthereumContract()
    tx = await contract.offerAuction(tokenId, biddable, sec, min, hour, day, {
      from: connectedAccount,
    })
    await tx.wait()
    await loadAuctions()
  } catch (error) {
    reportError(error)
  }
}

const buyNFTItem = async ({ tokenId, price }) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEthereumContract()
    tx = await contract.buyAuctionedItem(tokenId, {
      from: connectedAccount,
      value: toWei(price),
    })
    await tx.wait()
    await loadAuctions()
    await loadAuction(tokenId)
  } catch (error) {
    reportError(error)
  }
}

const bidOnNFT = async ({ tokenId, price }) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEthereumContract()
    tx = await contract.placeBid(tokenId, {
      from: connectedAccount,
      value: toWei(price),
    })

    await tx.wait()
    await getBidders(tokenId)
    await loadAuction(tokenId)
  } catch (error) {
    reportError(error)
  }
}

const claimPrize = async ({ tokenId, id }) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEthereumContract()
    tx = await contract.claimPrize(tokenId, id, {
      from: connectedAccount,
    })
    await tx.wait()
    await getBidders(tokenId)
  } catch (error) {
    reportError(error)
  }
}

const loadAuctions = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = await getEthereumContract()
    const auctions = await contract.getLiveAuctions()
    setGlobalState('auctions', structuredAuctions(auctions))
    setGlobalState(
      'auction',
      structuredAuctions(auctions).sort(() => 0.5 - Math.random())[0],
    )
  } catch (error) {
    reportError(error)
  }
}

const loadAuction = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = await getEthereumContract()
    const auction = await contract.getAuction(id)
    setGlobalState('auction', structuredAuctions([auction])[0])
  } catch (error) {
    reportError(error)
  }
}

const getBidders = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = await getEthereumContract()
    const bidders = await contract.getBidders(id)
    setGlobalState('bidders', structuredBidders(bidders))
  } catch (error) {
    reportError(error)
  }
}

const loadCollections = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = await getEthereumContract()
    const collections = await contract.getMyAuctions({ from: connectedAccount })
    setGlobalState('collections', structuredAuctions(collections))
  } catch (error) {
    reportError(error)
  }
}

const structuredAuctions = (auctions) =>
  auctions
    .map((auction) => ({
      tokenId: auction.tokenId.toNumber(),
      owner: auction.owner.toLowerCase(),
      seller: auction.seller.toLowerCase(),
      winner: auction.winner.toLowerCase(),
      name: auction.name,
      description: auction.description,
      duration: Number(auction.duration + '000'),
      image: auction.image,
      price: fromWei(auction.price),
      biddable: auction.biddable,
      sold: auction.sold,
      live: auction.live,
    }))
    .reverse()

const structuredBidders = (bidders) =>
  bidders
    .map((bidder) => ({
      timestamp: Number(bidder.timestamp + '000'),
      bidder: bidder.bidder.toLowerCase(),
      price: fromWei(bidder.price),
      refunded: bidder.refunded,
      won: bidder.won,
    }))
    .sort((a, b) => b.price - a.price)

const reportError = (error) => {
  console.log(error.message)
  throw new Error('No ethereum object.')
}

export {
  isWallectConnected,
  connectWallet,
  createNftItem,
  loadAuctions,
  loadAuction,
  loadCollections,
  offerItemOnMarket,
  buyNFTItem,
  bidOnNFT,
  getBidders,
  claimPrize,
  updatePrice,
}

Магазин

Магазин — это служба управления состоянием, включенная в это приложение. Здесь хранятся все данные, извлеченные из блокчейна. Для репликации создайте папку хранилища в каталоге src. Затем в этой папке создайте файл index.jsx и вставьте в него приведенные ниже коды.

import { createGlobalState } from 'react-hooks-global-state'

const { getGlobalState, useGlobalState, setGlobalState } = createGlobalState({
  boxModal: 'scale-0',
  bidBox: 'scale-0',
  offerModal: 'scale-0',
  priceModal: 'scale-0',
  connectedAccount: '',
  collections: [],
  bidders: [],
  auctions: [],
  auction: null,
  currentUser: null,
  group: null,
})

const truncate = (text, startChars, endChars, maxLength) => {
  if (text.length > maxLength) {
    let start = text.substring(0, startChars)
    let end = text.substring(text.length - endChars, text.length)
    while (start.length + end.length < maxLength) {
      start = start + '.'
    }
    return start + end
  }
  return text
}

const convertToSeconds = (minutes, hours, days) => {
  const seconds = minutes * 60 + hours * 3600 + days * 86400
  const timestamp = new Date().getTime()
  return timestamp + seconds
}

export {
  getGlobalState,
  useGlobalState,
  setGlobalState,
  truncate,
  convertToSeconds,
}

Теперь запустите приложение, запустив **yarn start** на другом терминале, чтобы увидеть результат на терминале. Если у вас возникнут проблемы с воспроизведением этого проекта, вы можете задать вопрос на нашем канале Discord.

Вы также можете посмотреть это видео, чтобы узнать больше о том, как создать торговую площадку NFT от проектирования до развертывания.

https://youtu.be/oizjJKlPwHo?embedable=true

Поздравляем, именно так вы создаете торговую площадку NFT с помощью React, Solidity и CometChat.

Заключение

В заключение, создание аукционного сайта NFT с помощью React, Solidity и CometChat требует сочетания навыков разработки интерфейса и сервера.

Используя эти инструменты вместе, можно создать полнофункциональный аукционный сайт NFT, который будет безопасным, масштабируемым и удобным для пользователя.

Если вы готовы глубже погрузиться в разработку Web3, запишитесь на частные уроки Web3 со мной, чтобы ускорить изучение Web3.

Тем не менее, увидимся в следующий раз и хорошего дня!

Об авторе

Госпел Дарлингтон – разработчик комплексного блокчейна с 6+ летним опытом работы в индустрии разработки программного обеспечения.

Сочетая разработку программного обеспечения, написание текстов и преподавание, он демонстрирует, как создавать децентрализованные приложения в сетях блокчейнов, совместимых с EVM.

Его стеки включают JavaScript, React, Vue, Angular, Node, React Native, NextJs, Solidity и другие.


Оригинал