DEV Community

Cover image for A Simple WAX NFT
Ivan Montiel
Ivan Montiel

Posted on • Edited on

A Simple WAX NFT

Introduction

In previous sections, we introduced how to write a Smart Contract. Before we jump to a feature rich NFT implementation, let’s start with a simple NFT Smart Contract and go over the downsides of it.

Creating the Project

Let’s sh into the docker container as we did before:

docker run -it --name wax-tutorial--publish 8888:8888  -v $(pwd):/wax waxteam/dev bash
Enter fullscreen mode Exit fullscreen mode

Now we’ll create a new project called simplenft:

cd wax
eosio-init -project simplenft
Enter fullscreen mode Exit fullscreen mode

Setting up the Interface

We will define our interface in simplenft.hpp. We’ll start with just a mint action that only the Smart Contract account can call, and a transfer action that the owner of the NFT can call.

Let’s first start with the actions a user can take:

/**
 * We can create (or mint) a new cat NFT.
 */
ACTION create(string cat_name);

/**
 * The owner of the NFT can transfer it to a new owner
 */
ACTION transfer(uint64_t token_id, name from, name to);
Enter fullscreen mode Exit fullscreen mode

Then we will need to define a structure for storing the NFTs on the blockchain:

/**
 * This table defines an NFT as a (token_id, issuer, cat_name).
 *
 * We not only want unique token_ids so that users can reference
 * the NFT, but also each cat_name will be unique.
 *
 * In order to implement the cat_name uniqueness, we will need to
 * create a secondary index using `by_cat_name`.
 */
TABLE cat_nft_s {
  uint64_t token_id;
  name issuer;
  string cat_name;

  uint64_t primary_key() const { return token_id; }
  uint64_t by_cat_name() const { return std::hash<string>{}(cat_name); }
};
Enter fullscreen mode Exit fullscreen mode

We also need to define the full table definition using the above table struct:

typedef eosio::multi_index<
    name("cats"), cat_nft_s,
    indexed_by<name("bycatname"), const_mem_fun<cat_nft_s, uint64_t,
                                                &cat_nft_s::by_cat_name>>>
    cat_nft_t;
cat_nft_t cats = cat_nft_t(get_self(), get_self().value);
Enter fullscreen mode Exit fullscreen mode

The full contents of simplenft.hpp are here:

#include <eosio/eosio.hpp>

using namespace eosio;
using namespace std;

CONTRACT simplenft : public contract {
   public:
      using contract::contract;

      ACTION create(string cat_name);
      ACTION transfer(uint64_t token_id, name from, name to);

      TABLE cat_nft_s {
        uint64_t token_id;
        name issuer;
        string cat_name;

        uint64_t primary_key() const { return token_id; }
        uint64_t by_cat_name() const { return std::hash<string>{}(cat_name); }
      };

      typedef eosio::multi_index<
          name("cats"), cat_nft_s,
          indexed_by<name("bycatname"), const_mem_fun<cat_nft_s, uint64_t,
                                                      &cat_nft_s::by_cat_name>>>
          cat_nft_t;
      cat_nft_t cats = cat_nft_t(get_self(), get_self().value);
};
Enter fullscreen mode Exit fullscreen mode

Minting

In simplenft.cpp we will define our actions. Let’s start with minting an NFT:

/**
 * Implement minting an NFT with the given cat_name
 */
ACTION simplenft::create(string cat_name) {
   // Only the contract account can mint tokens
   require_auth(get_self());

   // Check that the cat name doesn't already exist
   auto idx = cats.get_index<name("bycatname")>();
   auto cat_itr = idx.find(std::hash<string>{}(cat_name));

   // If we can't find a cat with the given cat_name we are okay to mint.
   check(cat_itr == idx.end(), "Cat name already exists");

   // Create the new cat by adding it to the blockchain
   cats.emplace(get_self(), [&](auto &cat) {
      cat.token_id = cats.available_primary_key();
      cat.issuer = get_self();
      cat.cat_name = cat_name;
   });
}

Enter fullscreen mode Exit fullscreen mode

The above verifies that only the account that the Smart Contract is deployed to can mint an NFT using require_auth(get_self()).

Next, we use our index bycatname to make sure there are no cats that are already minted that have that name. If everything looks good, we add an entry to our table using cats.emplace to mint the NFT.

Transferring

Once an NFT is minted to the contract owner, it can be transferred by the owner using the transfer action we are about to implement:

/**
 * Implement the transfer action for letting users transfer the NFT to
 * a different account.
*/
ACTION simplenft::transfer(uint64_t token_id, name from, name to) {
   require_auth(from);

   // Check that the token exists
   auto cat_itr = cats.find(token_id);
   check(cat_itr != cats.end(), "Cat does not exist");

   // Check that the sender owns the token
   check(cat_itr->issuer == from "You do not own this cat");

   // Check that the recipient exists
   check(is_account(to), "Recipient account does not exist");

   // Transfer the token to the recipient
   cats.modify(cat_itr, get_self(), [&](auto &cat) {
      cat.issuer = to;
   });
}
Enter fullscreen mode Exit fullscreen mode

We have to pay extra attention to security when implementing this function. First, we make sure that the caller has the permissions of the from parameter that they passed in. Next we find that the NFT exists in our table. If it doesn’t exist, we exit early – a very common pattern in NFT contract security.

Then, we verify that the issuer (owner) is the same as from which we know the caller has permissions for at this point.

A nice check to have is that we validate that the to account name is a valid account.

Lastly, now that we are confident that the account can transfer this NFT, we update the table.

Deploying

Let’s build our contract:

( cd simplenft/build ; cmake .. ; make )
Enter fullscreen mode Exit fullscreen mode

And deploy it to the blockchain:

💡 Heads up, this will overwrite our previous smart contract, but all the tables will still remain on the blockchain. In a real-world scenario, you would want to create new accounts to deploy contracts to.

cleos wallet open -n testwallet
cleos wallet unlock -n testwallet --password="$(cat ./secrets)"

cleos -v -u https://testnet.wax.pink.gg set contract waxcourse123 ./simplenft/build/simplenft -p waxcourse123@active
Enter fullscreen mode Exit fullscreen mode

Downsides of a Simple NFT

The biggest drawback of creating NFTs with the above Smart Contract is that they are harder to interact with for an end consumer. The NFTs we just created have no interactivity, are hard to discover on the blockchain, and don’t provide much confidence in what is being purchased.

In the next section, we’ll talk about AtomicAssets which have better built-in functionality for creating, managing, and minting NFTs. An AtomicAsset can be interacted with, discovered, and easily explained on AtomicHub. You can think of AtomicHub as a marketplace built on the WAX blockchain for interacting with AtomicAssets NFTs.

Instead of building all that additional functionality ourselves, users can just use AtomicHub to find, trade, and discover NFTs from the entire WAX community.

Next post: IPFS Bootcamp

E-book

Get this entire WAX tutorial as an e-book on Amazon.

Additional links

Top comments (0)