Use VanillaJS To Connect To MetaMask, Read From A Contract, & Write To A Contract
Build A Frontend With No Libraries & Use Raw RPC Requests To Manage Wallet Connections & Contract Interactions
Why VanillaJS?
While there are libraries that are out there like like web3.js, ethers.js, wagmi, web3-react, useDApp, and a bunch more, I made this project to show that you can perform wallet interactions with just the injected provider by MetaMask and soon Phantom wallet.
It should be noted that all the contract requests are standard JSON-RPC requests that are supported by most EVM chains.
What We’re Building
We’ll be building a frontend that checks when a user is connected, what network they are on, when the user switches wallets or networks, reading from a live contract, and writing to a contract.
Demo
Here is a demo of the application working.
NOTE: You’ll need MetaMask installed with your Chrome browser.
Building The App
Remember, we’re building an app from scratch with just pure JavaScript and HTML. I should mention that most requests will be performed with just JavaScript, there are however some things that need a library to perform specific types of hashing/encoding.
The Contract
The contract that we will be using as a base is a simple Solidity contract that allows for setters, getters, and logging events.
File: Greeter.sol
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract Greeter {
// Events that allows for emitting a message
event NewGreeting(address sender, string message);
// Variables
string greeting;
// Main constructor run at deployment
constructor(string memory _greeting) {
greeting = _greeting;
emit NewGreeting(msg.sender, _greeting);
}
// Get function
function getGreeting() public view returns (string memory) {
return greeting;
}
// Set function
function setGreeting(string memory _greeting) public {
greeting = _greeting;
emit NewGreeting(msg.sender, _greeting);
}
}
The code is just to show you what you will be working with, but I already took the liberty of deploying this contract on various networks so that you can interact with it.
- Ethereum Mainnet Greeter Contract
- Goerli Testnet Greeter Contract
- Polygon Mainnet Greeter Contract
- Polygon zkEVM Testnet Greeter Contract
- Mumbai Testnet Greeter Contract
- Sepolia Testnet Greeter Contract
Requirements
Before we begin, make sure to have the following installed on your computer to continue.
- nvm or node
v18.12.1
- pnpm
v7.15.0
Setting Up Our Environment
Because we’ll using just pure JavaScript, we’ll still need a server, and because I still want to take advantage of HMR so that we can see our changes reflected in frontend, we’re going to use live-server.
# Create our project directory
mkdir vanillajs-wallet-connection;
cd vanillajs-wallet-connection;
# Setup the server
git init;
pnpm init;
pnpm add live-server;
# Create our files
mkdir app;
touch app/index.html;
touch app/scripts.js;
echo "node_modules" > .gitignore;
I’ll skip the steps of explaining the UI, but one thing I want to do is add Tailwind to the project to make it look nicer but not installed as node package but a CDN. We’ll copy this code to our index.html
file.
File: ./app/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VanillaJS Wallet Connection</title>
<!-- TAILWIND STYLING -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Our main script -->
<script src="./scripts.js"></script>
</head>
<body class="bg-zinc-900">
<div class="p-8">
<h1 class="text-2xl text-white mb-4">VanillaJS Wallet Connection</h1>
<p class="text-zinc-400 mb-4">Basic JavaScript demonstration to interact with a MetaMask wallet and a contract.</p>
<div class="mb-8">
<button type="button" disabled id="button-connect"
class="h-10 bg-zinc-200 text-zinc-800 px-5 rounded-full font-medium disabled:bg-opacity-30 hover:bg-white transition-colors ease-in-out duration-200">
Connect Wallet (Unsupported)
</button>
<div id="div-error-connect" class="mt-4 bg-red-300 rounded p-6 text-red-800 hidden"></div>
</div>
<hr class="border-zinc-700 mb-8" />
<section id="section-connected" class="hidden">
<h2 id="wallet-connection" class="text-xl text-zinc-200 mb-4">Wallet Connection</h2>
<p class="text-zinc-400 mb-4">If you're seeing this then your wallet is connected.</p>
<div class="mb-4">
<button type="button" id="button-disconnect"
class="h-10 mb-2 bg-zinc-200 text-zinc-800 px-5 rounded-full font-medium disabled:bg-opacity-30 hover:bg-white transition-colors ease-in-out duration-200">
Disconnect*
</button>
<p class="text-sm text-zinc-300">
<small>
*Remember you're not really disconnecting unless the wallet removes the website from Connected Sites.
</small>
</p>
</div>
<div class="mb-8">
<label class="block mb-2 text-zinc-600">Wallet Connected</label>
<code class="block bg-zinc-500 p-6 rounded bg-zinc-800 text-zinc-200">
<pre id="pre-wallet-address"> </pre>
</code>
</div>
<h2 id="wallet-network" class="text-xl text-zinc-200 mb-4">Wallet Network</h2>
<p class="text-zinc-400 mb-4">Here we'll make sure to switch networks to <span class="bg-zinc-700 text-zinc-200 py-1 px-1.5 rounded chain-name"></span>.</p>
<div class="mb-6">
<label class="block mb-2 text-zinc-600">Network</label>
<code class="block bg-zinc-500 p-6 rounded bg-zinc-800 text-zinc-200">
<pre id="pre-wallet-network"> </pre>
</code>
</div>
<div class="mb-6">
<button type="button" id="button-network"
class="hidden h-10 mb-2 bg-zinc-200 text-zinc-800 px-5 rounded-full font-medium disabled:bg-opacity-30 hover:bg-white transition-colors ease-in-out duration-200">
Switch Network To <span class="chain-name"></span>
</button>
</div>
<hr class="border-zinc-700 mb-8" />
<h2 id="contract-details" class="text-xl text-zinc-100 mb-4">Contract Details</h2>
<div id="div-error-network" class="mb-4 bg-red-300 rounded p-6 text-red-800">Please switch to <span class="chain-name"></span>. Currently on <span></span>.</div>
<p class="text-zinc-400 mb-4">These are all the interactions with a contract deployed to the <a class="text-blue-500 underline italic chain-link" href="#" target="_blank"></a>.</p>
<div class="mb-8">
<label class="block mb-2 text-zinc-600">ABI</label>
<code class="block bg-zinc-500 p-6 rounded bg-zinc-800 text-zinc-200 overflow-scroll max-h-[300px]">
<pre id="pre-wallet-network">
[
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "message",
"type": "string"
}
],
"name": "NewGreeting",
"type": "event"
},
{
"inputs": [],
"name": "getGreeting",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"name": "setGreeting",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
] </pre>
</code>
</div>
<h2 id="contract-read" class="text-xl text-zinc-100 mb-4">Contract Read</h2>
<p class="text-zinc-400 mb-4">Making a read request for <span class="bg-zinc-700 text-zinc-200 py-1 px-1.5 rounded">getGreeting</span>.</p>
<form id="form-contract-read">
<div class="mb-2">
<button
disabled
type="submit"
class="h-10 mb-2 bg-zinc-200 text-zinc-800 px-5 rounded-full font-medium disabled:bg-opacity-30 hover:bg-white transition-colors ease-in-out duration-200">
Get Greeting
</button>
</div>
<div class="mb-8">
<label class="block mb-2 text-zinc-600">Response</label>
<code class="block bg-zinc-500 p-6 rounded bg-zinc-800 text-zinc-200 overflow-scroll">
<pre id="pre-contract-read"></pre>
</code>
</div>
</form>
<h2 id="contract-write" class="text-xl text-zinc-100 mb-4">Contract Write</h2>
<p class="text-zinc-400 mb-4">Making a write request for <span class="bg-zinc-700 text-zinc-200 py-1 px-1.5 rounded">setGreeting</span>.</p>
<form id="form-contract-write">
<div class="mb-6">
<label class="block mb-2 text-zinc-600">New Greeting</label>
<input disabled class="h-10 w-full px-3 rounded border bg-white disabled:opacity-30" id="input-contract-write" type="text" name="greeting" placeholder="Ex: Your greeting..." />
</div>
<div class="mb-2">
<button
disabled
type="submit"
class="h-10 mb-2 bg-zinc-200 text-zinc-800 px-5 rounded-full font-medium disabled:bg-opacity-30 hover:bg-white transition-colors ease-in-out duration-200">
Set Greeting
</button>
</div>
<div class="mb-8">
<label class="block mb-2 text-zinc-600">Response</label>
<code class="block bg-zinc-500 p-6 rounded bg-zinc-800 text-zinc-200 overflow-scroll">
<pre id="pre-contract-write"></pre>
</code>
</div>
</form>
<hr class="border-zinc-700 mb-8" />
</section>
<p class="text-zinc-400 mb-4 text-sm">
Built By <a class="hover:text-zinc-200 transition-colors ease-in-out duration-200" target="_blank" href="https://linktr.ee/codingwithmanny">
@codingwithmanny
</a>
</p>
</div>
</body>
And we’ll setup our scripts.js
with the following code for now:
File: ./app/scripts.js
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
console.groupEnd();
};
Next, we’ll take advantage of live-server
to allow us to run a server with these files.
File: ./package.json
{
"name": "vanillajs-wallet-connection",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "./node_modules/.bin/live-server --port=3001 --watch=app --mount=/:./app",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"live-server": "^1.2.2"
}
}
Now if we run the http server, we should see the following:
# FROM ./vanillajs-wallet-connection
pnpm run dev;
# Expected Output:
# > vanillajs-wallet-connection@1.0.0 dev /path/to/vanillajs-wallet-connection
# > ./node_modules/.bin/live-server --port=3001 --watch=app --mount=/:./app
#
# Mapping / to "/path/to/vanillajs-wallet-connection/app"
# Serving "/path/to/vanillajs-wallet-connection" at http://127.0.0.1:3001
Browser Supported Functionality
The first check we’re going to do is check if the browser supports wallet interactions with window.ethereum
. If the browser is supported then we’ll enable the Connect Wallet button and remove the “(Unsupported)” text.
File: ./app/scripts.js
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
}
console.groupEnd();
};
We can now see what it looks like with a browser that is supported and unsupported.
Connect Wallet
Next we’ll add basic functionality to prompt the wallet for a request to connect with eth_requestAccounts
. You’ll notice that this request isn’t part of the Ethereum JSON-RPC API. This is because it’s a standard defined by MetaMask in their RPC API documentation to prompt for identification and connection of a wallet.
File: ./app/scripts.js
// Constants
// ========================================================
/**
* To keep track of which wallet is connected throughout our app
*/
let WALLET_CONNECTED = '';
// Functions
// ========================================================
/**
* When Connect Wallet button is clicked
*/
const connect = async () => {
console.group('connect');
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
WALLET_CONNECTED = accounts[0];
console.log({ WALLET_CONNECTED });
} catch (error) {
console.log({ error });
}
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
// Event Interactions
buttonConnect.addEventListener('click', connect);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
}
console.groupEnd();
};
You’ll notice when we get the prompt for the request when we click the Connect Wallet button, and we reject the request, we should see the error in the console logs.
Fortunately, we have an error element that we’re going to use to catch and display this error instead of it being in the console logs.
File: ./app/scripts.js
// Constants
// ========================================================
/**
* To keep track of which wallet is connected throughout our app
*/
let WALLET_CONNECTED = '';
// Functions
// ========================================================
/**
* When Connect Wallet button is clicked
*/
const connect = async () => {
console.group('connect');
// Reset our error element each time the button is clicked
const devErrorConnect = document.getElementById('div-error-connect');
devErrorConnect.innerHTML = '';
devErrorConnect.classList = devErrorConnect.classList.value.includes('hidden')
? devErrorConnect.classList.value
: `${devErrorConnect.classList.value} hidden`;
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
WALLET_CONNECTED = accounts[0];
} catch (error) {
console.log({ error });
// If error connecting, display the error message
devErrorConnect.innerHTML = error?.message ?? 'Unknown wallet connection error.'
devErrorConnect.classList = devErrorConnect.classList.value.replaceAll('hidden', '');
}
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
// Event Interactions
buttonConnect.addEventListener('click', connect);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
}
console.groupEnd();
};
When it’s connected, we also want to disable the Connect Wallet button, and show the other functionality that comes with being connected.
We will be using this functionality in multiple places, so we’ll create a sort of callback that executes once the connection is successful.
File: ./app/scripts.js
// Constants
// ========================================================
/**
* To keep track of which wallet is connected throughout our app
*/
let WALLET_CONNECTED = '';
// Functions
// ========================================================
/**
* When a wallet connection occurs
*/
const onWalletConnection = () => {
console.group('onWalletConnection');
// Disable connect button
const buttonConnect = document.getElementById('button-connect');
buttonConnect.setAttribute('disabled', true);
buttonConnect.innerHTML = 'Connected';
// Show connected section
const sectionConnected = document.getElementById('section-connected');
sectionConnected.classList = '';
// Set the wallet address to show the user
const preWalletAddress = document.getElementById('pre-wallet-address');
preWalletAddress.innerHTML = WALLET_CONNECTED;
console.groupEnd();
};
/**
* When Connect Wallet button is clicked
*/
const connect = async () => {
console.group('connect');
// Reset our error element each time the button is clicked
const devErrorConnect = document.getElementById('div-error-connect');
devErrorConnect.innerHTML = '';
devErrorConnect.classList = devErrorConnect.classList.value.includes('hidden')
? devErrorConnect.classList.value
: `${devErrorConnect.classList.value} hidden`;
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
WALLET_CONNECTED = accounts[0];
onWalletConnection();
} catch (error) {
console.log({ error });
// If error connecting, display the error message
devErrorConnect.innerHTML = error?.message ?? 'Unknown wallet connection error.'
devErrorConnect.classList = devErrorConnect.classList.value.replaceAll('hidden', '');
}
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
// Event Interactions
buttonConnect.addEventListener('click', connect);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
}
console.groupEnd();
};
Disconnect Wallet
The next step is to add a disconnect interaction. It should be noted that the disconnect functionality is purely for looks because true disconnection is made in the wallet, when the URL is removed from the Connected Sites. We’ll add this functionality as well.
Superficial Disconnect
The we’ll add some functionality for the superficial disconnect but make use a callback to handle disconnects in general to use for our true disconnect with Connected Sites.
File: ./app/scripts.js
// Constants
// ========================================================
/**
* To keep track of which wallet is connected throughout our app
*/
let WALLET_CONNECTED = '';
// Functions
// ========================================================
/**
* When wallet disconnect occurs
*/
const onWalletDisconnect = () => {
console.group('onWalletDisconnect');
// Hide connected section
const sectionConnected = document.getElementById('section-connected');
sectionConnected.classList = 'hidden';
// Enabled connect button
const buttonConnect = document.getElementById('button-connect');
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = 'Connect Wallet';
console.groupEnd();
};
// ...
/**
* When Disconnect button is clicked
*/
const disconnect = () => {
console.group('disconnect');
WALLET_CONNECTED = false;
onWalletDisconnect();
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
}
console.groupEnd();
};
Fully Disconnected
Now let’s account for when a wallet is fully disconnected with the Connected Site is removed.
In order to listen when a site has been removed, we need to take advantage of a events, which are like hooks or callbacks when the account has changed. This is built into the window.ethereum
functionality injected by the wallet. To see the full list of events, check out MetaMask Ethereum Provider Events.
We’ll be taking advantage of the even accountsChanged
and creating a new function called onAccountsChanged
.
File: ./app/scripts.js
// Constants
// ========================================================
/**
* To keep track of which wallet is connected throughout our app
*/
let WALLET_CONNECTED = '';
// Functions
// ========================================================
/**
* When wallet connects or disconnects
* @param {*} accounts Array of accounts that have changed - typicall array of one
*/
const onAccountsChanged = (accounts) => {
console.group('onAccountsChanged');
console.log({ accounts });
// No accounts found - use onWalletDisconnect to update UI
if (accounts.length === 0) {
onWalletDisconnect();
} else {
// Accounts found - use callback for onWalletConnection to update UI
WALLET_CONNECTED = accounts?.[0];
onWalletConnection();
}
console.groupEnd();
};
// ...
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
// Events
window.ethereum.on('accountsChanged', onAccountsChanged);
}
console.groupEnd();
};
If disconnect from the site directly now from the Connected Sites in our wallet, we can see that the UI has updated to reflect that the account has changed.
Automatic Wallet Connection
This next functionality will automatically connect a wallet if the wallet has the site URL on its list of Connected Sites.
We’re going to do this in a slightly different way, because we could just call eth_requestAccounts
, which would prompt the user to connect if they haven’t connected before, but if we’re a first time user this is a bad UX in my opinion. Forcing a user to connect before seeing anything in general is a frustrating experience.
Instead, we’re going to rely on another request that let’s us see if we have any permissions to the wallet first, and if there are any then we can assume the wallet is already connected. For this we’ll be using wallet_getPermissions
, you can see more from MetaMask’s Wallet Permissions Docs.
First, let’s see what the output looks like with a console log.
File: ./app/scripts.js
// ...
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
// Events
window.ethereum.on('accountsChanged', onAccountsChanged);
// Check if already connected with the number of permissions we have
const hasWalletPermissions = await window.ethereum.request({ method: 'wallet_getPermissions' });
console.log({ hasWalletPermissions });
}
console.groupEnd();
};
With this, we can now decipher that if the array length is greater than zero then we can call our connect
function to update the UI as necessary. The reason why connect
and not onWalletConnection
is because we still want to be able to retrieve the account data to set it for the entire application and then call onWalletConnection
.
File: ./app/scripts.js
// ...
/**
* When a wallet connection occurs
*/
const onWalletConnection = () => {
console.group('onWalletConnection');
// Disable connect button
const buttonConnect = document.getElementById('button-connect');
buttonConnect.setAttribute('disabled', true);
buttonConnect.innerHTML = 'Connected';
// Show connected section
const sectionConnected = document.getElementById('section-connected');
sectionConnected.classList = '';
// Set the wallet address to show the user
const preWalletAddress = document.getElementById('pre-wallet-address');
preWalletAddress.innerHTML = WALLET_CONNECTED;
console.groupEnd();
};
/**
* When Connect Button is clicked
*/
const connect = async () => {
console.group('connect');
// Reset our error element each time the button is clicked
const devErrorConnect = document.getElementById('div-error-connect');
devErrorConnect.innerHTML = '';
devErrorConnect.classList = devErrorConnect.classList.value.includes('hidden')
? devErrorConnect.classList.value
: `${devErrorConnect.classList.value} hidden`;
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
WALLET_CONNECTED = accounts[0];
onWalletConnection();
} catch (error) {
console.log({ error });
// If error connecting, display the error message
devErrorConnect.innerHTML = error?.message ?? 'Unknown wallet connection error.'
devErrorConnect.classList = devErrorConnect.classList.value.replaceAll('hidden', '');
}
console.groupEnd();
};
// ...
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
// Events
window.ethereum.on('accountsChanged', onAccountsChanged);
// Check if already connected with the number of permissions we have
const hasWalletPermissions = await window.ethereum.request({ method: 'wallet_getPermissions' });
console.log({ hasWalletPermissions });
// If wallet has permissions update the site UI
if (hasWalletPermissions.length > 0) {
connect();
}
}
console.groupEnd();
};
Now if the wallet is already connected and we refresh the page, we’ll see that we’re already connected.
Saving Our Preferences
There is one issue though, that if we press the Disconnect button and refresh the page, we’re automatically connected. In order to fix this we’re going to need to save our preference. We can take advantage of localStorage
to save our preference and load the state of the UI based on that preference. You’ll note that we also created a WALLET_CONNECTION_PREF_KEY
to better handle the key for localStorage
.
We also updated the connect
and disconnect
functionality to change the localStorage
value.
File: ./app/scripts.js
// Constants
// ========================================================
/**
* To keep track of which wallet is connected throughout our app
*/
let WALLET_CONNECTED = '';
/**
* localStorage key
*/
let WALLET_CONNECTION_PREF_KEY = 'WC_PREF';
// ...
/**
* When Connect Button is clicked
*/
const connect = async () => {
console.group('connect');
// Reset our error element each time the button is clicked
const devErrorConnect = document.getElementById('div-error-connect');
devErrorConnect.innerHTML = '';
devErrorConnect.classList = devErrorConnect.classList.value.includes('hidden')
? devErrorConnect.classList.value
: `${devErrorConnect.classList.value} hidden`;
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
WALLET_CONNECTED = accounts[0];
// Update wallet connection preference to true
localStorage.setItem(WALLET_CONNECTION_PREF_KEY, true);
onWalletConnection();
} catch (error) {
console.log({ error });
// If error connecting, display the error message
devErrorConnect.innerHTML = error?.message ?? 'Unknown wallet connection error.'
devErrorConnect.classList = devErrorConnect.classList.value.replaceAll('hidden', '');
}
console.groupEnd();
};
/**
* When Disconnect button is clicked
*/
const disconnect = () => {
console.group('disconnect');
WALLET_CONNECTED = false;
// Remove wallet connection preference
localStorage.removeItem(WALLET_CONNECTION_PREF_KEY);
onWalletDisconnect();
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
// Events
window.ethereum.on('accountsChanged', onAccountsChanged);
// Check if already connected with the number of permissions we have
const hasWalletPermissions = await window.ethereum.request({ method: 'wallet_getPermissions' });
console.log({ hasWalletPermissions });
// Retrieve wallet connection preference from localStorage
const shouldBeConnected = JSON.parse(localStorage.getItem(WALLET_CONNECTION_PREF_KEY)) || false;
console.log({ shouldBeConnected });
// If wallet has permissions update the site UI
if (hasWalletPermissions.length > 0 && shouldBeConnected) {
connect();
}
}
console.groupEnd();
};
Here’s our full code so far with the wallet connections complete.
File: ./app/scripts.js
// Constants
// ========================================================
/**
* To keep track of which wallet is connected throughout our app
*/
let WALLET_CONNECTED = '';
/**
* localStorage key
*/
let WALLET_CONNECTION_PREF_KEY = 'WC_PREF';
// Functions
// ========================================================
/**
* When wallet connects or disconnects
* @param {*} accounts Array of accounts that have changed - typicall array of one
*/
const onAccountsChanged = (accounts) => {
console.group('onAccountsChanged');
console.log({ accounts });
// No accounts found - use onWalletDisconnect to update UI
if (accounts.length === 0) {
onWalletDisconnect();
} else {
// Accounts found - use callback for onWalletConnection to update UI
WALLET_CONNECTED = accounts?.[0];
onWalletConnection();
}
console.groupEnd();
};
/**
* When wallet disconnect occurs
*/
const onWalletDisconnect = () => {
console.group('onWalletDisconnect');
// Hide connected section
const sectionConnected = document.getElementById('section-connected');
sectionConnected.classList = 'hidden';
// Enabled connect button
const buttonConnect = document.getElementById('button-connect');
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = 'Connect Wallet';
console.groupEnd();
};
/**
* When a wallet connection occurs
*/
const onWalletConnection = () => {
console.group('onWalletConnection');
// Disable connect button
const buttonConnect = document.getElementById('button-connect');
buttonConnect.setAttribute('disabled', true);
buttonConnect.innerHTML = 'Connected';
// Show connected section
const sectionConnected = document.getElementById('section-connected');
sectionConnected.classList = '';
// Set the wallet address to show the user
const preWalletAddress = document.getElementById('pre-wallet-address');
preWalletAddress.innerHTML = WALLET_CONNECTED;
console.groupEnd();
};
/**
* When Connect Button is clicked
*/
const connect = async () => {
console.group('connect');
// Reset our error element each time the button is clicked
const devErrorConnect = document.getElementById('div-error-connect');
devErrorConnect.innerHTML = '';
devErrorConnect.classList = devErrorConnect.classList.value.includes('hidden')
? devErrorConnect.classList.value
: `${devErrorConnect.classList.value} hidden`;
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
WALLET_CONNECTED = accounts[0];
// Update wallet connection preference to true
localStorage.setItem(WALLET_CONNECTION_PREF_KEY, true);
onWalletConnection();
} catch (error) {
console.log({ error });
// If error connecting, display the error message
devErrorConnect.innerHTML = error?.message ?? 'Unknown wallet connection error.'
devErrorConnect.classList = devErrorConnect.classList.value.replaceAll('hidden', '');
}
console.groupEnd();
};
/**
* When Disconnect button is clicked
*/
const disconnect = () => {
console.group('disconnect');
WALLET_CONNECTED = false;
// Remove wallet connection preference
localStorage.removeItem(WALLET_CONNECTION_PREF_KEY);
onWalletDisconnect();
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
// Events
window.ethereum.on('accountsChanged', onAccountsChanged);
// Check if already connected with the number of permissions we have
const hasWalletPermissions = await window.ethereum.request({ method: 'wallet_getPermissions' });
console.log({ hasWalletPermissions });
// Retrieve wallet connection preference from localStorage
const shouldBeConnected = JSON.parse(localStorage.getItem(WALLET_CONNECTION_PREF_KEY)) || false;
console.log({ shouldBeConnected });
// If wallet has permissions update the site UI
if (hasWalletPermissions.length > 0 && shouldBeConnected) {
connect();
}
}
console.groupEnd();
};
Wallet Network
This next part is all about network connections, and network switching. For the first part, we’re going to add the functionality to check which network we’re currently connected to.
To do this we’re to first request the current chain id and then store it within out application as a local variable. We’re going to take advantage of the method eth_chainId
to get the chain details.
We’ll also add a list of dictionaries for different networks at the top, and update our connect
and disconnect
functions.
File: ./app/scripts.js
// Constants
// ========================================================
/**
* To keep track of which wallet is connected throughout our app
*/
let WALLET_CONNECTED = '';
/**
* localStorage key
*/
let WALLET_CONNECTION_PREF_KEY = 'WC_PREF';
/**
* Current chain connected with chain id and name as a object { id: 1, name: "Ethereum Mainnet" }
*/
const CHAIN_CONNECTED = {
id: null,
name: null
};
/**
* Chain ids and their names
*/
const CHAIN_DICTIONARY = {
1: 'Ethereum Mainnet',
5: 'Goerli Testnet',
137: 'Polygon Mainnet',
1337: 'Localhost',
1402: 'zkEVM Testnet',
80001: 'Mumbai Testnet',
11155111: 'Sepolia Testnet'
};
// ...
/**
* When Connect Button is clicked
*/
const connect = async () => {
console.group('connect');
// Reset our error element each time the button is clicked
const devErrorConnect = document.getElementById('div-error-connect');
devErrorConnect.innerHTML = '';
devErrorConnect.classList = devErrorConnect.classList.value.includes('hidden')
? devErrorConnect.classList.value
: `${devErrorConnect.classList.value} hidden`;
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
WALLET_CONNECTED = accounts[0];
// Update chain connected
const chainId = await ethereum.request({ method: 'eth_chainId' });
const parsedChainId = parseInt(`${chainId}`, 16);
CHAIN_CONNECTED.name = CHAIN_DICTIONARY?.[parsedChainId];
CHAIN_CONNECTED.id = parsedChainId;
console.log({ chainId });
console.log({ CHAIN_CONNECTED });
// Update wallet connection preference to true
localStorage.setItem(WALLET_CONNECTION_PREF_KEY, true);
onWalletConnection();
} catch (error) {
console.log({ error });
// If error connecting, display the error message
devErrorConnect.innerHTML = error?.message ?? 'Unknown wallet connection error.'
devErrorConnect.classList = devErrorConnect.classList.value.replaceAll('hidden', '');
}
console.groupEnd();
};
/**
* When Disconnect button is clicked
*/
const disconnect = () => {
console.group('disconnect');
WALLET_CONNECTED = false;
CHAIN_CONNECTED.name = null;
CHAIN_CONNECTED.id = null;
console.log({ CHAIN_CONNECTED });
// Remove wallet connection preference
localStorage.removeItem(WALLET_CONNECTION_PREF_KEY);
onWalletDisconnect();
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
// Events
window.ethereum.on('accountsChanged', onAccountsChanged);
// Check if already connected with the number of permissions we have
const hasWalletPermissions = await window.ethereum.request({ method: 'wallet_getPermissions' });
console.log({ hasWalletPermissions });
// Retrieve wallet connection preference from localStorage
const shouldBeConnected = JSON.parse(localStorage.getItem(WALLET_CONNECTION_PREF_KEY)) || false;
console.log({ shouldBeConnected });
// If wallet has permissions update the site UI
if (hasWalletPermissions.length > 0 && shouldBeConnected) {
connect();
}
}
console.groupEnd();
};
Now when we connect and disconnect our wallet we should see the outcomes of our current connected chain.
Next we want to add when the browser is refreshed and the user’s preference is to be connected, that we also update our CHAIN_CONNECTED
variable. We’ll also add functionality to display the chain in our UI in the onWalletConnection
callback function. The issue you’ll find is that we need to refactor the chain connection to be its own function to account for the following interactions.
- Connect
- Disconnect
- Account Changed
- Automatically Connect Based On Preference
The new function will look like:
File: ./app/scripts.js
// ...
// Functions
// ========================================================
/**
* When the chainId has changed
* @param {string|null} chainId
*/
const onChainChanged = (chainId) => {
console.group('onChainChanged');
console.log({ chainId });
// Get the UI element that displays the wallet network
const preWalletNetwork = document.getElementById('pre-wallet-network');
if (!chainId) {
CHAIN_CONNECTED.name = null;
CHAIN_CONNECTED.id = null;
// Set the network to blank
preWalletNetwork.innerHTML = ``;
} else {
const parsedChainId = parseInt(`${chainId}`, 16);
CHAIN_CONNECTED.name = CHAIN_DICTIONARY?.[parsedChainId];
CHAIN_CONNECTED.id = parsedChainId;
// Set the network to show the current connected network
preWalletNetwork.innerHTML = `${CHAIN_CONNECTED?.id} / ${CHAIN_CONNECTED?.name}`;
}
console.log({ CHAIN_CONNECTED });
console.groupEnd();
};
// ...
Our connect
function will changed to this:
File: ./app/scripts.js
// ...
// Functions
// ========================================================
// ...
/**
* When Connect Button is clicked
*/
const connect = async () => {
console.group('connect');
// Reset our error element each time the button is clicked
const devErrorConnect = document.getElementById('div-error-connect');
devErrorConnect.innerHTML = '';
devErrorConnect.classList = devErrorConnect.classList.value.includes('hidden')
? devErrorConnect.classList.value
: `${devErrorConnect.classList.value} hidden`;
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
WALLET_CONNECTED = accounts[0];
// Update chain connected
const chainId = await ethereum.request({ method: 'eth_chainId' });
onChainChanged(chainId);
// Update wallet connection preference to true
localStorage.setItem(WALLET_CONNECTION_PREF_KEY, true);
onWalletConnection();
} catch (error) {
console.log({ error });
// If error connecting, display the error message
devErrorConnect.innerHTML = error?.message ?? 'Unknown wallet connection error.'
devErrorConnect.classList = devErrorConnect.classList.value.replaceAll('hidden', '');
}
console.groupEnd();
};
// ..
Our disconnect
function will changed to this:
File: ./app/scripts.js
// ...
// Functions
// ========================================================
// ...
/**
* When Disconnect button is clicked
*/
const disconnect = () => {
console.group('disconnect');
WALLET_CONNECTED = false;
onChainChanged(null);
// Remove wallet connection preference
localStorage.removeItem(WALLET_CONNECTION_PREF_KEY);
onWalletDisconnect();
console.groupEnd();
};
// ...
To account for the accountsChanged
event, we’ll update our onAccountsChanged
function to the following:
File: ./app/scripts.js
// ...
// Functions
// ========================================================
// ..
/**
* When wallet connects or disconnects with accountsChanged event
* @param {*} accounts Array of accounts that have changed - typicall array of one
*/
const onAccountsChanged = async (accounts) => {
console.group('onAccountsChanged');
console.log({ accounts });
// No accounts found - use onWalletDisconnect to update UI
if (accounts.length === 0) {
onChainChanged(null);
onWalletDisconnect();
} else {
// Accounts found - use callback for onWalletConnection to update UI
WALLET_CONNECTED = accounts?.[0];
// Update chain connected
const chainId = await ethereum.request({ method: 'eth_chainId' });
onChainChanged(chainId);
onWalletConnection();
}
console.groupEnd();
};
// ...
And lastly we’ll update our automatic connection in our window.onload
to the following:
File: ./app/scripts.js
// ...
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// ...
if (typeof window?.ethereum !== "undefined") {
// Retrieve wallet connection preference from localStorage
const shouldBeConnected = JSON.parse(localStorage.getItem(WALLET_CONNECTION_PREF_KEY)) || false;
console.log({ shouldBeConnected });
// If wallet has permissions update the site UI
if (hasWalletPermissions.length > 0 && shouldBeConnected) {
// Retrieve chain
const chainId = await ethereum.request({ method: 'eth_chainId' });
onChainChanged(chainId);
connect();
}
}
console.groupEnd();
};
Switching Networks
Now that we know what network we’re currently on, let’s add the functionality where a user might switch the wallets current network, and we want our UI to reflect those updated changes.
Because we created a callback function for onChainChanged
we can use it for another event function from MetaMask called chainChanged
.
File: ./app/scripts.js
// ...
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
// ...
// Events
window.ethereum.on('accountsChanged', onAccountsChanged);
window.ethereum.on('chainChanged', onChainChanged);
// ...
console.groupEnd();
};
Now when we switch networks, we can see that the network details have changed with it.
Here’s the full code so far:
File: ./app/scripts.js
// Constants
// ========================================================
/**
* To keep track of which wallet is connected throughout our app
*/
let WALLET_CONNECTED = '';
/**
* localStorage key
*/
let WALLET_CONNECTION_PREF_KEY = 'WC_PREF';
/**
* Current chain connected with chain id and name as a object { id: 1, name: "Ethereum Mainnet" }
*/
const CHAIN_CONNECTED = {
id: null,
name: null
};
/**
* Chain ids and their names
*/
const CHAIN_DICTIONARY = {
1: 'Ethereum Mainnet',
5: 'Goerli Testnet',
137: 'Polygon Mainnet',
1337: 'Localhost',
1402: 'zkEVM Testnet',
80001: 'Mumbai Testnet',
11155111: 'Sepolia Testnet'
};
// Functions
// ========================================================
/**
* When the chainId has changed
* @param {string|null} chainId
*/
const onChainChanged = (chainId) => {
console.group('onChainChanged');
console.log({ chainId });
// Get the UI element that displays the wallet network
const preWalletNetwork = document.getElementById('pre-wallet-network');
if (!chainId) {
CHAIN_CONNECTED.name = null;
CHAIN_CONNECTED.id = null;
// Set the network to blank
preWalletNetwork.innerHTML = ``;
} else {
const parsedChainId = parseInt(`${chainId}`, 16);
CHAIN_CONNECTED.name = CHAIN_DICTIONARY?.[parsedChainId];
CHAIN_CONNECTED.id = parsedChainId;
// Set the network to show the current connected network
preWalletNetwork.innerHTML = `${CHAIN_CONNECTED?.id} / ${CHAIN_CONNECTED?.name}`;
}
console.log({ CHAIN_CONNECTED });
console.groupEnd();
};
/**
* When wallet connects or disconnects with accountsChanged event
* @param {*} accounts Array of accounts that have changed - typicall array of one
*/
const onAccountsChanged = async (accounts) => {
console.group('onAccountsChanged');
console.log({ accounts });
// No accounts found - use onWalletDisconnect to update UI
if (accounts.length === 0) {
onChainChanged(null);
onWalletDisconnect();
} else {
// Accounts found - use callback for onWalletConnection to update UI
WALLET_CONNECTED = accounts?.[0];
// Update chain connected
const chainId = await ethereum.request({ method: 'eth_chainId' });
onChainChanged(chainId);
onWalletConnection();
}
console.groupEnd();
};
/**
* When wallet disconnect occurs
*/
const onWalletDisconnect = () => {
console.group('onWalletDisconnect');
// Hide connected section
const sectionConnected = document.getElementById('section-connected');
sectionConnected.classList = 'hidden';
// Enabled connect button
const buttonConnect = document.getElementById('button-connect');
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = 'Connect Wallet';
console.groupEnd();
};
/**
* When a wallet connection occurs
*/
const onWalletConnection = () => {
console.group('onWalletConnection');
// Disable connect button
const buttonConnect = document.getElementById('button-connect');
buttonConnect.setAttribute('disabled', true);
buttonConnect.innerHTML = 'Connected';
// Show connected section
const sectionConnected = document.getElementById('section-connected');
sectionConnected.classList = '';
// Set the wallet address to show the user
const preWalletAddress = document.getElementById('pre-wallet-address');
preWalletAddress.innerHTML = WALLET_CONNECTED;
console.groupEnd();
};
/**
* When Connect Button is clicked
*/
const connect = async () => {
console.group('connect');
// Reset our error element each time the button is clicked
const devErrorConnect = document.getElementById('div-error-connect');
devErrorConnect.innerHTML = '';
devErrorConnect.classList = devErrorConnect.classList.value.includes('hidden')
? devErrorConnect.classList.value
: `${devErrorConnect.classList.value} hidden`;
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
WALLET_CONNECTED = accounts[0];
// Update chain connected
const chainId = await ethereum.request({ method: 'eth_chainId' });
onChainChanged(chainId);
// Update wallet connection preference to true
localStorage.setItem(WALLET_CONNECTION_PREF_KEY, true);
onWalletConnection();
} catch (error) {
console.log({ error });
// If error connecting, display the error message
devErrorConnect.innerHTML = error?.message ?? 'Unknown wallet connection error.'
devErrorConnect.classList = devErrorConnect.classList.value.replaceAll('hidden', '');
}
console.groupEnd();
};
/**
* When Disconnect button is clicked
*/
const disconnect = () => {
console.group('disconnect');
WALLET_CONNECTED = false;
onChainChanged(null);
// Remove wallet connection preference
localStorage.removeItem(WALLET_CONNECTION_PREF_KEY);
onWalletDisconnect();
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
// Events
window.ethereum.on('accountsChanged', onAccountsChanged);
window.ethereum.on('chainChanged', onChainChanged);
// Check if already connected with the number of permissions we have
const hasWalletPermissions = await window.ethereum.request({ method: 'wallet_getPermissions' });
console.log({ hasWalletPermissions });
// Retrieve wallet connection preference from localStorage
const shouldBeConnected = JSON.parse(localStorage.getItem(WALLET_CONNECTION_PREF_KEY)) || false;
console.log({ shouldBeConnected });
// If wallet has permissions update the site UI
if (hasWalletPermissions.length > 0 && shouldBeConnected) {
// Retrieve chain
const chainId = await ethereum.request({ method: 'eth_chainId' });
onChainChanged(chainId);
connect();
}
}
console.groupEnd();
};
Prompt To Switch Network
For our code, we want users to be able to use Mumbai Testnet, and enforce that they use this network going forward to interact with our deployed Greeter contract to Mumbai Testnet.
To start we’re going to add constant for the chain we want to identity as the one we want users to use and also add a few more constants.
// Constants
// ========================================================
// ...
/**
* Required chain to interact with contract
*/
const CHAIN_ID_REQUIRED = 80001; //Mumbai
/**
* Same contract deployed to each network
*/
const CONTRACT_ON_CHAINS = {
1: '0x76460E73eadE1DDe315E07a5eCa092448c193a2F',
5: '0x3aC587078b344a3d27e56632dFf236F1Aff04D56',
137: '0x375F01b156D9BdDDd41fd38c5CC74C514CB71f73',
1337: '',
1402: '0x76460E73eadE1DDe315E07a5eCa092448c193a2F',
80001: '0x7Bd54062eFa363A97dC20f404825597455E93582',
11155111: '0x375f01b156d9bdddd41fd38c5cc74c514cb71f73',
};
/**
* All blockchain explorers
*/
const BLOCKCHAIN_EXPLORERS = {
1: 'https://etherscan.io',
5: 'https://goerli.etherscan.io',
137: 'https://polygonscan.com',
1337: null,
1402: 'https://explorer.public.zkevm-test.net',
80001: 'https://mumbai.polygonscan.com',
11155111: 'https://sepolia.etherscan.io',
};
// Functions
// ========================================================
// ...
We’ll also add some functionality to show that CHAIN_ID_REQUIRED
wherever .chain-name
or .chain-link
is set as a class.
File: ./app/scripts.js
// ...
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// Replace elements with required chain name
const chainNameReplace = document.querySelectorAll('.chain-name');
chainNameReplace.forEach(el => {
el.innerHTML = `${CHAIN_DICTIONARY[CHAIN_ID_REQUIRED]}`
});
// Replace elements with required chain name and link
const chainLinkReplace = document.querySelectorAll('.chain-link');
chainLinkReplace.forEach(el => {
el.innerHTML = `${CHAIN_DICTIONARY[CHAIN_ID_REQUIRED]}`
el.setAttribute('href', `${BLOCKCHAIN_EXPLORERS[CHAIN_ID_REQUIRED]}/address/${CONTRACT_ON_CHAINS[CHAIN_ID_REQUIRED]}`)
});
// ...
console.groupEnd();
};
Now we’ll add functionality that when the wallet is connected and it’s not set to the desired CHAIN_ID_REQUIRED
, it will show the correct error messages.
File: ./app/scripts.js
// ...
// Functions
// ========================================================
/**
* When the chainId has changed
* @param {string|null} chainId
*/
const onChainChanged = (chainId) => {
console.group('onChainChanged');
console.log({ chainId });
// Get the UI element that displays the wallet network
const preWalletNetwork = document.getElementById('pre-wallet-network');
if (!chainId) {
CHAIN_CONNECTED.name = null;
CHAIN_CONNECTED.id = null;
// Set the network to blank
preWalletNetwork.innerHTML = ``;
} else {
const parsedChainId = parseInt(`${chainId}`, 16);
CHAIN_CONNECTED.name = CHAIN_DICTIONARY?.[parsedChainId];
CHAIN_CONNECTED.id = parsedChainId;
const buttonNetwork = document.getElementById('button-network');
const divErrorNetwork = document.getElementById('div-error-network');
const formContractReadButton = document.querySelector('#form-contract-read button');
const formContractWriteInput = document.querySelector('#form-contract-write input');
const formContractWriteButton = document.querySelector('#form-contract-write button');
if (parsedChainId !== CHAIN_ID_REQUIRED) {
// Show error elements
buttonNetwork.classList = `${buttonNetwork.classList.value.replaceAll('hidden', '')}`;
divErrorNetwork.classList = `${divErrorNetwork.classList.value.replaceAll('hidden', '')}`;
divErrorNetwork.children[1].innerHTML = `${CHAIN_CONNECTED.name}`;
// Disable forms
formContractReadButton.setAttribute('disabled', true);
formContractWriteInput.setAttribute('disabled', true);
formContractWriteButton.setAttribute('disabled', true);
} else {
// Hide error elements
buttonNetwork.classList = `${buttonNetwork.classList.value} hidden`;
divErrorNetwork.classList = `${divErrorNetwork.classList.value} hidden`;
divErrorNetwork.children[1].innerHTML = '';
// Enable forms
formContractReadButton.removeAttribute('disabled');
formContractWriteInput.removeAttribute('disabled');
formContractWriteButton.removeAttribute('disabled');
}
// Set the network to show the current connected network
preWalletNetwork.innerHTML = `${CHAIN_CONNECTED?.id} / ${CHAIN_CONNECTED?.name}`;
}
console.log({ CHAIN_CONNECTED });
console.groupEnd();
};
// ...
This is what it looks like when we’re not connected to the right network.
This is what it looks like when we’re connected to the right network.
Lastly, we want to use the Switch Network button to prompt the wallet to switch networks to the correct network. In order to switch networks, we’re going to take advantage of the wallet_switchEthereumChain
provided by MetaMask.
File: ./app/scripts.js
// ...
// Functions
// ========================================================
// ...
/**
* Switches network to Mumbai Testnet or CHAIN_ID_REQUIRED
*/
const switchNetwork = async () => {
console.group('switchNetwork');
console.log({ CHAIN_ID_REQUIRED: CHAIN_ID_REQUIRED.toString(16) });
try {
await window.ethereum.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: `0x${CHAIN_ID_REQUIRED.toString(16)}` }], })
} catch (error) {
console.log({ error });
}
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// ...
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
const buttonNetwork = document.getElementById('button-network');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
buttonNetwork.addEventListener('click', switchNetwork);
// ...
console.groupEnd();
};
With the network button interaction set up, we can now see that our button prompts the wallet to change the network to Mumbai Testnet.
Additionally we’ll add functionality in our onChainChanged function to enable our form buttons and inputs. Here is the full code so far with the form functionality:
File: ./app/scripts.js
// Constants
// ========================================================
/**
* To keep track of which wallet is connected throughout our app
*/
let WALLET_CONNECTED = '';
/**
* localStorage key
*/
let WALLET_CONNECTION_PREF_KEY = 'WC_PREF';
/**
* Current chain connected with chain id and name as a object { id: 1, name: "Ethereum Mainnet" }
*/
const CHAIN_CONNECTED = {
id: null,
name: null
};
/**
* Chain ids and their names
*/
const CHAIN_DICTIONARY = {
1: 'Ethereum Mainnet',
5: 'Goerli Testnet',
137: 'Polygon Mainnet',
1337: 'Localhost',
1402: 'zkEVM Testnet',
80001: 'Mumbai Testnet',
11155111: 'Sepolia Testnet'
};
/**
* Required chain to interact with contract
*/
const CHAIN_ID_REQUIRED = 80001; //Mumbai
/**
* Same contract deployed to each network
*/
const CONTRACT_ON_CHAINS = {
1: '0x76460E73eadE1DDe315E07a5eCa092448c193a2F',
5: '0x3aC587078b344a3d27e56632dFf236F1Aff04D56',
137: '0x375F01b156D9BdDDd41fd38c5CC74C514CB71f73',
1337: '',
1402: '0x76460E73eadE1DDe315E07a5eCa092448c193a2F',
80001: '0x7Bd54062eFa363A97dC20f404825597455E93582',
11155111: '0x375f01b156d9bdddd41fd38c5cc74c514cb71f73',
};
/**
* All blockchain explorers
*/
const BLOCKCHAIN_EXPLORERS = {
1: 'https://etherscan.io',
5: 'https://goerli.etherscan.io',
137: 'https://polygonscan.com',
1337: null,
1402: 'https://explorer.public.zkevm-test.net',
80001: 'https://mumbai.polygonscan.com',
11155111: 'https://sepolia.etherscan.io',
};
// Functions
// ========================================================
/**
* When the chainId has changed
* @param {string|null} chainId
*/
const onChainChanged = (chainId) => {
console.group('onChainChanged');
console.log({ chainId });
// Get the UI element that displays the wallet network
const preWalletNetwork = document.getElementById('pre-wallet-network');
if (!chainId) {
CHAIN_CONNECTED.name = null;
CHAIN_CONNECTED.id = null;
// Set the network to blank
preWalletNetwork.innerHTML = ``;
} else {
const parsedChainId = parseInt(`${chainId}`, 16);
CHAIN_CONNECTED.name = CHAIN_DICTIONARY?.[parsedChainId];
CHAIN_CONNECTED.id = parsedChainId;
const buttonNetwork = document.getElementById('button-network');
const divErrorNetwork = document.getElementById('div-error-network');
const formContractReadButton = document.querySelector('#form-contract-read button');
const formContractWriteInput = document.querySelector('#form-contract-write input');
const formContractWriteButton = document.querySelector('#form-contract-write button');
if (parsedChainId !== CHAIN_ID_REQUIRED) {
// Show error elements
buttonNetwork.classList = `${buttonNetwork.classList.value.replaceAll('hidden', '')}`;
divErrorNetwork.classList = `${divErrorNetwork.classList.value.replaceAll('hidden', '')}`;
divErrorNetwork.children[1].innerHTML = `${CHAIN_CONNECTED.name}`;
// Disable forms
formContractReadButton.setAttribute('disabled', true);
formContractWriteInput.setAttribute('disabled', true);
formContractWriteButton.setAttribute('disabled', true);
} else {
// Hide error elements
buttonNetwork.classList = `${buttonNetwork.classList.value} hidden`;
divErrorNetwork.classList = `${divErrorNetwork.classList.value} hidden`;
divErrorNetwork.children[1].innerHTML = '';
// Enable forms
formContractReadButton.removeAttribute('disabled');
formContractWriteInput.removeAttribute('disabled');
formContractWriteButton.removeAttribute('disabled');
}
// Set the network to show the current connected network
preWalletNetwork.innerHTML = `${CHAIN_CONNECTED?.id} / ${CHAIN_CONNECTED?.name}`;
}
console.log({ CHAIN_CONNECTED });
console.groupEnd();
};
/**
* When wallet connects or disconnects with accountsChanged event
* @param {*} accounts Array of accounts that have changed - typicall array of one
*/
const onAccountsChanged = async (accounts) => {
console.group('onAccountsChanged');
console.log({ accounts });
// No accounts found - use onWalletDisconnect to update UI
if (accounts.length === 0) {
onChainChanged(null);
onWalletDisconnect();
} else {
// Accounts found - use callback for onWalletConnection to update UI
WALLET_CONNECTED = accounts?.[0];
// Update chain connected
const chainId = await ethereum.request({ method: 'eth_chainId' });
onChainChanged(chainId);
onWalletConnection();
}
console.groupEnd();
};
/**
* When wallet disconnect occurs
*/
const onWalletDisconnect = () => {
console.group('onWalletDisconnect');
// Hide connected section
const sectionConnected = document.getElementById('section-connected');
sectionConnected.classList = 'hidden';
// Enabled connect button
const buttonConnect = document.getElementById('button-connect');
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = 'Connect Wallet';
console.groupEnd();
};
/**
* When a wallet connection occurs
*/
const onWalletConnection = () => {
console.group('onWalletConnection');
// Disable connect button
const buttonConnect = document.getElementById('button-connect');
buttonConnect.setAttribute('disabled', true);
buttonConnect.innerHTML = 'Connected';
// Show connected section
const sectionConnected = document.getElementById('section-connected');
sectionConnected.classList = '';
// Set the wallet address to show the user
const preWalletAddress = document.getElementById('pre-wallet-address');
preWalletAddress.innerHTML = WALLET_CONNECTED;
console.groupEnd();
};
/**
* When Connect Button is clicked
*/
const connect = async () => {
console.group('connect');
// Reset our error element each time the button is clicked
const devErrorConnect = document.getElementById('div-error-connect');
devErrorConnect.innerHTML = '';
devErrorConnect.classList = devErrorConnect.classList.value.includes('hidden')
? devErrorConnect.classList.value
: `${devErrorConnect.classList.value} hidden`;
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
WALLET_CONNECTED = accounts[0];
// Update chain connected
const chainId = await ethereum.request({ method: 'eth_chainId' });
onChainChanged(chainId);
// Update wallet connection preference to true
localStorage.setItem(WALLET_CONNECTION_PREF_KEY, true);
onWalletConnection();
} catch (error) {
console.log({ error });
// If error connecting, display the error message
devErrorConnect.innerHTML = error?.message ?? 'Unknown wallet connection error.'
devErrorConnect.classList = devErrorConnect.classList.value.replaceAll('hidden', '');
}
console.groupEnd();
};
/**
* When Disconnect button is clicked
*/
const disconnect = () => {
console.group('disconnect');
WALLET_CONNECTED = false;
onChainChanged(null);
// Remove wallet connection preference
localStorage.removeItem(WALLET_CONNECTION_PREF_KEY);
onWalletDisconnect();
console.groupEnd();
};
/**
* Switches network to Mumbai Testnet or CHAIN_ID_REQUIRED
*/
const switchNetwork = async () => {
console.group('switchNetwork');
console.log({ CHAIN_ID_REQUIRED: CHAIN_ID_REQUIRED.toString(16) });
try {
await window.ethereum.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: `0x${CHAIN_ID_REQUIRED.toString(16)}` }], })
} catch (error) {
console.log({ error });
}
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// Replace elements with required chain name
const chainNameReplace = document.querySelectorAll('.chain-name');
chainNameReplace.forEach(el => {
el.innerHTML = `${CHAIN_DICTIONARY[CHAIN_ID_REQUIRED]}`
});
// Replace elements with required chain name and link
const chainLinkReplace = document.querySelectorAll('.chain-link');
chainLinkReplace.forEach(el => {
el.innerHTML = `${CHAIN_DICTIONARY[CHAIN_ID_REQUIRED]}`
el.setAttribute('href', `${BLOCKCHAIN_EXPLORERS[CHAIN_ID_REQUIRED]}/address/${CONTRACT_ON_CHAINS[CHAIN_ID_REQUIRED]}`)
});
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
const buttonNetwork = document.getElementById('button-network');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
buttonNetwork.addEventListener('click', switchNetwork);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
// Events
window.ethereum.on('accountsChanged', onAccountsChanged);
window.ethereum.on('chainChanged', onChainChanged);
// Check if already connected with the number of permissions we have
const hasWalletPermissions = await window.ethereum.request({ method: 'wallet_getPermissions' });
console.log({ hasWalletPermissions });
// Retrieve wallet connection preference from localStorage
const shouldBeConnected = JSON.parse(localStorage.getItem(WALLET_CONNECTION_PREF_KEY)) || false;
console.log({ shouldBeConnected });
// If wallet has permissions update the site UI
if (hasWalletPermissions.length > 0 && shouldBeConnected) {
// Retrieve chain
const chainId = await ethereum.request({ method: 'eth_chainId' });
onChainChanged(chainId);
connect();
}
}
console.groupEnd();
};
Contract Reading
Now that we can ensure we’re on the right network, we need to be able to send a read request to our contract, and to do so we’ll utilize eth_call
which is a standard Ethereum JSON-RPC API for making read calls to the blockchain. The function that we’re going to be calling is getGreeting
from our contract, but in order to call it we can’t simply refer to it as getGreeting
, we need to hash it for the RPC to interpret it correctly.
To hash the function, we could build out even more functionality to hash the data appropriately, but this would make this article even longer. To help we’re going to take advantage of ethers.js as an imported CDN file to take advantage of its hashing functionality.
The goal we’re aiming for is to change getGreeting
into:
"getGreeting" === "0xfe50cc72"
Ethers.js CDN
To start let’s add the ethers.js CDN to the top of our html file to allow us to leverage its functionality.
File: ./app/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VanillaJS Wallet Interactions</title>
<!-- OPTIONAL TAILWIND STYLING -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- ethers.js - Needed for verifying signature, and various hashing -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.umd.min.js" crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<!-- Our main script -->
<script src="./scripts.js"></script>
</head>
<!-- // ... -->
ABI File
Next, we need the ABI file of our contract for ethers.js to interpret the function correctly. We’ll add this as a constant at the top of our scripts.js
file.
File: ./app/scripts.js
// Constants
// ========================================================
// ...
/**
* ABI needed to interpret how to interact with the contract
*/
const CONTRACT_ABI = [
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "message",
"type": "string"
}
],
"name": "NewGreeting",
"type": "event"
},
{
"inputs": [],
"name": "getGreeting",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"name": "setGreeting",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
];
// Functions
// ========================================================
// ...
Encoding The Function
In order for the read function to be interpreted correctly for the RPC to process the request we need to encode the function to 8 bytes, or 8 characters excluding 0x
.
We’ll add functionality for our Get Greeting form with the hashed encoded function.
Thanks to William Schwab and his article on Everything You Ever Wanted To Know About Events And Logs On Ethereum. This helped immensely with all the encoded functions for
eth_call
,eth_sendTransaction
, andeth_getLogs
.
File: ./app/scripts.js
// Functions
// ========================================================
// ...
/**
* When the getGreeting form is submitted
* @param {*} event
*/
const onSubmitContractRead = async (event) => {
console.group('onSubmitContractRead');
event.preventDefault();
// Reset & Set Loading State
const preContractRead = document.getElementById('pre-contract-read');
preContractRead.innerHTML = '(Loading...)';
const button = document.querySelector(`#${event.currentTarget.id} button`);
button.setAttribute('disabled', true);
// Setup Interface + Encode Function
const GetGreeting = CONTRACT_ABI.find(i => i.name === 'getGreeting');
const interface = new ethers.utils.Interface([GetGreeting]);
const encodedFunction = interface.encodeFunctionData(`${GetGreeting.name}`);
console.log({ encodedFunction });
// Request getGreeting
// (Up next)
button.removeAttribute('disabled');
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// ...
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
const buttonNetwork = document.getElementById('button-network');
const formContractRead = document.getElementById('form-contract-read');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
buttonNetwork.addEventListener('click', switchNetwork);
formContractRead.addEventListener('submit', onSubmitContractRead);
// ...
console.groupEnd();
};
If we click the button to submit the form, it will show a loading state and in the console log it will show the encoded function we need.
Making The Request
To make the request we need to use eth_call
, which is meant for read requests and doesn’t require any native tokens to process the requests.
In the params of the RPC request we need to pass to values, based on the Ethereum JSON-RPIC eth_call documentation.
to
— the contract we are making the request in reference todata
— the encoded hash function we want to call
File: ./app/scripts.js
// Functions
// ========================================================
// ...
/**
* When the getGreeting form is submitted
* @param {*} event
*/
const onSubmitContractRead = async (event) => {
console.group('onSubmitContractRead');
event.preventDefault();
// Reset & Set Loading State
const preContractRead = document.getElementById('pre-contract-read');
preContractRead.innerHTML = '(Loading...)';
const button = document.querySelector(`#${event.currentTarget.id} button`);
button.setAttribute('disabled', true);
// Setup Interface + Encode Function
const GetGreeting = CONTRACT_ABI.find(i => i.name === 'getGreeting');
const interface = new ethers.utils.Interface([GetGreeting]);
const encodedFunction = interface.encodeFunctionData(`${GetGreeting.name}`);
console.log({ encodedFunction });
// Request getGreeting
try {
const result = await window.ethereum.request({
method: 'eth_call', params: [{
to: CONTRACT_ON_CHAINS[CHAIN_CONNECTED.id],
data: encodedFunction
}]
});
preContractRead.innerHTML = `${result}`;
} catch (error) {
console.log({ error });
preContractRead.innerHTML = error?.message ?? 'Unknown contract read error.';
}
button.removeAttribute('disabled');
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
// ...
And if we submit the request, we should the following response show up in our UI.
Response:
0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000f4465632034206c617465206e6f77210000000000000000000000000000000000
Decoding The Response
As you can imagine this is not really readable for humans, so we’re going to create a helper function that allows us to convert hex values to ascii (a string).
File: ./app/scripts.js
// ...
// Functions
// ========================================================
/**
* Helper function that converts hex values to strings
* @param {*} hex
* @returns
*/
const hex2ascii = (hex) => {
console.group('hex2ascii');
console.log({ hex });
let str = '';
for (let i = 0; i < hex.length; i += 2) {
const v = parseInt(hex.substr(i, 2), 16);
if (v) str += String.fromCharCode(v);
}
console.groupEnd();
return str;
};
// ...
/**
* When the getGreeting form is submitted
* @param {*} event
*/
const onSubmitContractRead = async (event) => {
console.group('onSubmitContractRead');
event.preventDefault();
// Reset & Set Loading State
const preContractRead = document.getElementById('pre-contract-read');
preContractRead.innerHTML = '(Loading...)';
const button = document.querySelector(`#${event.currentTarget.id} button`);
button.setAttribute('disabled', true);
// Setup Interface + Encode Function
const GetGreeting = CONTRACT_ABI.find(i => i.name === 'getGreeting');
const interface = new ethers.utils.Interface([GetGreeting]);
const encodedFunction = interface.encodeFunctionData(`${GetGreeting.name}`);
console.log({ encodedFunction });
// Request getGreeting
try {
const result = await window.ethereum.request({
method: 'eth_call', params: [{
to: CONTRACT_ON_CHAINS[CHAIN_CONNECTED.id],
data: encodedFunction
}]
});
preContractRead.innerHTML = `${result}\n\n// ${hex2ascii(result)}`;
} catch (error) {
console.log({ error });
preContractRead.innerHTML = error?.message ?? 'Unknown contract read error.';
}
button.removeAttribute('disabled');
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
// ...
Now when we make the request, we can see the human-readable value.
Contract Writing
Contract read didn’t seem too bad, but for contract write we actually need to encode a function and its payload in the same hash. If you don’t pass it the correct amount of inputs for the function it will give you an error where the values expected mismatch.
Also because we’re performing a writeable action we need to use a different RPC method, the standard eth_sendTransaction
JSON-RPC API. For this method we need to send in its params the following:
from
— our wallet connected to the siteto
— the contract we are making the request in reference todata
— the encoded hash function and data we want to send to the contract
Let’s add a new submit function for our contract write request and perform nearly the same request as our read.
File: ./app/scripts.js
// ...
// Functions
// ========================================================
// ...
/**
* When the setGreeting form is submitted
* @param {*} event
*/
const onSubmitContractWrite = async (event) => {
event.preventDefault();
console.group('onSubmitContractWrite');
const greeting = event.currentTarget.greeting.value;
console.log({ greeting });
// Reset & Set Loading State
const preContractWrite = document.getElementById('pre-contract-write');
preContractWrite.innerHTML = '(Loading...)';
const input = document.querySelector(`#${event.currentTarget.id} input`);
const button = document.querySelector(`#${event.currentTarget.id} button`);
button.setAttribute('disabled', true);
// Setup Interface + Encode Function
const SetGreeting = CONTRACT_ABI.find(i => i.name === 'setGreeting');
const interface = new ethers.utils.Interface([SetGreeting]);
const encodedFunction = interface.encodeFunctionData(`${SetGreeting.name}`, [greeting]);
console.log({ encodedFunction });
// Request setGreeting
try {
const result = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: WALLET_CONNECTED,
to: CONTRACT_ON_CHAINS[CHAIN.id],
data: encodedFunction
}]
});
preContractWrite.innerHTML = `${result}`;
} catch (error) {
console.log({ error });
preContractWrite.innerHTML = error?.message ?? 'Unknown contract write error.';
}
input.removeAttribute('disabled');
button.removeAttribute('disabled');
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// ...
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
const buttonNetwork = document.getElementById('button-network');
const formContractRead = document.getElementById('form-contract-read');
const formContractWrite = document.getElementById('form-contract-write');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
buttonNetwork.addEventListener('click', switchNetwork);
formContractRead.addEventListener('submit', onSubmitContractRead);
formContractWrite.addEventListener('submit', onSubmitContractWrite);
// ...
console.groupEnd();
};
Now if we submit the form with an input, we should get a prompt from our wallet to complete the transaction, and get a transaction id back as a response.
NOTE: Depending if the network is busy, you might need to try this a few times.
We’re just going to add one last functionality where we make it easy to get the link to the transaction id with the current chain’s native block explorer.
Full code so far with that additional block explorer link.
File: ./app/scripts.js
// Constants
// ========================================================
/**
* To keep track of which wallet is connected throughout our app
*/
let WALLET_CONNECTED = '';
/**
* localStorage key
*/
let WALLET_CONNECTION_PREF_KEY = 'WC_PREF';
/**
* Current chain connected with chain id and name as a object { id: 1, name: "Ethereum Mainnet" }
*/
const CHAIN_CONNECTED = {
id: null,
name: null
};
/**
* Chain ids and their names
*/
const CHAIN_DICTIONARY = {
1: 'Ethereum Mainnet',
5: 'Goerli Testnet',
137: 'Polygon Mainnet',
1337: 'Localhost',
1402: 'zkEVM Testnet',
80001: 'Mumbai Testnet',
11155111: 'Sepolia Testnet'
};
/**
* Required chain to interact with contract
*/
const CHAIN_ID_REQUIRED = 80001; //Mumbai
/**
* Same contract deployed to each network
*/
const CONTRACT_ON_CHAINS = {
1: '0x76460E73eadE1DDe315E07a5eCa092448c193a2F',
5: '0x3aC587078b344a3d27e56632dFf236F1Aff04D56',
137: '0x375F01b156D9BdDDd41fd38c5CC74C514CB71f73',
1337: '',
1402: '0x76460E73eadE1DDe315E07a5eCa092448c193a2F',
80001: '0x7Bd54062eFa363A97dC20f404825597455E93582',
11155111: '0x375f01b156d9bdddd41fd38c5cc74c514cb71f73',
};
/**
* All blockchain explorers
*/
const BLOCKCHAIN_EXPLORERS = {
1: 'https://etherscan.io',
5: 'https://goerli.etherscan.io',
137: 'https://polygonscan.com',
1337: null,
1402: 'https://explorer.public.zkevm-test.net',
80001: 'https://mumbai.polygonscan.com',
11155111: 'https://sepolia.etherscan.io',
};
/**
* ABI needed to interpret how to interact with the contract
*/
const CONTRACT_ABI = [
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "message",
"type": "string"
}
],
"name": "NewGreeting",
"type": "event"
},
{
"inputs": [],
"name": "getGreeting",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_greeting",
"type": "string"
}
],
"name": "setGreeting",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
];
// Functions
// ========================================================
/**
* Helper function that converts hex values to strings
* @param {*} hex
* @returns
*/
const hex2ascii = (hex) => {
console.group('hex2ascii');
console.log({ hex });
let str = '';
for (let i = 0; i < hex.length; i += 2) {
const v = parseInt(hex.substr(i, 2), 16);
if (v) str += String.fromCharCode(v);
}
console.groupEnd();
return str;
};
/**
* When the chainId has changed
* @param {string|null} chainId
*/
const onChainChanged = (chainId) => {
console.group('onChainChanged');
console.log({ chainId });
// Get the UI element that displays the wallet network
const preWalletNetwork = document.getElementById('pre-wallet-network');
if (!chainId) {
CHAIN_CONNECTED.name = null;
CHAIN_CONNECTED.id = null;
// Set the network to blank
preWalletNetwork.innerHTML = ``;
} else {
const parsedChainId = parseInt(`${chainId}`, 16);
CHAIN_CONNECTED.name = CHAIN_DICTIONARY?.[parsedChainId];
CHAIN_CONNECTED.id = parsedChainId;
const buttonNetwork = document.getElementById('button-network');
const divErrorNetwork = document.getElementById('div-error-network');
const formContractReadButton = document.querySelector('#form-contract-read button');
const formContractWriteInput = document.querySelector('#form-contract-write input');
const formContractWriteButton = document.querySelector('#form-contract-write button');
if (parsedChainId !== CHAIN_ID_REQUIRED) {
// Show error elements
buttonNetwork.classList = `${buttonNetwork.classList.value.replaceAll('hidden', '')}`;
divErrorNetwork.classList = `${divErrorNetwork.classList.value.replaceAll('hidden', '')}`;
divErrorNetwork.children[1].innerHTML = `${CHAIN_CONNECTED.name}`;
// Disable forms
formContractReadButton.setAttribute('disabled', true);
formContractWriteInput.setAttribute('disabled', true);
formContractWriteButton.setAttribute('disabled', true);
} else {
// Hide error elements
buttonNetwork.classList = `${buttonNetwork.classList.value} hidden`;
divErrorNetwork.classList = `${divErrorNetwork.classList.value} hidden`;
divErrorNetwork.children[1].innerHTML = '';
// Enable forms
formContractReadButton.removeAttribute('disabled');
formContractWriteInput.removeAttribute('disabled');
formContractWriteButton.removeAttribute('disabled');
}
// Set the network to show the current connected network
preWalletNetwork.innerHTML = `${CHAIN_CONNECTED?.id} / ${CHAIN_CONNECTED?.name}`;
}
console.log({ CHAIN_CONNECTED });
console.groupEnd();
};
/**
* When wallet connects or disconnects with accountsChanged event
* @param {*} accounts Array of accounts that have changed - typicall array of one
*/
const onAccountsChanged = async (accounts) => {
console.group('onAccountsChanged');
console.log({ accounts });
// No accounts found - use onWalletDisconnect to update UI
if (accounts.length === 0) {
onChainChanged(null);
onWalletDisconnect();
} else {
// Accounts found - use callback for onWalletConnection to update UI
WALLET_CONNECTED = accounts?.[0];
// Update chain connected
const chainId = await ethereum.request({ method: 'eth_chainId' });
onChainChanged(chainId);
onWalletConnection();
}
console.groupEnd();
};
/**
* When wallet disconnect occurs
*/
const onWalletDisconnect = () => {
console.group('onWalletDisconnect');
// Hide connected section
const sectionConnected = document.getElementById('section-connected');
sectionConnected.classList = 'hidden';
// Enabled connect button
const buttonConnect = document.getElementById('button-connect');
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = 'Connect Wallet';
console.groupEnd();
};
/**
* When a wallet connection occurs
*/
const onWalletConnection = () => {
console.group('onWalletConnection');
// Disable connect button
const buttonConnect = document.getElementById('button-connect');
buttonConnect.setAttribute('disabled', true);
buttonConnect.innerHTML = 'Connected';
// Show connected section
const sectionConnected = document.getElementById('section-connected');
sectionConnected.classList = '';
// Set the wallet address to show the user
const preWalletAddress = document.getElementById('pre-wallet-address');
preWalletAddress.innerHTML = WALLET_CONNECTED;
console.groupEnd();
};
/**
* When Connect Button is clicked
*/
const connect = async () => {
console.group('connect');
// Reset our error element each time the button is clicked
const devErrorConnect = document.getElementById('div-error-connect');
devErrorConnect.innerHTML = '';
devErrorConnect.classList = devErrorConnect.classList.value.includes('hidden')
? devErrorConnect.classList.value
: `${devErrorConnect.classList.value} hidden`;
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
WALLET_CONNECTED = accounts[0];
// Update chain connected
const chainId = await ethereum.request({ method: 'eth_chainId' });
onChainChanged(chainId);
// Update wallet connection preference to true
localStorage.setItem(WALLET_CONNECTION_PREF_KEY, true);
onWalletConnection();
} catch (error) {
console.log({ error });
// If error connecting, display the error message
devErrorConnect.innerHTML = error?.message ?? 'Unknown wallet connection error.'
devErrorConnect.classList = devErrorConnect.classList.value.replaceAll('hidden', '');
}
console.groupEnd();
};
/**
* When Disconnect button is clicked
*/
const disconnect = () => {
console.group('disconnect');
WALLET_CONNECTED = false;
onChainChanged(null);
// Remove wallet connection preference
localStorage.removeItem(WALLET_CONNECTION_PREF_KEY);
onWalletDisconnect();
console.groupEnd();
};
/**
* Switches network to Mumbai Testnet or CHAIN_ID_REQUIRED
*/
const switchNetwork = async () => {
console.group('switchNetwork');
console.log({ CHAIN_ID_REQUIRED: CHAIN_ID_REQUIRED.toString(16) });
try {
await window.ethereum.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: `0x${CHAIN_ID_REQUIRED.toString(16)}` }], })
} catch (error) {
console.log({ error });
}
console.groupEnd();
};
/**
* When the getGreeting form is submitted
* @param {*} event
*/
const onSubmitContractRead = async (event) => {
console.group('onSubmitContractRead');
event.preventDefault();
// Reset & Set Loading State
const preContractRead = document.getElementById('pre-contract-read');
preContractRead.innerHTML = '(Loading...)';
const button = document.querySelector(`#${event.currentTarget.id} button`);
button.setAttribute('disabled', true);
// Setup Interface + Encode Function
const GetGreeting = CONTRACT_ABI.find(i => i.name === 'getGreeting');
const interface = new ethers.utils.Interface([GetGreeting]);
const encodedFunction = interface.encodeFunctionData(`${GetGreeting.name}`);
console.log({ encodedFunction });
// Request getGreeting
try {
const result = await window.ethereum.request({
method: 'eth_call', params: [{
to: CONTRACT_ON_CHAINS[CHAIN_CONNECTED.id],
data: encodedFunction
}]
});
preContractRead.innerHTML = `${result}\n\n// ${hex2ascii(result)}`;
} catch (error) {
console.log({ error });
preContractRead.innerHTML = error?.message ?? 'Unknown contract read error.';
}
button.removeAttribute('disabled');
console.groupEnd();
};
/**
* When the setGreeting form is submitted
* @param {*} event
*/
const onSubmitContractWrite = async (event) => {
event.preventDefault();
console.group('onSubmitContractWrite');
const greeting = event.currentTarget.greeting.value;
console.log({ greeting });
// Reset & Set Loading State
const preContractWrite = document.getElementById('pre-contract-write');
preContractWrite.innerHTML = '(Loading...)';
const input = document.querySelector(`#${event.currentTarget.id} input`);
const button = document.querySelector(`#${event.currentTarget.id} button`);
button.setAttribute('disabled', true);
// Setup Interface + Encode Function
const SetGreeting = CONTRACT_ABI.find(i => i.name === 'setGreeting');
const interface = new ethers.utils.Interface([SetGreeting]);
const encodedFunction = interface.encodeFunctionData(`${SetGreeting.name}`, [greeting]);
console.log({ encodedFunction });
// Request setGreeting
try {
const result = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: WALLET_CONNECTED,
to: CONTRACT_ON_CHAINS[CHAIN_CONNECTED.id],
data: encodedFunction
}]
});
preContractWrite.innerHTML = `${result}\n\n// ${BLOCKCHAIN_EXPLORERS?.[CHAIN_CONNECTED?.id] ?? ''}/tx/${result}`;
} catch (error) {
console.log({ error });
preContractWrite.innerHTML = error?.message ?? 'Unknown contract write error.';
}
input.removeAttribute('disabled');
button.removeAttribute('disabled');
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init
*/
window.onload = async () => {
console.group('window.onload');
// Replace elements with required chain name
const chainNameReplace = document.querySelectorAll('.chain-name');
chainNameReplace.forEach(el => {
el.innerHTML = `${CHAIN_DICTIONARY[CHAIN_ID_REQUIRED]}`
});
// Replace elements with required chain name and link
const chainLinkReplace = document.querySelectorAll('.chain-link');
chainLinkReplace.forEach(el => {
el.innerHTML = `${CHAIN_DICTIONARY[CHAIN_ID_REQUIRED]}`
el.setAttribute('href', `${BLOCKCHAIN_EXPLORERS[CHAIN_ID_REQUIRED]}/address/${CONTRACT_ON_CHAINS[CHAIN_ID_REQUIRED]}`)
});
// All HTML Elements
const buttonConnect = document.getElementById('button-connect');
const buttonDisconnect = document.getElementById('button-disconnect');
const buttonNetwork = document.getElementById('button-network');
const formContractRead = document.getElementById('form-contract-read');
const formContractWrite = document.getElementById('form-contract-write');
// Event Interactions
buttonConnect.addEventListener('click', connect);
buttonDisconnect.addEventListener('click', disconnect);
buttonNetwork.addEventListener('click', switchNetwork);
formContractRead.addEventListener('submit', onSubmitContractRead);
formContractWrite.addEventListener('submit', onSubmitContractWrite);
// Check if browser has wallet integration
if (typeof window?.ethereum !== "undefined") {
// Enable Button
buttonConnect.removeAttribute('disabled');
buttonConnect.innerHTML = "Connect Wallet";
// Events
window.ethereum.on('accountsChanged', onAccountsChanged);
window.ethereum.on('chainChanged', onChainChanged);
// Check if already connected with the number of permissions we have
const hasWalletPermissions = await window.ethereum.request({ method: 'wallet_getPermissions' });
console.log({ hasWalletPermissions });
// Retrieve wallet connection preference from localStorage
const shouldBeConnected = JSON.parse(localStorage.getItem(WALLET_CONNECTION_PREF_KEY)) || false;
console.log({ shouldBeConnected });
// If wallet has permissions update the site UI
if (hasWalletPermissions.length > 0 && shouldBeConnected) {
// Retrieve chain
const chainId = await ethereum.request({ method: 'eth_chainId' });
onChainChanged(chainId);
connect();
}
}
console.groupEnd();
};
🎉 There we go, we successfully connected a wallet, disconnected it, automatically connected it, saved our connection preference, read from a contract, and wrote to a contract.
Full Code
If you want to see the full source code, check out the following repository.
Where To Go From Here?
Want to see more RPC requests, with different networks, and even try it with a local network? Take a look at a comprehensive repository that has more wallet interactions here:
I should be writing more articles on how a frontend can interact with a wallet and different EVM chains, so make sure to follow my account.
If you got value from this, please give it some love by sharing it, and please also follow me on twitter (where I’m quite active) @codingwithmanny and instagram at @codingwithmanny.