Skip to main content
Go back

Build an nft-gated dapp and deploy on a decentralized hosting service

· 14 min read
Isaac Jesse



In the past years, the Non-Fungible Token category has gained popularity through the myriad of use cases and inventions influenced by virtual machines. The NFT industry witnesses liquidity counting to billions of dollars and this is only made possible through people's creativity and sustainable blockchain networks such as Celo. As a web3 developer, as important as your role is, you need to understand NFT use cases to build a disruptive Dapp.


As an extension of the previous tutorial on how to build a persistent Dapp on Celo using wagmi, I have prepared this tutorial to depict one of the many NFT use cases which we will deploy to Celo's testnet - Alfajores. We will also build a user interface using Nextjs, react, and materialUi to interact with the Dapp. The frontend will be deployed on Fauna - a decentralized hosting service. This is an advanced tutorial, and it requires that you have a sound background in the following areas.

  • Smart contract development using solidity.
  • You should know Javascript and have considerable knowledge of typescript.
  • For how to use hardhat for smart contract development, please refer to this and this.


Before you proceed, please install the following tools:

What we are building

We will build a decentralized application for swapping ERC20 tokens to $Celo. Detail can be found in the readme.

The dApp will be in two parts. The first part is the smart contract that houses Dapp's logic while the other manages the user interface.

Smart contracts

  • Clone the project or use it as a template, then clone it to your computer.
git clone
  • Navigate to the root folder
cd persistent-dapp-on-celo-using-wagmi/backend
  • Install dependencies
yarn install

All files in the contract folder remain intact as we extend its functionalities by adding a non-fungible asset using the Openzeppelin library.


  • SwapLab.sol contains the main contract functions for adding and removing liquidity and swapping Celo's ERC20 token for the $Celo coin.

  • TestToken.sol has the asset code we will use for testing the swap contract.

  • Add a new folder named erc721 under the contract folder.

    • The OZ's ERC721 contract module with path @openzeppelin/contracts/interfaces/IERC721.sol is a standard interface for interacting with non-fungible tokens on the blockchain. But we need to extend its reach so we can mint a membership token with the ability to cancel it. On the frontend, we'll require that users own a membership nft before they can interact with the Dapp.

    • contracts/interfaces/IERC721Extended.sol

    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.17;

    import "@openzeppelin/contracts/interfaces/IERC721.sol";
    import "@openzeppelin/contracts/interfaces/IERC721Receiver.sol";
    import "@openzeppelin/contracts/interfaces/IERC721Metadata.sol";

    interface IERC721Extended is IERC721, IERC721Receiver, IERC721Metadata {
    function burn(uint256 tokenId) external returns(bool);
    function mint() external payable returns(bool);
    • You would notice I included separately ERC721.sol and ERC721Pausable.sol. Sometimes, this is my way of doing things. You don't have to do it my way. We can simply import the circuit-breaker extended ERC721Pausable.sol in the final token deployable file.

    • Membership.sol

      • Import and inherit Pausable.sol and Ownable.sol from the openzeppelin modules.

      • Previously, we declared two additional function interfaces. It then becomes imperative that we implement these functions in the current file. Here, we implement the mint and burn functions.

      • The notZeroAddress modifier ensures that the target address is not empty.

      • NFTs are unique properties or elements of a collection. They could share similar parents i.e be part of a collection but are never the same in properties. To introduce uniqueness, we generate a new NFT by increasing tokenId.

      • User addresses that have minted the membership nft are kept in isMember storage for reference purposes. This method ensures that no address can mint twice.

      • To encourage users to mint our membership nft, we have lowered the swapping fee. But they need to pay a tiny amount in order to have their membership minted. This method has introduced a vulnerability that we need to guard against. A user could mint and transfer or resell to other users. We do not want this to happen. To prevent the occurrence of such an event, we will intercept to place a barrier in the internal function by overriding _transfer() so that no one is able to transfer membership. The right to do this is given to the owner.

      • We effected the circuit breaker by implementing the pause and unpause functions.

    // SPDX-License-Identifier: Unlicense

    pragma solidity 0.8.17;

    import "./ERC721Pausable.sol";
    import "@openzeppelin/contracts/access/Ownable.sol";

    contract Membership is Ownable, ERC721Pausable {
    error ZeroAddress(address);

    uint public tokenId;
    mapping (address => bool) public isMember;

    @dev Enforces that @param target - must not be empty
    modifier notZeroAddress(address target) {
    if(target == address(0)) revert ZeroAddress(target);

    //Initialize state vars
    constructor () ERC721("SwapLab User", "SLU") {

    receive() external payable {

    @notice Mint Membership NFT
    Owner's privilege.
    o 'TokenId' must not have been minted before now.
    o No user can have more than one membership NFT.
    function mint() public payable returns(bool) {
    require(msg.value >= 1e16 wei, "Insufficient value");
    require(!isMember[_msgSender()], "Already a user");
    isMember[_msgSender()] = true;
    tokenId ++;
    uint tk = tokenId;
    (bool sent,) = owner().call{value: msg.value}('');
    require(sent, "Failed");
    _safeMint(_msgSender(), tk);
    _approve(_msgSender(), tk);

    return true;

    ///@dev Burns 'tokenId' Note - Owner's privilege
    function burn(uint _tokenId) external onlyOwner returns(bool) {

    return true;

    /** See ERC721 _transfer. Membership is not transferable*/
    function _transfer(address from, address to, uint256 _tokenId ) internal override
    require(paused() || _msgSender() == owner(), "Tansfer not alloowed");
    super._transfer(from, to, _tokenId);

    /** @dev Halts contract execution */
    function pause() public onlyOwner

    /** @dev Continues contract execution */
    function unpause() public onlyOwner

    function onERC721Received(
    address operator,
    address from,
    uint256 _tokenId,
    bytes calldata data
    ) external override returns (bytes4) {}
  • Compile

npx hardhat compile


  • Testing

I have modified the test file to ensure that no function runs unless the nft balance of the calling account is more significant than zero.

npx hardhat test


  • Deploy

The hardhat config file already has the right configuration to enable us to deploy to the Celo testnet.

yarn deploy

Deployment artifacts are saved in the deployments folder.


This project is NFT-gated hence users will be denied access to the Dapp interactive page unless they already owned our nft. We will build intelligence that automatically detects if users own membership nft otherwise we'll ask them to mint first before granting them access.

Exit the current directory into the root folder and install the dependencies:

cd ..
cd frontend
yarn install

The project uses wagmi to manage the Dapp. To know how to set up the wagmi project, please refer to the previous tutorial.

We are going to modify the components/App directory. Only two files are in this folder - CardComponent.tsx and index.tsx. Both the landing and interactive pages are contained in the index file. We need to separate them so users can be routed to the landing page if their wallet is not activated otherwise they're shown the interactive view but will be disabled if no membership is detected. Let's separate the file as follows:

  • components/App/Home/index.tsx

We will transfer the content of the landing page into this file.

import React from 'react'
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import Link from '@mui/material/Link';
import { ConnectKitButton } from 'connectkit';

export function Home () {
return (
<main >
<Box sx={{ bgcolor: '', pt: 16, pb: 6, }}>
<Container maxWidth="md">
color="rgb(150, 150, 150)"
Decentralized Token swap powered by Celo
<Typography variant="h6" align="center" color="rgba(150, 150, 150, 0.7)" paragraph>
Exchange ERC20 compatible token for $CELO
<Typography variant="overline" align="center" color="green" paragraph>
Built by <span style={{color: 'rgba(170, 170, 170, 0.9)'}}><a href="">Isaac J.</a></span> for Celo developers - #celosage
<Stack sx={{ pt: 4 }} direction="row" spacing={2} justifyContent="center">
<ConnectKitButton />
<Button variant="outlined">
<Link color="rgba(150, 150, 150, 0.8)" sx={{
'&:hover': {
border: 'rgba(100, 100, 100, 0.5)'
}} href="" underline='none'>Source code</Link>

  • components/App/Mint/index.tsx

In this file, we mint new membership nft for new users. In the useEffect function, we watch for the user's balance and compare if greater than zero to authenticate them.

The component renders a button that calls handleMint when clicked. If the operation is successful, it will be hidden and the Dapp component is rendered.

import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Typography from '@mui/material//Typography';
import Container from '@mui/material//Container';
import React from 'react';
import { useAccount } from 'wagmi';
import runContractFunc from '../../apis';
import { SignUprops } from '../../../interfaces';
import { Spinner } from '../../Spinner';
import green from '@mui/material/colors/green';

const FUNC_NAME = 'mint';

export function Mint (props: SignUprops) {
const [ loading, setLoading ] = React.useState<boolean>(false);
const { handleClick, setauth, isUser } = props;
const { address, isConnected, connector } = useAccount();

React.useEffect(() => {
const endtask = new AbortController();
const refetchData = async() => {
if(connector) {
const provider = await connector?.getProvider();
if(isConnected) {
if(provider) {
const bal = await runContractFunc({
account: address,
providerOrSigner: provider,
functionName: 'nftBalance'
if(bal.balanceOrAllowance.toString() > '0') {
return () => endtask.abort();
}, [isConnected]);

const handleMint = async() => {
await handleClick(FUNC_NAME);

return (
<Container maxWidth={isUser ? 'xs' : 'md'}>
<Typography component='button' variant='h5' color='rgba(150, 150, 150, 0.7)'>Membership Not Found</Typography>
<Typography component='button' variant='h5' color='rgba(150, 150, 150, 0.7)'>To interact with this Dapp, Please mint membership NFT</Typography>
endIcon={loading? <Spinner color={'white'} /> : 'Proceed to mint'}
color: 'whitesmoke',
'&:hover': {
background: 'rgba(150, 150, 150, 0.3)',
  • components/App/AppProtected/index.tsx

    Move the content of the previous App/index.tsx into this file and make the following changes.

    • import the Mint component. By default, if the user does not own the $SLU nft, the button components are disabled until otherwise proven true.
    import getContractData from '../../apis/contractdata';
    import { Mint } from '../Mint';
    • We dynamically hide some components based on the outcome of the Mint component using getOpacity function. Every child components in the Stack component are blurred out when getOpacity returned 0 vice versa.
      const getOpacity = () => {
    return isUser? 1 : 0
    • Add an additional case block to the switch statement in the afterTrx function that watches for 'mint' to be true. The nft balance from the resulting operation from handleClik is extracted and passed to afterTrx. Then in the mint case, we set the pass by calling setauth if the balance is greater than zero. In your Dapp, you'd want to handle it in a manner that allows your Dapp read from a collection of nfts, and you can filter out the current user.
      const afterTrx = (x:string, result: Result) => {
    switch (x) {
    case 'swap':
    case 'approve':
    case 'clearAllowance':
    case 'addLiquidity':
    case 'removeLiquidity':
    case 'mint':
    if(result.balanceOrAllowance.toString() > '0') setauth();
  • Since we reduced the swap fee in the SwapLab contract, we will affect it on the frontend. A place to do that is in the handleClick function.

Reset the conditional statement that checks if functionName equals 'swap', then set the swap fee in variable value to 1e14 wei equivalent to 0.0001 Celo.

Set additional statement to check for 'mint' function, then set value to 1e16 wei. For the first time that the mint function is executed for the current user, if no error is thrown, then a membership nft is minted for the user. We can then set the pass to true.

    const handleClick = async(functionName: string, flag?:boolean) => {
if(flag && functionName !== 'approve' && amount === 0) return alert('Please enter amount');
if(functionName === 'addLiquidity') {
if(value === '0') return alert('Please set value');
const provider = await connector?.getProvider();

try {
const amt = BigNumber(amount);
let val = ethers.utils.parseEther(value);
if(functionName === 'swap') val = ethers.utils.parseEther('0.0001');
if(functionName === 'mint') val = ethers.utils.parseEther('0.01');
console.log("Val", val.toString())
const result = await runContractFunc({
functionName: functionName,
providerOrSigner: provider,
amount: ethers.utils.hexValue(ethers.utils.parseUnits(amt.toString())),
cancelLoading: () => setLoading(false),
account: address,
value: val
if(functionName === 'mint') setAuth(true);
afterTrx(functionName, result);

} catch (error: any) {
if(error) {
const result = await runContractFunc({
functionName: 'getData',
providerOrSigner: provider,
afterTrx('getData', result);
setError(error?.reason || error?.data.message || error?.message);
console.log("Error1", error?.reason|| error?.message || error?.data.message);

Now, let's run the program. In the frontend directory, run

yarn run dev
npm run dev

If everything works correctly, you should have the view as follows.

  • Landing page


  • Page after the provider is activated but membership nft is not yet minted, and the buttons are disabled.


  • Transaction is being broadcasted.


  • After nft is minted.


Deploying to decentralized hosting service​

At this point, our Dapp's frontend is ready to be deployed to a hosting service. Although there are a couple of decentralized hosting services that we could consider, I have selected to deploy to "Spheron" for its easy, simple, and faster deployment rate.

Firstly, let us understand what decentralized hosting means. Just as the name implies, it is a method of uploading or storing data to a server not owned and controlled by a central authority. Unlike centralized counterpart, decentralized service uses distributed networks most of which resemble blockchain. When files are uploaded, they're hashed into compartments and distributed across nodes of the computer so that no single node is able to claim the monopoly of storage.

Spheron provides an easy method of deploying Dapps to developers. With a few clicks, you're there. Read more. To deploy on either Filecoin or IPFS using Spheron, you will need to sign up on the website. Select the convenient method. It is recommended to authenticate using GitHub so as to easily extract repositories.

  • image

Thereafter, follow these steps:

  • After you're logged in, click on dashboard, then 'New Project' at the top right-hand corner.

    • image
  • Select the repository to link and give permissions to the app.

    • image
  • Connect using your favorite provider.

    • image

Note: Be sure to trust the app you are granting permission to access your GitHub account.

  • Review to select the desired permission. For me, I'd prefer to allow access to selected repositories.

    • image
  • Next step is to pick a protocol. I have selected IPFS.

    • image
  • Review the deployment settings

    • image

    • In the ROOT DIRECTORY column, we will type 'frontend/ as the root directory to deploy from.

    • Pick Nextjs as the FRAMEWORK and leave the build command as it is.

    • Under the Node Engine, be sure to select Node version 16 and above otherwise you'd likely get a build error. Then Click deploy.

      • image
  • While deploying, you can inspect the deploy log for the failed attempt. image

  • From the log, you can find the stream information such as the content hash.


  • Deploy success.



This Dapp is deployed to IPFS using Spheron, and you can interact with it via this link

The complete code for this tutorial can be found here.


Congratulation on completing this tutorial. You're a step away from building your dream Dapp on Celo. So far, we have learned to build a full Dapp using solidity, compile, test, and deployed to Celo Alfajores. We also build an interface for users to interact with what we've built. Lastly, we deployed the Dapp to a decentralized hosting service - Spheron. If you're for more tutorials relating to Celo, here is a place to get started.

What next?​

​ Are you thinking of launching your own project on the Celo blockchain right now? Kindly visit the developers documentation.

About the Author​​

Isaac Jesse , aka Bobelr is a smart contract/Web3 developer. He has been in the field since 2018, worked as an ambassador with several projects like Algorand and so on as a content producer. He has also contributed to Web3 projects as a developer.