Viem React Js Example: Transfer, Mint, and View Blockchain State

In this tutorial, we’ll build a fully functional Dapp with the Viem typescript library + React (Next.js). We’ll cover the necessary steps to connect your wallet, transfer crypto, interact with smart contracts (eg.g mint NFTs) and query the blockchain.

Viem is a Typescript alternative to existing low-level Ethereum interfaces like web3.js and ethers.js. It supports browser native BigInt and automatically infer types from ABIs and EIP-712. It has a 35kb bundle size, tree-shakable design to minimize the final bundle, 99.8% test coverage. Checkout their benchmarks and full documentation.

We’ve structured this tutorial with concise explanations delivered via in-line code comments. Simply copy-paste the codes and read the explanations.

Here’s an outline of the tutorial:

  • Viem Clarifying Terminologies: Client | Transport | Chain
  • Getting Started: Set up Client & Transport with React + Viem
  • Part 1: Connect to Web3 Wallet with React + Viem
  • Part 2: Transferring Crypto with React + Viem
  • Part 3: Minting an NFT with React + Viem

A showcase of what you’ll build:

viem typescript demo dapp

Full code in Git Repository

Viem Clarifying Terminologies: Client | Transport | Chain

Viem has three fundamental concepts: Client, Transport and Chain

  • Client in viem, is similar to Ether.js Provider. It provides the typescript functions for doing common actions on Ethereum. Depending on the action, it will fall under one of three types of clients.
    • Public Client is an interface to “public” JSON RPC API methods, e.g., retrieving block numbers, querying account balances, accessing “view” functions on smart contracts, and other read-only, non-state-changing operations. These functions are refered to as Public Actions.
    • Wallet Client is an interface to interact with Ethereum Accounts, e.g., sending transactions, signing messages, requesting addresses, switching chains, and operations requiring the user’s permission. For example, minting an NFT is state-changing, so this would be done under the wallet client. These functions are refered to as Wallet Actions.
    • Test Client is used to create simulated transactions for testing. This would typically be used in unit tests.
  • Transport is instantiated with the Client, it represents the intermediary layer for executing requests. There are three types of Transport:
    • HTTP Transport, utilizing HTTP JSON-RPC;
    • WebSocket Transport for real-time connections via WebSocket JSON-RPC;
    • Custom Transport, which handle requests via EIP-1193 request method;
    • Fallback allows you to specify multiple transports in a list. If one fails, it moves down the list to find a functional one. An example will be provided later.
  • Chain refers to the EVM-compatible chain for establishing a connection, they are identified through the chain object (identified by chain id). Only one chain can be instantiated along with a client. We can use the provided viem chain library, e.g., polygon, eth mainnet or build your own manually.

Public Client

This is how to declare a Public Client.

import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'

const publicClient = createPublicClient({ 
  chain: mainnet,
  transport: http()
})

This is how you utilize Public actions.

const balance = await publicClient.getBalance({ 
  address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
})

const block = await publicClient.getBlockNumber()

These are the available Public Actions:

Viem has a feature called Optimization Public Client, supports eth_call Aggregation for improved performance by sending batch requests. This is very well documented.

Wallet Client

This is how to establish a Wallet Client:

import { createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'

const walletClient = createWalletClient({
    chain: mainnet,transport: custom(window.ethereum)
})

This is how to utilize Wallet Actions:

// Get's the user address
const [address] = await walletClient.getAddresses()

// Sends a transaction
const hash = await walletClient.sendTransaction({
    account: address,
    to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
    value: parseEther('0.001')
})

Available wallet functions:

  • requestAddresses ( Wallets like Metamask may need a user’s to requestAddresses first )
  • switchChain
  • signMessage
  • getPermissions
  • sendTransaction
  • More on Wallet Action Documentation

We’ll demonstrate how to use sendTransaction and getAddresses in this tutorial.

Optional

You are able to extend Wallet Client with Public Actions. This helps you avoid handling several clients. The following code snippet extends the wallet client with public actions.

import { createWalletClient, http, publicActions } from 'viem'
import { mainnet } from 'viem/chains'

const extendedClient = createWalletClient({
  chain: mainnet,
  transport: http()
}).extend(publicActions)

// Public Action
const block = await extendedClient.getBlockNumber() 
// Wallet Action
const [address] = await extendedClient.getAddresses(); 

Test Client

Test Client provides an interface to shadow accounts, mining blocks and impersonate transactions through a local test node such as Anvil or Hardhat. We won’t be discussing this in detail but you can read more on Test Client Documentation.

Transport

We can only pass one means of transport (the protocol we will use to connect to the blockchain) at a time, here’s an example of how each of the transport can be used.

HTTP

import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const transport = http('https://eth-mainnet.g.alchemy.com/v2/...')

const client = createPublicClient({
  chain: mainnet,
  transport,
})

The transport will fallback to a public RPC URL if no url is provided. It is recommended to pass an authenticated RPC URL to minimize issues with rate-limiting.

WebSocket

import { createPublicClient, webSocket } from 'viem'
import { mainnet } from 'viem/chains'

const transport = webSocket('wss://eth-mainnet.g.alchemy.com/v2/...')
const client = createPublicClient({
  chain: mainnet, 
  transport,
})

Transport will fallback to public RPC URL for the same reasons above.

Note how in both examples above, the transport is defined by the kind of URL specified for transport. The first url is an HTTPS url, the second is a WSS url.

Custom (EIP-1193) (We will be using this)

This transport is used to integrate with injected wallets that provide an EIP-1193 provider such as WalletConnect, Coinbase SDK and Metamask.

import { createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'

const client = createWalletClient({
  chain: mainnet,
  transport: custom(window.ethereum)
})

Fallback

This transport takes in multiple Tranpsorts. If a transport fails, it will resort to the next transport method given. In the following example, if Alchemy fails, it will fallback to Infura.

import { createPublicClient, fallback, http } from 'viem'
import { mainnet } from 'viem/chains'

const alchemy = http('https://eth-mainnet.g.alchemy.com/v2/...')

const infura = http('https://mainnet.infura.io/v3/...')

const client = createPublicClient({
  chain: mainnet,
  transport: fallback([alchemy, infura]),
})

Chains

Viem provides popular EVM-Compatible chains through the viem/chains library such as: Polygon, Optimism, Avalanche, and more on Viem Chains Documentation.

You can switch chains by passing them as the argument, eg. polygonMumbai.

import { createPublicClient, http } from 'viem'
import { polygonMumbai } from 'viem/chains'

const client = createPublicClient({
    chain: polygonMumbai,
    transport: http(),
})

You can also build your own chain object that inherits the Chain type (Credits: Viem.sh)

import { Chain } from 'viem'

export const avalanche = {
    id: 43_114,
    name: 'Avalanche',
    network: 'avalanche',
    nativeCurrency: {
        decimals: 18,
        name: 'Avalanche',
        symbol: 'AVAX',
    },
    rpcUrls: {
        public: { 
            https: ['https://api.avax.network/ext/bc/C/rpc']        
        },
        default: { 
            https: ['https://api.avax.network/ext/bc/C/rpc'] 
        },
    },
    blockExplorers: {
        etherscan: { 
            name: 'SnowTrace', 
            url: 'https://snowtrace.io' 
        },
            default: { 
                name: 'SnowTrace', 
                url: 'https://snowtrace.io' 
            },
        },
        contracts: {multicall3: {
            address: '0xca11bde05977b3631167028862be2a173976ca11',
            blockCreated: 11_907_934,
        },
    },
} as const satisfies Chain

Remember, only one chain can be assigned to a client at a time.

Getting Started: Set up Client & Transport with React + Viem

For simplicity we recommend using polygonMumbai or Sepolia as your test network.

Step 1: Create a Next.js Project & Install Viem

First create your Next.js project with

npx create-next-app@latest myapp

Check [yes] for the following:

  • Typescript
  • ESLint
  • Tailwind
  • App Router (preferably)

Open your project in vscode.

Install viem with one the following command:

npm i viem 
pnpm i viem
yarn add viem

Step 2: Set up Client and Transport

In the app directory, create two new files:

  • client.ts
  • walletButton.tsx

Your app directory should look like this:

app ├── client.ts ├── globals.css ├── layout.tsx ├── page.tsx └── walletButton.tsx

Client.ts

We’ll initialize the Client & Transport in a separate typescript file. Go ahead and copy paste the following codes into client.ts.

// client.ts
import { createWalletClient, createPublicClient, custom, http } from "viem";
import { polygonMumbai, mainnet } from "viem/chains";
import "viem/window";

// Instantiate Public Client
const publicClient = createPublicClient({
  chain: mainnet,
  transport: http(),
});
  
// Instantiate Wallet Clientconst walletClient = createWalletClient({
    chain: polygonMumbai,
    transport: custom(window.ethereum),
});

This will inevitably throw a type error, window.ethereum might be undefined since some browsers like safari does not support the window.ethereum object.

We can handle the error by checking if window.ethereum is present or undefined.

// client.ts

import { createWalletClient, createPublicClient, custom, http } from "viem";
import { polygonMumbai } from "viem/chains";
import "viem/window";

export function ConnectWalletClient() {
    // Check for window.ethereum
    let transport;
    if (window.ethereum) {
        transport = custom(window.ethereum);
    } else {
        const errorMessage ="MetaMask or another web3 wallet is not installed. Please install one to proceed.";
        throw new Error(errorMessage);
    }
    
    // Delcalre a Wallet Client
    const walletClient = createWalletClient({
        chain: polygonMumbai,
        transport: transport,
    });
    
    return walletClient;
}

export function ConnectPublicClient() {
    // Check for window.ethereum
    let transport;
    if (window.ethereum) {
        transport = custom(window.ethereum);
    } else {
        const errorMessage ="MetaMask or another web3 wallet is not installed. Please install one to proceed.";
        throw new Error(errorMessage);
    }
    
    // Delcare a Public Client
    const publicClient = createPublicClient({
        chain: polygonMumbai,
        transport: transport,
    });
    
    return publicClient;
}

This will still allow you to open the website in a browser that does not support window.ethereum.

It is recommended to keep the chains for the walletClient and publicClient consistent or else you might run into incompatible chain errors.

Part 1: Connect to Web3 Wallet with React + Viem

This section demonstrates how the viem Client connects to your web3 wallet.

Step 2: Create a Button that Connects to Web3 Wallet

walletButton.tsx

We’ll now create a client component, that handles the connection logic to your web3 wallet.

The button instantiates a walletClient and requests user’s wallet address, if the wallet isn’t already connected it will prompt it to, and finally output it’s address.

There is a lot of code here, but focus on the handleClick() function.

// walletButton.tsx
"use client";
import { useState } from "react";
import { ConnectWalletClient, ConnectPublicClient } from "./client";

export default function WalletButton() {
    //State variables for address & balance
    const [address, setAddress] = useState<string | null>(null);
    const [balance, setBalance] = useState<BigInt>(BigInt(0));
    // Function requests connection and retrieves the address of wallet
    // Then it retrievies the balance of the address 
    // Finally it updates the value for address & balance variable
    async function handleClick() {
        try {
            // Instantiate a Wallet & Public Client
            const walletClient = ConnectWalletClient();
            const publicClient = ConnectPublicClient();
        
            // Performs Wallet Action to retrieve wallet address
            const [address] = await walletClient.getAddresses();
            
            // Performs Public Action to retrieve address balance
            const balance = await publicClient.getBalance({ address });
            // Update values for address & balance state variable
            setAddress(address);
            setBalance(balance);
        } catch (error) {
            // Error handling
            alert(`Transaction failed: ${error}`);
        }
    }
// Unimportant Section Below / Nice to Have UI
    return (
        <>
            
            );}
            
// Displays the wallet address once it’s successfuly connected
// You do not have to read it, it's just frontend stuff

function Status({
  address,
  balance,}: {
  address: string | null;
  balance: BigInt;
}) {
    if (!address) {
        return (
            
Disconnected
); } return (
{address}
Balance: {balance.toString()}
); } ``` ### Step 3: Insert walletButton component ### ### page.tsx What’s left is to design the main page and import the WalletButton component. We’ve commented out some code you will add later. ```tsx! import WalletButton from "./walletButton"; // import MintButton from "./mintButton"; // import SendButton from "./sendButton"; export default function Home() { return (
); } ``` ### globals.css Some nice background UI, replace **globals.css** with the codes below. ```css! @tailwind base;@tailwind components;@tailwind utilities; 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%);} ``` ### Step 4: Run the website and test it When you click the button, it should initiate a connection to your wallet. Once you’ve approved, it should look like this: ```bash npm run dev ``` ![viem connect wallet](https://rareskills.io/wp-content/uploads/2024/09/935a00_87d0b6e917ab4b6d9aa0a5758d39fdd9~mv2.png) After the button is clicked, the following will show: ![viem showing connected address](https://rareskills.io/wp-content/uploads/2024/09/935a00_4a990af10c7342349ab08f07a72c0e2d~mv2.png) ## Part 2: Transferring Crypto with React + Viem Now that our wallet is connected, we can start transfering cryptocurrencies. We will utilize the **sendTransaction** wallet action. - Get some Matic from [Matic Faucet](https://faucet.polygon.technology/) ### Step 5: Add feature to transfer cryptocurrency Create a new tsx file **sendButton.tsx** in the app directory app ├── client.ts ├── globals.css ├── layout.tsx ├── page.tsx ├── **sendButton.tsx** └── walletButton.tsx ### sendButton.tsx We’ll create a button that initiates the **sendTransaction** action. Viem make’s it very simple for us to do so. The logic flow should be similar to **walletButton.tsx**, instantiate the walletClient and perform Wallet Client actions. ```tsx! "use client"; import { parseEther } from "viem"; import { ConnectWalletClient} from "./client"; export default function SendButton() { //Send Transaction Function async function handleClick() { try { // Declare wallet client const walletClient = ConnectWalletClient(); // Get the main wallet address const [address] = await walletClient.getAddresses(); // sendTransaction is a Wallet action. // It returns the transaction hash // requires 3 parameters to transfer cryptocurrency, // account, to and value const hash = await walletClient.sendTransaction({ account: address, to: "Account_Address", value: parseEther("0.001"), // send 0.001 matic }); // Display the transaction hash in an alert alert(`Transaction successful. Transaction Hash: ${hash}`); } catch (error) { // Handle Error alert(`Transaction failed: ${error}`); } } return ( ); } ``` ### Step 6: Insert sendButton Component ### page.tsx Uncomment the lines relating to the sendButton component. ```tsx! import WalletButton from "./walletButton"; import SendButton from "./sendButton"; // import MintButton from "./mintButton"; export default function Home() { return (
); } ``` Your browser should look like this now! ![viem send transaction](https://rareskills.io/wp-content/uploads/2024/09/935a00_7a03df653a83485ebeccac4b0eded604~mv2.png) ## Part 3: Minting an NFT with React + Viem This section will discuss how to interact with a Smart Contract and give an example through minting an NFT. To interact with a smart contract, we need two things: - Contract Address - Contract ABI For this example, we’ll demonstrate it with Rareskill’s Contract that has a Mint function that doesn’t rlly do anything but track the number of times you Mint. - Rareskill’s Contract Address: **0x7E6Ddd9dC419ee2F10eeAa8cBB72C215B9Eb5E23** - [Rareskill’s Contract ABI](https://mumbai.polygonscan.com/address/0x7e6ddd9dc419ee2f10eeaa8cbb72c215b9eb5e23#code) Feel free to use your own contract. ### Step 7: Add functionality to interact with Smart Contract Create two new files **abi.ts** and **mintButton.tsx** app ├── **abi.ts** ├── client.ts ├── globals.css ├── layout.tsx ├── **mintButton.tsx** ├── page.tsx ├── sendButton.tsx └── walletButton.tsx ### abi.ts Copy Paste the Rareskill’s Contract ABI or your own. ```ts // abi.ts export const wagmiAbi = [...contract abi...] as const; ``` Remember to follow this exact format and do not forget the “**as const;”** at the end. ### Contract Instance & Contract Action Method **Contract Instance method** ```ts //Contract Instance const contract = getContract({ address: "0x7E6Ddd9dC419ee2F10eeAa8cBB72C215B9Eb5E23", abi: wagmiAbi, publicClient, walletClient, }); ``` The **getContract** function creates our contract instance **contract**. Upon creation we are able to invoke contract methods, listen to events, etc. This is a simpler method as we do not have to repeatedly pass the **address** and **abi** properties to perform a contract action. **Parameters:** - address - abi - publicClient (optional) - walletClient (optional) We are required to pass the address and abi parameters. Passing publicClient and walletClient is optional, but it allows us access to a set of contract methods depending on the type of client. Available contract methods for **publicClient**: - [**createEventFilter**](https://viem.sh/docs/contract/createContractEventFilter.html) - [**estimateGas**](https://viem.sh/docs/contract/estimateContractGas.html) - [**read**](https://viem.sh/docs/contract/readContract.html) - [**simulate**](https://viem.sh/docs/contract/simulateContract.html) - [**watchEvent**](https://viem.sh/docs/contract/watchContractEvent.html) Available contract methods for **walletClient**: - [**estimateGas**](https://viem.sh/docs/contract/estimateContractGas.html) - [**write**](https://viem.sh/docs/contract/writeContract.html) In general, calling a contract Instance method follows format below: ```ts // function contract.(estimateGas|read|simulate|write).(functionName)(args, options) // event contract.(createEventFilter|watchEvent).(eventName)(args, options) ``` Calling Contract Methods using Contract Instance ```ts // Read Contract symbol const symbol = await contract.read.symbol(); // Read Contract name const name = await contract.read.name(); // Call mint method const result = await contract.write.mint({account: address}); ``` The examples above invokes the read and write contract method through the **contract** instance.If you’re using Type-script it would auto-complete suggestions of the available contract methods. The **read.symbol()** and **read.name()** is straight forward. On the other hand, the write function ```ts const result = await contract.write.mint({account: address}); ``` takes {account: address} as it’s required parameter and all other’s optional. If you find yourself troubled with what parameters to add, hover your mouse on the “mint()” keyword, VS Code should hint you. **Contract Action Method — the Tedious way** The code in the above section is syntactic sugar for the following. We include this section to show you what is happening under the hood. This code gets the totalSupply of the contract: ```ts const totalSupply = await publicClient.readContract({ address: '0x7E6Ddd9dC419ee2F10eeAa8cBB72C215B9Eb5E23', abi: wagmiAbi, functionName: 'totalSupply', }) ``` Tedious right? You have to recurringly pass the address and abi. This is the equivalent of calling the **mint** function from the example above using the Contract Action method. Upon success it returns the transaction hash. ```ts const hash = await walletClient.writeContract({ address: "0x7E6Ddd9dC419ee2F10eeAa8cBB72C215B9Eb5E23", abi: wagmiAbi, functionName: "mint", account, }); ``` To keep bundle size minimal, use Contract Action; while Contract instance provides more functions, it increases memory use. ### mintButton.tsx To demonstrate a state-chaning transaction, we’ll create a button that invokes the Smart Contract’s **mint** function as well as querying it’s **name, symbol** and **totalSupply**. We utilized both the Contract Instance and Contract Action method to showcase how it’s implemented. ```tsx! "use client"; import { formatEther, getContract } from "viem"; import { wagmiAbi } from "./abi"; import { ConnectWalletClient, ConnectPublicClient } from "./client"; export default function MintButton() { // Function to Interact With Smart Contract async function handleClick() { // Declare Client const walletClient = ConnectWalletClient(); const publicClient = ConnectPublicClient(); // Create a Contract Instance // Pass publicClient to perform Public Client Contract Methods // Pass walletClient to perform Wallet Client Contract Methods const contract = getContract({ address: "0x7E6Ddd9dC419ee2F10eeAa8cBB72C215B9Eb5E23", abi: wagmiAbi, publicClient, walletClient, }); // Reads the view state function symbol via Contract Instance method const symbol = await contract.read.symbol(); // Reads the view state function name via Contract Instance method const name = await contract.read.name(); // Reads the view state function symbol via Contract Action method const totalSupply = await publicClient.readContract({ address: '0x7E6Ddd9dC419ee2F10eeAa8cBB72C215B9Eb5E23', abi: wagmiAbi, functionName: 'totalSupply', }) // Format ether converts BigInt(Wei) to String(Ether) const totalSupplyInEther = formatEther(totalSupply); alert(`Symbol: ${symbol}\nName: ${name}\ntotalSupply: ${totalSupplyInEther}`); try { // Declare Wallet Client and Retrieve wallet address const client = walletClient; const [address] = await client.getAddresses(); // Writes the state-changin function mint via Contract Instance method. const result = await contract.write.mint({ account: address }); alert(`${result} ${name}`); } catch (error) { // Handle any errors that occur during the transaction alert(`Transaction failed: ${error}`); }} return ( <> <button className="py-2.5 px-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}> <svg className="w-4 h-4 mr-2 -ml-1 text-[#626890]" aria-hidden="true" focusable="false" data-prefix="fab" data-icon="ethereum" role="img" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 320 512"> <path fill="currentColor" d="M311.9 260.8L160 353.6 8 260.8 160 0l151.9 260.8zM160 383.4L8 290.6 160 512l152-221.4-152 92.8z"> </path> </svg> <h1 className="text-center">Mint</h1> </button> </> ); }

Congratulations on finishing this tutorial! Your final product should look like this:

viem mint NFT

Authorship

This article was co-authored by Aymeric Taylor (LinkedIn, X), a research intern at RareSkills.

Originally Published August 10, 2023

ZK-addition-dapp with Noir and Nextjs

ZK-addition-dapp with Noir and Nextjs We will demonstrate a step-by-step exploration of a basic zk-dapp designed for verifying additions. This application enables users to prove that the sum of two numbers, X and Y, equals Z without disclosing the actual numbers on the blockchain. Although solving this problem does not necessarily demand zero-knowledge proofs, we […]

Web3.js Example. Latest version 4.x. Transfer, Mint and Query the Blockchain

Web3.js Example. Latest version 4.x. Transfer, Mint and Query the Blockchain The newest version of web3.js, 4.x, has just been unveiled. In this guide, we’ll delve into integrating web3.js into HTML to Transfer, Mint NFTs as well as querying the Blockchain. Several features within the web3.js v 1.10.0 documentation are set to be deprecated, therefore […]

Wagmi + ReactJS Example: Transfer Crypto and Mint an NFT

Wagmi + ReactJS Example: Transfer Crypto and Mint an NFT In this tutorial, we’ll learn how to build a Web3 Dapp (Decentralized Application) that connects to your crypto wallet, allowing you to transfer funds and mint NFTs. We’ll be using Next.js, a popular React framework, and Wagmi.sh, a collection of React Hooks that easily integrates […]