DEV Community

Cover image for Let's mint an on-chain NFT mfers!
Abdul Rauf
Abdul Rauf

Posted on

Let's mint an on-chain NFT mfers!

This guide will teach you how to deploy an ERC-721 smart contract that lets you mint on-chain SVG NFTs.

What are on-chain NFTs?

On-chain NFTs are NFTs on f*cking steroids! They are the best kind of NFTs out there. If an NFT is on-chain, it means that the token's metadata + image is stored directly on the blockchain.

Most of the projects out there store their metadata on a decentralized file storage system such as IPFS. It's not a bad solution at all and many big names like Bored Ape Yacht Club and Cool Cats use it. Having said that, if IPFS goes down one day, you will most probably lose your NFTs.

There's an even better solution available to us i.e. storing the NFTs on-chain. Projects like Loot and Autoglyphs are popular projects that are using this technique.

The only downside is that it is quite costly to store data this way, and there's a limited type of formats that we can store on-chain.

Pre-requisites

This tutorial covers how to write and deploy a smart contract on Ethereum, but does not go into how to install all of the individual dependencies.

Instead, I will list the dependencies and link to the documentation for how to install them.

  1. Node.js - I recommend installing Node using nvm
  2. MetaMask - An Ethereum browser wallet
  3. Hardhat - An Ethereum development environment to compile, deploy, and test our smart contracts
  4. ethers.js - A JavaScript library to interact with our deployed smart contract
  5. Alchemy - A blockchain API that we'll use to access the Rinkeby test network

Here's a guide that will help you set up your development environment. The only difference is that I will be using the Rinkeby network to deploy my smart contract instead of Ropsten.

You also need some test ETH for this tutorial which you can get it from here.

Now let's get started!

Let's write our smart contract

Create a new file in the /contracts directory named OnChainNFT.sol. Add the following code:

// contracts/onChainNFT.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/* 
    A library that provides a function for encoding some bytes in base64
    Source: https://github.com/zlayine/epic-game-buildspace/blob/master/contracts/libraries/Base64.sol
*/
import {Base64} from "./Base64.sol";

contract OnChainNFT is ERC721URIStorage, Ownable {
    event Minted(uint256 tokenId);

    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721("OnChainNFT", "ONC") {}

    /* Converts an SVG to Base64 string */
    function svgToImageURI(string memory svg)
        public
        pure
        returns (string memory)
    {
        string memory baseURL = "data:image/svg+xml;base64,";
        string memory svgBase64Encoded = Base64.encode(bytes(svg));
        return string(abi.encodePacked(baseURL, svgBase64Encoded));
    }

    /* Generates a tokenURI using Base64 string as the image */
    function formatTokenURI(string memory imageURI)
        public
        pure
        returns (string memory)
    {
        return
            string(
                abi.encodePacked(
                    "data:application/json;base64,",
                    Base64.encode(
                        bytes(
                            abi.encodePacked(
                                '{"name": "LCM ON-CHAINED", "description": "A simple SVG based on-chain NFT", "image":"',
                                imageURI,
                                '"}'
                            )
                        )
                    )
                )
            );
    }

    /* Mints the token */
    function mint(string memory svg) public onlyOwner {
        string memory imageURI = svgToImageURI(svg);
        string memory tokenURI = formatTokenURI(imageURI);

        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();

        _safeMint(msg.sender, newItemId);
        _setTokenURI(newItemId, tokenURI);

        emit Minted(newItemId);
    }
}


Enter fullscreen mode Exit fullscreen mode

In this contract, we are inheriting from the ERC721URIStorage standard implemented by OpenZeppelin. If you want to use the contracts provided by Openzeppelin, you can install them using the following command:

npm install @openzeppelin/contracts
Enter fullscreen mode Exit fullscreen mode

Let's go through some of the difficult bits together.

1. Base64 encoding

Why do we have to encode our SVG and JSON data? Base64 encoding allows you to convert your byte data into a nice string that can be transmitted easily across a network.

Let's take a look at the svgToImageURI function again:


function svgToImageURI(string memory svg)
    public
    pure
    returns (string memory)
{
    string memory baseURL = "data:image/svg+xml;base64,";
    string memory svgBase64Encoded = Base64.encode(bytes(svg));
    /* 
      abi.encodePacked is a function provided by Solidity which
      is used to concatenate two strings, similar to a `concat()`
      function in JavaScript.
    */
    return string(abi.encodePacked(baseURL, svgBase64Encoded));
}
Enter fullscreen mode Exit fullscreen mode

In this function, we are encoding our SVG into a Base64 string. We are also prepending our Base64 string with data:image/svg+xml;base64 - which simply states that we want this string to be processed as an SVG.

After passing our SVG image through this function, we get something like this:

data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMDI0JyBoZWlnaHQ9JzEwMjQnPgogICAgICA8ZGVmcz48Y2xpcFBhdGggaWQ9J2EnPjxwYXRoIGQ9J00wIDBoMTAyNHYxMDI0SDB6Jy8+PC9jbGlwUGF0aD48L2RlZnM+CiAgICAgIDxnIGNsaXAtcGF0aD0ndXJsKCNhKSc+CiAgICAgICAgPHBhdGggZD0nTTAgMGgxMDI0djEwMjRIMHonLz4KICAgICAgICA8cGF0aCBmaWxsPScjZmZmJyBkPSdNMCAyNDFoMTAyNHYyMEgwek0wIDUwMmgxMDI0djIwSDB6TTAgNzYzaDEwMjR2MjBIMHonLz4KICAgICAgICA8cGF0aCBmaWxsPScjZmZmJyBkPSdNMjQxIDBoMjB2MTAyNGgtMjB6Jy8+CiAgICAgIDwvZz4KICAgIDwvc3ZnPg==
Enter fullscreen mode Exit fullscreen mode

Paste this string in your browser and you'll see our SVG. Here's how my SVG looks:

SVG NFT by LCM

So f*cking cool, am I right? 🀯

2. Generate 'tokenURI'

What is a tokenURI you ask? It is a link to your token's metadata. Basically, it is just a JSON object containing the NFTs name, description, properties, and image. Let's look at the simplified version of the formatTokenURI function:

function simplifiedFormatTokenURI(string memory imageURI)
    public
    pure
    returns (string memory)
{
    string memory baseURL = "data:application/json;base64,";
    string memory json = string(
        abi.encodePacked(
            '{"name": "LCM ON-CHAINED", "description": "A simple SVG based on-chain NFT", "image":"',
            imageURI,
            '"}'
        )
    );
    string memory jsonBase64Encoded = Base64.encode(bytes(json));
    return string(abi.encodePacked(baseURL, jsonBase64Encoded));
}
Enter fullscreen mode Exit fullscreen mode

This is similar to the svgToImageURI function but instead of encoding the SVG, we're going to base64 encode our entire JSON object. After passing our previously generated imageURI through this function, this is what we get:

data:application/json;base64,eyJuYW1lIjogIkxDTSBPTi1DSEFJTkVEIiwgImRlc2NyaXB0aW9uIjogIkEgc2ltcGxlIFNWRyBiYXNlZCBvbi1jaGFpbiBORlQiLCAiaW1hZ2UiOiAiZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCNGJXeHVjejBuYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TWpBd01DOXpkbWNuSUhkcFpIUm9QU2N4TURJMEp5Qm9aV2xuYUhROUp6RXdNalFuUGdvZ0lDQWdJQ0E4WkdWbWN6NDhZMnhwY0ZCaGRHZ2dhV1E5SjJFblBqeHdZWFJvSUdROUowMHdJREJvTVRBeU5IWXhNREkwU0RCNkp5OCtQQzlqYkdsd1VHRjBhRDQ4TDJSbFpuTStDaUFnSUNBZ0lEeG5JR05zYVhBdGNHRjBhRDBuZFhKc0tDTmhLU2MrQ2lBZ0lDQWdJQ0FnUEhCaGRHZ2daRDBuVFRBZ01HZ3hNREkwZGpFd01qUklNSG9uTHo0S0lDQWdJQ0FnSUNBOGNHRjBhQ0JtYVd4c1BTY2pabVptSnlCa1BTZE5NQ0F5TkRGb01UQXlOSFl5TUVnd2VrMHdJRFV3TW1neE1ESTBkakl3U0RCNlRUQWdOell6YURFd01qUjJNakJJTUhvbkx6NEtJQ0FnSUNBZ0lDQThjR0YwYUNCbWFXeHNQU2NqWm1abUp5QmtQU2ROTWpReElEQm9NakIyTVRBeU5HZ3RNakI2Snk4K0NpQWdJQ0FnSUR3dlp6NEtJQ0FnSUR3dmMzWm5QZz09In0=
Enter fullscreen mode Exit fullscreen mode

Once again copy and paste this string in your browser and you'll see the JSON object in all its glory! This is what I get:

{"name": "LCM ON-CHAINED", "description": "A simple SVG based on-chain NFT", "image": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMDI0JyBoZWlnaHQ9JzEwMjQnPgogICAgICA8ZGVmcz48Y2xpcFBhdGggaWQ9J2EnPjxwYXRoIGQ9J00wIDBoMTAyNHYxMDI0SDB6Jy8+PC9jbGlwUGF0aD48L2RlZnM+CiAgICAgIDxnIGNsaXAtcGF0aD0ndXJsKCNhKSc+CiAgICAgICAgPHBhdGggZD0nTTAgMGgxMDI0djEwMjRIMHonLz4KICAgICAgICA8cGF0aCBmaWxsPScjZmZmJyBkPSdNMCAyNDFoMTAyNHYyMEgwek0wIDUwMmgxMDI0djIwSDB6TTAgNzYzaDEwMjR2MjBIMHonLz4KICAgICAgICA8cGF0aCBmaWxsPScjZmZmJyBkPSdNMjQxIDBoMjB2MTAyNGgtMjB6Jy8+CiAgICAgIDwvZz4KICAgIDwvc3ZnPg=="}
Enter fullscreen mode Exit fullscreen mode

Perfect πŸ‘

Now let's move on to the mint function.

3. The f*cking 'mint' function

This is a standard mint function and there's not much we're doing here:

function mint(string memory svg) public onlyOwner {
   /* Encode the SVG to a Base64 string and then generate the tokenURI */
    string memory imageURI = svgToImageURI(svg);
    string memory tokenURI = formatTokenURI(imageURI);

    /* Increment the token id everytime we call the mint function */
    _tokenIds.increment();
    uint256 newItemId = _tokenIds.current();

    /* Mint the token id and set the token URI */
    _safeMint(msg.sender, newItemId);
    _setTokenURI(newItemId, tokenURI);

    /* Emit an event that returns the newly minted token id */
    emit Minted(newItemId);
 }
Enter fullscreen mode Exit fullscreen mode

First of all, we are Base64 encoding our SVG using the svgToImageURI function. After that, we pass the encoded string to the formatTokenURI function to generate the token URI.

Then, we increment the token id and mint our NFT. Lastly, we emit an event that returns the newly minted token id which will be used to generate our NFT's OpenSea link.

By adding the onlyOwner keyword, the mint function can only be called by the owner of the contract.

Compile the contract

You can compile the contract by running the following command in the root directory:

npx hardhat compile
Enter fullscreen mode Exit fullscreen mode

If everything goes correctly, you should see this on your screen:

Compiling 1 file with 0.8.4
Solidity compilation finished successfully
Enter fullscreen mode Exit fullscreen mode

Let's write the deploy script

Create a new file in the /scripts directory named deploy.js. Add the following code:

// scripts/deploy.js
const main = async () => {
  // Get 'OnChainNFT' contract
  const nftContractFactory = await hre.ethers.getContractFactory('OnChainNFT');

  // Deploy contract
  const nftContract = await nftContractFactory.deploy();
  await nftContract.deployed();
  console.log('βœ… Contract deployed to:', nftContract.address);

  // SVG image that you want to mint
  const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='1024' height='1024'>
      <defs><clipPath id='a'><path d='M0 0h1024v1024H0z'/></clipPath></defs>
      <g clip-path='url(#a)'>
        <path d='M0 0h1024v1024H0z'/>
        <path fill='#fff' d='M0 241h1024v20H0zM0 502h1024v20H0zM0 763h1024v20H0z'/>
        <path fill='#fff' d='M241 0h20v1024h-20z'/>
      </g>
    </svg>`;

  // Call the mint function from our contract
  const txn = await nftContract.mint(svg);
  const txnReceipt = await txn.wait();

  // Get the token id of the minted NFT (using our event)
  const event = txnReceipt.events?.find((event) => event.event === 'Minted');
  const tokenId = event?.args['tokenId'];

  console.log(
    '🎨 Your minted NFT:',
    `https://testnets.opensea.io/assets/${nftContract.address}/${tokenId}`
  );
};

const runMain = async () => {
  try {
    await main();
    process.exit(0);
  } catch (error) {
    console.log(error);
    process.exit(1);
  }
};

runMain();
Enter fullscreen mode Exit fullscreen mode

I've tried my best to explain what's going on through comments. It should be pretty easy to follow in my opinion.

Time to mint our NFT!

We’re finally ready to deploy the smart contract and mint our on-chain NFT. Navigate to the root of your project directory, and run the following script:

npx hardhat --network rinkeby run scripts/deploy.js
Enter fullscreen mode Exit fullscreen mode

You should see something like this:

βœ… Contract deployed to: 0x6F25096874fA386802aB30516003Df22873EeEF5
🎨 Your minted NFT: https://testnets.opensea.io/assets/0x6F25096874fA386802aB30516003Df22873EeEF5/1
Enter fullscreen mode Exit fullscreen mode

The live contract can now be viewed on Etherscan Rinkeby Testnet Explorer. Also, you can look at your freshly minted NFT on Opensea.

LFG πŸš€

See the full code

Enough chit-chat! if you want to see the full code, you can find the Github repo here.

What's next?

I plan to create a frontend react app where the users can interact with this contract. You will be able to copy-paste your SVG code and mint your on-chain NFTs πŸ”₯


Don't leave me, take me with you

Like what you read? Follow me on social media to know more about NFTs, web development, and shit-posting.

Twitter: @lilcoderman

Instagram: @lilcoderman

Top comments (1)

Collapse
 
onlyonealexia profile image
Onlyonealexia

so we tried following your article in web3bride, during classworks and we had to edit the{
constructor() ERC721("OnChainNFT", "ONC") Ownable(msg.sender) {}
} before it worked.
we also this await nftContract.deployed;