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>
)
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:
After we click “Connect with MetaMask”, it asks to connect the account.
When accepted, our application shows the account number.
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
After Ganache is initialized, we can see all the accounts created in our local development blockchain
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.
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.
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
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.
Then, choose Custom RPC
in MetaMask network menu.
Enter network details from Ganache.
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.
After we successfully imported the account, we can see ETH balance from our Ganache network account
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))
})
}
Now it is possible to increment the number using the button!