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.
- Node.js - I recommend installing Node using nvm
- MetaMask - An Ethereum browser wallet
- Hardhat - An Ethereum development environment to compile, deploy, and test our smart contracts
- ethers.js - A JavaScript library to interact with our deployed smart contract
-
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);
}
}
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
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));
}
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==
Paste this string in your browser and you'll see our SVG. Here's how my SVG looks:
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));
}
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=
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=="}
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);
}
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
If everything goes correctly, you should see this on your screen:
Compiling 1 file with 0.8.4
Solidity compilation finished successfully
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();
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
You should see something like this:
β
Contract deployed to: 0x6F25096874fA386802aB30516003Df22873EeEF5
π¨ Your minted NFT: https://testnets.opensea.io/assets/0x6F25096874fA386802aB30516003Df22873EeEF5/1
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)
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;