engineering, meditation, and other thoughts

Building an NFT project from scratch

Building an NFT project from scratch

I've been itching to try my hand at coding in web3, and what started as a brief exploration quickly became a project to launch end to end with all the bells and whistles that come with a production-grade software project.

My talented friend Brent[0] doodled a collection of pixel art based on vices, and I decided to make an NFT collection out of it.

The main building blocks of the project are:

  1. Image combination and metadata generation algorithm. Along with managing pins via Pinata.
  2. Solidity & the smart contract
  3. Landing page & minting functionality
  4. Server for serving landing page data, metadata, the images, and managing Opensea listings

Image Combination

Brent provided 7 background images and 14 foregrounds, and I combined and upscaled them in Python with PIL to get 98 pngs.

The program also generates NFT metadata in the format Opensea expects it.

{
    "dna": "eebbff9f829740f4ef233d22474b8725fc9406f25025a608b7ff00ca",
    "name": "Metadreams # 0",
    "description": "What dreams are made of",
    "image": "https://ipfs.io/ipfs/QmSwxmWYdHhRP3aMErg4CWthhHgwSWMUcm78iKekGWtBhn",
    "edition": 1,
    "date": "1635482449831",
    "artist": "burn",
    "dev": "pokonono",
    "attributes": [
        {
            "trait_type": "Background",
            "value": "metadreams-molly1"
        },
        {
            "trait_type": "Dream",
            "value": "metadreams_0000_dawn"
        }
    ]
}

I compute the provenance hash[1] from the image files, which we'll use later in the smart contract.

Pinata was pretty straightforward to use, even though their UI has a lot to be desired (bulk deletes please?). Their minimal API was functional enough where I could do everything I need, pin, unpin, pinList, so it ended up being my choice.

By generating images in parallel to take advantage of my 16 core machine, I was able to cut the generation time down by around 10x. Python made this really easy with pool = mp.Pool(mp.cpu_count()) pool.apply_async .

Solidity & the Smart Contract

Solidity isn't as hard as it seems; it's a tiny library one can pick up in a few hours. What's difficult is anticipating and identifying all the bugs therein. Luckily, NFT contracts are very straightforward. There are plenty of popular NFT projects where you can look at their contracts on etherscan[2].

Note that Opensea testnet is on Rinkeby, so that's your best choice for testnet.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;

import "@openzeppelin/contracts/access/Ownable.sol";
import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol';

contract Metadreams is ERC721Enumerable, Ownable {
    using Strings for uint256;

    string _baseTokenURI;
    uint256 private _reserved = 2;
    uint256 private _price = 0.06 ether;

    uint256 public constant maxSupply = 98;
    bool public _paused = false;
    string public DRUG_PROVENANCE = "b75d85c9aa5a4b91a1fa5be552273a025ced656d6a3bacf7ab7a92b4745f79cc";

    // withdraw addresses
    address t = 0x0000000000000000;

    constructor(string memory baseURI) ERC721("Metadreams", "DRUG")  {
        setBaseURI(baseURI);

        // team gets the first dream
        _safeMint(t, 0);
    }

    function mint(uint256 num) public payable {
        uint256 supply = totalSupply();
        require(!_paused, "Sale paused");
        require(num < 2, "You can mint a maximum of 1 dream");
        require(supply + num < maxSupply - _reserved + 1, "Exceeds maximum dream supply");
        require(msg.value >= _price * num, "Ether sent is not correct");

        for (uint256 i; i < num; i++) {
            _safeMint(msg.sender, supply + i);
        }
    }

    function pause(bool val) public onlyOwner {
        _paused = val;
    }

    function getPrice() public view returns (uint256){
        return _price;
    }

    function setPrice(uint256 _newPrice) public onlyOwner() {
        _price = _newPrice;
    }

    function setProvenanceHash(string memory provenanceHash) public onlyOwner {
        DRUG_PROVENANCE = provenanceHash;
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return _baseTokenURI;
    }

    function setBaseURI(string memory baseURI) public onlyOwner {
        _baseTokenURI = baseURI;
    }

    function walletOfOwner(address _owner) public view returns (uint256[] memory) {
        uint256 tokenCount = balanceOf(_owner);

        uint256[] memory tokensId = new uint256[](tokenCount);
        for (uint256 i; i < tokenCount; i++) {
            tokensId[i] = tokenOfOwnerByIndex(_owner, i);
        }
        return tokensId;
    }

    function withdrawAll() public payable onlyOwner {
        require(payable(t).send(address(this).balance));

    }

}
the smart contract in its entirety

After completing the contract, I installed hardhat, compiled and deployed the contract. To be able to connect to the blockchain, I signed up for Alchemy. Infura is a similar product and also a common choice.  As a last step, I grabbed an Etherscan API key and used @nomiclabs/hardhat-etherscan - this lets users validate contracts from the command line. Validating the contract is surprisingly finicky otherwise.

Landing Page & Minting

As an infra engineer, front-end engineering is IMO the hardest type of engineering because one has to know so much, work with different tools that don't necessarily work well together, and the space moves so fast. If any front-end engineers are reading this and want to share tips on how to get better, ping me on Twitter!

I used NextJS to bootstrap but did not use their Server Side Rendering, since the site is so simple. I want to optimize for serving speed by putting the whole thing up in a CDN.  

landing page for metadreams

The landing page is where most of my time went. Just handling all the different states that can occur when a user attempts to connect, mint, including various errors, connection issues, gas prices, etc was super tedious, and I'm sure solutions will soon arise to make this easy.

I had to learn (read: copy) a lot of CSS to make things look ok and be responsive, and styling was where I spent ~50% of all my time in this entire project 😱. I really hope there's a better way to style apps in the future.

The libraries I used here were WalletConnect, web3.js, Web3Modal. web3.js was easy to use and the documentation was great. I also tried ethers.js, which has a lot of great things going for it - it's a much smaller library, the separation of wallet and provider, but in the end, I felt the documentation wasn't strong enough, which as a newbie is a no-go.

With the abi generated from hardhat compile from the previous step, it's pretty straightforward to use with web3 and start writing code against the contract. I iterated a couple of times on the contract to get all the methods and variables I needed.

I rabbitholed into the unnecessary but satisfying task of trying to reduce my compiled code size, beginning with 3MB  and reducing it down to 1.14MB and then down to 309kB.

I did this by installing a custom webpack resolver to remove all repeated instances of bn.js , removing the node dependencies for web3.js and walletconnect with minified scripts to be loaded, reducing image sizes. All of this reduces the compiled size by 10x.

What also surprised me was that I wasn't able to find a good Modal React component. I tried MUI and Bootstrap, and ended up writing my own with functionality like clicking outside the box to close. Overall, I'm surprised that in 2022 there is still so much boilerplate code to write for a basic app. Again, I'm not an expert here, ping me to share!

Server

A server is not necessary for most NFT projects, but I wanted a bit more finesse with the metadata reveals and a layer of indirection for functionality I might add in later.

The server starts up and queries the state on the blockchain to get data on the contract and see how many NFTs are minted. It starts up a cron process to query every hour for state. The server gates reveals by serving up metadata dynamically. Every hour, based on the new NFTs that were minted,  the server makes calls to Opensea's api to refresh Opensea's metadata for new reveals. It also provides basic state for the front end to query.

Express.js was a joy to use, with so many great 3rd party plugins. I was against writing server code in a scripting language for the longest time, but it's so easy to move quickly and I see the light now.

Summary

This was a fun project that helped identify a couple of interesting gaps in the ecosystem. You can check out the end product at metadreams.dev [3], and say hi on Twitter [4].

Upcoming posts include thoughts on web3, engineering management for dummies, how to hire your first engineer / engineering leader. Subscribe or check back in!

[0] Brent is awesome, follow him on Twitter: https://twitter.com/burnto

[1] Article on provenance hash https://medium.com/coinmonks/the-elegance-of-the-nft-provenance-hash-solution-823b39f99473

[2]Bored Ape Yacht Club's contract https://etherscan.io/address/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d#code

[3] Metadreams NFT website https://www.metadreams.dev/

[4] Come say hi! https://twitter.com/emmaytang

Subscribe to Emma Tang

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe