Building an NFT Marketplace on Celo with Python

Building an NFT Marketplace on Celo with Python https://celo.academy/uploads/default/optimized/2X/7/71905b050ea130bcf1f1dd0384da4564a6993f40_2_1024x576.jpeg
none 0.0 0

Introduction

NFTs or Non-Fungible, an application of blockchain is used to represent unique digital assets such as collectibles, arts, etc. Celo is a blockchain platform that is designed to send, receive, and store digital assets securely on a mobile phone. The Celo blockchain is built using the Solidity programming language and is fully compatible with Ethereum. In this tutorial, we will be building an NFT marketplace with Python using the brownie framework and deploying it on Celo Testnet.

Prerequisites

To understand this tutorial, you must be familiar with the following:

  • Building Smart Contracts
  • The Python programming language

Requirements

You should have the following installed on your computer to follow along:

Setting Up Project

To get started, we have to create a new directory for our project and install the following dependencies:

mkdir nft-marketplace
cd nft-nft-marketplace
# Create virtual environment
python3.10 -m venv venv
# activate virtual environment
source venv/bin/activate

Note: Brownie works bet with python3.10

# Install ganache
npm install -g ganache
# Install eth-brownie and python-dotenv
pip3 install eth-brownie python-dotenv

After installing dependencies, we need to initialize our project as a brownie project.

brownie int

This command generates some folder which will look like this:

After initializing brownie into our project, in your root directory, create two files called .env and brownie-config.yaml. The .env file is used to store environment variables that should not be exposed to the public such as our private key, mnemonic phrase, etc, while brownie-config.yaml is used to configure brownie in our project.

.env

MNEMONIC="..."
PRIVATE_KEY="0x..."

brownie-config.yaml

reports:
    exclude_contracts:
        - SafeMath
depedencies:
    - OpenZeppelin/openzeppelin-contracts@4.8.0
compiler:
    solc:
        version: 0.8.15
        optimizer:
            enabled: true
            runs: 200
        remappings:
            - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.8.0"
networks:
    default: celo-alfajores
console:
    show_colors: true
    color_style: monokai
    auto_suggest: true
    completions: true
    editing_mode: emacs
dotenv: .env
wallets:
    from_mnemonic: ${MNEMONIC}
		from_key: ${PRIVATE_KEY}

The next step is to add Celo Testnet (Alfajores) to our brownie project:

brownie networks add Celo celo-alfajores host=https://alfajores-forno.celo-testnet.org chainid=44787 explorer=https://alfajores-blockscout.celo-testnet.org

You can see the list of networs that have been added to our brownie project:

brownie network list

Implementing the Smart Contract

Next ,we have to write the smart contraxr for our NFT marketplace. In your contracts directory, create a new file called NFTMarketplace.sol

agma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract NFTMarketplace {
    using SafeMath for uint256;
    struct Auction {
        address tokenAddress;
        uint256 tokenId;
        address payable seller;
        uint256 price;
        uint256 endTime;
        bool active;
    }
    address public owner;
    uint256 public feePercentage; // percentage of the sale price taken as fee
    mapping(address => mapping(uint256 => Auction)) public auctions; // map of all active auctions
    event AuctionCreated(
        address indexed tokenAddress,
        uint256 indexed tokenId,
        address indexed seller,
        uint256 price,
        uint256 endTime
    );
    event AuctionEnded(
        address indexed tokenAddress,
        uint256 indexed tokenId,
        address indexed buyer,
        uint256 price
    );
    constructor() {
        owner = msg.sender;
        feePercentage = 1; // 1% fee by default
    }
    function createAuction(address _tokenAddress, uint256 _tokenId, uint256 _price, uint256 _duration) public {
        require(_duration > 0, "Duration must be greater than zero");
        require(_price > 0, "Price must be greater than zero");
        IERC721 tokenContract = IERC721(_tokenAddress);
        require(tokenContract.ownerOf(_tokenId) == msg.sender, "You don't own this NFT");
        uint256 endTime = block.timestamp.add(_duration);
        Auction memory auction = Auction(_tokenAddress, _tokenId, payable(msg.sender), _price, endTime, true);
        auctions[_tokenAddress][_tokenId] = auction;
        emit AuctionCreated(_tokenAddress, _tokenId, msg.sender, _price, endTime);
    }
    function endAuction(address _tokenAddress, uint256 _tokenId) public {
        Auction storage auction = auctions[_tokenAddress][_tokenId];
        require(auction.active, "Auction has already ended");
        require(block.timestamp >= auction.endTime, "Auction hasn't ended yet");
        address payable seller = auction.seller;
        uint256 price = auction.price;
        auction.active = false;
        IERC721 tokenContract = IERC721(_tokenAddress);
        tokenContract.safeTransferFrom(address(this), msg.sender, _tokenId);
        uint256 fee = price.mul(feePercentage).div(100);
        seller.transfer(price.sub(fee));
        emit AuctionEnded(_tokenAddress, _tokenId, msg.sender, price);
    }
    function setFeePercentage(uint256 _feePercentage) public {
        require(msg.sender == owner, "Only the owner can set the fee percentage");
        require(_feePercentage >= 0 && _feePercentage <= 100, "Fee percentage must be between 0 and 100");
        feePercentage = _feePercentage;
    }
    function withdraw() public {
        require(msg.sender == owner, "Only the owner can withdraw funds");
        address payable self = payable(address(this));
        self.transfer(self.balance);
    }
    // fallback function
    receive() external payable {}
}

Now let’s go through the code step by step

pragma solidity ^0.8.0;
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./SafeMath.sol";

The above code specifies our solidity version and imports the IERC721,
IERC721Receiver, and SafeMath (for blockchain math operations) interfaces.

using SafeMath for uint256;
 struct Auction {
     address tokenAddress;
      uint256 tokenId;
      address payable seller;
      uint256 price;
      uint256 endTime;
      bool active;
  }

Enables us to use the SafeMath foe uint256 tyoe operations and the Auction struct defines the structure of the Auction.

mapping(address => mapping(uint256 => Auction)) public auctions;

This maps each token address and token ID to an active auction.

event AuctionCreated(
        address indexed tokenAddress,
        uint256 indexed tokenId,
        address indexed seller,
        uint256 price,
        uint256 endTime
    );
    event AuctionEnded(
        address indexed tokenAddress,
        uint256 indexed tokenId,
        address indexed buyer,
        uint256 price
    );

These are the eents that are emitted when an auction is creares and ended.

constructor() {
        owner = msg.sender;
        feePercentage = 1; // 1% fee by default
    }

This sets the owner of the contract and the default fee (in percentage).

function createAuction(address _tokenAddress, uint256 _tokenId, uint256 _price, uint256 _duration) public {
        require(_duration > 0, "Duration must be greater than zero");
        require(_price > 0, "Price must be greater than zero");
        IERC721 tokenContract = IERC721(_tokenAddress);
        require(tokenContract.ownerOf(_tokenId) == msg.sender, "You don't own this NFT");
        uint256 endTime = block.timestamp.add(_duration);
        Auction memory auction = Auction(_tokenAddress, _tokenId, payable(msg.sender), _price, endTime, true);
        auctions[_tokenAddress][_tokenId] = auction;
        emit AuctionCreated(_tokenAddress, _tokenId, msg.sender, _price, endTime);
    }
    function endAuction(address _tokenAddress, uint256 _tokenId) public {
        Auction storage auction = auctions[_tokenAddress][_tokenId];
        require(auction.active, "Auction has already ended");
        require(block.timestamp >= auction.endTime, "Auction hasn't ended yet");
        address payable seller = auction.seller;
        uint256 price = auction.price;
        auction.active = false;
        IERC721 tokenContract = IERC721(_tokenAddress);
        tokenContract.safeTransferFrom(address(this), msg.sender, _tokenId);
        uint256 fee = price.mul(feePercentage).div(100);
        seller.transfer(price.sub(fee));
        emit AuctionEnded(_tokenAddress, _tokenId, msg.sender, price);
    }

The createAuction function creates a new auction and the endAuction ends an active auction.

function setFeePercentage(uint256 _feePercentage) public {
        require(msg.sender == owner, "Only the owner can set the fee percentage");
        require(_feePercentage >= 0 && _feePercentage <= 100, "Fee percentage must be between 0 and 100");
        feePercentage = _feePercentage;
    }
    function withdraw() public {
        require(msg.sender == owner, "Only the owner can withdraw funds");
        address payable self = payable(address(this));
        self.transfer(self.balance);
    }

The setFeePercentage functino sets the percentage fee for the contract and the withdraw function enables the creator of the contract to withdraw the fund from it.

// fallback function
  receive() external payable {}

A callback functnion that is called when ether is sent to the contract

Deploying the Contract

Next, we need to compile and deploy the contract on the Celo Testnet, Run the following command to compile the contract.

brownie compile

To deploy the smart contract on Celo create a new file called deploy.py in the scripts directory of your project.

from brownie import NFTMarketplace, accounts, config, network
def deploy_marketplace():
    # Load the account to deploy from
    dev = accounts.add(config["wallets"]["from_key"])
    print(f"Deploying from {dev.address}")
    # Deploy the contract
    marketplace = NFTMarketplace.deploy({"from": dev})
    print(f"NFTMarketplace contract deployed to {marketplace.address}")
def main():
    # Set the network to deploy to
    network_name = network.show_active()
    print(f"Deploying to {network_name} network")
    # Call the deploy function
    deploy_marketplace()

The deploy_marketplace functino get the account we would use to deploy the contract.

Deploy the Contract

 Deploy the contract to the Celo Alfajores testnet
brownie run scripts/deploy.py --network celo-alfajores

Conclusion

In this tutorial we implemented an NFT smart contract that allows users to create and participate in ERC721 token auctions. The contract charges a fee for each sale, which is determined by the contract’s owner. When an auction concludes, the winning bidder receives the NFT, while the seller receives the sale price less the fee. The contract also gives the owner the option to withdraw funds from the contract.

We have learned how to implement an NFT exchange on Celo using brownie in Python. We explored topics such as the basics of setting up a development environment, creating a smart contract and finally compiling and deploying the contract on Celo Testnet or Afrajores.

Next Step

We can explore more functionalities that we can add to this contract, such as the ability for bidders to increase the current bid by a minimum increment, the ability to see auction history, time extension on bid time, etc.

References

  1. Celo Developer Documentation
  2. Solidity Documentation
  3. Brownie Documentation
  4. Github Repository

About the Author

Israel Okunaya is an ace writer with a flair for simplifying complexities and a knack for storytelling. He leverages over four years of experience to meet the most demanding writing needs in different niches, especially food and travel, blockchain, and marketing. He sees blockchain as a fascinating yet tricky affair. So, he’s given to simplifying its complexities with text and video tutorials.

1 Like