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

  1. Create client.ts
  2. Create walletButton.tsx
  3. 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.