├── .env
├── .eslintrc
├── .example.infuraid
├── .gitignore
├── .gitpod.yml
├── LICENSE.txt
├── README.md
├── config.example.js
├── contracts
└── NFTMarketplace.sol
├── hardhat.config.js
├── next.config.js
├── package.json
├── pages
├── _app.js
├── create-nft.js
├── dashboard.js
├── index.js
├── my-nfts.js
└── resell-nft.js
├── postcss.config.js
├── public
├── favicon.ico
└── vercel.svg
├── scripts
└── deploy.js
├── styles
├── Home.module.css
└── globals.css
├── tailwind.config.js
├── test
└── sample-test.js
├── wallet.png
└── yarn.lock
/.env:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_WORKSPACE_URL=$CLIENT_URL
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "next/core-web-vitals"]
3 | }
4 |
--------------------------------------------------------------------------------
/.example.infuraid:
--------------------------------------------------------------------------------
1 | ""
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | .secret
4 | .infuraid
5 | config.js
6 |
7 | # dependencies
8 | /node_modules
9 | /.pnp
10 | .pnp.js
11 |
12 | # testing
13 | /coverage
14 |
15 | # next.js
16 | /.next/
17 | /out/
18 |
19 | # production
20 | /build
21 |
22 | # misc
23 | .DS_Store
24 | *.pem
25 |
26 | # debug
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
31 | # local env files
32 | .env.local
33 | .env.development.local
34 | .env.test.local
35 | .env.production.local
36 |
37 | # vercel
38 | .vercel
39 |
40 | node_modules
41 |
42 | #Hardhat files
43 | cache
44 | artifacts
45 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | tasks:
2 | - name: Install, start node, and deploy
3 | init: yarn
4 | command: npx hardhat node
5 | - name: Deploy smart contract and start server
6 | command: |
7 | export CLIENT_URL="$(gp url 8545)"
8 | gp await-port 8545
9 | npx hardhat run scripts/deploy.js --network localhost
10 | npm run dev
11 | openMode: tab-after
12 | ports:
13 | - port: 3000-8545
14 | visibility: public
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022 Nader Dabit
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Full stack NFT marketplace built with Polygon, Solidity, IPFS, & Next.js
2 |
3 | 
4 |
5 | This is the codebase to go along with tbe blog post [Building a Full Stack NFT Marketplace on Ethereum with Polygon](https://dev.to/dabit3/building-scalable-full-stack-apps-on-ethereum-with-polygon-2cfb)
6 |
7 | ### Running this project
8 |
9 | #### Gitpod
10 |
11 | To deploy this project to Gitpod, follow these steps:
12 |
13 | 1. Click this link to deploy
14 |
15 | [](https://gitpod.io/#github.com/dabit3/polygon-ethereum-nextjs-marketplace)
16 |
17 | 2. Import the RPC address given to you by GitPod into your MetaMask wallet
18 |
19 | This endpoint will look something like this:
20 |
21 | ```
22 | https://8545-copper-swordtail-j1mvhxv3.ws-eu18.gitpod.io/
23 | ```
24 |
25 | The chain ID should be 1337. If you have a localhost rpc set up, you may need to overwrite it.
26 |
27 | 
28 |
29 | #### Local setup
30 |
31 | To run this project locally, follow these steps.
32 |
33 | 1. Clone the project locally, change into the directory, and install the dependencies:
34 |
35 | ```sh
36 | git clone https://github.com/dabit3/polygon-ethereum-nextjs-marketplace.git
37 |
38 | cd polygon-ethereum-nextjs-marketplace
39 |
40 | # install using NPM or Yarn
41 | npm install
42 |
43 | # or
44 |
45 | yarn
46 | ```
47 |
48 | 2. Start the local Hardhat node
49 |
50 | ```sh
51 | npx hardhat node
52 | ```
53 |
54 | 3. With the network running, deploy the contracts to the local network in a separate terminal window
55 |
56 | ```sh
57 | npx hardhat run scripts/deploy.js --network localhost
58 | ```
59 |
60 | 4. Start the app
61 |
62 | ```
63 | npm run dev
64 | ```
65 |
66 | ### Configuration
67 |
68 | To deploy to Polygon test or main networks, update the configurations located in __hardhat.config.js__ to use a private key and, optionally, deploy to a private RPC like Infura.
69 |
70 | ```javascript
71 | require("@nomiclabs/hardhat-waffle");
72 | const fs = require('fs');
73 | const privateKey = fs.readFileSync(".secret").toString().trim() || "01234567890123456789";
74 |
75 | // infuraId is optional if you are using Infura RPC
76 | const infuraId = fs.readFileSync(".infuraid").toString().trim() || "";
77 |
78 | module.exports = {
79 | defaultNetwork: "hardhat",
80 | networks: {
81 | hardhat: {
82 | chainId: 1337
83 | },
84 | mumbai: {
85 | // Infura
86 | // url: `https://polygon-mumbai.infura.io/v3/${infuraId}`
87 | url: "https://rpc-mumbai.matic.today",
88 | accounts: [privateKey]
89 | },
90 | matic: {
91 | // Infura
92 | // url: `https://polygon-mainnet.infura.io/v3/${infuraId}`,
93 | url: "https://rpc-mainnet.maticvigil.com",
94 | accounts: [privateKey]
95 | }
96 | },
97 | solidity: {
98 | version: "0.8.4",
99 | settings: {
100 | optimizer: {
101 | enabled: true,
102 | runs: 200
103 | }
104 | }
105 | }
106 | };
107 | ```
108 |
109 | If using Infura, update __.infuraid__ with your [Infura](https://infura.io/) project ID.
110 |
--------------------------------------------------------------------------------
/config.example.js:
--------------------------------------------------------------------------------
1 | export const nftmarketaddress = ""
2 | export const nftaddress = ""
--------------------------------------------------------------------------------
/contracts/NFTMarketplace.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.4;
3 |
4 | import "@openzeppelin/contracts/utils/Counters.sol";
5 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
6 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
7 |
8 | import "hardhat/console.sol";
9 |
10 | contract NFTMarketplace is ERC721URIStorage {
11 | using Counters for Counters.Counter;
12 | Counters.Counter private _tokenIds;
13 | Counters.Counter private _itemsSold;
14 |
15 | uint256 listingPrice = 0.025 ether;
16 | address payable owner;
17 |
18 | mapping(uint256 => MarketItem) private idToMarketItem;
19 |
20 | struct MarketItem {
21 | uint256 tokenId;
22 | address payable seller;
23 | address payable owner;
24 | uint256 price;
25 | bool sold;
26 | }
27 |
28 | event MarketItemCreated (
29 | uint256 indexed tokenId,
30 | address seller,
31 | address owner,
32 | uint256 price,
33 | bool sold
34 | );
35 |
36 | constructor() ERC721("Metaverse Tokens", "METT") {
37 | owner = payable(msg.sender);
38 | }
39 |
40 | /* Updates the listing price of the contract */
41 | function updateListingPrice(uint _listingPrice) public payable {
42 | require(owner == msg.sender, "Only marketplace owner can update listing price.");
43 | listingPrice = _listingPrice;
44 | }
45 |
46 | /* Returns the listing price of the contract */
47 | function getListingPrice() public view returns (uint256) {
48 | return listingPrice;
49 | }
50 |
51 | /* Mints a token and lists it in the marketplace */
52 | function createToken(string memory tokenURI, uint256 price) public payable returns (uint) {
53 | _tokenIds.increment();
54 | uint256 newTokenId = _tokenIds.current();
55 |
56 | _mint(msg.sender, newTokenId);
57 | _setTokenURI(newTokenId, tokenURI);
58 | createMarketItem(newTokenId, price);
59 | return newTokenId;
60 | }
61 |
62 | function createMarketItem(
63 | uint256 tokenId,
64 | uint256 price
65 | ) private {
66 | require(price > 0, "Price must be at least 1 wei");
67 | require(msg.value == listingPrice, "Price must be equal to listing price");
68 |
69 | idToMarketItem[tokenId] = MarketItem(
70 | tokenId,
71 | payable(msg.sender),
72 | payable(address(this)),
73 | price,
74 | false
75 | );
76 |
77 | _transfer(msg.sender, address(this), tokenId);
78 | emit MarketItemCreated(
79 | tokenId,
80 | msg.sender,
81 | address(this),
82 | price,
83 | false
84 | );
85 | }
86 |
87 | /* allows someone to resell a token they have purchased */
88 | function resellToken(uint256 tokenId, uint256 price) public payable {
89 | require(idToMarketItem[tokenId].owner == msg.sender, "Only item owner can perform this operation");
90 | require(msg.value == listingPrice, "Price must be equal to listing price");
91 | idToMarketItem[tokenId].sold = false;
92 | idToMarketItem[tokenId].price = price;
93 | idToMarketItem[tokenId].seller = payable(msg.sender);
94 | idToMarketItem[tokenId].owner = payable(address(this));
95 | _itemsSold.decrement();
96 |
97 | _transfer(msg.sender, address(this), tokenId);
98 | }
99 |
100 | /* Creates the sale of a marketplace item */
101 | /* Transfers ownership of the item, as well as funds between parties */
102 | function createMarketSale(
103 | uint256 tokenId
104 | ) public payable {
105 | uint price = idToMarketItem[tokenId].price;
106 | address seller = idToMarketItem[tokenId].seller;
107 | require(msg.value == price, "Please submit the asking price in order to complete the purchase");
108 | idToMarketItem[tokenId].owner = payable(msg.sender);
109 | idToMarketItem[tokenId].sold = true;
110 | idToMarketItem[tokenId].seller = payable(address(0));
111 | _itemsSold.increment();
112 | _transfer(address(this), msg.sender, tokenId);
113 | payable(owner).transfer(listingPrice);
114 | payable(seller).transfer(msg.value);
115 | }
116 |
117 | /* Returns all unsold market items */
118 | function fetchMarketItems() public view returns (MarketItem[] memory) {
119 | uint itemCount = _tokenIds.current();
120 | uint unsoldItemCount = _tokenIds.current() - _itemsSold.current();
121 | uint currentIndex = 0;
122 |
123 | MarketItem[] memory items = new MarketItem[](unsoldItemCount);
124 | for (uint i = 0; i < itemCount; i++) {
125 | if (idToMarketItem[i + 1].owner == address(this)) {
126 | uint currentId = i + 1;
127 | MarketItem storage currentItem = idToMarketItem[currentId];
128 | items[currentIndex] = currentItem;
129 | currentIndex += 1;
130 | }
131 | }
132 | return items;
133 | }
134 |
135 | /* Returns only items that a user has purchased */
136 | function fetchMyNFTs() public view returns (MarketItem[] memory) {
137 | uint totalItemCount = _tokenIds.current();
138 | uint itemCount = 0;
139 | uint currentIndex = 0;
140 |
141 | for (uint i = 0; i < totalItemCount; i++) {
142 | if (idToMarketItem[i + 1].owner == msg.sender) {
143 | itemCount += 1;
144 | }
145 | }
146 |
147 | MarketItem[] memory items = new MarketItem[](itemCount);
148 | for (uint i = 0; i < totalItemCount; i++) {
149 | if (idToMarketItem[i + 1].owner == msg.sender) {
150 | uint currentId = i + 1;
151 | MarketItem storage currentItem = idToMarketItem[currentId];
152 | items[currentIndex] = currentItem;
153 | currentIndex += 1;
154 | }
155 | }
156 | return items;
157 | }
158 |
159 | /* Returns only items a user has listed */
160 | function fetchItemsListed() public view returns (MarketItem[] memory) {
161 | uint totalItemCount = _tokenIds.current();
162 | uint itemCount = 0;
163 | uint currentIndex = 0;
164 |
165 | for (uint i = 0; i < totalItemCount; i++) {
166 | if (idToMarketItem[i + 1].seller == msg.sender) {
167 | itemCount += 1;
168 | }
169 | }
170 |
171 | MarketItem[] memory items = new MarketItem[](itemCount);
172 | for (uint i = 0; i < totalItemCount; i++) {
173 | if (idToMarketItem[i + 1].seller == msg.sender) {
174 | uint currentId = i + 1;
175 | MarketItem storage currentItem = idToMarketItem[currentId];
176 | items[currentIndex] = currentItem;
177 | currentIndex += 1;
178 | }
179 | }
180 | return items;
181 | }
182 | }
--------------------------------------------------------------------------------
/hardhat.config.js:
--------------------------------------------------------------------------------
1 | require("@nomiclabs/hardhat-waffle");
2 | const fs = require('fs');
3 | // const infuraId = fs.readFileSync(".infuraid").toString().trim() || "";
4 |
5 | module.exports = {
6 | defaultNetwork: "hardhat",
7 | networks: {
8 | hardhat: {
9 | chainId: 1337
10 | },
11 | /*
12 | mumbai: {
13 | // Infura
14 | // url: `https://polygon-mumbai.infura.io/v3/${infuraId}`
15 | url: "https://rpc-mumbai.matic.today",
16 | accounts: [process.env.privateKey]
17 | },
18 | matic: {
19 | // Infura
20 | // url: `https://polygon-mainnet.infura.io/v3/${infuraId}`,
21 | url: "https://rpc-mainnet.maticvigil.com",
22 | accounts: [process.env.privateKey]
23 | }
24 | */
25 | },
26 | solidity: {
27 | version: "0.8.4",
28 | settings: {
29 | optimizer: {
30 | enabled: true,
31 | runs: 200
32 | }
33 | }
34 | }
35 | };
36 |
37 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "polygon-next",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@nomiclabs/hardhat-ethers": "^2.0.5",
13 | "@nomiclabs/hardhat-waffle": "^2.0.2",
14 | "@openzeppelin/contracts": "^4.5.0",
15 | "axios": "^0.26.0",
16 | "chai": "^4.3.6",
17 | "ethereum-waffle": "^3.4.0",
18 | "ethers": "^5.5.4",
19 | "hardhat": "^2.8.4",
20 | "ipfs-http-client": "^56.0.1",
21 | "next": "11.0.1",
22 | "react": "17.0.2",
23 | "react-dom": "17.0.2",
24 | "web3modal": "^1.9.5"
25 | },
26 | "devDependencies": {
27 | "autoprefixer": "^10.2.6",
28 | "eslint": "7.29.0",
29 | "eslint-config-next": "11.0.1",
30 | "postcss": "^8.3.5",
31 | "tailwindcss": "^2.2.4"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | /* pages/_app.js */
2 | import '../styles/globals.css'
3 | import Link from 'next/link'
4 |
5 | function MyApp({ Component, pageProps }) {
6 | return (
7 |
8 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default MyApp
--------------------------------------------------------------------------------
/pages/create-nft.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { ethers } from 'ethers'
3 | import { create as ipfsHttpClient } from 'ipfs-http-client'
4 | import { useRouter } from 'next/router'
5 | import Web3Modal from 'web3modal'
6 |
7 | const client = ipfsHttpClient('https://ipfs.infura.io:5001/api/v0')
8 |
9 | import {
10 | marketplaceAddress
11 | } from '../config'
12 |
13 | import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'
14 |
15 | export default function CreateItem() {
16 | const [fileUrl, setFileUrl] = useState(null)
17 | const [formInput, updateFormInput] = useState({ price: '', name: '', description: '' })
18 | const router = useRouter()
19 |
20 | async function onChange(e) {
21 | const file = e.target.files[0]
22 | try {
23 | const added = await client.add(
24 | file,
25 | {
26 | progress: (prog) => console.log(`received: ${prog}`)
27 | }
28 | )
29 | const url = `https://ipfs.infura.io/ipfs/${added.path}`
30 | setFileUrl(url)
31 | } catch (error) {
32 | console.log('Error uploading file: ', error)
33 | }
34 | }
35 | async function uploadToIPFS() {
36 | const { name, description, price } = formInput
37 | if (!name || !description || !price || !fileUrl) return
38 | /* first, upload to IPFS */
39 | const data = JSON.stringify({
40 | name, description, image: fileUrl
41 | })
42 | try {
43 | const added = await client.add(data)
44 | const url = `https://ipfs.infura.io/ipfs/${added.path}`
45 | /* after file is uploaded to IPFS, return the URL to use it in the transaction */
46 | return url
47 | } catch (error) {
48 | console.log('Error uploading file: ', error)
49 | }
50 | }
51 |
52 | async function listNFTForSale() {
53 | const url = await uploadToIPFS()
54 | const web3Modal = new Web3Modal()
55 | const connection = await web3Modal.connect()
56 | const provider = new ethers.providers.Web3Provider(connection)
57 | const signer = provider.getSigner()
58 |
59 | /* next, create the item */
60 | const price = ethers.utils.parseUnits(formInput.price, 'ether')
61 | let contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
62 | let listingPrice = await contract.getListingPrice()
63 | listingPrice = listingPrice.toString()
64 | let transaction = await contract.createToken(url, price, { value: listingPrice })
65 | await transaction.wait()
66 |
67 | router.push('/')
68 | }
69 |
70 | return (
71 |
104 | )
105 | }
--------------------------------------------------------------------------------
/pages/dashboard.js:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers'
2 | import { useEffect, useState } from 'react'
3 | import axios from 'axios'
4 | import Web3Modal from 'web3modal'
5 |
6 | import {
7 | marketplaceAddress
8 | } from '../config'
9 |
10 | import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'
11 |
12 | export default function CreatorDashboard() {
13 | const [nfts, setNfts] = useState([])
14 | const [loadingState, setLoadingState] = useState('not-loaded')
15 | useEffect(() => {
16 | loadNFTs()
17 | }, [])
18 | async function loadNFTs() {
19 | const web3Modal = new Web3Modal({
20 | network: 'mainnet',
21 | cacheProvider: true,
22 | })
23 | const connection = await web3Modal.connect()
24 | const provider = new ethers.providers.Web3Provider(connection)
25 | const signer = provider.getSigner()
26 |
27 | const contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
28 | const data = await contract.fetchItemsListed()
29 |
30 | const items = await Promise.all(data.map(async i => {
31 | const tokenUri = await contract.tokenURI(i.tokenId)
32 | const meta = await axios.get(tokenUri)
33 | let price = ethers.utils.formatUnits(i.price.toString(), 'ether')
34 | let item = {
35 | price,
36 | tokenId: i.tokenId.toNumber(),
37 | seller: i.seller,
38 | owner: i.owner,
39 | image: meta.data.image,
40 | }
41 | return item
42 | }))
43 |
44 | setNfts(items)
45 | setLoadingState('loaded')
46 | }
47 | if (loadingState === 'loaded' && !nfts.length) return (No NFTs listed
)
48 | return (
49 |
50 |
51 |
Items Listed
52 |
53 | {
54 | nfts.map((nft, i) => (
55 |
56 |

57 |
58 |
Price - {nft.price} Eth
59 |
60 |
61 | ))
62 | }
63 |
64 |
65 |
66 | )
67 | }
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers'
2 | import { useEffect, useState } from 'react'
3 | import axios from 'axios'
4 | import Web3Modal from 'web3modal'
5 |
6 | import {
7 | marketplaceAddress
8 | } from '../config'
9 |
10 | import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'
11 |
12 | export default function Home() {
13 | const [nfts, setNfts] = useState([])
14 | const [loadingState, setLoadingState] = useState('not-loaded')
15 | useEffect(() => {
16 | loadNFTs()
17 | }, [])
18 | async function loadNFTs() {
19 | /* create a generic provider and query for unsold market items */
20 | const provider = new ethers.providers.JsonRpcProvider()
21 | const contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, provider)
22 | const data = await contract.fetchMarketItems()
23 |
24 | /*
25 | * map over items returned from smart contract and format
26 | * them as well as fetch their token metadata
27 | */
28 | const items = await Promise.all(data.map(async i => {
29 | const tokenUri = await contract.tokenURI(i.tokenId)
30 | const meta = await axios.get(tokenUri)
31 | let price = ethers.utils.formatUnits(i.price.toString(), 'ether')
32 | let item = {
33 | price,
34 | tokenId: i.tokenId.toNumber(),
35 | seller: i.seller,
36 | owner: i.owner,
37 | image: meta.data.image,
38 | name: meta.data.name,
39 | description: meta.data.description,
40 | }
41 | return item
42 | }))
43 | setNfts(items)
44 | setLoadingState('loaded')
45 | }
46 | async function buyNft(nft) {
47 | /* needs the user to sign the transaction, so will use Web3Provider and sign it */
48 | const web3Modal = new Web3Modal()
49 | const connection = await web3Modal.connect()
50 | const provider = new ethers.providers.Web3Provider(connection)
51 | const signer = provider.getSigner()
52 | const contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
53 |
54 | /* user will be prompted to pay the asking proces to complete the transaction */
55 | const price = ethers.utils.parseUnits(nft.price.toString(), 'ether')
56 | const transaction = await contract.createMarketSale(nft.tokenId, {
57 | value: price
58 | })
59 | await transaction.wait()
60 | loadNFTs()
61 | }
62 | if (loadingState === 'loaded' && !nfts.length) return (No items in marketplace
)
63 | return (
64 |
65 |
66 |
67 | {
68 | nfts.map((nft, i) => (
69 |
70 |

71 |
72 |
{nft.name}
73 |
74 |
{nft.description}
75 |
76 |
77 |
78 |
{nft.price} ETH
79 |
80 |
81 |
82 | ))
83 | }
84 |
85 |
86 |
87 | )
88 | }
--------------------------------------------------------------------------------
/pages/my-nfts.js:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers'
2 | import { useEffect, useState } from 'react'
3 | import axios from 'axios'
4 | import Web3Modal from 'web3modal'
5 | import { useRouter } from 'next/router'
6 |
7 | import {
8 | marketplaceAddress
9 | } from '../config'
10 |
11 | import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'
12 |
13 | export default function MyAssets() {
14 | const [nfts, setNfts] = useState([])
15 | const [loadingState, setLoadingState] = useState('not-loaded')
16 | const router = useRouter()
17 | useEffect(() => {
18 | loadNFTs()
19 | }, [])
20 | async function loadNFTs() {
21 | const web3Modal = new Web3Modal({
22 | network: "mainnet",
23 | cacheProvider: true,
24 | })
25 | const connection = await web3Modal.connect()
26 | const provider = new ethers.providers.Web3Provider(connection)
27 | const signer = provider.getSigner()
28 |
29 | const marketplaceContract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
30 | const data = await marketplaceContract.fetchMyNFTs()
31 |
32 | const items = await Promise.all(data.map(async i => {
33 | const tokenURI = await marketplaceContract.tokenURI(i.tokenId)
34 | const meta = await axios.get(tokenURI)
35 | let price = ethers.utils.formatUnits(i.price.toString(), 'ether')
36 | let item = {
37 | price,
38 | tokenId: i.tokenId.toNumber(),
39 | seller: i.seller,
40 | owner: i.owner,
41 | image: meta.data.image,
42 | tokenURI
43 | }
44 | return item
45 | }))
46 | setNfts(items)
47 | setLoadingState('loaded')
48 | }
49 | function listNFT(nft) {
50 | console.log('nft:', nft)
51 | router.push(`/resell-nft?id=${nft.tokenId}&tokenURI=${nft.tokenURI}`)
52 | }
53 | if (loadingState === 'loaded' && !nfts.length) return (No NFTs owned
)
54 | return (
55 |
56 |
57 |
58 | {
59 | nfts.map((nft, i) => (
60 |
61 |

62 |
63 |
Price - {nft.price} Eth
64 |
65 |
66 |
67 | ))
68 | }
69 |
70 |
71 |
72 | )
73 | }
--------------------------------------------------------------------------------
/pages/resell-nft.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { ethers } from 'ethers'
3 | import { useRouter } from 'next/router'
4 | import axios from 'axios'
5 | import Web3Modal from 'web3modal'
6 |
7 | import {
8 | marketplaceAddress
9 | } from '../config'
10 |
11 | import NFTMarketplace from '../artifacts/contracts/NFTMarketplace.sol/NFTMarketplace.json'
12 |
13 | export default function ResellNFT() {
14 | const [formInput, updateFormInput] = useState({ price: '', image: '' })
15 | const router = useRouter()
16 | const { id, tokenURI } = router.query
17 | const { image, price } = formInput
18 |
19 | useEffect(() => {
20 | fetchNFT()
21 | }, [id])
22 |
23 | async function fetchNFT() {
24 | if (!tokenURI) return
25 | const meta = await axios.get(tokenURI)
26 | updateFormInput(state => ({ ...state, image: meta.data.image }))
27 | }
28 |
29 | async function listNFTForSale() {
30 | if (!price) return
31 | const web3Modal = new Web3Modal()
32 | const connection = await web3Modal.connect()
33 | const provider = new ethers.providers.Web3Provider(connection)
34 | const signer = provider.getSigner()
35 |
36 | const priceFormatted = ethers.utils.parseUnits(formInput.price, 'ether')
37 | let contract = new ethers.Contract(marketplaceAddress, NFTMarketplace.abi, signer)
38 | let listingPrice = await contract.getListingPrice()
39 |
40 | listingPrice = listingPrice.toString()
41 | let transaction = await contract.resellToken(id, priceFormatted, { value: listingPrice })
42 | await transaction.wait()
43 |
44 | router.push('/')
45 | }
46 |
47 | return (
48 |
49 |
50 |
updateFormInput({ ...formInput, price: e.target.value })}
54 | />
55 | {
56 | image && (
57 |

58 | )
59 | }
60 |
63 |
64 |
65 | )
66 | }
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dabit3/polygon-ethereum-nextjs-marketplace/b353632726550826dc3125be3ca20d2a787f3afb/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/deploy.js:
--------------------------------------------------------------------------------
1 | const hre = require("hardhat");
2 | const fs = require('fs');
3 |
4 | async function main() {
5 | const NFTMarketplace = await hre.ethers.getContractFactory("NFTMarketplace");
6 | const nftMarketplace = await NFTMarketplace.deploy();
7 | await nftMarketplace.deployed();
8 | console.log("nftMarketplace deployed to:", nftMarketplace.address);
9 |
10 | fs.writeFileSync('./config.js', `
11 | export const marketplaceAddress = "${nftMarketplace.address}"
12 | `)
13 | }
14 |
15 | main()
16 | .then(() => process.exit(0))
17 | .catch((error) => {
18 | console.error(error);
19 | process.exit(1);
20 | });
21 |
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | height: 100vh;
9 | }
10 |
11 | .main {
12 | padding: 5rem 0;
13 | flex: 1;
14 | display: flex;
15 | flex-direction: column;
16 | justify-content: center;
17 | align-items: center;
18 | }
19 |
20 | .footer {
21 | width: 100%;
22 | height: 100px;
23 | border-top: 1px solid #eaeaea;
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | }
28 |
29 | .footer a {
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 | flex-grow: 1;
34 | }
35 |
36 | .title a {
37 | color: #0070f3;
38 | text-decoration: none;
39 | }
40 |
41 | .title a:hover,
42 | .title a:focus,
43 | .title a:active {
44 | text-decoration: underline;
45 | }
46 |
47 | .title {
48 | margin: 0;
49 | line-height: 1.15;
50 | font-size: 4rem;
51 | }
52 |
53 | .title,
54 | .description {
55 | text-align: center;
56 | }
57 |
58 | .description {
59 | line-height: 1.5;
60 | font-size: 1.5rem;
61 | }
62 |
63 | .code {
64 | background: #fafafa;
65 | border-radius: 5px;
66 | padding: 0.75rem;
67 | font-size: 1.1rem;
68 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
69 | Bitstream Vera Sans Mono, Courier New, monospace;
70 | }
71 |
72 | .grid {
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 | flex-wrap: wrap;
77 | max-width: 800px;
78 | margin-top: 3rem;
79 | }
80 |
81 | .card {
82 | margin: 1rem;
83 | padding: 1.5rem;
84 | text-align: left;
85 | color: inherit;
86 | text-decoration: none;
87 | border: 1px solid #eaeaea;
88 | border-radius: 10px;
89 | transition: color 0.15s ease, border-color 0.15s ease;
90 | width: 45%;
91 | }
92 |
93 | .card:hover,
94 | .card:focus,
95 | .card:active {
96 | color: #0070f3;
97 | border-color: #0070f3;
98 | }
99 |
100 | .card h2 {
101 | margin: 0 0 1rem 0;
102 | font-size: 1.5rem;
103 | }
104 |
105 | .card p {
106 | margin: 0;
107 | font-size: 1.25rem;
108 | line-height: 1.5;
109 | }
110 |
111 | .logo {
112 | height: 1em;
113 | margin-left: 0.5rem;
114 | }
115 |
116 | @media (max-width: 600px) {
117 | .grid {
118 | width: 100%;
119 | flex-direction: column;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | /* ./styles/globals.css */
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: [],
3 | darkMode: false, // or 'media' or 'class'
4 | theme: {
5 | extend: {},
6 | },
7 | variants: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
12 |
--------------------------------------------------------------------------------
/test/sample-test.js:
--------------------------------------------------------------------------------
1 | /* test/sample-test.js */
2 | describe("NFTMarket", function() {
3 | it("Should create and execute market sales", async function() {
4 | /* deploy the marketplace */
5 | const NFTMarketplace = await ethers.getContractFactory("NFTMarketplace")
6 | const nftMarketplace = await NFTMarketplace.deploy()
7 | await nftMarketplace.deployed()
8 |
9 | let listingPrice = await nftMarketplace.getListingPrice()
10 | listingPrice = listingPrice.toString()
11 |
12 | const auctionPrice = ethers.utils.parseUnits('1', 'ether')
13 |
14 | /* create two tokens */
15 | await nftMarketplace.createToken("https://www.mytokenlocation.com", auctionPrice, { value: listingPrice })
16 | await nftMarketplace.createToken("https://www.mytokenlocation2.com", auctionPrice, { value: listingPrice })
17 |
18 | const [_, buyerAddress] = await ethers.getSigners()
19 |
20 | /* execute sale of token to another user */
21 | await nftMarketplace.connect(buyerAddress).createMarketSale(1, { value: auctionPrice })
22 |
23 | /* resell a token */
24 | await nftMarketplace.connect(buyerAddress).resellToken(1, auctionPrice, { value: listingPrice })
25 |
26 | /* query for and return the unsold items */
27 | items = await nftMarketplace.fetchMarketItems()
28 | items = await Promise.all(items.map(async i => {
29 | const tokenUri = await nftMarketplace.tokenURI(i.tokenId)
30 | let item = {
31 | price: i.price.toString(),
32 | tokenId: i.tokenId.toString(),
33 | seller: i.seller,
34 | owner: i.owner,
35 | tokenUri
36 | }
37 | return item
38 | }))
39 | console.log('items: ', items)
40 | })
41 | })
--------------------------------------------------------------------------------
/wallet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dabit3/polygon-ethereum-nextjs-marketplace/b353632726550826dc3125be3ca20d2a787f3afb/wallet.png
--------------------------------------------------------------------------------