In this tutorial we’re going to create our own ERC721 NFT collection and publish it on the Opensea marketplace.
Our NFT collection will consist of 3 items of dog images. You can find the collection here: https://drive.google.com/drive/folders/1eV9RCOhXCkBmWvMHURyRI21sWfmW-W0S?usp=sharing. We are going to publish this collection on the Opensea rinkeby testnet.
Here is the result collection: https://testnets.opensea.io/collection/my-nft-dogs-v2
The full source code: https://github.com/ryzhak/my-nft
How it works basically:
- 1. Create NFT images and upload them to IPFS (or whenever you want)
- 2. Create metadata files describing those NFT images and upload them to IPFS(or whenever you want).
- 3. Create an NFT smart contract.
- 4. Deploy NFT smart contract.
- 5. Publish NFT collection on Opensea.
Initial setup
Create a new folder my-nft somewhere on your hard drive. Inside the my-nft folder run npm init -y to initialize an empty npm project. Then run npm install truffle -g to install the truffle framework globally.
Initialize a new truffle project via truffle init. Then run npm install @openzeppelin/contracts –save to install Openzeppelin contacts. Openzeppelin maintains a set of community trusted smart contracts where you can also find ERC721 contracts for NFT collections.
Creating NFT images
Luckily for us we already have our images designed. You can find them at https://drive.google.com/drive/folders/1eV9RCOhXCkBmWvMHURyRI21sWfmW-W0S?usp=sharing. Create a new folder images in the project root and copy all 3 dog images there.
We are going to use the https://nft.storage/ service to upload our images to IPFS. To upload any files to https://nft.storage we need to convert those files to the CAR format.
Run npm i ipfs-car –save to install the converter. Now convert the images folder to the images.car format via ./node_modules/.bin/ipfs-car –pack images –output images.car.
Then go to https://nft.storage/files/ and upload images.car file.
Creating NFT metadata
Each image must have a corresponding metadata according to Opensea docs. Create a new folder metadata in the project root.
Create a file “1” (without any extension) with the following JSON content:
1 2 3 4 5 |
{ "description" : "Friendly Doggo that enjoys life.", "image" : "https://bafybeidw7n7catw5jra6judyqvufifesqleijiaqxc6qhqupalbvogfdna.ipfs.nftstorage.link/images/1.png", "name" : "John Dog" } |
Create a file “2” (without extensions) for the token with id 2:
1 2 3 4 5 |
{ "description" : "Friendly Doggo that enjoys life.", "image" : "https://bafybeidw7n7catw5jra6judyqvufifesqleijiaqxc6qhqupalbvogfdna.ipfs.nftstorage.link/images/2.png", "name" : "Bob Dog" } |
Create a file “3” (without extensions) for the token with id 3:
1 2 3 4 5 |
{ "description" : "Friendly Doggo that enjoys life.", "image" : "https://bafybeidw7n7catw5jra6judyqvufifesqleijiaqxc6qhqupalbvogfdna.ipfs.nftstorage.link/images/3.png", "name" : "Snoop Dog" } |
Now convert the metadata folder to metadata.car format via ./node_modules/.bin/ipfs-car –pack metadata –output metadata.car. Then go to https://nft.storage/files/ and upload the metadata.car file there. The URL from https://nft.storage/files/ which contains the metadata folder is going to be our base token URI in the ERC721 NFT smart contract.
Creating ERC721 NFT contract
Run truffle create contract MyNFT to create a new contract. Edit the contracts/MyNFT.sol file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
// SPDX-License-Identifier: MIT pragma solidity >=0.4.22 <0.9.0; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; /** * @title Our NFT contract */ contract MyNFT is ERC721, Ownable { // use Counters library using Counters for Counters.Counter; // max supply is 10k items uint256 public constant TOTAL_SUPPLY = 10000; // token counter Counters.Counter private currentTokenId; // base token URI for metadata string public baseTokenURI; /** * Contract constructor */ constructor() ERC721("My NFT dogs", "NFTDGS") { baseTokenURI = ""; } /** * @dev Mints a new token to recipient address * @param recipient new token owner address * @return tokenId minted token id */ function mintTo(address recipient) public onlyOwner returns (uint256) { // check that max supply is not reached uint256 tokenId = currentTokenId.current(); require(tokenId < TOTAL_SUPPLY, "Max supply reached"); // increase current token count and mint it to a new owner currentTokenId.increment(); uint256 newItemId = currentTokenId.current(); _safeMint(recipient, newItemId); return newItemId; } /** * @dev Sets base token URI * @param _baseTokenURI base token URI */ function setBaseTokenURI(string memory _baseTokenURI) public onlyOwner { baseTokenURI = _baseTokenURI; } /** * @dev Returns base token URL * @return baseTokenURI base token URI */ function _baseURI() internal virtual override view returns (string memory) { return baseTokenURI; } } |
Above we created a smart contract with the following features:
- – NFT token description: “My NFT dogs”
- – NFT token short name: “NFTDGS”
- – Max supply is restricted to 10k items
- – Only owner can mint new items
- – Only owner can set base token URI
Run truffle compile to create contract build files.
Notice about token URIs.
Each NFT token should have a unique URI which is basically a URL address. Example of token URI with id 1: https://example.com/tokens/1. When dealing with Opensea this URI should return a JSON file with metadata in a special format. This format also contains the image field which is basically our NFT image. Our smart contract has a method setBaseTokenURI which must set the base URL for token URI (https://example.com/tokens/) on contract deploy.
You can check how token URI is created here https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L97. If base token URI exists then concatenate base token url with token number. If the base token URI does not exist then return an empty string.
So you can see that anything can be converted to NFT.
Deploying NFT smart contract
Install metamask and create a new account. Save your mnemonic seed phrase as we are going to use it later.
In metamask connect to the rinkeby network. Use any rinkeby faucet to send some ETH to the 1st account in your metamask wallet.
Register at https://www.alchemy.com/ and create a new app connected to the rinkeby network. In the alchemy dashboard you should see the API_URL which we are going to use later.
Install library for working with accounts via npm install @truffle/hdwallet-provider –save.
Run npm install dotenv –save to install dotenv library (which helps to store secrets in a pretty secure way).
Create a new .env in the project root with the following content:
1 2 |
API_URL = "https://eth-rinkeby.alchemyapi.io/v2/YOUR_API_KEY" MNEMONIC = "YOUR_MNEMONIC" |
Here API_URL is the Alchemy API URL (which you can find in the Alchemy dashboard) and MNEMONIC is your seed phrase which you used when creating a new account in metamask wallet.
Update your truffle-config.js file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
require('dotenv').config(); const HDWalletProvider = require("@truffle/hdwallet-provider"); const { API_URL, MNEMONIC } = process.env; module.exports = { // Configure available networks networks: { rinkeby: { provider: function() { return new HDWalletProvider(MNEMONIC, API_URL) }, network_id: 4, } }, // Configure your compilers compilers: { solc: { version: "0.8.13", // Fetch exact version from solc-bin (default: truffle's version) } }, }; |
Here we added a rinkeby network configuration.
Now create a new file migrations/2_deploy_contracts.js with the following content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const MyNFT = artifacts.require("MyNFT"); module.exports = async function(deployer, network, accounts) { // deploy our NFT contract await deployer.deploy(MyNFT); // get contract instance const instance = await MyNFT.deployed(); // set base token URL await instance.setBaseTokenURI('https://bafybeicc6yg3ramjkhwvyfouyfi4apb4fejepv76dn75dzm4jxetus23cq.ipfs.nftstorage.link/metadata/'); // mint 3 tokens to our address for (let i = 0; i < 3; i++) { await instance.mintTo(accounts[0]); } }; |
Here we:
- 1. Deploy our NFT contract
- 2. Set base token URI. NOTICE: you should update base token URI in the script to your own address. Base token URL is the metadata folder url which you can find at https://nft.storage/files/.
- 3. Mint 3 NFT tokens to our own address.
Now run truffle migrate –network rinkeby to deploy the smart contract to the rinkeby testnet. You should see the following console output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
Starting migrations... ====================== > Network name: 'rinkeby' > Network id: 4 > Block gas limit: 29970705 (0x1c95111) 1_initial_migration.js ====================== Replacing 'Migrations' ---------------------- > transaction hash: 0xe6c2dd2925f686fe22a271f008d99722526d97db2796078c4711a7141c8203bc > Blocks: 0 Seconds: 9 > contract address: 0xB0b29c4933C8bd4dcB13E53339EED35e04710631 > block number: 10463184 > block timestamp: 1649339444 > account: 0x034A68a6BA5a51a5EF4CdD475a6331256cfDcA03 > balance: 0.083991899669164336 > gas used: 250154 (0x3d12a) > gas price: 2.514751843 gwei > value sent: 0 ETH > total cost: 0.000629075232533822 ETH ✓ Saving migration to chain. > Saving migration to chain. > Saving artifacts ------------------------------------- > Total cost: 0.000629075232533822 ETH 2_deploy_contracts.js ===================== Replacing 'MyNFT' ----------------- > transaction hash: 0x3d1faead29d46c578e65714361aef5715d13c25e5bbb7a1ff407e982ba9e5dc8 > Blocks: 0 Seconds: 13 > contract address: 0xD0cF0b4D240AaA7b4FE67723FC995779c01b4fE3 > block number: 10463186 > block timestamp: 1649339474 > account: 0x034A68a6BA5a51a5EF4CdD475a6331256cfDcA03 > balance: 0.076854131562758228 > gas used: 2792278 (0x2a9b56) > gas price: 2.514903752 gwei > value sent: 0 ETH > total cost: 0.007022310418827056 ETH ✓ Saving migration to chain. > Saving migration to chain. > Saving artifacts ------------------------------------- > Total cost: 0.007022310418827056 ETH Summary ======= > Total deployments: 2 > Final cost: 0.007651385651360878 ETH |
You can see that our NFT contract is deployed at address 0xD0cF0b4D240AaA7b4FE67723FC995779c01b4fE3.
Publishing NFT collection on Opensea
Now open Opensea get listed page, click on the “Live on a testnet” button, enter your NFT contract address and hit “Submit”. You should see your NFT collection live https://testnets.opensea.io/collection/my-nft-dogs-v2.
The full source code can be found here: https://github.com/ryzhak/my-nft
Summary
In this tutorial we learned how to deploy images and metadata to IPFS, created our own NFT smart contract, deployed it on the rinkeby testnet and published our NFT collection on Opensea. Now you should have a basic understanding of how NFTs work.