SEI ZK Series Part 10 - How to build a fullstack nft market place on the sei blockchain

Building a Fullstack NFT Marketplace on Sei Blockchain

Introduction

The Sei blockchain is a high-performance Layer 1 blockchain designed for trading applications. With its EVM compatibility, we can leverage familiar Ethereum development tools while benefiting from Sei's high throughput and low latency. In this tutorial, we'll build a complete NFT marketplace that allows users to:

  • Mint NFTs

  • List NFTs for sale

  • Buy NFTs

  • View listed NFTs

  • Manage their NFT collection

Prerequisites

Before we begin, ensure you have the following installed:

  • Node.js (v16 or higher)

  • npm or yarn

  • MetaMask wallet

  • Git

Project Setup

Let's start by setting up our development environment:

  1. Create a new directory and initialize the project:
mkdir sei-nft-marketplace
cd sei-nft-marketplace
npm init -y
  1. Install Hardhat and required dependencies:
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts dotenv
  1. Initialize Hardhat:
npx hardhat init

Choose "Create a JavaScript project" when prompted.

  1. Create a .env file in the root directory:
PRIVATE_KEY=your_wallet_private_key
SEI_RPC_URL=your_sei_rpc_url

Smart Contract Development

We'll create two main smart contracts:

  1. NFT Contract (ERC721)

  2. Marketplace Contract

1. NFT Contract

Create a new file contracts/SeiNFT.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract SeiNFT is ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721("SeiNFT", "SEI") Ownable(msg.sender) {}

    function mintNFT(address recipient, string memory tokenURI)
        public
        returns (uint256)
    {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);
        return newItemId;
    }
}

2. Marketplace Contract

Create a new file contracts/NFTMarketplace.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract NFTMarketplace is ReentrancyGuard, Ownable {
    struct Listing {
        address seller;
        uint256 price;
        bool isActive;
    }

    // Mapping from NFT contract address to token ID to Listing
    mapping(address => mapping(uint256 => Listing)) public listings;

    // Platform fee percentage (2%)
    uint256 public platformFee = 200;
    uint256 public constant BASIS_POINTS = 10000;

    event NFTListed(
        address indexed nftContract,
        uint256 indexed tokenId,
        address indexed seller,
        uint256 price
    );

    event NFTSold(
        address indexed nftContract,
        uint256 indexed tokenId,
        address indexed seller,
        address buyer,
        uint256 price
    );

    event NFTListingCancelled(
        address indexed nftContract,
        uint256 indexed tokenId,
        address indexed seller
    );

    constructor() Ownable(msg.sender) {}

    function listNFT(
        address nftContract,
        uint256 tokenId,
        uint256 price
    ) external nonReentrant {
        require(price > 0, "Price must be greater than 0");
        require(
            IERC721(nftContract).ownerOf(tokenId) == msg.sender,
            "Not the owner"
        );
        require(
            IERC721(nftContract).isApprovedForAll(msg.sender, address(this)),
            "Contract not approved"
        );

        listings[nftContract][tokenId] = Listing({
            seller: msg.sender,
            price: price,
            isActive: true
        });

        emit NFTListed(nftContract, tokenId, msg.sender, price);
    }

    function buyNFT(address nftContract, uint256 tokenId)
        external
        payable
        nonReentrant
    {
        Listing memory listing = listings[nftContract][tokenId];
        require(listing.isActive, "Listing not active");
        require(msg.value >= listing.price, "Insufficient payment");

        uint256 platformFeeAmount = (listing.price * platformFee) / BASIS_POINTS;
        uint256 sellerAmount = listing.price - platformFeeAmount;

        // Transfer NFT to buyer
        IERC721(nftContract).transferFrom(
            listing.seller,
            msg.sender,
            tokenId
        );

        // Transfer payment to seller
        (bool sellerTransfer, ) = payable(listing.seller).call{
            value: sellerAmount
        }("");
        require(sellerTransfer, "Seller transfer failed");

        // Transfer platform fee to owner
        (bool feeTransfer, ) = payable(owner()).call{
            value: platformFeeAmount
        }("");
        require(feeTransfer, "Fee transfer failed");

        // Clear listing
        delete listings[nftContract][tokenId];

        emit NFTSold(
            nftContract,
            tokenId,
            listing.seller,
            msg.sender,
            listing.price
        );
    }

    function cancelListing(address nftContract, uint256 tokenId)
        external
        nonReentrant
    {
        Listing memory listing = listings[nftContract][tokenId];
        require(listing.seller == msg.sender, "Not the seller");
        require(listing.isActive, "Listing not active");

        delete listings[nftContract][tokenId];

        emit NFTListingCancelled(nftContract, tokenId, msg.sender);
    }

    function updatePlatformFee(uint256 newFee) external onlyOwner {
        require(newFee <= 1000, "Fee too high"); // Max 10%
        platformFee = newFee;
    }
}

Frontend Development

For the frontend, we'll use React with ethers.js. Let's set up the frontend project:

  1. Create a new React project:
npx create-react-app frontend
cd frontend
npm install ethers@5.7.2 @web3-react/core @web3-react/injected-connector axios
  1. Create the main components:

App.js

import React from "react";
import { Web3ReactProvider } from "@web3-react/core";
import { ethers } from "ethers";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Navbar from "./components/Navbar";
import Home from "./components/Home";
import CreateNFT from "./components/CreateNFT";
import MyNFTs from "./components/MyNFTs";

function getLibrary(provider) {
  return new ethers.providers.Web3Provider(provider);
}

function App() {
  return (
    <Web3ReactProvider getLibrary={getLibrary}>
      <Router>
        <div className="App">
          <Navbar />
          <Switch>
            <Route exact path="/" component={Home} />
            <Route path="/create" component={CreateNFT} />
            <Route path="/my-nfts" component={MyNFTs} />
          </Switch>
        </div>
      </Router>
    </Web3ReactProvider>
  );
}

export default App;

components/Navbar.js

import React from "react";
import { useWeb3React } from "@web3-react/core";
import { InjectedConnector } from "@web3-react/injected-connector";
import { Link } from "react-router-dom";

const injected = new InjectedConnector({
  supportedChainIds: [713715], // Sei Testnet
});

function Navbar() {
  const { active, account, activate, deactivate } = useWeb3React();

  async function connect() {
    try {
      await activate(injected);
    } catch (error) {
      console.log(error);
    }
  }

  async function disconnect() {
    try {
      deactivate();
    } catch (error) {
      console.log(error);
    }
  }

  return (
    <nav className="navbar">
      <div className="nav-brand">
        <Link to="/">Sei NFT Marketplace</Link>
      </div>
      <div className="nav-links">
        <Link to="/">Home</Link>
        <Link to="/create">Create NFT</Link>
        <Link to="/my-nfts">My NFTs</Link>
      </div>
      <div className="nav-connect">
        {active ? (
          <>
            <span>{account}</span>
            <button onClick={disconnect}>Disconnect</button>
          </>
        ) : (
          <button onClick={connect}>Connect Wallet</button>
        )}
      </div>
    </nav>
  );
}

export default Navbar;

components/CreateNFT.js

import React, { useState } from "react";
import { useWeb3React } from "@web3-react/core";
import { ethers } from "ethers";
import NFTMarketplaceABI from "../contracts/NFTMarketplace.json";
import SeiNFTABI from "../contracts/SeiNFT.json";

const NFTMarketplaceAddress = "YOUR_MARKETPLACE_CONTRACT_ADDRESS";
const SeiNFTAddress = "YOUR_NFT_CONTRACT_ADDRESS";

function CreateNFT() {
  const { library, account } = useWeb3React();
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [price, setPrice] = useState("");
  const [file, setFile] = useState(null);
  const [loading, setLoading] = useState(false);

  async function uploadToIPFS() {
    // Implement IPFS upload logic here
    // Return the IPFS hash
  }

  async function mintNFT(e) {
    e.preventDefault();
    if (!library || !account) return;

    try {
      setLoading(true);
      const ipfsHash = await uploadToIPFS();

      const signer = library.getSigner();
      const nftContract = new ethers.Contract(
        SeiNFTAddress,
        SeiNFTABI.abi,
        signer
      );

      const tx = await nftContract.mintNFT(account, ipfsHash);
      await tx.wait();

      alert("NFT minted successfully!");
    } catch (error) {
      console.error("Error minting NFT:", error);
      alert("Error minting NFT");
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="create-nft">
      <h2>Create New NFT</h2>
      <form onSubmit={mintNFT}>
        <div>
          <label>Name:</label>
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            required
          />
        </div>
        <div>
          <label>Description:</label>
          <textarea
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            required
          />
        </div>
        <div>
          <label>Price (in SEI):</label>
          <input
            type="number"
            value={price}
            onChange={(e) => setPrice(e.target.value)}
            required
          />
        </div>
        <div>
          <label>Image:</label>
          <input
            type="file"
            onChange={(e) => setFile(e.target.files[0])}
            required
          />
        </div>
        <button type="submit" disabled={loading}>
          {loading ? "Minting..." : "Mint NFT"}
        </button>
      </form>
    </div>
  );
}

export default CreateNFT;

Testing and Deployment

  1. Create a test file test/NFTMarketplace.test.js:
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("NFTMarketplace", function () {
  let nftMarketplace;
  let seiNFT;
  let owner;
  let seller;
  let buyer;

  beforeEach(async function () {
    [owner, seller, buyer] = await ethers.getSigners();

    const SeiNFT = await ethers.getContractFactory("SeiNFT");
    seiNFT = await SeiNFT.deploy();
    await seiNFT.deployed();

    const NFTMarketplace = await ethers.getContractFactory("NFTMarketplace");
    nftMarketplace = await NFTMarketplace.deploy();
    await nftMarketplace.deployed();
  });

  it("Should allow listing and buying NFTs", async function () {
    // Mint NFT
    const tokenURI = "ipfs://Qm...";
    await seiNFT.connect(seller).mintNFT(seller.address, tokenURI);

    // Approve marketplace
    await seiNFT
      .connect(seller)
      .setApprovalForAll(nftMarketplace.address, true);

    // List NFT
    const price = ethers.utils.parseEther("1.0");
    await nftMarketplace.connect(seller).listNFT(seiNFT.address, 1, price);

    // Buy NFT
    await nftMarketplace.connect(buyer).buyNFT(seiNFT.address, 1, {
      value: price,
    });

    // Verify ownership
    expect(await seiNFT.ownerOf(1)).to.equal(buyer.address);
  });
});
  1. Deploy the contracts:

Create a deployment script scripts/deploy.js:

const hre = require("hardhat");

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

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

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
  1. Update hardhat.config.js:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

module.exports = {
  solidity: "0.8.20",
  networks: {
    seiTestnet: {
      url: process.env.SEI_RPC_URL,
      accounts: [process.env.PRIVATE_KEY],
    },
  },
};
  1. Deploy to Sei Testnet:
npx hardhat run scripts/deploy.js --network seiTestnet

Conclusion

In this tutorial, we've built a complete NFT marketplace on the Sei blockchain. We've covered:

  1. Setting up the development environment

  2. Creating and deploying smart contracts

  3. Building a React frontend

  4. Implementing core marketplace features

  5. Testing and deployment

The marketplace includes essential features like:

  • NFT minting

  • Listing NFTs for sale

  • Buying NFTs

  • Managing listings

  • Platform fees

To enhance this marketplace further, you could add:

  • Advanced search and filtering

  • Auction functionality

  • Collection management

  • Social features

  • Analytics dashboard

Additional Resources

Subscribe to 4undRaiser
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.