fbpx

Diving into Web3: Building a Ruby on Rails NFT Minting Application from Concept to Reality

Diving into Web3: Building a Ruby on Rails NFT Minting Application from Concept to Reality
Reading Time: 10 minutes

Proving the concept works

In early 2022, as the hype of Web3 came to a peak, I was asked to develop a proof of concept (POC) in the form of a Ruby on Rails application to mint NFTs for a client. The idea was to allow a user to enter a valid alphanumeric code into an application and redeem a unique NFT. Despite dealing with something simple in theory, I knew very little about the inner workings of Web3. Taking on the POC presented the perfect opportunity for me to dive into the world of blockchains, all while building a solid application for our client.

A Lesson on Web3

Before writing a single line of code, the first thing I needed to do was learn about Web3. In this section I’ll attempt to cover all of the topics and nomenclature you need to know about Web3.

Blockchains

Blockchains are decentralized ledgers that keep track of transactions. Per IBM’s definition of a blockchain, virtually anything can be traded on a blockchain. Polygon is an example of a layer 2 Ethereum blockchain. This means Polygon is essentially an extension of Ethereum, with many of the same features, but with lower gas prices. Gas price usually refers to the small amount you pay every time you submit a transaction on the blockchain. As Ethereum’s usage has skyrocketed, many layer 2 blockchains have emerged as substitutes, allowing for similar features, transferability to Ethereum itself, and low gas prices. Polygon is a production blockchain, meaning all funds used there are real. It’s therefore not the best place to build and test a project. Luckily, Polygon, along with many other blockchains, has its own testnet. Polygon’s testnet is known as the Mumbai Testnet. For the purposes of this article we will be using Mumbai as the primary blockchain.

Smart Contracts

Smart contracts are a form of transaction that push a set of rules onto the blockchain. These rules can be executed, much like a function in a class in object-oriented programming languages.

Non-Fungible Tokens (NFTs)

NFTs are a type of digital asset that live on the blockchain. They usually contain some metadata that makes them unique from other NFTs. This metadata is stored on the NFT itself as a link to an IPFS pin. We will cover IPFS a little later, but for now, think of the metadata as a JSON file stored on the web that the NFT accesses via a link. NFTs tend to come in collections and share a theme. NFTs of the same collection also come from the same smart contract; the smart contract they were minted on. This article has a great overview of NFTs and their relationship to smart contracts.

OpenZeppelin

Smart contracts are fully customizable, so how can we make sure our smart contract correctly mints NFTs? OpenZeppelin provides consistent implementations of many different types of smart contracts that serve as interfaces. A common NFT smart contract standard is ERC-721, which provides all expected behaviors of NFTs. This is the standard we used for our custom smart contract.

Solidity

So far, I’ve mentioned that smart contracts are customizable, but how does someone go about writing their own smart contracts? The answer is Solidity. Solidity is an object-oriented programming language used for writing smart contracts that can be compiled and deployed onto a blockchain (most notably Ethereum). Smart contracts are written much like a class in other object-oriented languages. Despite being a new language for me, Solidity reads a lot like other object-oriented languages. Along with the Solidity documentation, I was able to get a good understanding of some Solidity basics.

Metamask

In order to interact with smart contracts, transactions, and other users, we need a way to connect to the blockchain. To do this, we need to make a wallet. A wallet holds an address on the blockchain that represents the user. To send cryptocurrency to your friend, for example, you would need their wallet’s address, in order to send the funds to the correct location on the blockchain. Metamask provides a quick and easy way to connect to many blockchains (Metamask).

Truffle Suite

Metamask provides us with a wallet, and Solidity allows us to write smart contracts, but we now need to combine these functionalities in Truffle Suite. This framework allows us to take our contracts and deploy them to a blockchain of our choice, under the ownership of our wallet. Here is a more in depth guide. Once we deploy a smart contract we receive its address on the blockchain in return. This is an important value to store somewhere safe, as it is needed to interact with our contract.

Testnet Faucets

Deploying smart contracts and minting NFTs on any blockchain requires us to pay gas prices with cryptocurrencies like Eth in Ethereum or Matic In Polygon. As mentioned earlier, testnets exist to allow us to use worthless cryptocurrencies. In order to get these “fake” cryptocurrencies, we need to use a faucet. Here is Mumbai’s faucet. You simply enter your wallet address and test Matic is sent to you. This allows us to have the funds to deploy smart contracts.

Designing and building the POC

After deepening my understanding of the Web3 world and NFTs, I was ready to start designing and implementing the project itself.

Custom Smart Contract

Since the client wanted to mint their own NFTs, we’d needed a custom smart contract using the ERC-721 standard. Now, there are many examples of ERC-721 contracts available online, but ours needed a twist. Specifically, we wanted to mint NFTs using lazy minting. Normally, smart contracts mint a set number of NFTs (think upwards of 1,000) when they are deployed, all of which belong to the smart contract creator. NFTs are then sold by the creator to whomever they choose. The problem with this approach is two fold. One, minting thousands upon thousands of NFTs at once can be very pricey, especially in our case where the NFTs are not sold, but rather handed out for free. And two, we want newly minted NFTs to belong to someone other than the contract creator. The solution to this problem is to create a smart contract that does nothing upon deployment, but has a mintToken() function that can be called later. This is known as lazy minting. Everytime a user redeems a code, an NFT is minted to their wallet. We wrote our mintToken() to accept a recipient address as well as metadata to be associated with the NFT minted. Here is the smart contract code:

contract CustomNFT is ERC721URIStorage, Ownable {
  using Counters for Counters.Counter;
  Counters.Counter private _tokenIds;
  address payable private ownerAddress;
  constructor() ERC721("CustomNFT", "CNFT") {
    ownerAddress = payable(msg.sender);
  }
  function mintToken(address recipient, string memory tokenURI) public onlyOwner returns (uint256) {
    _tokenIds.increment();
    uint256 newItemId = _tokenIds.current();
    _safeMint(recipient, newItemId);
    _setTokenURI(newItemId, tokenURI);
    return newItemId;
  } 
}

Using Truffle Suite, and loosely following the guide mentioned earlier, I was able to compile and deploy this contract to Mumbai Testnet. We now have the contract address (which we will denote as contractAddress), as well as the contract abi (a serialized form of the smart contract) from compiling the contract. One thing to point out here is the onlyOwner keyword used in the mintToken function. This means that only the owner of the smart contract (whoever deploys it, me in this case) can call the mintToken function. This prevents people from accessing my smart contract and minting as many NFTs as they wish, which would cost me a ton in gas. At this point all of the functionality of this contract was available for use, but accessing it in its raw form was very complicated. This means it was time to build a web application to provide easy access to our contract and to make minting NFTs much simpler.

Considering NFT Metadata

Before we get into the web application, I want to cover IPFS. NFT metadata is what makes different NFTs from the same contract unique from one another. This metadata is the actual content held by the NFT, and it is usually represented as a link to an image or other content. A standard storage place for NFT metadata is IPFS. InterPlanetary File System (IPFS) is a peer-to-peer file storage system. Its decentralized nature lends itself well to the decentralization of web3. For the POC, I used Pinata as a place to pin NFT metadata to IPFS. Each file stored on IPFS has its own url, which will be passed to our mintToken() mentioned earlier.

Designing the Web Application

After writing and deploying our smart contract onto Mumbai Testnet using Truffle Suite, it was time to build a full-stack Ruby on Rails application to connect to our contract and start minting NFTs.

The NFT Code Record

The minimum viable product described by the client included the ability to redeem a code in exchange for their own NFT. That means that every single NFT the client planned on having available to mint would have its own associated alphanumeric code. Each code would also correspond to a certain image pinned on IPFS. When a user enters their code, the application should check to see if it is valid. If it is, it should fetch the associated metadata, and mint an NFT with that metadata. Also, once a code is redeemed, it should not be available to redeem again. This scenario is perfect for leveraging Ruby on Rails’ Active Record. With these considerations in mind, the  NFT Code record ended up looking like this in:

NftCode(id: integer, code: string, created_at: datetime, updated_at: datetime,
code_redeemed: boolean, metadata_uri: string, image_uri: string)

With this record now available, and some test data populated in our MySQL server, it was time to build the api.

The Web3 Controller

I now needed an API endpoint to take in a code, and fetch the correct NFTCode record. Later on we will also add the logic to mint to the actual smart contract.

For now, the logic is simply this:

class Api::Web3Controller < ApplicationController
  # Endpoint for minting an nft
  def mint_nft
    if params[:code].present?
      code = NftCode.find_by(code: params[:code])
      if !code.present? || code.code_redeemed
        respond_to do |format|
          format.json { render json: "Invalid code entered", status: :not_found }
        end
      end

      code.update(code_redeemed: true)

      #Minting logic to go here

      respond_to do |format|
        format.json { render json: "NFT Minted", status: :ok }
      end
    else
      respond_to do |format|
        format.json { render json: "No code entered", status: :bad_request }
      end
    end
  end
end

This endpoint accepts a “code” parameter that represents the alphanumeric code entered by the user. If no code is passed to the endpoint, we return bad_request. If a code is passed, the endpoint uses Active Record to fetch an NFTCode record with a matching code. If no matching record is found, or if the matching record was already redeemed, we return not_found. Finally, if a matching record that has not yet been redeemed is found, we update the NFT Code to reflect its new redeemed status. We now need to add logic to mint the NFT itself.

Eth.rb

Eth.rb is a Ruby gem that allows for Web3 interaction. One of its latest features at the time of building the POC was its ability to interact with smart contracts. 

First, we needed to connect to the Mumbai Testnet. We did so by creating an RPC client and passing Mumbai’s RPC url (https://rpc-mumbai.maticvigil.com/). We then created a contract object using the contract address, contract abi, and contract name. These three values are used to identify the contract on the blockchain. Next, with the transact_and_wait() function we could call a function on a smart contract, like mintToken() on ours. Its parameters are the contract object we made earlier, the name of the function we wish to call, as well as the params that the function requires. 

Before we continue, there is one consideration we haven’t mentioned so far. The mintToken function could only be called by me, as the creator of the smart contract. To properly confirm that this transaction was approved by me, it needed to be signed by my private key. Every user wallet on the blockchain has a private alphanumeric key that essentially serves as the password for the wallet. As such, it serves as the ultimate proof of identity on the blockchain. The transact_and_wait() takes in a sender_key param that is used by Eth.rb to sign the function, allowing it to succeed. The updated controller looks like this:

class Api::Web3Controller < ApplicationController
  # Endpoint for minting an nft
  def mint_nft
    if params[:code].present? && params[:user_address].present?
      code = NftCode.find_by(code: params[:code])
      if !code.present? || code.code_redeemed
        respond_to do |format|
          format.json { render json: "Invalid code entered", status: :not_found }
        end
      end

      rpc_url = Eth::Client.create rpc_url_string
      contract = Eth::Contract.from_bin(bin: contract_bin, abi: contract_abi, name: contract_name)
      ticket = rpc_url.transact_and_wait(contract, "mintToken", params[:user_address].to_s, code.metadata_uri, sender_key: private_key)

      code.update(code_redeemed: true)

      respond_to do |format|
        format.json { render json: "NFT Minted", status: :ok }
      end
    else
      respond_to do |format|
        format.json { render json: "No code entered", status: :bad_request }
      end
    end
  end
end

From here our endpoint was able to connect to Mumbai Testnet, our smart contract, and mint an NFT. However, we were missing one thing. How do we know who to mint the NFT to? The code above includes a Rails param called user_address. This user_address param was added, to allow our frontend to pass the user’s address along with their code to the API. This wallet address is given to the user the moment they make a Metamask.

Summary

Learning about Web3 and everything that comes with it has been incredibly valuable. Not only did I familiarize myself with blockchains and NFTs, but I was able to contextualize these new technologies within established frameworks like Ruby on Rails. Proving out the concept allowed me to refine how I learn. I established an understanding of Web3, and tested my new knowledge by applying it to building the MVP. Ultimately, I was able to execute the task at hand and provide value to our client.

 

Ready to unlock the potential of Web3 for your business? Contact us today to explore custom NFT solutions and bring your ideas to life.