To integrate a smart contract with a frontend, the first task is usually how to connect a wallet to your frontend application. This blog post will show you how to connect a wallet from a React application.
Packages to install
The following packages will be needed to connect the wallet:
- WalletConnectProvider
- WalletLink
- Web3Modal
- Ether (this is used to connect to the smart contract on the blockchain)
The following packages above can be installed in a React application by running the following commands on terminal:
npm i --save walletlink @walletconnect/web3-provider ethers
web3modal
After installing the following packages, open your React App.js
file or the top most component of your application. We will firstly import the packages we just installed.
import { useEffect, useState, useCallback } from "react";
import { ethers, providers } from "ethers";
import Web3Modal from "web3modal";
import WalletConnectProvider from '@walletconnect/web3-provider'
import WalletLink from 'walletlink';
const App = () => {
return (
<div>
<h1>Hello</h1>
</div>
)
}
This is the skeleton of a simple React component. We declared a web3Modal that will allow the select of the wallet we want to use. We need to create a provider option that will be passed to the web3Modal
.
const INFURA_ID = "your-infura-api-key";
const providerOptions = {
walletconnect: {
package: WalletConnectProvider, // required
options: {
infuraId: INFURA_ID, // required
},
},
'custom-walletlink': {
display: {
logo: 'https://play-lh.googleusercontent.com/PjoJoG27miSglVBXoXrxBSLveV6e3EeBPpNY55aiUUBM9Q1RCETKCOqdOkX2ZydqVf0',
name: 'Coinbase',
description: 'Connect to Coinbase Wallet',
},
options: {
appName: 'Coinbase', // Your app name
networkUrl: `https://mainnet.infura.io/v3/${INFURA_ID}`,
chainId: 1,
},
package: WalletLink,
connector: async (_, options) => {
const { appName, networkUrl, chainId } = options
const walletLink = new WalletLink({
appName,
})
const provider = walletLink.makeWeb3Provider(networkUrl, chainId)
await provider.enable()
return provider
},
},
}
The WalletLink
package allows users to use your application in any desktop browser without installing an extension, and it established a secure tunnel between your app and the mobile wallet with end-to-end encryption utilizing client-generated keys and keeps all user activity private.
WalletConnect
package is an open protocol that helps to communicate securely between Wallets and Dapps (Web3 Apps). The protocol establishes a remote connection between two apps and/or devices using a Bridge server to relay payloads. We need to provide an api key to connect.
let web3Modal
if (typeof window !== 'undefined') {
web3Modal = new Web3Modal({
network: 'mainnet', // optional
cacheProvider: true,
providerOptions, // required
})
}
We pass the providerOptions to the web3Modal instance. The App.jsx looks exactly like this now.
import { useEffect, useState, useCallback } from "react";
import { ethers, providers } from "ethers";
import Web3Modal from "web3modal";
import WalletConnectProvider from '@walletconnect/web3-provider'
import WalletLink from 'walletlink';
const INFURA_ID = "your-infura-api-key";
const providerOptions = {
walletconnect: {
package: WalletConnectProvider, // required
options: {
infuraId: INFURA_ID, // required
},
},
'custom-walletlink': {
display: {
logo: 'https://play-lh.googleusercontent.com/PjoJoG27miSglVBXoXrxBSLveV6e3EeBPpNY55aiUUBM9Q1RCETKCOqdOkX2ZydqVf0',
name: 'Coinbase',
description: 'Connect to Coinbase Wallet',
},
options: {
appName: 'Coinbase', // Your app name
networkUrl: `https://mainnet.infura.io/v3/${INFURA_ID}`,
chainId: 1,
},
package: WalletLink,
connector: async (_, options) => {
const { appName, networkUrl, chainId } = options
const walletLink = new WalletLink({
appName,
})
const provider = walletLink.makeWeb3Provider(networkUrl, chainId)
await provider.enable()
return provider
},
},
}
let web3Modal
if (typeof window !== 'undefined') {
web3Modal = new Web3Modal({
network: 'mainnet', // optional
cacheProvider: true,
providerOptions, // required
})
}
const App = () => {
return (
<div>
<h1>Hello</h1>
</div>
)
}
The provider object needs to be persistent in our application and to prevent passing props down all the component of our application. We will create a context file which we will name walletContext.js
.
This file will contain the following contents:
import * as React from "react";
const WalletContext = React.createContext();
const accountDetails = {
provider: null,
address: null,
signer: null,
web3Provider: null,
network: null
}
function WalletProvider({children}){
const [account, setAccountDetails ] =
React.useState(accountDetails);
const value = { account, setAccountDetails };
return <WalletContext.Provider value={value}>{children}
</WalletContext.Provider>
}
function useWallet(){
const context = React.useContext(WalletContext);
if (!context){
throw new Error("useWallet must be used within a WalletProvider")
}
return context;
}
export {WalletProvider, useWallet }
We created a React context and named it WalletContext
. Then we created a WalletProvider
function that returns the WalletContext.Provider
component wrapped with any children
that will be passed to it. Inside the WalletProvider
function, we declared a React state that will be used to store the details of the connected account. The account
state and the function to change the state setAccountDetails
is passed to the value
property of the WalletContext.Provider
component. See application state management
We need to create a button that will be used to connect the wallet. We will define two functions which are connect
and the disconnect
functions. As their name suggest, they will be used to connect and disconnect and disconnect the wallet from the blockchain. Before then we need to create React state that will be used to store the value returned from the functions.
Next we created a function that will consume the created WalletContext
. The useWallet
hook consumes the WalletContext
and returns the context
for use.
<-- previous code above -->
const App = () => {
const { account, setAccountDetails } = useWallet();
const { provider,
address,
signer,
web3Provider,
network } = account;
const connect = useCallback(async function () {
const provider = await web3Modal.connect();
const web3Provider = new providers.Web3Provider(provider);
const signer = web3Provider.getSigner()
const address = await signer.getAddress()
const network = await web3Provider.getNetwork();
const accountDetails = {
provider,
web3Provider,
signer,
address,
network
}
setAccountDetails(accountDetails);
}, []);
const disconnect = useCallback(
async function () {
await web3Modal.clearCachedProvider()
if (provider?.disconnect && typeof provider.disconnect === 'function') {
await provider.disconnect()
}
//reset the state here
const accountDetails = {
provider: null,
web3Provider: null,
signer: null,
address: null,
network: null
}
setAccountDetails(accountDetails);
},
[provider]
)
return (
<div>
<h1>Hello</h1>
</div>
)
}
The connect function creates a provider variable that is set to the default web3Modal connection. This provider is then plugged into the provider object of ether.js
.
const web3Provider = new providers.Web3Provider(provider)
We are able to use the web3Provider
object created by ether.js
to get the signer
, network
, address
from ether.js
. This values are saved in the context using the setAccountDetails
function.
The disconnect
function clears the web3Modal
cache , the saved state and also disconnect the provider
. We optimized both functions by wrapping them in a useCallback
and passing dependencies.
Auto connecting
If we have already connected to our wallet before, we will want to automatically reconnect when visiting the page or component. This we can do by calling connect
in a useEffect
hook.
// Auto connect to the cached provider
useEffect(() => {
if (web3Modal.cachedProvider) {
connect()
}
}, [connect]);
We can also listen to events on the provider object to handle some scenarios. Like we may want to be still connected, if a user switch the connected account. The function handleAccountsChanged
does that by listening for an account changed event and updates the address portion of the WalletContext
.
useEffect(() => {
if (provider?.on) {
const handleAccountsChanged = (accounts) => {
console.log('accountsChanged', accounts);
setAccountDetails({
...account,
address: accounts[0],
})
}
const handleChainChanged = (_hexChainId) => {
window.location.reload()
}
const handleDisconnect = (error) => {
console.log('disconnect', error)
disconnect()
}
provider.on('accountsChanged', handleAccountsChanged)
provider.on('chainChanged', handleChainChanged)
provider.on('disconnect', handleDisconnect)
// Subscription Cleanup
return () => {
if (provider.removeListener) {
provider.removeListener('accountsChanged', handleAccountsChanged)
provider.removeListener('chainChanged', handleChainChanged)
provider.removeListener('disconnect', handleDisconnect)
}
}
}
}, [provider, disconnect])
Putting it all together
//APP>JX
import { useEffect, useState, useCallback } from "react";
import { ethers, providers } from "ethers";
import Web3Modal from "web3modal";
import WalletConnectProvider from '@walletconnect/web3-provider'
import WalletLink from 'walletlink';
import { useWallet } from './walletContext';
import './App.css';
const trimAddress = ( address ) => {
const firstpart = address.slice(0, 4);
const midpart = "....";
const endpart = address.slice(address.length - 4, address.length );
return `${firstpart}${midpart}${endpart}`
}
const INFURA_ID = '460f40a260564ac4a4f4b3fffb032dad'
const providerOptions = {
walletconnect: {
package: WalletConnectProvider, // required
options: {
infuraId: INFURA_ID, // required
},
},
'custom-walletlink': {
display: {
logo: 'https://play-lh.googleusercontent.com/PjoJoG27miSglVBXoXrxBSLveV6e3EeBPpNY55aiUUBM9Q1RCETKCOqdOkX2ZydqVf0',
name: 'Coinbase',
description: 'Connect to Coinbase Wallet (not Coinbase App)',
},
options: {
appName: 'Coinbase', // Your app name
networkUrl: `https://mainnet.infura.io/v3/${INFURA_ID}`,
chainId: 1,
},
package: WalletLink,
connector: async (_, options) => {
const { appName, networkUrl, chainId } = options
const walletLink = new WalletLink({
appName,
})
const provider = walletLink.makeWeb3Provider(networkUrl, chainId)
await provider.enable()
return provider
},
},
}
let web3Modal
if (typeof window !== 'undefined') {
web3Modal = new Web3Modal({
network: 'mainnet', // optional
cacheProvider: true,
providerOptions, // required
})
}
function App() {
const { account, setAccountDetails } = useWallet();
const { provider,
address,
signer,
web3Provider,
network } = account;
const connect = useCallback(async function () {
const provider = await web3Modal.connect();
const web3Provider = new providers.Web3Provider(provider);
const signer = web3Provider.getSigner()
const address = await signer.getAddress()
const network = await web3Provider.getNetwork();
const accountDetails = {
provider,
web3Provider,
signer,
address,
network
}
setAccountDetails(accountDetails);
}, []);
const disconnect = useCallback(
async function () {
await web3Modal.clearCachedProvider()
if (provider?.disconnect && typeof provider.disconnect === 'function') {
await provider.disconnect()
}
//reset the state here
const accountDetails = {
provider: null,
web3Provider: null,
signer: null,
address: null,
network: null
}
setAccountDetails(accountDetails);
},
[provider]
)
// Auto connect to the cached provider
useEffect(() => {
if (web3Modal.cachedProvider) {
connect()
}
}, [connect]);
useEffect(() => {
if (provider?.on) {
const handleAccountsChanged = (accounts) => {
// eslint-disable-next-line no-console
console.log('accountsChanged', accounts);
setAccountDetails({
...account,
address: accounts[0],
})
}
const handleChainChanged = (_hexChainId) => {
window.location.reload()
}
const handleDisconnect = (error) => {
console.log('disconnect', error)
disconnect()
}
provider.on('accountsChanged', handleAccountsChanged)
provider.on('chainChanged', handleChainChanged)
provider.on('disconnect', handleDisconnect)
// Subscription Cleanup
return () => {
if (provider.removeListener) {
provider.removeListener('accountsChanged', handleAccountsChanged)
provider.removeListener('chainChanged', handleChainChanged)
provider.removeListener('disconnect', handleDisconnect)
}
}
}
}, [provider, disconnect])
return (
<div className="App">
{web3Provider ? (
<button className="btn btn-danger" type="button" onClick={disconnect}>
{trimAddress(address)}
</button>
) : (
<button className="btn btn-success" type="button" onClick={connect}>
Connect
</button>
)}
</div>
);
}
export default App;
To make use of the WalletProvider
we wrap our App component with it. This makes the WalletContext
available in all the components that it will be needed.
import { WalletProvider } from "./walletContext";
ReactDOM.render(
<React.StrictMode>
<WalletProvider>
<App />
</WalletProvider>
</React.StrictMode>,
document.getElementById('root')
The code is available here
Thanks for reading.
Top comments (1)
ok