Ethereum allows us to build Decentralized Applications (DApps). The main difference between a typical application and a DApp is that you don't need to deploy a backend. At least as long as you take advantage of the other smart contracts deployed in the Ethereum MainNet.
Because of that, the Frontend plays a major role. It is in charge of marshalling and unmarshalling the data from the smart contracts, handling the interactions with the wallet (hardware or software) and, as usual, managing the UX. Not only that, by design, a DApp uses JSON-RPC calls and it can open a socket connection to receive updates.
As you can see there are a few things to orchestrate but don't worry, the ecosystem has matured quite a lot in the last few months.
Prerequisites
During this tutorial, I will assume you already have the the following:
A wallet connect to a Geth node to connect
The simplest is installing MetaMask so that you can use Infura infrastructure out of the box.
Some Ether in your account
When you are developing with Ethereum I strongly advise you to switch to a test net and use fake Ethers. If you need funds for testing purpose you can use a faucet e.g. https://faucet.rinkeby.io/
Basic understand of React
I will guide you step by step but I will assume you know how React works (including hooks). If something seems unfamiliar consult the React documentation.
A working React playground
I wrote this tutorial with Typescript but just few things are typed so with minimal changes you can use it as it is in javascript as well. I used Parcel.js but feel free to use Create React App too or other web application bundler.
Connect to Ethereum Mainet
Once you have Metamask ready we are going to use web3-react to handle the interaction with the network. It will give you a quite handy hook useWeb3React
which contains many useful utilities for playing with Ethereum.
yarn add @web3-react/core @web3-react/injected-connector
Then you need a Provider. A Provider abstracts a connection to the Ethereum blockchain, for issuing queries and sending signed state-changing transactions.
We will use Web3Provider
from Ether.js.
It seems already a few libraries, but when interacting with Etherum you need to translate Javascript data types to Solidity ones. And, you are also required to sign the transactions when you want to execute an action. Ether.js elegantly provide these functionalities.
yarn add @ethersproject/providers
notice: the above Ether.js package is the v5 currently in beta
After that we are ready to jot down a minimal hello world to check if we have everything we need:
import React from 'react'
import { Web3ReactProvider } from '@web3-react/core'
import { Web3Provider } from '@ethersproject/providers'
import { useWeb3React } from '@web3-react/core'
import { InjectedConnector } from '@web3-react/injected-connector'
export const injectedConnector = new InjectedConnector({
supportedChainIds: [
1, // Mainet
3, // Ropsten
4, // Rinkeby
5, // Goerli
42, // Kovan
],
})
function getLibrary(provider: any): Web3Provider {
const library = new Web3Provider(provider)
library.pollingInterval = 12000
return library
}
export const Wallet = () => {
const { chainId, account, activate, active } = useWeb3React<Web3Provider>()
const onClick = () => {
activate(injectedConnector)
}
return (
<div>
<div>ChainId: {chainId}</div>
<div>Account: {account}</div>
{active ? (
<div>✅ </div>
) : (
<button type="button" onClick={onClick}>
Connect
</button>
)}
</div>
)
}
export const App = () => {
return (
<Web3ReactProvider getLibrary={getLibrary}>
<Wallet />
</Web3ReactProvider>
)
}
If you did your homework you should have something like this:
Here what we did so far: GIT - step-1
How to fetch data from the MainNet
I will use SWR to manage the data fetching.
This is what I want to achieve.
const { data: balance } = useSWR(["getBalance", account, "latest"])
Quite cool :)
Let's unveil the trick! SWR
means Stale-While-Revalidate, an HTTP cache invalidation strategy popularized by RFC 5861.
SWR first returns the data from cache (stale), then sends the fetch request (revalidate), and finally comes with the up-to-date data again.
SWR accepts akey
and behind the scenes will manage to resolve
To do that SWR allows passing a fetcher
capable of resolving the key
by returning a promise. The hello world of SWR is based on REST API requests with a fetcher based on fetch
API or Axios
.
What is brilliant about SWR is that the only requirement to create a fetcher is it must return a promise.
So here is my first implementation of a fetcher for Ethereum:
const fetcher = (library) => (...args) => {
const [method, ...params] = args
console.log(method, params)
return library[method](...params)
}
As you can see, it is a partially applied function
. In that way, I can inject the library
( my Web3Provider
) when I configure the fetcher. Later, every time a key
changes, the function can be resolved by returning the required promise.
Now I can create my <Balance/>
component
export const Balance = () => {
const { account, library } = useWeb3React<Web3Provider>()
const { data: balance } = useSWR(['getBalance', account, 'latest'], {
fetcher: fetcher(library),
})
if(!balance) {
return <div>...</div>
}
return <div>Balance: {balance.toString()}</div>
}
The balance object returned is a BigNumber
.
As you can see, the number is not formated and extremely large. This is because Solidity uses Integer up to 256 bits.
To display the number in a human readable format, the solution is using one of the aforementioned utilities from Ether.js utilities: formatEther(balance)
yarn install @ethersproject/units
Now that I can rework my <Balance/>
component to handle and format the BitInt in a human readable form:
export const Balance = () => {
const { account, library } = useWeb3React<Web3Provider>()
const { data: balance } = useSWR(['getBalance', account, 'latest'], {
fetcher: fetcher(library),
})
if(!balance) {
return <div>...</div>
}
return <div>Ξ {parseFloat(formatEther(balance)).toPrecision(4)}</div>
}
this what we did so far: GIT step-2
How to update the data in real-time
SWR exposes a mutate
function to update its internal cache.
const { data: balance, mutate } = useSWR(['getBalance', account, 'latest'], {
fetcher: fetcher(library),
})
const onClick = () => {
mutate(new BigNumber(10), false)
}
The mutate
function is automatically bound to the key (e.g. ['getBalance', account, 'latest']
from which it has been generated. It accepts two parameters. The new data and if a validation should be triggered. If it should, SWR will automatically use the fetcher to update the cache 💥
As anticipated, Solidity events give a tiny abstraction on top of the EVM’s logging functionality. Applications can subscribe and listen to these events through the RPC interface of an Ethereum client.
Ether.js has a simple API to subscribe to an event:
const { account, library } = useWeb3React<Web3Provider>()
library.on("blockNumber", (blockNumber) => {
console.log({blockNumber})
})
Now let's combine both approaches in the new <Balance/>
component
export const Balance = () => {
const { account, library } = useWeb3React<Web3Provider>()
const { data: balance, mutate } = useSWR(['getBalance', account, 'latest'], {
fetcher: fetcher(library),
})
useEffect(() => {
// listen for changes on an Ethereum address
console.log(`listening for blocks...`)
library.on('block', () => {
console.log('update balance...')
mutate(undefined, true)
})
// remove listener when the component is unmounted
return () => {
library.removeAllListeners('block')
}
// trigger the effect only on component mount
}, [])
if (!balance) {
return <div>...</div>
}
return <div>Ξ {parseFloat(formatEther(balance)).toPrecision(4)}</div>
}
Initially, SWR will fetch the account balance, and then every time it receives a block
event it will use mutate
to trigger a re-fetch.
notice: We used mutate(undefined, true)
because we can't retrieve from the current event the actual balance we just trigger a re-fetch of the balance.
Below is quick demo with two wallets that are exchanging some Ether.
Here what we did so far: GIT step-3
How to interact with a smart contract
So far we illustrated the basics of using SWR and how to make a basic call via a Web3Provider
. Let's now discover how to interact with a smart contract.
Ether.js handles smart contract interaction using the Contract Application Binary Interface (ABI) ABI generated by the Solidity Compiler.
The Contract Application Binary Interface (ABI) is the standard way to interact with contracts in the Ethereum ecosystem, both from outside the blockchain and for contract-to-contract interaction.
For example, given the below simple smart contract:
pragma solidity ^0.5.0;
contract Test {
constructor() public { b = hex"12345678901234567890123456789012"; }
event Event(uint indexed a, bytes32 b);
event Event2(uint indexed a, bytes32 b);
function foo(uint a) public { emit Event(a, b); }
bytes32 b;
}
this is the ABI generated
[
{
"type": "event",
"inputs": [
{ "name": "a", "type": "uint256", "indexed": true },
{ "name": "b", "type": "bytes32", "indexed": false }
],
"name": "Event"
},
{
"type": "event",
"inputs": [
{ "name": "a", "type": "uint256", "indexed": true },
{ "name": "b", "type": "bytes32", "indexed": false }
],
"name": "Event2"
},
{
"type": "function",
"inputs": [{ "name": "a", "type": "uint256" }],
"name": "foo",
"outputs": []
}
]
To use the ABIs, we can simply copy them directly into your code and import them where it is required. In this demo, we will use a standard ERC20 ABI because we want to retrieve the balances of two tokens: DAI and MKR.
Next step is creating the <TokenBalance/>
component
export const TokenBalance = ({ symbol, address, decimals }) => {
const { account, library } = useWeb3React<Web3Provider>()
const { data: balance, mutate } = useSWR([address, 'balanceOf', account], {
fetcher: fetcher(library, ERC20ABI),
})
useEffect(() => {
// listen for changes on an Ethereum address
console.log(`listening for Transfer...`)
const contract = new Contract(address, ERC20ABI, library.getSigner())
const fromMe = contract.filters.Transfer(account, null)
library.on(fromMe, (from, to, amount, event) => {
console.log('Transfer|sent', { from, to, amount, event })
mutate(undefined, true)
})
const toMe = contract.filters.Transfer(null, account)
library.on(toMe, (from, to, amount, event) => {
console.log('Transfer|received', { from, to, amount, event })
mutate(undefined, true)
})
// remove listener when the component is unmounted
return () => {
library.removeAllListeners(toMe)
library.removeAllListeners(fromMe)
}
// trigger the effect only on component mount
}, [])
if (!balance) {
return <div>...</div>
}
return (
<div>
{parseFloat(formatUnits(balance, decimals)).toPrecision(4)} {symbol}
</div>
)
}
Let's zoom in. There are two main differences:
Key definition
The key, used by useSWR([address, 'balanceOf', account])
), needs to start with an Ethereum address
rather than a method
. Because of that, the fetcher can recognize what we want to achieve and use the ABI.
Let's refactor the fetcher accordingly:
const fetcher = (library: Web3Provider, abi?: any) => (...args) => {
const [arg1, arg2, ...params] = args
// it's a contract
if (isAddress(arg1)) {
const address = arg1
const method = arg2
const contract = new Contract(address, abi, library.getSigner())
return contract[method](...params)
}
// it's a eth call
const method = arg1
return library[method](arg2, ...params)
}
Now we have a general-purpose fetcher capable of interacting with the JSON-RPC calls of Ethereum. 🙌
Log filters
The other aspect in <TokenBalance/>
is how to listen for the ERC20 events. Ether.js
provides a handy way to configure a filter based on the topics and name of the event. More info about what is a topic can be found in the Solidity Doc
const contract = new Contract(address, ERC20ABI, library.getSigner())
const fromMe = contract.filters.Transfer(account, null)
Once you have built a contract instance with the ABI, then you can pass the filter to the library instance.
Warning:
You could be tempted to use the
amount
in the ERC20 event directly to increase or decrease the balance.
Be aware of the dragon. When you setup the fetcher, you passed a closjure as callback to theon
function, which contained the balance value at the time.
This could be fixed using auseRef
but for the sake of somplicity let's revalidate the cache to ensure the balances are fresh:mutate(undefined, true)
We now have all of the pieces required, the last bit is a bit of glue.
I configured a few constants in order to have a nice way to map my TokenBalance component to a list of tokens depending on the network where we are working:
export const Networks = {
MainNet: 1,
Rinkeby: 4,
Ropsten: 3,
Kovan: 42,
}
export interface IERC20 {
symbol: string
address: string
decimals: number
name: string
}
export const TOKENS_BY_NETWORK: {
[key: number]: IERC20[]
} = {
[Networks.Rinkeby]: [
{
address: "0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa",
symbol: "DAI",
name: "Dai",
decimals: 18,
},
{
address: "0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85",
symbol: "MKR",
name: "Maker",
decimals: 18,
},
],
}
Once we have the constants it's easy to map the configured tokens to my <TokenList/>
component:
export const TokenList = ({ chainId }) => {
return (
<>
{TOKENS_BY_NETWORK[chainId].map((token) => (
<TokenBalance key={token.address} {...token} />
))}
</>
)
}
All set! Now we have an Ethereum Wallet that loads Ether and Token balances. And If the user sends or receives funds the wallet UI is updated.
Here what we did so far: GIT step-4
Refactoring
Let's move every component in a separated file and make the fetcher a globally available using SWRConfig provider.
<SWRConfig value={{ fetcher: fetcher(library, ERC20ABI) }}>
<EthBalance />
<TokenList chainId={chainId} />
<SWRConfig/>
With SWRConfig
we can configure some options as always available, so that we can have a more convenient usage of SWR.
const {data: balance, mutate} = useSWR([address, 'balanceOf', account])
Here after the refactoring: GIT step-5
Wrap Up
SWR and Ether.js are two nice libraries to work with if you want to streamline your data fetching strategy with Ethereum Dapp.
Key Advantages
- Declarative approach
- Data always fresh via web sockets or SWR options
- Avoid reinventing the wheel for state management with custom React context
If you use multiple smart contracts in your DApp and you liked this tutorial I generalised the web3 fetcher into a small util: swr-eth (Stars are appreciated 👻)
Feel free to use it and let me know what you think about this approach in the comments below
And finally, here is the full GIT repo: (https://github.com/aboutlo/swr-eth-tutorial)
Top comments (1)
hello, can I ask if this SWR hook works also in Next.js where data must be ready at build-time?