Building Ethereum Dapps With ReactJS + Truffle Contract + Web3, A UI For TokenZendR A Smart Contract That Transfers ERC20 Tokens To Other Addresses

in #utopian-io6 years ago (edited)

Repository

https://github.com/facebook/react

enter image description here

Overview

In my last Post i wrote a tutorial on how you can code a smart contract that helps you transfer any of your ERC20 compatible token to another address, be it exchange wallet , metamask or MEW. And we were able to write tests to assert that our smart contract works the way we want it to, also interact with it via the truffle console. If you have not checked it out yet, you probably should.

This tutorial is a second part where we would build a clean interface using React together with Truffle Contract ,web3js and Bulma for UI, that a user visiting our application can use to interact with the smart contract on the blockchain.

What Will I Learn?

In the second part of this tutorial, you will learn how to:

  • Build standard Dapps interfaces using React
  • Architecting And Workflow for easily building an interface for Dapps, built with truffle
  • Using Truffle Contract in combination with Web3 to interact with our contract on the blockchain

Requirements

For this tutorial, it's required for you to clone the repository from the previous tutorial, although it's not a requirement to read the first tutorial before you jump right into this one, as each tutorial address two different aspects of building an Ethereum Dapps and therefore can be considered self-contained.

This tutorial assumes you are using a UNIX operating system

Difficulty

  • Intermediate

Tutorial Contents

To start with, create a new react app

npx create-react-app token-zendr

enter image description here

Remeber ealier we mention we will be using web3js, bulma & truffle contract. Now is the time to install them, to do so replace your project package.json with what you see below, then run npm install

{  
  "name": "token-zendr",  
  "version": "0.1.0",  
  "private": true,  
  "dependencies": {  
  "bulma-start": "0.0.2",  
  "react": "^16.4.2",  
  "react-dom": "^16.4.2",  
  "react-scripts": "1.1.4",  
  "truffle-contract": "^3.0.6",  
  "web3-js": "^1.0.5-beta.26"  
  },  
  "scripts": {  
  "start": "react-scripts start",  
  "build": "react-scripts build",  
  "test": "react-scripts test --env=jsdom",  
  "eject": "react-scripts eject"  
  }  
}

After successfull installation of the packages, fire up your app with npm start, your default browser will automatically fire up a new tab with the default react screen showing. Leave the tab open.

Before we proceed forward i need you to clone the smart contract that handles the transfer and deploy it on a your private blockchain (Ganache).

Note : Ganache must have been started on your machine before you run the following commands

git clone https://github.com/slim12kg/tokenzendr-contract.git

cd tokenzendr-contract

npm install

truffle console

truffle(development)> compile

truffle(development)> migrate

enter image description here

If you open your Ganache, you should see the transactions mined and new blocks created similar to the images below

8 Blocks Mined

enter image description here

Transactions Log Showing Contract Creation & Call

enter image description here

One of the challenges, developers new to building ethereum Dapps faces is to have to always edit the address of the contract in thier code anytime the contract is redeployed. To solve the issue, my approach is to create a soft link of the build directory in the token tokenzendr-contract to the src directory of our new project.

This ensures that anytime the contract is redeployed we will be referencing the updated contract address. Genuis !!

From your command line run this command, substituiting your project path in the command as it applies to your project

//Remember to substituite your project path as it applies to you
ln -s ~/Desktop/Dapps/tokenzendr-contract/build/ ~/Desktop/Dapps/token-zendr/src!

Soft link build directory

If your screenshot looks like what we have above then you should carry on with the tutorial, else check that you specified the correct directory path and have substituited correctly the directory path that applies to you.

As a autonomous smart developer, we want to only support to transfer some vetted tokens or maybe the token creator will have to pay us to support the transfer of their token on our platform. The smart contract already have a method to add or remove supported tokens, but on our frontend we want to also do the same thing, but this time just list them in a json file with the token address, name, symbol, decimal.

Looking back on the smart contract we deployed to our private blockchain, one important thing to realise is that we did not only deploy the smart contract that handles the token transfer but also two ERC20 tokens (BearToken, CubToken) contract that we will be using for testing purpose in this tutorial. We will be transfering them between two addresses in our wallet which will be connected to the same same network they were deployed to.

Now that i've mentioned about two token been deployed alongside our transfer contract, you need to connect your metamask to custom RPC. Click on mainnet a dropdown will drop with the available option to select custom RPC, click this option and enter the address http://127.0.0.1:7545 for new RPC.

Soft link build directory

Enter new RPC URL
Soft link build directory

See the first address same as the one in Ganache show up with the same balance

Soft link build directory

When the two tokens contract was deployed, it assigns all the total token supply to the address that was used to deploy the contract, in our own case the first account. We want the token balances to show in our wallet, this way you see your balance history as you make transfers, to do this simply open the contract directory of the build folder we created soft link for ealier, you would see files BearToken.json and CubToken.json files. Under the networks section copy the address (contact address) value and add them as new tokens to metamask.

...
"networks": {  
  "5777": {  
  "events": {},  
  "links": {},  
  "address": "0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f",  
  "transactionHash": "0x2a3fd7782a1a7b5c4b388f639e949cac29ca9d51ed0343be91eb8b0b110c8f81"  
  }  
},
...

Soft link build directory

We will begin by start writing our react components. This tutorial is a not beginners post therefore we won't be coding all from scratch as this will obviously take a longer time. Instead i will pick at each component at a time and mention the role each play while relating it to the App.js file.

Oh, one quick addition to the setup process, create a Tokens directory in the src directory of the project, add three files all.js, Bear.js and Cub.js. The Bear.jsand Cub.jswill carry the information of the each tokens such as the address, decimal, name, symbol, icon and most importantly the abi and this will be the process of adding a new supported token. You can get the address, decimal and abi from the json file of each tokens in the build directory, usually on mainnet you can always get this information from etherscan.io

Finally create icons folder in the public folder and add the cub and bear token icons. You can find them here https://github.com/slim12kg/token-zendr-react-interface/tree/master/public/icons

In actual sense, the ABI (Application Baniary Interface) will contain much more information than displayed below.

export default {  
  address: "0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f",  
  decimal: 5,  
  name: "BearToken",  
  symbol: "Bear",  
  icon: "bear_x28.png",  
  abi: [  
     {  
        "constant": true,  
        "inputs": [],  
        "name": "name",  
        "outputs": [  
            {  
                "name": "",  
                "type": "string"  
             }  
        ],  
       ...,
       "name": "Transfer",  
      "type": "event"  
      }  
     ]
 }

Create a Components folder in the src directory, add the following components

//InstallMetamask.js
import React from 'react';  
  
function InstallMetamask() {  
  return (  
 <div className="modal is-active">  
     <div className="modal-background"></div>  
     <div className="modal-content">  
         <p className="image download-metamask">  
             <a href="https://metamask.io/" rel="noopener noreferrer" target="_blank">  
             <img src="https://metamask.io/img/metamask.png" alt=""></img>
             </a>  
         </p>  
     </div>  
     <button className="modal-close is-large" aria-label="close"></button>  
 </div>  
 )}  
  
export default InstallMetamask; 

This is a notification component that shows up when a user doesn't have metamask installed.

//UnlcokMetaMask.js
import React from 'react';  
  
function UnlockMetamask(props) {  
  return (  
     <div className="column is-4 is-offset-4">  
         <div className="notification is-danger">  
             <button className="delete"></button>  
             {props.message}  
         </div>  
     </div>  
 )}  
  
export default UnlockMetamask;

This is also a notification component that displays a warning if the user metamask account is locked.

//Nav.js
import React, { Component } from 'react';  
  
class Nav extends Component {  
      render(){  
          return (  
             <nav className="navbar is-link" aria-label="main navigation">  
                 <div className="navbar-brand">  
                     <a className="navbar-item" href="/">  
                         <strong><i className="fa fa-coins"></i> {this.props.appName}</strong>  
                     </a>  
                      
                     <a role="button" className="navbar-burger" aria-label="menu" aria-expanded="false">  
                         <span aria-hidden="true"></span>  
                         <span aria-hidden="true"></span>  
                         <span aria-hidden="true"></span>  
                     </a>  
                 </div>  
                 <div className="navbar-menu">  
                     <div className="navbar-end">  
                         <a className="navbar-item">  
                             <div className="tags has-addons">  
                                 <span className="tag">  
                                 <i className="fa fa-signal"></i> &nbsp; Network  
                                                                </span>  
                                 <span className="tag is-danger">{this.props.network}</span>  
                             </div>  
                         </a>  
                     </div>  
                 </div>  
             </nav>  
         ) 
     }
 }  
  
export default Nav;

This component purpose is to serve as our application nav. Its is passed the value of the network we are curretly connect to.

//Description.js
import React from 'react';  
  
function Description(props) {  
  return (  
     <section className="container">  
         <div className="has-text-centered content">  
             <br/>  
             <h1 className="title is-4 is-uppercase has-text-danger">Simple Way To Transfer</h1>  
             <h2 className="subtitle is-6 has-text-grey-light">ERC20 Tokens</h2>  
         </div>  
     </section>  
 )}  
  
export default Description;

Simply a component to add a descripion for our application. I used Easy Way To Transfer ERC20 Tokens, you can modify it to whatever suites you

//Container.js

import React, { Component } from 'react';  
import AddressBar from './AddressBar';  
import TokenBlock from './TokenBlock';  
import TradeMarkBlock from './TradeMarkBlock';  
import SortTokenBlock from './SortTokenBlock';  
import TransferToken from './TransferToken';  
import TransferHeader from './TransferHeader';  
import SuccessTransaction from './SuccessTransaction';  
  
class Container extends Component {  
  render(){  
      return (  
         <section className="container">  
             <div className="columns">  
                <div className="is-half is-offset-one-quarter column">  
                         <div className="panel">  
                         {  this.props.tx ?  
                         <SuccessTransaction tx={this.props.tx} /> :  
                          ''  
                          }  
                      
                     <AddressBar account={this.props.account} tx={this.props.tx}/>  
                     {  
                         this.props.transferDetail.hasOwnProperty('name') ?  
                         <div>  
                             <TransferHeader token={this.props.transferDetail} />  
                             <TransferToken closeTransfer={this.props.closeTransfer}  
                                          transferDetail={this.props.transferDetail}  
                                          fields={this.props.fields}  
                                          account={this.props.account}  
                                          Transfer={this.props.Transfer}  
                                          inProgress={this.props.inProgress}  
                                          defaultGasPrice={this.props.defaultGasPrice}  
                                          defaultGasLimit={this.props.defaultGasLimit}  
                                          onInputChangeUpdateField={this.props.onInputChangeUpdateField}/>  
                     </div> :  
                     <div className={this.props.tx ? 'is-hidden' : ''}>  
                         <SortTokenBlock />  
                         <TokenBlock newTransfer={this.props.newTransfer} tokens={this.props.tokens} />  
                     </div>  
                     } 
                     <TradeMarkBlock tx={this.props.tx}/>  
                     </div>  
                 </div>  
             </div>  
         </section>  
         ) 
     }
 }  
  
export default Container;

This Container component holds several other components , toggles some components display as state changes and passes down their respectives props to them passed from app.js to it.

AddressBar Component displays the address of the active account
TokenBlock Component list the supported tokens available in a user wallet
TradeMarkBlock Component card footer shows images of badges
SortTokenBlock Component to sort list of only supported tokens in a wallet ASC/DESC
TransferToken Component contains the form to make transfer. It takes the address, amount to transfer and gas limit
TransferHeader Component shows information of token initiated for transfer, its name and description
SuccessTransaction Component displays notification message that shows right after a successfull transfer

Finally is our App.js that handle our state and event and passes data to the Container component. I will be commenting each section of the code to shed more light.

import React, { Component } from 'react';  
import Web3 from 'web3'  
import TruffleContract from 'truffle-contract'  
import Tokens from './Tokens/all';  
import Nav from './Components/Nav';  
import Description from './Components/Description';  
import Container from './Components/Container';  
import InstallMetamask from './Components/InstallMetamask';  
import UnlockMetamask from './Components/UnlockMetamask';  
import TokenZendR from './build/contracts/TokenZendR.json';  
  
class App extends Component {  
  constructor(){  
      super();  
    
      this.tokens = Tokens;  //list of supported tokens by token-zendr contract
      this.appName = 'TokenZendR';  
      this.isWeb3 = true; //If metamask is installed  
      this.isWeb3Locked = false; //If metamask account is locked  
      
      //bind this methods to enable them change state from children components
      this.newTransfer = this.newTransfer.bind(this);  
      this.closeTransfer = this.closeTransfer.bind(this);  
      this.onInputChangeUpdateField = this.onInputChangeUpdateField.bind(this);  
      
      this.state = {  
          tzAddress: null,          //address of the token-zendr contract
          inProgress: false,        //if a transfer action is in progress
          tx: null,                 //tx returned after a successfull transaction
          network: 'Checking...',   //default message to show while detecting network
          account: null,            //address of the currently unlocked metamask
          tokens: [],               //list of supported tokens owned by the user address
          transferDetail: {},  
          fields: {                 //form fields to be submitted for a transfer to be initiated 
              receiver: null,  
              amount: null,  
              gasPrice: null,  
              gasLimit: null,  
         },     
         defaultGasPrice: null,     
         defaultGasLimit: 200000  
      };  
      
      let web3 = window.web3;  
      
      //set web3 & truffle contract
      if (typeof web3 !== 'undefined') {  
          // Use Mist/MetaMask's provider  
          this.web3Provider = web3.currentProvider;  
          this.web3 = new Web3(web3.currentProvider);  
          
          this.tokenZendr = TruffleContract(TokenZendR);  
          this.tokenZendr.setProvider(this.web3Provider);  
          
          if (web3.eth.coinbase === null) this.isWeb3Locked = true;  
  
     }else{  
      this.isWeb3 = false;  
     } 
 }  
 
  //switch statement to check the current network and set the
  //value to be displayed on the nav component 
  setNetwork = () => {  
      let networkName,that = this;  
      
      this.web3.version.getNetwork(function (err, networkId) {  
      switch (networkId) {  
      case "1":  
          networkName = "Main";  
          break;  
      case "2":  
          networkName = "Morden";  
          break;  
      case "3":  
          networkName = "Ropsten";  
          break;  
      case "4":  
          networkName = "Rinkeby";  
          break;  
      case "42":  
          networkName = "Kovan";  
          break;  
      default:  
          networkName = networkId;  
     }  
     
     that.setState({  
          network: networkName  
       })  
    }); 
 };  
 
 //When a new transfer is initiated
 //set details of the token to be
 //transfered such as the address, symbol.. etc
  newTransfer = (index) => {  
      this.setState({  
      transferDetail: this.state.tokens[index]  
     }) 
 }; 
  
  //Called at the end of a successful
  //transfer to cclear form fields & transferDetails
  closeTransfer = () => {  
      this.setState({  
          transferDetail: {},  
          fields: {},  
     }) 
 };  
     
  setGasPrice = () => {  
      this.web3.eth.getGasPrice((err,price) => {  
          price = this.web3.fromWei(price,'gwei');  
          if(!err) this.setState({defaultGasPrice: price.toNumber()})  
     }); 
 };  
 
  setContractAddress = ()=> {  
      this.tokenZendr.deployed().then((instance) => {  
      this.setState({tzAddress: instance.address});  
     }); 
 };  
 
 //Reset app state
  resetApp = () => {  
      this.setState({  
          transferDetail: {},  
          fields: {  
              receiver: null,  
              amount: null,  
              gasPrice: null,  
              gasLimit: null,  
         },  
         defaultGasPrice: null,  
     })
  };  
 
  Transfer = () => {  
      //Set to true to allow some component disabled
      //and button loader to show transaction progress
      this.setState({  
          inProgress: true  
      });  
      
      //Use the ABI of a token at a particular address to call its methods
      let contract = this.web3.eth.contract(this.state.transferDetail.abi)
                          .at(this.state.transferDetail.address);  
      let transObj = {
      from: this.state.account,
      gas: this.state.defaultGasLimit,
      gasPrice: this.state.defaultGasPrice
      };  
      let app = this;  
      //Use the decimal places the token creator set to get actual amount of tokens to transfer
      let amount = this.state.fields.amount + 'e' + this.state.transferDetail.decimal;  
      let symbol = this.state.transferDetail.symbol;  
      let receiver = this.state.fields.receiver;  
  
  amount = new this.web3.BigNumber(amount).toNumber();  
  
  //Approve the token-zendr contract to spend on your behalf
  contract.approve(this.state.tzAddress, amount ,transObj, (err,response)=>{  
      if(!err) {  
          app.tokenZendr.deployed().then((instance) => {  
              this.tokenZendrInstance = instance;  
              this.watchEvents();  
              
              //Transfer the token to third party on user behalf
              this.tokenZendrInstance.transferTokens(symbol, receiver, amount, transObj)  
                 .then((response,err) => {  
                      if(response) { 
                          app.resetApp();  
                          
                          app.setState({  
                              tx: response.tx,  
                              inProgress: false  
                          });  
                     }else{  
                      console.log(err);  
                     } 
                 }); 
             }) 
         }else{  
          console.log(err);  
         } 
     }); 
 };  
 
 /**  
 * @dev Just a console log to list all transfers  
 */  
 watchEvents() {  
  let param = {from: this.state.account,to: this.state.fields.receiver,amount: this.state.fields.amount};  
  
  this.tokenZendrInstance.TransferSuccessful(param, {  
      fromBlock: 0,  
      toBlock: 'latest'  
  }).watch((error, event) => {  
      console.log(event);  
     }) 
 }  
 
  onInputChangeUpdateField = (name,value) => {  
      let fields = this.state.fields;  
  
      fields[name] = value;  
  
      this.setState({  
          fields  
      });  
 };  
 
  componentDidMount(){  
      let account = this.web3.eth.coinbase;  
      let app = this;  
      
      this.setNetwork();  
      this.setGasPrice();  
      this.setContractAddress();  
  
      this.setState({  
          account  
      });  
  
      //Loop through list of allowed tokens
      //using the token ABI & contract address
      //call the balanceOf method to see if this
      //address carries the token, then list on UI
      Tokens.forEach((token) => {  
          let contract = this.web3.eth.contract(token.abi);  
          let erc20Token = contract.at(token.address);  
      
      erc20Token.balanceOf(account,function (err,response) {  
          if(!err) {  
              let decimal = token.decimal;  
              let precision = '1e' + decimal;  
              let balance = response.c[0] / precision;  
              let name = token.name;  
              let symbol = token.symbol;  
              let icon = token.icon;  
              let abi = token.abi;  
              let address = token.address;  
      
              balance = balance >= 0 ? balance : 0;  
      
              let tokens = app.state.tokens;  
      
              if(balance > 0) tokens.push({  
                  decimal,  
                  balance,  
                  name,  
                  symbol,  
                  icon,  
                  abi,  
                  address,  
             });  
             
              app.setState({  
                  tokens  
              })  
            } 
         }); 
     }); 
 }  
 
  render() {  
      if(this.isWeb3) {  
          if(this.isWeb3Locked) {  
          return (  
             <div>  
                 <Nav appName={this.appName} network={this.state.network} />  
                 <UnlockMetamask message="Unlock Your Metamask/Mist Wallet" />  
             </div> 
         ) 
         }else {  
          return (  
             <div>  
                 <Nav appName={this.appName} network={this.state.network} />  
                 <Description />  
                 <Container onInputChangeUpdateField={this.onInputChangeUpdateField}  
                              transferDetail={this.state.transferDetail}  
                              closeTransfer={this.closeTransfer}  
                              newTransfer={this.newTransfer}  
                              Transfer={this.Transfer}  
                              account={this.state.account}  
                              defaultGasPrice={this.state.defaultGasPrice}  
                              defaultGasLimit={this.state.defaultGasLimit}  
                              tx={this.state.tx}  
                              inProgress={this.state.inProgress}  
                              fields={this.state.fields}  
                              tokens={this.state.tokens} />  
             </div>  
            ) 
        } 
    }else{  
         return(  
            <InstallMetamask />  
         ) 
        } 
     }
 }  
  
export default App;

Rename the app.css in src folder and replace it with the style below.

img.meta-trademark {  
  width: 20%;  
}  
  
#token-lists {  
  height: 300px;  
  overflow-y: scroll;  
}  
  
#token-lists div.token:nth-child(even) {  
  background: #f5f5f5;  
}  
  
#token-lists div.token {  
  cursor: pointer;  
}  
  
.sortby {  
  font-weight: 300;  
  cursor: pointer;  
}  
  
.token-icon {  
  width: 28px;  
  height: 28px;  
}  
  
.download-metamask {  
  height: 30%;  
  cursor: pointer;  
  width: 70%;  
  margin: auto;  
}  
  
.is-ellipsis {  
  overflow: hidden;  
  text-overflow: ellipsis;  
}

Add font-awesome library CDN to your public/index.html

<!DOCTYPE html>  
<html lang="en">  
<head>  
 <meta charset="utf-8">  
 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">  
 <meta name="theme-color" content="#000000">  
 <script defer  
 src="https://use.fontawesome.com/releases/v5.1.0/js/all.js"></script>  
  (html comment removed:   
 manifest.json provides metadata used when your web app is added to the homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ )  <link rel="manifest" href="%PUBLIC_URL%/manifest.json">  
 <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">  
 <title>React App</title>  
</head>  
<body>  
<noscript>  
  You need to enable JavaScript to run this app.  
</noscript>  
<div id="root"></div>  
</body>  
</html>

By now the application will be displaying but without bulma styling, to fix this problem we need to include bulma in the src/index.js file. Simply replace your index.js with this

import React from 'react';  
import ReactDOM from 'react-dom';  
import App from './App';  
import '.././node_modules/bulma-start/css/main.css'  
import './app.css';  
  
  
ReactDOM.render(<App />, document.getElementById('root'));

Run npm start or refresh the application if its already opened. When i do so the first screen am presented with because my metamask account is locked is this screenshot below.

enter image description here

Then i enter my metamask password, refresh the application and Voila!!
enter image description here

Interact with the application and see what else you can add. I hope you find this tutorial well explained, very educative with quality content. Let me hear your thought in the comment section.

Curriculum

Proof of Work Done

https://github.com/slim12kg/tokenzendr-contract.git
https://github.com/slim12kg/token-zendr-react-interface

Sort:  

Thank you for your contribution.
While I liked the content of your contribution, I would still like to extend one advice for your upcoming contributions:

  • Nice work on the explanations of your code, although adding a bit more comments to the code can be helpful as well

Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.

To view those questions and the relevant answers related to your post, click here.


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Thank's for your review @portugalcoin.

Thank you for your review, @portugalcoin!

So far this week you've reviewed 8 contributions. Keep up the good work!

Hey @alofe.oluwafemi
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!

Congratulations @alofe.oluwafemi! You have completed the following achievement on Steemit and have been rewarded with new badge(s) :

Award for the total payout received

Click on the badge to view your Board of Honor.
If you no longer want to receive notifications, reply to this comment with the word STOP

Do you like SteemitBoard's project? Then Vote for its witness and get one more award!

Congratulations @alofe.oluwafemi! You have completed the following achievement on Steemit and have been rewarded with new badge(s) :

Award for the number of upvotes

Click on the badge to view your Board of Honor.
If you no longer want to receive notifications, reply to this comment with the word STOP

Do not miss the last post from @steemitboard:
SteemitBoard and the Veterans on Steemit - The First Community Badge.

Do you like SteemitBoard's project? Then Vote for its witness and get one more award!

Congratulations @alofe.oluwafemi! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

You made more than 500 upvotes. Your next target is to reach 600 upvotes.

Click here to view your Board of Honor
If you no longer want to receive notifications, reply to this comment with the word STOP

Do not miss the last post from @steemitboard:

Meet the Steemians Contest - The results, the winners and the prizes
Meet the Steemians Contest - Special attendees revealed
Meet the Steemians Contest - Intermediate results

Support SteemitBoard's project! Vote for its witness and get one more award!

Congratulations @alofe.oluwafemi! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 1 year!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Do not miss the last post from @steemitboard:

Are you a DrugWars early adopter? Benvenuto in famiglia!
Vote for @Steemitboard as a witness to get one more award and increased upvotes!