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
Before we begin, ensure you have the following installed:
Node.js (v16 or higher)
npm or yarn
MetaMask wallet
Git
Let's start by setting up our development environment:
mkdir sei-nft-marketplace
cd sei-nft-marketplace
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts dotenv
npx hardhat init
Choose "Create a JavaScript project" when prompted.
.env
file in the root directory:PRIVATE_KEY=your_wallet_private_key
SEI_RPC_URL=your_sei_rpc_url
We'll create two main smart contracts:
NFT Contract (ERC721)
Marketplace 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;
}
}
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;
}
}
For the frontend, we'll use React with ethers.js. Let's set up the frontend project:
npx create-react-app frontend
cd frontend
npm install ethers@5.7.2 @web3-react/core @web3-react/injected-connector axios
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;
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;
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;
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);
});
});
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);
});
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],
},
},
};
npx hardhat run scripts/deploy.js --network seiTestnet
In this tutorial, we've built a complete NFT marketplace on the Sei blockchain. We've covered:
Setting up the development environment
Creating and deploying smart contracts
Building a React frontend
Implementing core marketplace features
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