How To Create MetaMask Wallet Signatures With Pure VanillaJS
Learn How To Create Signature Hashes With Crypto Wallets & Verify Those Signatures
What Are Digital Signatures?
Digital signatures are a base functionality that allows for verification with crypto wallets. This functionality to sign a message and create a signature is something that is built-in into every EVM-based wallet.
A short string of data a user produces for a document using a private key such that anyone with the corresponding public key, the signature, and the document can verify that (1) the document was “signed” by the owner of that particular private key, and (2) the document was not changed after it was signed.
Another way to look at digital signatures is a mechanism to verify that a piece of data came from a specific source or user.
You could think of it almost like stamping a document and making it official that it was approved by a company to verify that it was indeed agreed upon and the uniqueness of the stamp is a way to verify that it came from the original source.
Why Do We Need Digital Signatures?
The same way we want to verify user says who they say they are with a username and password, we can use a signatures to verify them and then give them access to private or confidential information.
If this sounds familiar, this is the fundamentals of authentication and authorization. We prove the user is and then ultimately authorize them to gain access to sensitive information.
How Is A Digital Signature Produced?
With a crypto wallet, a signature is produced when a wallet is prompted with a message to sign, similar to being presented a legal contract and asked to sign the document in your own hand writing.
Once the message is signed, it produces a unique signatures in the form of a hash, which is a unique string output with a fixed number of characters.
There are a few different algotirhtms for hashing from MD5, SHA256, Keccak256, and many more.
The process of hashing usually converts inputs into a one directional output, which means it can’t be reverted back like encryption where you can encrypt and decrypt data.
A commonly used practice for hashing is a hashed password authentication comparison with what has been stored in a database. That way the system that is managing the database doesn’t see the passwords in plain text, but just with text.
// Example
const sha3 = require("js-sha3");
const mysql = require('mysql');
const hashedPassword = sha3.keccak256("My password");
// ...
const query = `SELECT * FROM users WHERE password="${hashedPassword}"`;
One key feature about hashing that gives to its uniqueness and validity is the fact that if you change one single byte or character for the inputs, it can create an entirely new hash.
Building A Frontend For Wallet Signatures
We’ll be building a frontend that connects our MetaMask wallet to the site, prompts the wallet to sign a message, and verifies the signature with wallet address that signed it to validate it.
Requirements
Before we go further you’ll need to make sure you have the following installed on your computer:
- NVM or Node
v18.12.1
- pnpm
v7.21.0
- Chrome
- MetaMask Chrome Extension
Setup Files & Server
I’m going to use pure JavaScript to interact with the MetaMask wallet, but there is one thing I will need which is an HTTP server. In order to get one set up I’m going to take advantage of a package called live-server and this will be the only npm
package we install.
We’ll initiate our npm
packages, create a public folder to store our HTML and JavaScript files.
# FROM ./vanillajs-wallet-digital-signature
pnpm init;
pnpm add -D live-server;
mkdir public;
touch public/index.html
touch public/scripts.js
Let’s create a simple html structure and create an event listener that will console log a “loaded” message once the page is loaded.
File: ./public/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 Digital Signature</title>
<script src="./scripts.js"></script>
</head>
<body>
<h1>VanillaJS Wallet Digital Signature</h1>
</body>
</html>
File: ./public/scripts.js
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init - Runs as soon as window is loaded
*/
window.onload = () => {
console.log('Loaded');
}';
Let’s start up our server using the binary from our live-server
package which exposes the public folder to port 3001
.
# FROM ./vanillajs-wallet-digital-signature
./node_modules/.bin/live-server --port=3001 --watch=$PWD/public --mount=/:./public
# Expected Output:
# Mapping / to "/path/to/vanillajs-wallet-digital-signature/public"
# Serving "/path/to/vanillajs-wallet-digital-signature" at http://127.0.0.1:3001
# Ready for changes
Wallet Connection
Now that we have our server up and running let’s all the full HTML to our site to see all elements we want to build for and then start adding functionality.
File: ./public/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 Digital Signature</title>
<script src="/scripts.js"></script>
</head>
<body>
<h1>VanillaJS Wallet Digital Signature</h1>
<p>Demonstrates how an EVM crypto wallet creates a digital signature and verifies the signature with it's source.</p>
<h2>Connect Wallet</h2>
<button id="button-wallet-connect">Connect Wallet</button>
<pre><code id="code-wallet-address"></code></pre>
<section id="connected" style="display: none;">
<hr />
<h2>Sign Message</h2>
<form id="form-wallet-sign">
<div>
<label>Message</label>
<input type="text" name="message" />
</div>
<div>
<button type="submit">Sign</button>
</div>
</form>
<div>
<pre><code id="code-wallet-signature"></code></pre>
</div>
<hr />
<h2>Verify Message & Signature</h2>
<button id="button-signature-verify">Verify</button>
<div>
<pre><code id="code-wallet-signature-verify"></code></pre>
</div>
</section>
<hr />
<p><small>Made by <a target="_blank" href="https://linktr.ee/codingwithmanny">@codingwithmanny</a></small></p>
</body>
</html>
Now let’s add the functionality for wallet connection by taking advantage of an object that MetaMask injects into our browser called window.ethereum
.
This injected object allows us to interact with our MetaMask wallet for various things like connection, signing, and even blockchain requests.
Let’s add the functionality to our scripts.js
file that prompts the user’s wallet to connect to the current site.
File: ./public/scripts.js
// Globals
// ========================================================
/**
*
*/
let WALLET_ADDRESS = '';
// Functions
// ========================================================
/**
* Connects wallet to site
*/
const onClickWalletConnect = async () => {
console.group('onClickWalletConnect');
// Get the element we want to output the result of connecting the wallet
const codeWalletAddress = document.getElementById('code-wallet-address');
try {
// eth_requestAccounts is a MetaMask RPC API request that will
// prompt the wallet to connect or if already has connected successfully connect
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
console.log({ accounts });
codeWalletAddress.innerHTML = accounts[0];
// We want to store the wallet address to a globla variable to use later
WALLET_ADDRESS = accounts[0];
// Shows the additional sections that will be used if the wallet is connected
const sectionConnected = document.getElementById('connected');
sectionConnected.removeAttribute('style');
} catch (error) {
console.log({ error });
codeWalletAddress.innerHTML = error?.message ?? 'Unknown wallet connection error.'
}
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init - Runs as soon as window is loaded
*/
window.onload = () => {
// Elements
const buttonWalletConnect = document.getElementById('button-wallet-connect');
// Events
buttonWalletConnect.addEventListener('click', onClickWalletConnect);
};
Message Prompt For Signature
This next piece is to demonstrate how to send a message to the wallet to prompt it to sign it and retrieve a signature.
File: ./public/scripts.js
// Globals
// ========================================================
/**
*
*/
let WALLET_ADDRESS = '';
/**
*
*/
let MESSAGE = '';
/**
*
*/
let SIGNATURE = '';
// Functions
// ========================================================
/**
* Connects wallet to site
*/
const onClickWalletConnect = async () => {
console.group('onClickWalletConnect');
// Get the element we want to output the result of connecting the wallet
const codeWalletAddress = document.getElementById('code-wallet-address');
try {
// eth_requestAccounts is a MetaMask RPC API request that will
// prompt the wallet to connect or if already has connected successfully connect
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
console.log({ accounts });
codeWalletAddress.innerHTML = accounts[0];
// We want to store the wallet address to a globla variable to use later
WALLET_ADDRESS = accounts[0];
// Shows the additional sections that will be used if the wallet is connected
const sectionConnected = document.getElementById('connected');
sectionConnected.removeAttribute('style');
} catch (error) {
console.log({ error });
codeWalletAddress.innerHTML = error?.message ?? 'Unknown wallet connection error.'
}
console.groupEnd();
};
/**
* Prompts wallet to sign a message to produce a signature
* @param {*} event
*/
const onSubmitWalletSign = async (event) => {
console.group('onSubmitWalletSign');
event.preventDefault();
// Retrieve message from input form with name "message"
const message = event.currentTarget.message.value;
console.log({ message });
// Store the message in a global variable
MESSAGE = message;
// Get the element we want to output the result
// of prompting the wallet for signature
const codeWalletSignature = document.getElementById('code-wallet-signature');
// Prompt wallet for signature
try {
// Perform a personal sign with the original message and the wallet address
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [`${MESSAGE}`, WALLET_ADDRESS]
});
console.log({ signature });
// Show the signature
codeWalletSignature.innerHTML = signature;
SIGNATURE = signature;
console.log({ SIGNATURE });
} catch (error) {
console.log({ error });
codeWalletSignature.innerHTML = error?.message ?? 'Unknown wallet signature error.'
}
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init - Runs as soon as window is loaded
*/
window.onload = () => {
// Elements
const buttonWalletConnect = document.getElementById('button-wallet-connect');
const formWalletSign = document.getElementById('form-wallet-sign');
// Events
buttonWalletConnect.addEventListener('click', onClickWalletConnect);
formWalletSign.addEventListener('submit', onSubmitWalletSign);
};
Verify Signature
Now that we have the signature let’s verify the signature to see that it originated from the original wallet address.
Now in order to do this, there are quite a few different functionality that needs to be implemented in order to get it in the format it needs to get the wallet address from the signature.
To go through this a bit faster, we’re going to use a library but not as an npm package. We’re going to see use pure JavaScript but we’re going to take advantage of a CDN that loads our needed library to take advantage of in our scripts.js
.
The library we’re be using is ethers.js, but only the verifyMessage
functionality.
To enable ethers.js we’ll add a CDN from Ethers.JS CNDJS into our HTML file.
File: ./public/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 Digital Signature</title>
<!-- CDN Resource for ethersjs -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.umd.min.js"></script>
<!-- Custom script -->
<script src="/scripts.js"></script>
</head>
<body>
<h1>VanillaJS Wallet Digital Signature</h1>
<p>Demonstrates how an EVM crypto wallet creates a digital signature and verifies the signature with it's source.</p>
<h2>Connect Wallet</h2>
<button id="button-wallet-connect">Connect Wallet</button>
<pre><code id="code-wallet-address"></code></pre>
<section id="connected" style="display: none;">
<hr />
<h2>Sign Message</h2>
<form id="form-wallet-sign">
<div>
<label>Message</label>
<input type="text" name="message" />
</div>
<div>
<button type="submit">Sign</button>
</div>
</form>
<div>
<pre><code id="code-wallet-signature"></code></pre>
</div>
<hr />
<h2>Verify Message & Signature</h2>
<button id="button-signature-verify">Verify</button>
<div>
<pre><code id="code-wallet-signature-verify"></code></pre>
</div>
</section>
<hr />
<p><small>Made by <a target="_blank" href="https://linktr.ee/codingwithmanny">@codingwithmanny</a></small></p>
</body>
</html>
Now we can take advantage of Ethers.js with the main instance of ethers
.
File: ./public/scripts.js
// Globals
// ========================================================
/**
*
*/
let WALLET_ADDRESS = '';
/**
*
*/
let MESSAGE = '';
/**
*
*/
let SIGNATURE = '';
// Functions
// ========================================================
/**
* Connects wallet to site
*/
const onClickWalletConnect = async () => {
console.group('onClickWalletConnect');
// Get the element we want to output the result of connecting the wallet
const codeWalletAddress = document.getElementById('code-wallet-address');
try {
// eth_requestAccounts is a MetaMask RPC API request that will
// prompt the wallet to connect or if already has connected successfully connect
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
console.log({ accounts });
codeWalletAddress.innerHTML = accounts[0];
// We want to store the wallet address to a globla variable to use later
WALLET_ADDRESS = accounts[0];
// Shows the additional sections that will be used if the wallet is connected
const sectionConnected = document.getElementById('connected');
sectionConnected.removeAttribute('style');
} catch (error) {
console.log({ error });
codeWalletAddress.innerHTML = error?.message ?? 'Unknown wallet connection error.'
}
console.groupEnd();
};
/**
* Prompts wallet to sign a message to produce a signature
* @param {*} event
*/
const onSubmitWalletSign = async (event) => {
console.group('onSubmitWalletSign');
event.preventDefault();
// Retrieve message from input form with name "message"
const message = event.currentTarget.message.value;
console.log({ message });
// Store the message in a global variable
MESSAGE = message;
// Get the element we want to output the result
// of prompting the wallet for signature
const codeWalletSignature = document.getElementById('code-wallet-signature');
// Prompt wallet for signature
try {
// Perform a personal sign with the original message and the wallet address
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [`${MESSAGE}`, WALLET_ADDRESS]
});
console.log({ signature });
// Show the signature
codeWalletSignature.innerHTML = signature;
SIGNATURE = signature;
console.log({ SIGNATURE });
} catch (error) {
console.log({ error });
codeWalletSignature.innerHTML = error?.message ?? 'Unknown wallet signature error.'
}
console.groupEnd();
};
/**
* Verifies signature signed by wallet
*/
const onClickSignatureVerify = () => {
console.group('onClickSignatureVerify');
console.log({ MESSAGE });
console.log({ SIGNATURE });
console.log({ WALLET_ADDRESS });
// Take advantage of ethers to verify the message and signature
const walletAddress = ethers.utils.verifyMessage(MESSAGE, SIGNATURE);
console.log({ walletAddress });
// Get the element we want to output the result
const codeWalletSignatureVerify = document.getElementById('code-wallet-signature-verify');
codeWalletSignatureVerify.innerHTML = `${walletAddress.toLowerCase()}\nisMatching: ${walletAddress.toLowerCase() === WALLET_ADDRESS}`
console.groupEnd();
};
// Initial Script Loaded On Window Loaded
// ========================================================
/**
* Init - Runs as soon as window is loaded
*/
window.onload = () => {
// Elements
const buttonWalletConnect = document.getElementById('button-wallet-connect');
const formWalletSign = document.getElementById('form-wallet-sign');
const buttonSignatureVerify = document.getElementById('button-signature-verify');
// Events
buttonWalletConnect.addEventListener('click', onClickWalletConnect);
formWalletSign.addEventListener('submit', onSubmitWalletSign);
buttonSignatureVerify.addEventListener('click', onClickSignatureVerify);
};
Now when we connect, sign a message, we can now verify the message and see that it matches the wallet address that we signed the message with.
Tailwind Styling
The last bit we’ll just make our UI a bit nicer by using the Tailwind CDN and applying styles to all our elements.
File: ./public/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 Digital Signature</title>
<!-- Tailwind Styling -->
<script src="https://cdn.tailwindcss.com"></script>
<style type="text/tailwindcss">
@layer base {
body {
@apply bg-zinc-900 p-8;
}
h1 {
@apply text-4xl font-semibold text-zinc-50 mb-4;
}
h2 {
@apply text-xl text-zinc-200 mb-4;
}
p {
@apply text-zinc-400 mb-8;
}
button, button[type="submit"] {
@apply h-12 font-semibold bg-zinc-700 text-zinc-200 px-6 rounded mb-8 hover:bg-zinc-600 transition-colors ease-in-out duration-200;
}
pre {
@apply block bg-zinc-800 text-zinc-50 p-6 mb-8 overflow-scroll;
}
hr {
@apply border-none h-[1px] bg-zinc-700 mb-8;
}
label {
@apply block text-zinc-500 mb-2;
}
form div {
@apply block mb-4;
}
input {
@apply h-12 rounded px-6 w-full;
}
a {
@apply text-zinc-100 hover:underline;
}
}
</style>
<!-- end Tailwind Styling -->
<!-- CDN Resource for ethersjs -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.umd.min.js"></script>
<!-- Custom script -->
<script src="/scripts.js"></script>
</head>
<body>
<h1>VanillaJS Wallet Digital Signature</h1>
<p>Demonstrates how an EVM crypto wallet creates a digital signature and verifies the signature with it's source.</p>
<h2>Connect Wallet</h2>
<button id="button-wallet-connect">Connect Wallet</button>
<pre><code id="code-wallet-address"></code></pre>
<section id="connected" style="display: none;">
<hr />
<h2>Sign Message</h2>
<form id="form-wallet-sign">
<div>
<label>Message</label>
<input type="text" name="message" />
</div>
<div>
<button type="submit">Sign</button>
</div>
</form>
<div>
<pre><code id="code-wallet-signature"></code></pre>
</div>
<hr />
<h2>Verify Message & Signature</h2>
<button id="button-signature-verify">Verify</button>
<div>
<pre><code id="code-wallet-signature-verify"></code></pre>
</div>
</section>
<hr />
<p><small>Made by <a target="_blank" href="https://linktr.ee/codingwithmanny">@codingwithmanny</a></small></p>
</body>
</html>
Now you should see our UI is updated with Tailwind.
Next Steps
If you want to read more about wallet interactions with just pure vanilla JavaScript, I recommend checking out my other article Use VanillaJS To Connect To MetaMask, Read From A Contract, & Write To A Contract.
Keep coming back to my Medium to see some of my upcoming articles on using WAGMI, ethersJS directly from the backend and other libraries and packages to interact with crypto wallets and the blockchain.
Make sure to also follow me on twitter (where I’m quite active) @codingwithmanny.