Добавить в корзинуПозвонить
Найти в Дзене
Nuances of programming

Как создать NFT-маркетплейс с полным стеком

Источник: Nuances of Programming Чтобы познакомиться с V1 этого руководства, нажмите здесь. Чтобы просмотреть видеокурс к нему, кликните здесь. Из этого руководства вы узнаете, как разработать, развернуть и протестировать полнофункциональный NFT-маркетплейс на Ethereum. Мы также рассмотрим развертывание на Polygon. Уже нельзя не замечать того, как быстро набирают обороты и внедряются такие решения по Ethereum-масштабированию, как Polygon, Arbitrum и Optimism. Эти технологии позволяют разработчикам создавать те же приложения, что и непосредственно на Ethereum, только с дополнительными преимуществами в виде более низкой стоимости газа и более высокой скорости транзакций. Учитывая выгодность этих технологий, а также отсутствие руководств по ним, я буду создавать проекты и туториалы по разработке полнофункциональных приложений с использованием различных решений по Ethereum-масштабированию. В этой статье начну с проекта на Polygon. Просмотреть окончательный исходный код данного проекта можн
Оглавление

Источник: Nuances of Programming

Чтобы познакомиться с V1 этого руководства, нажмите здесь. Чтобы просмотреть видеокурс к нему, кликните здесь.

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

Уже нельзя не замечать того, как быстро набирают обороты и внедряются такие решения по Ethereum-масштабированию, как Polygon, Arbitrum и Optimism. Эти технологии позволяют разработчикам создавать те же приложения, что и непосредственно на Ethereum, только с дополнительными преимуществами в виде более низкой стоимости газа и более высокой скорости транзакций.

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

Просмотреть окончательный исходный код данного проекта можно, открыв этот репозиторий.

Предварительные условия

Для успешного следования данному руководству необходимо подготовить следующее.

  • Фреймворк Node.js (версии 16.14.0 или более поздней), установленный на вашем компьютере. Рекомендую устанавливать Node с помощью nvm или fnm.
  • Расширение для кошелька Metamask, установленное в качестве расширения браузера.

Стек

Это руководство предполагает создание приложения с полным стеком.

  • Фреймворк для веб-приложений  —  Next.js.
  • Среда разработки Solidity — Hardhat.
  • Файловое хранилище — IPFS.
  • Библиотека веб-клиента Ethereum — Ethers.js.

Хотя это не входит в состав данного руководства, я покажу, как создать более надежный уровень API с использованием протокола The Graph. Это позволит обойти ограничения в шаблонах доступа к данным, предоставляемых нативным уровнем блокчейна.

О проекте

Проект, который мы будем создавать, называется Metaverse Marketplace. Он представляет собой NFT-маркетплейс.

-2

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

Когда пользователь покупает NFT, стоимость покупки переходит от покупателя к продавцу, а позиция переходит из маркетплейса к покупателю.

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

Логика работы маркетплейса будет состоять всего из одного смарт-контракта  —  NFT Marketplace Contract. Такой контракт позволяет пользователям майнить NFT и размещать их на торговой площадке.

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

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

О Polygon

Из документации:

“Polygon  —  это протокол и фреймворк для создания и соединения блокчейн-сетей, совместимых с Ethereum. Агрегирование масштабируемых решений на Ethereum с поддержкой многоцепочечной экосистемы Ethereum”.

Polygon примерно в 10 раз быстрее, чем Ethereum, и при этом транзакции на Polygon более чем в 10 раз дешевле.

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

Как уже упоминалось, существует множество других решений для Ethereum-масштабирования, таких как Arbitrum и Optimism, которые обладают похожим функционалом. Большинство этих решений по масштабированию имеют технические различия и относятся к различным категориям, таким как сайдчейн, второй уровень и каналы состояния.

Компания Polygon образовалась в результате ребрендинга Matic, поэтому слово Matic используется как взаимозаменяемое при упоминании о Polygon. Оно все еще служит названием для некоторых частей экосистемы, например используется в названиях ее токенов и сетей.

Чтобы узнать больше о Polygon, ознакомьтесь с их документацией здесь.

Теперь, после обзора проекта и связанных с ним технологий, приступим к разработке!

Настройка проекта

Чтобы начать работу, создадим новое приложение Next.js. Для этого откройте терминал. Создайте или перейдите в новый пустой каталог и выполните следующую команду:

npx create-next-app nft-marketplace

Затем перейдите в новый каталог и установите зависимости с помощью менеджера пакетов, например npm, yarn или pnpm:

cd nft-marketplace
npm install ethers hardhat @nomiclabs/hardhat-waffle \
ethereum-waffle chai @nomiclabs/hardhat-ethers \
web3modal @openzeppelin/contracts ipfs-http-client \
axios

Настройка Tailwind CSS

Для стилизации будем использовать Tailwind CSS. Настроим его на этом этапе.

Tailwind  —  это CSS-фреймворк на основе концепции utility-first, который позволяет легко добавлять стили и создавать красивые сайты без особых усилий.

Итак, установим зависимости Tailwind:

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

Далее создадим конфигурационные файлы, необходимые для работы Tailwind с Next.js (tailwind.config.js и postcss.config.js), выполнив следующую команду:

npx tailwindcss init -p

Затем настроим пути к шаблону content в файле tailwind.config.js:

/* tailwind.config.js */
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

Наконец, удалим код в styles/globals.css и обновим его следующим образом:

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

Настройка Hardhat

Далее инициализируйте новую среду разработки Hardhat из корня проекта:

npx hardhat

? What do you want to do? Create a basic sample project
? Hardhat project root: <Choose default path>
Если получите ошибку при ссылке на README.md, удалите README.md и запустите npx hardhat снова.

В корневом каталоге вы увидите следующие файлы и папки.

  • hardhat.config.js  —  файл, где содержится все настройки Hardhat, т.е. конфиг, плагины и настраиваемые задачи.
  • scripts  —  папка, содержащая скрипт под названием sample-script.js, который при выполнении развернет смарт-контракт.
  • test  —  папка, содержащая пример скрипта тестирования.
  • contracts  —  папка, содержащая пример смарт-контракта Solidity.

Теперь обновите конфигурацию в файле hardhat.config.js следующим образом:

/* hardhat.config.js */
require("@nomiclabs/hardhat-waffle")

module.exports = {
defaultNetwork: "hardhat",
networks: {
hardhat: {
chainId: 1337
},
// закомментирована неиспользуемая конфигурация
// mumbai: {
// url: "https://rpc-mumbai.maticvigil.com",
// accounts: [process.env.privateKey]
// }
},
solidity: {
version: "0.8.4",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
}
}
Смотрите код на GitHub здесь.

В этой конфигурации мы настроили локальную среду разработки Hardhat, а также тестовую сеть Mumbai (на данный момент закомментированную).

Подробнее об обеих сетях Matic можно прочитать здесь.

Смарт-контракт

Далее создадим смарт-контракт!

В этом файле я постараюсь максимально прокомментировать все, что происходит в коде.

Создайте новый файл в каталоге contracts под названием NFTMarketplace.sol. Добавьте в него следующий код:

// Идентификатор SPDX-лицензии: MIT
pragma solidity ^0.8.4;

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

import "hardhat/console.sol";

contract NFTMarketplace is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
Counters.Counter private _itemsSold;

uint256 listingPrice = 0.025 ether;
address payable owner;

mapping(uint256 => MarketItem) private idToMarketItem;

struct MarketItem {
uint256 tokenId;
address payable seller;
address payable owner;
uint256 price;
bool sold;
}

event MarketItemCreated (
uint256 indexed tokenId,
address seller,
address owner,
uint256 price,
bool sold
);

constructor() ERC721("Metaverse Tokens", "METT") {
owner = payable(msg.sender);
}

/* Обновление листинговой цены контракта */
function updateListingPrice(uint _listingPrice) public payable {
require(owner == msg.sender, "Only marketplace owner can update listing price.");
listingPrice = _listingPrice;
}

/* Возврат листинговой цены контракта */
function getListingPrice() public view returns (uint256) {
return listingPrice;
}

/* Чеканка токена и его листинг на маркетплейсе */
function createToken(string memory tokenURI, uint256 price) public payable returns (uint) {
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();

_mint(msg.sender, newTokenId);
_setTokenURI(newTokenId, tokenURI);
createMarketItem(newTokenId, price);
return newTokenId;
}

function createMarketItem(
uint256 tokenId,
uint256 price
) private {
require(price > 0, "Price must be at least 1 wei");
require(msg.value == listingPrice, "Price must be equal to listing price");

idToMarketItem[tokenId] = MarketItem(
tokenId,
payable(msg.sender),
payable(address(this)),
price,
false
);

_transfer(msg.sender, address(this), tokenId);
emit MarketItemCreated(
tokenId,
msg.sender,
address(this),
price,
false
);
}

/* Возможность перепродажи приобретенного токена */
function resellToken(uint256 tokenId, uint256 price) public payable {
require(idToMarketItem[tokenId].owner == msg.sender, "Only item owner can perform this operation");
require(msg.value == listingPrice, "Price must be equal to listing price");
idToMarketItem[tokenId].sold = false;
idToMarketItem[tokenId].price = price;
idToMarketItem[tokenId].seller = payable(msg.sender);
idToMarketItem[tokenId].owner = payable(address(this));
_itemsSold.decrement();

_transfer(msg.sender, address(this), tokenId);
}

/* Открытие продажи позиции на маркетплейсе */
/* Передача права собственности на позицию, а также денежных средств между сторонами */
function createMarketSale(
uint256 tokenId
) public payable {
uint price = idToMarketItem[tokenId].price;
address seller = idToMarketItem[tokenId].seller;
require(msg.value == price, "Please submit the asking price in order to complete the purchase");
idToMarketItem[tokenId].owner = payable(msg.sender);
idToMarketItem[tokenId].sold = true;
idToMarketItem[tokenId].seller = payable(address(0));
_itemsSold.increment();
_transfer(address(this), msg.sender, tokenId);
payable(owner).transfer(listingPrice);
payable(seller).transfer(msg.value);
}

/* Возврат всех непроданных позиций */
function fetchMarketItems() public view returns (MarketItem[] memory) {
uint itemCount = _tokenIds.current();
uint unsoldItemCount = _tokenIds.current() - _itemsSold.current();
uint currentIndex = 0;

MarketItem[] memory items = new MarketItem[](unsoldItemCount);
for (uint i = 0; i < itemCount; i++) {
if (idToMarketItem[i + 1].owner == address(this)) {
uint currentId = i + 1;
MarketItem storage currentItem = idToMarketItem[currentId];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}

/* Возврат только тех позиций, которые были приобретены пользователем */
function fetchMyNFTs() public view returns (MarketItem[] memory) {
uint totalItemCount = _tokenIds.current();
uint itemCount = 0;
uint currentIndex = 0;

for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].owner == msg.sender) {
itemCount += 1;
}
}

MarketItem[] memory items = new MarketItem[](itemCount);
for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].owner == msg.sender) {
uint currentId = i + 1;
MarketItem storage currentItem = idToMarketItem[currentId];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}

/* Возврат только тех позиций, который разместил пользователь */
function fetchItemsListed() public view returns (MarketItem[] memory) {
uint totalItemCount = _tokenIds.current();
uint itemCount = 0;
uint currentIndex = 0;

for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].seller == msg.sender) {
itemCount += 1;
}
}

MarketItem[] memory items = new MarketItem[](itemCount);
for (uint i = 0; i < totalItemCount; i++) {
if (idToMarketItem[i + 1].seller == msg.sender) {
uint currentId = i + 1;
MarketItem storage currentItem = idToMarketItem[currentId];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}
}
Смотрите код на GitHub здесь.

В этом контракте мы следуем ERC721-стандарту, реализованному OpenZeppelin.

Теперь, когда работа над кодом и средой смарт-контракта завершена, можно протестировать его.

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

Чтобы создать тест, откройте test/sample-test.js и обновите его следующим кодом:

/* test/sample-test.js */
describe("NFTMarket", function() {
it("Should create and execute market sales", async function() {
/* развертывание маркетплейса */
const NFTMarketplace = await ethers.getContractFactory("NFTMarketplace")
const nftMarketplace = await NFTMarketplace.deploy()
await nftMarketplace.deployed()

let listingPrice = await nftMarketplace.getListingPrice()
listingPrice = listingPrice.toString()

const auctionPrice = ethers.utils.parseUnits('1', 'ether')

/* создание двух токенов */
await nftMarketplace.createToken("https://www.mytokenlocation.com", auctionPrice, { value: listingPrice })
await nftMarketplace.createToken("https://www.mytokenlocation2.com", auctionPrice, { value: listingPrice })

const [_, buyerAddress] = await ethers.getSigners()

/* выполнение продажи токена другому пользователю */
await nftMarketplace.connect(buyerAddress).createMarketSale(1, { value: auctionPrice })

/* перепродажа токена */
await nftMarketplace.connect(buyerAddress).resellToken(1, auctionPrice, { value: listingPrice })

/* запрос и возврат непроданных позиций */
items = await nftMarketplace.fetchMarketItems()
items = await Promise.all(items.map(async i => {
const tokenUri = await nftMarketplace.tokenURI(i.tokenId)
let item = {
price: i.price.toString(),
tokenId: i.tokenId.toString(),
seller: i.seller,
owner: i.owner,
tokenUri
}
return item
}))
console.log('items: ', items)
})
})
Смотрите код на GitHub здесь.

Затем запустите тест из командной строки:

npx hardhat test

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

-3

Разработка фронтенда

Теперь, когда смарт-контракт работает и готов к использованию, можно приступить к созданию пользовательского интерфейса.

Первое, о чем стоит подумать,  —  настройка макета. Она позволит подключить навигацию, которая будет сохраняться на всех страницах.

Для этого откройте файл pages/_app.js и обновите его следующим кодом:

/* pages/_app.js */
import '../styles/globals.css'
import Link from 'next/link'

function MyApp({ Component, pageProps }) {
return (
<div>
<nav className="border-b p-6">
<p className="text-4xl font-bold">Metaverse Marketplace</p>
<div className="flex mt-4">
<Link href="/">
<a className="mr-4 text-pink-500">
Home
</a>
</Link>
<Link href="/create-nft">
<a className="mr-6 text-pink-500">
Sell NFT
</a>
</Link>
<Link href="/my-nfts">
<a className="mr-6 text-pink-500">
My NFTs
</a>
</Link>
<Link href="/dashboard">
<a className="mr-6 text-pink-500">
Dashboard
</a>
</Link>
</div>
</nav>
<Component {...pageProps} />
</div>
)
}

export default MyApp
Смотрите код на GitHub здесь.

Навигация содержит ссылки на домашний маршрут, а также страницу для продажи NFT, просмотра купленных вами NFT и дашборд для просмотра NFT, выставленных вами на продажу.

Запрос контракта для позиций на маркетплейсе

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

/* pages/index.js */
import { ethers } from 'ethers'
import { useEffect, useState } from 'react'
import axios from 'axios'
import Web3Modal from 'web3modal'

import {
marketplaceAddress
} from '../config'

import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'

export default function Home() {
const [nfts, setNfts] = useState([])
const [loadingState, setLoadingState] = useState('not-loaded')
useEffect(() => {
loadNFTs()
}, [])
async function loadNFTs() {
/* создание общего провайдера и запрос непроданных позиций */
const provider = new ethers.providers.JsonRpcProvider()
const contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, provider)
const data = await contract.fetchMarketItems()

/*
* отображение позиций, полученных из смарт-контракта, и их
форматирование,
* а также получение метаданных их токенов
*/
const items = await Promise.all(data.map(async i => {
const tokenUri = await contract.tokenURI(i.tokenId)
const meta = await axios.get(tokenUri)
let price = ethers.utils.formatUnits(i.price.toString(), 'ether')
let item = {
price,
tokenId: i.tokenId.toNumber(),
seller: i.seller,
owner: i.owner,
image: meta.data.image,
name: meta.data.name,
description: meta.data.description,
}
return item
}))
setNfts(items)
setLoadingState('loaded')
}
async function buyNft(nft) {
/* Необходимо, чтобы пользователь подписал транзакцию, поэтому будет использоваться Web3Provider для подписи */
const web3Modal = new Web3Modal()
const connection = await web3Modal.connect()
const provider = new ethers.providers.Web3Provider(connection)
const signer = provider.getSigner()
const contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)

/* Пользователю будет предложено оплатить запрос для завершения транзакции */
const price = ethers.utils.parseUnits(nft.price.toString(), 'ether')
const transaction = await contract.createMarketSale(nft.tokenId, {
value: price
})
await transaction.wait()
loadNFTs()
}
if (loadingState === 'loaded' && !nfts.length) return (<h1 className="px-20 py-10 text-3xl">No items in marketplace</h1>)
return (
<div className="flex justify-center">
<div className="px-4" style={{ maxWidth: '1600px' }}>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
{
nfts.map((nft, i) => (
<div key={i} className="border shadow rounded-xl overflow-hidden">
<img src={nft.image} />
<div className="p-4">
<p style={{ height: '64px' }} className="text-2xl font-semibold">{nft.name}</p>
<div style={{ height: '70px', overflow: 'hidden' }}>
<p className="text-gray-400">{nft.description}</p>
</div>
</div>
<div className="p-4 bg-black">
<p className="text-2xl font-bold text-white">{nft.price} ETH</p>
<button className="mt-4 w-full bg-pink-500 text-white font-bold py-2 px-12 rounded" onClick={() => buyNft(nft)}>Buy</button>
</div>
</div>
))
}
</div>
</div>
</div>
)
}
Смотрите код на GitHub здесь.

Когда страница загрузится, мы запросим смарт-контракт для всех непроданных NFT и выведем их на экран вместе с метаданными о позициях и кнопкой для их покупки.

Создание и листинг NFT

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

Эта страница позволит пользователю выполнять несколько функций:

  1. Загрузка и сохранение файлов в IPFS.
  2. Создание нового NFT.
  3. Настройка метаданных и цены позиции, а также выставление позиции на продажу на маркетплейсе.

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

Смотрите код на GitHub здесь.
/* pages/create-nft.js */
import { useState } from 'react'
import { ethers } from 'ethers'
import { create as ipfsHttpClient } from 'ipfs-http-client'
import { useRouter } from 'next/router'
import Web3Modal from 'web3modal'

const client = ipfsHttpClient('https://ipfs.infura.io:5001/api/v0')

import {
marketplaceAddress
} from '../config'

import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'

export default function CreateItem() {
const [fileUrl, setFileUrl] = useState(null)
const [formInput, updateFormInput] = useState({ price: '', name: '', description: '' })
const router = useRouter()

async function onChange(e) {
/* Загрузка изображения в IPFS */
const file = e.target.files[0]
try {
const added = await client.add(
file,
{
progress: (prog) => console.log(`received: ${prog}`)
}
)
const url = `https://ipfs.infura.io/ipfs/${added.path}`
setFileUrl(url)
} catch (error) {
console.log('Error uploading file: ', error)
}
}
async function uploadToIPFS() {
const { name, description, price } = formInput
if (!name || !description || !price || !fileUrl) return
/* Прежде всего, загрузка метаданных в IPFS */
const data = JSON.stringify({
name, description, image: fileUrl
})
try {
const added = await client.add(data)
const url = `https://ipfs.infura.io/ipfs/${added.path}`
/* После загрузки метаданных в IPFS следует вернуть URL для использования в транзакции */
return url
} catch (error) {
console.log('Error uploading file: ', error)
}
}

async function listNFTForSale() {
const url = await uploadToIPFS()
const web3Modal = new Web3Modal()
const connection = await web3Modal.connect()
const provider = new ethers.providers.Web3Provider(connection)
const signer = provider.getSigner()

/* Создание NFT */
const price = ethers.utils.parseUnits(formInput.price, 'ether')
let contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
let listingPrice = await contract.getListingPrice()
listingPrice = listingPrice.toString()
let transaction = await contract.createToken(url, price, { value: listingPrice })
await transaction.wait()

router.push('/')
}

return (
<div className="flex justify-center">
<div className="w-1/2 flex flex-col pb-12">
<input
placeholder="Asset Name"
className="mt-8 border rounded p-4"
onChange={e => updateFormInput({ ...formInput, name: e.target.value })}
/>
<textarea
placeholder="Asset Description"
className="mt-2 border rounded p-4"
onChange={e => updateFormInput({ ...formInput, description: e.target.value })}
/>
<input
placeholder="Asset Price in Eth"
className="mt-2 border rounded p-4"
onChange={e => updateFormInput({ ...formInput, price: e.target.value })}
/>
<input
type="file"
name="Asset"
className="my-4"
onChange={onChange}
/>
{
fileUrl && (
<img className="rounded mt-4" width="350" src={fileUrl} />
)
}
<button onClick={listNFTForSale} className="font-bold mt-4 bg-pink-500 text-white rounded p-4 shadow-lg">
Create NFT
</button>
</div>
</div>
)
}

Просмотр только тех NFT, что приобретены пользователем

В смарт-контракте NFTMarketplace.sol мы создали функцию fetchMyNFTs, которая возвращает только NFT, принадлежащие пользователю.

В pages/my-nfts.js будем использовать эту функцию для их получения и отображения.

Такая функциональность отличается от работы основной страницы запросов pages/index.js, поскольку нам нужно запросить у пользователя его адрес и использовать его в контракте, пользователь должен подписать транзакцию, чтобы функция получила ее соответствующим образом.

/* pages/my-nfts.js */
import { ethers } from 'ethers'
import { useEffect, useState } from 'react'
import axios from 'axios'
import Web3Modal from 'web3modal'
import { useRouter } from 'next/router'

import {
marketplaceAddress
} from '../config'

import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'

export default function MyAssets() {
const [nfts, setNfts] = useState([])
const [loadingState, setLoadingState] = useState('not-loaded')
const router = useRouter()
useEffect(() => {
loadNFTs()
}, [])
async function loadNFTs() {
const web3Modal = new Web3Modal({
network: "mainnet",
cacheProvider: true,
})
const connection = await web3Modal.connect()
const provider = new ethers.providers.Web3Provider(connection)
const signer = provider.getSigner()

const marketplaceContract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
const data = await marketplaceContract.fetchMyNFTs()

const items = await Promise.all(data.map(async i => {
const tokenURI = await marketplaceContract.tokenURI(i.tokenId)
const meta = await axios.get(tokenURI)
let price = ethers.utils.formatUnits(i.price.toString(), 'ether')
let item = {
price,
tokenId: i.tokenId.toNumber(),
seller: i.seller,
owner: i.owner,
image: meta.data.image,
tokenURI
}
return item
}))
setNfts(items)
setLoadingState('loaded')
}
function listNFT(nft) {
router.push(`/resell-nft?id=${nft.tokenId}&tokenURI=${nft.tokenURI}`)
}
if (loadingState === 'loaded' && !nfts.length) return (<h1 className="py-10 px-20 text-3xl">No NFTs owned</h1>)
return (
<div className="flex justify-center">
<div className="p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
{
nfts.map((nft, i) => (
<div key={i} className="border shadow rounded-xl overflow-hidden">
<img src={nft.image} className="rounded" />
<div className="p-4 bg-black">
<p className="text-2xl font-bold text-white">Price - {nft.price} Eth</p>
<button className="mt-4 w-full bg-pink-500 text-white font-bold py-2 px-12 rounded" onClick={() => listNFT(nft)}>List</button>
</div>
</div>
))
}
</div>
</div>
</div>
)
}
Смотрите код на GitHub здесь.

Дашборд

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

Эта страница будет использовать функцию fetchItemsListed из смарт-контракта NFTMarketplace.sol, которая возвращает только те позиции, которые соответствуют адресу пользователя, выполняющего вызов функции.

Создайте новый файл dashboard.js в каталоге pages со следующим кодом:

/* pages/dashboard.js */
import { ethers } from 'ethers'
import { useEffect, useState } from 'react'
import axios from 'axios'
import Web3Modal from 'web3modal'

import {
marketplaceAddress
} from '../config'

import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'

export default function CreatorDashboard() {
const [nfts, setNfts] = useState([])
const [loadingState, setLoadingState] = useState('not-loaded')
useEffect(() => {
loadNFTs()
}, [])
async function loadNFTs() {
const web3Modal = new Web3Modal({
network: 'mainnet',
cacheProvider: true,
})
const connection = await web3Modal.connect()
const provider = new ethers.providers.Web3Provider(connection)
const signer = provider.getSigner()

const contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
const data = await contract.fetchItemsListed()

const items = await Promise.all(data.map(async i => {
const tokenUri = await contract.tokenURI(i.tokenId)
const meta = await axios.get(tokenUri)
let price = ethers.utils.formatUnits(i.price.toString(), 'ether')
let item = {
price,
tokenId: i.tokenId.toNumber(),
seller: i.seller,
owner: i.owner,
image: meta.data.image,
}
return item
}))

setNfts(items)
setLoadingState('loaded')
}
if (loadingState === 'loaded' && !nfts.length) return (<h1 className="py-10 px-20 text-3xl">No NFTs listed</h1>)
return (
<div>
<div className="p-4">
<h2 className="text-2xl py-2">Items Listed</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 pt-4">
{
nfts.map((nft, i) => (
<div key={i} className="border shadow rounded-xl overflow-hidden">
<img src={nft.image} className="rounded" />
<div className="p-4 bg-black">
<p className="text-2xl font-bold text-white">Price - {nft.price} Eth</p>
</div>
</div>
))
}
</div>
</div>
</div>
)
}
Смотрите код на GitHub здесь.

Перепродажа токена

Последняя страница, которую мы создадим, позволит пользователю перепродать NFT, приобретенный у кого-то другого.

Эта страница будет использовать функцию resellToken из смарт-контракта NFTMarketplace.sol.

/* pages/resell-nft.js */
import { useEffect, useState } from 'react'
import { ethers } from 'ethers'
import { useRouter } from 'next/router'
import axios from 'axios'
import Web3Modal from 'web3modal'

import {
marketplaceAddress
} from '../config'

import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'

export default function ResellNFT() {
const [formInput, updateFormInput] = useState({ price: '', image: '' })
const router = useRouter()
const { id, tokenURI } = router.query
const { image, price } = formInput

useEffect(() => {
fetchNFT()
}, [id])

async function fetchNFT() {
if (!tokenURI) return
const meta = await axios.get(tokenURI)
updateFormInput(state => ({ ...state, image: meta.data.image }))
}

async function listNFTForSale() {
if (!price) return
const web3Modal = new Web3Modal()
const connection = await web3Modal.connect()
const provider = new ethers.providers.Web3Provider(connection)
const signer = provider.getSigner()

const priceFormatted = ethers.utils.parseUnits(formInput.price, 'ether')
let contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
let listingPrice = await contract.getListingPrice()

listingPrice = listingPrice.toString()
let transaction = await contract.resellToken(id, priceFormatted, { value: listingPrice })
await transaction.wait()

router.push('/')
}

return (
<div className="flex justify-center">
<div className="w-1/2 flex flex-col pb-12">
<input
placeholder="Asset Price in Eth"
className="mt-2 border rounded p-4"
onChange={e => updateFormInput({ ...formInput, price: e.target.value })}
/>
{
image && (
<img className="rounded mt-4" width="350" src={image} />
)
}
<button onClick={listNFTForSale} className="font-bold mt-4 bg-pink-500 text-white rounded p-4 shadow-lg">
List NFT
</button>
</div>
</div>
)
}
Смотрите код на GitHub здесь.

Запуск проекта

Чтобы запустить проект, нам понадобится скрипт развертывания для разворачивания смарт-контрактов в блокчейн-сети.

Развертывание контрактов в локальной сети

При создании проекта Hardhat сгенерировал пример скрипта развертывания в scripts/sample-script.js.

Чтобы четче обозначить назначение этого скрипта, измените название scripts/sample-script.js на scripts/deploy.js.

Затем обновите scripts/deploy.js следующим кодом:

const hre = require("hardhat");
const fs = require('fs');

async function main() {
const NFTMarketplace = await hre.ethers.getContractFactory("NFTMarketplace");
const nftMarketplace = await NFTMarketplace.deploy();
await nftMarketplace.deployed();
console.log("nftMarketplace deployed to:", nftMarketplace.address);

fs.writeFileSync('./config.js', `
export const marketplaceAddress = "${nftMarketplace.address}"
`)
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

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

Сначала протестируем его в локальной сети, а затем развернем в тестовой сети Mumbai.

Чтобы развернуть локальную сеть, откройте терминал и выполните следующую команду:

npx hardhat node

Создастся локальная сеть с 20 аккаунтами.

-4

Теперь, сохраняя узел запущенным, откройте отдельное окно терминала для развертывания контракта.

В отдельном окне выполните следующую команду:

npx hardhat run scripts/deploy.js --network localhost

Когда разворачивание будет завершено, CLI выведет адрес развернутого контракта:

-5

Вы также должны увидеть файл config.js, заполненный адресом этого смарт-контракта.

Импорт аккаунтов в MetaMask

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

Каждый из этих аккаунтов пополнен на 10000 ETH.

Чтобы импортировать один из этих аккаунтов, переключите сначала сеть вашего кошелька MetaMask на Localhost 8545.

-6

Затем в MetaMask нажмите на Import Account в меню аккаунтов:

-7

Скопируйте и вставьте один из приватных ключей (Private Keys), выведенных CLI, и нажмите Import. После импорта аккаунта вы должны увидеть в нем определенное количество Eth:

-8

Рекомендую проделать эту процедуру с 2 или 3 аккаунтами, чтобы протестировать различные функции взаимодействия между пользователями.

Запуск приложения

Теперь наконец можно протестировать приложение!

Чтобы запустить его, выполните следующую команду в CLI:

npm run dev

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

Развертывание в Polygon

Теперь, когда проект запущен и протестирован локально, развернем его в Polygon. Начнем с развертывания в Mumbai, тестовой сети Polygon.

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

Чтобы получить приватный ключ, можете использовать один из приватных ключей, предоставленных Hardhat, или экспортировать их непосредственно из MetaMask.

-9

Если вы работаете на Mac, можете установить переменную среды из командной строки следующим образом (не забудьте запустить скрипт развертывания из этого же терминала и сессии):

export privateKey="your-private-key"

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

Конфигурирование сети

Теперь нам нужно переключиться с локальной тестовой сети на тестовую сеть Mumbai.

Для этого следует создать и настроить конфигурацию сети.

Сначала откройте MetaMask и нажмите на Settings (Настройки).

-10

Далее нажмите на Networks (Сети), а затем Add Network (Добавить сеть):

-11

На этом этапе добавьте конфигурации для тестовой сети Mumbai:

  • Network Name (имя сети): Mumbai TestNet.
  • New RPC URL (новый URL RPC): https://rpc-mumbai.maticvigil.com.
  • Chain ID (идентификатор цепи): 80001.
  • Currency Symbol (символ валюты): Matic.

Сохраните эти настройки, после чего сможете переключиться на новую сеть и использовать ее!

Наконец, для взаимодействия с приложениями вам понадобятся токены тестовой сети Matic.

Чтобы получить их, можете посетить “кран” этой сети Matic Faucet и ввести адреса кошельков, с которых вы хотели бы запросить токены.

Развертывание в сети Matic/Polygon

Теперь, когда у вас есть токены Matic, вы можете провести развертывание в сети Polygon!

Для начала убедитесь, что на адрес, связанный с приватным ключом, с помощью которого вы развертываете свой контракт, поступили токены Matic для оплаты газовых сборов за транзакцию.

Также не забудьте раскомментировать конфигурацию mumbai в hardhat.config.js:

mumbai: {
url: "https://rpc-mumbai.maticvigil.com",
accounts: [process.env.privateKey]
}

Для развертывания в Matic выполните следующую команду:

npx hardhat run scripts/deploy.js --network mumbai

Если у вас возникла ошибка при развертывании, возможно, причина в перегрузке публичного RPC (сервиса вызова удаленных процедур). В производственной среде рекомендуется использовать провайдер RPC, например Infura, Alchemy, Quicknode и Figment DataHub.

После развертывания контрактов обновите вызов функции loadNFTs в pages/index.js, чтобы включить новую конечную точку RPC:

/* pages/index.js */

/* старый провайдер */
const provider = new ethers.providers.JsonRpcProvider()

/* новый провайдер */
const provider = new ethers.providers.JsonRpcProvider("https://rpc-mumbai.maticvigil.com")

Теперь вы можете обновить адреса контрактов в вашем проекте и провести тесты в новой сети!

npm run dev

Если вы столкнулись с ошибкой, причина может заключаться в неверном адресе контракта, выводимого в консоль Hardhat, из-за бага, с которым я недавно столкнулся. Здесь можно найти правильные адреса контрактов. Нужно вставить адрес, с которого были развернуты контракты, чтобы увидеть последние транзакции и получить адреса контрактов из данных транзакций.

Развертывание в Mainnet

Для развертывания в основной сети Matic/Polygon можете использовать те же шаги, которые выполняли для тестовой сети Mumbai.

Основное отличие заключается в том, что вам нужно будет применить конечную точку для Matic, а также импортировать сеть в ваш кошелек MetaMask, как указано здесь.

Вот пример обновления, которое требуется для этого в вашем проекте:

/* hardhat.config.js */

/* Добавление конфигурации основной сети Matic к существующей конфигурации */
...
matic: {
url: "https://rpc-mainnet.maticvigil.com",
accounts: [privateKey]
}
...

Публичные RPC, подобные приведенному выше, могут иметь ограничения в плане трафика или скорости в зависимости от конкретного случая. Вы можете подписаться на бесплатное использование URL RPC посредством таких сервисов, как Infura, MaticVigil, QuickNode, Alchemy, Chainstack и Ankr.

Пример с Infura:

url: `https://polygon-mainnet.infura.io/v3/${infuraId}`
Чтобы просмотреть окончательный исходный код проекта, посетите этот репозиторий.

Следующие шаги

Поздравляю! Вы развернули нетривиальное приложение в Polygon.

Самое приятное в работе с такими решениями, как Polygon,  —  это то, что не нужно прикладывать дополнительные усилия или изучать что-то новое, чего нельзя сказать о разработке непосредственно в системе Ethereum. Почти все API и инструменты на 2-м уровне и сайдчейнах остаются неизменными, что позволяет применять те же навыки на различных платформах, таких как Polygon.

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

Читайте также:

Читайте нас в Telegram, VK

Перевод статьи Edge and Node — DEV Community: How to Build a Full Stack NFT Marketplace — V2 (2022)