Create your own collectable token on Rootstock network | Rootstock (RSK)
In this tutorial, you will learn about blockchain programming from scratch by building a fully decentralized application (DApp), step by step.
You will also learn how to create your own collectable token on the RSK blockchain network using the Truffle framework, Open Zeppelin (OZ) libraries, and build a front end with React, using create-react-app
.
We will create a dapp inspired by Cryptokitties, a popular blockchain game where you can collect and breed digital cats. In this tutorial, instead of collecting felines in our app, we are going to collect exclusive color tokens.
Fungible vs Non-Fungible Token
A fungible token represents an asset that can be exchanged for any other asset of equal value in its class.
A currency is an example of a fungible asset. A $100 bill is equal to any other $100 bill, you can freely exchange these bills with one another because they have the same value, no matter the serial number on the specific $100 bill. They are fungible bills.
On the other hand, a Non-Fungible Token (NFT) is a unique token. So collectible items are non-fungible assets, and can be represented by NFTs.
ERC-721
ERC-721 was the first standard, and currently still the most popular standard, for representing non-fungible digital assets.
The most important properties for this kind of asset is to have a way to check who owns what and a way to move things around.
It is easy to create new ERC721-compliant contracts by importing it from the OZ library and we will do so in this tutorial.
The interface for ERC-721 provides two methods:
ownerOf
: to query a token's ownertransferFrom
: to transfer ownership of a token
And this is enough to represent an NFT!
Colors
In this tutorial, we are going to create an NFT to represent our collectible color tokens.
You will be able to create new color tokens and claim them so that they can be held in a digital blockchain wallet.
Overview
Here is a summary of the steps to be taken to build our token:
- Installation requirements;
- Initialize a project using Truffle and OZ;
- Configure Truffle to connect to RSK testnet;
- Get a wallet with some testnet RBTCs;
- Initialize the client side application;
- Add more configurations to Truffle;
- Create smart contract of the token and compile it;
- Create deploy instructions file in Truffle;
- Deploy a smart contract on RSK Testnet using Truffle;
- Create client side application;
- Interact with the smart contract.
Steps 1 to 4 are explained in detail in the tutorial link below:
Webinar
We have run a webinar.
The same webinar is also available in Português.
Check out our other webinars.
Translations
This article is also available in Português.
Requirements
- Node.js and NPM (Node Package Manager)
- Visual Studio Code (VSCode) or any other editor of your choice
- Truffle
- Metamask - google chrome extension
The requirements 1 to 3 are explained in detail in the tutorial links below:
For requirement 4, installing Metamask, connecting to RSK testnet, and getting some tRBTCs, this is explained step-by-step in the tutorial link below:
Setup the project
Create a new folder named colors
.
Inside the folder colors
, do the steps below, following instructions from the tutorial
Setup a project with Truffle and OpenZeppelin
- Initialize an empty Truffle project;
- Initialize an npm project;
- Install OZ;
- Install HD wallet provider;
- Create a wallet mnemonic;
- Create .secret file;
- Get the current gas price at RSK testnet;
- Configure Truffle to connect to RSK testnet;
- Use Truffle console;
- Test the connection to RSK network;
- Get addresses;
- Check balance;
- Get some testnet RBTCs at faucet;
Initialize the client side application
We have 3 requirements to build the frontend:
- Create React App
- Web3.js
- Bootstrap
Create React App
This is the official template to create single-page React applications. It offers a build setup with no configuration.
To learn more: create react app
In the project folder, at terminal, run:
npx create-react-app app --use-npm
The option --use-npm
is to select npm as package manager.
npx comes with npm 5.2+ and higher, see instructions for older npm versions.
This is a large package and this might take a couple of minutes to show the message of successful installation:
Now you have a new folder named app
and we will customize our frontend later.
Web3.js
Web3.js helps us to develop websites or clients that interact with the blockchain - writing code that reads and writes data from the blockchain with smart contracts.
The web3.js library is an Ethereum Javascript API which connects using the generic JSON-RPC spec. As RSK's virtual machine implementation is compatible with the Ethereum Virtual Machine (EVM), it is possible to use web3.js to interact with the front end and the RSK local node.
To install web3.js, input the command below into the terminal and press enter
at folder app
cd app
npm install -E web3@1.2.7
The option -E
is to save dependencies with an exact version rather than using npm's default.
More info: web3.js
Bootstrap
cd app
npm install -E bootstrap@4.4.1
As I said before, the option -E
is to save dependencies with an exact version rather than using npm's default.
Configure Truffle
Come back to the project folder, open truffle-config.js
file in VS Code and overwrite it with the following code:
const HDWalletProvider = require('@truffle/hdwallet-provider');
const fs = require('fs');
const mnemonic = fs.readFileSync(".secret").toString().trim();
if (!mnemonic || mnemonic.split(' ').length !== 12) {
throw new Error('unable to retrieve mnemonic from .secret');
}
const gasPriceTestnetRaw = fs.readFileSync(".gas-price-testnet.json").toString().trim();
const gasPriceTestnet = parseInt(JSON.parse(gasPriceTestnetRaw).result, 16);
if (typeof gasPriceTestnet !== 'number' || isNaN(gasPriceTestnet)) {
throw new Error('unable to retrieve network gas price from .gas-price-testnet.json');
}
console.log("Gas price Testnet: " + gasPriceTestnet);
const path = require("path");
module.exports = {
networks: {
testnet: {
provider: () => new HDWalletProvider(mnemonic, 'https://public-node.testnet.rsk.co/'),
network_id: 31,
gasPrice: Math.floor(gasPriceTestnet * 1.1),
networkCheckTimeout: 1e9
},
},
contracts_build_directory: path.join(__dirname, "app/src/contracts"),
compilers: {
solc: {
version: "0.5.7",
}
}
}
It looked like this:
About contracts_build_directory
We add the library path
to use with a new parameter contracts_build_directory
that defines the locale where files for contracts artifacts, like abi and deployed addresses are saved.
It will be located in a different folder: app/src/contracts
.
Smart contract architecture
We will create a smart contract named Color.sol
that will inherit the ERC721 definition from the OZ library.
Create the smart contract Color.sol
In the Contracts folder, create a new file named Color.sol
.
File Color.sol
Copy and paste the smart contract from the following gist, or inline below:
pragma solidity 0.5.7;
import '@openzeppelin/contracts/token/ERC721/ERC721Full.sol';
contract Color is ERC721Full {
bytes3[] public colors;
mapping(bytes3 => bool) private _colorExists;
constructor() ERC721Full("Color", "COLOR") public {
}
// E.G. color = "#FFFFFF"
function mint(bytes3 _color) public {
require(!_colorExists[_color], "color exists");
uint _id = colors.push(_color);
_mint(msg.sender, _id);
_colorExists[_color] = true;
}
}
It looked like this:
Understanding the smart contract
To create our ERC-721 Token, we will import ERC721Full
from OZ.
This library itself imports several other libraries such as SafeMath.sol
,
the standards for this kind of token and some extra features,
like enumeration and metadata.
With metadata we can customize our token by giving it a name and a symbol at constructor.
This function gets run only once; whenever the smart contract is created the first time, i.e., deployed to the blockchain.
We are calling the constructor function of the parent smart contract ERC721Full
and passing in custom arguments like the name Color
and the symbol COLOR
.
The color management is performed with the variable colors
, which is an array of colors and _colorExists
, which is a fast "lookup" to know when a color is already minted.
Also we have a function to create new color tokens.
This is the basic structure of the function.
It will accept 1 argument of the bytes3
type, which will be a hexadecimal code that corresponds to the token's color.
For example, if we want to create a green token, we will pass "#00FF00" when we call this function. Or if we want to create a red token, we'll use "#FF0000".
Compile a smart contract
In the terminal, run this command:
truffle compile
Deploy a smart contract at testnet
First of all, we need to create a file in Truffle structure with instructions to deploy the smart contract.
Create file 2_deploy_contracts.js
Folder migrations
has JavaScript files that help you deploy contracts to the network.
These files are responsible for staging your deployment tasks, and they're written under the assumption that your deployment needs will change over time.
A history of previously run migrations is recorded on-chain through a special Migrations contract which is automatically created by Truffle.
(source: running migrations)
In the migrations
folder, create the file 2_deploy_contracts.js
Copy and paste this code:
const Color = artifacts.require("Color");
module.exports = function(deployer) {
deployer.deploy(Color);
};
It looked like this:
Migrate
In the terminal, run this command:
truffle migrate --network testnet
Wait a few minutes while the transactions for the smart contract deployments are sent to the blockchain.
The migrate command will compile the smart contract again if necessary.
First, it deploys the smart contract Migrations.sol
, file generated by Truffle:
This is the transaction at RSK testnet:
0x3de61b8983dc3db2ca21a9d10106a19c445885fcb7040774bd6937daf94a4702
And then it deploys our smart contract Color.sol
:
This is the transaction at RSK testnet:
0x2c2d2932a7d637fbba100b5c482c1fa1899c4fe24bd1a458976a93cee6c5ba85
A tip: if there is a communication problem with the testnet between the publication of Migrations.sol and Color.sol, just run the migrate command again, it will deploy only what is missing.
Congratulations!
Our NFT Color is published at RSK Testnet.
Save the contract address of token, it can be used later:
tokenAddress = "0x5505a54a8F3e63D37095c37d9f8AcF0f4900B61F"
Client side application
Now let's start building out the front end that will interact with the smart contract. It will allow us to create new color tokens, and list out all of the existing tokens in your wallet.
In the app
folder, we need to customize some files.
index.html
In the app\public
folder, open index.html
file.
At head
section, update the title
:
<title>NFT Colors</title>
index.js
In the app\src
folder, open index.js
file and add a line to use bootstrap in out project
import 'bootstrap/dist/css/bootstrap.css';
Also remove this line:
import './index.css';
The final index.js
is this. You can overwrite it with the code from following gist, or inline below:
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
At this point, your completed index.js
file should looks like this:
App.css
Open App.css
file and overwrite it with the code from the following gist, or copy and paste the code below:
.token {
height: 150px;
width: 150px;
border-radius: 50%;
display: inline-block;
}
This code customizes the appearance for tokens.
This is the result:
App.js
Open App.js
file and overwrite it with the code from following gist, or copy and paste the code below:
import React, { Component } from 'react';
import Web3 from 'web3';
import './App.css';
import Color from './contracts/Color.json';
function colorHexToString(hexStr) {
return '#' + hexStr.substring(2);
}
function colorStringToBytes(str) {
if (str.length !== 7 || str.charAt(0) !== '#') {
throw new Error('invalid color string');
}
const hexStr = '0x' + str.substring(1);
return Web3.utils.hexToBytes(hexStr);
}
class App extends Component {
constructor(props) {
super(props);
this.state = {
account: '',
contract: null,
totalSupply: 0,
colors: [],
};
}
async componentWillMount() {
await this.loadWeb3();
await this.loadBlockchainData();
}
async loadWeb3() {
if (window.ethereum) {
// current web3 providers
window.web3 = new Web3(window.ethereum);
await window.ethereum.enable();
}
else if (window.web3) {
// fallback for older web3 providers
window.web3 = new Web3(window.web3.currentProvider);
}
else {
// no web3 provider, user needs to install one in their browser
window.alert(
'No injected web3 provider detected');
}
console.log(window.web3.currentProvider);
}
async loadBlockchainData() {
const web3 = window.web3;
// Load account
const accounts = await web3.eth.getAccounts();
console.log ('account: ', accounts[0]);
this.setState({ account: accounts[0] });
const networkId = await web3.eth.net.getId();
const networkData = Color.networks[networkId];
if (!networkData) {
window.alert('Smart contract not deployed to detected network.');
return;
}
const abi = Color.abi;
const address = networkData.address;
const contract = new web3.eth.Contract(abi, address);
this.setState({ contract });
const totalSupply = await contract
.methods.totalSupply().call();
this.setState({ totalSupply });
// Load Colors
for (var i = 1; i <= totalSupply; i++) {
const colorBytes = await contract
.methods.colors(i - 1).call();
const colorStr = colorHexToString(colorBytes);
this.setState({
colors: [...this.state.colors, colorStr],
});
}
}
mint = (colorStr) => {
const colorBytes = colorStringToBytes(colorStr);
this.state.contract.methods
.mint(colorBytes)
.send({ from: this.state.account })
.once('receipt', (receipt) => {
console.log ('transaction receipt: ', receipt)
this.setState({
colors: [...this.state.colors, colorStr],
});
});
}
render() {
return (
<div>
<nav className="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow">
<span className="navbar-brand col-sm-3 col-md-2 mr-0">
Color Tokens
</span>
<ul className="navbar-nav px-3">
<li className="nav-item text-nowrap d-none d-sm-none d-sm-block">
<small className="text-white"><span id="account">{this.state.account}</span></small>
</li>
</ul>
</nav>
<div className="container-fluid mt-5">
<div className="row">
<main role="main" className="col-lg-12 d-flex text-center">
<div className="content mr-auto ml-auto">
<h1>Issue Token</h1>
<form onSubmit={(event) => {
event.preventDefault();
const colorStr = this.color.value;
this.mint(colorStr);
}}>
<input
type='text'
className='form-control mb-1'
placeholder='e.g. #FF00FF'
ref={(input) => { this.color = input }}
/>
<input
type='submit'
className='btn btn-block btn-primary'
value='MINT'
/>
</form>
</div>
</main>
</div>
<hr/>
<div className="row text-center">
{ this.state.colors.map((colorStr, key) => {
return (
<div key={key} className="col-md-3 mb-3">
<div className="token" style={ { backgroundColor: colorStr } }></div>
<div>{colorStr}</div>
</div>
);
})}
</div>
</div>
</div>
);
}
}
export default App;
Understanding App.js
Import web3 here:
import Web3 from 'web3'
This part is connected to the RSK Testnet using the injected web3 provider, in this case, MetaMask:
async loadWeb3() {
if (window.ethereum) {
// current web3 providers
window.web3 = new Web3(window.ethereum);
await window.ethereum.enable();
}
else if (window.web3) {
// fallback for older web3 providers
window.web3 = new Web3(window.web3.currentProvider);
}
else {
// no web3 provider, user needs to install one in their browser
window.alert(
'No injected web3 provider detected');
}
console.log(window.web3.currentProvider);
}
To load the instance of smart contract Color already published, we need to load the informations from Truffle deploy:
import Color from './contracts/Color.json';
And after connecting successfully, the function loadBlockchainData
loads accounts, network information and the smart contract Color.
async loadBlockchainData() {
const web3 = window.web3;
// Load account
const accounts = await web3.eth.getAccounts();
console.log ('account: ', accounts[0]);
this.setState({ account: accounts[0] });
const networkId = await web3.eth.net.getId();
const networkData = Color.networks[networkId];
if (!networkData) {
window.alert('Smart contract not deployed to detected network.');
return;
}
const abi = Color.abi;
const address = networkData.address;
const contract = new web3.eth.Contract(abi, address);
this.setState({ contract });
const totalSupply = await contract
.methods.totalSupply().call();
this.setState({ totalSupply });
// Load Colors
for (var i = 1; i <= totalSupply; i++) {
const colorBytes = await contract
.methods.colors(i - 1).call();
const colorStr = colorHexToString(colorBytes);
this.setState({
colors: [...this.state.colors, colorStr],
});
}
}
Also we have a mint function at App.js which sends a transaction to the network calling the mint function in the smart contract.
mint = (colorStr) => {
const colorBytes = colorStringToBytes(colorStr);
this.state.contract.methods
.mint(colorBytes)
.send({ from: this.state.account })
.once('receipt', (receipt) => {
console.log ('transaction receipt: ', receipt)
this.setState({
colors: [...this.state.colors, colorStr],
});
});
}
Finally, the render()
function is responsible for the HTML code for the application. It has 3 primary functions:
- Displays the current address on the Navbar
- Provides a form to mint new tokens
- Displays all the existing tokens on the page in a grid, showing the color and the code for each token
Running
In the app
folder, at terminal, run:
npm start
It will automatically open the default browser at http://localhost:3000/
If it does not open, you can enter the local url manually in the browser.
Metamask automatically detects that our app would like to connect, authorize this action by clicking on the Connect
button.
And this is our frontend!
Interact with the smart contract
The colors are saved with hexadecimal representation for each.
To know more about color and hex color codes:
- https://htmlcolorcodes.com/
- https://www.color-hex.com/color-names.html
- https://www.rapidtables.com/web/color/RGB_Color.html
Some color codes:
Red | #FF0000 |
Green | #00FF00 |
Blue | #0000FF |
Yellow | #FFFF00 |
Mint
Choose a color and enter your hexadecimal representation in the info text field, and click on the MINT
button.
It will call the mint()
function at the smart contract instance Color, with the color that you defined.
I will enter the color red, value #FF0000
.
Do not forget to use the symbol
#
Click on the confirm
button.
Great! Now I have my first color collectable token:
I would like to mint the blue color: #0000FF
Wait for a few seconds for your transaction to be mined…
And now I have two colors in my collection!
And my collection is growing!
Congratulations!
Hope it was easy for you to create a NFT!
I showed you how to connect Truffle to the RSK network and deploy your own NFT using the OZ libraries, and that they work on the RSK network.
This tutorial was inspired by Gregory McCubbin's tutorial, from dApp University. Check out the original article.
Our goal is to join forces and give options to people who believe in smart contracts based on Ethereum, and also believe in the power of Bitcoin, through RSK.
I hope this tutorial has been helpful and I’d appreciate your feedback. Happy with this tutorial? Share it if you like it :)