Создание NFT marketplace DApp, подобного Opensea, с помощью Solidity и Javascript/React может стать важным шагом на вашем пути web3-разработки. Давайте приступим.
В моих предыдущих уроках вы, возможно, узнали:
-
как написать смарт-контракт и запустить его на локальном блокчейн-тестнете с помощью hardhat.
-
как создать полностековый DApp, что означает работу со всеми тремя частями DApp: смарт-контрактом, веб-приложением и кошельком пользователя.
-
как провести модульное тестирование, развернуть в публичной тестовой сети, проверить смарт-контракт в блокчейн-проводнике Etherscan.
Теперь вы можете начать писать смарт-контракт с полным функционалом: торговая площадка для цифровых товаров. Цифровыми товарами, торгуемыми здесь, являются предметы коллекции NFT.
Таблица содержания
- Задача 1: Что мы создаем и настройка проекта
- Задача 2: Смарт-контракт коллекции NFT
- Задача 3: Веб-страница для отображения элементов NFT
- Задача 4: Смарт-контракт NFT marketplace
- Задача 5: Webapp для NFTMarketplace
- Задача 6: Развертывание на Polygon и запрос с помощью API Alchemy NFT.
Надер Дабит написал две версии книги How to Build a Full Stack NFT Marketplace — V2 (2022). Вдохновленный его идеей и основываясь на его кодовой базе смарт-контрактов, я написал больше кода и написал это руководство для вас.
Возможно, вы уже читали мои предыдущие руководства. Если нет, советую прочитать следующие два, так как я не буду объяснять некоторые тактики, которые уже есть.
-
Web3 Tutorial: создание DApp с помощью Hardhat, React и Ethers.js https://dev.to/yakult/a-tutorial-build-dapp-with-hardhat-react-and-ethersjs-1gmi
-
Web3 Tutorial: build DApp with Web3-React and SWR https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0
Давайте начнем строить.
Задача 1: Что мы собираем и настройка проекта
Задача 1.1: Что мы создаем — три части
-
Смарт-контракт для сбора NFT и простая веб-страница для отображения элемента NFT. Мы будем использовать SVG на цепочке в качестве изображения элемента NFT. Нам нужен этот образец коллекции NFT для работы с контрактом торговой площадки, а также для витрины магазина.
-
Смарт-контракт торговой площадки NFT, в котором пользователь может разместить в списке товар NFT и купить товар NFT. Продавец также может удалить свой собственный NFT с рынка. Этот контракт рынка также предоставляет функции запроса для webapp для запроса данных рынка. Мы постараемся как можно больше описать этот смарт-контракт с помощью модульных тестов.
-
Витрина рынка NFT, использующая React/Web3-React/SWR. (Для упрощения мы создадим только необходимые компоненты витрины в одностраничном webapp. Например, мы не будем предоставлять в webapp компоненты пользовательского интерфейса для продавцов, чтобы они могли выставлять НМТ на продажу. )
Ключевой частью этого проекта является смарт-контракт торговой площадки (NFTMarketplace
) с хранилищем данных, основными функциями и функциями запросов.
Основные функции:
function createMarketItem(address nftContract,uint256 tokenId,uint256 price) payable
function deleteMarketItem(uint256 itemId) public
function createMarketSale(address nftContract,uint256 id) public payable
Функции запросов:
function fetchActiveItems() public view returns (MarketItem[] memory)
function fetchMyPurchasedItems() public view returns (MarketItem[] memory)
function fetchMyCreatedItems() public view returns (MarketItem[] memory)
Продавец может использовать смарт-контракт, чтобы:
- утвердить контракт NFT на продажу
- создать рыночный товар с платой за листинг
- …(ожидая покупателя, который купит НМТ)…
- получить стоимость, которую заплатил покупатель
Когда покупатель совершает покупку на рынке, рыночный контракт облегчает процесс покупки:
- покупатель приобретает НМТ, заплатив стоимость цены
- рыночный контракт завершает процесс покупки:
- передает продавцу стоимость цены
- передача НМТ от продавца к покупателю
- перечисление платы за листинг владельцу рынка
- изменить состояние рыночного товара с
Created
наRelease
.
GitHub repos этого руководства:
- смарт-контракты (проект hardhat): https://github.com/fjun99/nftmarketplace
- веб-приложение с использованием React: https://github.com/fjun99/web3app-tutrial-using-web3react (ветка nftmarket).
Хотя я многому научился из учебника по NFT marketplace от Dabit, есть 3 основных отличия между тем, что мы будем создавать, и его учебником:
-
NFT Дабита — это традиционный NFT, который хранит изображения на IPFS, в то время как наш NFT хранит SVG изображения на цепочке (только данные, не изображение). Мы используем этот вариант, чтобы сделать наш учебник простым, так как нам не нужно настраивать сервер для предоставления NFT tokenURI (restful json api) и иметь дело с хранением изображений на сервере или IPFS.
-
В первой версии учебника Дабит разделил смарт-контракт токена NFT ERC721 и смарт-контракт рынка. Во второй версии он решил построить смарт-контракт NFT ERC721 с функциональностью маркетплейса в одном смарт-контракте. Мы решили разделить их здесь, поскольку хотим построить NFT-маркетплейс общего назначения.
-
В учебнике Dabit, когда продавец выставляет товар NFT на marketplace, он передает товар NFT в рыночный контракт и ждет, пока он будет продан. Мне, как пользователю блокчейна и web3.0, такая схема не нравится. Я бы хотел, чтобы в рыночный контракт утверждался только товар NFT. И пока он не продан, товар все еще находится в моем кошельке. (Я также не хочу использовать
setApprovalForAll()
, чтобы одобрить все NFT предметы в этой коллекции в моем адресе для рыночного контракта. Мы предпочитаем утверждать НМТ поштучно).
Задание 1.2: Настройка каталога и проекта
ШАГ 1: Создайте каталоги
Мы разделим наш проект на два подкаталога, chain
для проекта hardhat, и webapp
для проекта React/Next.js.
--nftmarket
--chain
--webapp
ШАГ 2: Проект Hardhat
В подкаталоге chain
установите среду разработки hardhat
и библиотеку Solidity @openzeppelin/contracts
. Затем инициализируем пустой проект hardhat.
yarn init -y
yarn add hardhat
yarn add @openzeppelin/contracts
yarn hardhat
В качестве альтернативы вы можете загрузить стартовый проект цепочки hardhat из репозитория github. В директории nftmarket
запустите:
git clone git@github.com:fjun99/chain-tutorial-hardhat-starter.git chain
ШАГ 3: Проект вебаппа на React/Next.js
Вы можете загрузить пустой webapp scaffold:
git clone https://github.com/fjun99/webapp-tutorial-scaffold.git webapp
Вы также можете загрузить кодовую базу webapp этого учебника:
git clone git@github.com:fjun99/web3app-tutrial-using-web3react.git webapp
cd webapp
git checkout nftmarket
Задание 2: Смарт-контракт NFT по сбору средств
Задача 2.1: написать смарт-контракт NFT
Мы пишем смарт-контракт NFT ERC721, наследуя реализацию OpenZeppelin ERC721. Здесь мы добавляем три функции:
- tokenId: автоматический инкремент tokenId, начиная с 1
- функция
mintTo(адрес _to)
: каждый может вызвать ее, чтобы майнить NFT - функция
tokenURI()
для реализации URI токена и SVG изображений на цепочке
// contracts/BadgeToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
contract BadgeToken is ERC721 {
uint256 private _currentTokenId = 0; //tokenId will start from 1
constructor(
string memory _name,
string memory _symbol
) ERC721(_name, _symbol) {
}
/**
* @dev Mints a token to an address with a tokenURI.
* @param _to address of the future owner of the token
*/
function mintTo(address _to) public {
uint256 newTokenId = _getNextTokenId();
_mint(_to, newTokenId);
_incrementTokenId();
}
/**
* @dev calculates the next token ID based on value of _currentTokenId
* @return uint256 for the next token ID
*/
function _getNextTokenId() private view returns (uint256) {
return _currentTokenId+1;
}
/**
* @dev increments the value of _currentTokenId
*/
function _incrementTokenId() private {
_currentTokenId++;
}
/**
* @dev return tokenURI, image SVG data in it.
*/
function tokenURI(uint256 tokenId) override public pure returns (string memory) {
string[3] memory parts;
parts[0] = "<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'><style>.base { fill: white; font-family: serif; font-size: 300px; }</style><rect width='100%' height='100%' fill='brown' /><text x='100' y='260' class='base'>";
parts[1] = Strings.toString(tokenId);
parts[2] = "</text></svg>";
string memory json = Base64.encode(bytes(string(abi.encodePacked(
"{"name":"Badge #",
Strings.toString(tokenId),
"","description":"Badge NFT with on-chain SVG image.",",
""image": "data:image/svg+xml;base64,",
// Base64.encode(bytes(output)),
Base64.encode(bytes(abi.encodePacked(parts[0], parts[1], parts[2]))),
""}"
))));
return string(abi.encodePacked("data:application/json;base64,", json));
}
}
Мы также добавим скрипт развертывания scripts/deploy_BadgeToken.ts
, который развернет этот контракт NFT с именем:BadgeToken
и символом:BADGE
:
const token = await BadgeToken.deploy('BadgeToken','BADGE')
Задача 2.2: Понять tokenURI()
Объясним реализацию функции ERC721 tokenURI()
.
tokenURI()
— это функция метаданных для стандарта ERC721. Документация OpenZeppelin :
tokenURI(uint256 tokenId) → строка
Возвращает унифицированный идентификатор ресурса (URI) для токена tokenId.
Обычно tokenURI()
возвращает URI. Вы можете получить результирующий URI для каждого токена, соединив baseURI и tokenId.
В нашей tokenURI()
мы возвращаем URI в виде объекта с кодировкой base64:
Сначала мы создаем объект. Изображение svg в объекте также кодируется base64.
{
"name":"Badge #1",
"description":"Badge NFT with on-chain SVG image."
"image":"data:image/svg+xml;base64,[svg base64 encoded]"
}
Затем мы возвращаем объект в кодировке base64.
data:application/json;base64,(object base64 encoded)
Webapp может получить URI, вызвав tokenURI(tokenId)
, и декодировать его, чтобы получить название, описание и SVG-изображение.
SVG-изображение адаптировано из проекта LOOT. Оно очень простое. Оно отображает идентификатор токена на изображении.
<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'>
<style>.base { fill: white; font-family: serif; font-size: 300px; }</style>
<rect width='100%' height='100%' fill='brown' />
<text x='100' y='260' class='base'>
1
</text>
</svg>
Задача 2.3: Юнит-тест для контракта ERC721
Давайте напишем скрипт модульного тестирования для этого контракта:
// test/BadgeToken.test.ts
import { expect } from "chai"
import { Signer } from "ethers"
import { ethers } from "hardhat"
import { BadgeToken } from "../typechain"
const base64 = require( "base-64")
const _name='BadgeToken'
const _symbol='BADGE'
describe("BadgeToken", function () {
let badge:BadgeToken
let account0:Signer,account1:Signer
beforeEach(async function () {
[account0, account1] = await ethers.getSigners()
const BadgeToken = await ethers.getContractFactory("BadgeToken")
badge = await BadgeToken.deploy(_name,_symbol)
})
it("Should have the correct name and symbol ", async function () {
expect(await badge.name()).to.equal(_name)
expect(await badge.symbol()).to.equal(_symbol)
})
it("Should tokenId start from 1 and auto increment", async function () {
const address1=await account1.getAddress()
await badge.mintTo(address1)
expect(await badge.ownerOf(1)).to.equal(address1)
await badge.mintTo(address1)
expect(await badge.ownerOf(2)).to.equal(address1)
expect(await badge.balanceOf(address1)).to.equal(2)
})
it("Should mint a token with event", async function () {
const address1=await account1.getAddress()
await expect(badge.mintTo(address1))
.to.emit(badge, 'Transfer')
.withArgs(ethers.constants.AddressZero,address1, 1)
})
it("Should mint a token with desired tokenURI (log result for inspection)", async function () {
const address1=await account1.getAddress()
await badge.mintTo(address1)
const tokenUri = await badge.tokenURI(1)
// console.log("tokenURI:")
// console.log(tokenUri)
const tokenId = 1
const data = base64.decode(tokenUri.slice(29))
const itemInfo = JSON.parse(data)
expect(itemInfo.name).to.be.equal('Badge #'+String(tokenId))
expect(itemInfo.description).to.be.equal('Badge NFT with on-chain SVG image.')
const svg = base64.decode(itemInfo.image.slice(26))
const idInSVG = svg.slice(256,-13)
expect(idInSVG).to.be.equal(String(tokenId))
// console.log("SVG image:")
// console.log(svg)
})
it("Should mint 10 token with desired tokenURI", async function () {
const address1=await account1.getAddress()
for(let i=1;i<=10;i++){
await badge.mintTo(address1)
const tokenUri = await badge.tokenURI(i)
const data = base64.decode(tokenUri.slice(29))
const itemInfo = JSON.parse(data)
expect(itemInfo.name).to.be.equal('Badge #'+String(i))
expect(itemInfo.description).to.be.equal('Badge NFT with on-chain SVG image.')
const svg = base64.decode(itemInfo.image.slice(26))
const idInSVG = svg.slice(256,-13)
expect(idInSVG).to.be.equal(String(i))
}
expect(await badge.balanceOf(address1)).to.equal(10)
})
})
Запустите модульный тест:
yarn hardhat test test/BadgeToken.test.ts
Результаты:
BadgeToken
✓ Should have the correct name and symbol
✓ Should tokenId start from 1 and auto increment
✓ Should mint a token with event
✓ Should mint a token with desired tokenURI (log result for inspection) (62ms)
✓ Should mint 10 token with desired tokenURI (346ms)
5 passing (1s)
Мы также можем распечатать tokenURI, полученный в модульном тесте, для проверки:
tokenURI:
data:application/json;base64,eyJuYW1lIjoiQmFkZ2UgIzEiLCJkZXNjcmlwdGlvbiI6IkJhZGdlIE5GVCB3aXRoIG9uLWNoYWluIFNWRyBpbWFnZS4iLCJpbWFnZSI6ICJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MG5hSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY25JSEJ5WlhObGNuWmxRWE53WldOMFVtRjBhVzg5SjNoTmFXNVpUV2x1SUcxbFpYUW5JSFpwWlhkQ2IzZzlKekFnTUNBek5UQWdNelV3Sno0OGMzUjViR1UrTG1KaGMyVWdleUJtYVd4c09pQjNhR2wwWlRzZ1ptOXVkQzFtWVcxcGJIazZJSE5sY21sbU95Qm1iMjUwTFhOcGVtVTZJRE13TUhCNE95QjlQQzl6ZEhsc1pUNDhjbVZqZENCM2FXUjBhRDBuTVRBd0pTY2dhR1ZwWjJoMFBTY3hNREFsSnlCbWFXeHNQU2RpY205M2JpY2dMejQ4ZEdWNGRDQjRQU2N4TURBbklIazlKekkyTUNjZ1kyeGhjM005SjJKaGMyVW5QakU4TDNSbGVIUStQQzl6ZG1jKyJ9
SVG image:
<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'><style>.base { fill: white; font-family: serif; font-size: 300px; }</style><rect width='100%' height='100%' fill='brown' /><text x='100' y='260' class='base'>1</text></svg>
Задача 3: Веб-страница для отображения элемента NFT
Задача 3.1: Настройте проект webapp, используя Web3-React
& Chakra UI
.
Мы будем использовать web3 connection framework Web3-React
для выполнения нашей работы. Стеком веб-приложений являются:
- React
- Next.js
- Chakra UI
- Web3-React
- Ethers.js
- SWR
_app.tsx
:
// src/pages/_app.tsx
import { ChakraProvider } from '@chakra-ui/react'
import type { AppProps } from 'next/app'
import { Layout } from 'components/layout'
import { Web3ReactProvider } from '@web3-react/core'
import { Web3Provider } from '@ethersproject/providers'
function getLibrary(provider: any): Web3Provider {
const library = new Web3Provider(provider)
return library
}
function MyApp({ Component, pageProps }: AppProps) {
return (
<Web3ReactProvider getLibrary={getLibrary}>
<ChakraProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</ChakraProvider>
</Web3ReactProvider>
)
}
export default MyApp
Мы будем использовать компонент ConnectMetamask
в нашем предыдущем уроке: Tutorial: build DApp with Web3-React and SWRTutorial: build DApp with Web3-React and SWR.
Задание 3.2: Напишите компонент для отображения времени NFT
В этом компоненте мы также используем SWR
, как и в предыдущем уроке. Фетчер SWR
находится в utils/fetcher.tsx
.
// components/CardERC721.tsx
import React, { useEffect,useState } from 'react';
import { useWeb3React } from '@web3-react/core'
import { Web3Provider } from '@ethersproject/providers'
import { Box, Text} from '@chakra-ui/react'
import useSWR from 'swr'
import { ERC721ABI as abi} from "abi/ERC721ABI"
import { BigNumber } from 'ethers'
import { fetcher } from 'utils/fetcher'
const base64 = require( "base-64")
interface Props {
addressContract: string,
tokenId:BigNumber
}
interface ItemInfo{
name:string,
description:string,
svg:string
}
export default function CardERC721(props:Props){
const addressContract = props.addressContract
const { account, active, library } = useWeb3React<Web3Provider>()
const [itemInfo, setItemInfo] = useState<ItemInfo>()
const { data: nftURI } = useSWR([addressContract, 'tokenURI', props.tokenId], {
fetcher: fetcher(library, abi),
})
useEffect( () => {
if(!nftURI) return
const data = base64.decode(nftURI.slice(29))
const itemInfo = JSON.parse(data)
const svg = base64.decode(itemInfo.image.slice(26))
setItemInfo({
"name":itemInfo.name,
"description":itemInfo.description,
"svg":svg})
},[nftURI])
return (
<Box my={2} bg='gray.100' borderRadius='md' width={220} height={260} px={3} py={4}>
{itemInfo
?<Box>
<img src={`data:image/svg+xml;utf8,${itemInfo.svg}`} alt={itemInfo.name} width= '200px' />
<Text fontSize='xl' px={2} py={2}>{itemInfo.name}</Text>
</Box>
:<Box />
}
</Box>
)
}
Некоторые пояснения:
- При подключении к кошельку MetaMask этот компонент запрашивает tokenURI(tokenId), чтобы получить название, описание и svg изображение элемента NFT.
Давайте напишем страницу для отображения элемента NFT.
// src/pages/samplenft.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import { VStack, Heading } from "@chakra-ui/layout"
import ConnectMetamask from 'components/ConnectMetamask'
import CardERC721 from 'components/CardERC721'
import { BigNumber } from 'ethers'
const nftAddress = '0x5fbdb2315678afecb367f032d93f642f64180aa3'
const tokenId = BigNumber.from(1)
const SampleNFTPage: NextPage = () => {
return (
<>
<Head>
<title>My DAPP</title>
</Head>
<Heading as="h3" my={4}>NFT Marketplace</Heading>
<ConnectMetamask />
<VStack>
<CardERC721 addressContract={nftAddress} tokenId={tokenId} ></CardERC721>
</VStack>
</>
)
}
export default SampleNFTPage
Задание 3.3: Запустить проект webapp
ШАГ 1: Запустите автономный локальный тестнет
В другом терминале запустите в директории chain/
:
yarn hardhat node
ШАГ 2: Разверните BadgeToken (ERC721) в локальной тестовой сети
yarn hardhat run scripts/deploy_BadgeToken.ts --network localhost
Результат:
Deploying BadgeToken ERC721 token...
BadgeToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
ШАГ 3: Создайте BadgeToken (tokenId = 1) в консоли hardhat
Запустите консоль hardhat connect to local testenet
yarn hardhat console --network localhost
В консоли:
nftaddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
nft = await ethers.getContractAt("BadgeToken", nftaddress)
await nft.name()
//'BadgeToken'
await nft.mintTo('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266')
// tx response ...
await nft.tokenURI(1)
//'data:application/json;base64,eyJuYW1lIjoiQmFkZ2UgIzEiLCJk...'
Теперь у нас есть пункт NFT. Мы отобразим его на веб-странице.
ШАГ 3: подготовьте вашу MetaMask
Убедитесь, что в вашей MetaMask есть локальный testnet wich RPC URL http://localhost:8545
и chain id 31337
.
ШАГ 4: запустите webapp
В webapp/
запустите:
yarn dev
В браузере chrome перейдите на страницу: http://localhost:3000/samplenft
.
Подключите MetaMask, элемент NFT будет отображен на странице. (Обратите внимание, что изображение загружается медленно. Дождитесь завершения загрузки.
Мы видим, что NFT «Badge #1» с tokenId 1
отображается правильно.
Задача 4: Смарт-контракт NFT marketplace
Задача 4.1: Структура данных контракта
Мы адаптировали смарт-контракт Market.sol
из учебника Надера Дабита (V1) для написания нашего маркетплейса. Большое спасибо. Но вы должны заметить, что мы вносим много изменений в этот контракт.
Мы определяем struct MarketItem
:
struct MarketItem {
uint id;
address nftContract;
uint256 tokenId;
address payable seller;
address payable buyer;
uint256 price;
State state;
}
Каждый элемент рынка может находиться в одном из трех состояний:
enum State { Created, Release, Inactive }
Обратите внимание, что мы не можем полагаться на состояние Created
. Если продавец передаст предмет NFT другим или продавец снимет одобрение с предмета NFT, состояние все еще будет Created
, указывая, что другие могут покупать на рынке. На самом деле другие не могут его купить.
Все предметы хранятся в mapping
:
mapping(uint256 => MarketItem) private marketItems;
У рынка есть владелец, который является организатором контракта. Плата за листинг будет поступать владельцу рынка, когда на рынке будет продан выставленный на продажу предмет NFT.
В будущем мы можем добавить функциональные возможности для передачи прав собственности на другой адрес или кошелек с мульти-сигмой. Чтобы сделать учебник простым, мы пропустим эти функции.
Этот рынок имеет статическую плату за листинг:
uint256 public listingFee = 0.025 ether;
function getListingFee() public view returns (uint256)
Задание 4.2: методы работы с рынком
Рынок имеет две категории методов:
Основные функции:
function createMarketItem(address nftContract,uint256 tokenId,uint256 price) payable
function deleteMarketItem(uint256 itemId) public
function createMarketSale(address nftContract,uint256 id) public payable
Функции запроса:
function fetchActiveItems() public view returns (MarketItem[] memory)
function fetchMyPurchasedItems() public view returns (MarketItem[] memory)
function fetchMyCreatedItems() public view returns (MarketItem[] memory)
Полный смарт-контракт выглядит следующим образом:
// contracts/NFTMarketplace.sol
// SPDX-License-Identifier: MIT OR Apache-2.0
//
// adapt and edit from (Nader Dabit):
// https://github.com/dabit3/polygon-ethereum-nextjs-marketplace/blob/main/contracts/Market.sol
pragma solidity ^0.8.3;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "hardhat/console.sol";
contract NFTMarketplace is ReentrancyGuard {
using Counters for Counters.Counter;
Counters.Counter private _itemCounter;//start from 1
Counters.Counter private _itemSoldCounter;
address payable public marketowner;
uint256 public listingFee = 0.025 ether;
enum State { Created, Release, Inactive }
struct MarketItem {
uint id;
address nftContract;
uint256 tokenId;
address payable seller;
address payable buyer;
uint256 price;
State state;
}
mapping(uint256 => MarketItem) private marketItems;
event MarketItemCreated (
uint indexed id,
address indexed nftContract,
uint256 indexed tokenId,
address seller,
address buyer,
uint256 price,
State state
);
event MarketItemSold (
uint indexed id,
address indexed nftContract,
uint256 indexed tokenId,
address seller,
address buyer,
uint256 price,
State state
);
constructor() {
marketowner = payable(msg.sender);
}
/**
* @dev Returns the listing fee of the marketplace
*/
function getListingFee() public view returns (uint256) {
return listingFee;
}
/**
* @dev create a MarketItem for NFT sale on the marketplace.
*
* List an NFT.
*/
function createMarketItem(
address nftContract,
uint256 tokenId,
uint256 price
) public payable nonReentrant {
require(price > 0, "Price must be at least 1 wei");
require(msg.value == listingFee, "Fee must be equal to listing fee");
require(IERC721(nftContract).getApproved(tokenId) == address(this), "NFT must be approved to market");
// change to approve mechanism from the original direct transfer to market
// IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
_itemCounter.increment();
uint256 id = _itemCounter.current();
marketItems[id] = MarketItem(
id,
nftContract,
tokenId,
payable(msg.sender),
payable(address(0)),
price,
State.Created
);
emit MarketItemCreated(
id,
nftContract,
tokenId,
msg.sender,
address(0),
price,
State.Created
);
}
/**
* @dev delete a MarketItem from the marketplace.
*
* de-List an NFT.
*
* todo ERC721.approve can't work properly!! comment out
*/
function deleteMarketItem(uint256 itemId) public nonReentrant {
require(itemId <= _itemCounter.current(), "id must <= item count");
require(marketItems[itemId].state == State.Created, "item must be on market");
MarketItem storage item = marketItems[itemId];
require(IERC721(item.nftContract).ownerOf(item.tokenId) == msg.sender, "must be the owner");
require(IERC721(item.nftContract).getApproved(item.tokenId) == address(this), "NFT must be approved to market");
item.state = State.Inactive;
emit MarketItemSold(
itemId,
item.nftContract,
item.tokenId,
item.seller,
address(0),
0,
State.Inactive
);
}
/**
* @dev (buyer) buy a MarketItem from the marketplace.
* Transfers ownership of the item, as well as funds
* NFT: seller -> buyer
* value: buyer -> seller
* listingFee: contract -> marketowner
*/
function createMarketSale(
address nftContract,
uint256 id
) public payable nonReentrant {
MarketItem storage item = marketItems[id]; //should use storge!!!!
uint price = item.price;
uint tokenId = item.tokenId;
require(msg.value == price, "Please submit the asking price");
require(IERC721(nftContract).getApproved(tokenId) == address(this), "NFT must be approved to market");
IERC721(nftContract).transferFrom(item.seller, msg.sender, tokenId);
payable(marketowner).transfer(listingFee);
item.seller.transfer(msg.value);
item.buyer = payable(msg.sender);
item.state = State.Release;
_itemSoldCounter.increment();
emit MarketItemSold(
id,
nftContract,
tokenId,
item.seller,
msg.sender,
price,
State.Release
);
}
/**
* @dev Returns all unsold market items
* condition:
* 1) state == Created
* 2) buyer = 0x0
* 3) still have approve
*/
function fetchActiveItems() public view returns (MarketItem[] memory) {
return fetchHepler(FetchOperator.ActiveItems);
}
/**
* @dev Returns only market items a user has purchased
* todo pagination
*/
function fetchMyPurchasedItems() public view returns (MarketItem[] memory) {
return fetchHepler(FetchOperator.MyPurchasedItems);
}
/**
* @dev Returns only market items a user has created
* todo pagination
*/
function fetchMyCreatedItems() public view returns (MarketItem[] memory) {
return fetchHepler(FetchOperator.MyCreatedItems);
}
enum FetchOperator { ActiveItems, MyPurchasedItems, MyCreatedItems}
/**
* @dev fetch helper
* todo pagination
*/
function fetchHepler(FetchOperator _op) private view returns (MarketItem[] memory) {
uint total = _itemCounter.current();
uint itemCount = 0;
for (uint i = 1; i <= total; i++) {
if (isCondition(marketItems[i], _op)) {
itemCount ++;
}
}
uint index = 0;
MarketItem[] memory items = new MarketItem[](itemCount);
for (uint i = 1; i <= total; i++) {
if (isCondition(marketItems[i], _op)) {
items[index] = marketItems[i];
index ++;
}
}
return items;
}
/**
* @dev helper to build condition
*
* todo should reduce duplicate contract call here
* (IERC721(item.nftContract).getApproved(item.tokenId) called in two loop
*/
function isCondition(MarketItem memory item, FetchOperator _op) private view returns (bool){
if(_op == FetchOperator.MyCreatedItems){
return
(item.seller == msg.sender
&& item.state != State.Inactive
)? true
: false;
}else if(_op == FetchOperator.MyPurchasedItems){
return
(item.buyer == msg.sender) ? true: false;
}else if(_op == FetchOperator.ActiveItems){
return
(item.buyer == address(0)
&& item.state == State.Created
&& (IERC721(item.nftContract).getApproved(item.tokenId) == address(this))
)? true
: false;
}else{
return false;
}
}
}
Этот контракт NFTMarket может работать, но он не очень хорош. Необходимо сделать как минимум две работы:
-
Мы должны добавить пагинацию в функции запроса. Если на рынке тысячи товаров, функция запроса не может работать хорошо.
-
Когда мы пытаемся проверить, передал ли продавец предмет NFT другим или удалил одобрение с рынка, мы вызываем
nft.getApproved
тысячи раз. Это плохая практика. Мы должны попытаться найти решение.
Мы также можем обнаружить, что позволять веб-приложению запрашивать данные непосредственно из смарт-контракта — не лучший вариант. Необходим слой индексирования данных. Протокол Graph Protocol и подграф могут выполнить эту работу. Объяснение того, как использовать subgraph, вы можете найти в учебнике Dabit по рынку NFT.
Замечание о delegatecall
Когда я создавал смарт-контракт NFTMarketplace, я исследовал неправильный путь в течение одного дня и многому научился. Вот что я узнал.
-
Когда продавец размещает НМТ на торговой площадке, он дает разрешение рыночному контракту
approve(marketaddress)
на передачу НМТ от продавца к покупателю, вызываяtransferFrom()
. Я хотел бы не использоватьsetApprovalForAll(operator, approved)
, который даст одобрение рыночного контракта для всех моих НМТ в одной коллекции. -
Продавец может захотеть удалить (исключить из списка) НМТ с рынка, поэтому мы добавляем функцию
deleteMarketItem(itemId)
. -
Здесь начинается неправильный путь. Я пытаюсь удалить одобрение для продавца в рыночном контракте.
- Вызов
nft.approve(address(0),tokenId)
вернется назад. Рыночный контракт не является владельцем этого NFT и не одобрен для всех как оператор. - Возможно, мы можем использовать
delegatecall
, который будет вызван с использованием оригинальногоmsg.sender
(продавец). Продавец является владельцем. - Я всегда получаю «Error: VM Exception while processing transaction: reverted with reason string ‘ERC721: owner query for nonexistent token'». Что происходит?
- Когда я пытаюсь делегировать вызов других функций, таких как
name()
, результат не правильный. - Копать, копать и копать. Наконец, я обнаружил, что неправильно понял
delegatecall
. Delegatecall использует хранилище вызывающей стороны (контракт market) и не использует хранилище вызываемой стороны (контракт nft). Solidity Docs пишет: «Хранилище, текущий адрес и баланс по-прежнему относятся к вызывающему контракту, только код берется из вызываемого адреса. « - Поэтому мы не можем делегировать вызов
nft.approve()
для удаления одобрения в рыночном контракте. Мы не можем получить доступ к исходным данным в контракте NFT через delegatecall.
- Вызов
Фрагмент кода delegatecall (который является неправильным):
bytes memory returndata = Address.functionDelegateCall(
item.nftContract,
abi.encodeWithSignature("approve(address,uint256)",address(0),1)
);
Address.verifyCallResult(true, returndata, "approve revert");
-
Но это еще не конец. В конце концов я обнаружил, что не стоит пытаться убрать одобрение в рыночном контракте. Логика неправильная.
- Продавец вызывает рыночный контракт
deleteMarketItem
для удаления рыночного товара. - Продавец не просит рыночный контракт вызвать nft-контракт «approve()» для удаления одобрения. (Существует
ERC20Permit
, но в ERC721 разрешения пока нет). - Дизайн блокчейна не позволяет контракту делать это.
- Продавец вызывает рыночный контракт
-
Если продавец хочет это сделать, он должен сделать это сам, вызвав
approve()
напрямую. Именно это мы делаем в модульном тестеawait nft.approve(ethers.constants.AddressZero,1)
.
Opensea предлагает использовать isApprovedForAll
в своем руководстве (пример кода):
/**
* Override isApprovedForAll to whitelist user's OpenSea proxy accounts to enable gas-less listings.
*/
function isApprovedForAll(address owner, address operator)
override
public
view
returns (bool)
{
// Whitelist OpenSea proxy contract for easy trading.
ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress);
if (address(proxyRegistry.proxies(owner)) == operator) {
return true;
}
return super.isApprovedForAll(owner, operator);
}
Механизм «одобрить для всех» довольно сложен, и вы можете обратиться к контракту opensea proxy для получения дополнительной информации.
Задача 4.3: Юнит-тест для NFTMarketplace (основная функция)
Мы добавим два скрипта модульного тестирования для NFTMarketplace:
- один для основных функций
- один для функций запроса/выборки
Скрипт модульного тестирования для основных функций:
// NFTMarketplace.test.ts
import { expect } from "chai"
import { BigNumber, Signer } from "ethers"
import { ethers } from "hardhat"
import { BadgeToken, NFTMarketplace } from "../typechain"
import { TransactionResponse, TransactionReceipt } from "@ethersproject/providers"
const _name='BadgeToken'
const _symbol='BADGE'
describe("NFTMarketplace", function () {
let nft:BadgeToken
let market:NFTMarketplace
let account0:Signer,account1:Signer,account2:Signer
let address0:string, address1:string, address2:string
let listingFee:BigNumber
const auctionPrice = ethers.utils.parseUnits('1', 'ether')
beforeEach(async function () {
[account0, account1, account2] = await ethers.getSigners()
address0 = await account0.getAddress()
address1 = await account1.getAddress()
address2 = await account2.getAddress()
const BadgeToken = await ethers.getContractFactory("BadgeToken")
nft = await BadgeToken.deploy(_name,_symbol)
const Market = await ethers.getContractFactory("NFTMarketplace")
market = await Market.deploy()
listingFee = await market.getListingFee()
})
it("Should create market item successfully", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
const items = await market.fetchMyCreatedItems()
expect(items.length).to.be.equal(1)
})
it("Should create market item with EVENT", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await expect(market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee }))
.to.emit(market, 'MarketItemCreated')
.withArgs(
1,
nft.address,
1,
address0,
ethers.constants.AddressZero,
auctionPrice,
0)
})
it("Should revert to create market item if nft is not approved", async function() {
await nft.mintTo(address0) //tokenId=1
// await nft.approve(market.address,1)
await expect(market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee }))
.to.be.revertedWith('NFT must be approved to market')
})
it("Should create market item and buy (by address#1) successfully", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
await expect(market.connect(account1).createMarketSale(nft.address, 1, { value: auctionPrice}))
.to.emit(market, 'MarketItemSold')
.withArgs(
1,
nft.address,
1,
address0,
address1,
auctionPrice,
1)
expect(await nft.ownerOf(1)).to.be.equal(address1)
})
it("Should revert buy if seller remove approve", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
await nft.approve(ethers.constants.AddressZero,1)
await expect(market.connect(account1).createMarketSale(nft.address, 1, { value: auctionPrice}))
.to.be.reverted
})
it("Should revert buy if seller transfer the token out", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
await nft.transferFrom(address0,address2,1)
await expect(market.connect(account1).createMarketSale(nft.address, 1, { value: auctionPrice}))
.to.be.reverted
})
it("Should revert to delete(de-list) with wrong params", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
//not a correct id
await expect(market.deleteMarketItem(2)).to.be.reverted
//not owner
await expect(market.connect(account1).deleteMarketItem(1)).to.be.reverted
await nft.transferFrom(address0,address1,1)
//not approved to market now
await expect(market.deleteMarketItem(1)).to.be.reverted
})
it("Should create market item and delete(de-list) successfully", async function() {
await nft.mintTo(address0) //tokenId=1
await nft.approve(market.address,1)
await market.createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
await market.deleteMarketItem(1)
await nft.approve(ethers.constants.AddressZero,1)
// should revert if trying to delete again
await expect(market.deleteMarketItem(1))
.to.be.reverted
})
it("Should seller, buyer and market owner correct ETH value after sale", async function() {
let txresponse:TransactionResponse, txreceipt:TransactionReceipt
let gas
const marketownerBalance = await ethers.provider.getBalance(address0)
await nft.connect(account1).mintTo(address1) //tokenId=1
await nft.connect(account1).approve(market.address,1)
let sellerBalance = await ethers.provider.getBalance(address1)
txresponse = await market.connect(account1).createMarketItem(nft.address, 1, auctionPrice, { value: listingFee })
const sellerAfter = await ethers.provider.getBalance(address1)
txreceipt = await txresponse.wait()
gas = txreceipt.gasUsed.mul(txreceipt.effectiveGasPrice)
// sellerAfter = sellerBalance - listingFee - gas
expect(sellerAfter).to.equal(sellerBalance.sub( listingFee).sub(gas))
const buyerBalance = await ethers.provider.getBalance(address2)
txresponse = await market.connect(account2).createMarketSale(nft.address, 1, { value: auctionPrice})
const buyerAfter = await ethers.provider.getBalance(address2)
txreceipt = await txresponse.wait()
gas = txreceipt.gasUsed.mul(txreceipt.effectiveGasPrice)
expect(buyerAfter).to.equal(buyerBalance.sub(auctionPrice).sub(gas))
const marketownerAfter = await ethers.provider.getBalance(address0)
expect(marketownerAfter).to.equal(marketownerBalance.add(listingFee))
})
})
Запуск:
yarn hardhat test test/NFTMarketplace.test.ts
Результаты:
NFTMarketplace
✓ Should create market item successfully (49ms)
✓ Should create market item with EVENT
✓ Should revert to create market item if nft is not approved
✓ Should create market item and buy (by address#1) successfully (48ms)
✓ Should revert buy if seller remove approve (49ms)
✓ Should revert buy if seller transfer the token out (40ms)
✓ Should revert to delete(de-list) with wrong params (49ms)
✓ Should create market item and delete(de-list) successfully (44ms)
✓ Should seller, buyer and market owner correct ETH value after sale (43ms)
9 passing (1s)
Задача 4.4: Модульное тестирование для NFTMarketplace (функция запроса)
Сценарий модульного тестирования для функции запроса:
// NFTMarketplaceFetch.test.ts
import { expect } from "chai"
import { BigNumber, Signer } from "ethers"
import { ethers } from "hardhat"
import { BadgeToken, NFTMarketplace } from "../typechain"
const _name='BadgeToken'
const _symbol='BADGE'
describe("NFTMarketplace Fetch functions", function () {
let nft:BadgeToken
let market:NFTMarketplace
let account0:Signer,account1:Signer,account2:Signer
let address0:string, address1:string, address2:string
let listingFee:BigNumber
const auctionPrice = ethers.utils.parseUnits('1', 'ether')
beforeEach(async function () {
[account0, account1, account2] = await ethers.getSigners()
address0 = await account0.getAddress()
address1 = await account1.getAddress()
address2 = await account2.getAddress()
const BadgeToken = await ethers.getContractFactory("BadgeToken")
nft = await BadgeToken.deploy(_name,_symbol)
// tokenAddress = nft.address
const Market = await ethers.getContractFactory("NFTMarketplace")
market = await Market.deploy()
listingFee = await market.getListingFee()
// console.log("1. == mint 1-6 to account#0")
for(let i=1;i<=6;i++){
await nft.mintTo(address0)
}
// console.log("3. == mint 7-9 to account#1")
for(let i=7;i<=9;i++){
await nft.connect(account1).mintTo(address1)
}
// console.log("2. == list 1-6 to market")
for(let i=1;i<=6;i++){
await nft.approve(market.address,i)
await market.createMarketItem(nft.address, i, auctionPrice, { value: listingFee })
}
})
it("Should fetchActiveItems correctly", async function() {
const items = await market.fetchActiveItems()
expect(items.length).to.be.equal(6)
})
it("Should fetchMyCreatedItems correctly", async function() {
const items = await market.fetchMyCreatedItems()
expect(items.length).to.be.equal(6)
//should delete correctly
await market.deleteMarketItem(1)
const newitems = await market.fetchMyCreatedItems()
expect(newitems.length).to.be.equal(5)
})
it("Should fetchMyPurchasedItems correctly", async function() {
const items = await market.fetchMyPurchasedItems()
expect(items.length).to.be.equal(0)
})
it("Should fetchActiveItems with correct return values", async function() {
const items = await market.fetchActiveItems()
expect(items[0].id).to.be.equal(BigNumber.from(1))
expect(items[0].nftContract).to.be.equal(nft.address)
expect(items[0].tokenId).to.be.equal(BigNumber.from(1))
expect(items[0].seller).to.be.equal(address0)
expect(items[0].buyer).to.be.equal(ethers.constants.AddressZero)
expect(items[0].state).to.be.equal(0)//enum State.Created
})
it("Should fetchMyPurchasedItems with correct return values", async function() {
await market.connect(account1).createMarketSale(nft.address, 1, { value: auctionPrice})
const items = await market.connect(account1).fetchMyPurchasedItems()
expect(items[0].id).to.be.equal(BigNumber.from(1))
expect(items[0].nftContract).to.be.equal(nft.address)
expect(items[0].tokenId).to.be.equal(BigNumber.from(1))
expect(items[0].seller).to.be.equal(address0)
expect(items[0].buyer).to.be.equal(address1)//address#1
expect(items[0].state).to.be.equal(1)//enum State.Release
})
})
Запуск:
yarn hardhat test test/NFTMarketplaceFetch.test.ts
Результаты:
NFTMarketplace Fetch functions
✓ Should fetchActiveItems correctly (48ms)
✓ Should fetchMyCreatedItems correctly (54ms)
✓ Should fetchMyPurchasedItems correctly
✓ Should fetchActiveItems with correct return values
✓ Should fetchMyPurchasedItems with correct return values
5 passing (2s)
Задание 4.5: Вспомогательный скрипт playMarket.ts
для разработки смарт-контракта
Напишем скрипт src/playMarket.ts
. В процессе разработки и отладки я запускаю этот скрипт снова и снова. Он помогает мне понять, может ли рыночный контракт работать так, как он задуман.
// src/playMarket.ts
import { Signer } from "ethers"
import { ethers } from "hardhat"
import { BadgeToken, NFTMarketplace } from "../typechain"
const base64 = require( "base-64")
const _name='BadgeToken'
const _symbol='BADGE'
async function main() {
let account0:Signer,account1:Signer
[account0, account1] = await ethers.getSigners()
const address0=await account0.getAddress()
const address1=await account1.getAddress()
/* deploy the marketplace */
const Market = await ethers.getContractFactory("NFTMarketplace")
const market:NFTMarketplace = await Market.deploy()
await market.deployed()
const marketAddress = market.address
/* deploy the NFT contract */
const NFT = await ethers.getContractFactory("BadgeToken")
const nft:BadgeToken = await NFT.deploy(_name,_symbol)
await nft.deployed()
const tokenAddress = nft.address
console.log("marketAddress",marketAddress)
console.log("nftContractAddress",tokenAddress)
/* create two tokens */
await nft.mintTo(address0) //'1'
await nft.mintTo(address0) //'2'
await nft.mintTo(address0) //'3'
const listingFee = await market.getListingFee()
const auctionPrice = ethers.utils.parseUnits('1', 'ether')
await nft.approve(marketAddress,1)
await nft.approve(marketAddress,2)
await nft.approve(marketAddress,3)
console.log("Approve marketAddress",marketAddress)
// /* put both tokens for sale */
await market.createMarketItem(tokenAddress, 1, auctionPrice, { value: listingFee })
await market.createMarketItem(tokenAddress, 2, auctionPrice, { value: listingFee })
await market.createMarketItem(tokenAddress, 3, auctionPrice, { value: listingFee })
// test transfer
await nft.transferFrom(address0,address1,2)
/* execute sale of token to another user */
await market.connect(account1).createMarketSale(tokenAddress, 1, { value: auctionPrice})
/* query for and return the unsold items */
console.log("==after purchase & Transfer==")
let items = await market.fetchActiveItems()
let printitems
printitems = await parseItems(items,nft)
printitems.map((item)=>{printHelper(item,true,false)})
// console.log( await parseItems(items,nft))
console.log("==after delete==")
await market.deleteMarketItem(3)
items = await market.fetchActiveItems()
printitems = await parseItems(items,nft)
printitems.map((item)=>{printHelper(item,true,false)})
// console.log( await parseItems(items,nft))
console.log("==my list items==")
items = await market.fetchMyCreatedItems()
printitems = await parseItems(items,nft)
printitems.map((item)=>{printHelper(item,true,false)})
console.log("")
console.log("==address1 purchased item (only one, tokenId =1)==")
items = await market.connect(account1).fetchMyPurchasedItems()
printitems = await parseItems(items,nft)
printitems.map((item)=>{printHelper(item,true,true)})
}
async function parseItems(items:any,nft:BadgeToken) {
let parsed= await Promise.all(items.map(async (item:any) => {
const tokenUri = await nft.tokenURI(item.tokenId)
return {
price: item.price.toString(),
tokenId: item.tokenId.toString(),
seller: item.seller,
buyer: item.buyer,
tokenUri
}
}))
return parsed
}
function printHelper(item:any,flagUri=false,flagSVG=false){
if(flagUri){
const {name,description,svg}= parseNFT(item)
console.log("id & name:",item.tokenId,name)
if(flagSVG) console.log(svg)
}else{
console.log("id :",item.tokenId)
}
}
function parseNFT(item:any){
const data = base64.decode(item.tokenUri.slice(29))
const itemInfo = JSON.parse(data)
const svg = base64.decode(itemInfo.image.slice(26))
return(
{"name":itemInfo.name,
"description":itemInfo.description,
"svg":svg})
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
Что мы делаем в этом скрипте:
- развертываем BadgeToken NFT и NFTMarketplace
- чеканить 3 предмета NFT на адрес0
- утвердить 3 элемента NFT в рыночном контракте
- перечислить 3 предмета NFT на NFTMarketplace
- передайте Бейдж #3 другому
- перечисленные предметы должны быть #1,#2
- адрес1(счет1) купить значок №1
- адрес1 приобретенный товар должен быть №1
- распечатать tokenId, имя, svg для проверки
Выполнить:
yarn hardhat run src/playMarket.ts
Результат:
marketAddress 0x5FbDB2315678afecb367f032d93F642f64180aa3
nftContractAddress 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Approve marketAddress 0x5FbDB2315678afecb367f032d93F642f64180aa3
==after purchase & Transfer==
id & name: 3 Badge #3
==after delete==
==my list items==
id & name: 1 Badge #1
id & name: 2 Badge #2
==address1 purchased item svg (only one, tokenId =1)==
id & name: 1 Badge #1
<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'><style>.base { fill: white; font-family: serif; font-size: 300px; }</style><rect width='100%' height='100%' fill='brown' /><text x='100' y='260' class='base'>1</text></svg>
✨ Done in 4.42s.
Задание 4.6: Скрипты для подготовки к работе с webapp
Нам необходимо подготовить данные для webapp:
- 1-6 по Счету#0, 1- Счет1, 2- Счет#2
- 7-9 по счету#1, 7,8 — счет#0
- На рынке: 3,4,5,9 (6 делистинг по счету#0)
- Счет#0: покупка 7,8, список: 1-5 (6 исключены из списка)
- Счет#1:покупка 1, список:7-9
- Счет#2:Купить 2, Список:н/а
// src/prepare.ts
import { Signer } from "ethers"
import { ethers } from "hardhat"
import { BadgeToken, NFTMarketplace } from "../typechain"
import { tokenAddress, marketAddress } from "./projectsetting"
const _name='BadgeToken'
const _symbol='BADGE'
async function main() {
console.log("======== deploy to a **new** localhost ======")
/* deploy the NFT contract */
const NFT = await ethers.getContractFactory("BadgeToken")
const nftContract:BadgeToken = await NFT.deploy(_name,_symbol)
await nftContract.deployed()
/* deploy the marketplace */
const Market = await ethers.getContractFactory("NFTMarketplace")
const marketContract:NFTMarketplace = await Market.deploy()
console.log("nftContractAddress:",nftContract.address)
console.log("marketAddress :",marketContract.address)
console.log("======== Prepare for webapp dev ======")
console.log("nftContractAddress:",tokenAddress)
console.log("marketAddress :",marketAddress)
console.log("**should be the same**")
let owner:Signer,account1:Signer,account2:Signer
[owner, account1,account2] = await ethers.getSigners()
const address0 = await owner.getAddress()
const address1 = await account1.getAddress()
const address2 = await account2.getAddress()
const market:NFTMarketplace = await ethers.getContractAt("NFTMarketplace", marketAddress)
const nft:BadgeToken = await ethers.getContractAt("BadgeToken", tokenAddress)
const listingFee = await market.getListingFee()
const auctionPrice = ethers.utils.parseUnits('1', 'ether')
console.log("1. == mint 1-6 to account#0")
for(let i=1;i<=6;i++){
await nft.mintTo(address0)
}
console.log("2. == list 1-6 to market")
for(let i=1;i<=6;i++){
await nft.approve(marketAddress,i)
await market.createMarketItem(tokenAddress, i, auctionPrice, { value: listingFee })
}
console.log("3. == mint 7-9 to account#1")
for(let i=7;i<=9;i++){
await nft.connect(account1).mintTo(address1)
}
console.log("4. == list 1-6 to market")
for(let i=7;i<=9;i++){
await nft.connect(account1).approve(marketAddress,i)
await market.connect(account1).createMarketItem(tokenAddress, i, auctionPrice, { value: listingFee })
}
console.log("5. == account#0 buy 7 & 8")
await market.createMarketSale(tokenAddress, 7, { value: auctionPrice})
await market.createMarketSale(tokenAddress, 8, { value: auctionPrice})
console.log("6. == account#1 buy 1")
await market.connect(account1).createMarketSale(tokenAddress, 1, { value: auctionPrice})
console.log("7. == account#2 buy 2")
await market.connect(account2).createMarketSale(tokenAddress, 2, { value: auctionPrice})
console.log("8. == account#0 delete 6")
await market.deleteMarketItem(6)
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
Запустите автономный локальный тестнет на другом терминале:
yarn hardhat node
Запустить:
yarn hardhat run src/prepare.ts --network localhost
Результаты:
======== deploy to a **new** localhost ======
nftContractAddress: 0x5FbDB2315678afecb367f032d93F642f64180aa3
marketAddress : 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
======== Prepare for webapp dev ======
nftContractAddress: 0x5FbDB2315678afecb367f032d93F642f64180aa3
marketAddress : 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
**should be the same**
1. == mint 1-6 to account#0
2. == list 1-6 to market
3. == mint 7-9 to account#1
4. == list 1-6 to market
5. == account#0 buy 7 & 8
6. == account#1 buy 1
7. == account#2 buy 2
8. == account#0 delete 6
✨ Done in 5.81s.
Задание 5: Webapp для NFTMarketplace
Задача 5.1: добавить компонент ReadNFTMarket
.
В настоящее время мы запрашиваем рыночный контракт напрямую вместо использования SWR
в этом фрагменте кода.
// components/ReadNFTMarket.tsx
import React from 'react'
import { useEffect,useState } from 'react';
import { useWeb3React } from '@web3-react/core'
import { Web3Provider } from '@ethersproject/providers'
import { Contract } from "@ethersproject/contracts";
import { Grid, GridItem, Box, Text, Button } from "@chakra-ui/react"
import { BigNumber, ethers } from 'ethers';
import useSWR from 'swr'
import { addressNFTContract, addressMarketContract } from '../projectsetting'
import CardERC721 from "./CardERC721"
interface Props {
option: number
}
export default function ReadNFTMarket(props:Props){
const abiJSON = require("abi/NFTMarketplace.json")
const abi = abiJSON.abi
const [items,setItems] = useState<[]>()
const { account, active, library} = useWeb3React<Web3Provider>()
// const { data: items} = useSWR([addressContract, 'fetchActiveItems'], {
// fetcher: fetcher(library, abi),
// })
useEffect( () => {
if(! active)
setItems(undefined)
if(!(active && account && library)) return
// console.log(addressContract,abi,library)
const market:Contract = new Contract(addressMarketContract, abi, library);
console.log(market.provider)
console.log(account)
library.getCode(addressMarketContract).then((result:string)=>{
//check whether it is a contract
if(result === '0x') return
switch(props.option){
case 0:
market.fetchActiveItems({from:account}).then((items:any)=>{
setItems(items)
})
break;
case 1:
market.fetchMyPurchasedItems({from:account}).then((items:any)=>{
setItems(items)
})
break;
case 2:
market.fetchMyCreatedItems({from:account}).then((items:any)=>{
setItems(items)
console.log(items)
})
break;
default:
}
})
//called only when changed to active
},[active,account])
async function buyInNFTMarket(event:React.FormEvent,itemId:BigNumber) {
event.preventDefault()
if(!(active && account && library)) return
//TODO check whether item is available beforehand
const market:Contract = new Contract(addressMarketContract, abi, library.getSigner());
const auctionPrice = ethers.utils.parseUnits('1', 'ether')
market.createMarketSale(
addressNFTContract,
itemId,
{ value: auctionPrice}
).catch('error', console.error)
}
const state = ["On Sale","Sold","Inactive"]
return (
<Grid templateColumns='repeat(3, 1fr)' gap={0} w='100%'>
{items
?
(items.length ==0)
?<Box>no item</Box>
:items.map((item:any)=>{
return(
<GridItem key={item.id} >
<CardERC721 addressContract={item.nftContract} tokenId={item.tokenId} ></CardERC721>
<Text fontSize='sm' px={5} pb={1}> {state[item.state]} </Text>
{((item.seller == account && item.buyer == ethers.constants.AddressZero) || (item.buyer == account))
?<Text fontSize='sm' px={5} pb={1}> owned by you </Text>
:<Text></Text>
}
<Box>{
(item.seller != account && item.state == 0)
? <Button width={220} type="submit" onClick={(e)=>buyInNFTMarket(e,item.id)}>Buy this!</Button>
: <Text></Text>
}
</Box>
</GridItem>)
})
:<Box></Box>}
</Grid>
)
}
Задача 5.2: добавить ReadNFTMarket
в индекс
Мы добавляем три ReadNFTMarket
в index.tsx:
- один для всех товаров на рынке
- один для моих купленных товаров
- один для моих созданных товаров
Задача 5.3: Запуск DApp
ШАГ 1: запустите новую локальную тестовую сеть
В другом терминале запустите chain/
.
yarn hardhat node
ШАГ 2: подготовить данные для webapp
Запустите в chain/
yarn hardhat run src/prepare.ts --network localhost
ШАГ 3: запустить webapp
Запустить в webapp/
yarn dev
ШАГ 4: браузер http://localhost:3000/
и подключите MetaMask
Установите в качестве мнемоники MetaMask предустановленную реф-ссылку Hardhat и добавьте в нее аккаунты:
test test test test test test
test test test test test junk
ШАГ 5: купите значок #9 как аккаунт#0
ШАГ 6: переключитесь на аккаунт №1 в MetaMask, купите значок №3.
Теперь у вас есть рынок NFT. Поздравляем.
Вы можете продолжить его развертывание в публичном testnet(ropsten), ethereum mainnet, sidechain(BSC/Polygon), Layer2(Arbitrum/Optimism).
Необязательное задание 6: Развертывание на Polygon и запрос с помощью API Alchemy NFT
Задание 6.1 Развертывание в Polygon
В этом дополнительном задании я разверну контракт NFT и контракт NFTMarketplace в Polygon mainnet, поскольку плата за газ является нормальной. Вы также можете выбрать развертывание в Ethereum testnet (Goerli), Polygon testnet (Mumbai) или Layer 2 testnet (например, Arbitrum Goerli).
ШАГ 1. Отредактируйте .env
с URL Alchemy с ключом, вашим приватным ключом для тестирования, ключом API Polygonscan. Вам может понадобиться добавить полигон в ваш hardhat.config.ts
.
POLYGONSCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1
POLYGON_URL=https://polygon-mainnet.g.alchemy.com/v2/<YOUR ALCHEMY KEY>
POLYGON_PRIVATE_KEY=0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1
ШАГ 2. Разверните контракт NFT и проверьте его на сайте polygonscan.com. Выполнить:
yarn hardhat run scripts/deploy_BadgeToken.ts --network polygon
// BadgeToken deployed to: 0x1FC8b9DC757FD50Bfec8bbE103256F176435faEE
yarn hardhat verify --network polygon 0x1FC8b9DC757FD50Bfec8bbE103256F176435faEE 'BadgeToken' 'BADGE'
// Successfully verified contract BadgeToken on Etherscan.
// https://polygonscan.com/address/0x1FC8b9DC757FD50Bfec8bbE103256F176435faEE#code
ШАГ 3. Разверните NFTMarketplace и проверьте.
yarn hardhat run scripts/deploy_Marketplace.ts --network polygon
// NFTMarketplace deployed to: 0x2B7302B1ABCD30Cd475d78688312529027d57bEf
yarn hardhat verify --network polygon 0x2B7302B1ABCD30Cd475d78688312529027d57bEf
// Successfully verified contract NFTMarketplace on Etherscan.
// https://polygonscan.com/address/0x2B7302B1ABCD30Cd475d78688312529027d57bEf#code
Задача 6.2 Монетирование NFT и размещение на торговой площадке
ШАГ 4. Монтируйте один NFT (tokenId=1) на свой тестовый аккаунт на https://polygonscan.com/.
Вы можете просмотреть NFT «Badge #1» на opensea: https://opensea.io/assets/matic/0x1fc8b9dc757fd50bfec8bbe103256f176435faee/1.
ШАГ 5. Внесите NFT «Badge #1» в контракт NFTMarketpalce на https://polygonscan.com/.
Сначала вам необходимо утвердить товар NFT «Badge #1» в NFTMarketpalce.
Затем вы вызываете CreateMarketItem()
.
ШАГ 6. Запустите веб-приложение. После подключения кошелька вы сможете увидеть предмет на рынке.
Примечание: не забудьте отредактировать контракт NFT и адрес контракта NFTMarketpalce в webapp/src/projectsetting.ts
.
Задача 6.3 Запрос к NFT с помощью API Alchemy NFT
Теперь мы можем перейти к использованию Alchemy NFT APIs (ссылка на документацию) для запроса данных NFT и отображения их в нашем webapp.
Давайте попробуем это сделать. Для демонстрации мы будем использовать Alchemy SDK
.
yarn add alchemy-sdk
Этот фрагмент кода адаптирован из документации Alchemy NFT APIs(ссылка). Для его выполнения вам потребуется ключ Alchemy API Key.
// This script demonstrates access to the NFT API via the Alchemy SDK.
import { Network, Alchemy } from "alchemy-sdk";
import base64 from "base-64"
const settings = {
apiKey: "Your Alchemy API Key",
network: Network.MATIC_MAINNET,
};
const alchemy = new Alchemy(settings);
const addressNFTContract = "0x1FC8b9DC757FD50Bfec8bbE103256F176435faEE"
const owner = await alchemy.nft.getOwnersForNft(addressNFTContract, "1")
console.log("Badge #1 owner:", owner )
// Print NFT metadata returned in the response:
const metadata = await alchemy.nft.getNftMetadata(
addressNFTContract,
"1"
)
console.log("tokenURI:", metadata.tokenUri)
const media = metadata.media[0].raw
console.log("media:", media)
const svg = base64.decode(media.slice(26))
console.log(svg)
Результаты:
Badge #1 owner: { owners: [ '0x08e2af90ff53a3d3952eaa881bf9b3c05e893462' ] }
tokenURI: {
raw: 'data:application/json;base64,eyJuYW...',
gateway: ''
}
media: data:image/svg+xml;base64,PHN2...
<svg xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMinYMin meet'
viewBox='0 0 350 350'>
<style>.base { fill: white; font-family: serif; font-size: 300px; }</style>
<rect width='100%' height='100%' fill='brown' />
<text x='100' y='260' class='base'>1</text>
</svg>
Вот и все. Мы разработали супер упрощенную версию Opensea, включая контракт и webapp. Нам предстоит еще много работы. Возьмем, например, такой пример:
-
Ваша первая версия NFTMarketpalce работает хорошо. Через несколько недель вы обнаруживаете, что вам нужно добавить новую функциональность в NFTMarketplace.
-
Смарт-контракт неизменяем. Развертывание новой версии NFTMarketplace и просьба пользователей перечислить свои NFT в новый контракт — не лучшая идея.
-
Теперь вам нужен обновляемый смарт-контракт (модель прокси-контракта). Вы можете узнать, как разработать прокси-контракт в моем другом руководстве: Tutorial: write upgradeable smart contract (proxy) using OpenZeppelin.
Список уроков:
1. Краткий учебник по Hardhat (3 части)
https://dev.to/yakult/a-concise-hardhat-tutorial-part-1-7eo
2. Понимание блокчейна с помощью Ethers.js
(5 частей)
https://dev.to/yakult/01-understanding-blockchain-with-ethersjs-4-tasks-of-basics-and-transfer-5d17
3. Учебник: создайте свой первый DAPP с Remix и Etherscan (7 заданий)
https://dev.to/yakult/tutorial-build-your-first-dapp-with-remix-and-etherscan-52kf
4. Учебник: создание DApp с помощью Hardhat, React и Ethers.js (6 заданий)
https://dev.to/yakult/a-tutorial-build-dapp-with-hardhat-react-and-ethersjs-1gmi
5. Учебник: создание DAPP с помощью Web3-React и SWR (5 заданий)
https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0
6. Учебник: написать обновляемый смарт-контракт (прокси) с помощью OpenZeppelin(7 задач)
https://dev.to/yakult/tutorial-write-upgradeable-smart-contract-proxy-contract-with-openzeppelin-1916
7. Учебник: Построение NFT маркетплейса DApp как Opensea(5 задач)
https://dev.to/yakult/tutorial-build-a-nft-marketplace-dapp-like-opensea-3ng9
Если вы нашли этот учебник полезным, следуйте за мной в Twitter @fjun99
Уважаемый Уткин Игнат. Как с Вами связаться?