Skip to content

Latest commit

 

History

History
570 lines (413 loc) · 17.1 KB

File metadata and controls

570 lines (413 loc) · 17.1 KB

Building a Digital Marketplace on Ethereum

This workshop will walk you through creating a digital marketplace on Ethereum.

The technologies used in this workshop are React, Next.js, Tailwind CSS, HardHat, Solidity, and Ethers.

Getting started

The first thing we need to do is write the smart contracts.

The marketplace will consist of two main smart contracts:

  • NFT Contract for minting NFTs
  • Markeplace contract for facilitating the sale of NFTs

For writing an NFT, we can use the ERC721 standard that is available via OpenZeppelin.

To get started, head over to https://remix.ethereum.org/ and create a new file called Marketplace.sol.

Here, we can get started by importing the contracts that we'll be needing to get started from Open Zeppelin:

// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.3;

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

import "hardhat/console.sol";

Creating the NFT contract

Next, let's create the NFT contract:

contract NFT is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
    address contractAddress;

    constructor(address marketplaceAddress) ERC721("Eat The Blocks NFTs", "ETBNFT") {
        contractAddress = marketplaceAddress;
    }

    function createToken(string memory tokenURI) public returns (uint) {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();

        _mint(msg.sender, newItemId);
        _setTokenURI(newItemId, tokenURI);
        setApprovalForAll(contractAddress, true);
        return newItemId;
    }
}

The contstructor takes an argument for the marketplaceAddress address, saving the value and making it available in the smart contract. This way when someone calls createToken, the contract can allow the Market contract approval to transfer the token away from the owner to the seller.

The newItemId value is returned from the function as we will be needing it in our client application to know the dynamic value of the tokenId that was generated by the smart contract.

Creating the Market contract

The Market contract is more complex than the NFT contract since we will be writing all of it from scratch.

contract NFTMarket is ReentrancyGuard {
  using Counters for Counters.Counter;
  Counters.Counter private _itemIds;
  Counters.Counter private _itemsSold;

  struct MarketItem {
    uint itemId;
    address nftContract;
    uint256 tokenId;
    address payable seller;
    address payable owner;
    uint256 price;
  }

  mapping(uint256 => MarketItem) private idToMarketItem;

  event MarketItemCreated (
    uint indexed itemId,
    address indexed nftContract,
    uint256 indexed tokenId,
    address seller,
    address owner,
    uint256 price
  );

  function getMarketItem(uint256 marketItemId) public view returns (MarketItem memory) {
    return idToMarketItem[marketItemId];
  }

  function createMarketItem(
    address nftContract,
    uint256 tokenId,
    uint256 price
  ) public payable nonReentrant {
    require(price > 0, "Price must be at least 1 wei");

    _itemIds.increment();
    uint256 itemId = _itemIds.current();
  
    idToMarketItem[itemId] =  MarketItem(
      itemId,
      nftContract,
      tokenId,
      payable(msg.sender),
      payable(address(0)),
      price
    );

    IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);

    emit MarketItemCreated(
      itemId,
      nftContract,
      tokenId,
      msg.sender,
      address(0),
      price
    );
  }

  function createMarketSale(
    address nftContract,
    uint256 itemId
    ) public payable nonReentrant {
    uint price = idToMarketItem[itemId].price;
    uint tokenId = idToMarketItem[itemId].tokenId;
    require(msg.value == price, "Please submit the asking price in order to complete the purchase");

    idToMarketItem[itemId].seller.transfer(msg.value);
    IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
    idToMarketItem[itemId].owner = payable(msg.sender);
    _itemsSold.increment();
  }

  function fetchMarketItem(uint itemId) public view returns (MarketItem memory) {
    MarketItem memory item = idToMarketItem[itemId];
    return item;
  }

  function fetchMarketItems() public view returns (MarketItem[] memory) {
    uint itemCount = _itemIds.current();
    uint unsoldItemCount = _itemIds.current() - _itemsSold.current();
    uint currentIndex = 0;

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

  function fetchMyNFTs() public view returns (MarketItem[] memory) {
    uint totalItemCount = _itemIds.current();
    uint itemCount = 0;
    uint currentIndex = 0;

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

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

There is a lot going on in this contract, so let's walk through some of it.

What is ReentrancyGuard

Inheriting from ReentrancyGuard will make the nonReentrant modifier available, which can be applied to functions to make sure there are no nested (reentrant) calls to them.

MarketItem

The MarketItem struct allows us to store records of items that we want to make available in the marketplace.

idToMarketItem

This mapping allows us to create a key value pairing between IDs and MarketItems.

createMarketItem

This function transfers an NFT to the contract address of the market, and puts the item for sale.

createMarketSale

This function enables the transfer of the NFT as well as Eth between the buyer and seller.

fetchMarketItems

This function returns all market items that are still for sale.

fetchMyNFTs

This function returns the NFTs that the user has purchased.

Building the front end

To build the front end, you can use the starting project in the marketplace_starter folder.

If you'd like to know how to bootstrap the project from scratch yourself, follow these steps
  1. Create a new Next.js app and change into the directory:
npx create-next-app marketplace-app
cd marketplace-app
  1. Install the necessary dependencies:
npm install web3modal ethers hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai \ 
@nomiclabs/hardhat-ethers axios ipfs-http-client @openzeppelin/contracts

Optional, install tailwind and dependencies:

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

npx tailwindcss init -p

update styles/globals.css with the following
@tailwind base;
@tailwind components;
@tailwind utilities;
  1. Initialize Hardhat environment
npx hardhat
? What do you want to do? Create a sample project
? Hardhat project root: <Choose default path>
  1. Open hardhat.config.js and update the module.exports to look like this:
module.exports = {
  solidity: "0.8.3",
  paths: {
    artifacts: './src/artifacts',
  },
  networks: {
    hardhat: {
      chainId: 1337
    }
  }
};

Dowloading and configuring the project

Clone the repo, then change into the marketplace-starter directory, and install the dependencies:

git clone https://github.com/dabit3/full-stack-ethereum-marketplace-workshop.git

cd full-stack-ethereum-marketplace-workshop/marketplace-starter

yarn

# or

npm install

Next, take a look at the tests in test/test.js. Here, we can mock the functionality that we'll need for our project.

To run the test and see the output, run the following command:

npx hardhat test

The smart contracts

The smart contracts are located in the contracts directory.

To use the smart contracts in our front-end (React) applications, we'll need to compile the contract into ABIs.

The Hardhat development environment allows us to do this using the Hardhat CLI.

We can define the location of the artifacts output by configuring its location in hardhat.config.js along with any other properties for our Hardhat development environment.

In our project, the location is set to ./artifacts in the root directory.

Writing the deployment script

Next, let's write the script to deploy the contracts.

To do so, open scripts/deploy.js and add the following to the main function body:

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

  const NFT = await hre.ethers.getContractFactory("NFT");
  const nft = await NFT.deploy(nftMarket.address);
  await nft.deployed();
  console.log("nft deployed to:", nft.address);
}

Running a local node and deploying the contract

Next, let's run a local node and deploy our contract to the node.

To do so, open your terminal and run the following command:

npx hardhat node

This should create and launch a local node.

Next, open another terminal and deploy the contracts:

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

When this completes successfully, you should have the addresses printed out to the console for your smart contract deployments.

Take those values and update the configuration in config.js:

export const nftmarketaddress = "market-contract-address"
export const nftaddress = "nft-contract-address"

Running the app

Now, we should be able to run the app.

To do so, open the terminal and run the following command:

npm run dev

Building the UI

Fetching NFTs

The first piece of functionality we'll implement will be fetching NFTs from the marketplace and rendering them to the UI.

To do so, open pages/index.js and update the loadNFTs function with the following:

async function loadNFTs() {
  const provider = new ethers.providers.JsonRpcProvider()
  const tokenContract = new ethers.Contract(nftaddress, NFT.abi, provider)
  const marketContract = new ethers.Contract(nftmarketaddress, Market.abi, provider)
  const data = await marketContract.fetchMarketItems()
  
  const items = await Promise.all(data.map(async i => {
    const tokenUri = await tokenContract.tokenURI(i.tokenId)
    const meta = await axios.get(tokenUri)
    let price = web3.utils.fromWei(i.price.toString(), 'ether');
    let item = {
      price,
      tokenId: i.tokenId.toNumber(),
      seller: i.seller,
      owner: i.owner,
      image: meta.data.image,
    }
    return item
  }))
  setNfts(items)
  setLoaded('loaded') 
}

The loadNFTs function calls the fetchMarketItems function in our smart contract and returns all unsold NFTs.

We then map over each NFT to transform the response into something more readable for the user interface.

Creating an NFT and placing it for sale

Next, let's create the function for minting the NFT and putting it for sale in the market.

To do so, open pages/create-item.js and update the createSale function with the following code:

async function createSale(url) {
  const web3Modal = new Web3Modal({
    network: "mainnet",
    cacheProvider: true,
  });
  const connection = await web3Modal.connect()
  const provider = new ethers.providers.Web3Provider(connection)    
  const signer = provider.getSigner()
  
  let contract = new ethers.Contract(nftaddress, NFT.abi, signer)
  let transaction = await contract.createToken(url)
  let tx = await transaction.wait()
  let event = tx.events[0]
  let value = event.args[2]
  let tokenId = value.toNumber()
  const price = web3.utils.toWei(formInput.price, 'ether')

  contract = new ethers.Contract(nftmarketaddress, Market.abi, signer)
  transaction = await contract.createMarketItem(nftaddress, tokenId, price)
  await transaction.wait()
  router.push('/')
}

This function writes two transactions to the network:

createToken mints the new token.

createMarketItem places the item for sale.

Once the item has been crated, we redict the user back to the main page.

Allowing a user to purchase an NFT

Next, let's create the function for allowing a user to purchase an NFT.

To do so, open pages/index.js and update the buyNft function with the following code:

async function buyNft(nft) {
  const web3Modal = new Web3Modal({
    network: "mainnet",
    cacheProvider: true,
  });
  const connection = await web3Modal.connect()
  const provider = new ethers.providers.Web3Provider(connection)
  const signer = provider.getSigner()
  const contract = new ethers.Contract(nftmarketaddress, Market.abi, signer)
  
  const price = web3.utils.toWei(nft.price.toString(), 'ether');
  
  const transaction = await contract.createMarketSale(nftaddress, nft.tokenId, {
    value: price
  })
  await transaction.wait()
  loadNFTs()
}

This function writes the createMarketSale to the network, allowing the user to transfer the amount from their wallet to the seller's wallet.

Allowing a user to view their own NFTs

The last piece of functionality we want to implement is to give users the ability to view the NFTs they have purchased. To do so, we'll be calling the fetchMyNFTs function from the smart contract.

Open pages/my-nfts.js and update the loadNFTs function with the following:

async function loadNFTs() {
  const web3Modal = new Web3Modal({
    network: "mainnet",
    cacheProvider: true,
  });
  const connection = await web3Modal.connect()
  const provider = new ethers.providers.Web3Provider(connection)
  const signer = provider.getSigner()
    
  const marketContract = new ethers.Contract(nftmarketaddress, Market.abi, signer)
  const tokenContract = new ethers.Contract(nftaddress, NFT.abi, provider)
  const data = await marketContract.fetchMyNFTs()
  
  const items = await Promise.all(data.map(async i => {
    const tokenUri = await tokenContract.tokenURI(i.tokenId)
    const meta = await axios.get(tokenUri)
    let price = web3.utils.fromWei(i.price.toString(), 'ether');
    let item = {
      price,
      tokenId: i.tokenId.toNumber(),
      seller: i.seller,
      owner: i.owner,
      image: meta.data.image,
    }
    return item
  }))

  setNfts(items)
  setLoaded('loaded') 
}

Enabling listing fees

Next, we want the operator of the marketplace to collect listing fees. We can add this functionality with only a few lines of code.

First, open the NFTMarket.sol contract located in the contracts directory.

Here, we will set a listing price that you want to be using. We will also, create a variable that we can use to store the owner of the contract.

Add the following lines of code below the _itemsSold variable initialization:

address payable owner;
uint256 listingPrice = 0.01 ether;

Next, create a constructor to store the contract owner's address:

constructor() {
  owner = payable(msg.sender);
}

In the createMarketItem function, set a check to make sure that the listing fee has been passed into the transaction:

require(price > 0, "Price must be at least 1 wei"); // below this line 👇
require(msg.value == listingPrice, "Price must be equal to listing price");

Finally, in the createMarketSale function, send the listingPrice value to the contract owner once the sale is completed. This can go at the end of the function:

payable(owner).transfer(listingPrice);

Next, in the client-side code, open pages/create-item.js and add the payment value to be sent along with the transaction in the createSale function:

// above code omitted
const listingPrice = web3.utils.toWei('0.01', 'ether')

contract = new ethers.Contract(nftmarketaddress, Market.abi, signer)
transaction = await contract.createMarketItem(nftaddress, tokenId, price, { value: listingPrice })
// below code omitted

Conclusion

The project should now be complete. You should be able to create, view, and purchase NFTs from the marketplace.

To view a completed version of the project, check out the code located in the marketplace-complete folder.