Simple Counter App on Ethereum Blockchain

Create new ReactJS application

Let’s create a very simple application on the Ethereum Blockchain using ReactJS and TypeScript.
First, let’s create our application via create-react-app.

PS> npx create-react-app blockchain-counter --template typescript
PS> cd blockchain-counter

We will use web3.js to interact with Blockchain. Let’s add the latest stable version with types

PS> yarn add web3
PS> yarn add -D @types/web3

For styling, we will use styled-components with types

PS> yarn add styled-components
PS> yarn add -D @types/styled-components

Since we will use StyledComponents, we can remove App.css, and clean up App.tsx a bit

import React from 'react'
import styled from 'styled-components'

const StyledAppContainer = styled.div``

function App() {
  return <StyledAppContainer />
}

export default App

To run the application, use:

PS> yarn start

Let’s give our application some nice background, by adding the following css to styled.div

height: 100vh;
display: flex;
align-items: center;
justify-content: center;

background: rgb(40, 61, 88);
background: linear-gradient(
  180deg,
  rgba(40, 61, 88, 1) 0%,
  rgba(45, 61, 82, 1) 32%,
  rgba(52, 58, 66, 1) 100%
);

Now, we will add our main form.
Create components/MainForm directory with the following index.tsx file.

import styled from 'styled-components'

const StyledMainForm = styled.div`
  background-color: #202429;
  border-radius: 30px;
  padding: 30px;
`

export default StyledMainForm

Let’s add a component showing users account number.
Create components/AccountDetails directory with the following index.tsx file.

import React from 'react'
import styled from 'styled-components'

type AccountDetailsProps = {
  accountNumber: string
}

const AccountDetails = (props: AccountDetailsProps) => {
  return <div {...props}>{props && props.accountNumber}</div>
}

const StyledAccountDetails = styled(AccountDetails)`
  display: inline-block;
  border: 2px solid #2b2f36;
  border-radius: 12px;
  padding: 10px;
  color: white;
  font-weight: 600;
`

export default StyledAccountDetails

Now in App.tsx file we will use useState() to store our current account number.
At the beginning of function App() add:

const [currentAccountNumber, setCurrentAccountNumber] = useState("");

To display the now empty account number to the user, we will use components we created before:

return (
  <StyledAppContainer>
    <MainForm>
      <AccountDetails accountNumber={currentAccountNumber} />
    </MainForm>
  </StyledAppContainer>
)

Retrieving account number

Before we can do anything with, we have to allow the user to connect to our site with MetaMask.
To do this, we will need another library called rimble-ui

PS> yarn add rimble-ui

We will use MetaMaskButton.

import { MetaMaskButton } from 'rimble-ui';

Since rimble-ui doesn’t have typings, we will declarations.d.ts to our src directory with the following content:

declare module 'rimble-ui'

Let’s state used for storing MetaMask connection status

const [isMetaMaskConnected, setIsMetaMaskConnected] = useState(false)

Now we can show our button if MetaMask is not connected

  return (
    <StyledAppContainer>
      {isMetaMaskConnected ?
        <MainForm>
          <AccountDetails accountNumber={currentAccountNumber} />
        </MainForm> :
        <MetaMaskButton.Outline onClick={() => handleOnConnectWithMetaMaskClick()}>
          Connect with MetaMask
        </MetaMaskButton.Outline>
      }
    </StyledAppContainer>
  );

We have to add logic that will retrieve account number when the user opens the application.

useEffect(() => {
  loadBlockchainData()
})

const loadBlockchainData = async () => {
  const web3 = new Web3(Web3.givenProvider || 'http://localhost:8545')
  const network = await web3.eth.net.getNetworkType()
  const accounts = await web3.eth.getAccounts()
  setCurrentAccountNumber(accounts[0])
  setIsMetaMaskConnected(accounts.length > 0)
}

And function that will be called on “Connect with MetaMask” click:

const handleOnConnectWithMetaMaskClick = async () => {
  if (
    await (window as any).ethereum.request({ method: 'eth_requestAccounts' })
  ) {
    loadBlockchainData()
  }
}

Now, our application looks like this:

Application

After we click “Connect with MetaMask”, it asks to connect the account.
When accepted, our application shows the account number.

Application

Building the counter contract in solidity

First, we need to globally install truffle - development environment for Ethereum.
In this tutorial I’ll use 5.1.46 version.

PS> npm install -g truffle@5.1.46

Now, lets create our project. I’ll create separate blockchain-counter-eth directory, and init a project there.

PS> truffle init

After the project has been initialized, remember to adjust truffle-config to use the newest compiler.
In our case it will be 0.7.1 version. compilers section of truffle-config should look like this:

  compilers: {
    solc: {
      version: "0.7.1",
      settings: {
       optimizer: {
         enabled: true,
         runs: 200
       }
      }
    }
  }

To finish the setup, we have to add package.json:

{
  "name": "blockchain-counter-eth",
  "version": "1.0.0",
  "description": "",
  "main": "truffle-config.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "dev": "lite-server"
  },
  "devDependencies": {
    "truffle": "^5.1.46",
    "@truffle/contract": "^4.2.23"
  }
}

To the contract we will create, we need to fire up a personal Ethereum blockchain.
To do this, we have to install Ganache from https://www.trufflesuite.com/ganache

After the installation, on first start, use “Quick Start Ethereum” in Ganache

Ganache Workspace Configuration

After Ganache is initialized, we can see all the accounts created in our local development blockchain

Ganache Workspace

Open the settings using the cog icon, and on the SERVER tab we can check server details.
We have to adjust the port on our truffle project. truffle-config.json development section should look like this:

    development: {
     host: "127.0.0.1",
     port: 7545,
     network_id: "*",
    }

Since we’ve configured the environment - Lets add new contract.

We will begin with a very simple implementation, that won’t contain any logic yet.

contracts/Counter.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.7.0;

contract Counter {
    uint public currentNumber = 0;
}

To be able to run our contract on the blockchain, we have to also add a migration

migrations/2_counter_migration.js

const Counter = artifacts.require('Counter')

module.exports = function(deployer) {
  deployer.deploy(Counter)
}

We can test this minimal implementation.
To build our contract, use:

PS> truffle build

To deploy it to the blockchain use

PS> truffle migrate
...
Summary
=======
> Total deployments:   2
> Final cost:          0.00550732 ETH

We can see our contract has been deployed, and the gas cost.

Lets check the contract, by using the truffle console

PS> truffle console
truffle(development)> counter = await Counter.deployed()
truffle(development)> counter.address
truffle(development)> currentNumber = await counter.currentNumber()
truffle(development)> currentNumber.toNumber()
0

We should see that currentNumber is 0. That is correct, since we havent implemented any increment() logic yet.

Add increment functionality

Lets add increment() function to our contract

contract Counter {
    uint public currentNumber = 0;

    function increment() public {
        currentNumber++;
    }
}

To update the contract, use

PS> truffle build
PS> truffle migrate --reset

Lets preview the currentNumber

truffle(development)> counter = await Counter.deployed()
truffle(development)> (await counter.currentNumber()).toNumber()
0

Lets check our increment function

truffle(development)> await counter.increment()

{
  tx: '0xe41de87e5fca4b14541dc736adb60ee19607a72de33e698b09a844fda85ea47d',
  receipt: {
    transactionHash: '0xe41de87e5fca4b14541dc736adb60ee19607a72de33e698b09a844fda85ea47d',
    transactionIndex: 0,
    blockHash: '0x99037bfbf4c718f2024dd8eb82863b38b2eeb1f3e648f27e496fa064db5a218d',
    blockNumber: 7,
    from: '0xbfc7bfc577c266e9788ab6ecbdfd5e13683ce0a6',
    to: '0xbabf8e8f008c554999778d5f17207d9c652aba59',
    gasUsed: 27023,
    cumulativeGasUsed: 27023,
    contractAddress: null,
    logs: [],
    status: true,
    logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
    rawLogs: []
  },
  logs: []
}

As we can see, new block has been created

Lets check the currentNumber again

truffle(development)> (await counter.currentNumber()).toNumber()
1

It works. The increment() function correctly increased the currentNumber by 1.

Adjusting ReactJS app to work with the new contract.

Now, we add a comopnent displaying current counter value, with a button to call our increase() function in the contract.

Create components/CounterDetails/index.tsx

import React from 'react'
import styled from 'styled-components'

type CounterDetailsProps = {
  currentNumber: number
  onIncreaseNumberClick: Function
}

const StyledIncreaseButton = styled.button`
  width: 100%;
  outline: none;
  font-size: 1em;
  margin-top: 15px;
  padding: 10px;
  border-radius: 3px;

  background-color: #080;
  color: white;
  border: 2px solid #080;

  &:hover {
    background-color: #090;
    border: 2px solid #090;
  }

  &:active {
    background-color: #080;
    border: 2px solid #080;
  }
`

const CounterDetails = (props: CounterDetailsProps) => {
  return (
    <div {...props}>
      <div>Current Number: {props.currentNumber}</div>
      <StyledIncreaseButton onClick={() => props.onIncreaseNumberClick()}>
        Increase
      </StyledIncreaseButton>
    </div>
  )
}

const StyledCounterDetails = styled(CounterDetails)`
  display: block;
  margin-top: 10px;
  border: 2px solid #2b2f36;
  border-radius: 12px;
  padding: 10px;
  color: white;
  font-weight: 600;
`

export default StyledCounterDetails

Execute contract logic

Before we can call blockchain logic, we have to connect MetaMask to Gnache.
On top of Ganache window we can find our local server address. Ganache RPC

Then, choose Custom RPC in MetaMask network menu.
Enter network details from Ganache.

Ganache custom network details

Lets import the first account from Ganache by clicking on the key icon next to the account, and copy the private address.
Then, choose Import Account in MetaMask account menu.

Metamask import account

After we successfully imported the account, we can see ETH balance from our Ganache network account

Metamask Ganache balance

Lets add state that will hold our ethereumSessionInstance:

const [ethereumSessionInstance, setEthereumSessionInstance] = useState<
  EthereumSessionType
>({ methods: undefined })

Now, we have to load our contract. To do so, we update loadBlockchainData()

const loadBlockchainData = async () => {
  const network = await web3.eth.net.getNetworkType()
  const accounts = await web3.eth.getAccounts()
  setCurrentAccountNumber(accounts[0])
  setIsMetaMaskConnected(accounts.length > 0)
  web3.eth.defaultAccount = accounts[0]

  let myContract = new web3.eth.Contract(
    [
      {
        inputs: [],
        name: 'currentNumber',
        outputs: [
          {
            internalType: 'uint256',
            name: '',
            type: 'uint256',
          },
        ],
        stateMutability: 'view',
        type: 'function',
        constant: true,
      },
      {
        inputs: [],
        name: 'increment',
        outputs: [],
        stateMutability: 'nonpayable',
        type: 'function',
      },
    ],
    '0xE399792aDAce0712B458b90225244b55DD61F157'
  )

  setEthereumSessionInstance(myContract)

  myContract.methods
    .currentNumber()
    .call()
    .then((x: number) => setCurrentNumber(x))
}

We define our contract via new web3.eth.Contract() called with Contract Application Binary Interface (ABI) of our contract.
It can be found in build/contracts/Counter.json file in our contract project directory.
Copy the array present in abi key, and paste it as a parameter to new web3.eth.Contract().

Lets execute our contract logic, after we click on then increment button.
Implementation of handleOnIncreaseNumberClick() is as follows:

const handleOnIncreaseNumberClick = async () => {
  ethereumSessionInstance.methods
    .increment()
    .send({
      from: web3.eth.defaultAccount,
    })
    .on('receipt', () => {
      ethereumSessionInstance.methods
        .currentNumber()
        .call()
        .then((x: number) => setCurrentNumber(x))
    })
}

Conclusion

Now it is possible to increment the number using the button!

Final App