In this tutorial we will create a non-fungible token (NFT) and deploy to a public testnet.
ERC721 is a standard for representing ownership of non-fungible tokens, that is, where each token is unique such as in real estate or collectibles.
We will use Presets contracts in OpenZeppelin Contracts 3 to create an ERC721 and deploy using Truffle.
Setting up the Environment
We begin by creating a new project.
$ mkdir mynft && cd mynft
$ npm init -y
Then we install OpenZeppelin Contracts which has an implementation of ERC721.
npm install --save-dev @openzeppelin/contracts
Next we install a development tool for deployment, for this tutorial we will use Truffle but we could use any other tools such as Buidler, Remix or OpenZeppelin CLI.
npm install truffle
Getting the contract artifacts
We will setup our Solidity project using truffle init
to create a contracts
directory and configuration to connect to a network.
$ npx truffle init
This directory is non-empty...
? Proceed anyway? (Y/n)
Starting unbox...
=================
? Proceed anyway? Yes
âś” Preparing to download box
âś” Downloading
âś” cleaning up temporary files
âś” Setting up box
Unbox successful, sweet!
Commands:
Compile: truffle compile
Migrate: truffle migrate
Test contracts: truffle test
We are going to use Preset ERC721PresetMinterPauserAutoId
which is an ERC721 that is preset so it can be minted (with auto token ID and metadata URI), paused and burned.
The Preset contracts have already been compiled, so we only need to copy the artifacts to the build/contracts
directory.
$ mkdir -p build/contracts/
$ cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/
Using your favorite editor create 2_deploy.js
in the migrations
directory with the following contents:
// migrations/2_deploy.js
// SPDX-License-Identifier: MIT
const ERC721PresetMinterPauserAutoId = artifacts.require("ERC721PresetMinterPauserAutoId");
module.exports = function(deployer) {
deployer.deploy(ERC721PresetMinterPauserAutoId, "My NFT","NFT", "https://my-json-server.typicode.com/abcoathup/samplenft/tokens/");
};
Deploy the contract to a local blockchain
We will use truffle develop
to open a Truffle console with a development blockchain
$ npx truffle develop
Truffle Develop started at http://127.0.0.1:9545/
Accounts:
(0) 0x0445c33bdce670d57189158b88c0034b579f37ce
(1) 0x46b68a577f95d02d2732cbe93c1809e9ca25b443
(2) 0xff8ddfd4ae8ed56c4b94738fef931b732f3aaeb5
...
Private Keys:
(0) 11e8321617611610d7561dfdf1fdbf87cee4cc99b97cc73c68f0eb5715fff7cc
(1) b28ee7deb262fbb6cccabb43179406e97bf7ff4c20b66feac3321019373531bd
(2) 44e5ce1f204a63e50faf2f202af1d5653aace83805dd9bc7062033f0625aaaa7
...
Mnemonic: ridge drop soon clutch empty north car drum maximum obey clinic coin
⚠️ Important ⚠️ : This mnemonic was created for you by Truffle. It is not secure.
Ensure you do not use it on production blockchains, or else you risk losing funds.
truffle(develop)>
We can deploy our new NFT to our development blockchain using migrate.
truffle(develop)> migrate
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Starting migrations...
======================
> Network name: 'develop'
> Network id: 5777
> Block gas limit: 6721975 (0x6691b7)
1_initial_migration.js
======================
Replacing 'Migrations'
----------------------
> transaction hash: 0x5d71b0a45a0fe20e2ca645393bb44b83fbd47351c009c48be0b8b84b610fb3b7
> Blocks: 0 Seconds: 0
> contract address: 0x3797c825cAC4a1FA765F6D8cd7787fB195849555
> block number: 1
> block timestamp: 1590736865
> account: 0x0445c33BdCe670D57189158b88c0034B579f37cE
> balance: 99.99671674
> gas used: 164163 (0x28143)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.00328326 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00328326 ETH
2_deploy.js
===========
Replacing 'ERC721PresetMinterPauserAutoId'
------------------------------------------
> transaction hash: 0x166d7b28f4afb949585b5a0e5b4151daa54acbcb70566b202fd62ab380a6650c
> Blocks: 0 Seconds: 0
> contract address: 0xDEE9411430c7Dd9b67fC6DA723DE729AdAB50AD7
> block number: 3
> block timestamp: 1590736866
> account: 0x0445c33BdCe670D57189158b88c0034B579f37cE
> balance: 99.92191642
> gas used: 3697675 (0x386c0b)
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.0739535 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.0739535 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.07723676 ETH
We can then use our deployed contract.
truffle(develop)> nft = await ERC721PresetMinterPauserAutoId.deployed()
undefined
Interact with our Token
The accounts that we can use were displayed when we started truffle develop
Token metadata
We can call the contract to read token metadata such as name
, symbol
and baseURI
truffle(develop)> await nft.name()
'My NFT'
truffle(develop)> await nft.symbol()
'NFT'
truffle(develop)> await nft.baseURI()
'https://my-json-server.typicode.com/abcoathup/samplenft/tokens/'
Mint
We can send a transaction to mint tokens to a given account, from an account with the minter role.
In our case we are minting from the account which deployed the token, which is given the minter role.
We will mint 1 NFT with token ID 0.
truffle(develop)> await nft.mint("0x0445c33bdce670d57189158b88c0034b579f37ce")
{ tx:
'0xd301a60dbb8ac187687f6639f200d4e6f2cfa065923092b3940330e35a26421d',
receipt:
{ transactionHash:
'0xd301a60dbb8ac187687f6639f200d4e6f2cfa065923092b3940330e35a26421d',
transactionIndex: 0,
blockHash:
'0x3ad3cfcb26da0c969e9d5a5414a5e90a91a3a862c9e9082afc38a0ec0f1a5d00',
blockNumber: 5,
from: '0x0445c33bdce670d57189158b88c0034b579f37ce',
to: '0xdee9411430c7dd9b67fc6da723de729adab50ad7',
gasUsed: 156470,
...
We can check the owner of the token and the token URI for the metadata
truffle(develop)> await nft.ownerOf(0)
'0x0445c33BdCe670D57189158b88c0034B579f37cE'
truffle(develop)> await nft.tokenURI(0)
'https://my-json-server.typicode.com/abcoathup/samplenft/tokens/0'
MetaData
EIP-721 includes an optional metadata extension with a name
, symbol
and for each tokenID a tokenURI
with can point to a JSON file with name
, description
and image
for the given token ID.
How you create and host this metadata is up to you.
I would suggest using a domain that you control to point to where you host the data so that you can move it as required.
For this tutorial, we will use My JSON Server where we can store a single JSON file in a GitHub repository that we can access via a fake JSON server.
For production we need to store our metadata in a permanent location that can exist for the life of the token.
For images we will use twemoji Graphics from https://github.com/twitter/twemoji which are licensed under CC BY 4.0
A sample JSON for tokenID 0 is:
http://my-json-server.typicode.com/abcoathup/samplenft/tokens/0
Deploy to a public testnet
Next we will deploy to Rinkeby public testnet as OpenSea supports Rinkeby for testing.
To deploy, we will use the instructions for Connecting to Public Test Networks with Truffle
You will need the following:
- An Infura project ID (or a public node provider of your choice)
-
@truffle/hdwallet-provider
installed - Configure
truffle-config.js
for Rinkeby network - A funded testnet account and mnemonic
- A
secrets.json
or another secret-management solution. Make sure you don’t commit this to GitHub!
My truffle-config.js
has the following rinkeby
configuration:
rinkeby: {
provider: () => new HDWalletProvider(mnemonic, `https://rinkeby.infura.io/v3/${projectId}`),
network_id: 4, // Rinkeby's id
gas: 8500000,
gasPrice: 1000000000, // 1 gwei (in wei) (default: 100 gwei)
confirmations: 2, // # of confs to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
},
Deploy to Rinkeby
$ npx truffle console --network rinkeby
truffle(rinkeby)> migrate
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Starting migrations...
======================
> Network name: 'rinkeby'
> Network id: 4
> Block gas limit: 10000000 (0x989680)
1_initial_migration.js
======================
Deploying 'Migrations'
----------------------
> transaction hash: 0xf817c3bb3b44349da50d90ef41317b64207eef76c5546a3348a8d8043a2d4d12
> Blocks: 1 Seconds: 30
> contract address: 0x1C883F26d124148c2E632539B4c37A6beD5eCD6f
> block number: 6572315
> block timestamp: 1590740195
> account: 0x77737a65C296012C67F8c7f656d1Df81827c9541
> balance: 3.986111900199984594
> gas used: 164163 (0x28143)
> gas price: 1 gwei
> value sent: 0 ETH
> total cost: 0.000164163 ETH
Pausing for 2 confirmations...
------------------------------
> confirmation number: 1 (block: 6572316)
> confirmation number: 2 (block: 6572317)
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.000164163 ETH
2_deploy.js
===========
Deploying 'ERC721PresetMinterPauserAutoId'
------------------------------------------
> transaction hash: 0x169e09cf7d50c226339493237b1d0981af0eed44698d35a132674105c311cb6e
> Blocks: 0 Seconds: 9
> contract address: 0x5c5c53D13aD8725cFE551D6E9dB93e9a9D38D78D
> block number: 6572320
> block timestamp: 1590740270
> account: 0x77737a65C296012C67F8c7f656d1Df81827c9541
> balance: 3.982371884199984594
> gas used: 3697675 (0x386c0b)
> gas price: 1 gwei
> value sent: 0 ETH
> total cost: 0.003697675 ETH
Pausing for 2 confirmations...
------------------------------
> confirmation number: 1 (block: 6572321)
> confirmation number: 2 (block: 6572322)
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.003697675 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.003861838 ETH
Mint
We can send a transaction to mint tokens to a given account, from an account with the minter role.
truffle(rinkeby)> nft = await ERC721PresetMinterPauserAutoId.deployed()
undefined
In our case we are minting from the account which deployed the token, which is given the minter role.
To see configured accounts run the command accounts
.
truffle(rinkeby)> accounts
[ '0x77737a65C296012C67F8c7f656d1Df81827c9541',
We will mint 1 NFT with token ID 0. Specify the address that you want to be the token holder (0x77737a65C296012C67F8c7f656d1Df81827c9541
is one of my test accounts)
truffle(rinkeby)> await nft.mint("0x77737a65C296012C67F8c7f656d1Df81827c9541")
{ tx:
'0xce8132ff0ffce128b8508d396fb0625a12c839fd1ee886eab5b4ddaf2755ca6f',
receipt:
{ blockHash:
'0x3f546e5caa9142d89dc3a87e7d369ba175629b480840d8bf88bd146f205e62de',
blockNumber: 6572543,
contractAddress: null,
cumulativeGasUsed: 1410693,
from: '0x77737a65c296012c67f8c7f656d1df81827c9541',
gasUsed: 149870,
...
OpenSea
We can validate our metadata
To obtain the address of our contract on Rinkeby we can use the Truffle console
truffle(rinkeby)> nft.address
'0x5c5c53D13aD8725cFE551D6E9dB93e9a9D38D78D'
OpenSea metadata validation is at https://rinkeby-api.opensea.io/asset/[nft contract address]/[token id]/validate
My example can be found at:
https://rinkeby-api.opensea.io/asset/0x5c5c53D13aD8725cFE551D6E9dB93e9a9D38D78D/0/validate/
Assuming that there aren’t any issues with our metadata, we can view our NFT on OpenSea at https://rinkeby.opensea.io/assets/[nft contract address]/[token id]
My example can be found at:
Next steps
- Create your artwork and metadata.
- Host your images and metadata.
- Define what rights your token holders have.
- Prepare for mainnet.