Connect A Wallet With Viem and Next
Update: Thanks to Zubin for pointing out I'm a doofus. The Viem docs explain how to deal with this. I've updated my code below with the simple example.
I spent far too long today trying to connect to MetaMask with Viem. I am writing this to keep track of what I figured out and share it with anyone in the future. This is not intended to be a complete tutorial but a quick start for myself and anyone else struggling like I did.
Why would you use Viem?
At first glance, Viem seems like more work. That’s intentional, though. It’s more flexible and verbose on purpose. Viem is designed to give developers more choices regarding what they can do with it and only bring in the pieces they need to work with.
This allows developers greater design flexibility and makes Viem lighter. You only need to import the parts you are using. It’s all about intentionality.
It’s also type-safe by default. It’s built-in TypeScript, which means you get type safety and inference out of the box.
Getting started with next
I set up a basic Next project using:
npx create-next-app@latest
Once everything is up and running with npm run dev
it’s time to install viem.
Installing viem
Stop the development environment ctrl-c
and install viem, npm i viem
That’s a short section!
Create the connect wallet button
Connecting the wallet consists of three parts
- Create
client.ts
- Create
walletButton.tsx
- Update
page.tsx
client.ts
import { createWalletClient, createPublicClient, custom, http } from "viem";
import { sepolia } from "viem/chains";
import "viem/window";
export async function ConnectWalletClient() {
// Check for window.ethereum
// window.ethereum is an object provided by MetaMask or other web3 wallets
let transport;
if (window.ethereum) {
// If window.ethereum exists, create a custom transport using it
transport = custom(window.ethereum);
} else {
// If window.ethereum is not available, throw an error
const errorMessage =
"MetaMask or another web3 wallet is not installed. Please install one to proceed.";
throw new Error(errorMessage);
}
// Declare a Wallet Client
// This creates a wallet client using the Sepolia chain and the custom transport
const walletClient = createWalletClient({
chain: sepolia,
transport: transport,
});
// Return the wallet client
return walletClient;
}
export function ConnectPublicClient() {
// Declare a Public Client
// This creates a public client using the Sepolia chain and an HTTP transport
const publicClient = createPublicClient({
chain: sepolia,
transport: http("https://rpc.sepolia.org"),
});
// Return the public client
return publicClient;
}
walletButton.tsx
"use client";
import { useState } from "react";
import { ConnectWalletClient, ConnectPublicClient } from "./client";
import { formatEther } from 'viem'
import Image from "next/image";
export default function WalletButton() {
// State variables to store the wallet address and balance
const [address, setAddress] = useState<string | null>(null);
const [balance, setBalance] = useState<string | null>(null);
// Function to handle the button click event
async function handleClick() {
try {
// Instantiate a Wallet Client and a Public Client
const walletClient = await ConnectWalletClient();
const publicClient = ConnectPublicClient();
// Retrieve the wallet address using the Wallet Client
const [address] = await walletClient.requestAddresses();
// const [address] = await walletClient.getAddresses();
// Retrieve the balance of the address using the Public Client
const balance = formatEther(await publicClient.getBalance({ address }));
// Update the state variables with the retrieved address and balance
setAddress(address);
setBalance(balance);
} catch (error) {
// Error handling: Display an alert if the transaction fails
alert(`Transaction failed: ${error}`);
}
}
return (
<>
{/* Render the Status component with the address and balance */}
<Status address={address} balance={balance} />
{/* Render the Connect Wallet button */}
<button
className="px-8 py-2 rounded-md bg-[#1e2124] flex flex-row items-center justify-center border border-[#1e2124] hover:border hover:border-indigo-600 shadow-md shadow-indigo-500/10"
onClick={handleClick}
>
{/* Display the MetaMask Fox image */}
<Image src="https://upload.wikimedia.org/wikipedia/commons/3/36/MetaMask_Fox.svg" alt="MetaMask Fox" width={25} height={25} />
<h1 className="mx-auto">Connect Wallet</h1>
</button>
</>
);
}
// Component to display the wallet status (connected or disconnected)
function Status({
address,
balance,
}: {
address: string | null;
balance: string;
}) {
if (!address) {
// If no address is provided, display "Disconnected" status
return (
<div className="flex items-center">
<div className="border bg-red-600 border-red-600 rounded-full w-1.5 h-1.5 mr-2">
</div>
<div>Disconnected</div>
</div>
);
}
// If an address is provided, display the address and balance
return (
<div className="flex items-center w-full">
<div className="border bg-green-500 border-green-500 rounded-full w-1.5 h-1.5 mr-2"></div>
<div className="text-xs md:text-xs">{address} <br /> Balance: {balance}</div>
</div>
);
}
page.tsx
import WalletButton from "./walletButton";
export default function Home() {
return (
<main className="min-h-screen">
{/* Flex container to center the content vertically and horizontally */}
<div className="flex flex-col items-center justify-center h-screen">
{/* Heading */}
<p className="text-white font-bold text-3xl"> Viem.sh </p>
{/* Card container */}
<div className="h-[300px] min-w-[150px] flex flex-col justify-between backdrop-blur-2xl bg-[#290333]/30 rounded-lg mx-auto p-7 text-white border border-purple-950">
{/* Render the WalletButton component */}
<WalletButton />
</div>
</div>
</main>
);
}
Done!
That’s it give it a save and if you are feeling fancy change global.css to something nicer like
global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
background-color: #0c002e;
background-image: radial-gradient(
at 100% 100%,rgb(84, 2, 103) 0px,
transparent 50%),
radial-gradient(at 0% 0%, rgb(97, 0, 118) 0px, transparent 50%);}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
And then you are done and have a button to connect MetaMask from next.js using viem.