Creating an NFT Marketplace with Solidity and JavaScript

5/5 - (1 vote)

This tutorial shows you how to implement your own prototype of an NFT marketplace with Solidity and JavaScript.

You can check out the code on our GitHub:

For additional help, you can find a full course with explainer videos on the Finxter Computer Science Academy:

Feel free to check out the full course to build your own first NFT marketplace prototype! 🙂

How does it look? Here’s one of the videos showing you how the NFT marketplace prototype looks:

Now you know what we’ll build. Let’s get started with the meat!

Quick Introduction to NFT

Let’s start by understanding a few key concepts of NFTs.

Tokens

In the real world, we use tokens everywhere.

The best example would be when you visit a pub with your friends to have a mug of beer. At the counter, you pay the fiat currency, and, in return, you get a token. You have to show this token at the pub entrance to get an entry.

Another real-life example would be when you visit a supermarket and get a token after depositing the belongings at the entrance, and, after shopping, at the exit, you get back the belongings after passing the token.

Technically in the crypto world, the token is just another word for cryptocurrency or crypto asset.

Fungible Token(FT)

Fungible means goods that can be replaced, by another identical item.

Examples of a fungible token are fiat currency, bitcoins, or ether.

They are fungible because a $10 note is the same as another $10 note or ‘5’ bitcoins is the same as another ‘5’ bitcoins.

You can easily exchange them as they have the same value.

Non-Fungible Token (NFT)

An NFT may represent a different underlying asset and thus have a unique value.

An example of an NFT is a car. It is unfeasible to exchange a BMW with a Toyota as they each have a different value based on the features, assets, and uniqueness.

Similarly in crypto, the art, photography, or collectibles created by artists or musicians also have a unique value and cannot be exchanged.

Presuming two artists, A and B,  make the same painting, e.g., the Japanese ‘Tō-Ji temple’.

Though the painting/art is the same, it is unreasonable to exchange them as each has a unique value, proposition, and feature. Thus the art becomes a non-fungible token as it cant be exchanged.

NFT Market

It is similar to the amazon or eBay eCommerce market, but only for NFT’s.

The NFT’s are minted (assigned owners) as they become part of the blockchain and then they can be sold, purchased, resold resulting in the transfer of ownership from the seller to the buyer.

Examples of the famous NFT markets are opensea.io, binance.com, blockparty.co, Rarible, etc.

To buy and sell in these marketplaces, you need Ether, Solana, or other cryptocurrencies in the wallets.

                                                           Open-Sea marketplace
BlockParty marketplace

ERC-721

ERC is the Ethereum Request for Comment and 721 is for the NFT.

It contains information regarding all the interfaces that must be implemented when implementing smart contracts related to NFT.

It is important to note that most of these ERC’s are already implemented by many companies such as zeppelin solutions and are made available as open-source.

Thus it is not necessary to rewrite these interfaces from scratch due to two reasons:

  1. Rewriting consumes a lot of time.
  2. Writing these interfaces from scratch may result in security loopholes or bugs.

The better solution would be to reuse the code written for the ERC721 standard and write the supporting contracts from scratch that need to be implemented to buy, sell or resell the Art.

ERC-721 Interfaces

An ERC-721 contains the following interfaces

contract ERC721 {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

function balanceOf(address owner) public view returns (uint256 balance);

function ownerOf(uint256 tokenId) public view returns (address owner);

function approve(address to, uint256 tokenId) public;

function getApproved(uint256 tokenId) puäblic view returns (address operator);

function setApprovalForAll(address operator, bool _approved) public;

function isApprovedForAll(address owner, address operator) public view returns (bool);

function transferFrom(address from, address to, uint256 tokenId) public;

function safeTransferFrom(address from, address to, uint256 tokenId) public; 

function _mint(tokenID);

function _safeMint(tokenID);
}

Most of the important functions are described in brief below. They are also available here

balanceOf(address owner)

Returns the number of NFT tokens held by the owner.

ownerOf(uint256 tokenId)

Gives the owner’s address that has this particular token.

approve(address to, uint256 tokenId)

Approves another address to transfer the given token ID. There can only be one approved address per token at a given time. Can only be called by the token owner or an approved operator.

getapproved(uint256 tokenId)

Gets the approved address for a token ID, or zero if no address set Reverts if the token ID does not exist.

setApproveforAll(address operator, bool _approved)

Sets or unsets the approval of a given operator An operator is allowed to transfer all tokens of the sender on their behalf.

isApprovedForAll(address owner, address operator)

Tells whether an operator is approved by a given owner.

transferFrom(address from, address to, uint256 tokenId)

Transfers the ownership of a given token ID to another address. Usage of this method is discouraged, use safeTransferFrom() whenever possible. Requires the msg.sender to be the owner, approved, or operator.

safeTransferFrom(address from, address to, uint256 tokenId)

Safely transfers the ownership of a given token ID to another address. If the target address is a contract, it must implement the IERC721Receiver.onERC721Received, which is called upon a safe transfer. This ensures that your token is not lost forever if sent to the wrong address.

_mint(tokenId)

Internal function to mint a new token. Reverts if the given token ID already exists.

_safeMint(tokenId)

Internal function to safely mint a new token. Reverts if the given token ID already exists. If the target address is a contract, it must implement onERC721Received, which is called upon a safe transfer.

In the next section, let us implement a smart contract to buy, sell and resell the art using the ERC-721 from Openzeppelin.

NFT Smart Contract

                                                           Author: Yogesh K

In this section, let us implement the smart contract needed to buy, sell or resell the art making use of the Openzeppelins ERC-721 contract.

To start with, create a folder nftmarket. We will use the truffle framework to write and deploy the smart contract.

$ mkdir nftmarket && cd nftmarket
$ truffle init

In the contracts folder, create a new file finxterArt.sol. Let us start editing the source code.

Step 1. Import openzeppelin’s ERC721 contract and mention the solidity compiler version.

//SPDX-License-Identifier: Unlicense

//Author: Yogesh K for Finxter academy

pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

Step 2. Declare the contract finxterArt and all the variables needed. Initialize the constructor for ERC721 with name and symbol

contract finxterArt is ERC721 {

   struct Art {
       uint256 id;
       string title;
       string description;
       uint256 price;
       string date;
       string authorName;
       address payable author;
       address payable owner;
   // 1 means token has sale status (or still in selling) and 0 means token is already sold, ownership transferred and moved to off-market gallery       
       uint status;  
       string image;
   }

   struct ArtTxn {
       uint256 id;
       uint256 price;
       address seller;
       address buyer;
       uint txnDate;
       uint status;
   }

   // gets updated during minting(creation), buying and reselling
   uint256 private pendingArtCount;
   mapping(uint256 => ArtTxn[]) private artTxns;
   uint256 public index;  // uint256 value; is cheaper than uint256 value = 0;.
   Art[] public arts;

   // log events back to the user interface
  event LogArtSold(uint _tokenId, string _title, string _authorName, uint256 _price,   address _author,  address _current_owner, address _buyer);

  event LogArtTokenCreate(uint _tokenId, string _title, string _category, string _authorName, uint256 _price, address _author,  address _current_owner);

  event LogArtResell(uint _tokenId, uint _status, uint256 _price);


  constructor (string memory name, string memory symbol) ERC721(name, symbol){}

Step 3. Minting or token creation function.

/* Create or minting the token */
   function createFinxterToken(string memory _title, string memory _description,string memory _date, string memory _authorName, 
uint256 _price, string memory _image) public {

       require(bytes(_title).length > 0, 'The title cannot be empty');
       require(bytes(_date).length > 0, 'The Date cannot be empty');
       require(bytes(_description).length > 0, 'The description cannot be empty');
       require(_price > 0, 'The price cannot be empty');
       require(bytes(_image).length > 0, 'The image cannot be empty');

       Art memory _art = Art({
           id: index,
           title: _title,
          description: _description,
           price: _price,
           date: _date,
           authorName: _authorName,
           author: payable(msg.sender),
          owner: payable(msg.sender),
           status: 1,
           image: _image
       });

       arts.push(_art);   // push to the array

// array length -1 to get the token ID = 0, 1,2 ...
 uint256 tokenId = arts.length -1 ;
_safeMint(msg.sender, tokenId);
       emit LogArtTokenCreate(tokenId,_title,  _date, _authorName,_price, msg.sender, msg.sender);

       index++;
       pendingArtCount++;
   }

Step 4. Buying the Art function. Some important highlights of this function

  • The buyFinxterArt() function verifies whether the buyer has enough balance to purchase the art.
  • The function also checks whether the seller and buyer both have a valid account address, the token owner’s address is not the same as the buyer’s address. The seller is the owner of the art. Once all of the conditions have been verified, it will start the payment and art token transfer process.
  • _transfer transfers an art token from the seller to the buyer’s address. _current_owner.transfer will transfer the buyer’s payment amount to the art owner’s account. If the seller pays extra Ether to buy the art, that ether will be refunded to the buyer’s account.
  • Finally, the buyFinxterArt() function will update art ownership information in the blockchain. The status will change to 0, also known as the sold status. The function implementations keep records of the art transaction in the ArtTxn array.
function buyFinxterArt(uint256 _tokenId) payable public {
       (uint256 _id, string memory _title, ,uint256 _price, uint _status, , string memory _authorName, address _author,address payable _current_owner,) =  findFinxterArt(_tokenId);
       require(_current_owner != address(0));
       require(msg.sender != address(0));
       require(msg.sender != _current_owner);
       require(msg.value >= _price);
       require(arts[_tokenId].owner != address(0));
       // transfer ownership of the art
       _safeTransfer(_current_owner, msg.sender, _tokenId, "");
       //return extra payment
       if(msg.value > _price) payable(msg.sender).transfer(msg.value - _price);
       //make a payment to the current owner
       _current_owner.transfer(_price);

       arts[_tokenId].owner = payable(msg.sender);
       arts[_tokenId].status = 0;
  
       ArtTxn memory _artTxn = ArtTxn({
           id: _id,
           price: _price,
           seller: _current_owner,
           buyer: msg.sender,
           txnDate: block.timestamp,
           status: _status
       });

       artTxns[_id].push(_artTxn);
       pendingArtCount--;
       emit LogArtSold(_tokenId,_title,  _authorName,_price, _author,_current_owner,msg.sender);
   }

Step 5. The buyFinxterArt() function has a helper function findFinxterArt() which is defined below.

   /* Pass the token ID and get the art Information */
   function findFinxterArt(uint256 _tokenId) public view returns (
       uint256, string memory, string memory, uint256, uint status,  string memory, string memory, address, address payable, string memory) 
 {
   Art memory art = arts[_tokenId];
   return (art.id, art.title, art.description,
           art.price, art.status, art.date, art.authorName,
    art.author, art.owner,art.image); 
 }

Step 6. Reselling the Art function. Some important highlights

  • The resellFinxterArt() function verifies whether the sender’s address is valid and makes sure that only the current art owner is allowed to resell the art.
  • Then, the resellFinxterArt() function updates the art status from 0 to 1 and moves to the sale state. It also updates the art’s selling price and increases the count of the current total pending arts.
  • emit LogArtResell() is used to add a log to the blockchain for the art’s status and price changes.
function resellFinxterArt(uint256 _tokenId, uint256 _price) payable public {
       require(msg.sender != address(0));
       require(isOwnerOf(_tokenId,msg.sender));
       arts[_tokenId].status = 1;
       arts[_tokenId].price = _price;
       pendingArtCount++;
       emit LogArtResell(_tokenId, 1, _price);
   }

Step 7. Finding all the pending arts (i.e. status = 1)

   /* returns all the pending arts (status =1) back to the user */
   function findAllPendingFinxterArt() public view  returns (uint256[] memory, address[] memory, address[] memory,  uint[] memory) {
     if (pendingArtCount == 0) {
          return (new uint256[](0),new address[](0), new address[](0), new uint[](0)); 
       }
  
       uint256 arrLength = arts.length;
       uint256[] memory ids = new uint256[](pendingArtCount);
       address[] memory authors = new address[](pendingArtCount);
       address[] memory owners= new address[](pendingArtCount);
       uint[] memory status = new uint[](pendingArtCount);
       uint256 idx = 0;
       for (uint i = 0; i < arrLength; ++i) {
           Art memory art = arts[i];
           if(art.status==1) {
                   ids[idx] = art.id;
                   authors[idx] = art.author;
                   owners[idx] = art.owner;
                   status[idx] = art.status;
                   idx++;
               }
           }

       return (ids,authors, owners, status); 
   }

Step 8. Finding all the Arts for the owner.

   /* Return the token ID's that belong to the caller */
   function findMyFinxterArts()  public view returns (uint256[] memory _myArts) {
       require(msg.sender != address(0));
       uint256 numOftokens = balanceOf(msg.sender);
       if (numOftokens == 0) {
           return new uint256[](0);
       }
       else{
           uint256[] memory myArts = new uint256[](numOftokens);
           uint256 idx = 0;
           uint256 arrLength = arts.length;
           for (uint256 i = 0; i < arrLength; i++) {
               if (ownerOf(i) == msg.sender) {
                   myArts[idx] = i;
                   idx++;
               }
           }
           return myArts;
       }
   }

Step 9. Some helper functions.

   /* return true if the address is the owner of the token or else false */
   function isOwnerOf(uint256 tokenId, address account) public view returns (bool) {
       address owner = ownerOf(tokenId);
       require(owner != address(0));
       return owner == account;
   }


   function get_symbol() external view returns (string memory)
   {
       return symbol();
   }

   function get_name() external view returns (string memory)
   {
       return name();
   }

} // End of contract

In the next section, we will compile and deploy the smart contract

Contract Deployment

In this section, we compile and deploy the smart contract on the local blockchain such as Ganache.

Step 1: In the truffle project, update the truffle_config.js file with the below content.

 networks: {
     development: {
     host: "127.0.0.1",     // Localhost (default: none)
     port: 7545,            // Standard Ethereum port (default: none)
     network_id: "*",       // Any network (default: none)
    },
}
// Configure your compilers
 compilers: {
   solc: {
      version: "0.8.9", 
    }
}

💡 Note: The port 7545 must match the one provided by Ganache (see step 5, check RPC server address is http://127.0.0.1:7545). This is only needed during deployment and not for compilation.

Step 2:  Install the openzeppelin’s contracts and compile the contract created as below

$ npm install @openzeppelin/contractsntracts
$ truffle compile

This should compile all the contracts after some time without any errors.

Step 3: Download Ganache depending on the OS (Windows, macOS, Linux) from here.

Step 4: Start the Ganache application, click NEW Workspace, add project and select the truffle_config.js for the truffle project created earlier as below and save workspace.

Ganache

Step 5: Next, start this workspace and go to the contracts section, it shows the finxterArt contract and its subcontracts as not deployed as below.

Contracts are not deployed

Step 6: In the migration folder of the truffle project create a file 2_deploy_contracts.js and add the below content to the file

var FinxterArt = artifacts.require("./finxterArt.sol");

module.exports = (deployer, network, accounts) => {

 deployer.then(async () => {
   try {
     await deployer.deploy(FinxterArt, "FinxterArtToken", "FT");
    } catch (err) {
     console.log(('Failed to Deploy new Contract', err))
   }
 })
}

Step 7: Now deploy the contract using

$ truffle deploy

Apart from the console logs to confirm if the deployment was successful, you can also confirm the deployment in the ganache contracts section. It should change from Not Deployed to Deployed address and in the transaction section, there must be transactions related to the deployed contracts.

After deployment

See the highlighted section in the above figure, it shows the deployed contract address and the status as deployed.

Step 8: Finally, open the Metamask wallet and create a local ganache network using the appropriate RPC server address as seen in the Ganache application (on my PC it was http://127.0.0.1:7545).

You can as well create accounts in the metamask using the first three addresses as seen in the Ganache with the option import account and private keys of the three addresses.

This should show the balances of the accounts. This will be needed when we implement the frontend in the next section to approve the transactions from one account to another.

For more details, check out our in-depth course here: