This is the fourth tutorial of NEAR Nigeria zero to hero series. The goal of this series is to transform a programmer with no experience in developing decentralized applications to a skilled NEAR developer. Learn more about this series here.
This article is part 1 of a three part tutorial on building Non-Fungible Token (NFT) on NEAR. At the end of this tutorial, you will cover some of the essential concepts and skills required to build and deploy a NFT contract.
In the four years since ERC-721 was ratified by the Ethereum community, Non-Fungible Tokens have proven themselves as an incredible new opportunity across a wide array of disciplines: collectibles, art, gaming, finance, virtual reality, real estate, and more.
In this NFT series, you will learn how to build and deploy a NFT contract using the de facto NFT standard NEP 171.
To complete this tutorial successfully, you will need:
#note: check the previous tutorial to set-up the first three requirements.
To get started, you will create a new cargo project. Go to your terminal and run:
cargo new nft_z2H — lib && cd nft_z2H
Ensure a Cargo.toml and src/lib.rs were created for you, and open your project in the code editor of your choice.
Open Cargo.toml in the code editor of your choice. Modify your cargo.toml like so:
Building the NFT contracts
As with most NEAR contracts, you will use some essential utilities from NEAR SDK and NEAR STD crates in your contract. To add the following lines of code to src/lib.rs to use near_sdk and std crates
From the previous tutorial, you can recall that the pattern of writing NEAR contracts is to define a struct and implement functions for that struct. In line with this design pattern, the next step is to declare and initialize the struct that will be used in your NFT contract.
Now you can go ahead and add a corresponding impl like so:
You will learn how to fill out the code as you progress through the tutorial
Adding NFT functionalities
So far, you have created a simple contract that uses external crates from near_sdk, and contains a struct with a corresponding impl. You have also written a cargo.toml file that allows your smart contract to use near-sdk and serde_json as dependencies.
In this section, you will add the core functionalities of the NFT contract.
Adding Minting Functionality
In order to implement the logic needed for minting, you should break it up into smaller tasks and handle those one-by-one. Step back a bit and think about the best way to do this by asking yourself a simple question: what does it mean to mint an NFT?
To mint a non-fungible token, in the most simple way possible, a contract needs to be able to associate a token with an owner on the blockchain. This means you will need:
- A way to keep track of tokens and other information on the contract.
- A way to store information for each token such as metadata (more on that later).
- A way to link a token with an owner.
That is it! You have now broken down the larger problem into some smaller, less daunting, sub-tasks. Let’s start by tackling the first and work our way through the rest.
Storing information on the contract
Start by navigating to src/lib.rs and filling in some of the code blocks. You need to be able to store important information on the contract such as the list of tokens that an account has.
The first thing to do is to modify the contract struct like so:
This allows you to get the information stored in these data structures from anywhere in the contract. The code above has created 3 token specific storage:
- tokens_per_owner: allows you to keep track of the tokens owned by any account
- tokens_by_id: returns all the information about a specific token
- token_metadata_by_id: returns just the metadata for a specific token
In addition, you’ll keep track of the owner of the contract as well as the metadata for the contract.
You might be confused as to some of the types that are being used. In order to make the code more readable, custom data types were introduced which we’ll briefly outline below:
- AccountId: a string that ensures there are no special or unsupported characters.
- TokenId: simply a string.
The Token, TokenMetadata, and NFTContractMetadata data types, those are structs that we’ll define later in this tutorial.
Next, you will modify the impl functions. Start by modifying what is called an initialization function; new(). This function needs to be invoked when you first deploy the contract. It will initialize all the contract’s fields that you have defined above with default values.
Modify the new() function in src/lib.rs so that it look like so:
This function will default all the collections to be empty and set the owner and metadata equal to what you pass in.
More often than not when doing development, you will need to deploy contracts several times. You can imagine that it might get tedious to have to pass in metadata every single time you want to initialize the contract. For this reason, let’s create a function that can initialize the contract with a set of default metadata. You can call it new_default_meta and it’ll only take the owner_id as a parameter.
Modify the new() function in src/lib.rs so that it look like so:
This function is simply calling the previous new function and passing in the owner that you specify and also passes in some default metadata.
Metadata and token information
Now that you have defined what information to store on the contract itself and you have defined some ways to initialize the contract, you need to define what information should go in the TokenMetadata, and NFTContractMetadata data types.
If you look at the standards for metadata, you’ll find all the necessary information that you need to store for both TokenMetadata and NFTContractMetadata. Simply fill in the following code.
Below your contract implementation add the following lines of code
This now leaves you with the Token struct and something called a JsonToken. The Token struct will hold all the information directly related to the token excluding the metadata. The metadata, if you remember, is stored in a map on the contract in a data structure called token_metadata_by_id.
This allows you to quickly get the metadata for any token by simply passing in the token’s ID.
For the Token struct, you’ll just keep track of the owner for now. Add the Token struct like so:
For the Token struct, you’ll just keep track of the owner for now.
The purpose of the JsonToken is to hold all the information for an NFT that you want to send back as JSON whenever someone does a view call (via a frontend). This means you’ll want to store the owner, token ID, and metadata.
Add JsonToken like so:
Function for querying contract metadata
Now that you have defined some of the types that were used in the previous section, let’s move on and create the first view function nft_metadata. This will allow users to query for the contract’s metadata as per the metadata standard.
Add the block of code below
This function will get the metadata object from the contract which is of type NFTContractMetadata and will return it.
Just like that, you’ve completed the first two tasks (create a way to keep track of tokens and other information on the contract, and a way to store information for each token such as metadata). You are now ready to move onto the last part of the tutorial.
Now that all the information and types are defined, let’s start brainstorming how the minting logic will play out. In the end, you need to link a Token and TokenId to a specific owner.
There are a couple data structures that might be useful:
#note: code already exists in the Contract struct.
Looking at these data structures, you could do the following:
- Add the token ID into the set of tokens that the receiver owns. This will be done on the tokens_per_owner field.
- Create a token object and map the token ID to that token object in the tokens_by_id field.
- Map the token ID to its metadata using the token_metadata_by_id.
With those steps outlined, it is important to take into consideration the storage costs of minting NFTs. Since you’re adding bytes to the contract by creating entries in the data structures, the contract needs to cover the storage costs.
If you just made it so any user could go and mint an NFT for free, that system could easily be abused and users could essentially “drain” the contract of all its funds by minting thousands of NFTs.
For this reason, you’ll make it so that users need to attach a deposit to the call to cover the cost of storage. You’ll measure the initial storage usage before anything was added and you’ll measure the final storage usage after all the logic is finished. Then you’ll make sure that the user has attached enough $NEAR to cover that cost and refund them if they’ve attached too much.
Now that you’ve got a good understanding of how everything should play out, let’s fill in the necessary code.
You’ll notice that you are using some internal methods such as refund_deposit() and internal_add_token_to_owner(). refund_deposit() has been described in the leading statement. internal_add_token_to_owner(), this will add a token to the set of tokens an account owns for the contract’s tokens_per_owner data structure. Add the functions to your src/lib.rs
As well as this helper function, which provides the sha256 of an accountId
Tidying up the smart contract
You may have observed that src/lib.rs is composed of four divisible components,
- internal functions
To keep the contract code modularized, you will group each component in a different file.
Create three new files like so; src/metadata.rs, src/internal.rs, and src/mint.rs
Add this code to the top of src/metadata.rs:
This allows metadata to “communicate” with other files in your contract. It also defines a data type Token of type String.
The next step is to cut the Token, TokenMetadata, and NFTContractMetadata data types and their implementation to src/metadata.rs.
Add this lines of code to the top of src/internal.rs
This allows metadata to “communicate” with other files in your contract. It also use CryptoHash utility from near_sdk
The next step is to cut the internal functions refund_deposit, as well as the helper function hash_account_id into src/internal.rs. Next, cut the implementation containing internal_add_token_to_owner into src/internal.rs.
By now you should be left with the Contract implementation for nft_mint and the bare struct and implementation for your contract.
Cut the implementation of nft_mint into the src/mint.rs
Add the line of code above at the top of the src/mint.rs file to allow its “communication” with src/internal.rs and other files of your contract.
Although your newly created files can “communicate” with one another, they can not be accessed from src/lib.rs which initializes the contract.
To facilitate the use of other files in src/lib.rs, you will have to use them.
Add the code below to src/lib.rs after the use statements for near_sdk and std
Finally add the helper Storage helper data type that is used to initialize the metadata and persist the data to the state of the blockchain like so:
Querying for token information
If you were to go ahead and deploy this contract, initialize it, and mint an NFT, you would have no way of knowing or querying for the information about the token you just minted.
Next, you will add a way to query for the information of a specific NFT.
Create a new file like so src/nft_core. Create a function nft_token which takes a token ID as a parameter and return the information for that token. The JsonToken contains the token ID, the owner ID, and the token’s metadata.
Your function should look like so:
Add the lines of code below to make it accessible anywhere in your contract.
With that finished, You can begin to add features that add utilities to your NFT. In the coming tutorial, you will add a mechanism to allow Royalty payment on owners of your NFT, as well as add a medium of moving your NFT from an external contract.
Versioning for this article
At the time of this writing, this example works with the following versions:
- cargo 1.62.0 (a748cf5a3 2022–06–08)
- rustc 1.62.0 (a8314ef7d 2022–06–27)
- near-cli: 3.4.0