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

Manny
43 min readDec 5, 2022
Use Pure JavaScript To Interact With Your MetaMask Wallet & RPC Requests To The Blockchain

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.

VanillaJS Wallet Connection App

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.

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">&nbsp;</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">&nbsp;</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
Localhost VanillaJS Wallet Connection

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.

Unsupported Browser
Supported Browser

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.

Rejected Wallet Connection

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();
};
Rejected Wallet Connection Error UI

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();
};
Successful Wallet Connection

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.

How To Disconnect Fully With MetMask

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();
};
Wallet Connected
Wallet Disconnected

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.

Fully Disconnecting From Site Using Event accountsChanged

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();
};
Wallet Connected To Site With Array Length Of 1
Wallet Completely Disconnected From Site With Array Length Of 0

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();
};
On Page Refresh Connection Preference Set To False
On Page Refresh Connection Preference Set To True

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.

Clicking Connect The First Time
Clicking Disconnect

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();
};
Network Set On Wallet Connected & On Page Refresh
Network Set On Wallet Account Changed

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.

Network Set On Wallet Network Changed

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.

Network Not Set To Mumbai & Showing Errors

This is what it looks like when we’re connected to the right network.

Network Set To Mumbai

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.

Wallet Prompt To Switch Networks

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, and eth_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.

Get Greeting Encoded getGreeting Function

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 to
  • data — 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
Get Greeting Runing eth_call With Function getGreeting

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.

getGreeting response hex decoded as an ascii string

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.

setGreeting Encoded Function Values 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 site
  • to — the contract we are making the request in reference to
  • data — 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.

setGreeting using eth_sendTransaction to successfully send “Hello there!”
Verifying our new message by request getGreeting with eth_call

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();
};
setGreeting successfully called with eth_sendTransaction and showing a block explorer url to the tx
Seeing the transaction link

🎉 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.

--

--

Manny
Manny

Written by Manny

DevRel Engineer @ Berachain | Prev Polygon | Ankr & Web Application / Full Stack Developer

No responses yet