paint-brush
Build Your Own Exciting NFT Marketplace With This Codeby@heydamali
142 reads

Build Your Own Exciting NFT Marketplace With This Code

by Kingsley OkonkwoDecember 3rd, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Learn how to build an NFT marketplace on the Linea blockchain.
featured image - Build Your Own Exciting NFT Marketplace With This Code
Kingsley Okonkwo HackerNoon profile picture

The rise of NFTs (Non-Fungible Tokens) created a new paradigm in the digital economy, revolutionizing how assets such as art, music, and collectibles are bought and sold. Building an NFT marketplace is foundational for developers looking to facilitate these transactions. Linea—a zkEVM rollup by Consensys—offers a robust and scalable environment to bring these projects to life.


This guide will explore how to develop and deploy a fully functional NFT marketplace on Linea. By the end of this guide, you’ll know how to create an innovative NFT marketplace and leverage Linea’s powerful blockchain solutions to cater to the growing demand for NFT trading.

Prerequisite

  • It would be best if you had a basic understanding of Solidity
  • You should have Nodejs and Foundry installed on your PC.

Overview of the Contract

Listing NFTs for Sale - Users can list their NFTs with a specified price, which gets stored securely in the contract.

Buying NFTs - Buyers can purchase listed NFTs by sending the required Ether, ensuring proper value exchange.

Batch Buying - Allows multiple NFTs to be purchased in a single transaction, optimizing for gas efficiency.

Delisting NFTs - NFT owners can delist their tokens and return them to their wallets.

Project Setup

Run the command below to initiate a foundry project. We will be using the Foundry framework to build the smart contract:


forge init market


Open the market folder on Vscode or your favorite code editor, and delete the scripts/counter.s.sol, src/counter.sol, and test/counter.t.sol


Install all dependencies


forge install foundry-rs/forge-std --no-commit &&  forge install OpenZeppelin/openzeppelin-contracts --no-commit

Full code of the NFT marketplace smart contract

Create a market.sol file in the src folder and add the code below to the file:


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "lib/openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {Test, console} from "forge-std/Test.sol";
contract NFTMarketplace  {
    struct Listing {
        address seller;
        uint256 price;
    }
    // NFT Contract => Token ID => Listing
    mapping(address => mapping(uint256 => Listing)) public listings;
    event Listed(address indexed nftContract, uint256 indexed tokenId, address seller, uint256 price);
    event Purchase(address indexed nftContract, uint256 indexed tokenId, address buyer, uint256 price);
    event Delisted(address indexed nftContract, uint256 indexed tokenId, address seller);
    modifier isSeller(address nftContract, uint256 tokenId, address spender) {
        require(listings[address(nftContract)][tokenId].seller == spender, "Not the seller");
        _;
    }
    modifier isListed(address nftContract, uint256 tokenId) {
        require(listings[nftContract][tokenId].price > 0, "Not listed");
        _;
    }
    modifier notListed(address nftContract, uint256 tokenId) {
        require(listings[nftContract][tokenId].price == 0, "Already listed");
        _;
    }
    /**
     * @dev List an NFT for sale.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to list.
     * @param price The price of the NFT in wei.
     */
    function listNFT(address nftContract, uint256 tokenId, uint256 price)
        external
        notListed(nftContract, tokenId)
    {
        require(price > 0, "Price must be greater than zero");
        require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "not owner");
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
        listings[nftContract][tokenId] = Listing(msg.sender, price);
        emit Listed(nftContract, tokenId, msg.sender, price);
    }
    /**
     * @dev Buy a listed NFT.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to buy.
     */
    function buyNFT(address nftContract, uint256 tokenId)
        public
        payable
        isListed(nftContract, tokenId)
    {
        Listing memory listing = listings[nftContract][tokenId];
        require(msg.value >= listing.price, "Insufficient funds");
        // Remove listing
        delete listings[nftContract][tokenId];
        // Transfer payment to the seller
        (bool sent, ) = listing.seller.call{value: listing.price}("");
        require(sent, "Failed to send Ether");
        // Transfer NFT to the buyer
        IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
        emit Purchase(nftContract, tokenId, msg.sender, listing.price);
    }

    function buyMultipleNFT(address nftContract, uint256 [] memory tokenIds)
        external
        payable
    {
        uint256 totalAmount;
        for (uint256 i; i < tokenIds.length; i++){
           Listing memory listing = listings[nftContract][tokenIds[i]];
           totalAmount += listing.price;
        }
        require(msg.value >= totalAmount, "Insufficient funds");
        for (uint256 i; i < tokenIds.length; i++){
           buyNFT(nftContract, tokenIds[i]);   
        }
    }
    /**
     * @dev Delist an NFT from the marketplace.
     * @param nftContract The address of the NFT contract.
     * @param tokenId The ID of the token to delist.
     */
    function delistNFT(address nftContract, uint256 tokenId)
        external
        isListed(nftContract, tokenId)
        isSeller(nftContract, tokenId, msg.sender)
    {
        Listing memory listing = listings[nftContract][tokenId];
        
        // Remove listing
        delete listings[nftContract][tokenId];
        // Return NFT to the seller
        IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
        emit Delisted(nftContract, tokenId, listing.seller);
    }
}

Key Features of the Code

Listing Struct


This is foundational for implementing features like listing, buying, and delisting in a decentralized NFT marketplace. It ensures a straightforward and efficient way to manage the listing. It enables the marketplace to record each item’s seller and price, which are fundamental for marketplace operations.

struct Listing {
        address seller; // NFT seller
        uint256 price;  // NFT price
    }


Listing Mapping


This line of code defines a nested mapping in Solidity. It is used to organize and store listings for the NFT marketplace. The outer mapping uses the address of an NFT contract as the key. This allows the smart contract to categorize listings by their associated NFT contract addresses, ensuring multiple NFT collections can be handled.


The inner mapping uses the token ID (a unique identifier for each NFT) as the key. This allows the marketplace to efficiently handle listings for multiple tokens under a single NFT contract.


 // NFT Contract => Token ID => Listing
    mapping(address => mapping(uint256 => Listing)) public listings;


NFT Listing


The listNFT function allows users to list their nft for sale on the nft marketplace. This function takes in three parameters, nftcontract, tokenId and price, the nftcontract is the address of the nft collection from which the user wants to list, the tokenId is the uinque id of the user’s nft in the collection and the price is the amount in which the user wants to sell the nft.


The function validates that the user owns the nft and ensures that the user enters a price greater than zero. After all validations, the nft is transferred from the seller to the nft marketplace, the listing mapping is updated, and the Listed event is emitted with the correct parameters.


function listNFT(address nftContract, uint256 tokenId, uint256 price)
        external
        notListed(nftContract, tokenId)
    {
        require(price > 0, "Price must be greater than zero");
        require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "not owner");
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);
        listings[nftContract][tokenId] = Listing(msg.sender, price);
        emit Listed(nftContract, tokenId, msg.sender, price);
    }


Buying NFT


The buyNFT function enables users to purchase an NFT listed on the marketplace. It takes two parameters: nftContract (the address of the NFT's smart contract) and tokenId (the unique identifier of the NFT being purchased).


The function first validates that the buyer has sent sufficient Ether to cover the NFT's price. Once the validation is successful, the listing entry for the NFT is removed from the listings mapping. The payment is then securely transferred to the seller, and the NFT is transferred from the marketplace contract to the buyer's address, completing the purchase.


function buyNFT(address nftContract, uint256 tokenId)
        public
        payable
        isListed(nftContract, tokenId)
    {
        Listing memory listing = listings[nftContract][tokenId];
        require(msg.value >= listing.price, "Insufficient funds");
        // Remove listing
        delete listings[nftContract][tokenId];
        // Transfer payment to the seller
        (bool sent, ) = listing.seller.call{value: listing.price}("");
        require(sent, "Failed to send Ether");
        // Transfer NFT to the buyer
        IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
        emit Purchase(nftContract, tokenId, msg.sender, listing.price);
    }


Buying Multiple NFTs


The buyMultipleNFT function allows a user to purchase multiple NFTs in a single transaction. It first calculates the total price for all the NFTs by summing the prices of the individual NFTs listed in the tokenIds array. The function then checks if the buyer has sent sufficient funds to cover the total price.


Once the funds are validated, the function iterates through the tokenIds array, calling the buyNFT function for each NFT in the list. This ensures that each NFT is purchased individually, with payment being sent to the seller and the NFTs transferred to the buyer, similar to the process in the buyNFT function described above.


function buyMultipleNFT(address nftContract, uint256 [] memory tokenIds)
        external
        payable
    {
        uint256 totalAmount;
        for (uint256 i; i < tokenIds.length; i++){
           Listing memory listing = listings[nftContract][tokenIds[i]];
           totalAmount += listing.price;
        }
        require(msg.value >= totalAmount, "Insufficient funds");
        for (uint256 i; i < tokenIds.length; i++){
           buyNFT(nftContract, tokenIds[i]);   
        }
    }


Delisting NFTs


The delistNFT function allows a seller to remove their NFT from the marketplace. It first verifies that the NFT is listed and that the caller is the seller of the NFT. After the validation, it deletes the listing from the marketplace, effectively removing it from the sale.


The function then transfers the NFT back to the seller, ensuring that the seller regains possession of their asset. The event Delisted is emitted to notify that the NFT has been delisted successfully.


function delistNFT(address nftContract, uint256 tokenId)
        external
        isListed(nftContract, tokenId)
        isSeller(nftContract, tokenId, msg.sender)
    {
        Listing memory listing = listings[nftContract][tokenId];
        
        // Remove listing
        delete listings[nftContract][tokenId];
        // Return NFT to the seller
        IERC721(nftContract).transferFrom(address(this), listing.seller, tokenId);
        emit Delisted(nftContract, tokenId, listing.seller);
    }


Key Events


The contract emits events for critical actions:

  • Listed: Triggered when an NFT is listed, providing details such as the seller, price, and NFT details.
  • Purchase: Captures the buyer’s purchase details, aiding in transaction traceability.
  • Delisted: Indicates that a listing was successfully removed, ensuring transparency.

Tests

Create a NFT.sol file in the src folder and add the code below to the file.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";
import "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract NFT is ERC721, Ownable {
    uint256 private _tokenIdCounter;
    string private _baseTokenURI;
    event Minted(address indexed to, uint256 tokenId);
    constructor(string memory name, string memory symbol, string memory baseTokenURI) ERC721(name, symbol) Ownable(msg.sender) {
        _baseTokenURI = baseTokenURI;
    }
    /// @dev Function to mint a new NFT
    function mint(address to) external onlyOwner {
        uint256 tokenId = _tokenIdCounter;
        _tokenIdCounter += 1;
        _safeMint(to, tokenId);
        emit Minted(to, tokenId);
    }
    /// @dev Override base token URI for metadata
    function _baseURI() internal view override returns (string memory) {
        return _baseTokenURI;
    }
    /// @dev Function to update the base token URI
    function setBaseTokenURI(string memory newBaseTokenURI) external onlyOwner {
        _baseTokenURI = newBaseTokenURI;
    }
    /// @dev Function to retrieve the current token ID count
    function getTokenCount() external view returns (uint256) {
        return _tokenIdCounter;
    }
}


Create a market.t.sol file in the test folder and add the code below to the file:


// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {NFTMarketplace} from "../src/Market.sol";
import {NFT} from "../src/NFT.sol";
contract MarketTest is Test {
    NFTMarketplace public market;
    NFT public nft;
    address public user1 = makeAddr("user1");
    address public user2 = makeAddr("user2");
    address public user3 = makeAddr("user3");
    function setUp() public {
        nft = new NFT("TestNFT", "tnft", "");
        market = new NFTMarketplace();
        nft.mint(user1);
        nft.mint(user1);
        nft.mint(user1);
    }
    function test_Listing() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        market.listNFT(address(nft), 0, 1 ether);
       
    }

     function test_BuyingNFT() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        market.listNFT(address(nft), 0, 1 ether);

        vm.startPrank(user2);
        deal(user2, 10 ether);
        market.buyNFT{value: 1 ether}(address(nft), 0);
        assert(nft.ownerOf(0) == user2);
       
    }

      function test_BuyingMultipleNFT() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        nft.approve(address(market), 1);
        nft.approve(address(market), 2);
        market.listNFT(address(nft), 0, 1 ether);
        market.listNFT(address(nft), 1, 1 ether);
        market.listNFT(address(nft), 2, 1 ether);

        vm.startPrank(user2);
        deal(user2, 10 ether);
        uint256 [] memory nfts = new uint256[] (3);
        nfts[0] = 0;
        nfts[1] = 1;
        nfts[2] = 2; 
        market.buyMultipleNFT{value: 3 ether}(address(nft), nfts);     
    }

     function test_DelistingNFT() public {
        vm.startPrank(user1);
        nft.approve(address(market), 0);
        market.listNFT(address(nft), 0, 1 ether);
        assert(nft.ownerOf(0) == address(market));
        market.delistNFT(address(nft), 0);
        assert(nft.ownerOf(0) == address(user1));
       
    }
}


The test suite above ensures that the NTF marketplace contract operates as intended across its core functionalities. Here’s a breakdown of the tests:


Setting Up the Test Environment


The setUp function initializes the environment by deploying both NFTMarketplace and NFT contracts. It mints three NFTs and assigns them to user1which will be used for testing. A mock NFT contract (NFT) is deployed with the mint function to generate NFTs. The marketplace contract (NFTMarketplace) is instantiated user1 owns all the minted NFTs, making it possible to test listing and delisting functionalities.


Test Listing


The test_Listing function ensures that user1 can list an NFT for sale on the marketplace, user1 approves the marketplace to transfer NFT tokenID0. The NFT is listed for one ether. The test ensures the listNFT the function works correctly, and the NFT is transferred to the marketplace.


Test buying a Single NFT


The test_BuyingNFT function simulates the purchase of a listed NFT by user2, user1 lists NFT tokenID0 for one ether, user2 is funded with ten ether, and calls the buyNFT function, sending one ether to purchase the NFT. After the purchase, the owner of NFT tokenID0 is verified to be user2.


Test Buying Multiple NFTs


The test_BuyingMultipleNFT function tests the ability to buy multiple NFTs in a single transaction, user1 lists three NFTs with tokenIds (0, 1, 2) for one ether each.


user2 calls the buyMultipleNFT function, passing the token IDs and sending three ether.

The test ensures that user2 it becomes the owner of all three NFTs and that the marketplace handles multiple purchases correctly.


Test Delisting an NFT


The test_DelistingNFT the function verifies that user1 they can delist their NFT from the marketplace. user1 lists NFT tokenID0 for one ether and calls the delistNFT function. The NFT is returned to user1 , and ownership of the NFT is confirmed to have reverted to user1.


How to run Tests


forge test

Deployment and Verification on Linea

Add the necessary variables to .env. The .env file should be at the root level:


LINEA_RPC_URL=https://linea-sepolia.blockpi.network/v1/rpc/public
PRIVATE_KEY=*************************************************
LINEA_API_KEY=*************************


Add the details below to foundry.toml:


[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }


Create a Makefile and add the details below:


deploy:
    forge create src/Market.sol:NFTMarketplace --rpc-url $(LINEA_RPC_URL) --private-key $(PRIVATE_KEY)

verify:; forge verify-contract --rpc-url $(LINEA_RPC_URL) --chain linea <contract address> src/Market.sol:NFTMarketplace


Deployment:


make deploy


Verification:


make verify

Conclusion

Building an NFT marketplace is an exciting and impactful way to engage with the growing world of decentralized applications and Web3. By leveraging smart contract platforms like Linea and modern development tools like Foundry and Solidity, you can create a robust platform where users can trade digital assets securely and transparently. This guide provided a comprehensive overview, including the core features of listing, buying, and delisting NFTs.