├── .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 | ![Header](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pfofv47dooojerkmfgr4.png) 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 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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 | ![MetaMask RPC Import](wallet.png) 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 |
72 |
73 | updateFormInput({ ...formInput, name: e.target.value })} 77 | /> 78 |