In 2022, I worked at AAX, a cryptocurrency exchange, where I served as the principal software engineer at AAX's Impact Lab. This was a sub-brand of AAX created to enhance brand awareness for AAX through partnerships with brands such as HTC and Elle Magazine. Although AAX went insolvent only 6 months after I joined the company, I was able to learn a lot about Web3 development while working on the ELLEverse project.

ELLEverse was a charity NFT project spearheaded by Elle magazine and executed by various partners, with Impact Lab serving as the technical partner. The idea behind ELLEverse was to let its participants mint and "encode" a secret love message in the form of a uniquely generated NFT that can be shared with and "decoded" by a "special person".

How it worked

To mint the token, participants had to first connect their wallets. If the event was in its public sale phase, or if the participant owned a whitelist token, they could proceed to enter a love message, a passcode to decode the message, and an ETH amount to be donated to the SPCA.

They would then have to pay this donation amount plus a gas fee in order to mint the NFT.

Upon successfully minting, participants would receive a "message token" in their wallet. A unique design would cover the token's message, which would remain hidden until the token was decoded.

Minting would also generate a unique link for participants to share the token with their loved one. The recipient could use this link to enter a passcode to decode the NFT, revealing its message.

Minters would also receive a second token, of which its nature was determined by the event's sale phase. If the event was in its private sale phase, the additional NFT would be a "whitelist token", as Elle sought to make the event feel exclusive for its participants by limiting token minting opportunities to those who possessed a whitelist token. If the event was in its public sale phase, the additional NFT would be a "prize token", which participants could use to redeem a prize from a lucky draw after the event concluded. You can see how it works from the below video.

How I built this

ELLEverse was my first foray into building a full stack Web3 application. As a long-time user of Web3 dApps, I was eager to learn what it was like to develop smart contracts and build a production-ready dApp.

My frontend framework of choice was NextJS, a popular React framework. I chose it mainly because it has excellent developer experience and integrates well with Vercel without needing any additional setup. Additionally, it plays well with Hardhat, my backend framework of choice. I chose Hardhat because it provides easy access to a suite of tools and features that simplify building, testing and deploying smart contracts. For example, it comes with numerous popular packages for blockchain development and also allows you to spin up a local blockchain node to easily test and run your smart contracts in different environments. Testing smart contracts is especially important because of how costly it could be, financially and otherwise, to make changes to a smart contract once deployed. It was easy to fit Hardhat into my NextJS repository and work on both the backend and frontend within the same codebase.

In terms of styling the website, Tailwind was my CSS framework of choice. I liked Tailwind CSS because of the following reasons: Firstly, it allows for a high degree of customization, which makes it easy for me to manage and update the unique appearances of the site's UI elements. Secondly it generates minimal and optimized CSS code, leading to faster page load. Lastly, it has a large community, so it would be fast for me to identify solutions to any obstacles I encounter during development. In the end, I was happy that I chose to use Tailwind CSS because it was fast for me to tweak and update our designs, which constantly changed over the course of the project due to the whims of the marketing team.

Technical Challenges

The project posed several technical challenges. One particular challenge I faced had to with how our partners wanted to implement the promotional event. During the initial few weeks, there would be a private sale phase where only users with a "whitelist token" could mint NFTs. Participants would have to obtain this token either by receiving it from another user, or going through the minting process where both a message token and a whitelist token are simultaneously minted. To support this feature, I had to create a method in the smart contract that would effectively allow the user to mint 2 different types of tokens with 1 mint action. This was complex because ERC-721 contracts are designed to store 1 type of token. In our case, the 2 tokens had different functions, and the whitelist token existed only to allow its holders to mint during the private sale phase. Holders of the message token would not be entitled to mint. I thus also had to create a method in the smart contract that checked whether or not a user owned a whitelist token.

My solution was honestly quite simple. In ERC-721, the token ID is represented by an integer that starts at 0 and increments by 1 each time there's a new token. Since there were 2 different types of tokens, and a single mint action would mint both of them, I figured I could determine whether a token was a message token or a whitelist token based on whether its ID was odd or even. I then created a function called ownsWhitelistToken that loops through the tokens in the contract to check whether the ID was odd or even.

function ownsWhitelistToken(address _owner) public view returns (bool) {
  if (balanceOf(_owner) >= 1) {
    for (uint256 i = 1; i < _nextTokenId(); i += 2) {
      if (_exists(i)) {
        if (ownerOf(i) == _owner) {
          return true;
        }
      }
    }
  }
  return false;
}

Upon connecting to a wallet, the website would then be able to call this function to detect whether or not the user can mint.

Another challenge had to do with developing the animations on the NFTs. Since the designs of the NFTs were based on what the user wrote in their messages, the animations couldn't be pre-generated; rather, they needed to be dynamically rendered through code.

The NFT contained a string of text designed to scroll clockwise around the image frame. However, there was no way for a letter in the text container to detect if it had reached the end of the frame and rotate accordingly. Therefore, the only way to build this animation was to create 4 identical strings of text on the 4 different sides of the square frame and transition them from right to left in terms of text direction.

To create the rotating effect, I had to ensure that the letter disappeared at the end of its side of the frame right before appearing on the next connecting side of the frame. To achieve this, I had to take into account both the length of the entire text element, which varies depending on the input string, and the width of the NFT container. This was necessary in order to determine the correct starting points of the animation, to achieve the desired timing.

To implement this, I first used window.getComputedStyle to get the width of the text element. Then, after adjusting for paddings and extra spaces, I calculated the animation duration by dividing the total width by the animation velocity. I then applied this animation duration, the starting positions of each frame-side text (using position: absolute), and an infinitely looping transition to the styles of each side of the frame. This was how I created the effect of connecting frame texts.

Developing the lucky draw page also came with its own set of challenges. The marketing team wanted to create an interactive lucky draw page that would appeal to the prize token holders. This meant the lucky draw animation had to create an element of surprise so that it would seem as if the prize was randomly selected, even though it had been predetermined. To accomplish this, I decided to create a slot reel with a spinning effect.

To arrange the reel, the website would first use the user's wallet address and prize token ID to fetch the name of the winning prize from a centralized database. This name would be inserted into a SlotReelItem element, which would then be appended to an array of other SlotReelItems consisting of random prize names. I then applied the following 3 animation keyframes to an ease-in-out transition to create the spin effect:

const getKeyFrames = (reelItemHeight: number) => [
  { transform: 'none', filter: 'blur(0)' },
  { filter: 'blur(1px)', offset: 0.5 },
  // "(Number of items - 1) * height of reel items" of wheel is the amount of pixel to move up
  {
    transform: `translateY(-${reelItemsLength * reelItemHeight}px)`,
    filter: 'blur(0)',
  },
]

Essentially, the slot reel animation would end up showing the predetermined prize, since it would always be the last element in the SlotReel.

Conclusion

Over the course of the project, I learned how much frontend work goes into building dApps. NFT projects in particular tend to be flashy and filled with animation to, I believe, attract potential NFT buyers, so I ended up learning a lot building out CSS transitions and animations.

I also learned how unpractical the Ethereum blockchain could be in terms of cost efficiency and application. For instance, simply checking if a token belonged to a certain wallet address requires iterating through a list of token owners. Even just checking if 2 strings matched requires first encoding both strings and then hashing them using Keccak256. This could arguably be a flaw of Solidity as a language itself, but it affects the entire smart contract development experience as you need to think about how best to optimize gas usage every line you write.

I also learned how troublesome and painful it could be to develop on the blockchain. It felt wasteful to make multiple (immutable) contract deployments onto Testnets just to test out how small changes could affect how the collection looks like on OpenSea and Rarible. In our case, the lack of technical experience and knowledge that the marketing team and our PM had made it quite a hassle.

Overall, it was an interesting experience that I am glad to be a part of.